-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpdf2png.m
376 lines (331 loc) · 16.3 KB
/
pdf2png.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <CoreGraphics/CoreGraphics.h>
#import <ImageIO/ImageIO.h>
#define PDF2PNG_VERSION "1.1.2"
/*
http://stackoverflow.com/questions/17507170/how-to-save-png-file-from-nsimage-retina-issues
*/
@interface NSImage (SSWPNGAdditions)
- (BOOL)writePNGToURL:(NSURL*)URL outputSize:(NSSize)outputSizePx alphaChannel:(BOOL)alpha error:(NSError*__autoreleasing*)error;
@end
// mark: -
@implementation NSImage (SSWPNGAdditions)
- (BOOL)writePNGToURL:(NSURL*)URL outputSize:(NSSize)outputSizePx alphaChannel:(BOOL)alpha error:(NSError*__autoreleasing*)error {
BOOL result = YES;
NSImage* sourceImage = [NSImage imageWithSize:self.size flipped:NO drawingHandler:^BOOL(NSRect dstRect) {
[self drawAtPoint:NSMakePoint(0.0, 0.0) fromRect:dstRect operation:NSCompositingOperationSourceOver fraction:1.0];
return YES;
}];
NSRect proposedRect = NSMakeRect(0.0, 0.0, outputSizePx.width, outputSizePx.height);
unsigned bitsPerComponent = 8;
unsigned components = (alpha ? 4 : 3);
unsigned bytesPerRow = proposedRect.size.width * (components * (bitsPerComponent / BYTE_SIZE));
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
CGContextRef cgContext = CGBitmapContextCreate(
NULL, proposedRect.size.width, proposedRect.size.height,
bitsPerComponent, bytesPerRow, colorSpace,
(alpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNone));
NSGraphicsContext* context = [NSGraphicsContext graphicsContextWithCGContext:cgContext flipped:NO];
if (proposedRect.size.width != outputSizePx.width) {
NSLog(@"WARNING proposedRect.size: %@ != outputSizePx %@", NSStringFromSize(proposedRect.size), NSStringFromSize(outputSizePx));
}
// scale the image
CGImageRef scaledImage = [sourceImage CGImageForProposedRect:&proposedRect context:context hints:@{
NSImageHintCTM: NSAffineTransform.transform
}];
NSSize scaledSize = NSMakeSize(CGImageGetWidth(scaledImage), CGImageGetHeight(scaledImage));
if (scaledSize.width != outputSizePx.width) {
NSLog(@"WARNING scaledSize: %@ != outputSizePx %@", NSStringFromSize(scaledSize), NSStringFromSize(outputSizePx));
}
// setup the destination
CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)(URL), kUTTypePNG, 1, NULL);
CFDictionaryRef destinationOptions = CFBridgingRetain(@{ (id)kCGImagePropertyHasAlpha: @(alpha) });
CGImageDestinationSetProperties(destination, destinationOptions);
CGImageDestinationAddImage(destination, scaledImage, destinationOptions);
// write the image
// NSLog(@"scaled alphaInfo: %u %u %@", alpha, CGImageGetAlphaInfo(scaledImage), destinationOptions);
if(!CGImageDestinationFinalize(destination)) {
NSDictionary* details = @{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error writing PNG to: %@", URL] };
*error = [NSError errorWithDomain:@"SSWPNGAdditionsErrorDomain" code:10 userInfo:details];
result = NO;
}
exit:
CGColorSpaceRelease(colorSpace);
CGContextRelease(cgContext);
CFRelease(destination);
CFRelease(destinationOptions);
return result;
}
@end
// MARK: - main
enum {
StatusUnkonwn = -1,
StatusSuccess = 0,
StatusInvalidTargetName,
StatusMissingArguments,
StatusInputFileNotFound,
StatusInputFileNotAnImage,
StatusOutputSizeInvalid,
StatusOutputWriteError
};
int main(int argc, const char * argv[]) {
int status = StatusUnkonwn;
@autoreleasepool {
NSDictionary* args = [NSUserDefaults.standardUserDefaults volatileDomainForName:NSArgumentDomain];
// NSLog(@"args: %@", args);
NSString* inputFileName = [args objectForKey:@"i"];
NSString* outputFilePrefix = [args objectForKey:@"o"];
NSString* target = [args objectForKey:@"t"];
NSArray* outputSizes = [[args objectForKey:@"s"] componentsSeparatedByString:@","];
NSNumber* alphaArg = [args objectForKey:@"a"];
NSString* assetCatalog = [args objectForKey:@"A"];
BOOL alphaChannel = YES;
if (alphaArg) {
alphaChannel = alphaArg.boolValue;
}
else if (target && [target rangeOfString:@"ios" options:NSAnchoredSearch].location != NSNotFound) { // iOS icons can't have an alpha channel
alphaChannel = NO;
}
/*
NSNumber* bitsArg = [args objectForKey:@"b"];
unsigned bitsPerChannel = 8;
if (bitsArg) {
bitsPerChannel = [bitsArg unsignedIntValue];
// TODO check for valid number of bits per channel
}
*/
NSDictionary* const targets = @{
// Android
@"android": @[
@"512", // Google Play
@"192", // xxxhdpi
@"144", // xxhdpi
@"96", // xhdpi
@"72", // hdpi
@"48", // mdpi small
@"36" // ldpi small
],
@"android-small": @[
@"72", // hdpi
@"48", // mdpi small
@"36" // ldpi small
],
@"android-large": @[
@"512", // Google Play
@"192", // xxxhdpi
@"144", // xxhdpi
@"96" // xhdpi
],
// iOS
@"ios": @[
@"20", @"20@2x", @"20@3x", // Notificaiton
@"29", @"29@2x", @"29@3x", // Settings
@"40", @"40@2x", // iPad Spotlight
@"76", @"76@2x", // iPad App
@"83.5@2x", // iPad Pro
@"60@2x", @"60@3x", // iPhone App
@"40@3x", // iPhone Spotlight
@"1024" // iTunes Store
],
@"ios-small": @[
@"20", @"20@2x", @"20@3x", // Notificaitons
@"29", @"29@2x", @"29@3x", // Settings
@"40", @"40@2x", @"40@3x" // Spotlight
],
@"ios-large": @[
@"76", @"76@2x", // iPad App
@"83.5@2x", // iPad Pro
@"60@2x", @"60@3x", // iPhone App
@"1024" // iTunes Store
],
// macOS
@"macos": @[
@"16", @"16@2x",
@"32", @"32@2x",
@"128", @"128@2x",
@"256", @"256@2x",
@"512", @"512@2x"
],
@"macos-small": @[
@"16", @"16@2x",
@"32", @"32@2x"
],
@"macos-large": @[
@"128", @"128@2x",
@"256", @"256@2x",
@"512", @"512@2x"
],
// Messages
@"messages-icon": @[ // iMessages App Icon
@"1024"
],
@"messages-settings": @[ // iMessages App Settings Icon
@"29@2x", @"29@3x"
],
@"messages": @[
@"1024x768@2x", @"1024x768@3x", // App Store - 1.3~
@"32x24@2x", @"32x24@3x", // Messages - 1.3~
@"27x20@2x", @"27x20@3x", // Messages - 1.35
@"67x50@2x", // Messages iPad - 1.34
@"74x55@2x", // Messages iPad Pro - 1.3454545455
],
// Retina sizes
@"retina": @[
@"@", @"@2x", @"@3x"
]
};
NSArray* targetSizes = nil;
if (target && ((targetSizes = [targets objectForKey:target]) == nil)) {
status = StatusInvalidTargetName;
NSLog(@"Error %i: invalid target name: %@", status, target);
goto exit;
}
if (outputSizes && targetSizes) { // combine the output and target sizes
outputSizes = [outputSizes arrayByAddingObjectsFromArray:targetSizes];
}
else if (targetSizes && !outputSizes) {
outputSizes = targetSizes;
}
if (!outputSizes) { // assume a single 100% size
outputSizes = @[@"@"];
}
if (!inputFileName || !outputSizes) {
NSString* usage = [NSString stringWithFormat:
@"usage: pdf2png -i <input.pdf> [-o <output-file-prefix>] [-s @,@2x,50,100x100,100@2x,400%%] [-a YES|NO] \n\t[-t %@]\nVersion %@\n",
[[targets.allKeys sortedArrayUsingSelector:@selector(compare:)] componentsJoinedByString:@"|"], @PDF2PNG_VERSION];
[NSFileHandle.fileHandleWithStandardOutput writeData:[usage dataUsingEncoding:NSUTF8StringEncoding]];
status = StatusMissingArguments;
goto exit;
}
if (![NSFileManager.defaultManager fileExistsAtPath:inputFileName isDirectory:nil]) {
status = StatusInputFileNotFound;
NSLog(@"Error %i: input file not found: %@", status, inputFileName);
goto exit;
}
// create a target NSImage at each of the specified sizes
NSImage* icon = [NSImage.alloc initByReferencingFile:inputFileName];
if (!icon) {
status = StatusInputFileNotAnImage;
NSLog(@"Error %i: image did not load from: %@", status, inputFileName);
goto exit;
}
// now that we know we can read the check to see if we're writing into an asset catalog
if (assetCatalog) {
outputFilePrefix = [[assetCatalog stringByAppendingPathComponent:outputFilePrefix] stringByAppendingPathExtension:@"imageset"];
NSURL* iamgesetURL = [NSURL fileURLWithPath:outputFilePrefix]; // resolved relative to working directory
BOOL isDirectory = NO;
NSError* createError = nil;
if ([NSFileManager.defaultManager fileExistsAtPath:iamgesetURL.path isDirectory:&isDirectory]) {
if (!isDirectory) { // strange condtion, exit
status = -420;
NSLog(@"ERROR %i: iamgeset exists but is not a directory: %@", status, outputFilePrefix);
goto exit;
}
}
else { // need to create the imageset
if (![NSFileManager.defaultManager createDirectoryAtPath:iamgesetURL.path withIntermediateDirectories:YES attributes:nil error:&createError]) {
status = -421;
NSLog(@"Error %i: cannot create imageset: %@", status, iamgesetURL.path);
goto exit;
}
}
}
if (!outputFilePrefix) { // infer it from the inputFileName if neither -o or -A were specified
outputFilePrefix = inputFileName.lastPathComponent.stringByDeletingPathExtension;
}
// write the target NSImage to the output-file-prefix specified
for (NSString* sizeString in outputSizes) {
BOOL isRetina = NO;
NSSize pointSize = icon.size;
NSSize outputSize = icon.size;
NSString* retinaSize = nil;
// check for size formats: @ @2x 123@2x 123x123 123% 123w 123h 123 and scale appropriately
if ([sizeString isEqualToString:@"@"]) { // 100%
// sizes are set above, just keep going
isRetina = YES;
}
else if ([sizeString rangeOfString:@"@"].location == 0) { // multiply the existing size
isRetina = YES;
retinaSize = [sizeString substringFromIndex:1];
CGFloat retina = [retinaSize substringToIndex:(retinaSize.length - 1)].doubleValue;
outputSize = NSMakeSize(pointSize.width * retina, pointSize.height * retina);
}
else if ([sizeString rangeOfString:@"@"].location != NSNotFound) {
NSArray* sizeComponents = [sizeString componentsSeparatedByString:@"@"];
retinaSize = sizeComponents[1]; // "2x" for the file name
CGFloat pixels = [sizeComponents[0] doubleValue];
CGFloat retina = [retinaSize substringToIndex:(retinaSize.length - 1)].doubleValue;
CGFloat retinaPixels = pixels * retina;
pointSize = NSMakeSize( pixels, pixels);
outputSize = NSMakeSize( retinaPixels, retinaPixels);
}
else if ([sizeString rangeOfString:@"x"].location < sizeString.length) { // anywhere but at the end of the string
NSArray* sizeArray = [sizeString componentsSeparatedByString:@"x"];
if (sizeArray.count == 2 ) { // widthxheight
CGFloat width = [sizeArray[0] doubleValue];
CGFloat height = [sizeArray[1] doubleValue];
pointSize = NSMakeSize( width, height);
outputSize = NSMakeSize( width, height);
}
}
else if ([sizeString rangeOfString:@"%"].location == (sizeString.length - 1)) { // only at the end of the string
NSString* percentString = [sizeString substringToIndex:(sizeString.length - 2)];
CGFloat percentSize = (percentString.doubleValue / 10);
outputSize = NSMakeSize((icon.size.width * percentSize), (icon.size.height * percentSize));
pointSize = outputSize;
}
else if ([sizeString rangeOfString:@"h"].location == (sizeString.length - 1)) { // fixed height
NSString* heightString = [sizeString substringToIndex:(sizeString.length - 2)];
CGFloat fixedHeight = heightString.doubleValue;
CGFloat scaleFactor = (icon.size.height / fixedHeight);
outputSize = NSMakeSize((icon.size.width * scaleFactor), fixedHeight);
pointSize = outputSize;
}
else if ([sizeString rangeOfString:@"w"].location == (sizeString.length - 1)) { // fixed width
NSString* widthString = [sizeString substringToIndex:(sizeString.length - 2)];
CGFloat fixedWidth = widthString.doubleValue;
CGFloat scaleFactor = (icon.size.width / fixedWidth);
outputSize = NSMakeSize(fixedWidth, (icon.size.height * scaleFactor));
pointSize = outputSize;
}
else { // it's a simple square size
CGFloat size = sizeString.doubleValue;
pointSize = NSMakeSize(size, size);
outputSize = NSMakeSize(size, size);
}
if (outputSize.width < 1 || outputSize.height < 1 // proposed image is less than 1x1
|| outputSize.width > 10000 || outputSize.height > 10000) { // proposed image is larger than any current display
status = StatusOutputSizeInvalid;
NSLog(@"Error %i: Invalid output size: %@ -> %@", status, sizeString, NSStringFromSize(outputSize));
goto exit;
}
NSError* error = nil;
NSString* outputFileName = nil;
if (isRetina) {
outputFileName = outputFilePrefix;
}
else {
outputFileName = [NSString stringWithFormat:@"%@_%.0fx%.0f", outputFilePrefix, pointSize.width, pointSize.height];
}
if (retinaSize) { // append the retina tag
outputFileName = [outputFileName stringByAppendingString:@"@"];
outputFileName = [outputFileName stringByAppendingString:retinaSize];
}
outputFileName = [outputFileName stringByAppendingPathExtension:@"png"];
[icon writePNGToURL:[NSURL fileURLWithPath:outputFileName] outputSize:outputSize alphaChannel:alphaChannel error:&error];
if (error) {
status = StatusOutputWriteError;
NSLog(@"Error %i: %@ writing: %@", status, error, outputFileName);
goto exit;
}
[NSFileHandle.fileHandleWithStandardOutput writeData:[[NSString stringWithFormat:@"pdf2png wrote [%.0f x %.0f] pixels to %@\n",
outputSize.width, outputSize.height, outputFileName] dataUsingEncoding:NSUTF8StringEncoding]];
}
if (assetCatalog) { // we need to update the plist in the catalog
}
}
status = StatusSuccess;
exit:
return status;
}