// // KMOCROperation.m // PDF Reader Pro Edition // // Created by 万军 on 2020/3/23. // #import "KMOCROperation.h" #import "KMGOCRManager.h" #import #define KMOCR_API_URL @"https://api.convertio.co/convert" #if VERSION_FREE #define KMOCR_API_KEY @"d1a0c1a4cc7fcac5eb08730570217f97" #else #define KMOCR_API_KEY @"d1a0c1a4cc7fcac5eb08730570217f97" #endif #define KMTIMER_MAXIMUM_CYCLE 10 @interface KMOCROperation () @property (assign, nonatomic, getter = isExecuting) BOOL executing; @property (assign, nonatomic, getter = isFinished) BOOL finished; @property (assign, nonatomic, getter = isCancelled) BOOL cancelled; @property (nonatomic, assign) NSInteger timerCycleCount; @property (nonatomic, assign) NSInteger imageIndex; @property (nonatomic, strong) NSURLSessionDataTask *task; @property (nonatomic, strong) NSString *fileID; @property (nonatomic, strong) NSString *fileName; @property (nonatomic, strong) NSImage *orcImage; @property (nonatomic, strong) NSURL *fileURL; @end @implementation KMOCROperation - (void)dealloc { } - (instancetype)initWithRecognitionImage:(NSImage *)image withImageIndex:(NSInteger)imageIndex { self = [super init]; if (self) { self.timerCycleCount = 0; self.orcImage = image; self.imageIndex = imageIndex; self.fileName = [self fileNameWithDate]; self.fileID = nil; self.fileURL = nil; self.queuePriority = NSOperationQueuePriorityNormal; self.name = self.fileName; self.executing = NO; self.finished = NO; } return self; } #pragma mark - overwrite - (void)start { if ([self p_checkCancelled]) { return; } self.executing = YES; [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil]; } - (void)main { @try { if ([self p_checkCancelled]) { return; } [self recognitionImage:_orcImage]; while (self.executing) { if ([self p_checkCancelled]) { return; } } } @catch (NSException * e) { NSLog(@"Exception %@", e); } } - (void)cancel { [super cancel]; if (self.task) { [self.task cancel]; self.task = nil; } if ([self.operationDelegate respondsToSelector:@selector(OCROperation:cancelOCRImageAtIndex:)]) { [self.operationDelegate OCROperation:self cancelOCRImageAtIndex:_imageIndex]; } if (self.executing) { self.executing = NO; self.finished = YES; } else { self.finished = NO; } self.cancelled = YES; } #pragma mark - private methods - (void)p_done { self.executing = NO; self.finished = YES; } - (BOOL)p_checkCancelled { if (self.cancelled) { self.finished = YES; return YES; } return NO; } - (void)recognitionImage:(NSImage *)image { __weak typeof(self)weakSelf = self; NSString *binaryImageData = [self base64EncodeImage:image]; if (!binaryImageData) { return; } NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:KMOCR_API_URL]]; [request setHTTPMethod: @"POST"]; [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; NSDictionary *paramsDictionary = @{@"apikey":KMOCR_API_KEY, @"input":@"base64", @"file":binaryImageData, @"outputformat":self.fileType, @"filename":@"1.pdf", @"options":@{@"ocr_enabled":@(true), @"ocr_settings":@{@"langs":self.selectedLanguages}}}; NSError *error; NSData *requestData = [NSJSONSerialization dataWithJSONObject:paramsDictionary options:0 error:&error]; [request setHTTPBody:requestData]; NSURLSession *URLSession = [NSURLSession sharedSession]; NSURLSessionDataTask *convertTask = [URLSession dataTaskWithRequest:request completionHandler:^ (NSData *data, NSURLResponse *response, NSError *error) { if (!data) { return; } if (NSURLErrorCancelled == error.code) { return ; } NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil]; NSString *statusStr = dictionary[@"status"]; NSLog(@"dictionary == %@",dictionary); if ([statusStr isEqualToString:@"ok"]) { weakSelf.fileID = dictionary[@"data"][@"id"]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(15.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf requestQuery]; }); } else { [weakSelf cancel]; // NSError *err = [self errorWithContent:dictionary[@"error"]]; if ([weakSelf.operationDelegate respondsToSelector:@selector(OCROperation:failureOCRImageAtIndex:error:)]) { [weakSelf.operationDelegate OCROperation:weakSelf failureOCRImageAtIndex:weakSelf.imageIndex error:error]; } } }]; [convertTask resume]; } #pragma mark - Private Methods - (NSString *)fileNameWithDate { NSDateFormatter *formatter = [[NSDateFormatter alloc ] init]; [formatter setDateFormat:@"YYYY-MM-dd-hh-mm-ss-SSS"]; NSString *dateString = [formatter stringFromDate:[NSDate date]]; NSString *fileName = [NSString stringWithFormat:@"%@ %ld",dateString, (long)_imageIndex]; return fileName; } - (NSData *)compressImageData:(NSData *)imageData toMaxFileSize:(NSInteger)maxFileSize { CGFloat compression = 0.9f; CGFloat maxCompression = 0.1f; NSData *compressImageData = imageData; while ([imageData length] > maxFileSize && compression > maxCompression) { compression -= 0.1; NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData]; compressImageData = [imageRep representationUsingType:NSJPEGFileType properties:@{NSImageCompressionFactor:@(compression)}]; } return compressImageData; } - (NSString *)base64EncodeImage:(NSImage *)image { NSData *data = [image TIFFRepresentation]; NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:data]; [imageRep setSize:[image size]]; NSData *imagedata = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; // Resize the image if it exceeds the 4MB API limit if ([imagedata length] > 4194304) { imagedata = [self compressImageData:imagedata toMaxFileSize:4194304]; } NSString *base64String = nil; if ([imagedata respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { base64String = [imagedata base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn]; } else { base64String = [imagedata base64Encoding]; } return base64String; } - (NSError *)errorWithContent:(NSString *)content { NSError *error; NSString *domain = @"com.MyCompany.MyApplication.ErrorDomain"; NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : content }; error = [NSError errorWithDomain:domain code:-101 userInfo:userInfo]; return error; } #pragma mark - NSTimer - (void)requestQuery { __weak typeof(self)weakSelf = self; NSString *urlString = [NSString stringWithFormat:@"%@/%@/status",KMOCR_API_URL,_fileID]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]]; [request setHTTPMethod: @"GET"]; NSURLSession *URLSession = [NSURLSession sharedSession]; self.task = [URLSession dataTaskWithRequest:request completionHandler:^ (NSData *data, NSURLResponse *response, NSError *error) { if (NSURLErrorCancelled == error.code) { return ; } NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil]; NSString *statusStr = dictionary[@"status"]; NSLog(@"dictionary == %@",dictionary); weakSelf.fileID = dictionary[@"data"][@"id"]; if ([statusStr isEqualToString:@"ok"]) { if (weakSelf.task) { [weakSelf.task cancel]; weakSelf.task = nil; } NSDictionary *fileData = dictionary[@"data"]; if ([fileData[@"step_percent"] intValue] == 100) { weakSelf.fileURL = [NSURL URLWithString:fileData[@"output"][@"url"]]; [weakSelf getResultFileContent]; } else { if (weakSelf.timerCycleCount >= KMTIMER_MAXIMUM_CYCLE) { [weakSelf cancel]; // NSError *err = [self errorWithContent:@"Network Timeout"]; if ([weakSelf.operationDelegate respondsToSelector:@selector(OCROperation:failureOCRImageAtIndex:error:)]) { [weakSelf.operationDelegate OCROperation:weakSelf failureOCRImageAtIndex:weakSelf.imageIndex error:error]; } } else { weakSelf.timerCycleCount++; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf requestQuery]; }); } } } else { [weakSelf cancel]; // NSError *err = [self errorWithContent:dictionary[@"error"]]; if ([weakSelf.operationDelegate respondsToSelector:@selector(OCROperation:failureOCRImageAtIndex:error:)]) { [weakSelf.operationDelegate OCROperation:weakSelf failureOCRImageAtIndex:weakSelf.imageIndex error:error]; } } }]; [self.task resume]; } #pragma mark - Get Result File Content - (void)getResultFileContent { __weak typeof(self)weakSelf = self; NSString *urlString = [NSString stringWithFormat:@"%@/%@/dl",KMOCR_API_URL,_fileID]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]]; [request setHTTPMethod: @"GET"]; NSURLSession *URLSession = [NSURLSession sharedSession]; NSURLSessionDataTask *fileContentTask = [URLSession dataTaskWithRequest:request completionHandler:^ (NSData *data, NSURLResponse *response, NSError *error) { if (NSURLErrorCancelled == error.code) { return ; } NSData *fileData; NSError *err; NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil]; NSLog(@"dictionary == %@",dictionary); NSString *statusStr = dictionary[@"status"]; if ([statusStr isEqualToString:@"ok"]) { fileData = [self responseDataResults:dictionary]; } else { // err = [self errorWithContent:dictionary[@"error"]]; } dispatch_async(dispatch_get_main_queue(), ^{ if (error || !fileData) { [self cancel]; if ([weakSelf.operationDelegate respondsToSelector:@selector(OCROperation:failureOCRImageAtIndex:error:)]) { [weakSelf.operationDelegate OCROperation:self failureOCRImageAtIndex:weakSelf.imageIndex error:error]; } } else { NSArray *arr = @[fileData]; NSLog(@"self.imageIndex == %ld",(long)self.imageIndex); if ([weakSelf.operationDelegate respondsToSelector:@selector(OCROperation:finishOCRImageAtIndex:results:)]) { [weakSelf.operationDelegate OCROperation:weakSelf finishOCRImageAtIndex:weakSelf.imageIndex results:arr]; } [self p_done]; } }); // if ([statusStr isEqualToString:@"ok"]) { // [self p_done]; // } else { // [self cancel]; // } }]; [fileContentTask resume]; } - (NSData *)responseDataResults:(NSDictionary *)dictionary { NSDictionary *responses = [dictionary isKindOfClass:[NSDictionary class]] ? [dictionary objectForKey:@"data"] : nil; NSString *stringBase64 = responses[@"content"]; NSData *data = [[NSData alloc] initWithBase64EncodedString:stringBase64 options:0]; [data writeToFile:self.filePath atomically:YES]; return data; } @end