#!/usr/bin/env python3 from __future__ import annotations import argparse import os import re import subprocess import sys from pathlib import Path SWIFT_CODE = r''' import Foundation import Vision import AppKit let path = CommandLine.arguments[1] let url = URL(fileURLWithPath: path) guard let img = NSImage(contentsOf: url), let tiff = img.tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiff), let cg = bitmap.cgImage else { fputs("image load failed\n", stderr) exit(3) } struct Line { let text: String let x: CGFloat let y: CGFloat } var lines: [Line] = [] var requestError: Error? let request = VNRecognizeTextRequest { req, err in if let err = err { requestError = err return } let observations = (req.results as? [VNRecognizedTextObservation]) ?? [] lines = observations.compactMap { observation in guard let candidate = observation.topCandidates(1).first else { return nil } let text = candidate.string.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { return nil } return Line(text: text, x: observation.boundingBox.minX, y: observation.boundingBox.maxY) } } request.recognitionLevel = .accurate request.usesLanguageCorrection = true request.recognitionLanguages = ["zh-Hans", "en-US"] let handler = VNImageRequestHandler(cgImage: cg, options: [:]) do { try handler.perform([request]) } catch { fputs("\(error.localizedDescription)\n", stderr) exit(4) } if let requestError = requestError { fputs("\(requestError.localizedDescription)\n", stderr) exit(5) } let sorted = lines.sorted { if abs($0.y - $1.y) > 0.012 { return $0.y > $1.y } return $0.x < $1.x } for line in sorted { print(line.text) } ''' def page_number(path: Path) -> int: match = re.search(r"(\d+)", path.stem) return int(match.group(1)) if match else 0 def run_ocr(image: Path, module_cache: Path) -> tuple[str, str | None]: module_cache.mkdir(parents=True, exist_ok=True) env = os.environ.copy() env["CLANG_MODULE_CACHE_PATH"] = str(module_cache) proc = subprocess.run( ["swift", "-e", SWIFT_CODE, str(image)], text=True, capture_output=True, check=False, env=env, ) if proc.returncode != 0: return "", proc.stderr.strip() or f"swift exited {proc.returncode}" return proc.stdout.strip(), None def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("output", type=Path) parser.add_argument("images", nargs="+", type=Path) args = parser.parse_args() images = sorted(args.images, key=lambda p: (page_number(p), str(p))) args.output.parent.mkdir(parents=True, exist_ok=True) module_cache = args.output.parent.parent / "tmp" / "swift_module_cache" with args.output.open("w", encoding="utf-8") as out: out.write("# GB/T 33923-2017 OCR 文本版\n\n") out.write("> 说明:此文本由 PDF 页面图像 OCR 生成,用于阅读、搜索和复制。公式、表格、上下标和个别专业符号可能需要对照原 PDF 校核。\n\n") for index, image in enumerate(images, 1): page = page_number(image) print(f"OCR page {index}/{len(images)}: {image.name}", file=sys.stderr, flush=True) text, error = run_ocr(image, module_cache) out.write(f"\n\n## 第 {page} 页\n\n") if error: out.write(f"[OCR 失败:{error}]\n") elif text: out.write(text) out.write("\n") else: out.write("[OCR 未识别出文本]\n") out.flush() return 0 if __name__ == "__main__": raise SystemExit(main())