diff options
Diffstat (limited to 'Lib/fontTools/ttLib/tables/_g_l_y_f.py')
-rw-r--r-- | Lib/fontTools/ttLib/tables/_g_l_y_f.py | 575 |
1 files changed, 327 insertions, 248 deletions
diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 4680ddbf..14c4519d 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -1,12 +1,11 @@ """_g_l_y_f.py -- Converter classes for the 'glyf' table.""" from collections import namedtuple -from fontTools.misc.py23 import bytechr, byteord, bytesjoin, tostr from fontTools.misc import sstruct from fontTools import ttLib from fontTools import version -from fontTools.misc.textTools import safeEval, pad -from fontTools.misc.arrayTools import calcBounds, calcIntBounds, pointInRect +from fontTools.misc.textTools import tostr, safeEval, pad +from fontTools.misc.arrayTools import calcIntBounds, pointInRect from fontTools.misc.bezierTools import calcQuadraticBounds from fontTools.misc.fixedTools import ( fixedToFloat as fi2fl, @@ -25,6 +24,7 @@ import logging import os from fontTools.misc import xmlWriter from fontTools.misc.filenames import userNameToFileName +from fontTools.misc.loggingTools import deprecateFunction log = logging.getLogger(__name__) @@ -47,6 +47,35 @@ 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) + + """ # this attribute controls the amount of padding applied to glyph data upon compile. # Glyph lenghts are aligned to multiples of the specified value. @@ -81,8 +110,11 @@ class table__g_l_y_f(DefaultTable.DefaultTable): if noname: log.warning('%s glyphs have no name', noname) if ttFont.lazy is False: # Be lazy for None and True - for glyph in self.glyphs.values(): - glyph.expand(self) + self.ensureDecompiled() + + def ensureDecompiled(self): + for glyph in self.glyphs.values(): + glyph.expand(self) def compile(self, ttFont): if not hasattr(self, "glyphOrder"): @@ -117,7 +149,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable): currentLocation += len(glyphData) locations[len(dataList)] = currentLocation - data = bytesjoin(dataList) + data = b''.join(dataList) if 'loca' in ttFont: ttFont['loca'].set(locations) if 'maxp' in ttFont: @@ -145,10 +177,10 @@ class table__g_l_y_f(DefaultTable.DefaultTable): path, ext = os.path.splitext(writer.file.name) existingGlyphFiles = set() for glyphName in glyphNames: - if glyphName not in self: + glyph = self.get(glyphName) + if glyph is None: log.warning("glyph '%s' does not exist in glyf table", glyphName) continue - glyph = self[glyphName] if glyph.numberOfContours: if splitGlyphs: glyphPath = userNameToFileName( @@ -215,16 +247,33 @@ class table__g_l_y_f(DefaultTable.DefaultTable): glyph.compact(self, 0) def setGlyphOrder(self, glyphOrder): + """Sets the glyph order + + Args: + glyphOrder ([str]): List of glyph names in order. + """ self.glyphOrder = glyphOrder 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 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. + """ # XXX optimize with reverse dict!!! return self.glyphOrder.index(glyphName) 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() @@ -236,6 +285,12 @@ class table__g_l_y_f(DefaultTable.DefaultTable): __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) @@ -254,49 +309,33 @@ class table__g_l_y_f(DefaultTable.DefaultTable): assert len(self.glyphOrder) == len(self.glyphs) return len(self.glyphs) - def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None): + 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. - If the ttFont doesn't contain a "vmtx" table, the hhea.ascent is used as the - vertical origin, and the head.unitsPerEm as the vertical advance. + 'hMetrics' should be ttFont['hmtx'].metrics. - The "defaultVerticalOrigin" (Optional[int]) is needed when the ttFont contains - neither a "vmtx" nor an "hhea" table, as may happen with 'sparse' masters. - The value should be the hhea.ascent of the default master. + '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] - assert glyphName in ttFont["hmtx"].metrics, ttFont["hmtx"].metrics - horizontalAdvanceWidth, leftSideBearing = ttFont["hmtx"].metrics[glyphName] if not hasattr(glyph, 'xMin'): glyph.recalcBounds(self) + + horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName] leftSideX = glyph.xMin - leftSideBearing rightSideX = leftSideX + horizontalAdvanceWidth - if "vmtx" in ttFont: - verticalAdvanceWidth, topSideBearing = ttFont["vmtx"].metrics[glyphName] + + if vMetrics: + verticalAdvanceWidth, topSideBearing = vMetrics[glyphName] topSideY = topSideBearing + glyph.yMax + bottomSideY = topSideY - verticalAdvanceWidth else: - # without vmtx, use ascent as vertical origin and UPEM as vertical advance - # like HarfBuzz does - verticalAdvanceWidth = ttFont["head"].unitsPerEm - if "hhea" in ttFont: - topSideY = ttFont["hhea"].ascent - else: - # sparse masters may not contain an hhea table; use the ascent - # of the default master as the vertical origin - if defaultVerticalOrigin is not None: - topSideY = defaultVerticalOrigin - else: - log.warning( - "font is missing both 'vmtx' and 'hhea' tables, " - "and no 'defaultVerticalOrigin' was provided; " - "the vertical phantom points may be incorrect." - ) - topSideY = verticalAdvanceWidth - bottomSideY = topSideY - verticalAdvanceWidth + bottomSideY = topSideY = 0 + return [ (leftSideX, 0), (rightSideX, 0), @@ -304,7 +343,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable): (0, bottomSideY), ] - def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None): + def _getCoordinatesAndControls(self, glyphName, hMetrics, vMetrics=None): """Return glyph coordinates and controls as expected by "gvar" table. The coordinates includes four "phantom points" for the glyph metrics, @@ -320,14 +359,14 @@ class table__g_l_y_f(DefaultTable.DefaultTable): - components: list of base glyph names (str) for each component in composite glyphs (None for simple glyphs). - The "ttFont" and "defaultVerticalOrigin" args are used to compute the - "phantom points" (see "getPhantomPoints" method). + The "hMetrics" and vMetrics are used to compute the "phantom points" (see + the "_getPhantomPoints" method). Return None if the requested glyphName is not present. """ - if glyphName not in self.glyphs: + glyph = self.get(glyphName) + if glyph is None: return None - glyph = self[glyphName] if glyph.isComposite(): coords = GlyphCoordinates( [(getattr(c, 'x', 0), getattr(c, 'y', 0)) for c in glyph.components] @@ -348,13 +387,11 @@ class table__g_l_y_f(DefaultTable.DefaultTable): components=None, ) # Add phantom points for (left, right, top, bottom) positions. - phantomPoints = self.getPhantomPoints( - glyphName, ttFont, defaultVerticalOrigin=defaultVerticalOrigin - ) + phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics) coords.extend(phantomPoints) return coords, controls - def setCoordinates(self, glyphName, coord, ttFont): + 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 @@ -363,9 +400,11 @@ class table__g_l_y_f(DefaultTable.DefaultTable): 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). """ - # TODO: Create new glyph if not already present - assert glyphName in self.glyphs glyph = self[glyphName] # Handle phantom points for (left, right, top, bottom) positions. @@ -396,14 +435,61 @@ class table__g_l_y_f(DefaultTable.DefaultTable): # https://github.com/fonttools/fonttools/pull/1198 horizontalAdvanceWidth = 0 leftSideBearing = otRound(glyph.xMin - leftSideX) - ttFont["hmtx"].metrics[glyphName] = horizontalAdvanceWidth, leftSideBearing + hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing - if "vmtx" in ttFont: + 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) - ttFont["vmtx"].metrics[glyphName] = verticalAdvanceWidth, topSideBearing + 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( @@ -488,8 +574,7 @@ def flagEncodeCoord(flag, mask, coord, coordBytes): elif byteCount == -1: coordBytes.append(-coord) elif byteCount == 2: - coordBytes.append((coord >> 8) & 0xFF) - coordBytes.append(coord & 0xFF) + coordBytes.extend(struct.pack('>h', coord)) def flagEncodeCoords(flag, x, y, xBytes, yBytes): flagEncodeCoord(flag, flagXsame|flagXShort, x, xBytes) @@ -515,8 +600,29 @@ CompositeMaxpValues = namedtuple('CompositeMaxpValues', ['nPoints', 'nContours', 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:: - def __init__(self, data=""): + >> 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 @@ -557,7 +663,7 @@ class Glyph(object): else: return self.data if self.numberOfContours == 0: - return "" + return b'' if recalcBBoxes: self.recalcBounds(glyfTable) data = sstruct.pack(glyphHeaderFormat, self) @@ -608,7 +714,7 @@ class Glyph(object): raise ttLib.TTLibError("can't mix composites and contours in glyph") self.numberOfContours = self.numberOfContours + 1 coordinates = GlyphCoordinates() - flags = [] + flags = bytearray() for element in content: if not isinstance(element, tuple): continue @@ -616,11 +722,10 @@ class Glyph(object): if name != "pt": continue # ignore anything but "pt" coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"]))) - flag = not not safeEval(attrs["on"]) + flag = bool(safeEval(attrs["on"])) if "overlap" in attrs and bool(safeEval(attrs["overlap"])): flag |= flagOverlapSimple flags.append(flag) - flags = array.array("B", flags) if not hasattr(self, "coordinates"): self.coordinates = coordinates self.flags = flags @@ -695,16 +800,14 @@ class Glyph(object): if sys.byteorder != "big": endPtsOfContours.byteswap() self.endPtsOfContours = endPtsOfContours.tolist() - data = data[2*self.numberOfContours:] - - instructionLength, = struct.unpack(">h", data[:2]) - data = data[2:] + pos = 2*self.numberOfContours + instructionLength, = struct.unpack(">h", data[pos:pos+2]) self.program = ttProgram.Program() - self.program.fromBytecode(data[:instructionLength]) - data = data[instructionLength:] + self.program.fromBytecode(data[pos+2:pos+2+instructionLength]) + pos += 2 + instructionLength nCoordinates = self.endPtsOfContours[-1] + 1 flags, xCoordinates, yCoordinates = \ - self.decompileCoordinatesRaw(nCoordinates, data) + self.decompileCoordinatesRaw(nCoordinates, data, pos) # fill in repetitions and apply signs self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates) @@ -741,24 +844,26 @@ class Glyph(object): assert yIndex == len(yCoordinates) coordinates.relativeToAbsolute() # discard all flags except "keepFlags" - self.flags = array.array("B", (f & keepFlags for f in flags)) + for i in range(len(flags)): + flags[i] &= keepFlags + self.flags = flags - def decompileCoordinatesRaw(self, nCoordinates, data): + def decompileCoordinatesRaw(self, nCoordinates, data, pos=0): # unpack flags and prepare unpacking of coordinates - flags = array.array("B", [0] * nCoordinates) + 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 - i = j = 0 + j = 0 while True: - flag = byteord(data[i]) - i = i + 1 + flag = data[pos] + pos += 1 repeat = 1 if flag & flagRepeat: - repeat = byteord(data[i]) + 1 - i = i + 1 + repeat = data[pos] + 1 + pos += 1 for k in range(repeat): if flag & flagXShort: xFormat = xFormat + 'B' @@ -773,15 +878,14 @@ class Glyph(object): if j >= nCoordinates: break assert j == nCoordinates, "bad glyph flags" - data = data[i:] # unpack raw coordinates, krrrrrr-tching! xDataLen = struct.calcsize(xFormat) yDataLen = struct.calcsize(yFormat) - if len(data) - (xDataLen + yDataLen) >= 4: + if len(data) - pos - (xDataLen + yDataLen) >= 4: log.warning( - "too much glyph data: %d excess bytes", len(data) - (xDataLen + yDataLen)) - xCoordinates = struct.unpack(xFormat, data[:xDataLen]) - yCoordinates = struct.unpack(yFormat, data[xDataLen:xDataLen+yDataLen]) + "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): @@ -811,9 +915,7 @@ class Glyph(object): data.append(instructions) deltas = self.coordinates.copy() - if deltas.isFloat(): - # Warn? - deltas.toInt() + deltas.toInt() deltas.absoluteToRelative() # TODO(behdad): Add a configuration option for this? @@ -821,14 +923,14 @@ class Glyph(object): #deltas = self.compileDeltasOptimal(self.flags, deltas) data.extend(deltas) - return bytesjoin(data) + 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 = [] - xPoints = [] - yPoints = [] + compressedFlags = bytearray() + compressedXs = bytearray() + compressedYs = bytearray() lastflag = None repeat = 0 for flag,(x,y) in zip(flags, deltas): @@ -842,9 +944,9 @@ class Glyph(object): flag = flag | flagXsame else: x = -x - xPoints.append(bytechr(x)) + compressedXs.append(x) else: - xPoints.append(struct.pack(">h", x)) + compressedXs.extend(struct.pack('>h', x)) # do y if y == 0: flag = flag | flagYsame @@ -854,24 +956,21 @@ class Glyph(object): flag = flag | flagYsame else: y = -y - yPoints.append(bytechr(y)) + compressedYs.append(y) else: - yPoints.append(struct.pack(">h", y)) + compressedYs.extend(struct.pack('>h', y)) # handle repeating flags if flag == lastflag and repeat != 255: repeat = repeat + 1 if repeat == 1: - compressedflags.append(flag) + compressedFlags.append(flag) else: - compressedflags[-2] = flag | flagRepeat - compressedflags[-1] = repeat + compressedFlags[-2] = flag | flagRepeat + compressedFlags[-1] = repeat else: repeat = 0 - compressedflags.append(flag) + compressedFlags.append(flag) lastflag = flag - compressedFlags = array.array("B", compressedflags).tobytes() - compressedXs = bytesjoin(xPoints) - compressedYs = bytesjoin(yPoints) return (compressedFlags, compressedXs, compressedYs) def compileDeltasOptimal(self, flags, deltas): @@ -902,9 +1001,9 @@ class Glyph(object): flags.append(flag) flags.reverse() - compressedFlags = array.array("B") - compressedXs = array.array("B") - compressedYs = array.array("B") + compressedFlags = bytearray() + compressedXs = bytearray() + compressedYs = bytearray() coords = iter(deltas) ff = [] for flag in flags: @@ -924,72 +1023,22 @@ class Glyph(object): raise Exception("internal error") except StopIteration: pass - compressedFlags = compressedFlags.tobytes() - compressedXs = compressedXs.tobytes() - compressedYs = compressedYs.tobytes() return (compressedFlags, compressedXs, compressedYs) def recalcBounds(self, glyfTable): + """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. + """ coords, endPts, flags = self.getCoordinates(glyfTable) - if len(coords) > 0: - if 0: - # This branch calculates exact glyph outline bounds - # analytically, handling cases without on-curve - # extremas, etc. However, the glyf table header - # simply says that the bounds should be min/max x/y - # "for coordinate data", so I suppose that means no - # fancy thing here, just get extremas of all coord - # points (on and off). As such, this branch is - # disabled. - - # Collect on-curve points - onCurveCoords = [coords[j] for j in range(len(coords)) - if flags[j] & flagOnCurve] - # Add implicit on-curve points - start = 0 - for end in endPts: - last = end - for j in range(start, end + 1): - if not ((flags[j] | flags[last]) & flagOnCurve): - x = (coords[last][0] + coords[j][0]) / 2 - y = (coords[last][1] + coords[j][1]) / 2 - onCurveCoords.append((x,y)) - last = j - start = end + 1 - # Add bounds for curves without an explicit extrema - start = 0 - for end in endPts: - last = end - for j in range(start, end + 1): - if not (flags[j] & flagOnCurve): - next = j + 1 if j < end else start - bbox = calcBounds([coords[last], coords[next]]) - if not pointInRect(coords[j], bbox): - # Ouch! - log.warning("Outline has curve with implicit extrema.") - # Ouch! Find analytical curve bounds. - pthis = coords[j] - plast = coords[last] - if not (flags[last] & flagOnCurve): - plast = ((pthis[0]+plast[0])/2, (pthis[1]+plast[1])/2) - pnext = coords[next] - if not (flags[next] & flagOnCurve): - pnext = ((pthis[0]+pnext[0])/2, (pthis[1]+pnext[1])/2) - bbox = calcQuadraticBounds(plast, pthis, pnext) - onCurveCoords.append((bbox[0],bbox[1])) - onCurveCoords.append((bbox[2],bbox[3])) - last = j - start = end + 1 - - self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(onCurveCoords) - else: - self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords) - else: - self.xMin, self.yMin, self.xMax, self.yMax = (0, 0, 0, 0) + self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords) def isComposite(self): - """Can be called on compact or expanded glyph.""" + """Test whether a glyph has components""" if hasattr(self, "data") and self.data: return struct.unpack(">h", self.data[:2])[0] == -1 else: @@ -1001,12 +1050,27 @@ class Glyph(object): return self.components[componentIndex] 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 = array.array("B") + allFlags = bytearray() allEndPts = [] for compo in self.components: g = glyfTable[compo.glyphName] @@ -1051,9 +1115,14 @@ class Glyph(object): allFlags.extend(flags) return allCoords, allEndPts, allFlags else: - return GlyphCoordinates(), [], array.array("B") + 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 not hasattr(self, "data"): if self.isComposite(): return [c.glyphName for c in self.components] @@ -1101,7 +1170,7 @@ class Glyph(object): if not self.data: return numContours = struct.unpack(">h", self.data[:2])[0] - data = array.array("B", self.data) + data = bytearray(self.data) i = 10 if numContours >= 0: i += 2 * numContours # endPtsOfContours @@ -1170,12 +1239,21 @@ class Glyph(object): # Remove padding data = data[:i] - self.data = data.tobytes() + 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: @@ -1221,7 +1299,7 @@ class Glyph(object): pen.closePath() def drawPoints(self, pen, glyfTable, offset=0): - """Draw the glyph using the supplied pointPen. Opposed to Glyph.draw(), + """Draw the glyph using the supplied pointPen. As opposed to Glyph.draw(), this will not change the point indices. """ @@ -1263,12 +1341,29 @@ class Glyph(object): return result if result is NotImplemented else not result 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 the base glyph name and a transform.""" + """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). @@ -1431,65 +1526,60 @@ class GlyphComponent(object): return result if result is NotImplemented else not result class GlyphCoordinates(object): + """A list of glyph coordinates. - def __init__(self, iterable=[], typecode="h"): - self._a = array.array(typecode) + 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 - def isFloat(self): - return self._a.typecode == 'd' - - def _ensureFloat(self): - if self.isFloat(): - return - # The conversion to list() is to work around Jython bug - self._a = array.array("d", list(self._a)) - - def _checkFloat(self, p): - if self.isFloat(): - return p - if any(v > 0x7FFF or v < -0x8000 for v in p): - self._ensureFloat() - return p - if any(isinstance(v, float) for v in p): - p = [int(v) if int(v) == v else v for v in p] - if any(isinstance(v, float) for v in p): - self._ensureFloat() - return p - @staticmethod def zeros(count): - return GlyphCoordinates([(0,0)] * count) + """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): - c = GlyphCoordinates(typecode=self._a.typecode) + """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)""" if isinstance(k, slice): indices = range(*k.indices(len(self))) return [self[i] for i in indices] - return self._a[2*k],self._a[2*k+1] + a = self._a + 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 - v = self._checkFloat(v) 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] @@ -1498,69 +1588,71 @@ class GlyphCoordinates(object): return 'GlyphCoordinates(['+','.join(str(c) for c in self)+'])' def append(self, p): - p = self._checkFloat(p) self._a.extend(tuple(p)) def extend(self, iterable): for p in iterable: - p = self._checkFloat(p) self._a.extend(p) def toInt(self, *, round=otRound): - if not self.isFloat(): - return - a = array.array("h") - for n in self._a: - a.append(round(n)) - self._a = a + a = self._a + for i in range(len(a)): + a[i] = round(a[i]) def relativeToAbsolute(self): a = self._a x,y = 0,0 - for i in range(len(a) // 2): - x = a[2*i ] + x - y = a[2*i+1] + y - self[i] = (x, y) + 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(len(a) // 2): - dx = a[2*i ] - x - dy = a[2*i+1] - y - x = a[2*i ] - y = a[2*i+1] - self[i] = (dx, dy) + 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) = self._checkFloat(p) + x,y = p + if x == 0 and y == 0: + return a = self._a - for i in range(len(a) // 2): - self[i] = (a[2*i] + x, a[2*i+1] + y) + 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) = self._checkFloat(p) + x,y = p + if x == 1 and y == 1: + return a = self._a - for i in range(len(a) // 2): - self[i] = (a[2*i] * x, a[2*i+1] * y) + 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(len(a) // 2): - x = a[2*i ] - y = a[2*i+1] + 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] - self[i] = (px, py) + a[i] = px + a[i+1] = py def __eq__(self, other): """ @@ -1645,23 +1737,22 @@ class GlyphCoordinates(object): >>> g = GlyphCoordinates([(1,2)]) >>> g += (.5,0) >>> g - GlyphCoordinates([(1.5, 2.0)]) + GlyphCoordinates([(1.5, 2)]) >>> g2 = GlyphCoordinates([(3,4)]) >>> g += g2 >>> g - GlyphCoordinates([(4.5, 6.0)]) + GlyphCoordinates([(4.5, 6)]) """ if isinstance(other, tuple): assert len(other) == 2 self.translate(other) return self if isinstance(other, GlyphCoordinates): - if other.isFloat(): self._ensureFloat() other = other._a a = self._a assert len(a) == len(other) - for i in range(len(a) // 2): - self[i] = (a[2*i] + other[2*i], a[2*i+1] + other[2*i+1]) + for i in range(len(a)): + a[i] += other[i] return self return NotImplemented @@ -1670,23 +1761,22 @@ class GlyphCoordinates(object): >>> g = GlyphCoordinates([(1,2)]) >>> g -= (.5,0) >>> g - GlyphCoordinates([(0.5, 2.0)]) + GlyphCoordinates([(0.5, 2)]) >>> g2 = GlyphCoordinates([(3,4)]) >>> g -= g2 >>> g - GlyphCoordinates([(-2.5, -2.0)]) + GlyphCoordinates([(-2.5, -2)]) """ if isinstance(other, tuple): assert len(other) == 2 self.translate((-other[0],-other[1])) return self if isinstance(other, GlyphCoordinates): - if other.isFloat(): self._ensureFloat() other = other._a a = self._a assert len(a) == len(other) - for i in range(len(a) // 2): - self[i] = (a[2*i] - other[2*i], a[2*i+1] - other[2*i+1]) + for i in range(len(a)): + a[i] -= other[i] return self return NotImplemented @@ -1696,20 +1786,23 @@ class GlyphCoordinates(object): >>> g *= (2,.5) >>> g *= 2 >>> g - GlyphCoordinates([(4.0, 2.0)]) + GlyphCoordinates([(4, 2)]) >>> g = GlyphCoordinates([(1,2)]) >>> g *= 2 >>> g GlyphCoordinates([(2, 4)]) """ - if isinstance(other, Number): - other = (other, other) if isinstance(other, tuple): - if other == (1,1): - return self assert len(other) == 2 self.scale(other) return self + 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): @@ -1718,7 +1811,7 @@ class GlyphCoordinates(object): >>> g /= (.5,1.5) >>> g /= 2 >>> g - GlyphCoordinates([(1.0, 1.0)]) + GlyphCoordinates([(1, 1)]) """ if isinstance(other, Number): other = (other, other) @@ -1750,20 +1843,6 @@ class GlyphCoordinates(object): __nonzero__ = __bool__ -def reprflag(flag): - bin = "" - if isinstance(flag, str): - flag = byteord(flag) - while flag: - if flag & 0x01: - bin = "1" + bin - else: - bin = "0" + bin - flag = flag >> 1 - bin = (14 - len(bin)) * "0" + bin - return bin - - if __name__ == "__main__": import doctest, sys sys.exit(doctest.testmod().failed) |