aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/ttLib/tables/_g_l_y_f.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/ttLib/tables/_g_l_y_f.py')
-rw-r--r--Lib/fontTools/ttLib/tables/_g_l_y_f.py575
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)