1 # Creates a document icon from an app icon and an optional text.
3 # The font is not quite right, use this script to create a document icon
4 # for 'PDF' and compare the D with the D in Preview's pdf.icns
6 # http://www.macresearch.org/cocoa-scientists-part-xx-python-scriptersmeet-cocoa
8 from Foundation
import *
12 dont_create
= True # most likely because we're on tiger
19 LARGE
= 0 # 512, 128, 32, 16; about 96kB
20 SMALL
= 1 # 128, 32, 16; about 36kB
21 LINK
= 2 # Create link to generic icon; 4kB (== smallest block size on HFS+)
23 # path to makeicns binary
24 MAKEICNS
= 'makeicns/makeicns'
26 # List of icons to create
27 # XXX: 32x32 variants only support 3-4 letters of text
28 GENERIC_ICON_NAME
= 'MacVim-generic'
30 GENERIC_ICON_NAME
: [u
'', LARGE
],
31 'MacVim-vim': [u
'VIM', LARGE
],
32 'MacVim-txt': [u
'TXT', SMALL
],
33 'MacVim-tex': [u
'TEX', SMALL
],
34 'MacVim-h': [u
'H', SMALL
],
35 'MacVim-c': [u
'C', SMALL
],
36 'MacVim-m': [u
'M', SMALL
],
37 'MacVim-mm': [u
'MM', SMALL
],
38 'MacVim-cpp': [u
'C\uff0b\uff0b', SMALL
], # fullwidth plusses
39 'MacVim-java': [u
'JAVA', SMALL
],
40 'MacVim-f': [u
'FTRAN', SMALL
],
41 'MacVim-html': [u
'HTML', SMALL
],
42 'MacVim-xml': [u
'XML', SMALL
],
43 'MacVim-js': [u
'JS', SMALL
],
44 'MacVim-perl': [u
'PERL', SMALL
],
45 'MacVim-py': [u
'PYTHON', SMALL
],
46 'MacVim-php': [u
'PHP', SMALL
],
47 'MacVim-rb': [u
'RUBY', SMALL
],
48 'MacVim-bash': [u
'SH', SMALL
],
49 'MacVim-patch': [u
'DIFF', SMALL
],
50 'MacVim-applescript': [u
'\uf8ffSCPT', SMALL
], # apple sign
51 'MacVim-as': [u
'FLASH', LINK
],
52 'MacVim-asp': [u
'ASP', LINK
],
53 'MacVim-bib': [u
'BIB', LINK
],
54 'MacVim-cs': [u
'C#', LINK
],
55 'MacVim-csfg': [u
'CFDG', LINK
],
56 'MacVim-csv': [u
'CSV', LINK
],
57 'MacVim-tsv': [u
'TSV', LINK
],
58 'MacVim-cgi': [u
'CGI', LINK
],
59 'MacVim-cfg': [u
'CFG', LINK
],
60 'MacVim-css': [u
'CSS', SMALL
],
61 'MacVim-dtd': [u
'DTD', LINK
],
62 'MacVim-dylan': [u
'DYLAN', LINK
],
63 'MacVim-erl': [u
'ERLANG', SMALL
],
64 'MacVim-fscript': [u
'FSCPT', SMALL
],
65 'MacVim-hs': [u
'HS', SMALL
],
66 'MacVim-inc': [u
'INC', LINK
],
67 'MacVim-ics': [u
'ICS', SMALL
],
68 'MacVim-ini': [u
'INI', LINK
],
69 'MacVim-io': [u
'IO', LINK
],
70 'MacVim-bsh': [u
'BSH', LINK
],
71 'MacVim-properties': [u
'PROP', LINK
],
72 'MacVim-jsp': [u
'JSP', SMALL
],
73 'MacVim-lisp': [u
'LISP', SMALL
],
74 'MacVim-log': [u
'LOG', SMALL
],
75 'MacVim-wiki': [u
'WIKI', SMALL
],
76 'MacVim-ps': [u
'PS', LINK
],
77 #'MacVim-plist': [u'PLIST', SMALL],
78 'MacVim-sch': [u
'SCHEME', SMALL
],
79 'MacVim-sql': [u
'SQL', SMALL
],
80 'MacVim-tcl': [u
'TCL', SMALL
],
81 'MacVim-xsl': [u
'XSL', LINK
],
82 'MacVim-vcf': [u
'VCARD', SMALL
],
83 'MacVim-vb': [u
'VBASIC', LINK
],
84 'MacVim-yaml': [u
'YAML', SMALL
],
85 'MacVim-gtd': [u
'GTD', LINK
],
91 u
'MacVim-perl': u
'PL',
92 u
'MacVim-applescript': u
'\uf8ffS',
93 u
'MacVim-erl': u
'ERL',
94 u
'MacVim-fscript': u
'FSCR',
95 u
'MacVim-sch': u
'SCM',
96 u
'MacVim-vcf': u
'VCF',
102 BACKGROUND
= '/System/Library/CoreServices/CoreTypes.bundle/' + \
103 'Contents/Resources/GenericDocumentIcon.icns' # might require leopard?
104 APPICON
= 'vim-noshadow-512.png'
105 #APPICON = 'vim-noshadow-no-v-512.png'
107 class Surface(object):
108 """Represents a simple bitmapped image."""
110 def __init__(self
, *p
, **kw
):
111 if not 'premultiplyAlpha' in kw
:
112 kw
['premultiplyAlpha'] = True
113 if len(p
) == 1 and isinstance(p
[0], NSBitmapImageRep
):
114 self
.bitmapRep
= p
[0]
115 elif len(p
) == 2 and isinstance(p
[0], int) and isinstance(p
[1], int):
116 format
= NSAlphaFirstBitmapFormat
117 if not kw
['premultiplyAlpha']:
118 format
+= NSAlphaNonpremultipliedBitmapFormat
119 self
.bitmapRep
= NSBitmapImageRep
.alloc().initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bitmapFormat_bytesPerRow_bitsPerPixel_(
120 None, p
[0], p
[1], 8, 4, True, False, NSDeviceRGBColorSpace
,
123 if not hasattr(self
, 'bitmapRep') or not self
.bitmapRep
:
124 raise Exception('Failed to create surface: ' + str(p
))
127 return map(int, self
.bitmapRep
.size()) # cocoa returns floats. cocoa ftw
130 """Returns data in ARGB order (on intel, at least)."""
132 if r
.bitmapFormat() != (NSAlphaNonpremultipliedBitmapFormat |
133 NSAlphaFirstBitmapFormat
) or \
134 r
.bitsPerPixel() != 32 or \
136 r
.samplesPerPixel() != 4:
137 raise Exception("Unsupported image format")
138 return self
.bitmapRep
.bitmapData()
140 def save(self
, filename
):
141 """Saves image as png file."""
142 self
.bitmapRep
.representationUsingType_properties_(NSPNGFileType
, None) \
143 .writeToFile_atomically_(filename
, True)
146 self
.bitmapRep
.draw()
149 # Note: Cocoa only supports contexts with premultiplied alpha
150 return NSGraphicsContext
.graphicsContextWithBitmapImageRep_(self
.bitmapRep
)
153 return Surface(self
.bitmapRep
.copy())
157 """Represents an image that can consist of several Surfaces."""
159 def __init__(self
, param
):
160 if isinstance(param
, str):
161 self
.image
= NSImage
.alloc().initWithContentsOfFile_(param
)
162 elif isinstance(param
, Surface
):
163 self
.image
= NSImage
.alloc().initWithSize_( param
.size() )
164 self
.image
.addRepresentation_(param
.bitmapRep
)
167 raise Exception('Failed to load image: ' + str(filename
))
169 def surfaceOfSize(self
, w
, h
):
170 """Returns an ARGB, non-premultiplied surface of size w*h or throws."""
172 for rep
in self
.image
.representations():
173 if map(int, rep
.size()) == [w
, h
]:
177 raise Exception('Unsupported size %dx%d', w
, h
)
181 self
.compositeInRect( ((0, 0), self
.image
.size()) )
183 def compositeInRect(self
, r
, mode
=NSCompositeSourceOver
):
184 self
.image
.drawInRect_fromRect_operation_fraction_(r
, NSZeroRect
,
188 class Context(object):
189 # Tiger has only Python2.3, so we can't use __enter__ / __exit__ for this :-(
191 def __init__(self
, surface
):
192 NSGraphicsContext
.saveGraphicsState()
193 c
= surface
.context()
194 c
.setShouldAntialias_(True);
195 c
.setImageInterpolation_(NSImageInterpolationHigh
);
196 NSGraphicsContext
.setCurrentContext_(c
)
199 NSGraphicsContext
.restoreGraphicsState()
202 class SplittableBackground(object):
204 def __init__(self
, unsplitted
, shouldSplit
=True):
205 self
.unsplitted
= unsplitted
206 self
.shouldSplit
= shouldSplit
210 def rawGroundAtSize(self
, s
):
211 return self
.unsplitted
.surfaceOfSize(s
, s
)
213 def groundAtSize(self
, s
):
214 if not self
.shouldSplit
:
215 return self
.rawGroundAtSize(s
)
216 self
._performSplit
(s
)
217 return self
.ground
[s
]
219 def shadowAtSize(self
, s
):
220 if not self
.shouldSplit
:
222 self
._performSplit
(s
)
223 return self
.shadow
[s
]
225 def _performSplit(self
, s
):
227 assert s
in self
.shadow
229 assert s
not in self
.shadow
230 ground
, shadow
= splitGenericDocumentIcon(self
.unsplitted
, s
)
231 self
.ground
[s
] = ground
232 self
.shadow
[s
] = shadow
235 class BackgroundRenderer(object):
237 def __init__(self
, bg
, icon
=None):
242 def drawIcon(self
, s
):
245 # found by flow program, better than anything i came up with manually before
246 # (except for the 16x16 variant :-( )
248 512: [ 0.7049, 0.5653, -4.2432, 0.5656],
249 256: [ 0.5690, 0.5658, -1.9331, 0.5656],
250 128: [ 1.1461, 0.5684, -0.8482, 0.5681],
252 32: [-0.2682, 0.5895, -2.2130, 0.5701], # intensity
253 #32: [-0.2731, 0.5898, -2.2262, 0.5729], # rgb (no rmse difference)
255 #16: [-0.3033, 0.4909, -1.3235, 0.4790], # program, intensity
256 #16: [-0.3087, 0.4920, -1.2990, 0.4750], # program, rgb mode
257 16: [ 0.0000, 0.5000, -1.0000, 0.5000], # manually, better
260 assert s
in [16, 32, 128, 256, 512]
263 # convert from `flow` coords to cocoa
264 a
[2] = -a
[2] # mirror y
266 w
, h
= s
*a
[1], s
*a
[3]
267 self
.icon
.compositeInRect( (((s
-w
)/2 + a
[0], (s
-h
)/2 + a
[2]), (w
, h
)) )
269 def drawAtSize(self
, s
):
270 self
.bgRenderer
.groundAtSize(s
).draw()
272 if self
.bgRenderer
.shouldSplit
:
273 # shadow needs to be composited, so it needs to be in an image
274 Image(self
.bgRenderer
.shadowAtSize(s
)).blend()
276 def backgroundAtSize(self
, s
):
277 if not s
in self
.cache
:
278 result
= Surface(s
, s
)
279 context
= Context(result
)
282 self
.cache
[s
] = result
286 def splitGenericDocumentIcon(img
, s
):
287 """Takes the generic document icon and splits it into a background and a
288 shadow layer. For the 32x32 and 16x16 variants, the white pixels of the page
289 curl are hardcoded into the otherwise transparent shadow layer."""
292 r
= img
.surfaceOfSize(w
, h
)
296 ground
= Surface(w
, h
, premultiplyAlpha
=False)
297 shadow
= Surface(w
, h
, premultiplyAlpha
=False)
299 grounddata
= ground
.data()
300 shadowdata
= shadow
.data()
305 ia
, ir
, ig
, ib
= data
[idx
:idx
+ 4]
307 # buffer objects don't support slice assignment :-(
309 grounddata
[idx
+ 1] = ir
310 grounddata
[idx
+ 2] = ig
311 grounddata
[idx
+ 3] = ib
312 shadowdata
[idx
] = chr(0)
313 shadowdata
[idx
+ 1] = chr(0)
314 shadowdata
[idx
+ 2] = chr(0)
315 shadowdata
[idx
+ 3] = chr(0)
318 assert ir
== ig
== ib
319 grounddata
[idx
] = chr(255)
320 grounddata
[idx
+ 1] = chr(255)
321 grounddata
[idx
+ 2] = chr(255)
322 grounddata
[idx
+ 3] = chr(255)
323 shadowdata
[idx
] = chr(255 - ord(ir
))
324 shadowdata
[idx
+ 1] = chr(0)
325 shadowdata
[idx
+ 2] = chr(0)
326 shadowdata
[idx
+ 3] = chr(0)
328 # Special-case 16x16 and 32x32 cases: Make some pixels on the fold white.
329 # Ideally, I could make the fold whiteish in all variants, but I can't.
330 whitePix
= { 16: [(10, 2), (10, 3), (11, 3), (10, 4), (11, 4), (12, 4)],
331 32: [(21, 4), (21, 5), (22, 5), (21, 6), (22, 6), (23, 6)]}
332 if (w
, h
) in [(16, 16), (32, 32)]:
333 for x
, y
in whitePix
[w
]:
335 shadowdata
[idx
] = chr(255)
336 shadowdata
[idx
+ 1] = chr(255)
337 shadowdata
[idx
+ 2] = chr(255)
338 shadowdata
[idx
+ 3] = chr(255)
340 return ground
, shadow
343 class TextRenderer(object):
348 def attribsAtSize(self
, s
):
349 if s
not in self
.cache
:
350 self
.cache
[s
] = self
._attribsAtSize
(s
)
353 def centeredStyle(self
):
354 style
= NSMutableParagraphStyle
.new()
355 style
.setParagraphStyle_(NSParagraphStyle
.defaultParagraphStyle())
356 style
.setAlignment_(NSCenterTextAlignment
)
359 def _attribsAtSize(self
, s
):
360 # This looks not exactly like the font on Preview.app's document icons,
361 # but I believe that's because Preview's icons are drawn by Photoshop,
362 # and Adobe's font rendering is different from Apple's.
363 fontname
= 'LucidaGrande-Bold'
365 # Prepare text format
366 fontsizes
= { 512: 72.0, 256: 36.0, 128: 18.0, 32: 7.0, 16: 3.0 }
367 # http://developer.apple.com/documentation/Cocoa/Conceptual/AttributedStrings/Articles/standardAttributes.html#//apple_ref/doc/uid/TP40004903
369 NSParagraphStyleAttributeName
: self
.centeredStyle(),
370 NSForegroundColorAttributeName
: NSColor
.colorWithDeviceWhite_alpha_(
372 NSFontAttributeName
: NSFont
.fontWithName_size_(fontname
, fontsizes
[s
])
375 # tighten font a bit for some sizes
377 attribs
[NSKernAttributeName
] = -1.0
379 attribs
[NSKernAttributeName
] = -0.25
381 if not attribs
[NSFontAttributeName
]:
382 raise Exception('Failed to load font %s' % fontname
)
385 def drawTextAtSize(self
, text
, s
):
386 """Draws text `s` into the current context of size `s`."""
389 512: ((0, 7), (512, 119)),
390 128: ((0, 6), (128, 26.5)),
391 256: ((0, 7), (256, 57)),
392 16: ((1, 1), (15, 5)),
393 #32: ((1, 1), (31, 9))
396 attribs
= self
.attribsAtSize(s
)
397 text
= NSString
.stringWithString_(text
)
398 if s
in [16, 128, 256, 512]:
399 text
.drawInRect_withAttributes_(textRects
[s
], attribs
)
401 # Try to align text on pixel boundary:
402 attribs
= attribs
.copy()
403 attribs
[NSParagraphStyleAttributeName
] = \
404 NSParagraphStyle
.defaultParagraphStyle()
405 ts
= text
.sizeWithAttributes_(attribs
)
406 text
.drawAtPoint_withAttributes_( (math
.floor((32.0-ts
[0])/2) + 0.5, 1.5),
410 class OfficeTextRenderer(TextRenderer
):
411 """Uses Office's LucidaSans font for 32x32.
413 This font looks much better for certain strings (e.g. "PDF") but much worse
414 for most others (e.g. "VIM", "JAVA") -- and office fonts are usually not
415 installed. Hence, this class is better not used.
418 def _attribsAtSize(self
, s
):
419 self
.useOfficeFont
= False
420 attribs
= TextRenderer
._attribsAtSize
(self
, s
)
422 font
= NSFont
.fontWithName_size_('LucidaSans-Demi', 7.0)
424 attribs
[NSFontAttributeName
] = font
425 attribs
[NSKernAttributeName
] = 0
426 self
.useOfficeFont
= True
429 def drawTextAtSize(self
, text
, s
):
430 attribs
= self
.attribsAtSize(s
)
431 if not self
.useOfficeFont
or s
!= 32:
432 TextRenderer
.drawTextAtSize(self
, text
, s
)
434 text
= NSString
.stringWithString_(text
)
435 text
.drawInRect_withAttributes_( ((0, 1), (31, 11)), attribs
)
438 def createIcon(outname
, s
, bg
, textRenderer
, text
, shorttext
=None):
441 output
= bg
.backgroundAtSize(s
).copy()
443 # Draw text on top of shadow
444 context
= Context(output
)
445 if s
in [16, 32] and shorttext
:
447 textRenderer
.drawTextAtSize(text
, s
)
454 def createLinks(icons
, target
):
455 assert len(icons
) > 0
457 icnsName
= '%s.icns' % name
458 if os
.access(icnsName
, os
.F_OK
):
460 os
.symlink(target
, icnsName
)
463 TMPFILE
= 'make_icons_tmp_%d.png'
464 sizes
= [512, 128, 32, 16]
467 if len(sys
.argv
) > 1:
468 os
.chdir(sys
.argv
[1])
469 appIcon
= os
.path
.join(srcdir
, APPICON
)
470 makeIcns
= os
.path
.join(srcdir
, MAKEICNS
)
473 print "PyObjC not found, only using a stock icon for document icons."
475 shutil
.copyfile(BACKGROUND
, '%s.icns' % GENERIC_ICON_NAME
)
476 createLinks([name
for name
in vimIcons
if name
!= GENERIC_ICON_NAME
],
477 '%s.icns' % GENERIC_ICON_NAME
)
480 # http://www.cocoabuilder.com/archive/message/cocoa/2008/8/6/214964
483 textRenderer
= TextRenderer()
484 #textRenderer = OfficeTextRenderer()
486 # Prepare input images
487 bgIcon
= Image(BACKGROUND
)
489 #bg = SplittableBackground(bgIcon, shouldSplit=False)
490 bg
= SplittableBackground(bgIcon
, shouldSplit
=True)
492 icon
= Image(appIcon
)
493 bgRenderer
= BackgroundRenderer(bg
, icon
)
495 if not os
.access(makeIcns
, os
.X_OK
):
496 print 'Cannot find makeicns at %s', makeIcns
499 # create LARGE and SMALL icons first...
500 for name
, t
in vimIcons
.iteritems():
502 if size
== LINK
: continue
504 icnsName
= '%s.icns' % name
507 currSizes
= [128, 32, 16]
508 args
= '-128 %s -32 %s -16 %s' % (
509 TMPFILE
% 128, TMPFILE
% 32, TMPFILE
% 16)
511 currSizes
= [512, 128, 32, 16]
512 args
= '-512 %s -128 %s -32 %s -16 %s' % (
513 TMPFILE
% 512, TMPFILE
% 128, TMPFILE
% 32, TMPFILE
% 16)
515 st
= shorttext
.get(name
)
517 createIcon(TMPFILE
% s
, s
, bgRenderer
, textRenderer
, text
, shorttext
=st
)
519 os
.system('%s %s -out %s' % (makeIcns
, args
, icnsName
))
521 del text
, size
, name
, t
523 # ...create links later (to make sure the link targets exist)
524 createLinks([name
for (name
, t
) in vimIcons
.items() if t
[1] == LINK
],
525 '%s.icns' % GENERIC_ICON_NAME
)
528 if __name__
== '__main__':
533 if os
.access(TMPFILE
% s
, os
.F_OK
):
534 os
.remove(TMPFILE
% s
)