#import #import #import static CGFloat PageWidth = 1440.0; static CGFloat OutputScale = 1.0; static CGFloat SharpenIntensity = 0.0; @interface PageRenderer : NSObject @property(nonatomic, strong) WKWebView *webView; @property(nonatomic, strong) NSURL *inputURL; @property(nonatomic, strong) NSURL *outputURL; @end @implementation PageRenderer - (instancetype)initWithInputURL:(NSURL *)inputURL outputURL:(NSURL *)outputURL { self = [super init]; if (self) { _inputURL = inputURL; _outputURL = outputURL; _webView = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, PageWidth, 900)]; _webView.navigationDelegate = self; } return self; } - (void)start { NSURL *repositoryRoot = self.inputURL.URLByDeletingLastPathComponent .URLByDeletingLastPathComponent.URLByDeletingLastPathComponent; [self.webView loadFileURL:self.inputURL allowingReadAccessToURL:repositoryRoot]; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { NSString *heightScript = @"Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)"; [webView evaluateJavaScript:heightScript completionHandler:^(id result, NSError *error) { if (error || ![result isKindOfClass:NSNumber.class]) { [self fail:[NSString stringWithFormat:@"Could not read page height: %@", error.localizedDescription ?: @"unknown error"]]; return; } CGFloat pageHeight = [(NSNumber *)result doubleValue]; [webView setFrameSize:NSMakeSize(PageWidth, pageHeight)]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ WKSnapshotConfiguration *configuration = [[WKSnapshotConfiguration alloc] init]; configuration.rect = CGRectMake(0, 0, PageWidth, pageHeight); configuration.snapshotWidth = @(PageWidth * OutputScale); [webView takeSnapshotWithConfiguration:configuration completionHandler:^(NSImage *image, NSError *snapshotError) { NSBitmapImageRep *bitmap = nil; if (image && SharpenIntensity > 0.0) { CIImage *inputImage = [[CIImage alloc] initWithData:image.TIFFRepresentation]; CIFilter *sharpen = [CIFilter filterWithName:@"CIUnsharpMask"]; [sharpen setValue:inputImage forKey:kCIInputImageKey]; [sharpen setValue:@(1.0) forKey:kCIInputRadiusKey]; [sharpen setValue:@(SharpenIntensity) forKey:kCIInputIntensityKey]; CIImage *outputImage = sharpen.outputImage; CIContext *context = [CIContext contextWithOptions:nil]; CGImageRef outputCGImage = outputImage ? [context createCGImage:outputImage fromRect:inputImage.extent] : nil; if (outputCGImage) { bitmap = [[NSBitmapImageRep alloc] initWithCGImage:outputCGImage]; CGImageRelease(outputCGImage); } } else if (image) { bitmap = [[NSBitmapImageRep alloc] initWithData:image.TIFFRepresentation]; } NSData *pngData = [bitmap representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; if (snapshotError || !pngData) { [self fail:[NSString stringWithFormat:@"Could not create snapshot: %@", snapshotError.localizedDescription ?: @"unknown error"]]; return; } NSError *writeError = nil; if (![pngData writeToURL:self.outputURL options:NSDataWritingAtomic error:&writeError]) { [self fail:[NSString stringWithFormat:@"Could not write PNG: %@", writeError.localizedDescription]]; return; } printf("Rendered %dx%d at %.2fx, sharpen %.2f -> %s\n", (int)PageWidth, (int)pageHeight, OutputScale, SharpenIntensity, self.outputURL.path.UTF8String); exit(0); }]; }); }]; } - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error { [self fail:[NSString stringWithFormat:@"Navigation failed: %@", error.localizedDescription]]; } - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { [self fail:[NSString stringWithFormat:@"Page load failed: %@", error.localizedDescription]]; } - (void)fail:(NSString *)message { fprintf(stderr, "%s\n", message.UTF8String); exit(1); } @end int main(int argc, const char *argv[]) { @autoreleasepool { if (argc < 3 || argc > 6) { fprintf(stderr, "Usage: render-overview [page-width] [output-scale] [sharpen-intensity]\n"); return 2; } if (argc >= 4) { double requestedWidth = strtod(argv[3], NULL); if (requestedWidth < 320.0 || requestedWidth > 4096.0) { fprintf(stderr, "Page width must be between 320 and 4096 pixels\n"); return 2; } PageWidth = (CGFloat)requestedWidth; } if (argc >= 5) { double requestedScale = strtod(argv[4], NULL); if (requestedScale < 1.0 || requestedScale > 4.0) { fprintf(stderr, "Output scale must be between 1 and 4\n"); return 2; } OutputScale = (CGFloat)requestedScale; } if (argc == 6) { double requestedSharpness = strtod(argv[5], NULL); if (requestedSharpness < 0.0 || requestedSharpness > 1.0) { fprintf(stderr, "Sharpen intensity must be between 0 and 1\n"); return 2; } SharpenIntensity = (CGFloat)requestedSharpness; } [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; NSString *inputPath = [NSString stringWithUTF8String:argv[1]].stringByStandardizingPath; NSString *outputPath = [NSString stringWithUTF8String:argv[2]].stringByStandardizingPath; PageRenderer *renderer = [[PageRenderer alloc] initWithInputURL:[NSURL fileURLWithPath:inputPath] outputURL:[NSURL fileURLWithPath:outputPath]]; [renderer start]; [NSRunLoop.mainRunLoop run]; } return 0; }