Doc icon generation cleanup

This commit is contained in:
Nico Weber
2009-01-23 23:28:07 +01:00
committed by Bjorn Winckler
parent 1e901456a5
commit 95676cfcd4
4 changed files with 688 additions and 448 deletions
+586
View File
@@ -0,0 +1,586 @@
from Foundation import *
from AppKit import *
import itertools
import math
import operator
import os
from optparse import OptionParser
# Resources
DEFAULT_BACKGROUND = '/System/Library/CoreServices/CoreTypes.bundle/' + \
'Contents/Resources/GenericDocumentIcon.icns' # might require leopard?
# Cache both images and background renderers globally
imageCache = {}
bgCache = {}
# Make us not crash
# http://www.cocoabuilder.com/archive/message/cocoa/2008/8/6/214964
NSApplicationLoad()
class Surface(object):
"""Represents a simple bitmapped image."""
def __init__(self, *p, **kw):
if not 'premultiplyAlpha' in kw:
kw['premultiplyAlpha'] = True
if len(p) == 1 and isinstance(p[0], NSBitmapImageRep):
self.bitmapRep = p[0]
elif len(p) == 2 and isinstance(p[0], int) and isinstance(p[1], int):
format = NSAlphaFirstBitmapFormat
if not kw['premultiplyAlpha']:
format += NSAlphaNonpremultipliedBitmapFormat
self.bitmapRep = NSBitmapImageRep.alloc().initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bitmapFormat_bytesPerRow_bitsPerPixel_(
None, p[0], p[1], 8, 4, True, False, NSDeviceRGBColorSpace,
format, 0, 0)
if not hasattr(self, 'bitmapRep') or not self.bitmapRep:
raise Exception('Failed to create surface: ' + str(p))
def size(self):
return map(int, self.bitmapRep.size()) # cocoa returns floats. cocoa ftw
def data(self):
"""Returns data in ARGB order (on intel, at least)."""
r = self.bitmapRep
if r.bitmapFormat() != (NSAlphaNonpremultipliedBitmapFormat |
NSAlphaFirstBitmapFormat) or \
r.bitsPerPixel() != 32 or \
r.isPlanar() or \
r.samplesPerPixel() != 4:
raise Exception("Unsupported image format")
return self.bitmapRep.bitmapData()
def save(self, filename):
"""Saves image as png file."""
self.bitmapRep.representationUsingType_properties_(NSPNGFileType, None) \
.writeToFile_atomically_(filename, True)
def draw(self):
self.bitmapRep.draw()
def context(self):
# Note: Cocoa only supports contexts with premultiplied alpha
return NSGraphicsContext.graphicsContextWithBitmapImageRep_(self.bitmapRep)
def copy(self):
return Surface(self.bitmapRep.copy())
class Image(object):
"""Represents an image that can consist of several Surfaces."""
def __init__(self, param):
if isinstance(param, str):
self.image = NSImage.alloc().initWithContentsOfFile_(param)
elif isinstance(param, Surface):
self.image = NSImage.alloc().initWithSize_( param.size() )
self.image.addRepresentation_(param.bitmapRep)
if not self.image:
raise Exception('Failed to load image: ' + str(param))
def surfaceOfSize(self, w, h):
"""Returns an ARGB, non-premultiplied surface of size w*h or throws."""
r = None
for rep in self.image.representations():
# Cocoa reports fraction widths for pngs (wtf?!), so use round()
if map(lambda x: int(round(x)), rep.size()) == [w, h]:
r = rep
break
# XXX: Resample in this case? That'd make the program easier to use, but
# can silently create blurry backgrounds. Since this happens with
# the app icon anyways, this might not be a huge deal?
if not r:
raise Exception('Unsupported size %dx%d', w, h)
return Surface(r)
def blend(self):
self.compositeInRect( ((0, 0), self.image.size()) )
def compositeInRect(self, r, mode=NSCompositeSourceOver):
self.image.drawInRect_fromRect_operation_fraction_(r, NSZeroRect,
mode, 1.0)
def sizes(self):
s = set()
for rep in self.image.representations():
s.add(tuple(map(lambda x: int(round(x)), rep.size())))
return s
class Context(object):
# Tiger has only Python2.3, so we can't use __enter__ / __exit__ for this :-(
def __init__(self, surface):
NSGraphicsContext.saveGraphicsState()
c = surface.context()
c.setShouldAntialias_(True);
c.setImageInterpolation_(NSImageInterpolationHigh);
NSGraphicsContext.setCurrentContext_(c)
def done(self):
NSGraphicsContext.restoreGraphicsState()
class SplittableBackground(object):
def __init__(self, unsplitted, shouldSplit=True):
self.unsplitted = unsplitted
self.shouldSplit = shouldSplit
self.ground = {}
self.shadow = {}
def rawGroundAtSize(self, s):
return self.unsplitted.surfaceOfSize(s, s)
def groundAtSize(self, s):
if not self.shouldSplit:
return self.rawGroundAtSize(s)
self._performSplit(s)
return self.ground[s]
def shadowAtSize(self, s):
if not self.shouldSplit:
return None
self._performSplit(s)
return self.shadow[s]
def _performSplit(self, s):
if s in self.ground:
assert s in self.shadow
return
assert s not in self.shadow
ground, shadow = splitGenericDocumentIcon(self.unsplitted, s)
self.ground[s] = ground
self.shadow[s] = shadow
class BackgroundRenderer(object):
def __init__(self, bg, icon=None, r={}):
self.bgRenderer = bg
self.icon = icon
self.cache = {}
self.rect = r
def drawIcon(self, s):
if not self.icon:
return
assert s in [16, 32, 128, 256, 512]
a = list(self.rect[s])
# convert from `flow` coords to cocoa
a[2] = -a[2] # mirror y
w, h = s*a[1], s*a[3]
self.icon.compositeInRect( (((s-w)/2 + a[0], (s-h)/2 + a[2]), (w, h)) )
def drawAtSize(self, s):
if not self.icon:
# No need to split the background if no icons is interleaved -- take
# the faster code path in that case.
self.bgRenderer.rawGroundAtSize(s).draw()
return
self.bgRenderer.groundAtSize(s).draw()
self.drawIcon(s)
if self.bgRenderer.shouldSplit:
# shadow needs to be composited, so it needs to be in an image
Image(self.bgRenderer.shadowAtSize(s)).blend()
def backgroundAtSize(self, s):
if not s in self.cache:
result = Surface(s, s)
context = Context(result)
self.drawAtSize(s)
context.done()
self.cache[s] = result
return self.cache[s]
def splitGenericDocumentIcon(img, s):
"""Takes the generic document icon and splits it into a background and a
shadow layer. For the 32x32 and 16x16 variants, the white pixels of the page
curl are hardcoded into the otherwise transparent shadow layer."""
w, h = s, s
r = img.surfaceOfSize(w, h)
bps = 4*w
data = r.data()
ground = Surface(w, h, premultiplyAlpha=False)
shadow = Surface(w, h, premultiplyAlpha=False)
grounddata = ground.data()
shadowdata = shadow.data()
for y in xrange(h):
for x in xrange(w):
idx = y*bps + 4*x
ia, ir, ig, ib = data[idx:idx + 4]
if ia != chr(255):
# buffer objects don't support slice assignment :-(
grounddata[idx] = ia
grounddata[idx + 1] = ir
grounddata[idx + 2] = ig
grounddata[idx + 3] = ib
shadowdata[idx] = chr(0)
shadowdata[idx + 1] = chr(0)
shadowdata[idx + 2] = chr(0)
shadowdata[idx + 3] = chr(0)
continue
assert ir == ig == ib
grounddata[idx] = chr(255)
grounddata[idx + 1] = chr(255)
grounddata[idx + 2] = chr(255)
grounddata[idx + 3] = chr(255)
shadowdata[idx] = chr(255 - ord(ir))
shadowdata[idx + 1] = chr(0)
shadowdata[idx + 2] = chr(0)
shadowdata[idx + 3] = chr(0)
# Special-case 16x16 and 32x32 cases: Make some pixels on the fold white.
# Ideally, I could make the fold whiteish in all variants, but I can't.
whitePix = { 16: [(10, 2), (10, 3), (11, 3), (10, 4), (11, 4), (12, 4)],
32: [(21, 4), (21, 5), (22, 5), (21, 6), (22, 6), (23, 6)]}
if (w, h) in [(16, 16), (32, 32)]:
for x, y in whitePix[w]:
idx = y*bps + 4*x
shadowdata[idx] = chr(255)
shadowdata[idx + 1] = chr(255)
shadowdata[idx + 2] = chr(255)
shadowdata[idx + 3] = chr(255)
return ground, shadow
class TextRenderer(object):
def __init__(self):
self.cache = {}
def attribsAtSize(self, s):
if s not in self.cache:
self.cache[s] = self._attribsAtSize(s)
return self.cache[s]
def centeredStyle(self):
style = NSMutableParagraphStyle.new()
style.setParagraphStyle_(NSParagraphStyle.defaultParagraphStyle())
style.setAlignment_(NSCenterTextAlignment)
return style
def _attribsAtSize(self, s):
# This looks not exactly like the font on Preview.app's document icons,
# but I believe that's because Preview's icons are drawn by Photoshop,
# and Adobe's font rendering is different from Apple's.
fontname = 'LucidaGrande-Bold'
# Prepare text format
fontsizes = { 512: 72.0, 256: 36.0, 128: 18.0, 32: 7.0, 16: 3.0 }
# http://developer.apple.com/documentation/Cocoa/Conceptual/AttributedStrings/Articles/standardAttributes.html#//apple_ref/doc/uid/TP40004903
attribs = {
NSParagraphStyleAttributeName: self.centeredStyle(),
NSForegroundColorAttributeName: NSColor.colorWithDeviceWhite_alpha_(
0.34, 1),
NSFontAttributeName: NSFont.fontWithName_size_(fontname, fontsizes[s])
}
# tighten font a bit for some sizes
if s in [256, 512]:
attribs[NSKernAttributeName] = -1.0
elif s == 32:
attribs[NSKernAttributeName] = -0.25
if not attribs[NSFontAttributeName]:
raise Exception('Failed to load font %s' % fontname)
return attribs
def drawTextAtSize(self, text, s):
"""Draws text `s` into the current context of size `s`."""
textRects = {
512: ((0, 7), (512, 119)),
128: ((0, 6), (128, 26.5)),
256: ((0, 7), (256, 57)),
16: ((1, 1), (15, 5)),
#32: ((1, 1), (31, 9))
}
attribs = self.attribsAtSize(s)
text = NSString.stringWithString_(text)
if s in [16, 128, 256, 512]:
text.drawInRect_withAttributes_(textRects[s], attribs)
elif s == 32:
# Try to align text on pixel boundary:
attribs = attribs.copy()
attribs[NSParagraphStyleAttributeName] = \
NSParagraphStyle.defaultParagraphStyle()
ts = text.sizeWithAttributes_(attribs)
text.drawAtPoint_withAttributes_( (math.floor((32.0-ts[0])/2) + 0.5, 1.5),
attribs)
class OfficeTextRenderer(TextRenderer):
"""Uses Office's LucidaSans font for 32x32.
This font looks much better for certain strings (e.g. "PDF") but much worse
for most others (e.g. "VIM", "JAVA") -- and office fonts are usually not
installed. Hence, this class is better not used.
"""
def _attribsAtSize(self, s):
self.useOfficeFont = False
attribs = TextRenderer._attribsAtSize(self, s)
if s == 32:
font = NSFont.fontWithName_size_('LucidaSans-Demi', 7.0)
if font:
attribs[NSFontAttributeName] = font
attribs[NSKernAttributeName] = 0
self.useOfficeFont = True
return attribs
def drawTextAtSize(self, text, s):
attribs = self.attribsAtSize(s)
if not self.useOfficeFont or s != 32:
TextRenderer.drawTextAtSize(self, text, s)
return
text = NSString.stringWithString_(text)
text.drawInRect_withAttributes_( ((0, 1), (31, 11)), attribs)
def createIcon(s, bg, textRenderer, text):
# Fill in background
output = bg.backgroundAtSize(s).copy()
# Draw text on top of shadow
context = Context(output)
if s in text and text[s]:
textRenderer.drawTextAtSize(text[s], s)
context.done()
return output
def textDictFromTextList(l):
assert 1 <= len(l) <= 3
if len(l) == 1:
return dict.fromkeys([16, 32, 128, 256, 512], l[0])
elif len(l) == 2:
return dict(zip([16, 32], 2*[l[1]]) + zip((128, 256, 512), 3*[l[0]]))
elif len(l) == 3:
return dict([(16, l[2]), (32, l[1])] + zip((128, 256, 512), 3*[l[0]]))
def saveIcns(icons, icnsName, makeIcns='./makeicns'):
"""Creates an icns file with several variants.
Params:
icons: A dict that contains icon size as key and Surface as value.
Valid keys are 512, 256, 128, 32, 16
icnsname: Name of the output file
"""
# If IconFamily was less buggy, we could wrap it into a python module and
# call it directly, which is about a lot faster. However, IconFamily does not
# work with NSAlphaNonpremultipliedBitmapFormat correctly, so this has to
# wait.
#import IconFamily
#typeDict = {
#16: IconFamily.kSmall32BitData,
#32: IconFamily.kLarge32BitData,
#128: IconFamily.kThumbnail32BitData,
#256: IconFamily.kIconServices256PixelDataARGB,
#512: IconFamily.IconServices512PixelDataARGB,
#}
#maskDict = {
#16: IconFamily.kSmall8BitMask,
#32: IconFamily.kLarge8BitMask,
#128: IconFamily.kThumbnail8BitMask,
#}
#output = IconFamily.IconFamily.iconFamily()
#for s, icon in icons.items():
#output.setIconFamilyElement_fromBitmapImageRep_(typeDict[s], icon.bitmapRep)
#if s in maskDict:
#output.setIconFamilyElement_fromBitmapImageRep_(
#maskDict[s], icon.bitmapRep)
#output.writeToFile_(icnsName)
TMPFILE = 'docerator_tmp_%d.png'
try:
args = []
for s, icon in icons.items():
assert s in [512, 256, 128, 32, 16]
assert icon.size() == [s, s]
icon.save(TMPFILE % s)
args.append('-%d %s' % (s, TMPFILE % s))
return \
os.system('%s %s -out %s' % (makeIcns, ' '.join(args), icnsName)) == 0
finally:
for s in icons:
if os.access(TMPFILE % s, os.F_OK):
os.remove(TMPFILE % s)
def getOutname(options):
def saneBasename(p):
""" '/my/path/to/file.txt' -> 'file' """
return os.path.splitext(os.path.basename(p))[0]
textPart = 'Generic'
if options.text:
textPart = options.text.split(',')[0]
if options.appicon:
base = saneBasename(options.appicon)
else:
base = saneBasename(getBgName(options))
return '%s-%s.icns' % (base, textPart)
def makedocicon(**kwargs):
options, _ = getopts().parse_args([]) # get default options
for k in kwargs:
setattr(options, k, kwargs[k])
makedocicon_opts(options)
def cachedImage(filename):
absPath = os.path.abspath(filename)
if not absPath in imageCache:
imageCache[absPath] = Image(absPath)
return imageCache[absPath]
def cachedBackground(img, split):
key = (img, split)
if not key in bgCache:
bgCache[key] = SplittableBackground(img, shouldSplit=split)
return bgCache[key]
# taken from running flow on preview
defaultRects = {
16: (-0.30890000000000001, 0.4919, -1.2968, 0.4743),
32: (-0.27810000000000001,
0.58930000000000005,
-2.2292999999999998,
0.57140000000000002),
128: (1.1774, 0.56820000000000004, -0.8246, 0.56799999999999995),
256: (0.5917, 0.56489999999999996, -1.8994, 0.56499999999999995),
512: (0.68700000000000006,
0.56530000000000002,
-4.2813999999999997,
0.56540000000000001)
}
def getBgName(options):
if not hasattr(options, 'background') \
or options.background in ['default-split', 'default-unsplit']:
return DEFAULT_BACKGROUND
else:
return options.background
def makedocicon_opts(options):
textRenderer = TextRenderer()
# Prepare input images
bgIcon = None
splitBackground = options.background == 'default-split'
bgIcon = cachedImage(getBgName(options))
testIcon = None
if options.appicon:
testIcon = cachedImage(options.appicon)
if hasattr(options, 'sizes') and options.sizes:
if isinstance(options.sizes, list):
sizes = options.sizes
else:
sizes = map(int, options.sizes.split(','))
else:
sizes = bgIcon.sizes()
if testIcon:
sizes = sizes.intersection(testIcon.sizes())
sizes = sorted(map(operator.itemgetter(0), sizes))
if not hasattr(options, 'rects') or not options.rects:
rects = defaultRects.copy()
rects[16] = [ 0.0000, 0.5000, -1.0000, 0.5000] # manually, better
else:
rects = options.rects
bg = cachedBackground(bgIcon, splitBackground)
bgRenderer = BackgroundRenderer(bg, testIcon, rects)
testtext = textDictFromTextList(options.text.split(','))
icons = dict([(s, createIcon(s, bgRenderer, textRenderer, testtext))
for s in sizes])
if options.debug:
for s, icon in icons.iteritems():
icon.save(options.debug % s)
if hasattr(options, 'outname') and options.outname:
outname = options.outname
else:
outname = getOutname(options)
if saveIcns(icons, outname, options.makeicns):
print 'Wrote', outname
else:
print 'Failed to write %s. Make sure makeicns is in your path.' % outname
def makedocicons(**kwargs):
options, _ = getopts().parse_args([]) # get default options
for k in kwargs:
setattr(options, k, kwargs[k])
makedocicons_opts(options)
def makedocicons_opts(options):
if not hasattr(options, 'text') or not options.text:
options.text = ['']
texts = options.text
for text in texts:
options.text = text
makedocicon_opts(options)
def getopts():
parser = OptionParser(usage='%prog [options]', version='%prog 1.01')
parser.add_option('--background', '--bg', default='default-split',
help='Used as background (special values: "default-split" (default), ' \
'"default-unsplit").')
parser.add_option('--appicon', help='App icon, defaults to no icon.')
parser.add_option('--text', help='Text on icon. Defaults to empty. '
'More than one text is supported, multiple docicons are generated in '
'that case.', action='append')
parser.add_option('--sizes', help='Sizes of icons. ' \
'Defaults to all sizes available in input appicon. Example: "512,128,16"')
# XXX(Nico): This has to go
parser.add_option('--debug', help='If set, write out pngs for all variants.' \
' This needs to look like "debug%d.png".')
# XXX(Nico): This has to go once IconFamily is less buggy and can be used
# directly
parser.add_option('--makeicns', help='Path to makeicns binary',
default='./makeicns')
return parser
def main():
options, args = getopts().parse_args()
makedocicons_opts(options)
if __name__ == '__main__':
main()
+65
View File
@@ -0,0 +1,65 @@
import docerator
import unittest
class TextDictFromTextListTest(unittest.TestCase):
def testBasic(self):
self.assertEquals({16: 'a', 32: 'a', 128: 'a', 256: 'a', 512: 'a'},
docerator.textDictFromTextList(['a']))
self.assertEquals({16: 'b', 32: 'b', 128: 'a', 256: 'a', 512: 'a'},
docerator.textDictFromTextList(['a', 'b']))
self.assertEquals({16: 'c', 32: 'b', 128: 'a', 256: 'a', 512: 'a'},
docerator.textDictFromTextList(['a', 'b', 'c']))
class OutnameTest(unittest.TestCase):
class FakeOptions(object):
def __init__(self, **kwargs):
for k in kwargs:
self.__setattr__(k, kwargs[k])
def testBasic(self):
options = OutnameTest.FakeOptions(
appicon = '/Applications/iTunes.app/Contents/Resources/iTunes.icns',
text='MP3')
self.assertEquals('iTunes-MP3.icns', docerator.getOutname(options))
def testTextList(self):
options = OutnameTest.FakeOptions(
appicon = '/Applications/iTunes.app/Contents/Resources/iTunes.icns',
text='PYTHON,PY')
self.assertEquals('iTunes-PYTHON.icns',
docerator.getOutname(options))
def testEmptyText(self):
options = OutnameTest.FakeOptions(
appicon = '/Applications/iTunes.app/Contents/Resources/iTunes.icns',
text='')
self.assertEquals('iTunes-Generic.icns', docerator.getOutname(options))
options = OutnameTest.FakeOptions(
appicon = '/Applications/iTunes.app/Contents/Resources/iTunes.icns',
text=None)
self.assertEquals('iTunes-Generic.icns',
docerator.getOutname(options))
def testEmptyIcon(self):
options = OutnameTest.FakeOptions(appicon=None, text='MP3')
self.assertEquals('GenericDocumentIcon-MP3.icns',
docerator.getOutname(options))
options = OutnameTest.FakeOptions(appicon=None, text='')
self.assertEquals('GenericDocumentIcon-Generic.icns',
docerator.getOutname(options))
options = OutnameTest.FakeOptions(appicon=None, text='',
background='/Applications/Bla/bgicon.icns')
self.assertEquals('bgicon-Generic.icns',
docerator.getOutname(options))
# XXX(Nico): Look at the doctest module.
if __name__ == '__main__':
unittest.main()
+34 -446
View File
@@ -5,27 +5,34 @@
# http://www.macresearch.org/cocoa-scientists-part-xx-python-scriptersmeet-cocoa
try:
from Foundation import *
from AppKit import *
import docerator
dont_create = False
except:
dont_create = True # most likely because we're on tiger
NSCompositeSourceOver = 2
import math
import os
import sys
# icon types
LARGE = 0 # 512, 128, 32, 16; about 96kB
SMALL = 1 # 128, 32, 16; about 36kB
LINK = 2 # Create link to generic icon; 4kB (== smallest block size on HFS+)
# path to makeicns binary
iconsizes = {
SMALL: [128, 32, 16],
LARGE: [512, 128, 32, 16],
}
# Resources
MAKEICNS = 'makeicns/makeicns'
APPICON = 'vim-noshadow-512.png'
DEFAULT_BACKGROUND = '/System/Library/CoreServices/CoreTypes.bundle/' + \
'Contents/Resources/GenericDocumentIcon.icns'
# List of icons to create
# XXX: 32x32 variants only support 3-4 letters of text
GENERIC_ICON_NAME = 'MacVim-generic'
vimIcons = {
GENERIC_ICON_NAME: [u'', LARGE],
@@ -42,13 +49,13 @@ vimIcons = {
'MacVim-html': [u'HTML', SMALL],
'MacVim-xml': [u'XML', SMALL],
'MacVim-js': [u'JS', SMALL],
'MacVim-perl': [u'PERL', SMALL],
'MacVim-py': [u'PYTHON', SMALL],
'MacVim-perl': [u'PERL,PL', SMALL],
'MacVim-py': [u'PYTHON,PY', SMALL],
'MacVim-php': [u'PHP', SMALL],
'MacVim-rb': [u'RUBY', SMALL],
'MacVim-rb': [u'RUBY,RB', SMALL],
'MacVim-bash': [u'SH', SMALL],
'MacVim-patch': [u'DIFF', SMALL],
'MacVim-applescript': [u'\uf8ffSCPT', SMALL], # apple sign
'MacVim-applescript': [u'\uf8ffSCPT,\uf8ffS', SMALL], # apple sign
'MacVim-as': [u'FLASH', LINK],
'MacVim-asp': [u'ASP', LINK],
'MacVim-bib': [u'BIB', LINK],
@@ -61,8 +68,8 @@ vimIcons = {
'MacVim-css': [u'CSS', SMALL],
'MacVim-dtd': [u'DTD', LINK],
'MacVim-dylan': [u'DYLAN', LINK],
'MacVim-erl': [u'ERLANG', SMALL],
'MacVim-fscript': [u'FSCPT', SMALL],
'MacVim-erl': [u'ERLANG,ERL', SMALL],
'MacVim-fscript': [u'FSCPT,FSCR', SMALL],
'MacVim-hs': [u'HS', SMALL],
'MacVim-inc': [u'INC', LINK],
'MacVim-ics': [u'ICS', SMALL],
@@ -76,380 +83,16 @@ vimIcons = {
'MacVim-wiki': [u'WIKI', SMALL],
'MacVim-ps': [u'PS', LINK],
#'MacVim-plist': [u'PLIST', SMALL],
'MacVim-sch': [u'SCHEME', SMALL],
'MacVim-sch': [u'SCHEME,SCM', SMALL],
'MacVim-sql': [u'SQL', SMALL],
'MacVim-tcl': [u'TCL', SMALL],
'MacVim-xsl': [u'XSL', LINK],
'MacVim-vcf': [u'VCARD', SMALL],
'MacVim-vb': [u'VBASIC', LINK],
'MacVim-vcf': [u'VCARD,VCF', SMALL],
'MacVim-vb': [u'VBASIC,VB', LINK],
'MacVim-yaml': [u'YAML', SMALL],
'MacVim-gtd': [u'GTD', LINK],
}
shorttext = {
u'MacVim-py': u'PY',
u'MacVim-rb': u'RB',
u'MacVim-perl': u'PL',
u'MacVim-applescript': u'\uf8ffS',
u'MacVim-erl': u'ERL',
u'MacVim-fscript': u'FSCR',
u'MacVim-sch': u'SCM',
u'MacVim-vcf': u'VCF',
u'MacVim-vb': u'VB',
}
# Resources
BACKGROUND = '/System/Library/CoreServices/CoreTypes.bundle/' + \
'Contents/Resources/GenericDocumentIcon.icns' # might require leopard?
APPICON = 'vim-noshadow-512.png'
#APPICON = 'vim-noshadow-no-v-512.png'
class Surface(object):
"""Represents a simple bitmapped image."""
def __init__(self, *p, **kw):
if not 'premultiplyAlpha' in kw:
kw['premultiplyAlpha'] = True
if len(p) == 1 and isinstance(p[0], NSBitmapImageRep):
self.bitmapRep = p[0]
elif len(p) == 2 and isinstance(p[0], int) and isinstance(p[1], int):
format = NSAlphaFirstBitmapFormat
if not kw['premultiplyAlpha']:
format += NSAlphaNonpremultipliedBitmapFormat
self.bitmapRep = NSBitmapImageRep.alloc().initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bitmapFormat_bytesPerRow_bitsPerPixel_(
None, p[0], p[1], 8, 4, True, False, NSDeviceRGBColorSpace,
format, 0, 0)
if not hasattr(self, 'bitmapRep') or not self.bitmapRep:
raise Exception('Failed to create surface: ' + str(p))
def size(self):
return map(int, self.bitmapRep.size()) # cocoa returns floats. cocoa ftw
def data(self):
"""Returns data in ARGB order (on intel, at least)."""
r = self.bitmapRep
if r.bitmapFormat() != (NSAlphaNonpremultipliedBitmapFormat |
NSAlphaFirstBitmapFormat) or \
r.bitsPerPixel() != 32 or \
r.isPlanar() or \
r.samplesPerPixel() != 4:
raise Exception("Unsupported image format")
return self.bitmapRep.bitmapData()
def save(self, filename):
"""Saves image as png file."""
self.bitmapRep.representationUsingType_properties_(NSPNGFileType, None) \
.writeToFile_atomically_(filename, True)
def draw(self):
self.bitmapRep.draw()
def context(self):
# Note: Cocoa only supports contexts with premultiplied alpha
return NSGraphicsContext.graphicsContextWithBitmapImageRep_(self.bitmapRep)
def copy(self):
return Surface(self.bitmapRep.copy())
class Image(object):
"""Represents an image that can consist of several Surfaces."""
def __init__(self, param):
if isinstance(param, str):
self.image = NSImage.alloc().initWithContentsOfFile_(param)
elif isinstance(param, Surface):
self.image = NSImage.alloc().initWithSize_( param.size() )
self.image.addRepresentation_(param.bitmapRep)
if not self.image:
raise Exception('Failed to load image: ' + str(filename))
def surfaceOfSize(self, w, h):
"""Returns an ARGB, non-premultiplied surface of size w*h or throws."""
r = None
for rep in self.image.representations():
if map(int, rep.size()) == [w, h]:
r = rep
break
if not r:
raise Exception('Unsupported size %dx%d', w, h)
return Surface(r)
def blend(self):
self.compositeInRect( ((0, 0), self.image.size()) )
def compositeInRect(self, r, mode=NSCompositeSourceOver):
self.image.drawInRect_fromRect_operation_fraction_(r, NSZeroRect,
mode, 1.0)
class Context(object):
# Tiger has only Python2.3, so we can't use __enter__ / __exit__ for this :-(
def __init__(self, surface):
NSGraphicsContext.saveGraphicsState()
c = surface.context()
c.setShouldAntialias_(True);
c.setImageInterpolation_(NSImageInterpolationHigh);
NSGraphicsContext.setCurrentContext_(c)
def done(self):
NSGraphicsContext.restoreGraphicsState()
class SplittableBackground(object):
def __init__(self, unsplitted, shouldSplit=True):
self.unsplitted = unsplitted
self.shouldSplit = shouldSplit
self.ground = {}
self.shadow = {}
def rawGroundAtSize(self, s):
return self.unsplitted.surfaceOfSize(s, s)
def groundAtSize(self, s):
if not self.shouldSplit:
return self.rawGroundAtSize(s)
self._performSplit(s)
return self.ground[s]
def shadowAtSize(self, s):
if not self.shouldSplit:
return None
self._performSplit(s)
return self.shadow[s]
def _performSplit(self, s):
if s in self.ground:
assert s in self.shadow
return
assert s not in self.shadow
ground, shadow = splitGenericDocumentIcon(self.unsplitted, s)
self.ground[s] = ground
self.shadow[s] = shadow
class BackgroundRenderer(object):
def __init__(self, bg, icon=None):
self.bgRenderer = bg
self.icon = icon
self.cache = {}
def drawIcon(self, s):
if not self.icon:
return
# found by flow program, better than anything i came up with manually before
# (except for the 16x16 variant :-( )
transforms = {
512: [ 0.7049, 0.5653, -4.2432, 0.5656],
256: [ 0.5690, 0.5658, -1.9331, 0.5656],
128: [ 1.1461, 0.5684, -0.8482, 0.5681],
32: [-0.2682, 0.5895, -2.2130, 0.5701], # intensity
#32: [-0.2731, 0.5898, -2.2262, 0.5729], # rgb (no rmse difference)
#16: [-0.3033, 0.4909, -1.3235, 0.4790], # program, intensity
#16: [-0.3087, 0.4920, -1.2990, 0.4750], # program, rgb mode
16: [ 0.0000, 0.5000, -1.0000, 0.5000], # manually, better
}
assert s in [16, 32, 128, 256, 512]
a = transforms[s]
# convert from `flow` coords to cocoa
a[2] = -a[2] # mirror y
w, h = s*a[1], s*a[3]
self.icon.compositeInRect( (((s-w)/2 + a[0], (s-h)/2 + a[2]), (w, h)) )
def drawAtSize(self, s):
self.bgRenderer.groundAtSize(s).draw()
self.drawIcon(s)
if self.bgRenderer.shouldSplit:
# shadow needs to be composited, so it needs to be in an image
Image(self.bgRenderer.shadowAtSize(s)).blend()
def backgroundAtSize(self, s):
if not s in self.cache:
result = Surface(s, s)
context = Context(result)
self.drawAtSize(s)
context.done()
self.cache[s] = result
return self.cache[s]
def splitGenericDocumentIcon(img, s):
"""Takes the generic document icon and splits it into a background and a
shadow layer. For the 32x32 and 16x16 variants, the white pixels of the page
curl are hardcoded into the otherwise transparent shadow layer."""
w, h = s, s
r = img.surfaceOfSize(w, h)
bps = 4*w
data = r.data()
ground = Surface(w, h, premultiplyAlpha=False)
shadow = Surface(w, h, premultiplyAlpha=False)
grounddata = ground.data()
shadowdata = shadow.data()
for y in xrange(h):
for x in xrange(w):
idx = y*bps + 4*x
ia, ir, ig, ib = data[idx:idx + 4]
if ia != chr(255):
# buffer objects don't support slice assignment :-(
grounddata[idx] = ia
grounddata[idx + 1] = ir
grounddata[idx + 2] = ig
grounddata[idx + 3] = ib
shadowdata[idx] = chr(0)
shadowdata[idx + 1] = chr(0)
shadowdata[idx + 2] = chr(0)
shadowdata[idx + 3] = chr(0)
continue
assert ir == ig == ib
grounddata[idx] = chr(255)
grounddata[idx + 1] = chr(255)
grounddata[idx + 2] = chr(255)
grounddata[idx + 3] = chr(255)
shadowdata[idx] = chr(255 - ord(ir))
shadowdata[idx + 1] = chr(0)
shadowdata[idx + 2] = chr(0)
shadowdata[idx + 3] = chr(0)
# Special-case 16x16 and 32x32 cases: Make some pixels on the fold white.
# Ideally, I could make the fold whiteish in all variants, but I can't.
whitePix = { 16: [(10, 2), (10, 3), (11, 3), (10, 4), (11, 4), (12, 4)],
32: [(21, 4), (21, 5), (22, 5), (21, 6), (22, 6), (23, 6)]}
if (w, h) in [(16, 16), (32, 32)]:
for x, y in whitePix[w]:
idx = y*bps + 4*x
shadowdata[idx] = chr(255)
shadowdata[idx + 1] = chr(255)
shadowdata[idx + 2] = chr(255)
shadowdata[idx + 3] = chr(255)
return ground, shadow
class TextRenderer(object):
def __init__(self):
self.cache = {}
def attribsAtSize(self, s):
if s not in self.cache:
self.cache[s] = self._attribsAtSize(s)
return self.cache[s]
def centeredStyle(self):
style = NSMutableParagraphStyle.new()
style.setParagraphStyle_(NSParagraphStyle.defaultParagraphStyle())
style.setAlignment_(NSCenterTextAlignment)
return style
def _attribsAtSize(self, s):
# This looks not exactly like the font on Preview.app's document icons,
# but I believe that's because Preview's icons are drawn by Photoshop,
# and Adobe's font rendering is different from Apple's.
fontname = 'LucidaGrande-Bold'
# Prepare text format
fontsizes = { 512: 72.0, 256: 36.0, 128: 18.0, 32: 7.0, 16: 3.0 }
# http://developer.apple.com/documentation/Cocoa/Conceptual/AttributedStrings/Articles/standardAttributes.html#//apple_ref/doc/uid/TP40004903
attribs = {
NSParagraphStyleAttributeName: self.centeredStyle(),
NSForegroundColorAttributeName: NSColor.colorWithDeviceWhite_alpha_(
0.34, 1),
NSFontAttributeName: NSFont.fontWithName_size_(fontname, fontsizes[s])
}
# tighten font a bit for some sizes
if s in [256, 512]:
attribs[NSKernAttributeName] = -1.0
elif s == 32:
attribs[NSKernAttributeName] = -0.25
if not attribs[NSFontAttributeName]:
raise Exception('Failed to load font %s' % fontname)
return attribs
def drawTextAtSize(self, text, s):
"""Draws text `s` into the current context of size `s`."""
textRects = {
512: ((0, 7), (512, 119)),
128: ((0, 6), (128, 26.5)),
256: ((0, 7), (256, 57)),
16: ((1, 1), (15, 5)),
#32: ((1, 1), (31, 9))
}
attribs = self.attribsAtSize(s)
text = NSString.stringWithString_(text)
if s in [16, 128, 256, 512]:
text.drawInRect_withAttributes_(textRects[s], attribs)
elif s == 32:
# Try to align text on pixel boundary:
attribs = attribs.copy()
attribs[NSParagraphStyleAttributeName] = \
NSParagraphStyle.defaultParagraphStyle()
ts = text.sizeWithAttributes_(attribs)
text.drawAtPoint_withAttributes_( (math.floor((32.0-ts[0])/2) + 0.5, 1.5),
attribs)
class OfficeTextRenderer(TextRenderer):
"""Uses Office's LucidaSans font for 32x32.
This font looks much better for certain strings (e.g. "PDF") but much worse
for most others (e.g. "VIM", "JAVA") -- and office fonts are usually not
installed. Hence, this class is better not used.
"""
def _attribsAtSize(self, s):
self.useOfficeFont = False
attribs = TextRenderer._attribsAtSize(self, s)
if s == 32:
font = NSFont.fontWithName_size_('LucidaSans-Demi', 7.0)
if font:
attribs[NSFontAttributeName] = font
attribs[NSKernAttributeName] = 0
self.useOfficeFont = True
return attribs
def drawTextAtSize(self, text, s):
attribs = self.attribsAtSize(s)
if not self.useOfficeFont or s != 32:
TextRenderer.drawTextAtSize(self, text, s)
return
text = NSString.stringWithString_(text)
text.drawInRect_withAttributes_( ((0, 1), (31, 11)), attribs)
def createIcon(s, bg, textRenderer, text, shorttext=None):
# Fill in background
output = bg.backgroundAtSize(s).copy()
# Draw text on top of shadow
context = Context(output)
if s in [16, 32] and shorttext:
text = shorttext
textRenderer.drawTextAtSize(text, s)
context.done()
return output
def createLinks(icons, target):
assert len(icons) > 0
@@ -460,80 +103,30 @@ def createLinks(icons, target):
os.symlink(target, icnsName)
def saveIcns(icons, icnsName, makeIcns='./makeicns'):
"""Creates an icns file with several variants.
Params:
icons: A dict that contains icon size as key and Surface as value.
Valid keys are 512, 256, 128, 32, 16
icnsname: Name of the output file
"""
# XXX: Figure out how to call ObjC directly from Python, then this doesn't
# need to call a different process and do lots of i/o.
args = []
for s, icon in icons.items():
assert s in [512, 256, 128, 32, 16]
assert icon.size() == [s, s]
icon.save(TMPFILE % s)
args.append('-%d %s' % (s, TMPFILE % s))
os.system('%s %s -out %s' % (makeIcns, ' '.join(args), icnsName))
TMPFILE = 'make_icons_tmp_%d.png'
sizes = [512, 128, 32, 16]
def main():
if dont_create:
print "PyObjC not found, only using a stock icon for document icons."
# Can't use the constants from docerator in this case
import shutil
shutil.copyfile(DEFAULT_BACKGROUND, '%s.icns' % GENERIC_ICON_NAME)
createLinks([name for name in vimIcons if name != GENERIC_ICON_NAME],
'%s.icns' % GENERIC_ICON_NAME)
return
srcdir = os.getcwd()
if len(sys.argv) > 1:
os.chdir(sys.argv[1])
appIcon = os.path.join(srcdir, APPICON)
makeIcns = os.path.join(srcdir, MAKEICNS)
if dont_create:
print "PyObjC not found, only using a stock icon for document icons."
import shutil
shutil.copyfile(BACKGROUND, '%s.icns' % GENERIC_ICON_NAME)
createLinks([name for name in vimIcons if name != GENERIC_ICON_NAME],
'%s.icns' % GENERIC_ICON_NAME)
return
# Make us not crash
# http://www.cocoabuilder.com/archive/message/cocoa/2008/8/6/214964
NSApplicationLoad()
textRenderer = TextRenderer()
#textRenderer = OfficeTextRenderer()
# Prepare input images
bgIcon = Image(BACKGROUND)
#bg = SplittableBackground(bgIcon, shouldSplit=False)
bg = SplittableBackground(bgIcon, shouldSplit=True)
icon = Image(appIcon)
bgRenderer = BackgroundRenderer(bg, icon)
if not os.access(makeIcns, os.X_OK):
print 'Cannot find makeicns at %s', makeIcns
return
# create LARGE and SMALL icons first...
for name, t in vimIcons.iteritems():
text, size = t
if size == LINK: continue
print name
icnsName = '%s.icns' % name
if size == SMALL:
currSizes = [128, 32, 16]
elif size == LARGE:
currSizes = [512, 128, 32, 16]
st = shorttext.get(name)
icons = [(s, createIcon(s, bgRenderer, textRenderer, text, shorttext=st))
for s in currSizes]
icons = dict(icons)
saveIcns(icons, '%s.icns' % name, makeIcns)
del text, size, name, t
docerator.makedocicon(outname='%s.icns' % name, appicon=appIcon, text=text,
sizes=iconsizes[size], makeicns=makeIcns)
# ...create links later (to make sure the link targets exist)
createLinks([name for (name, t) in vimIcons.items() if t[1] == LINK],
@@ -541,9 +134,4 @@ def main():
if __name__ == '__main__':
try:
main()
finally:
for s in sizes:
if os.access(TMPFILE % s, os.F_OK):
os.remove(TMPFILE % s)
+3 -2
View File
@@ -54,9 +54,10 @@ void usage() {
NSBitmapImageRep* getBitmapImageRepOfSize(NSImage* img, int size) {
// Don't resample if it's not necessary
// XXX: Seems as if this creates problems in some situations, disable this
// for now.
#if 0
// IconFamily does not work correctly with
// NSAlphaNonpremultipliedBitmapFormat images, so this has to stay disabled
// until IconFamily is fixed (if ever).
NSEnumerator* e = [[img representations] objectEnumerator];
NSImageRep* ir;
while ((ir = [e nextObject])) {