// // KMOCROperation.swift // PDF Reader Pro // // Created by liujiajie on 2023/11/15. // import Foundation let KMOCR_API_URL = "https://api.convertio.co/convert" #if VERSION_DMG let KMOCR_API_KEY = "d1a0c1a4cc7fcac5eb08730570217f97" #else let KMOCR_API_KEY = "d1a0c1a4cc7fcac5eb08730570217f97" #endif let KMTIMER_MAXIMUM_CYCLE = 10 @objc(KMOCROperationDelegate) protocol KMOCROperationDelegate: AnyObject { @objc optional func OCROperationCancel(_ op: KMOCROperation, atIndex: Int) @objc optional func OCROperationStart(_ op: KMOCROperation, atIndex: Int) @objc optional func OCROperationFinish(_ op: KMOCROperation, atIndex: Int, results: Array) @objc optional func OCROperationFail(_ op: KMOCROperation, atIndex: Int, err: Error?) } @objcMembers class KMOCROperation: Operation{ var operationDelegate: KMOCROperationDelegate? var selectedLanguages: Array? var fileType: String = "" var filePath: String = "" var timerCycleCount = 0 var imageIndex = 0 var task: URLSessionDataTask? var fileID: String? var fileName: String = "" var orcImage: NSImage? var fileURL:URL? override var isExecuting: Bool{ set{ self.willChangeValue(forKey: "isExecuting") didChangeValue(forKey: "isExecuting") } get{ return super.isExecuting } } override var isFinished: Bool{ set{ self.willChangeValue(forKey: "isFinished") self.isFinished = newValue didChangeValue(forKey: "isFinished") } get{ return super.isFinished } } override var isCancelled: Bool{ set{ self.willChangeValue(forKey: "isCancelled") self.isCancelled = newValue didChangeValue(forKey: "isCancelled") } get{ return super.isCancelled } } init(recognitionImg:NSImage, imgIndex:Int) { super.init() self.timerCycleCount = 0 self.imageIndex = imgIndex self.orcImage = recognitionImg self.fileName = fileNameWithDate() self.fileID = nil self.fileURL = nil self.queuePriority = .normal self.name = self.fileName self.isExecuting = false self.isFinished = false } func fileNameWithDate() -> String { let formatter = DateFormatter() formatter.dateFormat = "YYYY-MM-dd-hh-mm-ss-SSS" let dateString = formatter.string(from: Date()) let fileName = "\(dateString) \(imageIndex)" return fileName } override func start() { if p_checkCancelled() { return } isExecuting = true Thread.detachNewThreadSelector(#selector(main), toTarget: self, with: nil) } @objc override func main() { do { if p_checkCancelled() { return } recognitionImage(image: orcImage ?? NSImage()) while isExecuting { if p_checkCancelled() { return } } } catch let e { Swift.debugPrint("Exception %@", e) } } override func cancel() { super.cancel() if task != nil { task?.cancel() task = nil } self.operationDelegate?.OCROperationCancel?(self, atIndex: self.imageIndex) if isExecuting { isExecuting = false isFinished = true } else { isFinished = false } isCancelled = true } func p_done() { isExecuting = false isFinished = true } func p_checkCancelled() -> Bool { if isCancelled { isFinished = true return true } return false } func recognitionImage(image: NSImage) { guard let binaryImageData = base64EncodeImage(image) else { return } let url = URL(string: KMOCR_API_URL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") let paramsDictionary: [String: Any] = [ "apikey": KMOCR_API_KEY, "input": "base64", "file": binaryImageData, "outputformat": fileType, "filename": "1.pdf", "options": [ "ocr_enabled": true, "ocr_settings": [ "langs": selectedLanguages ] ] ] let requestData = try? JSONSerialization.data(withJSONObject: paramsDictionary, options: []) request.httpBody = requestData let URLSession = URLSession.shared let convertTask = URLSession.dataTask(with: request) { [self] (data, response, error) in guard let data = data else { return } if (error as NSError?)?.code == NSURLErrorCancelled { return } guard let dictionary = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as? [String: Any] else { return } guard let statusStr = dictionary["status"] as? String else { return } if statusStr == "ok" { let dic = (dictionary["data"] as? NSDictionary) self.fileID = dic?["id"] as? String DispatchQueue.main.asyncAfter(deadline: .now() + 15.0) { self.requestQuery() } } else { self.cancel() operationDelegate?.OCROperationFail?(self, atIndex: self.imageIndex, err: error) } } convertTask.resume() } func errorWithContent(content: String) -> NSError { let domain = "com.MyCompany.MyApplication.ErrorDomain" let userInfo = [NSLocalizedDescriptionKey: content] let error = NSError(domain: domain, code: -101, userInfo: userInfo) return error } func requestQuery() { let urlString = "\(KMOCR_API_URL)/\(fileID ?? "")/status" guard let url = URL(string: urlString) else { return } var request = URLRequest(url: url) request.httpMethod = "GET" let URLSession = URLSession.shared let task = URLSession.dataTask(with: request) { [self] (data, response, error) in if (error as NSError?)?.code == NSURLErrorCancelled { return } guard let dictionary = try? JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as? [String: Any] else { return } guard let statusStr = dictionary["status"] as? String else { return } let dic = (dictionary["data"] as? NSDictionary) self.fileID = dic?["id"] as? String if statusStr == "ok" { if let task = self.task { task.cancel() self.task = nil } if let fileData = dictionary["data"] as? [String: Any], let stepPercent = fileData["step_percent"] as? Int { if stepPercent == 100 { let dic = (fileData["output"] as? NSDictionary) if let fileURLString = dic?["url"] as? String, let fileURL = URL(string: fileURLString) { self.fileURL = fileURL self.getResultFileContent() } } else { if self.timerCycleCount >= KMTIMER_MAXIMUM_CYCLE { self.cancel() operationDelegate?.OCROperationFail?(self, atIndex: self.imageIndex, err: error) } else { self.timerCycleCount += 1 DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { self.requestQuery() } } } } } else { self.cancel() operationDelegate?.OCROperationFail?(self, atIndex: self.imageIndex, err: error) } } task.resume() } func getResultFileContent() { let urlString = "\(KMOCR_API_URL)/\(fileID ?? "")/dl" guard let url = URL(string: urlString) else { return } var request = URLRequest(url: url) request.httpMethod = "GET" let URLSession = URLSession.shared let fileContentTask = URLSession.dataTask(with: request) { (data, response, error) in if (error as NSError?)?.code == NSURLErrorCancelled { return } var fileData: Data? guard let dictionary = try? JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as? [String: Any] else { return } guard let statusStr = dictionary["status"] as? String else { return } if statusStr == "ok" { fileData = self.responseDataResults((dictionary as NSDictionary) as! [String : Any]) } else { // err = self.errorWithContent(dictionary["error"]) } DispatchQueue.main.async { [self] in if let error = error, fileData == nil { self.cancel() if let err = error as NSError? { operationDelegate?.OCROperationFail?(self, atIndex: self.imageIndex, err: error) } } else { if let fileData = fileData { let arr = [fileData] if let delegate = operationDelegate { delegate.OCROperationFinish?(self, atIndex: self.imageIndex, results: arr) } self.p_done() } } } } fileContentTask.resume() } func base64EncodeImage(_ image: NSImage) -> String? { guard let data = image.tiffRepresentation else { return nil } let imageRep = NSBitmapImageRep(data: data)! imageRep.size = image.size let imageData = imageRep.representation(using: .png, properties: [:]) // Resize the image if it exceeds the 4MB API limit if imageData?.count ?? 0 > 4194304 { let compressedData = compressImageData(imageData!, toMaxFileSize: 4194304) if let data = compressedData { return data.base64EncodedString(options: .endLineWithCarriageReturn) } } if let data = imageData { if #available(macOS 10.9, *) { return data.base64EncodedString(options: .endLineWithCarriageReturn) } else { return data.base64EncodedString(options: []) } } return nil } func compressImageData(_ imageData: Data, toMaxFileSize maxFileSize: Int) -> Data? { var compression: CGFloat = 0.9 let maxCompression: CGFloat = 0.1 var compressImageData = imageData while compressImageData.count > maxFileSize && compression > maxCompression { compression -= 0.1 let imageRep = NSBitmapImageRep(data: compressImageData)! compressImageData = imageRep.representation(using: .jpeg, properties: [NSBitmapImageRep.PropertyKey.compressionFactor: NSNumber(value: Float(compression))])! } return compressImageData } func responseDataResults(_ dictionary: [String: Any]) -> Data? { guard let responses = dictionary["data"] as? [String: Any] else { return nil } guard let stringBase64 = responses["content"] as? String else { return nil } guard let data = Data(base64Encoded: stringBase64, options: .ignoreUnknownCharacters) else { return nil } try? data.write(to: URL(fileURLWithPath: filePath), options: .atomic) return data } }