Source code for ttfquery.glyph

"""Representation of a single glyph including contour extraction"""
from ttfquery import glyphquery
import numpy

[docs]class Glyph( object): """Object encapsulating metadata regarding a particular glyph""" def __init__(self, glyphName ): """Initialize the glyph object glyphName -- font's glyphName for this glyph, see glyphquery.glyphName() for extracting the appropriate name from character and encoding. """ self.glyphName = glyphName
[docs] def compile( self, font, steps = 3 ): """Compile the glyph to a set of poly-line outlines""" self.contours = self.calculateContours( font ) self.outlines = [ decomposeOutline(contour,steps) for contour in self.contours ] self.width = glyphquery.width( font, self.glyphName ) self.height = glyphquery.lineHeight( font )
[docs] def calculateContours( self, font ): """Given a character, determine contours to draw returns a list of contours, with each contour being a list of ((x,y),flag) elements. There may be only a single contour. """ glyf = font['glyf'] charglyf = glyf[self.glyphName] return self._calculateContours( charglyf, glyf, font )
def _calculateContours( self, charglyf, glyf, font ): """Create expanded contour data-set from TTF charglyph entry This is called recursively for composite glyphs, so I suppose a badly-formed TTF file could cause an infinite loop. charglyph -- glyf table's entry for the target character glyf -- the glyf table (used for recursive calls) """ charglyf.expand( font ) # XXX is this extraneous? contours = [] if charglyf.numberOfContours == 0: # does not display at all, for instance, space pass elif charglyf.isComposite(): # composed out of other items... for component in charglyf.components: subContours = self._calculateContours( glyf[component.glyphName], glyf, font ) dx,dy = component.getComponentInfo()[1][-2:] # XXX we're ignoring the scaling/shearing/etceteras transformation # matrix which is given by component.getComponentInfo()[1][:4] subContours = [ [((x+dx,y+dy),f) for ((x,y),f) in subContour] for subContour in subContours ] contours.extend( subContours ) else: flags = charglyf.flags coordinates = charglyf.coordinates # We need to convert from the "end point" representation # to a distinct contour representation, requires slicing # each contour out of the list and then closing it last = 0 for e in charglyf.endPtsOfContours: set = map( None, list(coordinates[last:e+1])+list(coordinates[last:last+1]), list(flags[last:e+1])+list(flags[last:last+1]) ) contours.append( set ) last = e+1 if coordinates[last:]: contours.append( map( None, list(coordinates[last:])+list(coordinates[last:last+1]), list(flags[last:])+list(flags[last:]) ) ) return contours
[docs]def decomposeOutline( contour, steps=3 ): """Decompose a single TrueType contour to a line-loop In essence, this is the "interpretation" of the font as geometric primitives. I only support line and quadratic (conic) segments, which should support most TrueType fonts as far as I know. The process consists of first scanning for any multi- off-curve control-point runs. For each pair of such control points, we insert a new on-curve control point. Once we have the "expanded" control point array we scan through looking for each segment which includes an off-curve control point. These should only be bracketed by on-curve control points. For each found segment, we call our integrateQuadratic method to produce a set of points interpolating between the end points as affected by the middle control point. All other control points merely generate a single line-segment between the endpoints. """ # contours must be closed, but they can start with # (and even end with) items not on the contour... # so if we do, we need to create a new point to serve # as the midway... if len(contour)<3: return () set = contour[:] def on( ((Ax,Ay),Af) ): """Is this record on the contour?""" return Af==1 def merge( ((Ax,Ay),Af), ((Bx,By),Bf)): """Merge two off-point records into an on-point record""" return (((Ax+Bx)/2.0),((Ay+By))/2.0),1 # create an expanded set so that all adjacent # off-curve items have an on-curve item added # in between them last = contour[-1] expanded = [] for item in set: if (not on(item)) and (not on(last)): expanded.append( merge(last, item)) expanded.append( item ) last = item result = [] last = expanded[-1] while expanded: assert on(expanded[0]), "Expanded outline doesn't have proper format! Should always have either [on, off, on] or [on, on] as the first items in the outline" if len(expanded)>1: if on(expanded[1]): # line segment from 0 to 1 result.append( expanded[0][0] ) #result.append( expanded[1][0] ) del expanded[:1] else: if len(expanded) == 2: #KH assert on(expanded[0]), """Expanded outline finishes off-curve""" #KH result.append( expanded[1][0] ) #KH del expanded[:1] break assert on(expanded[2]), "Expanded outline doesn't have proper format!" points = integrateQuadratic( expanded[:3], steps = steps ) result.extend( points ) del expanded[:2] else: assert on(expanded[0]), """Expanded outline finishes off-curve""" result.append( expanded[0][0] ) del expanded[:1] result.append( result[-1] ) return result
[docs]def integrateQuadratic( points, steps=3 ): """Get points on curve for quadratic w/ end points A and C Basis Equations are taken from here: http://www.truetype.demon.co.uk/ttoutln.htm This is a very crude approach to the integration, everything is coded directly in Python, with no attempts to speed up the process. XXX Should eventually provide adaptive steps so that the angle between the elements can determine how many steps are used. """ step = 1.0/steps ((Ax,Ay),_),((Bx,By),_),((Cx,Cy),_) = points result = [(Ax,Ay)] ### XXX This is dangerous, in certain cases floating point error ## can wind up creating a new point at 1.0-(some tiny number) if step ## is sliaghtly less than precisely 1.0/steps for t in numpy.arange( step, 1.0, step ): invT = 1.0-t px = (invT*invT * Ax) + (2*t*invT*Bx) + (t*t*Cx) py = (invT*invT * Ay) + (2*t*invT*By) + (t*t*Cy) result.append( (px,py) ) # the end-point will be added by the next segment... #result.append( (Cx,Cy) ) return result