Make icon generation script faster and more flexible
[MacVim.git] / src / MacVim / icons / make_icons.py
blobb4567012ef92b201d2351822fed64a974c3e2e3d
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
7 try:
8 from Foundation import *
9 from AppKit import *
10 dont_create = False
11 except:
12 dont_create = True # most likely because we're on tiger
14 import math
15 import os
16 import sys
18 # icon types
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'
29 vimIcons = {
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],
88 shorttext = {
89 u'MacVim-py': u'PY',
90 u'MacVim-rb': u'RB',
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',
97 u'MacVim-vb': u'VB',
101 # Resources
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,
121 format, 0, 0)
123 if not hasattr(self, 'bitmapRep') or not self.bitmapRep:
124 raise Exception('Failed to create surface: ' + str(p))
126 def size(self):
127 return map(int, self.bitmapRep.size()) # cocoa returns floats. cocoa ftw
129 def data(self):
130 """Returns data in ARGB order (on intel, at least)."""
131 r = self.bitmapRep
132 if r.bitmapFormat() != (NSAlphaNonpremultipliedBitmapFormat |
133 NSAlphaFirstBitmapFormat) or \
134 r.bitsPerPixel() != 32 or \
135 r.isPlanar() 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)
145 def draw(self):
146 self.bitmapRep.draw()
148 def context(self):
149 # Note: Cocoa only supports contexts with premultiplied alpha
150 return NSGraphicsContext.graphicsContextWithBitmapImageRep_(self.bitmapRep)
152 def copy(self):
153 return Surface(self.bitmapRep.copy())
156 class Image(object):
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)
166 if not self.image:
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."""
171 r = None
172 for rep in self.image.representations():
173 if map(int, rep.size()) == [w, h]:
174 r = rep
175 break
176 if not r:
177 raise Exception('Unsupported size %dx%d', w, h)
178 return Surface(r)
180 def blend(self):
181 self.compositeInRect( ((0, 0), self.image.size()) )
183 def compositeInRect(self, r, mode=NSCompositeSourceOver):
184 self.image.drawInRect_fromRect_operation_fraction_(r, NSZeroRect,
185 mode, 1.0)
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)
198 def done(self):
199 NSGraphicsContext.restoreGraphicsState()
202 class SplittableBackground(object):
204 def __init__(self, unsplitted, shouldSplit=True):
205 self.unsplitted = unsplitted
206 self.shouldSplit = shouldSplit
207 self.ground = {}
208 self.shadow = {}
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:
221 return None
222 self._performSplit(s)
223 return self.shadow[s]
225 def _performSplit(self, s):
226 if s in self.ground:
227 assert s in self.shadow
228 return
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):
238 self.bgRenderer = bg
239 self.icon = icon
240 self.cache = {}
242 def drawIcon(self, s):
243 if not self.icon:
244 return
245 # found by flow program, better than anything i came up with manually before
246 # (except for the 16x16 variant :-( )
247 transforms = {
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]
261 a = transforms[s]
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()
271 self.drawIcon(s)
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)
280 self.drawAtSize(s)
281 context.done()
282 self.cache[s] = result
283 return self.cache[s]
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."""
291 w, h = s, s
292 r = img.surfaceOfSize(w, h)
293 bps = 4*w
294 data = r.data()
296 ground = Surface(w, h, premultiplyAlpha=False)
297 shadow = Surface(w, h, premultiplyAlpha=False)
299 grounddata = ground.data()
300 shadowdata = shadow.data()
302 for y in xrange(h):
303 for x in xrange(w):
304 idx = y*bps + 4*x
305 ia, ir, ig, ib = data[idx:idx + 4]
306 if ia != chr(255):
307 # buffer objects don't support slice assignment :-(
308 grounddata[idx] = ia
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)
316 continue
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]:
334 idx = y*bps + 4*x
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):
345 def __init__(self):
346 self.cache = {}
348 def attribsAtSize(self, s):
349 if s not in self.cache:
350 self.cache[s] = self._attribsAtSize(s)
351 return self.cache[s]
353 def centeredStyle(self):
354 style = NSMutableParagraphStyle.new()
355 style.setParagraphStyle_(NSParagraphStyle.defaultParagraphStyle())
356 style.setAlignment_(NSCenterTextAlignment)
357 return style
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
368 attribs = {
369 NSParagraphStyleAttributeName: self.centeredStyle(),
370 NSForegroundColorAttributeName: NSColor.colorWithDeviceWhite_alpha_(
371 0.34, 1),
372 NSFontAttributeName: NSFont.fontWithName_size_(fontname, fontsizes[s])
375 # tighten font a bit for some sizes
376 if s in [256, 512]:
377 attribs[NSKernAttributeName] = -1.0
378 elif s == 32:
379 attribs[NSKernAttributeName] = -0.25
381 if not attribs[NSFontAttributeName]:
382 raise Exception('Failed to load font %s' % fontname)
383 return attribs
385 def drawTextAtSize(self, text, s):
386 """Draws text `s` into the current context of size `s`."""
388 textRects = {
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)
400 elif s == 32:
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),
407 attribs)
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)
421 if s == 32:
422 font = NSFont.fontWithName_size_('LucidaSans-Demi', 7.0)
423 if font:
424 attribs[NSFontAttributeName] = font
425 attribs[NSKernAttributeName] = 0
426 self.useOfficeFont = True
427 return attribs
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)
433 return
434 text = NSString.stringWithString_(text)
435 text.drawInRect_withAttributes_( ((0, 1), (31, 11)), attribs)
438 def createIcon(outname, s, bg, textRenderer, text, shorttext=None):
440 # Fill in background
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:
446 text = shorttext
447 textRenderer.drawTextAtSize(text, s)
448 context.done()
450 # Save
451 output.save(outname)
454 def createLinks(icons, target):
455 assert len(icons) > 0
456 for name in icons:
457 icnsName = '%s.icns' % name
458 if os.access(icnsName, os.F_OK):
459 os.remove(icnsName)
460 os.symlink(target, icnsName)
463 TMPFILE = 'make_icons_tmp_%d.png'
464 sizes = [512, 128, 32, 16]
465 def main():
466 srcdir = os.getcwd()
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)
472 if dont_create:
473 print "PyObjC not found, only using a stock icon for document icons."
474 import shutil
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)
478 return
479 # Make us not crash
480 # http://www.cocoabuilder.com/archive/message/cocoa/2008/8/6/214964
481 NSApplicationLoad()
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
497 return
499 # create LARGE and SMALL icons first...
500 for name, t in vimIcons.iteritems():
501 text, size = t
502 if size == LINK: continue
503 print name
504 icnsName = '%s.icns' % name
506 if size == SMALL:
507 currSizes = [128, 32, 16]
508 args = '-128 %s -32 %s -16 %s' % (
509 TMPFILE % 128, TMPFILE % 32, TMPFILE % 16)
510 elif size == LARGE:
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)
516 for s in currSizes:
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__':
529 try:
530 main()
531 finally:
532 for s in sizes:
533 if os.access(TMPFILE % s, os.F_OK):
534 os.remove(TMPFILE % s)