Source code for fontTools.ttLib.tables._g_l_y_f

"""_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


[docs] 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)
[docs] def setGlyphOrder(self, glyphOrder): """Sets the glyph order Args: glyphOrder ([str]): List of glyph names in order. """ self.glyphOrder = glyphOrder self._reverseGlyphOrder = {}
[docs] 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
[docs] 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
[docs] 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
[docs] @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)
[docs] @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)
[docs] @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"] )
[docs] 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)
[docs] 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
[docs] 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) # empty components shouldn't update the bounds of the parent glyph if g.numberOfContours == 0: continue 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
[docs] 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
[docs] 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
[docs] 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()
[docs] 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
[docs] 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
[docs] def removeHinting(self): """Removes TrueType hinting instructions from the glyph.""" self.trim(remove_hinting=True)
[docs] 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()
[docs] 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()
[docs] 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
[docs] 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
[docs] 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
[docs] @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
[docs] 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
[docs] 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
[docs] 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
[docs] 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)