Unlike simple glyphs, which always have 'program' attribute (even when empty, with no instructions) in composite glyphs, it's the presence of 'program' attribute that determines whether the optional WE_HAVE_INSTRUCTIONS component flag is set or not. Thus, the trim method (with remove_hinting=True) should delete the attribute for composite glyphs. Fixes https://github.com/fonttools/fonttools/issues/1550
1595 lines
45 KiB
Python
1595 lines
45 KiB
Python
"""_g_l_y_f.py -- Converter classes for the 'glyf' table."""
|
|
|
|
from __future__ import print_function, division, absolute_import
|
|
from collections import namedtuple
|
|
from fontTools.misc.py23 import *
|
|
from fontTools.misc import sstruct
|
|
from fontTools import ttLib
|
|
from fontTools import version
|
|
from fontTools.misc.textTools import safeEval, pad
|
|
from fontTools.misc.arrayTools import calcBounds, calcIntBounds, pointInRect
|
|
from fontTools.misc.bezierTools import calcQuadraticBounds
|
|
from fontTools.misc.fixedTools import (
|
|
fixedToFloat as fi2fl,
|
|
floatToFixed as fl2fi,
|
|
otRound,
|
|
)
|
|
from numbers import Number
|
|
from . import DefaultTable
|
|
from . import ttProgram
|
|
import sys
|
|
import struct
|
|
import array
|
|
import logging
|
|
import os
|
|
from fontTools.misc import xmlWriter
|
|
from fontTools.misc.filenames import userNameToFileName
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# We compute the version the same as is computed in ttlib/__init__
|
|
# so that we can write 'ttLibVersion' attribute of the glyf TTX files
|
|
# when glyf is written to separate files.
|
|
version = ".".join(version.split('.')[:2])
|
|
|
|
#
|
|
# The Apple and MS rasterizers behave differently for
|
|
# scaled composite components: one does scale first and then translate
|
|
# and the other does it vice versa. MS defined some flags to indicate
|
|
# the difference, but it seems nobody actually _sets_ those flags.
|
|
#
|
|
# Funny thing: Apple seems to _only_ do their thing in the
|
|
# WE_HAVE_A_SCALE (eg. Chicago) case, and not when it's WE_HAVE_AN_X_AND_Y_SCALE
|
|
# (eg. Charcoal)...
|
|
#
|
|
SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple
|
|
|
|
|
|
class table__g_l_y_f(DefaultTable.DefaultTable):
|
|
|
|
# this attribute controls the amount of padding applied to glyph data upon compile.
|
|
# Glyph lenghts are aligned to multiples of the specified value.
|
|
# Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means
|
|
# no padding, except for when padding would allow to use short loca offsets.
|
|
padding = 1
|
|
|
|
def decompile(self, data, ttFont):
|
|
loca = ttFont['loca']
|
|
last = int(loca[0])
|
|
noname = 0
|
|
self.glyphs = {}
|
|
self.glyphOrder = glyphOrder = ttFont.getGlyphOrder()
|
|
for i in range(0, len(loca)-1):
|
|
try:
|
|
glyphName = glyphOrder[i]
|
|
except IndexError:
|
|
noname = noname + 1
|
|
glyphName = 'ttxautoglyph%s' % i
|
|
next = int(loca[i+1])
|
|
glyphdata = data[last:next]
|
|
if len(glyphdata) != (next - last):
|
|
raise ttLib.TTLibError("not enough 'glyf' table data")
|
|
glyph = Glyph(glyphdata)
|
|
self.glyphs[glyphName] = glyph
|
|
last = next
|
|
if len(data) - next >= 4:
|
|
log.warning(
|
|
"too much 'glyf' table data: expected %d, received %d bytes",
|
|
next, len(data))
|
|
if noname:
|
|
log.warning('%s glyphs have no name', noname)
|
|
if ttFont.lazy is False: # Be lazy for None and True
|
|
for glyph in self.glyphs.values():
|
|
glyph.expand(self)
|
|
|
|
def compile(self, ttFont):
|
|
if not hasattr(self, "glyphOrder"):
|
|
self.glyphOrder = ttFont.getGlyphOrder()
|
|
padding = self.padding
|
|
assert padding in (0, 1, 2, 4)
|
|
locations = []
|
|
currentLocation = 0
|
|
dataList = []
|
|
recalcBBoxes = ttFont.recalcBBoxes
|
|
for glyphName in self.glyphOrder:
|
|
glyph = self.glyphs[glyphName]
|
|
glyphData = glyph.compile(self, recalcBBoxes)
|
|
if padding > 1:
|
|
glyphData = pad(glyphData, size=padding)
|
|
locations.append(currentLocation)
|
|
currentLocation = currentLocation + len(glyphData)
|
|
dataList.append(glyphData)
|
|
locations.append(currentLocation)
|
|
|
|
if padding == 1 and currentLocation < 0x20000:
|
|
# See if we can pad any odd-lengthed glyphs to allow loca
|
|
# table to use the short offsets.
|
|
indices = [i for i,glyphData in enumerate(dataList) if len(glyphData) % 2 == 1]
|
|
if indices and currentLocation + len(indices) < 0x20000:
|
|
# It fits. Do it.
|
|
for i in indices:
|
|
dataList[i] += b'\0'
|
|
currentLocation = 0
|
|
for i,glyphData in enumerate(dataList):
|
|
locations[i] = currentLocation
|
|
currentLocation += len(glyphData)
|
|
locations[len(dataList)] = currentLocation
|
|
|
|
data = bytesjoin(dataList)
|
|
if 'loca' in ttFont:
|
|
ttFont['loca'].set(locations)
|
|
if 'maxp' in ttFont:
|
|
ttFont['maxp'].numGlyphs = len(self.glyphs)
|
|
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[glyphName]
|
|
if glyph.numberOfContours:
|
|
if splitGlyphs:
|
|
glyphPath = userNameToFileName(
|
|
tounicode(glyphName, 'utf-8'),
|
|
existingGlyphFiles,
|
|
prefix=path + ".",
|
|
suffix=ext)
|
|
existingGlyphFiles.add(glyphPath.lower())
|
|
glyphWriter = xmlWriter.XMLWriter(
|
|
glyphPath, idlefunc=writer.idlefunc,
|
|
newlinestr=writer.newlinestr)
|
|
glyphWriter.begintag("ttFont", ttLibVersion=version)
|
|
glyphWriter.newline()
|
|
glyphWriter.begintag("glyf")
|
|
glyphWriter.newline()
|
|
glyphWriter.comment(notice)
|
|
glyphWriter.newline()
|
|
writer.simpletag("TTGlyph", src=os.path.basename(glyphPath))
|
|
else:
|
|
glyphWriter = writer
|
|
glyphWriter.begintag('TTGlyph', [
|
|
("name", glyphName),
|
|
("xMin", glyph.xMin),
|
|
("yMin", glyph.yMin),
|
|
("xMax", glyph.xMax),
|
|
("yMax", glyph.yMax),
|
|
])
|
|
glyphWriter.newline()
|
|
glyph.toXML(glyphWriter, ttFont)
|
|
glyphWriter.endtag('TTGlyph')
|
|
glyphWriter.newline()
|
|
if splitGlyphs:
|
|
glyphWriter.endtag("glyf")
|
|
glyphWriter.newline()
|
|
glyphWriter.endtag("ttFont")
|
|
glyphWriter.newline()
|
|
glyphWriter.close()
|
|
else:
|
|
writer.simpletag('TTGlyph', name=glyphName)
|
|
writer.comment("contains no outline data")
|
|
if not splitGlyphs:
|
|
writer.newline()
|
|
writer.newline()
|
|
|
|
def fromXML(self, name, attrs, content, ttFont):
|
|
if name != "TTGlyph":
|
|
return
|
|
if not hasattr(self, "glyphs"):
|
|
self.glyphs = {}
|
|
if not hasattr(self, "glyphOrder"):
|
|
self.glyphOrder = ttFont.getGlyphOrder()
|
|
glyphName = attrs["name"]
|
|
log.debug("unpacking glyph '%s'", glyphName)
|
|
glyph = Glyph()
|
|
for attr in ['xMin', 'yMin', 'xMax', 'yMax']:
|
|
setattr(glyph, attr, safeEval(attrs.get(attr, '0')))
|
|
self.glyphs[glyphName] = glyph
|
|
for element in content:
|
|
if not isinstance(element, tuple):
|
|
continue
|
|
name, attrs, content = element
|
|
glyph.fromXML(name, attrs, content, ttFont)
|
|
if not ttFont.recalcBBoxes:
|
|
glyph.compact(self, 0)
|
|
|
|
def setGlyphOrder(self, glyphOrder):
|
|
self.glyphOrder = glyphOrder
|
|
|
|
def getGlyphName(self, glyphID):
|
|
return self.glyphOrder[glyphID]
|
|
|
|
def getGlyphID(self, glyphName):
|
|
# XXX optimize with reverse dict!!!
|
|
return self.glyphOrder.index(glyphName)
|
|
|
|
def removeHinting(self):
|
|
for glyph in self.glyphs.values():
|
|
glyph.removeHinting()
|
|
|
|
def keys(self):
|
|
return self.glyphs.keys()
|
|
|
|
def has_key(self, glyphName):
|
|
return glyphName in self.glyphs
|
|
|
|
__contains__ = has_key
|
|
|
|
def __getitem__(self, glyphName):
|
|
glyph = self.glyphs[glyphName]
|
|
glyph.expand(self)
|
|
return glyph
|
|
|
|
def __setitem__(self, glyphName, glyph):
|
|
self.glyphs[glyphName] = glyph
|
|
if glyphName not in self.glyphOrder:
|
|
self.glyphOrder.append(glyphName)
|
|
|
|
def __delitem__(self, glyphName):
|
|
del self.glyphs[glyphName]
|
|
self.glyphOrder.remove(glyphName)
|
|
|
|
def __len__(self):
|
|
assert len(self.glyphOrder) == len(self.glyphs)
|
|
return len(self.glyphs)
|
|
|
|
|
|
glyphHeaderFormat = """
|
|
> # big endian
|
|
numberOfContours: h
|
|
xMin: h
|
|
yMin: h
|
|
xMax: h
|
|
yMax: h
|
|
"""
|
|
|
|
# flags
|
|
flagOnCurve = 0x01
|
|
flagXShort = 0x02
|
|
flagYShort = 0x04
|
|
flagRepeat = 0x08
|
|
flagXsame = 0x10
|
|
flagYsame = 0x20
|
|
flagOverlapSimple = 0x40
|
|
flagReserved = 0x80
|
|
|
|
# These flags are kept for XML output after decompiling the coordinates
|
|
keepFlags = flagOnCurve + flagOverlapSimple
|
|
|
|
_flagSignBytes = {
|
|
0: 2,
|
|
flagXsame: 0,
|
|
flagXShort|flagXsame: +1,
|
|
flagXShort: -1,
|
|
flagYsame: 0,
|
|
flagYShort|flagYsame: +1,
|
|
flagYShort: -1,
|
|
}
|
|
|
|
def flagBest(x, y, onCurve):
|
|
"""For a given x,y delta pair, returns the flag that packs this pair
|
|
most efficiently, as well as the number of byte cost of such flag."""
|
|
|
|
flag = flagOnCurve if onCurve else 0
|
|
cost = 0
|
|
# do x
|
|
if x == 0:
|
|
flag = flag | flagXsame
|
|
elif -255 <= x <= 255:
|
|
flag = flag | flagXShort
|
|
if x > 0:
|
|
flag = flag | flagXsame
|
|
cost += 1
|
|
else:
|
|
cost += 2
|
|
# do y
|
|
if y == 0:
|
|
flag = flag | flagYsame
|
|
elif -255 <= y <= 255:
|
|
flag = flag | flagYShort
|
|
if y > 0:
|
|
flag = flag | flagYsame
|
|
cost += 1
|
|
else:
|
|
cost += 2
|
|
return flag, cost
|
|
|
|
def flagFits(newFlag, oldFlag, mask):
|
|
newBytes = _flagSignBytes[newFlag & mask]
|
|
oldBytes = _flagSignBytes[oldFlag & mask]
|
|
return newBytes == oldBytes or abs(newBytes) > abs(oldBytes)
|
|
|
|
def flagSupports(newFlag, oldFlag):
|
|
return ((oldFlag & flagOnCurve) == (newFlag & flagOnCurve) and
|
|
flagFits(newFlag, oldFlag, flagXsame|flagXShort) and
|
|
flagFits(newFlag, oldFlag, flagYsame|flagYShort))
|
|
|
|
def flagEncodeCoord(flag, mask, coord, coordBytes):
|
|
byteCount = _flagSignBytes[flag & mask]
|
|
if byteCount == 1:
|
|
coordBytes.append(coord)
|
|
elif byteCount == -1:
|
|
coordBytes.append(-coord)
|
|
elif byteCount == 2:
|
|
coordBytes.append((coord >> 8) & 0xFF)
|
|
coordBytes.append(coord & 0xFF)
|
|
|
|
def flagEncodeCoords(flag, x, y, xBytes, yBytes):
|
|
flagEncodeCoord(flag, flagXsame|flagXShort, x, xBytes)
|
|
flagEncodeCoord(flag, flagYsame|flagYShort, y, yBytes)
|
|
|
|
|
|
ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes
|
|
ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points
|
|
ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true
|
|
WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0
|
|
NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!)
|
|
MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one
|
|
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy
|
|
WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11
|
|
WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow
|
|
USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph
|
|
OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts
|
|
SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple)
|
|
UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS)
|
|
|
|
|
|
CompositeMaxpValues = namedtuple('CompositeMaxpValues', ['nPoints', 'nContours', 'maxComponentDepth'])
|
|
|
|
|
|
class Glyph(object):
|
|
|
|
def __init__(self, data=""):
|
|
if not data:
|
|
# empty char
|
|
self.numberOfContours = 0
|
|
return
|
|
self.data = data
|
|
|
|
def compact(self, glyfTable, recalcBBoxes=True):
|
|
data = self.compile(glyfTable, recalcBBoxes)
|
|
self.__dict__.clear()
|
|
self.data = data
|
|
|
|
def expand(self, glyfTable):
|
|
if not hasattr(self, "data"):
|
|
# already unpacked
|
|
return
|
|
if not self.data:
|
|
# empty char
|
|
del self.data
|
|
self.numberOfContours = 0
|
|
return
|
|
dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self)
|
|
del self.data
|
|
# Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in
|
|
# some glyphs; decompileCoordinates assumes that there's at least
|
|
# one, so short-circuit here.
|
|
if self.numberOfContours == 0:
|
|
return
|
|
if self.isComposite():
|
|
self.decompileComponents(data, glyfTable)
|
|
else:
|
|
self.decompileCoordinates(data)
|
|
|
|
def compile(self, glyfTable, recalcBBoxes=True):
|
|
if hasattr(self, "data"):
|
|
if recalcBBoxes:
|
|
# must unpack glyph in order to recalculate bounding box
|
|
self.expand(glyfTable)
|
|
else:
|
|
return self.data
|
|
if self.numberOfContours == 0:
|
|
return ""
|
|
if recalcBBoxes:
|
|
self.recalcBounds(glyfTable)
|
|
data = sstruct.pack(glyphHeaderFormat, self)
|
|
if self.isComposite():
|
|
data = data + self.compileComponents(glyfTable)
|
|
else:
|
|
data = data + self.compileCoordinates()
|
|
return data
|
|
|
|
def toXML(self, writer, ttFont):
|
|
if self.isComposite():
|
|
for compo in self.components:
|
|
compo.toXML(writer, ttFont)
|
|
haveInstructions = hasattr(self, "program")
|
|
else:
|
|
last = 0
|
|
for i in range(self.numberOfContours):
|
|
writer.begintag("contour")
|
|
writer.newline()
|
|
for j in range(last, self.endPtsOfContours[i] + 1):
|
|
attrs = [
|
|
("x", self.coordinates[j][0]),
|
|
("y", self.coordinates[j][1]),
|
|
("on", self.flags[j] & flagOnCurve),
|
|
]
|
|
if self.flags[j] & flagOverlapSimple:
|
|
# Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours
|
|
attrs.append(("overlap", 1))
|
|
writer.simpletag("pt", attrs)
|
|
writer.newline()
|
|
last = self.endPtsOfContours[i] + 1
|
|
writer.endtag("contour")
|
|
writer.newline()
|
|
haveInstructions = self.numberOfContours > 0
|
|
if haveInstructions:
|
|
if self.program:
|
|
writer.begintag("instructions")
|
|
writer.newline()
|
|
self.program.toXML(writer, ttFont)
|
|
writer.endtag("instructions")
|
|
else:
|
|
writer.simpletag("instructions")
|
|
writer.newline()
|
|
|
|
def fromXML(self, name, attrs, content, ttFont):
|
|
if name == "contour":
|
|
if self.numberOfContours < 0:
|
|
raise ttLib.TTLibError("can't mix composites and contours in glyph")
|
|
self.numberOfContours = self.numberOfContours + 1
|
|
coordinates = GlyphCoordinates()
|
|
flags = []
|
|
for element in content:
|
|
if not isinstance(element, tuple):
|
|
continue
|
|
name, attrs, content = element
|
|
if name != "pt":
|
|
continue # ignore anything but "pt"
|
|
coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"])))
|
|
flag = not not safeEval(attrs["on"])
|
|
if "overlap" in attrs and bool(safeEval(attrs["overlap"])):
|
|
flag |= flagOverlapSimple
|
|
flags.append(flag)
|
|
flags = array.array("B", flags)
|
|
if not hasattr(self, "coordinates"):
|
|
self.coordinates = coordinates
|
|
self.flags = flags
|
|
self.endPtsOfContours = [len(coordinates)-1]
|
|
else:
|
|
self.coordinates.extend (coordinates)
|
|
self.flags.extend(flags)
|
|
self.endPtsOfContours.append(len(self.coordinates)-1)
|
|
elif name == "component":
|
|
if self.numberOfContours > 0:
|
|
raise ttLib.TTLibError("can't mix composites and contours in glyph")
|
|
self.numberOfContours = -1
|
|
if not hasattr(self, "components"):
|
|
self.components = []
|
|
component = GlyphComponent()
|
|
self.components.append(component)
|
|
component.fromXML(name, attrs, content, ttFont)
|
|
elif name == "instructions":
|
|
self.program = ttProgram.Program()
|
|
for element in content:
|
|
if not isinstance(element, tuple):
|
|
continue
|
|
name, attrs, content = element
|
|
self.program.fromXML(name, attrs, content, ttFont)
|
|
|
|
def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1):
|
|
assert self.isComposite()
|
|
nContours = 0
|
|
nPoints = 0
|
|
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, maxComponentDepth = baseGlyph.getCompositeMaxpValues(
|
|
glyfTable, maxComponentDepth + 1)
|
|
nPoints = nPoints + nP
|
|
nContours = nContours + nC
|
|
return CompositeMaxpValues(nPoints, nContours, maxComponentDepth)
|
|
|
|
def getMaxpValues(self):
|
|
assert self.numberOfContours > 0
|
|
return len(self.coordinates), len(self.endPtsOfContours)
|
|
|
|
def decompileComponents(self, data, glyfTable):
|
|
self.components = []
|
|
more = 1
|
|
haveInstructions = 0
|
|
while more:
|
|
component = GlyphComponent()
|
|
more, haveInstr, data = component.decompile(data, glyfTable)
|
|
haveInstructions = haveInstructions | haveInstr
|
|
self.components.append(component)
|
|
if haveInstructions:
|
|
numInstructions, = struct.unpack(">h", data[:2])
|
|
data = data[2:]
|
|
self.program = ttProgram.Program()
|
|
self.program.fromBytecode(data[:numInstructions])
|
|
data = data[numInstructions:]
|
|
if len(data) >= 4:
|
|
log.warning(
|
|
"too much glyph data at the end of composite glyph: %d excess bytes",
|
|
len(data))
|
|
|
|
def decompileCoordinates(self, data):
|
|
endPtsOfContours = array.array("h")
|
|
endPtsOfContours.fromstring(data[:2*self.numberOfContours])
|
|
if sys.byteorder != "big": endPtsOfContours.byteswap()
|
|
self.endPtsOfContours = endPtsOfContours.tolist()
|
|
|
|
data = data[2*self.numberOfContours:]
|
|
|
|
instructionLength, = struct.unpack(">h", data[:2])
|
|
data = data[2:]
|
|
self.program = ttProgram.Program()
|
|
self.program.fromBytecode(data[:instructionLength])
|
|
data = data[instructionLength:]
|
|
nCoordinates = self.endPtsOfContours[-1] + 1
|
|
flags, xCoordinates, yCoordinates = \
|
|
self.decompileCoordinatesRaw(nCoordinates, data)
|
|
|
|
# fill in repetitions and apply signs
|
|
self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates)
|
|
xIndex = 0
|
|
yIndex = 0
|
|
for i in range(nCoordinates):
|
|
flag = flags[i]
|
|
# x coordinate
|
|
if flag & flagXShort:
|
|
if flag & flagXsame:
|
|
x = xCoordinates[xIndex]
|
|
else:
|
|
x = -xCoordinates[xIndex]
|
|
xIndex = xIndex + 1
|
|
elif flag & flagXsame:
|
|
x = 0
|
|
else:
|
|
x = xCoordinates[xIndex]
|
|
xIndex = xIndex + 1
|
|
# y coordinate
|
|
if flag & flagYShort:
|
|
if flag & flagYsame:
|
|
y = yCoordinates[yIndex]
|
|
else:
|
|
y = -yCoordinates[yIndex]
|
|
yIndex = yIndex + 1
|
|
elif flag & flagYsame:
|
|
y = 0
|
|
else:
|
|
y = yCoordinates[yIndex]
|
|
yIndex = yIndex + 1
|
|
coordinates[i] = (x, y)
|
|
assert xIndex == len(xCoordinates)
|
|
assert yIndex == len(yCoordinates)
|
|
coordinates.relativeToAbsolute()
|
|
# discard all flags except "keepFlags"
|
|
self.flags = array.array("B", (f & keepFlags for f in flags))
|
|
|
|
def decompileCoordinatesRaw(self, nCoordinates, data):
|
|
# unpack flags and prepare unpacking of coordinates
|
|
flags = array.array("B", [0] * nCoordinates)
|
|
# Warning: deep Python trickery going on. We use the struct module to unpack
|
|
# the coordinates. We build a format string based on the flags, so we can
|
|
# unpack the coordinates in one struct.unpack() call.
|
|
xFormat = ">" # big endian
|
|
yFormat = ">" # big endian
|
|
i = j = 0
|
|
while True:
|
|
flag = byteord(data[i])
|
|
i = i + 1
|
|
repeat = 1
|
|
if flag & flagRepeat:
|
|
repeat = byteord(data[i]) + 1
|
|
i = i + 1
|
|
for k in range(repeat):
|
|
if flag & flagXShort:
|
|
xFormat = xFormat + 'B'
|
|
elif not (flag & flagXsame):
|
|
xFormat = xFormat + 'h'
|
|
if flag & flagYShort:
|
|
yFormat = yFormat + 'B'
|
|
elif not (flag & flagYsame):
|
|
yFormat = yFormat + 'h'
|
|
flags[j] = flag
|
|
j = j + 1
|
|
if j >= nCoordinates:
|
|
break
|
|
assert j == nCoordinates, "bad glyph flags"
|
|
data = data[i:]
|
|
# unpack raw coordinates, krrrrrr-tching!
|
|
xDataLen = struct.calcsize(xFormat)
|
|
yDataLen = struct.calcsize(yFormat)
|
|
if len(data) - (xDataLen + yDataLen) >= 4:
|
|
log.warning(
|
|
"too much glyph data: %d excess bytes", len(data) - (xDataLen + yDataLen))
|
|
xCoordinates = struct.unpack(xFormat, data[:xDataLen])
|
|
yCoordinates = struct.unpack(yFormat, data[xDataLen:xDataLen+yDataLen])
|
|
return flags, xCoordinates, yCoordinates
|
|
|
|
def compileComponents(self, glyfTable):
|
|
data = b""
|
|
lastcomponent = len(self.components) - 1
|
|
more = 1
|
|
haveInstructions = 0
|
|
for i in range(len(self.components)):
|
|
if i == lastcomponent:
|
|
haveInstructions = hasattr(self, "program")
|
|
more = 0
|
|
compo = self.components[i]
|
|
data = data + compo.compile(more, haveInstructions, glyfTable)
|
|
if haveInstructions:
|
|
instructions = self.program.getBytecode()
|
|
data = data + struct.pack(">h", len(instructions)) + instructions
|
|
return data
|
|
|
|
def compileCoordinates(self):
|
|
assert len(self.coordinates) == len(self.flags)
|
|
data = []
|
|
endPtsOfContours = array.array("h", self.endPtsOfContours)
|
|
if sys.byteorder != "big": endPtsOfContours.byteswap()
|
|
data.append(endPtsOfContours.tostring())
|
|
instructions = self.program.getBytecode()
|
|
data.append(struct.pack(">h", len(instructions)))
|
|
data.append(instructions)
|
|
|
|
deltas = self.coordinates.copy()
|
|
if deltas.isFloat():
|
|
# Warn?
|
|
deltas.toInt()
|
|
deltas.absoluteToRelative()
|
|
|
|
# TODO(behdad): Add a configuration option for this?
|
|
deltas = self.compileDeltasGreedy(self.flags, deltas)
|
|
#deltas = self.compileDeltasOptimal(self.flags, deltas)
|
|
|
|
data.extend(deltas)
|
|
return bytesjoin(data)
|
|
|
|
def compileDeltasGreedy(self, flags, deltas):
|
|
# Implements greedy algorithm for packing coordinate deltas:
|
|
# uses shortest representation one coordinate at a time.
|
|
compressedflags = []
|
|
xPoints = []
|
|
yPoints = []
|
|
lastflag = None
|
|
repeat = 0
|
|
for flag,(x,y) in zip(flags, deltas):
|
|
# Oh, the horrors of TrueType
|
|
# do x
|
|
if x == 0:
|
|
flag = flag | flagXsame
|
|
elif -255 <= x <= 255:
|
|
flag = flag | flagXShort
|
|
if x > 0:
|
|
flag = flag | flagXsame
|
|
else:
|
|
x = -x
|
|
xPoints.append(bytechr(x))
|
|
else:
|
|
xPoints.append(struct.pack(">h", x))
|
|
# do y
|
|
if y == 0:
|
|
flag = flag | flagYsame
|
|
elif -255 <= y <= 255:
|
|
flag = flag | flagYShort
|
|
if y > 0:
|
|
flag = flag | flagYsame
|
|
else:
|
|
y = -y
|
|
yPoints.append(bytechr(y))
|
|
else:
|
|
yPoints.append(struct.pack(">h", y))
|
|
# handle repeating flags
|
|
if flag == lastflag and repeat != 255:
|
|
repeat = repeat + 1
|
|
if repeat == 1:
|
|
compressedflags.append(flag)
|
|
else:
|
|
compressedflags[-2] = flag | flagRepeat
|
|
compressedflags[-1] = repeat
|
|
else:
|
|
repeat = 0
|
|
compressedflags.append(flag)
|
|
lastflag = flag
|
|
compressedFlags = array.array("B", compressedflags).tostring()
|
|
compressedXs = bytesjoin(xPoints)
|
|
compressedYs = bytesjoin(yPoints)
|
|
return (compressedFlags, compressedXs, compressedYs)
|
|
|
|
def compileDeltasOptimal(self, flags, deltas):
|
|
# Implements optimal, dynaic-programming, algorithm for packing coordinate
|
|
# deltas. The savings are negligible :(.
|
|
candidates = []
|
|
bestTuple = None
|
|
bestCost = 0
|
|
repeat = 0
|
|
for flag,(x,y) in zip(flags, deltas):
|
|
# Oh, the horrors of TrueType
|
|
flag, coordBytes = flagBest(x, y, flag)
|
|
bestCost += 1 + coordBytes
|
|
newCandidates = [(bestCost, bestTuple, flag, coordBytes),
|
|
(bestCost+1, bestTuple, (flag|flagRepeat), coordBytes)]
|
|
for lastCost,lastTuple,lastFlag,coordBytes in candidates:
|
|
if lastCost + coordBytes <= bestCost + 1 and (lastFlag & flagRepeat) and (lastFlag < 0xff00) and flagSupports(lastFlag, flag):
|
|
if (lastFlag & 0xFF) == (flag|flagRepeat) and lastCost == bestCost + 1:
|
|
continue
|
|
newCandidates.append((lastCost + coordBytes, lastTuple, lastFlag+256, coordBytes))
|
|
candidates = newCandidates
|
|
bestTuple = min(candidates, key=lambda t:t[0])
|
|
bestCost = bestTuple[0]
|
|
|
|
flags = []
|
|
while bestTuple:
|
|
cost, bestTuple, flag, coordBytes = bestTuple
|
|
flags.append(flag)
|
|
flags.reverse()
|
|
|
|
compressedFlags = array.array("B")
|
|
compressedXs = array.array("B")
|
|
compressedYs = array.array("B")
|
|
coords = iter(deltas)
|
|
ff = []
|
|
for flag in flags:
|
|
repeatCount, flag = flag >> 8, flag & 0xFF
|
|
compressedFlags.append(flag)
|
|
if flag & flagRepeat:
|
|
assert(repeatCount > 0)
|
|
compressedFlags.append(repeatCount)
|
|
else:
|
|
assert(repeatCount == 0)
|
|
for i in range(1 + repeatCount):
|
|
x,y = next(coords)
|
|
flagEncodeCoords(flag, x, y, compressedXs, compressedYs)
|
|
ff.append(flag)
|
|
try:
|
|
next(coords)
|
|
raise Exception("internal error")
|
|
except StopIteration:
|
|
pass
|
|
compressedFlags = compressedFlags.tostring()
|
|
compressedXs = compressedXs.tostring()
|
|
compressedYs = compressedYs.tostring()
|
|
|
|
return (compressedFlags, compressedXs, compressedYs)
|
|
|
|
def recalcBounds(self, glyfTable):
|
|
coords, endPts, flags = self.getCoordinates(glyfTable)
|
|
if len(coords) > 0:
|
|
if 0:
|
|
# This branch calculates exact glyph outline bounds
|
|
# analytically, handling cases without on-curve
|
|
# extremas, etc. However, the glyf table header
|
|
# simply says that the bounds should be min/max x/y
|
|
# "for coordinate data", so I suppose that means no
|
|
# fancy thing here, just get extremas of all coord
|
|
# points (on and off). As such, this branch is
|
|
# disabled.
|
|
|
|
# Collect on-curve points
|
|
onCurveCoords = [coords[j] for j in range(len(coords))
|
|
if flags[j] & flagOnCurve]
|
|
# Add implicit on-curve points
|
|
start = 0
|
|
for end in endPts:
|
|
last = end
|
|
for j in range(start, end + 1):
|
|
if not ((flags[j] | flags[last]) & flagOnCurve):
|
|
x = (coords[last][0] + coords[j][0]) / 2
|
|
y = (coords[last][1] + coords[j][1]) / 2
|
|
onCurveCoords.append((x,y))
|
|
last = j
|
|
start = end + 1
|
|
# Add bounds for curves without an explicit extrema
|
|
start = 0
|
|
for end in endPts:
|
|
last = end
|
|
for j in range(start, end + 1):
|
|
if not (flags[j] & flagOnCurve):
|
|
next = j + 1 if j < end else start
|
|
bbox = calcBounds([coords[last], coords[next]])
|
|
if not pointInRect(coords[j], bbox):
|
|
# Ouch!
|
|
log.warning("Outline has curve with implicit extrema.")
|
|
# Ouch! Find analytical curve bounds.
|
|
pthis = coords[j]
|
|
plast = coords[last]
|
|
if not (flags[last] & flagOnCurve):
|
|
plast = ((pthis[0]+plast[0])/2, (pthis[1]+plast[1])/2)
|
|
pnext = coords[next]
|
|
if not (flags[next] & flagOnCurve):
|
|
pnext = ((pthis[0]+pnext[0])/2, (pthis[1]+pnext[1])/2)
|
|
bbox = calcQuadraticBounds(plast, pthis, pnext)
|
|
onCurveCoords.append((bbox[0],bbox[1]))
|
|
onCurveCoords.append((bbox[2],bbox[3]))
|
|
last = j
|
|
start = end + 1
|
|
|
|
self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(onCurveCoords)
|
|
else:
|
|
self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords)
|
|
else:
|
|
self.xMin, self.yMin, self.xMax, self.yMax = (0, 0, 0, 0)
|
|
|
|
def isComposite(self):
|
|
"""Can be called on compact or expanded glyph."""
|
|
if hasattr(self, "data") and self.data:
|
|
return struct.unpack(">h", self.data[:2])[0] == -1
|
|
else:
|
|
return self.numberOfContours == -1
|
|
|
|
def __getitem__(self, componentIndex):
|
|
if not self.isComposite():
|
|
raise ttLib.TTLibError("can't use glyph as sequence")
|
|
return self.components[componentIndex]
|
|
|
|
def getCoordinates(self, glyfTable):
|
|
if self.numberOfContours > 0:
|
|
return self.coordinates, self.endPtsOfContours, self.flags
|
|
elif self.isComposite():
|
|
# it's a composite
|
|
allCoords = GlyphCoordinates()
|
|
allFlags = array.array("B")
|
|
allEndPts = []
|
|
for compo in self.components:
|
|
g = glyfTable[compo.glyphName]
|
|
try:
|
|
coordinates, endPts, flags = g.getCoordinates(glyfTable)
|
|
except RecursionError:
|
|
raise ttLib.TTLibError("glyph '%s' contains a recursive component reference" % compo.glyphName)
|
|
if hasattr(compo, "firstPt"):
|
|
# move according to two reference points
|
|
x1,y1 = allCoords[compo.firstPt]
|
|
x2,y2 = coordinates[compo.secondPt]
|
|
move = x1-x2, y1-y2
|
|
else:
|
|
move = compo.x, compo.y
|
|
|
|
coordinates = GlyphCoordinates(coordinates)
|
|
if not hasattr(compo, "transform"):
|
|
coordinates.translate(move)
|
|
else:
|
|
apple_way = compo.flags & SCALED_COMPONENT_OFFSET
|
|
ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET
|
|
assert not (apple_way and ms_way)
|
|
if not (apple_way or ms_way):
|
|
scale_component_offset = SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file
|
|
else:
|
|
scale_component_offset = apple_way
|
|
if scale_component_offset:
|
|
# the Apple way: first move, then scale (ie. scale the component offset)
|
|
coordinates.translate(move)
|
|
coordinates.transform(compo.transform)
|
|
else:
|
|
# the MS way: first scale, then move
|
|
coordinates.transform(compo.transform)
|
|
coordinates.translate(move)
|
|
offset = len(allCoords)
|
|
allEndPts.extend(e + offset for e in endPts)
|
|
allCoords.extend(coordinates)
|
|
allFlags.extend(flags)
|
|
return allCoords, allEndPts, allFlags
|
|
else:
|
|
return GlyphCoordinates(), [], array.array("B")
|
|
|
|
def getComponentNames(self, glyfTable):
|
|
if not hasattr(self, "data"):
|
|
if self.isComposite():
|
|
return [c.glyphName for c in self.components]
|
|
else:
|
|
return []
|
|
|
|
# Extract components without expanding glyph
|
|
|
|
if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
|
|
return [] # Not composite
|
|
|
|
data = self.data
|
|
i = 10
|
|
components = []
|
|
more = 1
|
|
while more:
|
|
flags, glyphID = struct.unpack(">HH", data[i:i+4])
|
|
i += 4
|
|
flags = int(flags)
|
|
components.append(glyfTable.getGlyphName(int(glyphID)))
|
|
|
|
if flags & ARG_1_AND_2_ARE_WORDS: i += 4
|
|
else: i += 2
|
|
if flags & WE_HAVE_A_SCALE: i += 2
|
|
elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4
|
|
elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8
|
|
more = flags & MORE_COMPONENTS
|
|
|
|
return components
|
|
|
|
def trim(self, remove_hinting=False):
|
|
""" Remove padding and, if requested, hinting, from a glyph.
|
|
This works on both expanded and compacted glyphs, without
|
|
expanding it."""
|
|
if not hasattr(self, "data"):
|
|
if remove_hinting:
|
|
if self.isComposite():
|
|
if hasattr(self, "program"):
|
|
del self.program
|
|
else:
|
|
self.program = ttProgram.Program()
|
|
self.program.fromBytecode([])
|
|
# No padding to trim.
|
|
return
|
|
if not self.data:
|
|
return
|
|
numContours = struct.unpack(">h", self.data[:2])[0]
|
|
data = array.array("B", self.data)
|
|
i = 10
|
|
if numContours >= 0:
|
|
i += 2 * numContours # endPtsOfContours
|
|
nCoordinates = ((data[i-2] << 8) | data[i-1]) + 1
|
|
instructionLen = (data[i] << 8) | data[i+1]
|
|
if remove_hinting:
|
|
# Zero instruction length
|
|
data[i] = data [i+1] = 0
|
|
i += 2
|
|
if instructionLen:
|
|
# Splice it out
|
|
data = data[:i] + data[i+instructionLen:]
|
|
instructionLen = 0
|
|
else:
|
|
i += 2 + instructionLen
|
|
|
|
coordBytes = 0
|
|
j = 0
|
|
while True:
|
|
flag = data[i]
|
|
i = i + 1
|
|
repeat = 1
|
|
if flag & flagRepeat:
|
|
repeat = data[i] + 1
|
|
i = i + 1
|
|
xBytes = yBytes = 0
|
|
if flag & flagXShort:
|
|
xBytes = 1
|
|
elif not (flag & flagXsame):
|
|
xBytes = 2
|
|
if flag & flagYShort:
|
|
yBytes = 1
|
|
elif not (flag & flagYsame):
|
|
yBytes = 2
|
|
coordBytes += (xBytes + yBytes) * repeat
|
|
j += repeat
|
|
if j >= nCoordinates:
|
|
break
|
|
assert j == nCoordinates, "bad glyph flags"
|
|
i += coordBytes
|
|
# Remove padding
|
|
data = data[:i]
|
|
else:
|
|
more = 1
|
|
we_have_instructions = False
|
|
while more:
|
|
flags =(data[i] << 8) | data[i+1]
|
|
if remove_hinting:
|
|
flags &= ~WE_HAVE_INSTRUCTIONS
|
|
if flags & WE_HAVE_INSTRUCTIONS:
|
|
we_have_instructions = True
|
|
data[i+0] = flags >> 8
|
|
data[i+1] = flags & 0xFF
|
|
i += 4
|
|
flags = int(flags)
|
|
|
|
if flags & ARG_1_AND_2_ARE_WORDS: i += 4
|
|
else: i += 2
|
|
if flags & WE_HAVE_A_SCALE: i += 2
|
|
elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4
|
|
elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8
|
|
more = flags & MORE_COMPONENTS
|
|
if we_have_instructions:
|
|
instructionLen = (data[i] << 8) | data[i+1]
|
|
i += 2 + instructionLen
|
|
# Remove padding
|
|
data = data[:i]
|
|
|
|
self.data = data.tostring()
|
|
|
|
def removeHinting(self):
|
|
self.trim (remove_hinting=True)
|
|
|
|
def draw(self, pen, glyfTable, offset=0):
|
|
|
|
if self.isComposite():
|
|
for component in self.components:
|
|
glyphName, transform = component.getComponentInfo()
|
|
pen.addComponent(glyphName, transform)
|
|
return
|
|
|
|
coordinates, endPts, flags = self.getCoordinates(glyfTable)
|
|
if offset:
|
|
coordinates = coordinates.copy()
|
|
coordinates.translate((offset, 0))
|
|
start = 0
|
|
for end in endPts:
|
|
end = end + 1
|
|
contour = coordinates[start:end]
|
|
cFlags = flags[start:end]
|
|
start = end
|
|
if 1 not in cFlags:
|
|
# There is not a single on-curve point on the curve,
|
|
# use pen.qCurveTo's special case by specifying None
|
|
# as the on-curve point.
|
|
contour.append(None)
|
|
pen.qCurveTo(*contour)
|
|
else:
|
|
# Shuffle the points so that contour the is guaranteed
|
|
# to *end* in an on-curve point, which we'll use for
|
|
# the moveTo.
|
|
firstOnCurve = cFlags.index(1) + 1
|
|
contour = contour[firstOnCurve:] + contour[:firstOnCurve]
|
|
cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve]
|
|
pen.moveTo(contour[-1])
|
|
while contour:
|
|
nextOnCurve = cFlags.index(1) + 1
|
|
if nextOnCurve == 1:
|
|
pen.lineTo(contour[0])
|
|
else:
|
|
pen.qCurveTo(*contour[:nextOnCurve])
|
|
contour = contour[nextOnCurve:]
|
|
cFlags = cFlags[nextOnCurve:]
|
|
pen.closePath()
|
|
|
|
def drawPoints(self, pen, glyfTable, offset=0):
|
|
"""Draw the glyph using the supplied pointPen. Opposed to Glyph.draw(),
|
|
this will not change the point indices.
|
|
"""
|
|
|
|
if self.isComposite():
|
|
for component in self.components:
|
|
glyphName, transform = component.getComponentInfo()
|
|
pen.addComponent(glyphName, transform)
|
|
return
|
|
|
|
coordinates, endPts, flags = self.getCoordinates(glyfTable)
|
|
if offset:
|
|
coordinates = coordinates.copy()
|
|
coordinates.translate((offset, 0))
|
|
start = 0
|
|
for end in endPts:
|
|
end = end + 1
|
|
contour = coordinates[start:end]
|
|
cFlags = flags[start:end]
|
|
start = end
|
|
pen.beginPath()
|
|
# Start with the appropriate segment type based on the final segment
|
|
segmentType = "line" if cFlags[-1] == 1 else "qcurve"
|
|
for i, pt in enumerate(contour):
|
|
if cFlags[i] == 1:
|
|
pen.addPoint(pt, segmentType=segmentType)
|
|
segmentType = "line"
|
|
else:
|
|
pen.addPoint(pt)
|
|
segmentType = "qcurve"
|
|
pen.endPath()
|
|
|
|
def __eq__(self, other):
|
|
if type(self) != type(other):
|
|
return NotImplemented
|
|
return self.__dict__ == other.__dict__
|
|
|
|
def __ne__(self, other):
|
|
result = self.__eq__(other)
|
|
return result if result is NotImplemented else not result
|
|
|
|
class GlyphComponent(object):
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def getComponentInfo(self):
|
|
"""Return the base glyph name and a transform."""
|
|
# XXX Ignoring self.firstPt & self.lastpt for now: I need to implement
|
|
# something equivalent in fontTools.objects.glyph (I'd rather not
|
|
# convert it to an absolute offset, since it is valuable information).
|
|
# This method will now raise "AttributeError: x" on glyphs that use
|
|
# this TT feature.
|
|
if hasattr(self, "transform"):
|
|
[[xx, xy], [yx, yy]] = self.transform
|
|
trans = (xx, xy, yx, yy, self.x, self.y)
|
|
else:
|
|
trans = (1, 0, 0, 1, self.x, self.y)
|
|
return self.glyphName, trans
|
|
|
|
def decompile(self, data, glyfTable):
|
|
flags, glyphID = struct.unpack(">HH", data[:4])
|
|
self.flags = int(flags)
|
|
glyphID = int(glyphID)
|
|
self.glyphName = glyfTable.getGlyphName(int(glyphID))
|
|
data = data[4:]
|
|
|
|
if self.flags & ARG_1_AND_2_ARE_WORDS:
|
|
if self.flags & ARGS_ARE_XY_VALUES:
|
|
self.x, self.y = struct.unpack(">hh", data[:4])
|
|
else:
|
|
x, y = struct.unpack(">HH", data[:4])
|
|
self.firstPt, self.secondPt = int(x), int(y)
|
|
data = data[4:]
|
|
else:
|
|
if self.flags & ARGS_ARE_XY_VALUES:
|
|
self.x, self.y = struct.unpack(">bb", data[:2])
|
|
else:
|
|
x, y = struct.unpack(">BB", data[:2])
|
|
self.firstPt, self.secondPt = int(x), int(y)
|
|
data = data[2:]
|
|
|
|
if self.flags & WE_HAVE_A_SCALE:
|
|
scale, = struct.unpack(">h", data[:2])
|
|
self.transform = [[fi2fl(scale,14), 0], [0, fi2fl(scale,14)]] # fixed 2.14
|
|
data = data[2:]
|
|
elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE:
|
|
xscale, yscale = struct.unpack(">hh", data[:4])
|
|
self.transform = [[fi2fl(xscale,14), 0], [0, fi2fl(yscale,14)]] # fixed 2.14
|
|
data = data[4:]
|
|
elif self.flags & WE_HAVE_A_TWO_BY_TWO:
|
|
(xscale, scale01,
|
|
scale10, yscale) = struct.unpack(">hhhh", data[:8])
|
|
self.transform = [[fi2fl(xscale,14), fi2fl(scale01,14)],
|
|
[fi2fl(scale10,14), fi2fl(yscale,14)]] # fixed 2.14
|
|
data = data[8:]
|
|
more = self.flags & MORE_COMPONENTS
|
|
haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS
|
|
self.flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS |
|
|
SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET |
|
|
NON_OVERLAPPING | OVERLAP_COMPOUND)
|
|
return more, haveInstructions, data
|
|
|
|
def compile(self, more, haveInstructions, glyfTable):
|
|
data = b""
|
|
|
|
# reset all flags we will calculate ourselves
|
|
flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS |
|
|
SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET |
|
|
NON_OVERLAPPING | OVERLAP_COMPOUND)
|
|
if more:
|
|
flags = flags | MORE_COMPONENTS
|
|
if haveInstructions:
|
|
flags = flags | WE_HAVE_INSTRUCTIONS
|
|
|
|
if hasattr(self, "firstPt"):
|
|
if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255):
|
|
data = data + struct.pack(">BB", self.firstPt, self.secondPt)
|
|
else:
|
|
data = data + struct.pack(">HH", self.firstPt, self.secondPt)
|
|
flags = flags | ARG_1_AND_2_ARE_WORDS
|
|
else:
|
|
x = otRound(self.x)
|
|
y = otRound(self.y)
|
|
flags = flags | ARGS_ARE_XY_VALUES
|
|
if (-128 <= x <= 127) and (-128 <= y <= 127):
|
|
data = data + struct.pack(">bb", x, y)
|
|
else:
|
|
data = data + struct.pack(">hh", x, y)
|
|
flags = flags | ARG_1_AND_2_ARE_WORDS
|
|
|
|
if hasattr(self, "transform"):
|
|
transform = [[fl2fi(x,14) for x in row] for row in self.transform]
|
|
if transform[0][1] or transform[1][0]:
|
|
flags = flags | WE_HAVE_A_TWO_BY_TWO
|
|
data = data + struct.pack(">hhhh",
|
|
transform[0][0], transform[0][1],
|
|
transform[1][0], transform[1][1])
|
|
elif transform[0][0] != transform[1][1]:
|
|
flags = flags | WE_HAVE_AN_X_AND_Y_SCALE
|
|
data = data + struct.pack(">hh",
|
|
transform[0][0], transform[1][1])
|
|
else:
|
|
flags = flags | WE_HAVE_A_SCALE
|
|
data = data + struct.pack(">h",
|
|
transform[0][0])
|
|
|
|
glyphID = glyfTable.getGlyphID(self.glyphName)
|
|
return struct.pack(">HH", flags, glyphID) + data
|
|
|
|
def toXML(self, writer, ttFont):
|
|
attrs = [("glyphName", self.glyphName)]
|
|
if not hasattr(self, "firstPt"):
|
|
attrs = attrs + [("x", self.x), ("y", self.y)]
|
|
else:
|
|
attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)]
|
|
|
|
if hasattr(self, "transform"):
|
|
transform = self.transform
|
|
if transform[0][1] or transform[1][0]:
|
|
attrs = attrs + [
|
|
("scalex", transform[0][0]), ("scale01", transform[0][1]),
|
|
("scale10", transform[1][0]), ("scaley", transform[1][1]),
|
|
]
|
|
elif transform[0][0] != transform[1][1]:
|
|
attrs = attrs + [
|
|
("scalex", transform[0][0]), ("scaley", transform[1][1]),
|
|
]
|
|
else:
|
|
attrs = attrs + [("scale", transform[0][0])]
|
|
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 = safeEval(attrs["scalex"])
|
|
scale01 = safeEval(attrs["scale01"])
|
|
scale10 = safeEval(attrs["scale10"])
|
|
scaley = safeEval(attrs["scaley"])
|
|
self.transform = [[scalex, scale01], [scale10, scaley]]
|
|
elif "scalex" in attrs:
|
|
scalex = safeEval(attrs["scalex"])
|
|
scaley = safeEval(attrs["scaley"])
|
|
self.transform = [[scalex, 0], [0, scaley]]
|
|
elif "scale" in attrs:
|
|
scale = safeEval(attrs["scale"])
|
|
self.transform = [[scale, 0], [0, scale]]
|
|
self.flags = safeEval(attrs["flags"])
|
|
|
|
def __eq__(self, other):
|
|
if type(self) != type(other):
|
|
return NotImplemented
|
|
return self.__dict__ == other.__dict__
|
|
|
|
def __ne__(self, other):
|
|
result = self.__eq__(other)
|
|
return result if result is NotImplemented else not result
|
|
|
|
class GlyphCoordinates(object):
|
|
|
|
def __init__(self, iterable=[], typecode="h"):
|
|
self._a = array.array(typecode)
|
|
self.extend(iterable)
|
|
|
|
@property
|
|
def array(self):
|
|
return self._a
|
|
|
|
def isFloat(self):
|
|
return self._a.typecode == 'd'
|
|
|
|
def _ensureFloat(self):
|
|
if self.isFloat():
|
|
return
|
|
# The conversion to list() is to work around Jython bug
|
|
self._a = array.array("d", list(self._a))
|
|
|
|
def _checkFloat(self, p):
|
|
if self.isFloat():
|
|
return p
|
|
if any(v > 0x7FFF or v < -0x8000 for v in p):
|
|
self._ensureFloat()
|
|
return p
|
|
if any(isinstance(v, float) for v in p):
|
|
p = [int(v) if int(v) == v else v for v in p]
|
|
if any(isinstance(v, float) for v in p):
|
|
self._ensureFloat()
|
|
return p
|
|
|
|
@staticmethod
|
|
def zeros(count):
|
|
return GlyphCoordinates([(0,0)] * count)
|
|
|
|
def copy(self):
|
|
c = GlyphCoordinates(typecode=self._a.typecode)
|
|
c._a.extend(self._a)
|
|
return c
|
|
|
|
def __len__(self):
|
|
return len(self._a) // 2
|
|
|
|
def __getitem__(self, k):
|
|
if isinstance(k, slice):
|
|
indices = range(*k.indices(len(self)))
|
|
return [self[i] for i in indices]
|
|
return self._a[2*k],self._a[2*k+1]
|
|
|
|
def __setitem__(self, k, v):
|
|
if isinstance(k, slice):
|
|
indices = range(*k.indices(len(self)))
|
|
# XXX This only works if len(v) == len(indices)
|
|
for j,i in enumerate(indices):
|
|
self[i] = v[j]
|
|
return
|
|
v = self._checkFloat(v)
|
|
self._a[2*k],self._a[2*k+1] = v
|
|
|
|
def __delitem__(self, i):
|
|
i = (2*i) % len(self._a)
|
|
del self._a[i]
|
|
del self._a[i]
|
|
|
|
def __repr__(self):
|
|
return 'GlyphCoordinates(['+','.join(str(c) for c in self)+'])'
|
|
|
|
def append(self, p):
|
|
p = self._checkFloat(p)
|
|
self._a.extend(tuple(p))
|
|
|
|
def extend(self, iterable):
|
|
for p in iterable:
|
|
p = self._checkFloat(p)
|
|
self._a.extend(p)
|
|
|
|
def toInt(self):
|
|
if not self.isFloat():
|
|
return
|
|
a = array.array("h")
|
|
for n in self._a:
|
|
a.append(otRound(n))
|
|
self._a = a
|
|
|
|
def relativeToAbsolute(self):
|
|
a = self._a
|
|
x,y = 0,0
|
|
for i in range(len(a) // 2):
|
|
x = a[2*i ] + x
|
|
y = a[2*i+1] + y
|
|
self[i] = (x, y)
|
|
|
|
def absoluteToRelative(self):
|
|
a = self._a
|
|
x,y = 0,0
|
|
for i in range(len(a) // 2):
|
|
dx = a[2*i ] - x
|
|
dy = a[2*i+1] - y
|
|
x = a[2*i ]
|
|
y = a[2*i+1]
|
|
self[i] = (dx, dy)
|
|
|
|
def translate(self, p):
|
|
"""
|
|
>>> GlyphCoordinates([(1,2)]).translate((.5,0))
|
|
"""
|
|
(x,y) = self._checkFloat(p)
|
|
a = self._a
|
|
for i in range(len(a) // 2):
|
|
self[i] = (a[2*i] + x, a[2*i+1] + y)
|
|
|
|
def scale(self, p):
|
|
"""
|
|
>>> GlyphCoordinates([(1,2)]).scale((.5,0))
|
|
"""
|
|
(x,y) = self._checkFloat(p)
|
|
a = self._a
|
|
for i in range(len(a) // 2):
|
|
self[i] = (a[2*i] * x, a[2*i+1] * y)
|
|
|
|
def transform(self, t):
|
|
"""
|
|
>>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5)))
|
|
"""
|
|
a = self._a
|
|
for i in range(len(a) // 2):
|
|
x = a[2*i ]
|
|
y = a[2*i+1]
|
|
px = x * t[0][0] + y * t[1][0]
|
|
py = x * t[0][1] + y * t[1][1]
|
|
self[i] = (px, py)
|
|
|
|
def __eq__(self, other):
|
|
"""
|
|
>>> g = GlyphCoordinates([(1,2)])
|
|
>>> g2 = GlyphCoordinates([(1.0,2)])
|
|
>>> g3 = GlyphCoordinates([(1.5,2)])
|
|
>>> g == g2
|
|
True
|
|
>>> g == g3
|
|
False
|
|
>>> g2 == g3
|
|
False
|
|
"""
|
|
if type(self) != type(other):
|
|
return NotImplemented
|
|
return self._a == other._a
|
|
|
|
def __ne__(self, other):
|
|
"""
|
|
>>> g = GlyphCoordinates([(1,2)])
|
|
>>> g2 = GlyphCoordinates([(1.0,2)])
|
|
>>> g3 = GlyphCoordinates([(1.5,2)])
|
|
>>> g != g2
|
|
False
|
|
>>> g != g3
|
|
True
|
|
>>> g2 != g3
|
|
True
|
|
"""
|
|
result = self.__eq__(other)
|
|
return result if result is NotImplemented else not result
|
|
|
|
# Math operations
|
|
|
|
def __pos__(self):
|
|
"""
|
|
>>> g = GlyphCoordinates([(1,2)])
|
|
>>> g
|
|
GlyphCoordinates([(1, 2)])
|
|
>>> g2 = +g
|
|
>>> g2
|
|
GlyphCoordinates([(1, 2)])
|
|
>>> g2.translate((1,0))
|
|
>>> g2
|
|
GlyphCoordinates([(2, 2)])
|
|
>>> g
|
|
GlyphCoordinates([(1, 2)])
|
|
"""
|
|
return self.copy()
|
|
def __neg__(self):
|
|
"""
|
|
>>> g = GlyphCoordinates([(1,2)])
|
|
>>> g
|
|
GlyphCoordinates([(1, 2)])
|
|
>>> g2 = -g
|
|
>>> g2
|
|
GlyphCoordinates([(-1, -2)])
|
|
>>> g
|
|
GlyphCoordinates([(1, 2)])
|
|
"""
|
|
r = self.copy()
|
|
a = r._a
|
|
for i in range(len(a)):
|
|
a[i] = -a[i]
|
|
return r
|
|
def __round__(self):
|
|
"""
|
|
Note: This is Python 3 only. Python 2 does not call __round__.
|
|
As such, we cannot test this method either. :(
|
|
"""
|
|
r = self.copy()
|
|
r.toInt()
|
|
return r
|
|
|
|
def __add__(self, other): return self.copy().__iadd__(other)
|
|
def __sub__(self, other): return self.copy().__isub__(other)
|
|
def __mul__(self, other): return self.copy().__imul__(other)
|
|
def __truediv__(self, other): return self.copy().__itruediv__(other)
|
|
|
|
__radd__ = __add__
|
|
__rmul__ = __mul__
|
|
def __rsub__(self, other): return other + (-self)
|
|
|
|
def __iadd__(self, other):
|
|
"""
|
|
>>> g = GlyphCoordinates([(1,2)])
|
|
>>> g += (.5,0)
|
|
>>> g
|
|
GlyphCoordinates([(1.5, 2.0)])
|
|
>>> g2 = GlyphCoordinates([(3,4)])
|
|
>>> g += g2
|
|
>>> g
|
|
GlyphCoordinates([(4.5, 6.0)])
|
|
"""
|
|
if isinstance(other, tuple):
|
|
assert len(other) == 2
|
|
self.translate(other)
|
|
return self
|
|
if isinstance(other, GlyphCoordinates):
|
|
if other.isFloat(): self._ensureFloat()
|
|
other = other._a
|
|
a = self._a
|
|
assert len(a) == len(other)
|
|
for i in range(len(a) // 2):
|
|
self[i] = (a[2*i] + other[2*i], a[2*i+1] + other[2*i+1])
|
|
return self
|
|
return NotImplemented
|
|
|
|
def __isub__(self, other):
|
|
"""
|
|
>>> g = GlyphCoordinates([(1,2)])
|
|
>>> g -= (.5,0)
|
|
>>> g
|
|
GlyphCoordinates([(0.5, 2.0)])
|
|
>>> g2 = GlyphCoordinates([(3,4)])
|
|
>>> g -= g2
|
|
>>> g
|
|
GlyphCoordinates([(-2.5, -2.0)])
|
|
"""
|
|
if isinstance(other, tuple):
|
|
assert len(other) == 2
|
|
self.translate((-other[0],-other[1]))
|
|
return self
|
|
if isinstance(other, GlyphCoordinates):
|
|
if other.isFloat(): self._ensureFloat()
|
|
other = other._a
|
|
a = self._a
|
|
assert len(a) == len(other)
|
|
for i in range(len(a) // 2):
|
|
self[i] = (a[2*i] - other[2*i], a[2*i+1] - other[2*i+1])
|
|
return self
|
|
return NotImplemented
|
|
|
|
def __imul__(self, other):
|
|
"""
|
|
>>> g = GlyphCoordinates([(1,2)])
|
|
>>> g *= (2,.5)
|
|
>>> g *= 2
|
|
>>> g
|
|
GlyphCoordinates([(4.0, 2.0)])
|
|
>>> g = GlyphCoordinates([(1,2)])
|
|
>>> g *= 2
|
|
>>> g
|
|
GlyphCoordinates([(2, 4)])
|
|
"""
|
|
if isinstance(other, Number):
|
|
other = (other, other)
|
|
if isinstance(other, tuple):
|
|
if other == (1,1):
|
|
return self
|
|
assert len(other) == 2
|
|
self.scale(other)
|
|
return self
|
|
return NotImplemented
|
|
|
|
def __itruediv__(self, other):
|
|
"""
|
|
>>> g = GlyphCoordinates([(1,3)])
|
|
>>> g /= (.5,1.5)
|
|
>>> g /= 2
|
|
>>> g
|
|
GlyphCoordinates([(1.0, 1.0)])
|
|
"""
|
|
if isinstance(other, Number):
|
|
other = (other, other)
|
|
if isinstance(other, tuple):
|
|
if other == (1,1):
|
|
return self
|
|
assert len(other) == 2
|
|
self.scale((1./other[0],1./other[1]))
|
|
return self
|
|
return NotImplemented
|
|
|
|
def __bool__(self):
|
|
"""
|
|
>>> g = GlyphCoordinates([])
|
|
>>> bool(g)
|
|
False
|
|
>>> g = GlyphCoordinates([(0,0), (0.,0)])
|
|
>>> bool(g)
|
|
True
|
|
>>> g = GlyphCoordinates([(0,0), (1,0)])
|
|
>>> bool(g)
|
|
True
|
|
>>> g = GlyphCoordinates([(0,.5), (0,0)])
|
|
>>> bool(g)
|
|
True
|
|
"""
|
|
return bool(self._a)
|
|
|
|
__nonzero__ = __bool__
|
|
|
|
|
|
def reprflag(flag):
|
|
bin = ""
|
|
if isinstance(flag, str):
|
|
flag = byteord(flag)
|
|
while flag:
|
|
if flag & 0x01:
|
|
bin = "1" + bin
|
|
else:
|
|
bin = "0" + bin
|
|
flag = flag >> 1
|
|
bin = (14 - len(bin)) * "0" + bin
|
|
return bin
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import doctest, sys
|
|
sys.exit(doctest.testmod().failed)
|