| """_g_l_y_f.py -- Converter classes for the 'glyf' table.""" |
| |
| from collections import namedtuple |
| from fontTools.misc import sstruct |
| from fontTools import ttLib |
| from fontTools import version |
| from fontTools.misc.transform import DecomposedTransform |
| from fontTools.misc.textTools import tostr, safeEval, pad |
| from fontTools.misc.arrayTools import updateBounds, pointInRect |
| from fontTools.misc.bezierTools import calcQuadraticBounds |
| from fontTools.misc.fixedTools import ( |
| fixedToFloat as fi2fl, |
| floatToFixed as fl2fi, |
| floatToFixedToStr as fl2str, |
| strToFixedToFloat as str2fl, |
| ) |
| from fontTools.misc.roundTools import noRound, otRound |
| from fontTools.misc.vector import Vector |
| from numbers import Number |
| from . import DefaultTable |
| from . import ttProgram |
| import sys |
| import struct |
| import array |
| import logging |
| import math |
| import os |
| from fontTools.misc import xmlWriter |
| from fontTools.misc.filenames import userNameToFileName |
| from fontTools.misc.loggingTools import deprecateFunction |
| from enum import IntFlag |
| from functools import partial |
| from types import SimpleNamespace |
| from typing import Set |
| |
| 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): |
| """Glyph Data Table |
| |
| This class represents the `glyf <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf>`_ |
| table, which contains outlines for glyphs in TrueType format. In many cases, |
| it is easier to access and manipulate glyph outlines through the ``GlyphSet`` |
| object returned from :py:meth:`fontTools.ttLib.ttFont.getGlyphSet`:: |
| |
| >> from fontTools.pens.boundsPen import BoundsPen |
| >> glyphset = font.getGlyphSet() |
| >> bp = BoundsPen(glyphset) |
| >> glyphset["A"].draw(bp) |
| >> bp.bounds |
| (19, 0, 633, 716) |
| |
| However, this class can be used for low-level access to the ``glyf`` table data. |
| Objects of this class support dictionary-like access, mapping glyph names to |
| :py:class:`Glyph` objects:: |
| |
| >> glyf = font["glyf"] |
| >> len(glyf["Aacute"].components) |
| 2 |
| |
| Note that when adding glyphs to the font via low-level access to the ``glyf`` |
| table, the new glyphs must also be added to the ``hmtx``/``vmtx`` table:: |
| |
| >> font["glyf"]["divisionslash"] = Glyph() |
| >> font["hmtx"]["divisionslash"] = (640, 0) |
| |
| """ |
| |
| dependencies = ["fvar"] |
| |
| # 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): |
| self.axisTags = ( |
| [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else [] |
| ) |
| loca = ttFont["loca"] |
| pos = int(loca[0]) |
| nextPos = 0 |
| noname = 0 |
| self.glyphs = {} |
| self.glyphOrder = glyphOrder = ttFont.getGlyphOrder() |
| self._reverseGlyphOrder = {} |
| 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 |
| self.ensureDecompiled() |
| |
| def ensureDecompiled(self, recurse=False): |
| # The recurse argument is unused, but part of the signature of |
| # ensureDecompiled across the library. |
| for glyph in self.glyphs.values(): |
| glyph.expand(self) |
| |
| def compile(self, ttFont): |
| self.axisTags = ( |
| [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else [] |
| ) |
| 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 |
| boundsDone = set() |
| for glyphName in self.glyphOrder: |
| glyph = self.glyphs[glyphName] |
| glyphData = glyph.compile(self, recalcBBoxes, boundsDone=boundsDone) |
| 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 = b"".join(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: |
| glyph = self.get(glyphName) |
| if glyph is None: |
| log.warning("glyph '%s' does not exist in glyf table", glyphName) |
| continue |
| if glyph.numberOfContours: |
| if splitGlyphs: |
| glyphPath = userNameToFileName( |
| tostr(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): |
| """Sets the glyph order |
| |
| Args: |
| glyphOrder ([str]): List of glyph names in order. |
| """ |
| self.glyphOrder = glyphOrder |
| self._reverseGlyphOrder = {} |
| |
| def getGlyphName(self, glyphID): |
| """Returns the name for the glyph with the given ID. |
| |
| Raises a ``KeyError`` if the glyph name is not found in the font. |
| """ |
| return self.glyphOrder[glyphID] |
| |
| def _buildReverseGlyphOrderDict(self): |
| self._reverseGlyphOrder = d = {} |
| for glyphID, glyphName in enumerate(self.glyphOrder): |
| d[glyphName] = glyphID |
| |
| def getGlyphID(self, glyphName): |
| """Returns the ID of the glyph with the given name. |
| |
| Raises a ``ValueError`` if the glyph is not found in the font. |
| """ |
| glyphOrder = self.glyphOrder |
| id = getattr(self, "_reverseGlyphOrder", {}).get(glyphName) |
| if id is None or id >= len(glyphOrder) or glyphOrder[id] != glyphName: |
| self._buildReverseGlyphOrderDict() |
| id = self._reverseGlyphOrder.get(glyphName) |
| if id is None: |
| raise ValueError(glyphName) |
| return id |
| |
| def removeHinting(self): |
| """Removes TrueType hints from all glyphs in the glyphset. |
| |
| See :py:meth:`Glyph.removeHinting`. |
| """ |
| 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 get(self, glyphName, default=None): |
| glyph = self.glyphs.get(glyphName, default) |
| if glyph is not None: |
| glyph.expand(self) |
| return glyph |
| |
| 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, hMetrics, vMetrics=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. |
| |
| 'hMetrics' should be ttFont['hmtx'].metrics. |
| |
| 'vMetrics' should be ttFont['vmtx'].metrics if there is "vmtx" or None otherwise. |
| If there is no vMetrics passed in, vertical phantom points are set to the zero coordinate. |
| |
| https://docs.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantoms |
| """ |
| glyph = self[glyphName] |
| if not hasattr(glyph, "xMin"): |
| glyph.recalcBounds(self) |
| |
| horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName] |
| leftSideX = glyph.xMin - leftSideBearing |
| rightSideX = leftSideX + horizontalAdvanceWidth |
| |
| if vMetrics: |
| verticalAdvanceWidth, topSideBearing = vMetrics[glyphName] |
| topSideY = topSideBearing + glyph.yMax |
| bottomSideY = topSideY - verticalAdvanceWidth |
| else: |
| bottomSideY = topSideY = 0 |
| |
| return [ |
| (leftSideX, 0), |
| (rightSideX, 0), |
| (0, topSideY), |
| (0, bottomSideY), |
| ] |
| |
| def _getCoordinatesAndControls( |
| self, glyphName, hMetrics, vMetrics=None, *, round=otRound |
| ): |
| """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 "hMetrics" and vMetrics are used to compute the "phantom points" (see |
| the "_getPhantomPoints" method). |
| |
| Return None if the requested glyphName is not present. |
| """ |
| glyph = self.get(glyphName) |
| if glyph is None: |
| return None |
| 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, getattr(c, "transform", None)) |
| for c in glyph.components |
| ], |
| ) |
| elif glyph.isVarComposite(): |
| coords = [] |
| controls = [] |
| |
| for component in glyph.components: |
| ( |
| componentCoords, |
| componentControls, |
| ) = component.getCoordinatesAndControls() |
| coords.extend(componentCoords) |
| controls.extend(componentControls) |
| |
| coords = GlyphCoordinates(coords) |
| |
| controls = _GlyphControls( |
| numberOfContours=glyph.numberOfContours, |
| endPts=list(range(len(coords))), |
| flags=None, |
| components=[ |
| (c.glyphName, getattr(c, "flags", None)) 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, hMetrics, vMetrics) |
| coords.extend(phantomPoints) |
| coords.toInt(round=round) |
| return coords, controls |
| |
| def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None): |
| """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. |
| |
| The "hMetrics" and vMetrics are used to propagate "phantom points" |
| into "hmtx" and "vmtx" tables if desired. (see the "_getPhantomPoints" |
| method). |
| """ |
| 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.isVarComposite(): |
| for comp in glyph.components: |
| coord = comp.setCoordinates(coord) |
| assert not coord |
| elif glyph.numberOfContours == 0: |
| assert len(coord) == 0 |
| else: |
| assert len(coord) == len(glyph.coordinates) |
| glyph.coordinates = GlyphCoordinates(coord) |
| |
| glyph.recalcBounds(self, boundsDone=set()) |
| |
| 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) |
| hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing |
| |
| if vMetrics is not None: |
| verticalAdvanceWidth = otRound(topSideY - bottomSideY) |
| if verticalAdvanceWidth < 0: # unlikely but do the same as horizontal |
| verticalAdvanceWidth = 0 |
| topSideBearing = otRound(topSideY - glyph.yMax) |
| vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing |
| |
| # Deprecated |
| |
| def _synthesizeVMetrics(self, glyphName, ttFont, defaultVerticalOrigin): |
| """This method is wrong and deprecated. |
| For rationale see: |
| https://github.com/fonttools/fonttools/pull/2266/files#r613569473 |
| """ |
| vMetrics = getattr(ttFont.get("vmtx"), "metrics", None) |
| if vMetrics is None: |
| verticalAdvanceWidth = ttFont["head"].unitsPerEm |
| topSideY = getattr(ttFont.get("hhea"), "ascent", None) |
| if topSideY is None: |
| if defaultVerticalOrigin is not None: |
| topSideY = defaultVerticalOrigin |
| else: |
| topSideY = verticalAdvanceWidth |
| glyph = self[glyphName] |
| glyph.recalcBounds(self) |
| topSideBearing = otRound(topSideY - glyph.yMax) |
| vMetrics = {glyphName: (verticalAdvanceWidth, topSideBearing)} |
| return vMetrics |
| |
| @deprecateFunction("use '_getPhantomPoints' instead", category=DeprecationWarning) |
| def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None): |
| """Old public name for self._getPhantomPoints(). |
| See: https://github.com/fonttools/fonttools/pull/2266""" |
| hMetrics = ttFont["hmtx"].metrics |
| vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin) |
| return self._getPhantomPoints(glyphName, hMetrics, vMetrics) |
| |
| @deprecateFunction( |
| "use '_getCoordinatesAndControls' instead", category=DeprecationWarning |
| ) |
| def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None): |
| """Old public name for self._getCoordinatesAndControls(). |
| See: https://github.com/fonttools/fonttools/pull/2266""" |
| hMetrics = ttFont["hmtx"].metrics |
| vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin) |
| return self._getCoordinatesAndControls(glyphName, hMetrics, vMetrics) |
| |
| @deprecateFunction("use '_setCoordinates' instead", category=DeprecationWarning) |
| def setCoordinates(self, glyphName, ttFont): |
| """Old public name for self._setCoordinates(). |
| See: https://github.com/fonttools/fonttools/pull/2266""" |
| hMetrics = ttFont["hmtx"].metrics |
| vMetrics = getattr(ttFont.get("vmtx"), "metrics", None) |
| self._setCoordinates(glyphName, hMetrics, vMetrics) |
| |
| |
| _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 |
| flagCubic = 0x80 |
| |
| # These flags are kept for XML output after decompiling the coordinates |
| keepFlags = flagOnCurve + flagOverlapSimple + flagCubic |
| |
| _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.extend(struct.pack(">h", coord)) |
| |
| |
| 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): |
| """This class represents an individual TrueType glyph. |
| |
| TrueType glyph objects come in two flavours: simple and composite. Simple |
| glyph objects contain contours, represented via the ``.coordinates``, |
| ``.flags``, ``.numberOfContours``, and ``.endPtsOfContours`` attributes; |
| composite glyphs contain components, available through the ``.components`` |
| attributes. |
| |
| Because the ``.coordinates`` attribute (and other simple glyph attributes mentioned |
| above) is only set on simple glyphs and the ``.components`` attribute is only |
| set on composite glyphs, it is necessary to use the :py:meth:`isComposite` |
| method to test whether a glyph is simple or composite before attempting to |
| access its data. |
| |
| For a composite glyph, the components can also be accessed via array-like access:: |
| |
| >> assert(font["glyf"]["Aacute"].isComposite()) |
| >> font["glyf"]["Aacute"][0] |
| <fontTools.ttLib.tables._g_l_y_f.GlyphComponent at 0x1027b2ee0> |
| |
| """ |
| |
| def __init__(self, data=b""): |
| 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) |
| elif self.isVarComposite(): |
| self.decompileVarComponents(data, glyfTable) |
| else: |
| self.decompileCoordinates(data) |
| |
| def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None): |
| 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 b"" |
| |
| if recalcBBoxes: |
| self.recalcBounds(glyfTable, boundsDone=boundsDone) |
| |
| data = sstruct.pack(glyphHeaderFormat, self) |
| if self.isComposite(): |
| data = data + self.compileComponents(glyfTable) |
| elif self.isVarComposite(): |
| data = data + self.compileVarComponents(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") |
| elif self.isVarComposite(): |
| for compo in self.components: |
| compo.toXML(writer, ttFont) |
| haveInstructions = False |
| 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)) |
| if self.flags[j] & flagCubic: |
| attrs.append(("cubic", 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 = bytearray() |
| 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 = bool(safeEval(attrs["on"])) |
| if "overlap" in attrs and bool(safeEval(attrs["overlap"])): |
| flag |= flagOverlapSimple |
| if "cubic" in attrs and bool(safeEval(attrs["cubic"])): |
| flag |= flagCubic |
| flags.append(flag) |
| 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 == "varComponent": |
| if self.numberOfContours > 0: |
| raise ttLib.TTLibError("can't mix composites and contours in glyph") |
| self.numberOfContours = -2 |
| if not hasattr(self, "components"): |
| self.components = [] |
| component = GlyphVarComponent() |
| 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() or self.isVarComposite() |
| 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 decompileVarComponents(self, data, glyfTable): |
| self.components = [] |
| while len(data) >= GlyphVarComponent.MIN_SIZE: |
| component = GlyphVarComponent() |
| data = component.decompile(data, glyfTable) |
| self.components.append(component) |
| |
| def decompileCoordinates(self, data): |
| endPtsOfContours = array.array("H") |
| endPtsOfContours.frombytes(data[: 2 * self.numberOfContours]) |
| if sys.byteorder != "big": |
| endPtsOfContours.byteswap() |
| self.endPtsOfContours = endPtsOfContours.tolist() |
| |
| pos = 2 * self.numberOfContours |
| (instructionLength,) = struct.unpack(">h", data[pos : pos + 2]) |
| self.program = ttProgram.Program() |
| self.program.fromBytecode(data[pos + 2 : pos + 2 + instructionLength]) |
| pos += 2 + instructionLength |
| nCoordinates = self.endPtsOfContours[-1] + 1 |
| flags, xCoordinates, yCoordinates = self.decompileCoordinatesRaw( |
| nCoordinates, data, pos |
| ) |
| |
| # 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" |
| for i in range(len(flags)): |
| flags[i] &= keepFlags |
| self.flags = flags |
| |
| def decompileCoordinatesRaw(self, nCoordinates, data, pos=0): |
| # unpack flags and prepare unpacking of coordinates |
| flags = bytearray(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 |
| j = 0 |
| while True: |
| flag = data[pos] |
| pos += 1 |
| repeat = 1 |
| if flag & flagRepeat: |
| repeat = data[pos] + 1 |
| pos += 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" |
| # unpack raw coordinates, krrrrrr-tching! |
| xDataLen = struct.calcsize(xFormat) |
| yDataLen = struct.calcsize(yFormat) |
| if len(data) - pos - (xDataLen + yDataLen) >= 4: |
| log.warning( |
| "too much glyph data: %d excess bytes", |
| len(data) - pos - (xDataLen + yDataLen), |
| ) |
| xCoordinates = struct.unpack(xFormat, data[pos : pos + xDataLen]) |
| yCoordinates = struct.unpack( |
| yFormat, data[pos + xDataLen : pos + 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 compileVarComponents(self, glyfTable): |
| return b"".join(c.compile(glyfTable) for c in self.components) |
| |
| 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() |
| 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 b"".join(data) |
| |
| def compileDeltasGreedy(self, flags, deltas): |
| # Implements greedy algorithm for packing coordinate deltas: |
| # uses shortest representation one coordinate at a time. |
| compressedFlags = bytearray() |
| compressedXs = bytearray() |
| compressedYs = bytearray() |
| 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 |
| compressedXs.append(x) |
| else: |
| compressedXs.extend(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 |
| compressedYs.append(y) |
| else: |
| compressedYs.extend(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 |
| 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 = bytearray() |
| compressedXs = bytearray() |
| compressedYs = bytearray() |
| 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 |
| |
| return (compressedFlags, compressedXs, compressedYs) |
| |
| def recalcBounds(self, glyfTable, *, boundsDone=None): |
| """Recalculates the bounds of the glyph. |
| |
| Each glyph object stores its bounding box in the |
| ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be |
| recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds |
| must be provided to resolve component bounds. |
| """ |
| if self.isComposite() and self.tryRecalcBoundsComposite( |
| glyfTable, boundsDone=boundsDone |
| ): |
| return |
| try: |
| coords, endPts, flags = self.getCoordinates(glyfTable) |
| self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds() |
| except NotImplementedError: |
| pass |
| |
| def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None): |
| """Try recalculating the bounds of a composite glyph that has |
| certain constrained properties. Namely, none of the components |
| have a transform other than an integer translate, and none |
| uses the anchor points. |
| |
| Each glyph object stores its bounding box in the |
| ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be |
| recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds |
| must be provided to resolve component bounds. |
| |
| Return True if bounds were calculated, False otherwise. |
| """ |
| for compo in self.components: |
| if hasattr(compo, "firstPt") or hasattr(compo, "transform"): |
| return False |
| if not float(compo.x).is_integer() or not float(compo.y).is_integer(): |
| return False |
| |
| # All components are untransformed and have an integer x/y translate |
| bounds = None |
| for compo in self.components: |
| glyphName = compo.glyphName |
| g = glyfTable[glyphName] |
| |
| if boundsDone is None or glyphName not in boundsDone: |
| g.recalcBounds(glyfTable, boundsDone=boundsDone) |
| if boundsDone is not None: |
| boundsDone.add(glyphName) |
| |
| x, y = compo.x, compo.y |
| bounds = updateBounds(bounds, (g.xMin + x, g.yMin + y)) |
| bounds = updateBounds(bounds, (g.xMax + x, g.yMax + y)) |
| |
| if bounds is None: |
| bounds = (0, 0, 0, 0) |
| self.xMin, self.yMin, self.xMax, self.yMax = bounds |
| return True |
| |
| def isComposite(self): |
| """Test whether a glyph has components""" |
| if hasattr(self, "data"): |
| return struct.unpack(">h", self.data[:2])[0] == -1 if self.data else False |
| else: |
| return self.numberOfContours == -1 |
| |
| def isVarComposite(self): |
| """Test whether a glyph has variable components""" |
| if hasattr(self, "data"): |
| return struct.unpack(">h", self.data[:2])[0] == -2 if self.data else False |
| else: |
| return self.numberOfContours == -2 |
| |
| def getCoordinates(self, glyfTable): |
| """Return the coordinates, end points and flags |
| |
| This method returns three values: A :py:class:`GlyphCoordinates` object, |
| a list of the indexes of the final points of each contour (allowing you |
| to split up the coordinates list into contours) and a list of flags. |
| |
| On simple glyphs, this method returns information from the glyph's own |
| contours; on composite glyphs, it "flattens" all components recursively |
| to return a list of coordinates representing all the components involved |
| in the glyph. |
| |
| To interpret the flags for each point, see the "Simple Glyph Flags" |
| section of the `glyf table specification <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description>`. |
| """ |
| |
| if self.numberOfContours > 0: |
| return self.coordinates, self.endPtsOfContours, self.flags |
| elif self.isComposite(): |
| # it's a composite |
| allCoords = GlyphCoordinates() |
| allFlags = bytearray() |
| 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 |
| elif self.isVarComposite(): |
| raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs") |
| else: |
| return GlyphCoordinates(), [], bytearray() |
| |
| def getComponentNames(self, glyfTable): |
| """Returns a list of names of component glyphs used in this glyph |
| |
| This method can be used on simple glyphs (in which case it returns an |
| empty list) or composite glyphs. |
| """ |
| if hasattr(self, "data") and self.isVarComposite(): |
| # TODO(VarComposite) Add implementation without expanding glyph |
| self.expand(glyfTable) |
| |
| if not hasattr(self, "data"): |
| if self.isComposite() or self.isVarComposite(): |
| 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 |
| elif self.isVarComposite(): |
| pass # Doesn't have hinting |
| 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 = bytearray(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] |
| elif self.isComposite(): |
| 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] |
| elif self.isVarComposite(): |
| i = 0 |
| MIN_SIZE = GlyphVarComponent.MIN_SIZE |
| while len(data[i : i + MIN_SIZE]) >= MIN_SIZE: |
| size = GlyphVarComponent.getSize(data[i : i + MIN_SIZE]) |
| i += size |
| data = data[:i] |
| |
| self.data = data |
| |
| def removeHinting(self): |
| """Removes TrueType hinting instructions from the glyph.""" |
| self.trim(remove_hinting=True) |
| |
| def draw(self, pen, glyfTable, offset=0): |
| """Draws the glyph using the supplied pen object. |
| |
| Arguments: |
| pen: An object conforming to the pen protocol. |
| glyfTable: A :py:class:`table__g_l_y_f` object, to resolve components. |
| offset (int): A horizontal offset. If provided, all coordinates are |
| translated by this offset. |
| """ |
| |
| 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 |
| maybeInt = lambda v: int(v) if v == int(v) else v |
| for end in endPts: |
| end = end + 1 |
| contour = coordinates[start:end] |
| cFlags = [flagOnCurve & f for f in flags[start:end]] |
| cuFlags = [flagCubic & f for f in flags[start:end]] |
| start = end |
| if 1 not in cFlags: |
| assert all(cuFlags) or not any(cuFlags) |
| cubic = all(cuFlags) |
| if cubic: |
| count = len(contour) |
| assert count % 2 == 0, "Odd number of cubic off-curves undefined" |
| l = contour[-1] |
| f = contour[0] |
| p0 = (maybeInt((l[0] + f[0]) * 0.5), maybeInt((l[1] + f[1]) * 0.5)) |
| pen.moveTo(p0) |
| for i in range(0, count, 2): |
| p1 = contour[i] |
| p2 = contour[i + 1] |
| p4 = contour[i + 2 if i + 2 < count else 0] |
| p3 = ( |
| maybeInt((p2[0] + p4[0]) * 0.5), |
| maybeInt((p2[1] + p4[1]) * 0.5), |
| ) |
| pen.curveTo(p1, p2, p3) |
| else: |
| # 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 the contour 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] |
| cuFlags = cuFlags[firstOnCurve:] + cuFlags[: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: |
| cubicFlags = [f for f in cuFlags[: nextOnCurve - 1]] |
| assert all(cubicFlags) or not any(cubicFlags) |
| cubic = any(cubicFlags) |
| if cubic: |
| assert all( |
| cubicFlags |
| ), "Mixed cubic and quadratic segment undefined" |
| |
| count = nextOnCurve |
| assert ( |
| count >= 3 |
| ), "At least two cubic off-curve points required" |
| assert ( |
| count - 1 |
| ) % 2 == 0, "Odd number of cubic off-curves undefined" |
| for i in range(0, count - 3, 2): |
| p1 = contour[i] |
| p2 = contour[i + 1] |
| p4 = contour[i + 2] |
| p3 = ( |
| maybeInt((p2[0] + p4[0]) * 0.5), |
| maybeInt((p2[1] + p4[1]) * 0.5), |
| ) |
| lastOnCurve = p3 |
| pen.curveTo(p1, p2, p3) |
| pen.curveTo(*contour[count - 3 : count]) |
| else: |
| pen.qCurveTo(*contour[:nextOnCurve]) |
| contour = contour[nextOnCurve:] |
| cFlags = cFlags[nextOnCurve:] |
| cuFlags = cuFlags[nextOnCurve:] |
| pen.closePath() |
| |
| def drawPoints(self, pen, glyfTable, offset=0): |
| """Draw the glyph using the supplied pointPen. As 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 |
| |
| if cFlags[-1] & flagOnCurve: |
| segmentType = "line" |
| elif cFlags[-1] & flagCubic: |
| segmentType = "curve" |
| else: |
| segmentType = "qcurve" |
| for i, pt in enumerate(contour): |
| if cFlags[i] & flagOnCurve: |
| pen.addPoint(pt, segmentType=segmentType) |
| segmentType = "line" |
| else: |
| pen.addPoint(pt) |
| segmentType = "curve" if cFlags[i] & flagCubic else "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 |
| |
| |
| # Vector.__round__ uses the built-in (Banker's) `round` but we want |
| # to use otRound below |
| _roundv = partial(Vector.__round__, round=otRound) |
| |
| |
| def _is_mid_point(p0: tuple, p1: tuple, p2: tuple) -> bool: |
| # True if p1 is in the middle of p0 and p2, either before or after rounding |
| p0 = Vector(p0) |
| p1 = Vector(p1) |
| p2 = Vector(p2) |
| return ((p0 + p2) * 0.5).isclose(p1) or _roundv(p0) + _roundv(p2) == _roundv(p1) * 2 |
| |
| |
| def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: |
| """Drop impliable on-curve points from the (simple) glyph or glyphs. |
| |
| In TrueType glyf outlines, on-curve points can be implied when they are located at |
| the midpoint of the line connecting two consecutive off-curve points. |
| |
| If more than one glyphs are passed, these are assumed to be interpolatable masters |
| of the same glyph impliable, and thus only the on-curve points that are impliable |
| for all of them will actually be implied. |
| Composite glyphs or empty glyphs are skipped, only simple glyphs with 1 or more |
| contours are considered. |
| The input glyph(s) is/are modified in-place. |
| |
| Args: |
| interpolatable_glyphs: The glyph or glyphs to modify in-place. |
| |
| Returns: |
| The set of point indices that were dropped if any. |
| |
| Raises: |
| ValueError if simple glyphs are not in fact interpolatable because they have |
| different point flags or number of contours. |
| |
| Reference: |
| https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html |
| """ |
| staticAttributes = SimpleNamespace( |
| numberOfContours=None, flags=None, endPtsOfContours=None |
| ) |
| drop = None |
| simple_glyphs = [] |
| for i, glyph in enumerate(interpolatable_glyphs): |
| if glyph.numberOfContours < 1: |
| # ignore composite or empty glyphs |
| continue |
| |
| for attr in staticAttributes.__dict__: |
| expected = getattr(staticAttributes, attr) |
| found = getattr(glyph, attr) |
| if expected is None: |
| setattr(staticAttributes, attr, found) |
| elif expected != found: |
| raise ValueError( |
| f"Incompatible {attr} for glyph at master index {i}: " |
| f"expected {expected}, found {found}" |
| ) |
| |
| may_drop = set() |
| start = 0 |
| coords = glyph.coordinates |
| flags = staticAttributes.flags |
| endPtsOfContours = staticAttributes.endPtsOfContours |
| for last in endPtsOfContours: |
| for i in range(start, last + 1): |
| if not (flags[i] & flagOnCurve): |
| continue |
| prv = i - 1 if i > start else last |
| nxt = i + 1 if i < last else start |
| if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]: |
| continue |
| # we may drop the ith on-curve if halfway between previous/next off-curves |
| if not _is_mid_point(coords[prv], coords[i], coords[nxt]): |
| continue |
| |
| may_drop.add(i) |
| start = last + 1 |
| # we only want to drop if ALL interpolatable glyphs have the same implied oncurves |
| if drop is None: |
| drop = may_drop |
| else: |
| drop.intersection_update(may_drop) |
| |
| simple_glyphs.append(glyph) |
| |
| if drop: |
| # Do the actual dropping |
| flags = staticAttributes.flags |
| assert flags is not None |
| newFlags = array.array( |
| "B", (flags[i] for i in range(len(flags)) if i not in drop) |
| ) |
| |
| endPts = staticAttributes.endPtsOfContours |
| assert endPts is not None |
| newEndPts = [] |
| i = 0 |
| delta = 0 |
| for d in sorted(drop): |
| while d > endPts[i]: |
| newEndPts.append(endPts[i] - delta) |
| i += 1 |
| delta += 1 |
| while i < len(endPts): |
| newEndPts.append(endPts[i] - delta) |
| i += 1 |
| |
| for glyph in simple_glyphs: |
| coords = glyph.coordinates |
| glyph.coordinates = GlyphCoordinates( |
| coords[i] for i in range(len(coords)) if i not in drop |
| ) |
| glyph.flags = newFlags |
| glyph.endPtsOfContours = newEndPts |
| |
| return drop if drop is not None else set() |
| |
| |
| class GlyphComponent(object): |
| """Represents a component within a composite glyph. |
| |
| The component is represented internally with four attributes: ``glyphName``, |
| ``x``, ``y`` and ``transform``. If there is no "two-by-two" matrix (i.e |
| no scaling, reflection, or rotation; only translation), the ``transform`` |
| attribute is not present. |
| """ |
| |
| # The above documentation is not *completely* true, but is *true enough* because |
| # the rare firstPt/lastPt attributes are not totally supported and nobody seems to |
| # mind - see below. |
| |
| def __init__(self): |
| pass |
| |
| def getComponentInfo(self): |
| """Return information about the component |
| |
| This method returns a tuple of two values: the glyph name of the component's |
| base glyph, and a transformation matrix. As opposed to accessing the attributes |
| directly, ``getComponentInfo`` always returns a six-element tuple of the |
| component's transformation matrix, even when the two-by-two ``.transform`` |
| matrix is not present. |
| """ |
| # 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 |
| |
| |
| # |
| # Variable Composite glyphs |
| # https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md |
| # |
| |
| |
| class VarComponentFlags(IntFlag): |
| USE_MY_METRICS = 0x0001 |
| AXIS_INDICES_ARE_SHORT = 0x0002 |
| UNIFORM_SCALE = 0x0004 |
| HAVE_TRANSLATE_X = 0x0008 |
| HAVE_TRANSLATE_Y = 0x0010 |
| HAVE_ROTATION = 0x0020 |
| HAVE_SCALE_X = 0x0040 |
| HAVE_SCALE_Y = 0x0080 |
| HAVE_SKEW_X = 0x0100 |
| HAVE_SKEW_Y = 0x0200 |
| HAVE_TCENTER_X = 0x0400 |
| HAVE_TCENTER_Y = 0x0800 |
| GID_IS_24BIT = 0x1000 |
| AXES_HAVE_VARIATION = 0x2000 |
| RESET_UNSPECIFIED_AXES = 0x4000 |
| |
| |
| VarComponentTransformMappingValues = namedtuple( |
| "VarComponentTransformMappingValues", |
| ["flag", "fractionalBits", "scale", "defaultValue"], |
| ) |
| |
| VAR_COMPONENT_TRANSFORM_MAPPING = { |
| "translateX": VarComponentTransformMappingValues( |
| VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 |
| ), |
| "translateY": VarComponentTransformMappingValues( |
| VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0 |
| ), |
| "rotation": VarComponentTransformMappingValues( |
| VarComponentFlags.HAVE_ROTATION, 12, 180, 0 |
| ), |
| "scaleX": VarComponentTransformMappingValues( |
| VarComponentFlags.HAVE_SCALE_X, 10, 1, 1 |
| ), |
| "scaleY": VarComponentTransformMappingValues( |
| VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1 |
| ), |
| "skewX": VarComponentTransformMappingValues( |
| VarComponentFlags.HAVE_SKEW_X, 12, -180, 0 |
| ), |
| "skewY": VarComponentTransformMappingValues( |
| VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0 |
| ), |
| "tCenterX": VarComponentTransformMappingValues( |
| VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0 |
| ), |
| "tCenterY": VarComponentTransformMappingValues( |
| VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0 |
| ), |
| } |
| |
| |
| class GlyphVarComponent(object): |
| MIN_SIZE = 5 |
| |
| def __init__(self): |
| self.location = {} |
| self.transform = DecomposedTransform() |
| |
| @staticmethod |
| def getSize(data): |
| size = 5 |
| flags = struct.unpack(">H", data[:2])[0] |
| numAxes = int(data[2]) |
| |
| if flags & VarComponentFlags.GID_IS_24BIT: |
| size += 1 |
| |
| size += numAxes |
| if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: |
| size += 2 * numAxes |
| else: |
| axisIndices = array.array("B", data[:numAxes]) |
| size += numAxes |
| |
| for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): |
| if flags & mapping_values.flag: |
| size += 2 |
| |
| return size |
| |
| def decompile(self, data, glyfTable): |
| flags = struct.unpack(">H", data[:2])[0] |
| self.flags = int(flags) |
| data = data[2:] |
| |
| numAxes = int(data[0]) |
| data = data[1:] |
| |
| if flags & VarComponentFlags.GID_IS_24BIT: |
| glyphID = int(struct.unpack(">L", b"\0" + data[:3])[0]) |
| data = data[3:] |
| flags ^= VarComponentFlags.GID_IS_24BIT |
| else: |
| glyphID = int(struct.unpack(">H", data[:2])[0]) |
| data = data[2:] |
| self.glyphName = glyfTable.getGlyphName(int(glyphID)) |
| |
| if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: |
| axisIndices = array.array("H", data[: 2 * numAxes]) |
| if sys.byteorder != "big": |
| axisIndices.byteswap() |
| data = data[2 * numAxes :] |
| flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT |
| else: |
| axisIndices = array.array("B", data[:numAxes]) |
| data = data[numAxes:] |
| assert len(axisIndices) == numAxes |
| axisIndices = list(axisIndices) |
| |
| axisValues = array.array("h", data[: 2 * numAxes]) |
| if sys.byteorder != "big": |
| axisValues.byteswap() |
| data = data[2 * numAxes :] |
| assert len(axisValues) == numAxes |
| axisValues = [fi2fl(v, 14) for v in axisValues] |
| |
| self.location = { |
| glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues) |
| } |
| |
| def read_transform_component(data, values): |
| if flags & values.flag: |
| return ( |
| data[2:], |
| fi2fl(struct.unpack(">h", data[:2])[0], values.fractionalBits) |
| * values.scale, |
| ) |
| else: |
| return data, values.defaultValue |
| |
| for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): |
| data, value = read_transform_component(data, mapping_values) |
| setattr(self.transform, attr_name, value) |
| |
| if flags & VarComponentFlags.UNIFORM_SCALE: |
| if flags & VarComponentFlags.HAVE_SCALE_X and not ( |
| flags & VarComponentFlags.HAVE_SCALE_Y |
| ): |
| self.transform.scaleY = self.transform.scaleX |
| flags |= VarComponentFlags.HAVE_SCALE_Y |
| flags ^= VarComponentFlags.UNIFORM_SCALE |
| |
| return data |
| |
| def compile(self, glyfTable): |
| data = b"" |
| |
| if not hasattr(self, "flags"): |
| flags = 0 |
| # Calculate optimal transform component flags |
| for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): |
| value = getattr(self.transform, attr_name) |
| if fl2fi(value / mapping.scale, mapping.fractionalBits) != fl2fi( |
| mapping.defaultValue / mapping.scale, mapping.fractionalBits |
| ): |
| flags |= mapping.flag |
| else: |
| flags = self.flags |
| |
| if ( |
| flags & VarComponentFlags.HAVE_SCALE_X |
| and flags & VarComponentFlags.HAVE_SCALE_Y |
| and fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10) |
| ): |
| flags |= VarComponentFlags.UNIFORM_SCALE |
| flags ^= VarComponentFlags.HAVE_SCALE_Y |
| |
| numAxes = len(self.location) |
| |
| data = data + struct.pack(">B", numAxes) |
| |
| glyphID = glyfTable.getGlyphID(self.glyphName) |
| if glyphID > 65535: |
| flags |= VarComponentFlags.GID_IS_24BIT |
| data = data + struct.pack(">L", glyphID)[1:] |
| else: |
| data = data + struct.pack(">H", glyphID) |
| |
| axisIndices = [glyfTable.axisTags.index(tag) for tag in self.location.keys()] |
| if all(a <= 255 for a in axisIndices): |
| axisIndices = array.array("B", axisIndices) |
| else: |
| axisIndices = array.array("H", axisIndices) |
| if sys.byteorder != "big": |
| axisIndices.byteswap() |
| flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT |
| data = data + bytes(axisIndices) |
| |
| axisValues = self.location.values() |
| axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues)) |
| if sys.byteorder != "big": |
| axisValues.byteswap() |
| data = data + bytes(axisValues) |
| |
| def write_transform_component(data, value, values): |
| if flags & values.flag: |
| return data + struct.pack( |
| ">h", fl2fi(value / values.scale, values.fractionalBits) |
| ) |
| else: |
| return data |
| |
| for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): |
| value = getattr(self.transform, attr_name) |
| data = write_transform_component(data, value, mapping_values) |
| |
| return struct.pack(">H", flags) + data |
| |
| def toXML(self, writer, ttFont): |
| attrs = [("glyphName", self.glyphName)] |
| |
| if hasattr(self, "flags"): |
| attrs = attrs + [("flags", hex(self.flags))] |
| |
| for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): |
| v = getattr(self.transform, attr_name) |
| if v != mapping.defaultValue: |
| attrs.append((attr_name, fl2str(v, mapping.fractionalBits))) |
| |
| writer.begintag("varComponent", attrs) |
| writer.newline() |
| |
| writer.begintag("location") |
| writer.newline() |
| for tag, v in self.location.items(): |
| writer.simpletag("axis", [("tag", tag), ("value", fl2str(v, 14))]) |
| writer.newline() |
| writer.endtag("location") |
| writer.newline() |
| |
| writer.endtag("varComponent") |
| writer.newline() |
| |
| def fromXML(self, name, attrs, content, ttFont): |
| self.glyphName = attrs["glyphName"] |
| |
| if "flags" in attrs: |
| self.flags = safeEval(attrs["flags"]) |
| |
| for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): |
| if attr_name not in attrs: |
| continue |
| v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits) |
| setattr(self.transform, attr_name, v) |
| |
| for c in content: |
| if not isinstance(c, tuple): |
| continue |
| name, attrs, content = c |
| if name != "location": |
| continue |
| for c in content: |
| if not isinstance(c, tuple): |
| continue |
| name, attrs, content = c |
| assert name == "axis" |
| assert not content |
| self.location[attrs["tag"]] = str2fl(safeEval(attrs["value"]), 14) |
| |
| def getPointCount(self): |
| assert hasattr(self, "flags"), "VarComponent with variations must have flags" |
| |
| count = 0 |
| |
| if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: |
| count += len(self.location) |
| |
| if self.flags & ( |
| VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y |
| ): |
| count += 1 |
| if self.flags & VarComponentFlags.HAVE_ROTATION: |
| count += 1 |
| if self.flags & ( |
| VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y |
| ): |
| count += 1 |
| if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): |
| count += 1 |
| if self.flags & ( |
| VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y |
| ): |
| count += 1 |
| |
| return count |
| |
| def getCoordinatesAndControls(self): |
| coords = [] |
| controls = [] |
| |
| if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: |
| for tag, v in self.location.items(): |
| controls.append(tag) |
| coords.append((fl2fi(v, 14), 0)) |
| |
| if self.flags & ( |
| VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y |
| ): |
| controls.append("translate") |
| coords.append((self.transform.translateX, self.transform.translateY)) |
| if self.flags & VarComponentFlags.HAVE_ROTATION: |
| controls.append("rotation") |
| coords.append((fl2fi(self.transform.rotation / 180, 12), 0)) |
| if self.flags & ( |
| VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y |
| ): |
| controls.append("scale") |
| coords.append( |
| (fl2fi(self.transform.scaleX, 10), fl2fi(self.transform.scaleY, 10)) |
| ) |
| if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): |
| controls.append("skew") |
| coords.append( |
| ( |
| fl2fi(self.transform.skewX / -180, 12), |
| fl2fi(self.transform.skewY / 180, 12), |
| ) |
| ) |
| if self.flags & ( |
| VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y |
| ): |
| controls.append("tCenter") |
| coords.append((self.transform.tCenterX, self.transform.tCenterY)) |
| |
| return coords, controls |
| |
| def setCoordinates(self, coords): |
| i = 0 |
| |
| if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: |
| newLocation = {} |
| for tag in self.location: |
| newLocation[tag] = fi2fl(coords[i][0], 14) |
| i += 1 |
| self.location = newLocation |
| |
| self.transform = DecomposedTransform() |
| if self.flags & ( |
| VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y |
| ): |
| self.transform.translateX, self.transform.translateY = coords[i] |
| i += 1 |
| if self.flags & VarComponentFlags.HAVE_ROTATION: |
| self.transform.rotation = fi2fl(coords[i][0], 12) * 180 |
| i += 1 |
| if self.flags & ( |
| VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y |
| ): |
| self.transform.scaleX, self.transform.scaleY = fi2fl( |
| coords[i][0], 10 |
| ), fi2fl(coords[i][1], 10) |
| i += 1 |
| if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): |
| self.transform.skewX, self.transform.skewY = ( |
| fi2fl(coords[i][0], 12) * -180, |
| fi2fl(coords[i][1], 12) * 180, |
| ) |
| i += 1 |
| if self.flags & ( |
| VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y |
| ): |
| self.transform.tCenterX, self.transform.tCenterY = coords[i] |
| i += 1 |
| |
| return coords[i:] |
| |
| 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): |
| """A list of glyph coordinates. |
| |
| Unlike an ordinary list, this is a numpy-like matrix object which supports |
| matrix addition, scalar multiplication and other operations described below. |
| """ |
| |
| def __init__(self, iterable=[]): |
| self._a = array.array("d") |
| self.extend(iterable) |
| |
| @property |
| def array(self): |
| """Returns the underlying array of coordinates""" |
| return self._a |
| |
| @staticmethod |
| def zeros(count): |
| """Creates a new ``GlyphCoordinates`` object with all coordinates set to (0,0)""" |
| g = GlyphCoordinates() |
| g._a.frombytes(bytes(count * 2 * g._a.itemsize)) |
| return g |
| |
| def copy(self): |
| """Creates a new ``GlyphCoordinates`` object which is a copy of the current one.""" |
| c = GlyphCoordinates() |
| c._a.extend(self._a) |
| return c |
| |
| def __len__(self): |
| """Returns the number of coordinates in the array.""" |
| return len(self._a) // 2 |
| |
| def __getitem__(self, k): |
| """Returns a two element tuple (x,y)""" |
| a = self._a |
| if isinstance(k, slice): |
| indices = range(*k.indices(len(self))) |
| # Instead of calling ourselves recursively, duplicate code; faster |
| ret = [] |
| for k in indices: |
| x = a[2 * k] |
| y = a[2 * k + 1] |
| ret.append( |
| (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y) |
| ) |
| return ret |
| x = a[2 * k] |
| y = a[2 * k + 1] |
| return (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y) |
| |
| def __setitem__(self, k, v): |
| """Sets a point's coordinates to a two element tuple (x,y)""" |
| 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 |
| self._a[2 * k], self._a[2 * k + 1] = v |
| |
| def __delitem__(self, i): |
| """Removes a point from the list""" |
| 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): |
| self._a.extend(tuple(p)) |
| |
| def extend(self, iterable): |
| for p in iterable: |
| self._a.extend(p) |
| |
| def toInt(self, *, round=otRound): |
| if round is noRound: |
| return |
| a = self._a |
| for i in range(len(a)): |
| a[i] = round(a[i]) |
| |
| def calcBounds(self): |
| a = self._a |
| if not a: |
| return 0, 0, 0, 0 |
| xs = a[0::2] |
| ys = a[1::2] |
| return min(xs), min(ys), max(xs), max(ys) |
| |
| def calcIntBounds(self, round=otRound): |
| return tuple(round(v) for v in self.calcBounds()) |
| |
| def relativeToAbsolute(self): |
| a = self._a |
| x, y = 0, 0 |
| for i in range(0, len(a), 2): |
| a[i] = x = a[i] + x |
| a[i + 1] = y = a[i + 1] + y |
| |
| def absoluteToRelative(self): |
| a = self._a |
| x, y = 0, 0 |
| for i in range(0, len(a), 2): |
| nx = a[i] |
| ny = a[i + 1] |
| a[i] = nx - x |
| a[i + 1] = ny - y |
| x = nx |
| y = ny |
| |
| def translate(self, p): |
| """ |
| >>> GlyphCoordinates([(1,2)]).translate((.5,0)) |
| """ |
| x, y = p |
| if x == 0 and y == 0: |
| return |
| a = self._a |
| for i in range(0, len(a), 2): |
| a[i] += x |
| a[i + 1] += y |
| |
| def scale(self, p): |
| """ |
| >>> GlyphCoordinates([(1,2)]).scale((.5,0)) |
| """ |
| x, y = p |
| if x == 1 and y == 1: |
| return |
| a = self._a |
| for i in range(0, len(a), 2): |
| a[i] *= x |
| a[i + 1] *= y |
| |
| def transform(self, t): |
| """ |
| >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5))) |
| """ |
| a = self._a |
| for i in range(0, len(a), 2): |
| x = a[i] |
| y = a[i + 1] |
| px = x * t[0][0] + y * t[1][0] |
| py = x * t[0][1] + y * t[1][1] |
| a[i] = px |
| a[i + 1] = 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, *, round=otRound): |
| r = self.copy() |
| r.toInt(round=round) |
| 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)]) |
| >>> g2 = GlyphCoordinates([(3,4)]) |
| >>> g += g2 |
| >>> g |
| GlyphCoordinates([(4.5, 6)]) |
| """ |
| if isinstance(other, tuple): |
| assert len(other) == 2 |
| self.translate(other) |
| return self |
| if isinstance(other, GlyphCoordinates): |
| other = other._a |
| a = self._a |
| assert len(a) == len(other) |
| for i in range(len(a)): |
| a[i] += other[i] |
| return self |
| return NotImplemented |
| |
| def __isub__(self, other): |
| """ |
| >>> g = GlyphCoordinates([(1,2)]) |
| >>> g -= (.5,0) |
| >>> g |
| GlyphCoordinates([(0.5, 2)]) |
| >>> g2 = GlyphCoordinates([(3,4)]) |
| >>> g -= g2 |
| >>> g |
| GlyphCoordinates([(-2.5, -2)]) |
| """ |
| if isinstance(other, tuple): |
| assert len(other) == 2 |
| self.translate((-other[0], -other[1])) |
| return self |
| if isinstance(other, GlyphCoordinates): |
| other = other._a |
| a = self._a |
| assert len(a) == len(other) |
| for i in range(len(a)): |
| a[i] -= other[i] |
| return self |
| return NotImplemented |
| |
| def __imul__(self, other): |
| """ |
| >>> g = GlyphCoordinates([(1,2)]) |
| >>> g *= (2,.5) |
| >>> g *= 2 |
| >>> g |
| GlyphCoordinates([(4, 2)]) |
| >>> g = GlyphCoordinates([(1,2)]) |
| >>> g *= 2 |
| >>> g |
| GlyphCoordinates([(2, 4)]) |
| """ |
| if isinstance(other, tuple): |
| assert len(other) == 2 |
| self.scale(other) |
| return self |
| if isinstance(other, Number): |
| if other == 1: |
| return self |
| a = self._a |
| for i in range(len(a)): |
| a[i] *= other |
| return self |
| return NotImplemented |
| |
| def __itruediv__(self, other): |
| """ |
| >>> g = GlyphCoordinates([(1,3)]) |
| >>> g /= (.5,1.5) |
| >>> g /= 2 |
| >>> g |
| GlyphCoordinates([(1, 1)]) |
| """ |
| if isinstance(other, Number): |
| other = (other, other) |
| if isinstance(other, tuple): |
| if other == (1, 1): |
| return self |
| assert len(other) == 2 |
| self.scale((1.0 / other[0], 1.0 / 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__ |
| |
| |
| if __name__ == "__main__": |
| import doctest, sys |
| |
| sys.exit(doctest.testmod().failed) |