157 lines
5.9 KiB
Objective-C
157 lines
5.9 KiB
Objective-C
#import <Cocoa/Cocoa.h>
|
|
#import <CoreImage/CoreImage.h>
|
|
#import <WebKit/WebKit.h>
|
|
|
|
static CGFloat PageWidth = 1440.0;
|
|
static CGFloat OutputScale = 1.0;
|
|
static CGFloat SharpenIntensity = 0.0;
|
|
|
|
@interface PageRenderer : NSObject <WKNavigationDelegate>
|
|
@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 <input.html> <output.png> [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;
|
|
}
|