ScanPipeline.swift 55 lignes · 2068 octets
import UIKit

/// Image provenance. VisionKit already returns perspective-corrected pages;
/// picker images still need the `VisionScanEngine` pass.
enum ScanSource {
    case documentCamera
    case photoLibrary
}

/// Orchestrates the Android `ScanCarteViewModel.processCapture` chain:
/// (scan for picker images) -> orientation normalize -> OCR -> heuristic structuring.
struct ScanPipeline {

    /// iOS counterpart of Android `ContactDraftUi`.
    struct Output {
        let ocrText: String
        let draft: ContactCard
        /// Corrected image for the draft preview (Android `ContactDraftUi.preview`).
        let preview: UIImage
    }

    let scanEngine: ScanEngine
    let ocrEngine: OcrEngine

    init(scanEngine: ScanEngine = VisionScanEngine(), ocrEngine: OcrEngine = VisionOcrEngine()) {
        self.scanEngine = scanEngine
        self.ocrEngine = ocrEngine
    }

    func process(_ image: UIImage, source: ScanSource) async throws -> Output {
        // Orientation first: UIImages carry orientation metadata that must be baked
        // into pixels before rectangle detection / warping (Android normalizes at
        // capture-decode time, before ScanCarteViewModel.processCapture runs).
        let oriented = ImageOrientation.orient(image).bitmap

        let corrected: UIImage
        switch source {
        case .documentCamera:
            // VisionKit already cropped + perspective-corrected the page.
            corrected = oriented
        case .photoLibrary:
            corrected = try await scanEngine.scan(oriented).bitmap
        }

        // VisionOcrEngine runs OcrPostProcessor on the raw text internally,
        // matching the TesseractOcrEngine contract.
        let ocr = try await ocrEngine.recognize(corrected)

        // Android calls ContactDraftMerge.merge(null, ocr); with a nil LLM card
        // that reduces to the deterministic heuristics over the OCR result.
        let draft = ContactHeuristicParser.parse(ocr)

        return Output(ocrText: ocr.rawText, draft: draft, preview: corrected)
    }
}