1 from Foundation
import *
9 from optparse
import OptionParser
13 DEFAULT_BACKGROUND
= '/System/Library/CoreServices/CoreTypes.bundle/' + \
14 'Contents/Resources/GenericDocumentIcon.icns' # might require leopard?
17 # Cache both images and background renderers globally
23 # http://www.cocoabuilder.com/archive/message/cocoa/2008/8/6/214964
27 class Surface(object):
28 """Represents a simple bitmapped image."""
30 def __init__(self
, *p
, **kw
):
31 if not 'premultiplyAlpha' in kw
:
32 kw
['premultiplyAlpha'] = True
33 if len(p
) == 1 and isinstance(p
[0], NSBitmapImageRep
):
35 elif len(p
) == 2 and isinstance(p
[0], int) and isinstance(p
[1], int):
36 format
= NSAlphaFirstBitmapFormat
37 if not kw
['premultiplyAlpha']:
38 format
+= NSAlphaNonpremultipliedBitmapFormat
39 self
.bitmapRep
= NSBitmapImageRep
.alloc().initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bitmapFormat_bytesPerRow_bitsPerPixel_(
40 None, p
[0], p
[1], 8, 4, True, False, NSDeviceRGBColorSpace
,
43 if not hasattr(self
, 'bitmapRep') or not self
.bitmapRep
:
44 raise Exception('Failed to create surface: ' + str(p
))
47 return map(int, self
.bitmapRep
.size()) # cocoa returns floats. cocoa ftw
50 """Returns data in ARGB order (on intel, at least)."""
52 if r
.bitmapFormat() != (NSAlphaNonpremultipliedBitmapFormat |
53 NSAlphaFirstBitmapFormat
) or \
54 r
.bitsPerPixel() != 32 or \
56 r
.samplesPerPixel() != 4:
57 raise Exception("Unsupported image format")
58 return self
.bitmapRep
.bitmapData()
60 def save(self
, filename
):
61 """Saves image as png file."""
62 self
.bitmapRep
.representationUsingType_properties_(NSPNGFileType
, None) \
63 .writeToFile_atomically_(filename
, True)
69 # Note: Cocoa only supports contexts with premultiplied alpha
70 return NSGraphicsContext
.graphicsContextWithBitmapImageRep_(self
.bitmapRep
)
73 return Surface(self
.bitmapRep
.copy())
77 """Represents an image that can consist of several Surfaces."""
79 def __init__(self
, param
):
80 if isinstance(param
, str):
81 self
.image
= NSImage
.alloc().initWithContentsOfFile_(param
)
82 elif isinstance(param
, Surface
):
83 self
.image
= NSImage
.alloc().initWithSize_( param
.size() )
84 self
.image
.addRepresentation_(param
.bitmapRep
)
87 raise Exception('Failed to load image: ' + str(param
))
89 def surfaceOfSize(self
, w
, h
):
90 """Returns an ARGB, non-premultiplied surface of size w*h or throws."""
92 for rep
in self
.image
.representations():
93 # Cocoa reports fraction widths for pngs (wtf?!), so use round()
94 if map(lambda x
: int(round(x
)), rep
.size()) == [w
, h
]:
98 # XXX: Resample in this case? That'd make the program easier to use, but
99 # can silently create blurry backgrounds. Since this happens with
100 # the app icon anyways, this might not be a huge deal?
102 raise Exception('Unsupported size %dx%d', w
, h
)
106 self
.compositeInRect( ((0, 0), self
.image
.size()) )
108 def compositeInRect(self
, r
, mode
=NSCompositeSourceOver
):
109 self
.image
.drawInRect_fromRect_operation_fraction_(r
, NSZeroRect
,
114 for rep
in self
.image
.representations():
115 s
.add(tuple(map(lambda x
: int(round(x
)), rep
.size())))
119 class Context(object):
120 # Tiger has only Python2.3, so we can't use __enter__ / __exit__ for this :-(
122 def __init__(self
, surface
):
123 NSGraphicsContext
.saveGraphicsState()
124 c
= surface
.context()
125 c
.setShouldAntialias_(True);
126 c
.setImageInterpolation_(NSImageInterpolationHigh
);
127 NSGraphicsContext
.setCurrentContext_(c
)
130 NSGraphicsContext
.restoreGraphicsState()
133 class SplittableBackground(object):
135 def __init__(self
, unsplitted
, shouldSplit
=True):
136 self
.unsplitted
= unsplitted
137 self
.shouldSplit
= shouldSplit
141 def rawGroundAtSize(self
, s
):
142 return self
.unsplitted
.surfaceOfSize(s
, s
)
144 def groundAtSize(self
, s
):
145 if not self
.shouldSplit
:
146 return self
.rawGroundAtSize(s
)
147 self
._performSplit
(s
)
148 return self
.ground
[s
]
150 def shadowAtSize(self
, s
):
151 if not self
.shouldSplit
:
153 self
._performSplit
(s
)
154 return self
.shadow
[s
]
156 def _performSplit(self
, s
):
158 assert s
in self
.shadow
160 assert s
not in self
.shadow
161 ground
, shadow
= splitGenericDocumentIcon(self
.unsplitted
, s
)
162 self
.ground
[s
] = ground
163 self
.shadow
[s
] = shadow
166 class BackgroundRenderer(object):
168 def __init__(self
, bg
, icon
=None, r
={}):
174 def drawIcon(self
, s
):
178 assert s
in [16, 32, 128, 256, 512]
179 a
= list(self
.rect
[s
])
181 # convert from `flow` coords to cocoa
182 a
[2] = -a
[2] # mirror y
184 w
, h
= s
*a
[1], s
*a
[3]
185 self
.icon
.compositeInRect( (((s
-w
)/2 + a
[0], (s
-h
)/2 + a
[2]), (w
, h
)) )
187 def drawAtSize(self
, s
):
189 # No need to split the background if no icons is interleaved -- take
190 # the faster code path in that case.
191 self
.bgRenderer
.rawGroundAtSize(s
).draw()
194 self
.bgRenderer
.groundAtSize(s
).draw()
196 if self
.bgRenderer
.shouldSplit
:
197 # shadow needs to be composited, so it needs to be in an image
198 Image(self
.bgRenderer
.shadowAtSize(s
)).blend()
200 def backgroundAtSize(self
, s
):
201 if not s
in self
.cache
:
202 result
= Surface(s
, s
)
203 context
= Context(result
)
206 self
.cache
[s
] = result
210 def splitGenericDocumentIcon(img
, s
):
211 """Takes the generic document icon and splits it into a background and a
212 shadow layer. For the 32x32 and 16x16 variants, the white pixels of the page
213 curl are hardcoded into the otherwise transparent shadow layer."""
216 r
= img
.surfaceOfSize(w
, h
)
220 ground
= Surface(w
, h
, premultiplyAlpha
=False)
221 shadow
= Surface(w
, h
, premultiplyAlpha
=False)
223 grounddata
= ground
.data()
224 shadowdata
= shadow
.data()
229 ia
, ir
, ig
, ib
= data
[idx
:idx
+ 4]
231 # buffer objects don't support slice assignment :-(
233 grounddata
[idx
+ 1] = ir
234 grounddata
[idx
+ 2] = ig
235 grounddata
[idx
+ 3] = ib
236 shadowdata
[idx
] = chr(0)
237 shadowdata
[idx
+ 1] = chr(0)
238 shadowdata
[idx
+ 2] = chr(0)
239 shadowdata
[idx
+ 3] = chr(0)
242 assert ir
== ig
== ib
243 grounddata
[idx
] = chr(255)
244 grounddata
[idx
+ 1] = chr(255)
245 grounddata
[idx
+ 2] = chr(255)
246 grounddata
[idx
+ 3] = chr(255)
247 shadowdata
[idx
] = chr(255 - ord(ir
))
248 shadowdata
[idx
+ 1] = chr(0)
249 shadowdata
[idx
+ 2] = chr(0)
250 shadowdata
[idx
+ 3] = chr(0)
252 # Special-case 16x16 and 32x32 cases: Make some pixels on the fold white.
253 # Ideally, I could make the fold whiteish in all variants, but I can't.
254 whitePix
= { 16: [(10, 2), (10, 3), (11, 3), (10, 4), (11, 4), (12, 4)],
255 32: [(21, 4), (21, 5), (22, 5), (21, 6), (22, 6), (23, 6)]}
256 if (w
, h
) in [(16, 16), (32, 32)]:
257 for x
, y
in whitePix
[w
]:
259 shadowdata
[idx
] = chr(255)
260 shadowdata
[idx
+ 1] = chr(255)
261 shadowdata
[idx
+ 2] = chr(255)
262 shadowdata
[idx
+ 3] = chr(255)
264 return ground
, shadow
267 class TextRenderer(object):
272 def attribsAtSize(self
, s
):
273 if s
not in self
.cache
:
274 self
.cache
[s
] = self
._attribsAtSize
(s
)
277 def centeredStyle(self
):
278 style
= NSMutableParagraphStyle
.new()
279 style
.setParagraphStyle_(NSParagraphStyle
.defaultParagraphStyle())
280 style
.setAlignment_(NSCenterTextAlignment
)
283 def _attribsAtSize(self
, s
):
284 # This looks not exactly like the font on Preview.app's document icons,
285 # but I believe that's because Preview's icons are drawn by Photoshop,
286 # and Adobe's font rendering is different from Apple's.
287 fontname
= 'LucidaGrande-Bold'
289 # Prepare text format
290 fontsizes
= { 512: 72.0, 256: 36.0, 128: 18.0, 32: 7.0, 16: 3.0 }
291 # http://developer.apple.com/documentation/Cocoa/Conceptual/AttributedStrings/Articles/standardAttributes.html#//apple_ref/doc/uid/TP40004903
293 NSParagraphStyleAttributeName
: self
.centeredStyle(),
294 NSForegroundColorAttributeName
: NSColor
.colorWithDeviceWhite_alpha_(
296 NSFontAttributeName
: NSFont
.fontWithName_size_(fontname
, fontsizes
[s
])
299 # tighten font a bit for some sizes
301 attribs
[NSKernAttributeName
] = -1.0
303 attribs
[NSKernAttributeName
] = -0.25
305 if not attribs
[NSFontAttributeName
]:
306 raise Exception('Failed to load font %s' % fontname
)
309 def drawTextAtSize(self
, text
, s
):
310 """Draws text `s` into the current context of size `s`."""
313 512: ((0, 7), (512, 119)),
314 128: ((0, 6), (128, 26.5)),
315 256: ((0, 7), (256, 57)),
316 16: ((1, 1), (15, 5)),
317 #32: ((1, 1), (31, 9))
320 attribs
= self
.attribsAtSize(s
)
321 text
= NSString
.stringWithString_(text
)
322 if s
in [16, 128, 256, 512]:
323 text
.drawInRect_withAttributes_(textRects
[s
], attribs
)
325 # Try to align text on pixel boundary:
326 attribs
= attribs
.copy()
327 attribs
[NSParagraphStyleAttributeName
] = \
328 NSParagraphStyle
.defaultParagraphStyle()
329 ts
= text
.sizeWithAttributes_(attribs
)
330 text
.drawAtPoint_withAttributes_( (math
.floor((32.0-ts
[0])/2) + 0.5, 1.5),
334 class OfficeTextRenderer(TextRenderer
):
335 """Uses Office's LucidaSans font for 32x32.
337 This font looks much better for certain strings (e.g. "PDF") but much worse
338 for most others (e.g. "VIM", "JAVA") -- and office fonts are usually not
339 installed. Hence, this class is better not used.
342 def _attribsAtSize(self
, s
):
343 self
.useOfficeFont
= False
344 attribs
= TextRenderer
._attribsAtSize
(self
, s
)
346 font
= NSFont
.fontWithName_size_('LucidaSans-Demi', 7.0)
348 attribs
[NSFontAttributeName
] = font
349 attribs
[NSKernAttributeName
] = 0
350 self
.useOfficeFont
= True
353 def drawTextAtSize(self
, text
, s
):
354 attribs
= self
.attribsAtSize(s
)
355 if not self
.useOfficeFont
or s
!= 32:
356 TextRenderer
.drawTextAtSize(self
, text
, s
)
358 text
= NSString
.stringWithString_(text
)
359 text
.drawInRect_withAttributes_( ((0, 1), (31, 11)), attribs
)
362 def createIcon(s
, bg
, textRenderer
, text
):
365 output
= bg
.backgroundAtSize(s
).copy()
367 # Draw text on top of shadow
368 context
= Context(output
)
369 if s
in text
and text
[s
]:
370 textRenderer
.drawTextAtSize(text
[s
], s
)
376 def textDictFromTextList(l
):
377 assert 1 <= len(l
) <= 3
379 return dict.fromkeys([16, 32, 128, 256, 512], l
[0])
381 return dict(zip([16, 32], 2*[l
[1]]) + zip((128, 256, 512), 3*[l
[0]]))
383 return dict([(16, l
[2]), (32, l
[1])] + zip((128, 256, 512), 3*[l
[0]]))
386 def saveIcns(icons
, icnsName
, makeIcns
='./makeicns'):
387 """Creates an icns file with several variants.
390 icons: A dict that contains icon size as key and Surface as value.
391 Valid keys are 512, 256, 128, 32, 16
392 icnsname: Name of the output file
394 # If IconFamily was less buggy, we could wrap it into a python module and
395 # call it directly, which is about a lot faster. However, IconFamily does not
396 # work with NSAlphaNonpremultipliedBitmapFormat correctly, so this has to
400 #16: IconFamily.kSmall32BitData,
401 #32: IconFamily.kLarge32BitData,
402 #128: IconFamily.kThumbnail32BitData,
403 #256: IconFamily.kIconServices256PixelDataARGB,
404 #512: IconFamily.IconServices512PixelDataARGB,
407 #16: IconFamily.kSmall8BitMask,
408 #32: IconFamily.kLarge8BitMask,
409 #128: IconFamily.kThumbnail8BitMask,
411 #output = IconFamily.IconFamily.iconFamily()
412 #for s, icon in icons.items():
413 #output.setIconFamilyElement_fromBitmapImageRep_(typeDict[s], icon.bitmapRep)
415 #output.setIconFamilyElement_fromBitmapImageRep_(
416 #maskDict[s], icon.bitmapRep)
417 #output.writeToFile_(icnsName)
418 TMPFILE
= 'docerator_tmp_%d.png'
421 for s
, icon
in icons
.items():
422 assert s
in [512, 256, 128, 32, 16]
423 assert icon
.size() == [s
, s
]
424 icon
.save(TMPFILE
% s
)
425 args
.append('-%d %s' % (s
, TMPFILE
% s
))
427 os
.system('%s %s -out %s' % (makeIcns
, ' '.join(args
), icnsName
)) == 0
430 if os
.access(TMPFILE
% s
, os
.F_OK
):
431 os
.remove(TMPFILE
% s
)
434 def getOutname(options
):
436 """ '/my/path/to/file.txt' -> 'file' """
437 return os
.path
.splitext(os
.path
.basename(p
))[0]
440 textPart
= options
.text
.split(',')[0]
442 base
= saneBasename(options
.appicon
)
444 base
= saneBasename(getBgName(options
))
445 return '%s-%s.icns' % (base
, textPart
)
448 def cachedImage(filename
):
449 absPath
= os
.path
.abspath(filename
)
450 if not absPath
in imageCache
:
451 imageCache
[absPath
] = Image(absPath
)
452 return imageCache
[absPath
]
455 def cachedBackground(img
, split
):
457 if not key
in bgCache
:
458 bgCache
[key
] = SplittableBackground(img
, shouldSplit
=split
)
462 # taken from running flow on preview
464 16: (-0.30890000000000001, 0.4919, -1.2968, 0.4743),
465 32: (-0.27810000000000001,
468 0.57140000000000002),
469 128: (1.1774, 0.56820000000000004, -0.8246, 0.56799999999999995),
470 256: (0.5917, 0.56489999999999996, -1.8994, 0.56499999999999995),
471 512: (0.68700000000000006,
478 def getBgName(options
):
479 if not hasattr(options
, 'background') \
480 or options
.background
in ['default-split', 'default-unsplit']:
481 return DEFAULT_BACKGROUND
483 return options
.background
486 class IconGenerator(object):
487 def __init__(self
, options
):
488 if hasattr(options
, 'textrenderer') and options
.textrenderer
:
489 self
.textRenderer
= options
.textrenderer()
491 self
.textRenderer
= TextRenderer()
493 # Prepare input images
494 splitBackground
= options
.background
== 'default-split'
495 self
.bgIcon
= cachedImage(getBgName(options
))
499 self
.testIcon
= cachedImage(options
.appicon
)
501 rects
= defaultRects
.copy()
502 rects
[16] = [ 0.0000, 0.5000, -1.0000, 0.5000] # manually, better
503 if hasattr(options
, 'rects'):
504 rects
.update(options
.rects
)
506 bg
= cachedBackground(self
.bgIcon
, splitBackground
)
508 if hasattr(options
, 'backgroundrenderer') and options
.backgroundrenderer
:
509 self
.bgRenderer
= options
.backgroundrenderer(bg
, self
.testIcon
, rects
)
511 self
.bgRenderer
= BackgroundRenderer(bg
, self
.testIcon
, rects
)
513 self
.testtext
= textDictFromTextList(options
.text
.split(','))
515 def createIconAtSize(self
, s
):
516 return createIcon(s
, self
.bgRenderer
, self
.textRenderer
, self
.testtext
)
519 def iconGenerator(**kwargs
):
520 return IconGenerator(optsFromDict(**kwargs
))
523 def makedocicon_opts(options
):
524 renderer
= IconGenerator(options
)
526 if hasattr(options
, 'sizes') and options
.sizes
:
527 if isinstance(options
.sizes
, list):
528 sizes
= options
.sizes
530 sizes
= map(int, options
.sizes
.split(','))
532 sizes
= renderer
.bgIcon
.sizes()
533 if renderer
.testIcon
:
534 sizes
= sizes
.intersection(renderer
.testIcon
.sizes())
535 sizes
= sorted(map(operator
.itemgetter(0), sizes
))
537 icons
= dict([(s
, renderer
.createIconAtSize(s
)) for s
in sizes
])
540 for s
, icon
in icons
.iteritems():
541 icon
.save(options
.debug
% s
)
543 if hasattr(options
, 'outname') and options
.outname
:
544 outname
= options
.outname
546 outname
= getOutname(options
)
547 if saveIcns(icons
, outname
, options
.makeicns
):
548 print 'Wrote', outname
550 print 'Failed to write %s. Make sure makeicns is in your path.' % outname
553 def optsFromDict(**kwargs
):
554 options
, _
= getopts().parse_args([]) # get default options
556 setattr(options
, k
, kwargs
[k
])
560 def makedocicon(**kwargs
):
561 makedocicon_opts(optsFromDict(**kwargs
))
564 def makedocicons_opts(options
):
565 if not hasattr(options
, 'text') or not options
.text
:
570 makedocicon_opts(options
)
573 def makedocicons(**kwargs
):
574 makedocicons_opts(optsFromDict(**kwargs
))
578 parser
= OptionParser(usage
='%prog [options]', version
='%prog 1.01')
579 parser
.add_option('--background', '--bg', default
='default-split',
580 help='Used as background (special values: "default-split" (default), ' \
581 '"default-unsplit").')
582 parser
.add_option('--appicon', help='App icon, defaults to no icon.')
584 parser
.add_option('--text', help='Text on icon. Defaults to empty. '
585 'More than one text is supported, multiple docicons are generated in '
586 'that case.', action
='append')
587 parser
.add_option('--sizes', help='Sizes of icons. ' \
588 'Defaults to all sizes available in input appicon. Example: "512,128,16"')
589 # XXX(Nico): This has to go
590 parser
.add_option('--debug', help='If set, write out pngs for all variants.' \
591 ' This needs to look like "debug%d.png".')
592 # XXX(Nico): This has to go once IconFamily is less buggy and can be used
594 parser
.add_option('--makeicns', help='Path to makeicns binary',
595 default
='./makeicns')
600 options
, args
= getopts().parse_args()
601 makedocicons_opts(options
)
604 if __name__
== '__main__':