Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

117 lines
3.9 KiB
Swift

import AppKit
import Foundation
import MacToolsPluginKit
import Vision
private final class TranslatorOCRBundleToken {}
@MainActor
protocol OCRTextRecognizing {
func recognizeText(in image: NSImage) async throws -> OCRTextRecognitionResult
}
enum OCRTextRecognitionError: Error, Equatable, LocalizedError, Sendable {
case invalidImage
case emptyResult
case requestFailed(String)
var errorDescription: String? {
switch self {
case .invalidImage:
return PluginLocalization(bundle: Bundle(for: TranslatorOCRBundleToken.self)).string(
"ocr.error.invalidImage",
defaultValue: "无法读取截图。"
)
case .emptyResult:
return PluginLocalization(bundle: Bundle(for: TranslatorOCRBundleToken.self)).string(
"ocr.error.emptyResult",
defaultValue: "截图中没有识别到文字。"
)
case let .requestFailed(message):
return message
}
}
}
struct VisionOCRTextRecognizer: OCRTextRecognizing {
func recognizeText(in image: NSImage) async throws -> OCRTextRecognitionResult {
guard let cgImage = image.cgImageForOCR else {
throw OCRTextRecognitionError.invalidImage
}
let lines = try await recognizeLines(
in: cgImage,
recognitionLanguages: nil,
automaticallyDetectsLanguage: true
)
let fallbackLines: [OCRRecognizedLine]
if lines.isEmpty {
fallbackLines = try await recognizeLines(
in: cgImage,
recognitionLanguages: ["ja-JP"],
automaticallyDetectsLanguage: false
)
} else {
fallbackLines = lines
}
let text = OCRTextMerge.merge(fallbackLines)
guard !text.isEmpty else {
throw OCRTextRecognitionError.emptyResult
}
return OCRTextRecognitionResult(text: text, lines: fallbackLines)
}
private func recognizeLines(
in cgImage: CGImage,
recognitionLanguages: [String]?,
automaticallyDetectsLanguage: Bool
) async throws -> [OCRRecognizedLine] {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate
request.usesLanguageCorrection = true
if #available(macOS 13.0, *) {
request.automaticallyDetectsLanguage = automaticallyDetectsLanguage
}
if let recognitionLanguages {
request.recognitionLanguages = recognitionLanguages
}
do {
try VNImageRequestHandler(cgImage: cgImage).perform([request])
let recognizedLines = (request.results ?? []).compactMap { observation -> OCRRecognizedLine? in
guard let candidate = observation.topCandidates(1).first else {
return nil
}
return OCRRecognizedLine(
text: candidate.string,
boundingBox: observation.boundingBox,
confidence: candidate.confidence
)
}
continuation.resume(returning: recognizedLines)
} catch {
continuation.resume(throwing: OCRTextRecognitionError.requestFailed(error.localizedDescription))
}
}
}
}
}
private extension NSImage {
var cgImageForOCR: CGImage? {
guard size.width > 0, size.height > 0 else {
return nil
}
var proposedRect = CGRect(origin: .zero, size: size)
return cgImage(forProposedRect: &proposedRect, context: nil, hints: nil)
}
}