SOURCE RECORD · S12
S12 · 轻扫PDF / Scanly PDF
On-device Vision OCR
保留原行号。个人/账户信息如有删除,已用 REDACTED 标识。原文件 SHA-256 只用于识别截取时的本机原文件;不能证明编写或提交时间,也不等于脱敏下载文件的哈希。
Original line numbers are retained. Redactions are explicitly marked. The original full-file hash identifies a captured local file, not its creation/submission time or the redacted download. Public-file hashes are listed separately. This is selected source, not a whole-repository or binary absence finding.
Project key: scan
Original relative path: ScanlyPDF/Services/OCRService.swift
Captured: 2026-09-12T18:57:15+08:00
Original full-file SHA256: 4466d6c697543b42c508f5baf27ba70a6c722b892e0826047c2519b32f9f10c3
Evidence level: local source excerpt
Build correspondence: not yet established with an Apple-received binary.
Line numbers refer to the captured original file. Gaps are explicitly marked. No source code was changed.
Original lines 1 to 31
0001 import PDFKit
0002 import UIKit
0003 import Vision
0004
0005 actor OCRService {
0006 struct PageResult: Identifiable, Sendable { let id: Int; let text: String }
0007 struct LayoutPageResult: Identifiable, Sendable { let id: Int; let observations: [TextObservation] }
0008 struct TextObservation: Identifiable, Sendable {
0009 let id = UUID()
0010 let text: String
0011 /// Top-left normalized coordinates, matching the paper editor model.
0012 let rect: CGRect
0013 }
0014
0015 func recognize(image: UIImage, languages: [String] = ["zh-Hans", "zh-Hant", "en-US", "ja-JP", "ko-KR", "es-ES", "fr-FR", "de-DE"]) async throws -> String {
0016 guard let cgImage = image.cgImage else { throw AppError.invalidFile }
0017 return try await withCheckedThrowingContinuation { continuation in
0018 let request = VNRecognizeTextRequest { request, error in
0019 if let error { continuation.resume(throwing: error); return }
0020 let text = (request.results as? [VNRecognizedTextObservation])?.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n") ?? ""
0021 continuation.resume(returning: text)
0022 }
0023 request.recognitionLevel = .accurate
0024 request.usesLanguageCorrection = true
0025 let supported = (try? request.supportedRecognitionLanguages()) ?? []
0026 request.recognitionLanguages = languages.filter(supported.contains)
0027 if request.recognitionLanguages.isEmpty { request.recognitionLanguages = Array(supported.prefix(3)) }
0028 do { try VNImageRequestHandler(cgImage: cgImage).perform([request]) }
0029 catch { continuation.resume(throwing: error) }
0030 }
0031 }