Split project into frontend and backend

This commit is contained in:
2026-08-13 16:24:36 +08:00
commit f051146486
274 changed files with 953932 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
import AppKit
import Foundation
import Vision
struct OCRLine {
let text: String
let x: CGFloat
let y: CGFloat
}
func pageNumber(from path: String) -> Int {
let name = URL(fileURLWithPath: path).deletingPathExtension().lastPathComponent
let digits = name.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
return Int(digits) ?? 0
}
func loadCGImage(_ path: String) -> CGImage? {
let url = URL(fileURLWithPath: path)
guard let image = NSImage(contentsOf: url),
let tiff = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiff) else {
return nil
}
return bitmap.cgImage
}
func recognize(_ path: String) throws -> [OCRLine] {
guard let cgImage = loadCGImage(path) else {
throw NSError(domain: "VisionOCR", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not load image: \(path)"])
}
var lines: [OCRLine] = []
var requestError: Error?
let request = VNRecognizeTextRequest { request, error in
if let error = error {
requestError = error
return
}
let observations = (request.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 OCRLine(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: cgImage, options: [:])
try handler.perform([request])
if let requestError = requestError {
throw requestError
}
return lines.sorted {
if abs($0.y - $1.y) > 0.012 {
return $0.y > $1.y
}
return $0.x < $1.x
}
}
let args = Array(CommandLine.arguments.dropFirst())
guard args.count >= 2 else {
FileHandle.standardError.write(Data("Usage: vision_ocr.swift OUTPUT.md IMAGE...\n".utf8))
exit(2)
}
let output = args[0]
let imagePaths = args.dropFirst().sorted {
let lhs = pageNumber(from: $0)
let rhs = pageNumber(from: $1)
return lhs == rhs ? $0 < $1 : lhs < rhs
}
var result = "# GB/T 33923-2017 OCR 文本版\n\n"
result += "> 说明:此文本由 PDF 页面图像 OCR 生成,用于阅读、搜索和复制。公式、表格、上下标和个别专业符号可能需要对照原 PDF 校核。\n\n"
for (index, path) in imagePaths.enumerated() {
let page = pageNumber(from: path)
FileHandle.standardError.write(Data("OCR page \(index + 1)/\(imagePaths.count): \(URL(fileURLWithPath: path).lastPathComponent)\n".utf8))
do {
let lines = try recognize(path)
result += "\n\n## 第 \(page) 页\n\n"
result += lines.map(\.text).joined(separator: "\n")
result += "\n"
} catch {
result += "\n\n## 第 \(page) 页\n\n[OCR 失败:\(error.localizedDescription)]\n"
}
}
try result.write(toFile: output, atomically: true, encoding: .utf8)
+127
View File
@@ -0,0 +1,127 @@
#!/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())