"""_g_l_y_f.py -- Converter classes for the 'glyf' table.""" from __future__ import print_function, division, absolute_import from collections import namedtuple from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools import ttLib from fontTools import version from fontTools.misc.textTools import safeEval, pad from fontTools.misc.arrayTools import calcBounds, calcIntBounds, pointInRect from fontTools.misc.bezierTools import calcQuadraticBounds from fontTools.misc.fixedTools import ( fixedToFloat as fi2fl, floatToFixed as fl2fi, otRound, ) from numbers import Number from . import DefaultTable from . import ttProgram import sys import struct import array import logging import os from fontTools.misc import xmlWriter from fontTools.misc.filenames import userNameToFileName log = logging.getLogger(__name__) # We compute the version the same as is computed in ttlib/__init__ # so that we can write 'ttLibVersion' attribute of the glyf TTX files # when glyf is written to separate files. version = ".".join(version.split('.')[:2]) # # The Apple and MS rasterizers behave differently for # scaled composite components: one does scale first and then translate # and the other does it vice versa. MS defined some flags to indicate # the difference, but it seems nobody actually _sets_ those flags. # # Funny thing: Apple seems to _only_ do their thing in the # WE_HAVE_A_SCALE (eg. Chicago) case, and not when it's WE_HAVE_AN_X_AND_Y_SCALE # (eg. Charcoal)... # SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple class table__g_l_y_f(DefaultTable.DefaultTable): # this attribute controls the amount of padding applied to glyph data upon compile. # Glyph lenghts are aligned to multiples of the specified value. # Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means # no padding, except for when padding would allow to use short loca offsets. padding = 1 def decompile(self, data, ttFont): loca = ttFont['loca'] last = int(loca[0]) noname = 0 self.glyphs = {} self.glyphOrder = glyphOrder = ttFont.getGlyphOrder() for i in range(0, len(loca)-1): try: glyphName = glyphOrder[i] except IndexError: noname = noname + 1 glyphName = 'ttxautoglyph%s' % i next = int(loca[i+1]) glyphdata = data[last:next] if len(glyphdata) != (next - last): raise ttLib.TTLibError("not enough 'glyf' table data") glyph = Glyph(glyphdata) self.glyphs[glyphName] = glyph last = next if len(data) - next >= 4: log.warning( "too much 'glyf' table data: expected %d, received %d bytes", next, len(data)) if noname: log.warning('%s glyphs have no name', noname) if ttFont.lazy is False: # Be lazy for None and True for glyph in self.glyphs.values(): glyph.expand(self) def compile(self, ttFont): if not hasattr(self, "glyphOrder"): self.glyphOrder = ttFont.getGlyphOrder() padding = self.padding assert padding in (0, 1, 2, 4) locations = [] currentLocation = 0 dataList = [] recalcBBoxes = ttFont.recalcBBoxes for glyphName in self.glyphOrder: glyph = self.glyphs[glyphName] glyphData = glyph.compile(self, recalcBBoxes) if padding > 1: glyphData = pad(glyphData, size=padding) locations.append(currentLocation) currentLocation = currentLocation + len(glyphData) dataList.append(glyphData) locations.append(currentLocation) if padding == 1 and currentLocation < 0x20000: # See if we can pad any odd-lengthed glyphs to allow loca # table to use the short offsets. indices = [i for i,glyphData in enumerate(dataList) if len(glyphData) % 2 == 1] if indices and currentLocation + len(indices) < 0x20000: # It fits. Do it. for i in indices: dataList[i] += b'\0' currentLocation = 0 for i,glyphData in enumerate(dataList): locations[i] = currentLocation currentLocation += len(glyphData) locations[len(dataList)] = currentLocation data = bytesjoin(dataList) if 'loca' in ttFont: ttFont['loca'].set(locations) if 'maxp' in ttFont: ttFont['maxp'].numGlyphs = len(self.glyphs) return data def toXML(self, writer, ttFont, splitGlyphs=False): notice = ( "The xMin, yMin, xMax and yMax values\n" "will be recalculated by the compiler.") glyphNames = ttFont.getGlyphNames() if not splitGlyphs: writer.newline() writer.comment(notice) writer.newline() writer.newline() numGlyphs = len(glyphNames) if splitGlyphs: path, ext = os.path.splitext(writer.file.name) existingGlyphFiles = set() for glyphName in glyphNames: glyph = self[glyphName] if glyph.numberOfContours: if splitGlyphs: glyphPath = userNameToFileName( tounicode(glyphName, 'utf-8'), existingGlyphFiles, prefix=path + ".", suffix=ext) existingGlyphFiles.add(glyphPath.lower()) glyphWriter = xmlWriter.XMLWriter( glyphPath, idlefunc=writer.idlefunc, newlinestr=writer.newlinestr) glyphWriter.begintag("ttFont", ttLibVersion=version) glyphWriter.newline() glyphWriter.begintag("glyf") glyphWriter.newline() glyphWriter.comment(notice) glyphWriter.newline() writer.simpletag("TTGlyph", src=os.path.basename(glyphPath)) else: glyphWriter = writer glyphWriter.begintag('TTGlyph', [ ("name", glyphName), ("xMin", glyph.xMin), ("yMin", glyph.yMin), ("xMax", glyph.xMax), ("yMax", glyph.yMax), ]) glyphWriter.newline() glyph.toXML(glyphWriter, ttFont) glyphWriter.endtag('TTGlyph') glyphWriter.newline() if splitGlyphs: glyphWriter.endtag("glyf") glyphWriter.newline() glyphWriter.endtag("ttFont") glyphWriter.newline() glyphWriter.close() else: writer.simpletag('TTGlyph', name=glyphName) writer.comment("contains no outline data") if not splitGlyphs: writer.newline() writer.newline() def fromXML(self, name, attrs, content, ttFont): if name != "TTGlyph": return if not hasattr(self, "glyphs"): self.glyphs = {} if not hasattr(self, "glyphOrder"): self.glyphOrder = ttFont.getGlyphOrder() glyphName = attrs["name"] log.debug("unpacking glyph '%s'", glyphName) glyph = Glyph() for attr in ['xMin', 'yMin', 'xMax', 'yMax']: setattr(glyph, attr, safeEval(attrs.get(attr, '0'))) self.glyphs[glyphName] = glyph for element in content: if not isinstance(element, tuple): continue name, attrs, content = element glyph.fromXML(name, attrs, content, ttFont) if not ttFont.recalcBBoxes: glyph.compact(self, 0) def setGlyphOrder(self, glyphOrder): self.glyphOrder = glyphOrder def getGlyphName(self, glyphID): return self.glyphOrder[glyphID] def getGlyphID(self, glyphName): # XXX optimize with reverse dict!!! return self.glyphOrder.index(glyphName) def removeHinting(self): for glyph in self.glyphs.values(): glyph.removeHinting() def keys(self): return self.glyphs.keys() def has_key(self, glyphName): return glyphName in self.glyphs __contains__ = has_key def __getitem__(self, glyphName): glyph = self.glyphs[glyphName] glyph.expand(self) return glyph def __setitem__(self, glyphName, glyph): self.glyphs[glyphName] = glyph if glyphName not in self.glyphOrder: self.glyphOrder.append(glyphName) def __delitem__(self, glyphName): del self.glyphs[glyphName] self.glyphOrder.remove(glyphName) def __len__(self): assert len(self.glyphOrder) == len(self.glyphs) return len(self.glyphs) glyphHeaderFormat = """ > # big endian numberOfContours: h xMin: h yMin: h xMax: h yMax: h """ # flags flagOnCurve = 0x01 flagXShort = 0x02 flagYShort = 0x04 flagRepeat = 0x08 flagXsame = 0x10 flagYsame = 0x20 flagOverlapSimple = 0x40 flagReserved = 0x80 # These flags are kept for XML output after decompiling the coordinates keepFlags = flagOnCurve + flagOverlapSimple _flagSignBytes = { 0: 2, flagXsame: 0, flagXShort|flagXsame: +1, flagXShort: -1, flagYsame: 0, flagYShort|flagYsame: +1, flagYShort: -1, } def flagBest(x, y, onCurve): """For a given x,y delta pair, returns the flag that packs this pair most efficiently, as well as the number of byte cost of such flag.""" flag = flagOnCurve if onCurve else 0 cost = 0 # do x if x == 0: flag = flag | flagXsame elif -255 <= x <= 255: flag = flag | flagXShort if x > 0: flag = flag | flagXsame cost += 1 else: cost += 2 # do y if y == 0: flag = flag | flagYsame elif -255 <= y <= 255: flag = flag | flagYShort if y > 0: flag = flag | flagYsame cost += 1 else: cost += 2 return flag, cost def flagFits(newFlag, oldFlag, mask): newBytes = _flagSignBytes[newFlag & mask] oldBytes = _flagSignBytes[oldFlag & mask] return newBytes == oldBytes or abs(newBytes) > abs(oldBytes) def flagSupports(newFlag, oldFlag): return ((oldFlag & flagOnCurve) == (newFlag & flagOnCurve) and flagFits(newFlag, oldFlag, flagXsame|flagXShort) and flagFits(newFlag, oldFlag, flagYsame|flagYShort)) def flagEncodeCoord(flag, mask, coord, coordBytes): byteCount = _flagSignBytes[flag & mask] if byteCount == 1: coordBytes.append(coord) elif byteCount == -1: coordBytes.append(-coord) elif byteCount == 2: coordBytes.append((coord >> 8) & 0xFF) coordBytes.append(coord & 0xFF) def flagEncodeCoords(flag, x, y, xBytes, yBytes): flagEncodeCoord(flag, flagXsame|flagXShort, x, xBytes) flagEncodeCoord(flag, flagYsame|flagYShort, y, yBytes) ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0 NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!) MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11 WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple) UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS) CompositeMaxpValues = namedtuple('CompositeMaxpValues', ['nPoints', 'nContours', 'maxComponentDepth']) class Glyph(object): def __init__(self, data=""): if not data: # empty char self.numberOfContours = 0 return self.data = data def compact(self, glyfTable, recalcBBoxes=True): data = self.compile(glyfTable, recalcBBoxes) self.__dict__.clear() self.data = data def expand(self, glyfTable): if not hasattr(self, "data"): # already unpacked return if not self.data: # empty char del self.data self.numberOfContours = 0 return dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self) del self.data # Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in # some glyphs; decompileCoordinates assumes that there's at least # one, so short-circuit here. if self.numberOfContours == 0: return if self.isComposite(): self.decompileComponents(data, glyfTable) else: self.decompileCoordinates(data) def compile(self, glyfTable, recalcBBoxes=True): if hasattr(self, "data"): if recalcBBoxes: # must unpack glyph in order to recalculate bounding box self.expand(glyfTable) else: return self.data if self.numberOfContours == 0: return "" if recalcBBoxes: self.recalcBounds(glyfTable) data = sstruct.pack(glyphHeaderFormat, self) if self.isComposite(): data = data + self.compileComponents(glyfTable) else: data = data + self.compileCoordinates() return data def toXML(self, writer, ttFont): if self.isComposite(): for compo in self.components: compo.toXML(writer, ttFont) haveInstructions = hasattr(self, "program") else: last = 0 for i in range(self.numberOfContours): writer.begintag("contour") writer.newline() for j in range(last, self.endPtsOfContours[i] + 1): attrs = [ ("x", self.coordinates[j][0]), ("y", self.coordinates[j][1]), ("on", self.flags[j] & flagOnCurve), ] if self.flags[j] & flagOverlapSimple: # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours attrs.append(("overlap", 1)) writer.simpletag("pt", attrs) writer.newline() last = self.endPtsOfContours[i] + 1 writer.endtag("contour") writer.newline() haveInstructions = self.numberOfContours > 0 if haveInstructions: if self.program: writer.begintag("instructions") writer.newline() self.program.toXML(writer, ttFont) writer.endtag("instructions") else: writer.simpletag("instructions") writer.newline() def fromXML(self, name, attrs, content, ttFont): if name == "contour": if self.numberOfContours < 0: raise ttLib.TTLibError("can't mix composites and contours in glyph") self.numberOfContours = self.numberOfContours + 1 coordinates = GlyphCoordinates() flags = [] for element in content: if not isinstance(element, tuple): continue name, attrs, content = element if name != "pt": continue # ignore anything but "pt" coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"]))) flag = not not safeEval(attrs["on"]) if "overlap" in attrs and bool(safeEval(attrs["overlap"])): flag |= flagOverlapSimple flags.append(flag) flags = array.array("B", flags) if not hasattr(self, "coordinates"): self.coordinates = coordinates self.flags = flags self.endPtsOfContours = [len(coordinates)-1] else: self.coordinates.extend (coordinates) self.flags.extend(flags) self.endPtsOfContours.append(len(self.coordinates)-1) elif name == "component": if self.numberOfContours > 0: raise ttLib.TTLibError("can't mix composites and contours in glyph") self.numberOfContours = -1 if not hasattr(self, "components"): self.components = [] component = GlyphComponent() self.components.append(component) component.fromXML(name, attrs, content, ttFont) elif name == "instructions": self.program = ttProgram.Program() for element in content: if not isinstance(element, tuple): continue name, attrs, content = element self.program.fromXML(name, attrs, content, ttFont) def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1): assert self.isComposite() nContours = 0 nPoints = 0 for compo in self.components: baseGlyph = glyfTable[compo.glyphName] if baseGlyph.numberOfContours == 0: continue elif baseGlyph.numberOfContours > 0: nP, nC = baseGlyph.getMaxpValues() else: nP, nC, maxComponentDepth = baseGlyph.getCompositeMaxpValues( glyfTable, maxComponentDepth + 1) nPoints = nPoints + nP nContours = nContours + nC return CompositeMaxpValues(nPoints, nContours, maxComponentDepth) def getMaxpValues(self): assert self.numberOfContours > 0 return len(self.coordinates), len(self.endPtsOfContours) def decompileComponents(self, data, glyfTable): self.components = [] more = 1 haveInstructions = 0 while more: component = GlyphComponent() more, haveInstr, data = component.decompile(data, glyfTable) haveInstructions = haveInstructions | haveInstr self.components.append(component) if haveInstructions: numInstructions, = struct.unpack(">h", data[:2]) data = data[2:] self.program = ttProgram.Program() self.program.fromBytecode(data[:numInstructions]) data = data[numInstructions:] if len(data) >= 4: log.warning( "too much glyph data at the end of composite glyph: %d excess bytes", len(data)) def decompileCoordinates(self, data): endPtsOfContours = array.array("h") endPtsOfContours.fromstring(data[:2*self.numberOfContours]) if sys.byteorder != "big": endPtsOfContours.byteswap() self.endPtsOfContours = endPtsOfContours.tolist() data = data[2*self.numberOfContours:] instructionLength, = struct.unpack(">h", data[:2]) data = data[2:] self.program = ttProgram.Program() self.program.fromBytecode(data[:instructionLength]) data = data[instructionLength:] nCoordinates = self.endPtsOfContours[-1] + 1 flags, xCoordinates, yCoordinates = \ self.decompileCoordinatesRaw(nCoordinates, data) # fill in repetitions and apply signs self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates) xIndex = 0 yIndex = 0 for i in range(nCoordinates): flag = flags[i] # x coordinate if flag & flagXShort: if flag & flagXsame: x = xCoordinates[xIndex] else: x = -xCoordinates[xIndex] xIndex = xIndex + 1 elif flag & flagXsame: x = 0 else: x = xCoordinates[xIndex] xIndex = xIndex + 1 # y coordinate if flag & flagYShort: if flag & flagYsame: y = yCoordinates[yIndex] else: y = -yCoordinates[yIndex] yIndex = yIndex + 1 elif flag & flagYsame: y = 0 else: y = yCoordinates[yIndex] yIndex = yIndex + 1 coordinates[i] = (x, y) assert xIndex == len(xCoordinates) assert yIndex == len(yCoordinates) coordinates.relativeToAbsolute() # discard all flags except "keepFlags" self.flags = array.array("B", (f & keepFlags for f in flags)) def decompileCoordinatesRaw(self, nCoordinates, data): # unpack flags and prepare unpacking of coordinates flags = array.array("B", [0] * nCoordinates) # Warning: deep Python trickery going on. We use the struct module to unpack # the coordinates. We build a format string based on the flags, so we can # unpack the coordinates in one struct.unpack() call. xFormat = ">" # big endian yFormat = ">" # big endian i = j = 0 while True: flag = byteord(data[i]) i = i + 1 repeat = 1 if flag & flagRepeat: repeat = byteord(data[i]) + 1 i = i + 1 for k in range(repeat): if flag & flagXShort: xFormat = xFormat + 'B' elif not (flag & flagXsame): xFormat = xFormat + 'h' if flag & flagYShort: yFormat = yFormat + 'B' elif not (flag & flagYsame): yFormat = yFormat + 'h' flags[j] = flag j = j + 1 if j >= nCoordinates: break assert j == nCoordinates, "bad glyph flags" data = data[i:] # unpack raw coordinates, krrrrrr-tching! xDataLen = struct.calcsize(xFormat) yDataLen = struct.calcsize(yFormat) if len(data) - (xDataLen + yDataLen) >= 4: log.warning( "too much glyph data: %d excess bytes", len(data) - (xDataLen + yDataLen)) xCoordinates = struct.unpack(xFormat, data[:xDataLen]) yCoordinates = struct.unpack(yFormat, data[xDataLen:xDataLen+yDataLen]) return flags, xCoordinates, yCoordinates def compileComponents(self, glyfTable): data = b"" lastcomponent = len(self.components) - 1 more = 1 haveInstructions = 0 for i in range(len(self.components)): if i == lastcomponent: haveInstructions = hasattr(self, "program") more = 0 compo = self.components[i] data = data + compo.compile(more, haveInstructions, glyfTable) if haveInstructions: instructions = self.program.getBytecode() data = data + struct.pack(">h", len(instructions)) + instructions return data def compileCoordinates(self): assert len(self.coordinates) == len(self.flags) data = [] endPtsOfContours = array.array("h", self.endPtsOfContours) if sys.byteorder != "big": endPtsOfContours.byteswap() data.append(endPtsOfContours.tostring()) instructions = self.program.getBytecode() data.append(struct.pack(">h", len(instructions))) data.append(instructions) deltas = self.coordinates.copy() if deltas.isFloat(): # Warn? deltas.toInt() deltas.absoluteToRelative() # TODO(behdad): Add a configuration option for this? deltas = self.compileDeltasGreedy(self.flags, deltas) #deltas = self.compileDeltasOptimal(self.flags, deltas) data.extend(deltas) return bytesjoin(data) def compileDeltasGreedy(self, flags, deltas): # Implements greedy algorithm for packing coordinate deltas: # uses shortest representation one coordinate at a time. compressedflags = [] xPoints = [] yPoints = [] lastflag = None repeat = 0 for flag,(x,y) in zip(flags, deltas): # Oh, the horrors of TrueType # do x if x == 0: flag = flag | flagXsame elif -255 <= x <= 255: flag = flag | flagXShort if x > 0: flag = flag | flagXsame else: x = -x xPoints.append(bytechr(x)) else: xPoints.append(struct.pack(">h", x)) # do y if y == 0: flag = flag | flagYsame elif -255 <= y <= 255: flag = flag | flagYShort if y > 0: flag = flag | flagYsame else: y = -y yPoints.append(bytechr(y)) else: yPoints.append(struct.pack(">h", y)) # handle repeating flags if flag == lastflag and repeat != 255: repeat = repeat + 1 if repeat == 1: compressedflags.append(flag) else: compressedflags[-2] = flag | flagRepeat compressedflags[-1] = repeat else: repeat = 0 compressedflags.append(flag) lastflag = flag compressedFlags = array.array("B", compressedflags).tostring() compressedXs = bytesjoin(xPoints) compressedYs = bytesjoin(yPoints) return (compressedFlags, compressedXs, compressedYs) def compileDeltasOptimal(self, flags, deltas): # Implements optimal, dynaic-programming, algorithm for packing coordinate # deltas. The savings are negligible :(. candidates = [] bestTuple = None bestCost = 0 repeat = 0 for flag,(x,y) in zip(flags, deltas): # Oh, the horrors of TrueType flag, coordBytes = flagBest(x, y, flag) bestCost += 1 + coordBytes newCandidates = [(bestCost, bestTuple, flag, coordBytes), (bestCost+1, bestTuple, (flag|flagRepeat), coordBytes)] for lastCost,lastTuple,lastFlag,coordBytes in candidates: if lastCost + coordBytes <= bestCost + 1 and (lastFlag & flagRepeat) and (lastFlag < 0xff00) and flagSupports(lastFlag, flag): if (lastFlag & 0xFF) == (flag|flagRepeat) and lastCost == bestCost + 1: continue newCandidates.append((lastCost + coordBytes, lastTuple, lastFlag+256, coordBytes)) candidates = newCandidates bestTuple = min(candidates, key=lambda t:t[0]) bestCost = bestTuple[0] flags = [] while bestTuple: cost, bestTuple, flag, coordBytes = bestTuple flags.append(flag) flags.reverse() compressedFlags = array.array("B") compressedXs = array.array("B") compressedYs = array.array("B") coords = iter(deltas) ff = [] for flag in flags: repeatCount, flag = flag >> 8, flag & 0xFF compressedFlags.append(flag) if flag & flagRepeat: assert(repeatCount > 0) compressedFlags.append(repeatCount) else: assert(repeatCount == 0) for i in range(1 + repeatCount): x,y = next(coords) flagEncodeCoords(flag, x, y, compressedXs, compressedYs) ff.append(flag) try: next(coords) raise Exception("internal error") except StopIteration: pass compressedFlags = compressedFlags.tostring() compressedXs = compressedXs.tostring() compressedYs = compressedYs.tostring() return (compressedFlags, compressedXs, compressedYs) def recalcBounds(self, glyfTable): coords, endPts, flags = self.getCoordinates(glyfTable) if len(coords) > 0: if 0: # This branch calculates exact glyph outline bounds # analytically, handling cases without on-curve # extremas, etc. However, the glyf table header # simply says that the bounds should be min/max x/y # "for coordinate data", so I suppose that means no # fancy thing here, just get extremas of all coord # points (on and off). As such, this branch is # disabled. # Collect on-curve points onCurveCoords = [coords[j] for j in range(len(coords)) if flags[j] & flagOnCurve] # Add implicit on-curve points start = 0 for end in endPts: last = end for j in range(start, end + 1): if not ((flags[j] | flags[last]) & flagOnCurve): x = (coords[last][0] + coords[j][0]) / 2 y = (coords[last][1] + coords[j][1]) / 2 onCurveCoords.append((x,y)) last = j start = end + 1 # Add bounds for curves without an explicit extrema start = 0 for end in endPts: last = end for j in range(start, end + 1): if not (flags[j] & flagOnCurve): next = j + 1 if j < end else start bbox = calcBounds([coords[last], coords[next]]) if not pointInRect(coords[j], bbox): # Ouch! log.warning("Outline has curve with implicit extrema.") # Ouch! Find analytical curve bounds. pthis = coords[j] plast = coords[last] if not (flags[last] & flagOnCurve): plast = ((pthis[0]+plast[0])/2, (pthis[1]+plast[1])/2) pnext = coords[next] if not (flags[next] & flagOnCurve): pnext = ((pthis[0]+pnext[0])/2, (pthis[1]+pnext[1])/2) bbox = calcQuadraticBounds(plast, pthis, pnext) onCurveCoords.append((bbox[0],bbox[1])) onCurveCoords.append((bbox[2],bbox[3])) last = j start = end + 1 self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(onCurveCoords) else: self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords) else: self.xMin, self.yMin, self.xMax, self.yMax = (0, 0, 0, 0) def isComposite(self): """Can be called on compact or expanded glyph.""" if hasattr(self, "data") and self.data: return struct.unpack(">h", self.data[:2])[0] == -1 else: return self.numberOfContours == -1 def __getitem__(self, componentIndex): if not self.isComposite(): raise ttLib.TTLibError("can't use glyph as sequence") return self.components[componentIndex] def getCoordinates(self, glyfTable): if self.numberOfContours > 0: return self.coordinates, self.endPtsOfContours, self.flags elif self.isComposite(): # it's a composite allCoords = GlyphCoordinates() allFlags = array.array("B") allEndPts = [] for compo in self.components: g = glyfTable[compo.glyphName] try: coordinates, endPts, flags = g.getCoordinates(glyfTable) except RecursionError: raise ttLib.TTLibError("glyph '%s' contains a recursive component reference" % compo.glyphName) if hasattr(compo, "firstPt"): # move according to two reference points x1,y1 = allCoords[compo.firstPt] x2,y2 = coordinates[compo.secondPt] move = x1-x2, y1-y2 else: move = compo.x, compo.y coordinates = GlyphCoordinates(coordinates) if not hasattr(compo, "transform"): coordinates.translate(move) else: apple_way = compo.flags & SCALED_COMPONENT_OFFSET ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET assert not (apple_way and ms_way) if not (apple_way or ms_way): scale_component_offset = SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file else: scale_component_offset = apple_way if scale_component_offset: # the Apple way: first move, then scale (ie. scale the component offset) coordinates.translate(move) coordinates.transform(compo.transform) else: # the MS way: first scale, then move coordinates.transform(compo.transform) coordinates.translate(move) offset = len(allCoords) allEndPts.extend(e + offset for e in endPts) allCoords.extend(coordinates) allFlags.extend(flags) return allCoords, allEndPts, allFlags else: return GlyphCoordinates(), [], array.array("B") def getComponentNames(self, glyfTable): if not hasattr(self, "data"): if self.isComposite(): return [c.glyphName for c in self.components] else: return [] # Extract components without expanding glyph if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: return [] # Not composite data = self.data i = 10 components = [] more = 1 while more: flags, glyphID = struct.unpack(">HH", data[i:i+4]) i += 4 flags = int(flags) components.append(glyfTable.getGlyphName(int(glyphID))) if flags & ARG_1_AND_2_ARE_WORDS: i += 4 else: i += 2 if flags & WE_HAVE_A_SCALE: i += 2 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4 elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8 more = flags & MORE_COMPONENTS return components def trim(self, remove_hinting=False): """ Remove padding and, if requested, hinting, from a glyph. This works on both expanded and compacted glyphs, without expanding it.""" if not hasattr(self, "data"): if remove_hinting: self.program = ttProgram.Program() self.program.fromBytecode([]) # No padding to trim. return if not self.data: return numContours = struct.unpack(">h", self.data[:2])[0] data = array.array("B", self.data) i = 10 if numContours >= 0: i += 2 * numContours # endPtsOfContours nCoordinates = ((data[i-2] << 8) | data[i-1]) + 1 instructionLen = (data[i] << 8) | data[i+1] if remove_hinting: # Zero instruction length data[i] = data [i+1] = 0 i += 2 if instructionLen: # Splice it out data = data[:i] + data[i+instructionLen:] instructionLen = 0 else: i += 2 + instructionLen coordBytes = 0 j = 0 while True: flag = data[i] i = i + 1 repeat = 1 if flag & flagRepeat: repeat = data[i] + 1 i = i + 1 xBytes = yBytes = 0 if flag & flagXShort: xBytes = 1 elif not (flag & flagXsame): xBytes = 2 if flag & flagYShort: yBytes = 1 elif not (flag & flagYsame): yBytes = 2 coordBytes += (xBytes + yBytes) * repeat j += repeat if j >= nCoordinates: break assert j == nCoordinates, "bad glyph flags" i += coordBytes # Remove padding data = data[:i] else: more = 1 we_have_instructions = False while more: flags =(data[i] << 8) | data[i+1] if remove_hinting: flags &= ~WE_HAVE_INSTRUCTIONS if flags & WE_HAVE_INSTRUCTIONS: we_have_instructions = True data[i+0] = flags >> 8 data[i+1] = flags & 0xFF i += 4 flags = int(flags) if flags & ARG_1_AND_2_ARE_WORDS: i += 4 else: i += 2 if flags & WE_HAVE_A_SCALE: i += 2 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4 elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8 more = flags & MORE_COMPONENTS if we_have_instructions: instructionLen = (data[i] << 8) | data[i+1] i += 2 + instructionLen # Remove padding data = data[:i] self.data = data.tostring() def removeHinting(self): self.trim (remove_hinting=True) def draw(self, pen, glyfTable, offset=0): if self.isComposite(): for component in self.components: glyphName, transform = component.getComponentInfo() pen.addComponent(glyphName, transform) return coordinates, endPts, flags = self.getCoordinates(glyfTable) if offset: coordinates = coordinates.copy() coordinates.translate((offset, 0)) start = 0 for end in endPts: end = end + 1 contour = coordinates[start:end] cFlags = flags[start:end] start = end if 1 not in cFlags: # There is not a single on-curve point on the curve, # use pen.qCurveTo's special case by specifying None # as the on-curve point. contour.append(None) pen.qCurveTo(*contour) else: # Shuffle the points so that contour the is guaranteed # to *end* in an on-curve point, which we'll use for # the moveTo. firstOnCurve = cFlags.index(1) + 1 contour = contour[firstOnCurve:] + contour[:firstOnCurve] cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] pen.moveTo(contour[-1]) while contour: nextOnCurve = cFlags.index(1) + 1 if nextOnCurve == 1: pen.lineTo(contour[0]) else: pen.qCurveTo(*contour[:nextOnCurve]) contour = contour[nextOnCurve:] cFlags = cFlags[nextOnCurve:] pen.closePath() def drawPoints(self, pen, glyfTable, offset=0): """Draw the glyph using the supplied pointPen. Opposed to Glyph.draw(), this will not change the point indices. """ if self.isComposite(): for component in self.components: glyphName, transform = component.getComponentInfo() pen.addComponent(glyphName, transform) return coordinates, endPts, flags = self.getCoordinates(glyfTable) if offset: coordinates = coordinates.copy() coordinates.translate((offset, 0)) start = 0 for end in endPts: end = end + 1 contour = coordinates[start:end] cFlags = flags[start:end] start = end pen.beginPath() # Start with the appropriate segment type based on the final segment segmentType = "line" if cFlags[-1] == 1 else "qcurve" for i, pt in enumerate(contour): if cFlags[i] == 1: pen.addPoint(pt, segmentType=segmentType) segmentType = "line" else: pen.addPoint(pt) segmentType = "qcurve" pen.endPath() def __eq__(self, other): if type(self) != type(other): return NotImplemented return self.__dict__ == other.__dict__ def __ne__(self, other): result = self.__eq__(other) return result if result is NotImplemented else not result class GlyphComponent(object): def __init__(self): pass def getComponentInfo(self): """Return the base glyph name and a transform.""" # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement # something equivalent in fontTools.objects.glyph (I'd rather not # convert it to an absolute offset, since it is valuable information). # This method will now raise "AttributeError: x" on glyphs that use # this TT feature. if hasattr(self, "transform"): [[xx, xy], [yx, yy]] = self.transform trans = (xx, xy, yx, yy, self.x, self.y) else: trans = (1, 0, 0, 1, self.x, self.y) return self.glyphName, trans def decompile(self, data, glyfTable): flags, glyphID = struct.unpack(">HH", data[:4]) self.flags = int(flags) glyphID = int(glyphID) self.glyphName = glyfTable.getGlyphName(int(glyphID)) data = data[4:] if self.flags & ARG_1_AND_2_ARE_WORDS: if self.flags & ARGS_ARE_XY_VALUES: self.x, self.y = struct.unpack(">hh", data[:4]) else: x, y = struct.unpack(">HH", data[:4]) self.firstPt, self.secondPt = int(x), int(y) data = data[4:] else: if self.flags & ARGS_ARE_XY_VALUES: self.x, self.y = struct.unpack(">bb", data[:2]) else: x, y = struct.unpack(">BB", data[:2]) self.firstPt, self.secondPt = int(x), int(y) data = data[2:] if self.flags & WE_HAVE_A_SCALE: scale, = struct.unpack(">h", data[:2]) self.transform = [[fi2fl(scale,14), 0], [0, fi2fl(scale,14)]] # fixed 2.14 data = data[2:] elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE: xscale, yscale = struct.unpack(">hh", data[:4]) self.transform = [[fi2fl(xscale,14), 0], [0, fi2fl(yscale,14)]] # fixed 2.14 data = data[4:] elif self.flags & WE_HAVE_A_TWO_BY_TWO: (xscale, scale01, scale10, yscale) = struct.unpack(">hhhh", data[:8]) self.transform = [[fi2fl(xscale,14), fi2fl(scale01,14)], [fi2fl(scale10,14), fi2fl(yscale,14)]] # fixed 2.14 data = data[8:] more = self.flags & MORE_COMPONENTS haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS self.flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET | NON_OVERLAPPING | OVERLAP_COMPOUND) return more, haveInstructions, data def compile(self, more, haveInstructions, glyfTable): data = b"" # reset all flags we will calculate ourselves flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET | NON_OVERLAPPING | OVERLAP_COMPOUND) if more: flags = flags | MORE_COMPONENTS if haveInstructions: flags = flags | WE_HAVE_INSTRUCTIONS if hasattr(self, "firstPt"): if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255): data = data + struct.pack(">BB", self.firstPt, self.secondPt) else: data = data + struct.pack(">HH", self.firstPt, self.secondPt) flags = flags | ARG_1_AND_2_ARE_WORDS else: x = otRound(self.x) y = otRound(self.y) flags = flags | ARGS_ARE_XY_VALUES if (-128 <= x <= 127) and (-128 <= y <= 127): data = data + struct.pack(">bb", x, y) else: data = data + struct.pack(">hh", x, y) flags = flags | ARG_1_AND_2_ARE_WORDS if hasattr(self, "transform"): transform = [[fl2fi(x,14) for x in row] for row in self.transform] if transform[0][1] or transform[1][0]: flags = flags | WE_HAVE_A_TWO_BY_TWO data = data + struct.pack(">hhhh", transform[0][0], transform[0][1], transform[1][0], transform[1][1]) elif transform[0][0] != transform[1][1]: flags = flags | WE_HAVE_AN_X_AND_Y_SCALE data = data + struct.pack(">hh", transform[0][0], transform[1][1]) else: flags = flags | WE_HAVE_A_SCALE data = data + struct.pack(">h", transform[0][0]) glyphID = glyfTable.getGlyphID(self.glyphName) return struct.pack(">HH", flags, glyphID) + data def toXML(self, writer, ttFont): attrs = [("glyphName", self.glyphName)] if not hasattr(self, "firstPt"): attrs = attrs + [("x", self.x), ("y", self.y)] else: attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)] if hasattr(self, "transform"): transform = self.transform if transform[0][1] or transform[1][0]: attrs = attrs + [ ("scalex", transform[0][0]), ("scale01", transform[0][1]), ("scale10", transform[1][0]), ("scaley", transform[1][1]), ] elif transform[0][0] != transform[1][1]: attrs = attrs + [ ("scalex", transform[0][0]), ("scaley", transform[1][1]), ] else: attrs = attrs + [("scale", transform[0][0])] attrs = attrs + [("flags", hex(self.flags))] writer.simpletag("component", attrs) writer.newline() def fromXML(self, name, attrs, content, ttFont): self.glyphName = attrs["glyphName"] if "firstPt" in attrs: self.firstPt = safeEval(attrs["firstPt"]) self.secondPt = safeEval(attrs["secondPt"]) else: self.x = safeEval(attrs["x"]) self.y = safeEval(attrs["y"]) if "scale01" in attrs: scalex = safeEval(attrs["scalex"]) scale01 = safeEval(attrs["scale01"]) scale10 = safeEval(attrs["scale10"]) scaley = safeEval(attrs["scaley"]) self.transform = [[scalex, scale01], [scale10, scaley]] elif "scalex" in attrs: scalex = safeEval(attrs["scalex"]) scaley = safeEval(attrs["scaley"]) self.transform = [[scalex, 0], [0, scaley]] elif "scale" in attrs: scale = safeEval(attrs["scale"]) self.transform = [[scale, 0], [0, scale]] self.flags = safeEval(attrs["flags"]) def __eq__(self, other): if type(self) != type(other): return NotImplemented return self.__dict__ == other.__dict__ def __ne__(self, other): result = self.__eq__(other) return result if result is NotImplemented else not result class GlyphCoordinates(object): def __init__(self, iterable=[], typecode="h"): self._a = array.array(typecode) self.extend(iterable) @property def array(self): return self._a def isFloat(self): return self._a.typecode == 'd' def _ensureFloat(self): if self.isFloat(): return # The conversion to list() is to work around Jython bug self._a = array.array("d", list(self._a)) def _checkFloat(self, p): if self.isFloat(): return p if any(v > 0x7FFF or v < -0x8000 for v in p): self._ensureFloat() return p if any(isinstance(v, float) for v in p): p = [int(v) if int(v) == v else v for v in p] if any(isinstance(v, float) for v in p): self._ensureFloat() return p @staticmethod def zeros(count): return GlyphCoordinates([(0,0)] * count) def copy(self): c = GlyphCoordinates(typecode=self._a.typecode) c._a.extend(self._a) return c def __len__(self): return len(self._a) // 2 def __getitem__(self, k): if isinstance(k, slice): indices = range(*k.indices(len(self))) return [self[i] for i in indices] return self._a[2*k],self._a[2*k+1] def __setitem__(self, k, v): if isinstance(k, slice): indices = range(*k.indices(len(self))) # XXX This only works if len(v) == len(indices) for j,i in enumerate(indices): self[i] = v[j] return v = self._checkFloat(v) self._a[2*k],self._a[2*k+1] = v def __delitem__(self, i): i = (2*i) % len(self._a) del self._a[i] del self._a[i] def __repr__(self): return 'GlyphCoordinates(['+','.join(str(c) for c in self)+'])' def append(self, p): p = self._checkFloat(p) self._a.extend(tuple(p)) def extend(self, iterable): for p in iterable: p = self._checkFloat(p) self._a.extend(p) def toInt(self): if not self.isFloat(): return a = array.array("h") for n in self._a: a.append(otRound(n)) self._a = a def relativeToAbsolute(self): a = self._a x,y = 0,0 for i in range(len(a) // 2): x = a[2*i ] + x y = a[2*i+1] + y self[i] = (x, y) def absoluteToRelative(self): a = self._a x,y = 0,0 for i in range(len(a) // 2): dx = a[2*i ] - x dy = a[2*i+1] - y x = a[2*i ] y = a[2*i+1] self[i] = (dx, dy) def translate(self, p): """ >>> GlyphCoordinates([(1,2)]).translate((.5,0)) """ (x,y) = self._checkFloat(p) a = self._a for i in range(len(a) // 2): self[i] = (a[2*i] + x, a[2*i+1] + y) def scale(self, p): """ >>> GlyphCoordinates([(1,2)]).scale((.5,0)) """ (x,y) = self._checkFloat(p) a = self._a for i in range(len(a) // 2): self[i] = (a[2*i] * x, a[2*i+1] * y) def transform(self, t): """ >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5))) """ a = self._a for i in range(len(a) // 2): x = a[2*i ] y = a[2*i+1] px = x * t[0][0] + y * t[1][0] py = x * t[0][1] + y * t[1][1] self[i] = (px, py) def __eq__(self, other): """ >>> g = GlyphCoordinates([(1,2)]) >>> g2 = GlyphCoordinates([(1.0,2)]) >>> g3 = GlyphCoordinates([(1.5,2)]) >>> g == g2 True >>> g == g3 False >>> g2 == g3 False """ if type(self) != type(other): return NotImplemented return self._a == other._a def __ne__(self, other): """ >>> g = GlyphCoordinates([(1,2)]) >>> g2 = GlyphCoordinates([(1.0,2)]) >>> g3 = GlyphCoordinates([(1.5,2)]) >>> g != g2 False >>> g != g3 True >>> g2 != g3 True """ result = self.__eq__(other) return result if result is NotImplemented else not result # Math operations def __pos__(self): """ >>> g = GlyphCoordinates([(1,2)]) >>> g GlyphCoordinates([(1, 2)]) >>> g2 = +g >>> g2 GlyphCoordinates([(1, 2)]) >>> g2.translate((1,0)) >>> g2 GlyphCoordinates([(2, 2)]) >>> g GlyphCoordinates([(1, 2)]) """ return self.copy() def __neg__(self): """ >>> g = GlyphCoordinates([(1,2)]) >>> g GlyphCoordinates([(1, 2)]) >>> g2 = -g >>> g2 GlyphCoordinates([(-1, -2)]) >>> g GlyphCoordinates([(1, 2)]) """ r = self.copy() a = r._a for i in range(len(a)): a[i] = -a[i] return r def __round__(self): """ Note: This is Python 3 only. Python 2 does not call __round__. As such, we cannot test this method either. :( """ r = self.copy() r.toInt() return r def __add__(self, other): return self.copy().__iadd__(other) def __sub__(self, other): return self.copy().__isub__(other) def __mul__(self, other): return self.copy().__imul__(other) def __truediv__(self, other): return self.copy().__itruediv__(other) __radd__ = __add__ __rmul__ = __mul__ def __rsub__(self, other): return other + (-self) def __iadd__(self, other): """ >>> g = GlyphCoordinates([(1,2)]) >>> g += (.5,0) >>> g GlyphCoordinates([(1.5, 2.0)]) >>> g2 = GlyphCoordinates([(3,4)]) >>> g += g2 >>> g GlyphCoordinates([(4.5, 6.0)]) """ if isinstance(other, tuple): assert len(other) == 2 self.translate(other) return self if isinstance(other, GlyphCoordinates): if other.isFloat(): self._ensureFloat() other = other._a a = self._a assert len(a) == len(other) for i in range(len(a) // 2): self[i] = (a[2*i] + other[2*i], a[2*i+1] + other[2*i+1]) return self return NotImplemented def __isub__(self, other): """ >>> g = GlyphCoordinates([(1,2)]) >>> g -= (.5,0) >>> g GlyphCoordinates([(0.5, 2.0)]) >>> g2 = GlyphCoordinates([(3,4)]) >>> g -= g2 >>> g GlyphCoordinates([(-2.5, -2.0)]) """ if isinstance(other, tuple): assert len(other) == 2 self.translate((-other[0],-other[1])) return self if isinstance(other, GlyphCoordinates): if other.isFloat(): self._ensureFloat() other = other._a a = self._a assert len(a) == len(other) for i in range(len(a) // 2): self[i] = (a[2*i] - other[2*i], a[2*i+1] - other[2*i+1]) return self return NotImplemented def __imul__(self, other): """ >>> g = GlyphCoordinates([(1,2)]) >>> g *= (2,.5) >>> g *= 2 >>> g GlyphCoordinates([(4.0, 2.0)]) >>> g = GlyphCoordinates([(1,2)]) >>> g *= 2 >>> g GlyphCoordinates([(2, 4)]) """ if isinstance(other, Number): other = (other, other) if isinstance(other, tuple): if other == (1,1): return self assert len(other) == 2 self.scale(other) return self return NotImplemented def __itruediv__(self, other): """ >>> g = GlyphCoordinates([(1,3)]) >>> g /= (.5,1.5) >>> g /= 2 >>> g GlyphCoordinates([(1.0, 1.0)]) """ if isinstance(other, Number): other = (other, other) if isinstance(other, tuple): if other == (1,1): return self assert len(other) == 2 self.scale((1./other[0],1./other[1])) return self return NotImplemented def __bool__(self): """ >>> g = GlyphCoordinates([]) >>> bool(g) False >>> g = GlyphCoordinates([(0,0), (0.,0)]) >>> bool(g) True >>> g = GlyphCoordinates([(0,0), (1,0)]) >>> bool(g) True >>> g = GlyphCoordinates([(0,.5), (0,0)]) >>> bool(g) True """ return bool(self._a) __nonzero__ = __bool__ def reprflag(flag): bin = "" if isinstance(flag, str): flag = byteord(flag) while flag: if flag & 0x01: bin = "1" + bin else: bin = "0" + bin flag = flag >> 1 bin = (14 - len(bin)) * "0" + bin return bin if __name__ == "__main__": import doctest, sys sys.exit(doctest.testmod().failed)