"""
Copyright @ 2013 Mathias Westerdahl
"""
import sys, os, logging
from optparse import OptionParser
import itertools
try:
from PIL import Image
except ImportError:
import Image
def _patch_numpy():
""" When chasing optimizations, numpy's excessive imports actually amounted to
times that were not insignificant compared to the other functions in the font creator.
"""
class FooModule(object):
__all__ = []
class FooObject(object):
def __getattr__(self,*k,**kw): pass
# Timings went from 0.675 to 0.609 (~9.5%)
# Saved 0.01s out of 0.067s by not including all the documentation
sys.modules['numpy.add_newdocs'] = True
# Saved 0.002s out of 0,066s
#sys.modules['numpy.fft'] = True # needed by numpy.numarray
# Saved 0.016s from 0.658s
sys.modules['numpy.polynomial'] = True
# Saved 0.01s from 0.064s
sys.modules['numpy.ma'] = True
# Saved 0,01s from 0.63s
#sys.modules['numpy.random'] = True # needed by numpy.numarray
# Save 0.016s from 0.625s
sys.modules['numpy.testing'] = FooModule()
sys.modules['numpy.testing'].__dict__['Tester'] = FooObject
sys.modules['numpy.core.records'] = FooModule()
sys.modules['numpy.core.financial'] = FooModule()
sys.modules['numpy.lib.triu'] = FooModule()
_patch_numpy()
import numpy as np
import freetype as ft
import fontutils as fu
from fontinfo import SFontInfo
import fonteffects
"""
for n in sorted(sys.modules.keys()):
if 'numpy' in n:
print n, sys.modules[n]
"""
"""
http://www.freetype.org/freetype2/docs/reference/ft2-index.html
http://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html
http://code.google.com/p/freetype-py/source/browse/trunk/
http://dunnbypaul.net/blends/
http://www.catenary.com/howto/emboss.html
http://www.scipy.org/Numpy_Example_List#head-6a18109f2befe2d8e7bdc599e1834a8b3453f5ef
http://www.russellcottrell.com/greek/utilities/UnicodeRanges.htm
"""
class LogStream(object):
def __init__(self, path):
self.log = open(path, 'wb')
self.stdout = sys.stdout
def __del__(self):
self.log.close()
self.log = None
def write(self, s):
self.stdout.write(s)
self.log.write(s)
def flush(self):
self.stdout.flush()
self.log.flush()
def _apply_layer(info, glyph, layer, previmage, glyphimage):
# The max y should be the same for all characters in the same row
# This is necessary for having the same "space" during calculations
maxsize = info.maxsize
starty = info.maxbearingY - glyph.bearingY
layer.set_info( glyph, info )
image = layer.apply_color( 0, starty, glyph.bitmap.shape, maxsize, glyphimage, previmage )
image = layer.apply_effects( image )
image = layer.apply_mask( image )
image = layer.apply_blend( glyphimage, previmage, image )
assert image != None
return image
def _apply_layers(info):
#bbox = np.array( [0, 0] )
width = 0
top = 0
bottom = 0
max_bearing_y = 0
for glyph in info.glyphs:
if glyph.bitmap is None:
continue
width = max(width, glyph.bitmap.shape[0])
top = max(top, glyph.bearingY)
bottom = min(bottom, glyph.bearingY - glyph.bitmap.shape[1])
#bbox = np.maximum( bbox, glyph.bitmap.shape )
max_bearing_y = max(max_bearing_y, glyph.bearingY)
bbox = np.array( [width, top - bottom] )
bbox[0] += info.extrapadding[0] + info.extrapadding[2]
bbox[1] += info.extrapadding[1] + info.extrapadding[3]
info.maxsize = bbox
info.maxbearingY = max_bearing_y
elements = []
for layer in info.layers:
elements.append(layer)
elements.append(layer.color)
for element in elements + info.posteffects:
if hasattr(element, 'set_dimensions'):
element.set_dimensions(info.maxsize[0], info.maxsize[1])
for glyph in info.glyphs:
if glyph.bitmap is None:
continue
glyphimage = glyph.bitmap
glyphimage.flags.writeable = False
# ????
fonteffects.DefaultMask.idx = np.where(glyphimage == 0)
previmage = np.dstack((glyphimage, glyphimage, glyphimage, glyphimage))
previmage.flags.writeable = False
previmage = _apply_layer(info, glyph, info.layers[0], previmage, glyphimage)
previmage.flags.writeable = False
for layer in info.layers[1:]:
previmage = _apply_layer(info, glyph, layer, previmage, glyphimage)
previmage.flags.writeable = False
for effect in info.posteffects:
previmage = effect.apply(info, glyph, previmage)
previmage.flags.writeable = False
bgimage = np.ones( previmage.shape, float) * (info.bgcolor[0], info.bgcolor[1], info.bgcolor[2], 0.0)
bgimage.flags.writeable = False
previmage = fu.alpha_blend(bgimage, previmage)
previmage.flags.writeable = False
glyph.bitmap = previmage
def _convert_int_to_unicode(char):
""" Converts an UTF-8 encoded integer and converts it back to a unicode character
"""
if char <= 255:
return chr(char)
s = ''
if char & 0xFF000000:
s += '%x' % (char >> 24 & 0xFF)
if char & 0x00FF0000:
s += '%x' % (char >> 16 & 0xFF)
if char & 0x0000FF00:
s += '%x' % (char >> 8 & 0xFF)
if char & 0x000000FF:
s += '%x' % (char >> 0 & 0xFF)
return s.decode('hex').decode('utf-8')
def _get_extra_padding(info):
zero = (0,0,0,0)
extrapadding = (0,0,0,0)
for layer in info.layers:
layerpadding = layer.padding
extrapadding = np.maximum(extrapadding, layerpadding)
for effect in info.posteffects:
extrapadding = np.add(extrapadding, getattr(effect, 'padding', zero))
return extrapadding
[docs]class Glyph(object):
""" Holds the glyph info.
For a detailed description of the glyph metrics, see http://www.freetype.org/freetype2/docs/tutorial/step2.html
:param utf8: The UTF-8 character code
:param unicode: The unicode letter
:param bitmap: The numpy array of shape (x, y, 4)
:param bitmapbox: The box in the texture where the glyph is printed.
A 4-tuple: (left, top, width, height) *(In pixels)*
:param bearingX: The distance from the cursor to the leftmost border of the bitmap
:param bearingY: The distance from the baseline to the topmost border of the bitmap
:param advance: The distance used to increment the cursor
"""
def __init__(self, utf8, unicode):
self.utf8 = utf8
self.unicode = unicode
self.bitmap = None
self.bitmapbox = None
self.bearingX = 0
self.bearingY = 0
self.advance = 0
def _get_glyph_info(options, info, face):
logging.debug("Fetching glyph info")
face.set_char_size( width=0, height=info.size*64, hres=info.dpi, vres=info.dpi )
# find out the needed extra padding on each side of each glyph
info.extrapadding = _get_extra_padding(info)
info.ascender = (face.size.ascender >> 6) + info.extrapadding[1]
info.descender = (face.size.descender >> 6) - info.extrapadding[3]
info.fontsize = face.height >> 6
flags = ft.LOAD_NO_BITMAP
all_letters = set()
info.glyphs = []
for c in info.letters:
if c in all_letters:
continue
if options.writetext and not unichr(c) in options.writetext:
continue
all_letters.add(c)
unicode = _convert_int_to_unicode(c)
face.load_char( unicode, flags )
metrics = face.glyph.contents.metrics
glyph = Glyph(c, unicode)
glyph.bearingX = (metrics.horiBearingX >> 6) + info.extrapadding[0]
glyph.bearingY = (metrics.horiBearingY >> 6) + info.internalpadding[1] + info.extrapadding[1]
glyph.advance = (metrics.horiAdvance >> 6) + info.extrapadding[2]
info.glyphs.append( glyph )
def render(options, info, face):
found = False
for charmap in face.charmaps:
found = found or charmap.encoding == ft.ENCODING_UNICODE
if not found:
logging.warning("The font %s doesn't seem to support unicode!? Continuing anyway")
if options.writetext:
info.letters = [c for c in info.letters if unichr(c) in options.writetext]
logging.debug("Rendering %d characters" % len(info.letters))
# find out the needed extra padding on each side of each glyph
info.extrapadding = _get_extra_padding(info)
info.face = face
flags = ft.LOAD_RENDER
antialias = getattr(info, 'antialias', 'normal')
if antialias == 'none':
flags |= ft.LOAD_TARGET_MONO
elif antialias == 'light':
flags |= ft.LOAD_TARGET_LIGHT
elif antialias == 'normal':
flags |= ft.LOAD_TARGET_NORMAL
logging.debug("RENDERING %s", unicode([g.unicode for g in info.glyphs]))
assert len(info.glyphs) > 0, "No glyphs to process!"
# since the layers might set this, we set it back
face.set_char_size( width=0, height=info.size * 64, hres=info.dpi, vres=info.dpi )
for glyph in info.glyphs:
face.load_char( glyph.unicode, flags )
if face.glyph.contents.bitmap.rows:
glyph.bitmap = fu.make_array_from_bitmap(face.glyph.contents.bitmap)
shape = glyph.bitmap.shape
glyph.bitmap = fu.pad_bitmap(glyph.bitmap, info.extrapadding[0], info.extrapadding[1], info.extrapadding[2], info.extrapadding[3], 0.0)
if info.useadvanceaswidth:
# Pad the bitmap with the bearing width on each side
# This is useful for file formats that doesn't carry all the glyph info into it
padleft = glyph.bearingX
padright = glyph.advance - shape[0] - padleft
glyph.bitmap = fu.pad_bitmap(glyph.bitmap, 0, 0, padright, 0, 0.0)
if antialias != 'none':
glyph.bitmap /= 255.0
else:
logging.debug("char missing bitmap %X '%s'" % (glyph.utf8, glyph.unicode) )
# Apply all layers on all the tiny bitmaps
_apply_layers(info)
max_bearing_y = 0 # the maximum extent above the baseline
min_bearing_y = 0 # the maximum extent below the baseline
max_width = 0
# calculate the height/width, since the layers may increased/decreased the size
max_width = 0
max_height = 0
for glyph in info.glyphs:
if glyph.bitmap is None:
continue
# find the highest char
max_bearing_y = max(max_bearing_y, glyph.bearingY)
# find the lowest char
min_bearing_y = min(min_bearing_y, glyph.bearingY - glyph.bitmap.shape[1])
max_width = max(max_width, glyph.bitmap.shape[0])
max_height = max(max_height, glyph.bitmap.shape[1])
info.max_height = max(max_height, info.ascender - info.descender)
info.max_width = max_width
def _get_pair_kernings(info, face):
characters = set([ glyph.utf8 for glyph in info.glyphs])
pairkernings = dict()
for pair in itertools.product(characters, repeat=2):
prevc, c = pair
kerning = face.get_kerning(prevc, c)
if kerning.x != 0:
assert c < 0xFFFFFFFF
assert prevc < 0xFFFFFFFF
pairkernings[ _encode_pair(prevc, c) ] = kerning.x>>6
#print "kerning %s, %s: %d" % (chr(prevc), chr(c), kerning.x>>6), '\t', '0x%016x' % _encode_pair(prevc, c)
return pairkernings
def compile(options):
info = SFontInfo(options)
if not os.path.exists( info.name ):
raise fu.FontException("Failed to find font: %s" % info.name)
face = ft.new_face( info.name )
# gather the glyph info
_get_glyph_info(options, info, face)
pairkernings = None
if info.usepairkernings:
pairkernings = _get_pair_kernings(info, face)
# The actual compile step
render(options, info, face)
# assemble into a texture
image = info.texturerender( info )
pairkernings = dict()
if info.usepairkernings:
pairkernings = _get_pair_kernings(info, face)
if not options.writetext:
if not os.path.exists( os.path.dirname(options.output) ):
os.makedirs(os.path.dirname(options.output))
try:
info.texturewriter( options, info, image )
except Exception, e:
raise fu.FontException('Failed to write texture: %s' % str(e) )
info.writer.write(options, info, pairkernings)
return (info, pairkernings, image)
def _encode_pair(prevc, c):
""" Encodes two 16 bit integers into 32 bits """
return prevc << 32 | c
def _calc_bbox(info, characters, pairkernings, text):
extrapadding = _get_extra_padding(info)
maxx = 0
x = 0
lineheight = info.max_height + extrapadding[1] + extrapadding[3]
y = lineheight
prevc = 0
for i, c in enumerate(text):
c = ord(c)
if c == '\n' and i < len(text)-1:
maxx = max(x, maxx)
x = 0
y += lineheight
continue
char = characters.get( c, None )
if not char:
logging.warning("Character not in info.letters: '%s'" % unichr(c) )
continue
x += char.advance
prevc = c
continue
x += pairkernings.get( _encode_pair(prevc, c), 0)
x += char.advance
x += extrapadding[0] + extrapadding[2]
prevc = c
maxx = max(x, maxx)
return (maxx, y)
[docs]def write_text(options, info, pairkernings):
"""
Writes the given text to an image
"""
pairkernings = pairkernings or dict()
# first, calculate the texture size
if os.path.isfile(options.writetext):
with open(options.writetext, 'rt') as f:
options.writetext = f.read()
cinfo = {glyph.utf8 : glyph for glyph in info.glyphs}
texture_size = _calc_bbox(info, cinfo, pairkernings, options.writetext)
texture_size = (texture_size[0]+info.padding*2+60, texture_size[1]+info.padding*2)
ones = np.ones(texture_size, float)
bgcolor = info.bgcolor
if options.bgcolor:
bgcolor = eval(options.bgcolor)
bgcolor = map( lambda x: float(x)/255.0 if isinstance(x, int) else x, bgcolor )
if len(bgcolor) == 3:
bgcolor = list(bgcolor)+[1.0]
r = ones * bgcolor[0]
g = ones * bgcolor[1]
b = ones * bgcolor[2]
a = ones * bgcolor[3]
zeros = np.zeros(texture_size, float)
image = np.dstack( (r,g,b,zeros) )
x = 0
y = info.ascender
prevc = 0
found = False
for c in options.writetext:
c = ord(c)
char = cinfo.get( c, None )
if not char:
logging.warning("Character not in info.letters: '%s'" % unichr(c))
continue
if char.bitmap == None:
x += char.advance
prevc = c
continue
found = True
x += pairkernings.get( _encode_pair(prevc, c), 0)
bw, bh, _ = char.bitmap.shape
bx = x + char.bearingX
by = y - char.bearingY
# we must make sure that the first pixel is within range
if bx < 0:
x += -char.bearingX
bx = x
target = image[bx : bx + bw, by : by + bh ]
try:
image[bx : bx + bw, by : by + bh ] = fu.alpha_blend( target, char.bitmap )
except Exception, e:
print e
print 'image shape', image.shape
print 'target shape', target.shape
print 'bitmap shape', char.bitmap.shape
print "bx, bw", bx, bw
print "by, bh", by, bh
print "y", y
print "bearingY", char.bearingY
print "lineheight", info.max_height
print "c:", char.utf8, char.unicode
print ""
print ""
raise
x += char.advance
prevc = c
if not found:
raise fu.FontException("No characters found in font: %s" % options.writetext)
image = (image * 255.0).astype(np.uint8)
r,g,b,a = image.T
image = np.dstack((r,g,b,a))
if not os.path.exists( os.path.dirname(options.output) ):
os.makedirs(os.path.dirname(options.output))
Image.fromarray( image ).save( options.output )
logging.debug("Wrote %s" % options.output)
def init():
parser = OptionParser()
parser.add_option('-i', '--input', metavar='FILE', help='The input font (.fontinfo)')
parser.add_option('-o', '--output', metavar='FILE', help='The output font (.fntb)')
parser.add_option('-d', '--datadir', default='', metavar='DIRECTORY', help='The data directory that all resource paths should be relative to')
parser.add_option('-e', '--endian', default='little', choices=['little', 'big'], help='The endianess of the output file.')
parser.add_option('-v', '--verbose', action='store_true', default=False, help='Specifies verbose mode')
parser.add_option('-l', '--log', default='', help='A log file where the stdout is saved logged to.' )
parser.add_option('-w', '--writetext', metavar='TEXT', help='When used, a .fontinfo file is used as input and the text is written into the output texture.')
parser.add_option('--bgcolor', default='', metavar='COLOR', help='The background color used when writing text')
options, args = parser.parse_args()
if options.log:
sys.stdout = LogStream(options.log)
sys.stderr = sys.stdout
if not options.input:
parser.error("You must specify an input file")
if not options.output:
parser.error("You must specify an output file")
if not os.path.exists(options.input):
parser.error("The input file doesn't exist: %s" % options.input)
if not options.endian in ['little', 'big']:
parser.error("Invalid endianess: %s" % options.endian)
logging.basicConfig(level=(logging.INFO if options.verbose else logging.WARN))
return options
if __name__ == '__main__':
options = init()
if options.verbose:
logging.info("Using freetype-%d.%d.%d" % ft.version() )
try:
options.endian = '<' if options.endian == 'little' else '>'
(info, pairkernings, image) = compile(options)
if options.writetext:
write_text(options, info, pairkernings)
except fu.FontException, e:
if '-v' in sys.argv:
raise
logging.error( "%s: %s" % (options.input, str(e)) )
sys.exit(1)