| """_g_l_y_f.py -- Converter classes for the 'glyf' table.""" |
| |
| 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, |
| floatToFixedToStr as fl2str, |
| strToFixedToFloat as str2fl, |
| 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'] |
| pos = int(loca[0]) |
| nextPos = 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 |
| nextPos = int(loca[i+1]) |
| glyphdata = data[pos:nextPos] |
| if len(glyphdata) != (nextPos - pos): |
| raise ttLib.TTLibError("not enough 'glyf' table data") |
| glyph = Glyph(glyphdata) |
| self.glyphs[glyphName] = glyph |
| pos = nextPos |
| if len(data) - nextPos >= 4: |
| log.warning( |
| "too much 'glyf' table data: expected %d, received %d bytes", |
| nextPos, 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) |
| if not data: |
| # As a special case when all glyph in the font are empty, add a zero byte |
| # to the table, so that OTS doesn’t reject it, and to make the table work |
| # on Windows as well. |
| # See https://github.com/khaledhosny/ots/issues/52 |
| data = b"\0" |
| 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: |
| if glyphName not in self: |
| log.warning("glyph '%s' does not exist in glyf table", glyphName) |
| continue |
| 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) |
| |
| def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None): |
| """Compute the four "phantom points" for the given glyph from its bounding box |
| and the horizontal and vertical advance widths and sidebearings stored in the |
| ttFont's "hmtx" and "vmtx" tables. |
| |
| If the ttFont doesn't contain a "vmtx" table, the hhea.ascent is used as the |
| vertical origin, and the head.unitsPerEm as the vertical advance. |
| |
| The "defaultVerticalOrigin" (Optional[int]) is needed when the ttFont contains |
| neither a "vmtx" nor an "hhea" table, as may happen with 'sparse' masters. |
| The value should be the hhea.ascent of the default master. |
| |
| https://docs.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantoms |
| """ |
| glyph = self[glyphName] |
| assert glyphName in ttFont["hmtx"].metrics, ttFont["hmtx"].metrics |
| horizontalAdvanceWidth, leftSideBearing = ttFont["hmtx"].metrics[glyphName] |
| if not hasattr(glyph, 'xMin'): |
| glyph.recalcBounds(self) |
| leftSideX = glyph.xMin - leftSideBearing |
| rightSideX = leftSideX + horizontalAdvanceWidth |
| if "vmtx" in ttFont: |
| verticalAdvanceWidth, topSideBearing = ttFont["vmtx"].metrics[glyphName] |
| topSideY = topSideBearing + glyph.yMax |
| else: |
| # without vmtx, use ascent as vertical origin and UPEM as vertical advance |
| # like HarfBuzz does |
| verticalAdvanceWidth = ttFont["head"].unitsPerEm |
| if "hhea" in ttFont: |
| topSideY = ttFont["hhea"].ascent |
| else: |
| # sparse masters may not contain an hhea table; use the ascent |
| # of the default master as the vertical origin |
| if defaultVerticalOrigin is not None: |
| topSideY = defaultVerticalOrigin |
| else: |
| log.warning( |
| "font is missing both 'vmtx' and 'hhea' tables, " |
| "and no 'defaultVerticalOrigin' was provided; " |
| "the vertical phantom points may be incorrect." |
| ) |
| topSideY = verticalAdvanceWidth |
| bottomSideY = topSideY - verticalAdvanceWidth |
| return [ |
| (leftSideX, 0), |
| (rightSideX, 0), |
| (0, topSideY), |
| (0, bottomSideY), |
| ] |
| |
| def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None): |
| """Return glyph coordinates and controls as expected by "gvar" table. |
| |
| The coordinates includes four "phantom points" for the glyph metrics, |
| as mandated by the "gvar" spec. |
| |
| The glyph controls is a namedtuple with the following attributes: |
| - numberOfContours: -1 for composite glyphs. |
| - endPts: list of indices of end points for each contour in simple |
| glyphs, or component indices in composite glyphs (used for IUP |
| optimization). |
| - flags: array of contour point flags for simple glyphs (None for |
| composite glyphs). |
| - components: list of base glyph names (str) for each component in |
| composite glyphs (None for simple glyphs). |
| |
| The "ttFont" and "defaultVerticalOrigin" args are used to compute the |
| "phantom points" (see "getPhantomPoints" method). |
| |
| Return None if the requested glyphName is not present. |
| """ |
| if glyphName not in self.glyphs: |
| return None |
| glyph = self[glyphName] |
| if glyph.isComposite(): |
| coords = GlyphCoordinates( |
| [(getattr(c, 'x', 0), getattr(c, 'y', 0)) for c in glyph.components] |
| ) |
| controls = _GlyphControls( |
| numberOfContours=glyph.numberOfContours, |
| endPts=list(range(len(glyph.components))), |
| flags=None, |
| components=[c.glyphName for c in glyph.components], |
| ) |
| else: |
| coords, endPts, flags = glyph.getCoordinates(self) |
| coords = coords.copy() |
| controls = _GlyphControls( |
| numberOfContours=glyph.numberOfContours, |
| endPts=endPts, |
| flags=flags, |
| components=None, |
| ) |
| # Add phantom points for (left, right, top, bottom) positions. |
| phantomPoints = self.getPhantomPoints( |
| glyphName, ttFont, defaultVerticalOrigin=defaultVerticalOrigin |
| ) |
| coords.extend(phantomPoints) |
| return coords, controls |
| |
| def setCoordinates(self, glyphName, coord, ttFont): |
| """Set coordinates and metrics for the given glyph. |
| |
| "coord" is an array of GlyphCoordinates which must include the "phantom |
| points" as the last four coordinates. |
| |
| Both the horizontal/vertical advances and left/top sidebearings in "hmtx" |
| and "vmtx" tables (if any) are updated from four phantom points and |
| the glyph's bounding boxes. |
| """ |
| # TODO: Create new glyph if not already present |
| assert glyphName in self.glyphs |
| glyph = self[glyphName] |
| |
| # Handle phantom points for (left, right, top, bottom) positions. |
| assert len(coord) >= 4 |
| leftSideX = coord[-4][0] |
| rightSideX = coord[-3][0] |
| topSideY = coord[-2][1] |
| bottomSideY = coord[-1][1] |
| |
| coord = coord[:-4] |
| |
| if glyph.isComposite(): |
| assert len(coord) == len(glyph.components) |
| for p, comp in zip(coord, glyph.components): |
| if hasattr(comp, 'x'): |
| comp.x, comp.y = p |
| elif glyph.numberOfContours == 0: |
| assert len(coord) == 0 |
| else: |
| assert len(coord) == len(glyph.coordinates) |
| glyph.coordinates = GlyphCoordinates(coord) |
| |
| glyph.recalcBounds(self) |
| |
| horizontalAdvanceWidth = otRound(rightSideX - leftSideX) |
| if horizontalAdvanceWidth < 0: |
| # unlikely, but it can happen, see: |
| # https://github.com/fonttools/fonttools/pull/1198 |
| horizontalAdvanceWidth = 0 |
| leftSideBearing = otRound(glyph.xMin - leftSideX) |
| ttFont["hmtx"].metrics[glyphName] = horizontalAdvanceWidth, leftSideBearing |
| |
| if "vmtx" in ttFont: |
| verticalAdvanceWidth = otRound(topSideY - bottomSideY) |
| if verticalAdvanceWidth < 0: # unlikely but do the same as horizontal |
| verticalAdvanceWidth = 0 |
| topSideBearing = otRound(topSideY - glyph.yMax) |
| ttFont["vmtx"].metrics[glyphName] = verticalAdvanceWidth, topSideBearing |
| |
| |
| _GlyphControls = namedtuple( |
| "_GlyphControls", "numberOfContours endPts flags components" |
| ) |
| |
| |
| 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 |
| initialMaxComponentDepth = maxComponentDepth |
| 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, componentDepth = baseGlyph.getCompositeMaxpValues( |
| glyfTable, initialMaxComponentDepth + 1) |
| maxComponentDepth = max(maxComponentDepth, componentDepth) |
| 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.frombytes(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.tobytes()) |
| 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).tobytes() |
| 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.tobytes() |
| compressedXs = compressedXs.tobytes() |
| compressedYs = compressedYs.tobytes() |
| |
| 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) |
| coordinates = GlyphCoordinates(coordinates) |
| if hasattr(compo, "firstPt"): |
| # component uses two reference points: we apply the transform _before_ |
| # computing the offset between the points |
| if hasattr(compo, "transform"): |
| coordinates.transform(compo.transform) |
| x1,y1 = allCoords[compo.firstPt] |
| x2,y2 = coordinates[compo.secondPt] |
| move = x1-x2, y1-y2 |
| coordinates.translate(move) |
| else: |
| # component uses XY offsets |
| move = compo.x, compo.y |
| 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: |
| if self.isComposite(): |
| if hasattr(self, "program"): |
| del self.program |
| else: |
| 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.tobytes() |
| |
| 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 = [flagOnCurve & f for f in 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: |
| # Skip a final lineTo(), as it is implied by |
| # pen.closePath() |
| if len(contour) > 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] & flagOnCurve == 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", fl2str(transform[0][0], 14)), |
| ("scale01", fl2str(transform[0][1], 14)), |
| ("scale10", fl2str(transform[1][0], 14)), |
| ("scaley", fl2str(transform[1][1], 14)), |
| ] |
| elif transform[0][0] != transform[1][1]: |
| attrs = attrs + [ |
| ("scalex", fl2str(transform[0][0], 14)), |
| ("scaley", fl2str(transform[1][1], 14)), |
| ] |
| else: |
| attrs = attrs + [("scale", fl2str(transform[0][0], 14))] |
| 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 = str2fl(attrs["scalex"], 14) |
| scale01 = str2fl(attrs["scale01"], 14) |
| scale10 = str2fl(attrs["scale10"], 14) |
| scaley = str2fl(attrs["scaley"], 14) |
| self.transform = [[scalex, scale01], [scale10, scaley]] |
| elif "scalex" in attrs: |
| scalex = str2fl(attrs["scalex"], 14) |
| scaley = str2fl(attrs["scaley"], 14) |
| self.transform = [[scalex, 0], [0, scaley]] |
| elif "scale" in attrs: |
| scale = str2fl(attrs["scale"], 14) |
| 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) |