def _GetCoordinates(font, glyphName): """font, glyphName --> glyph coordinates as expected by "gvar" table The result includes four "phantom points" for the glyph metrics, as mandated by the "gvar" spec. """ glyf = font["glyf"] if glyphName not in glyf.glyphs: return None glyph = glyf[glyphName] if glyph.isComposite(): coord = GlyphCoordinates([(getattr(c, 'x', 0),getattr(c, 'y', 0)) for c in glyph.components]) control = [c.glyphName for c in glyph.components] else: allData = glyph.getCoordinates(glyf) coord = allData[0] control = allData[1:] # Add phantom points for (left, right, top, bottom) positions. horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName] if not hasattr(glyph, 'xMin'): glyph.recalcBounds(glyf) leftSideX = glyph.xMin - leftSideBearing rightSideX = leftSideX + horizontalAdvanceWidth # XXX these are incorrect. Load vmtx and fix. topSideY = glyph.yMax bottomSideY = -glyph.yMin coord = coord.copy() coord.extend([(leftSideX, 0), (rightSideX, 0), (0, topSideY), (0, bottomSideY)]) return coord, control
def test_compileGlyph_onlyRedundantVariations(self): table = table__g_v_a_r() axes = {"wght": (0.3, 0.4, 0.5), "opsz": (0.7, 0.8, 0.9)} table.variations = {"glyphname": [ GlyphVariation(axes, GlyphCoordinates.zeros(4)), GlyphVariation(axes, GlyphCoordinates.zeros(4)), GlyphVariation(axes, GlyphCoordinates.zeros(4)) ]} self.assertEqual(b"", table.compileGlyph_("glyphname", 8, ["wght", "opsz"], {}))
def test_double_precision_float(self): # https://github.com/fonttools/fonttools/issues/963 afloat = 242.50000000000003 g = GlyphCoordinates([(afloat, 0)]) g.toInt() # this would return 242 if the internal array.array typecode is 'f', # since the Python float is truncated to a C float. # when using typecode 'd' it should return the correct value 243 assert g[0][0] == round(afloat)
def decompileTuple_(numPoints, sharedCoords, sharedPoints, axisTags, data, tupleData): flags = struct.unpack(">H", data[2:4])[0] pos = 4 if (flags & EMBEDDED_TUPLE_COORD) == 0: coord = sharedCoords[flags & TUPLE_INDEX_MASK] else: coord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos) if (flags & INTERMEDIATE_TUPLE) != 0: minCoord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos) maxCoord, pos = GlyphVariation.decompileCoord_(axisTags, data, pos) else: minCoord, maxCoord = table__g_v_a_r.computeMinMaxCoord_(coord) axes = {} for axis in axisTags: coords = minCoord[axis], coord[axis], maxCoord[axis] if coords != (0.0, 0.0, 0.0): axes[axis] = coords pos = 0 if (flags & PRIVATE_POINT_NUMBERS) != 0: points, pos = GlyphVariation.decompilePoints_(numPoints, tupleData, pos) else: points = sharedPoints deltas_x, pos = GlyphVariation.decompileDeltas_(len(points), tupleData, pos) deltas_y, pos = GlyphVariation.decompileDeltas_(len(points), tupleData, pos) deltas = GlyphCoordinates.zeros(numPoints) for p, x, y in zip(points, deltas_x, deltas_y): deltas[p] = (x, y) return GlyphVariation(axes, deltas)
def test_toXML_allDeltasZero(self): writer = XMLWriter(StringIO()) axes = {"wght":(0.0, 1.0, 1.0)} g = GlyphVariation(axes, GlyphCoordinates.zeros(5)) g.toXML(writer, ["wght", "wdth"]) self.assertEqual([ '<tuple>', '<coord axis="wght" value="1.0"/>', '<!-- all deltas are (0,0) -->', '</tuple>' ], GlyphVariationTest.xml_lines(writer))
def test_fromXML(self): g = GlyphVariation({}, GlyphCoordinates.zeros(4)) g.fromXML("coord", {"axis":"wdth", "min":"0.3", "value":"0.4", "max":"0.5"}, []) g.fromXML("coord", {"axis":"wght", "value":"1.0"}, []) g.fromXML("coord", {"axis":"opsz", "value":"-0.5"}, []) g.fromXML("delta", {"pt":"1", "x":"33", "y":"44"}, []) g.fromXML("delta", {"pt":"2", "x":"-2", "y":"170"}, []) self.assertEqual({ "wdth":( 0.3, 0.4, 0.5), "wght":( 0.0, 1.0, 1.0), "opsz":(-0.5, -0.5, 0.0) }, g.axes) self.assertEqual("0,0 33,44 -2,170 0,0", " ".join(["%d,%d" % c for c in g.coordinates]))
def _GetCoordinates(font, glyphName, defaultVerticalOrigin=None): """font, glyphName --> glyph coordinates as expected by "gvar" table The result includes four "phantom points" for the glyph metrics, as mandated by the "gvar" spec. """ glyf = font["glyf"] if glyphName not in glyf.glyphs: return None glyph = glyf[glyphName] if glyph.isComposite(): coord = GlyphCoordinates([(getattr(c, 'x', 0),getattr(c, 'y', 0)) for c in glyph.components]) control = (glyph.numberOfContours,[c.glyphName for c in glyph.components]) else: allData = glyph.getCoordinates(glyf) coord = allData[0] control = (glyph.numberOfContours,)+allData[1:] # Add phantom points for (left, right, top, bottom) positions. phantomPoints = _get_phantom_points(font, glyphName, defaultVerticalOrigin) coord = coord.copy() coord.extend(phantomPoints) return coord, control
def draw(self, pen): glyph = self._ttFont['glyf'][self._glyphName] glyph = self._copyGlyph(glyph, self._ttFont['glyf']) variables = self._ttFont['gvar'].variables[self._glyphName] coordinates, _ = _GetCoordinates(self._ttFont, self._glyphName) for var in variables: scalar = supportScalar(self._location, var.axes) if not scalar: continue # print(var.coordinates) if None in var.coordinates: print("warning: replacing missing deltas with (0, 0)") deltas = GlyphCoordinates([pt or (0, 0) for pt in var.coordinates]) coordinates += deltas * scalar horizontalAdvanceWidth, leftSideBearing = setCoordinates( glyph, coordinates, self._ttFont['glyf']) self.width = horizontalAdvanceWidth glyph.draw(pen, self._ttFont['glyf']) # XXX offset based on lsb
def glyph(self, componentFlags=0x4): assert self._isClosed(), "Didn't close last contour." components = self._buildComponents(componentFlags) glyph = Glyph() glyph.coordinates = GlyphCoordinates(self.points) glyph.endPtsOfContours = self.endPts glyph.flags = array("B", self.types) self.init() if components: glyph.components = components glyph.numberOfContours = -1 else: glyph.numberOfContours = len(glyph.endPtsOfContours) glyph.program = ttProgram.Program() glyph.program.fromBytecode(b"") return glyph
def instantiateComponentOffsets(ttFont, glyphName, location): glyfTable = ttFont["glyf"] gvarTable = ttFont["gvar"] assert glyfTable[glyphName].isComposite() variations = gvarTable.variations[glyphName] coordinates, _ = glyfTable.getCoordinatesAndControls(glyphName, ttFont) origCoords, endPts = None, None for var in variations: scalar = supportScalar(location, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, g = glyfTable.getCoordinatesAndControls(glyphName, ttFont) endPts = g.endPts delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar assert len(coordinates) == len(glyfTable[glyphName].components) + 4 return coordinates[:-4]
def get_tt_glyph(self): """Return a special TT Glyph record for the sbix format. It contains two dummy contours with one point (bottom left and top right) each.""" # make dummy contours glyph = TTGlyph() glyph.program = NoProgram() glyph.numberOfContours = 0 box = self.get_box() if box is not None: contours = [ [(box[0], box[1], 1)], [(box[2], box[3], 1)], ] for contour in contours: coordinates = [] flags = [] for x, y, flag in contour: if not hasattr(glyph, "xMin"): glyph.xMin = x glyph.yMin = y glyph.xMax = x glyph.yMax = y else: glyph.xMin = min(glyph.xMin, x) glyph.yMin = min(glyph.yMin, y) glyph.xMax = max(glyph.xMax, x) glyph.yMax = max(glyph.yMax, y) coordinates.append([x, y]) flags.append(flag) coordinates = GlyphCoordinates(coordinates) flags = array.array("B", flags) if not hasattr(glyph, "coordinates"): glyph.coordinates = coordinates glyph.flags = flags glyph.endPtsOfContours = [len(coordinates) - 1] else: glyph.coordinates.extend(coordinates) glyph.flags.extend(flags) glyph.endPtsOfContours.append(len(glyph.coordinates) - 1) glyph.numberOfContours += 1 return glyph
def glyph(self, componentFlags=0x4): """Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.""" assert self._isClosed(), "Didn't close last contour." components = self._buildComponents(componentFlags) glyph = Glyph() glyph.coordinates = GlyphCoordinates(self.points) glyph.coordinates.toInt() glyph.endPtsOfContours = self.endPts glyph.flags = array("B", self.types) self.init() if components: glyph.components = components glyph.numberOfContours = -1 else: glyph.numberOfContours = len(glyph.endPtsOfContours) glyph.program = ttProgram.Program() glyph.program.fromBytecode(b"") return glyph
def _newGlyph(self, name, **kwargs): import array layer = self.naked() self._trashPost(layer) layer["hmtx"][name] = (0, 0) layer.glyphOrder.append(name) # newId = layer["maxp"].numGlyphs # layer["maxp"].numGlyphs = newId + 1 if "hdmx" in layer: del (layer["hdmx"]) # Obviously this is wrong. XXX layer["glyf"][name] = Glyph() # XXX Only TTF layer["glyf"][name].numberOfContours = -1 # Only components right now layer["glyf"][name].flags = array.array("B", []) layer["glyf"][name].coordinates = GlyphCoordinates([]) layer["glyf"][name].endPtsOfContours = [] layer["glyf"][name].program = ttProgram.Program() layer["glyf"][name].program.fromBytecode([]) layer["glyf"][name].xMin = 0 layer["glyf"][name].yMin = 0 layer["glyf"][name].xMax = 0 layer["glyf"][name].yMax = 0 return self[name]
def fromXML(self, name, attrs, content, ttFont): if name == "version": self.version = safeEval(attrs["value"]) elif name == "reserved": self.reserved = safeEval(attrs["value"]) elif name == "glyphVariations": if not hasattr(self, "variations"): self.variations = {} glyphName = attrs["glyph"] glyph = ttFont["glyf"][glyphName] numPoints = self.getNumPoints_(glyph) glyphVariations = [] for element in content: if isinstance(element, tuple): name, attrs, content = element if name == "tuple": gvar = GlyphVariation({}, GlyphCoordinates.zeros(numPoints)) glyphVariations.append(gvar) for tupleElement in content: if isinstance(tupleElement, tuple): tupleName, tupleAttrs, tupleContent = tupleElement gvar.fromXML(tupleName, tupleAttrs, tupleContent) self.variations[glyphName] = glyphVariations
def test_compileSharedCoords(self): class FakeFont: def getGlyphOrder(self): return ["A", "B", "C"] font = FakeFont() table = table__g_v_a_r() table.variations = {} table.variations["A"] = [ GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.5, 0.7, 1.0)}, GlyphCoordinates.zeros(4)) ] table.variations["B"] = [ GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.2, 0.7, 1.0)}, GlyphCoordinates.zeros(4)), GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.2, 0.8, 1.0)}, GlyphCoordinates.zeros(4)) ] table.variations["C"] = [ GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.3, 0.7, 1.0)}, GlyphCoordinates.zeros(4)), GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.3, 0.8, 1.0)}, GlyphCoordinates.zeros(4)), GlyphVariation({"wght": (1.0, 1.0, 1.0), "wdth": (0.3, 0.9, 1.0)}, GlyphCoordinates.zeros(4)) ] # {"wght":1.0, "wdth":0.7} is shared 3 times; {"wght":1.0, "wdth":0.8} is shared twice. # Min and max values are not part of the shared coordinate pool and should get ignored. result = table.compileSharedCoords_(font, ["wght", "wdth"]) self.assertEqual(["40 00 2C CD", "40 00 33 33"], [hexencode(c) for c in result])
def test__checkFloat_overflow(self): g = GlyphCoordinates([(1, 1)]) g.append((0x8000, 0)) assert list(g.array) == [1.0, 1.0, 32768.0, 0.0]
def test_getCoordinates(self): glyphSet = {} pen = TTGlyphPen(glyphSet) pen.moveTo((0, 0)) pen.lineTo((100, 0)) pen.lineTo((100, 100)) pen.lineTo((0, 100)) pen.closePath() # simple contour glyph glyphSet["a"] = a = pen.glyph() assert a.getCoordinates(glyphSet) == ( GlyphCoordinates([(0, 0), (100, 0), (100, 100), (0, 100)]), [3], array.array("B", [1, 1, 1, 1]), ) # composite glyph with only XY offset pen = TTGlyphPen(glyphSet) pen.addComponent("a", (1, 0, 0, 1, 10, 20)) glyphSet["b"] = b = pen.glyph() assert b.getCoordinates(glyphSet) == ( GlyphCoordinates([(10, 20), (110, 20), (110, 120), (10, 120)]), [3], array.array("B", [1, 1, 1, 1]), ) # composite glyph with a scale (and referencing another composite glyph) pen = TTGlyphPen(glyphSet) pen.addComponent("b", (0.5, 0, 0, 0.5, 0, 0)) glyphSet["c"] = c = pen.glyph() assert c.getCoordinates(glyphSet) == ( GlyphCoordinates([(5, 10), (55, 10), (55, 60), (5, 60)]), [3], array.array("B", [1, 1, 1, 1]), ) # composite glyph with unscaled offset (MS-style) pen = TTGlyphPen(glyphSet) pen.addComponent("a", (0.5, 0, 0, 0.5, 10, 20)) glyphSet["d"] = d = pen.glyph() d.components[0].flags |= UNSCALED_COMPONENT_OFFSET assert d.getCoordinates(glyphSet) == ( GlyphCoordinates([(10, 20), (60, 20), (60, 70), (10, 70)]), [3], array.array("B", [1, 1, 1, 1]), ) # composite glyph with a scaled offset (Apple-style) pen = TTGlyphPen(glyphSet) pen.addComponent("a", (0.5, 0, 0, 0.5, 10, 20)) glyphSet["e"] = e = pen.glyph() e.components[0].flags |= SCALED_COMPONENT_OFFSET assert e.getCoordinates(glyphSet) == ( GlyphCoordinates([(5, 10), (55, 10), (55, 60), (5, 60)]), [3], array.array("B", [1, 1, 1, 1]), ) # composite glyph where the 2nd and 3rd components use anchor points pen = TTGlyphPen(glyphSet) pen.addComponent("a", (1, 0, 0, 1, 0, 0)) glyphSet["f"] = f = pen.glyph() comp1 = GlyphComponent() comp1.glyphName = "a" # aling the new component's pt 0 to pt 2 of contour points added so far comp1.firstPt = 2 comp1.secondPt = 0 comp1.flags = 0 f.components.append(comp1) comp2 = GlyphComponent() comp2.glyphName = "a" # aling the new component's pt 0 to pt 6 of contour points added so far comp2.firstPt = 6 comp2.secondPt = 0 comp2.transform = [[0.707107, 0.707107], [-0.707107, 0.707107]] # rotate 45 deg comp2.flags = WE_HAVE_A_TWO_BY_TWO f.components.append(comp2) coords, end_pts, flags = f.getCoordinates(glyphSet) assert end_pts == [3, 7, 11] assert flags == array.array("B", [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) assert list(sum(coords, ())) == pytest.approx([ 0, 0, 100, 0, 100, 100, 0, 100, 100, 100, 200, 100, 200, 200, 100, 200, 200, 200, 270.7107, 270.7107, 200.0, 341.4214, 129.2893, 270.7107, ])
def test__rsub__(self): g = GlyphCoordinates([(1, 2)]) # other + (-self) assert (1, 1) - g == GlyphCoordinates([(0, -1)])
def main(args=None): if args is None: import sys args = sys.argv[1:] varfilename = args[0] locargs = args[1:] outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' loc = {} for arg in locargs: tag, val = arg.split('=') assert len(tag) <= 4 loc[tag.ljust(4)] = float(val) print("Location:", loc) print("Loading variable font") varfont = TTFont(varfilename) fvar = varfont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(loc, axes) # Location is normalized now print("Normalized location:", loc) gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates, _ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = _iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in varfont: del varfont[tag] print("Saving instance font", outfile) varfont.save(outfile)
def test_transform(self): g = GlyphCoordinates([(1, 2)]) g.transform(((.5, 0), (.2, .5))) assert g[0] == GlyphCoordinates([(0.9, 1.0)])[0]
def test__neg__(self): g = GlyphCoordinates([(1, 2)]) g2 = -g assert g2 == GlyphCoordinates([(-1, -2)])
def test__itruediv__(self): g = GlyphCoordinates([(1, 3)]) g /= (.5, 1.5) g /= 2 assert g == GlyphCoordinates([(1.0, 1.0)])
def test_translate(self): g = GlyphCoordinates([(1, 2)]) g.translate((.5, 0)) assert g == GlyphCoordinates([(1.5, 2.0)])
def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary defining the desired location along the variable font's axes. The location values must be specified as user-space coordinates, e.g.: {'wght': 400, 'wdth': 100} By default, a new TTFont object is returned. If ``inplace`` is True, the input varfont is modified and reduced to a static font. """ if not inplace: # make a copy to leave input varfont unmodified stream = BytesIO() varfont.save(stream) stream.seek(0) varfont = TTFont(stream) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} loc = normalizeLocation(location, axes) if 'avar' in varfont: maps = varfont['avar'].segments loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()} # Quantize to F2Dot14, to avoid surprise interpolations. loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()} # Location is normalized now log.info("Normalized location: %s", loc) if 'gvar' in varfont: log.info("Mutating glyf/gvar tables") gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) else: glyf = None if 'cvar' in varfont: log.info("Mutating cvt/cvar tables") cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += otRound(delta) if 'CFF2' in varfont: log.info("Mutating CFF2 table") glyphOrder = varfont.getGlyphOrder() CFF2 = varfont['CFF2'] topDict = CFF2.cff.topDictIndex[0] vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc) interpolateFromDeltas = vsInstancer.interpolateFromDeltas interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas) CFF2.desubroutinize() interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder) interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc) del topDict.rawDict['VarStore'] del topDict.VarStore if 'MVAR' in varfont: log.info("Mutating MVAR table") mvar = varfont['MVAR'].table varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) records = mvar.ValueRecord for rec in records: mvarTag = rec.ValueTag if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, getattr(varfont[tableTag], itemName) + delta) log.info("Mutating FeatureVariations") for tableTag in 'GSUB','GPOS': if not tableTag in varfont: continue table = varfont[tableTag].table if not hasattr(table, 'FeatureVariations'): continue variations = table.FeatureVariations for record in variations.FeatureVariationRecord: applies = True for condition in record.ConditionSet.ConditionTable: if condition.Format == 1: axisIdx = condition.AxisIndex axisTag = fvar.axes[axisIdx].axisTag Min = condition.FilterRangeMinValue Max = condition.FilterRangeMaxValue v = loc[axisTag] if not (Min <= v <= Max): applies = False else: applies = False if not applies: break if applies: assert record.FeatureTableSubstitution.Version == 0x00010000 for rec in record.FeatureTableSubstitution.SubstitutionRecord: table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature break del table.FeatureVariations if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003: log.info("Mutating GDEF/GPOS/GSUB tables") gdef = varfont['GDEF'].table instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc) merger = MutatorMerger(varfont, loc) merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS']) # Downgrade GDEF. del gdef.VarStore gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef is None: del gdef.MarkGlyphSetsDef gdef.Version = 0x00010000 if not (gdef.LigCaretList or gdef.MarkAttachClassDef or gdef.GlyphClassDef or gdef.AttachList or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)): del varfont['GDEF'] addidef = False if glyf: for glyph in glyf.glyphs.values(): if hasattr(glyph, "program"): instructions = glyph.program.getAssembly() # If GETVARIATION opcode is used in bytecode of any glyph add IDEF addidef = any(op.startswith("GETVARIATION") for op in instructions) if addidef: break if addidef: log.info("Adding IDEF to fpgm table for GETVARIATION opcode") asm = [] if 'fpgm' in varfont: fpgm = varfont['fpgm'] asm = fpgm.program.getAssembly() else: fpgm = newTable('fpgm') fpgm.program = ttProgram.Program() varfont['fpgm'] = fpgm asm.append("PUSHB[000] 145") asm.append("IDEF[ ]") args = [str(len(loc))] for a in fvar.axes: args.append(str(floatToFixed(loc[a.axisTag], 14))) asm.append("NPUSHW[ ] " + ' '.join(args)) asm.append("ENDF[ ]") fpgm.program.fromAssembly(asm) # Change maxp attributes as IDEF is added if 'maxp' in varfont: maxp = varfont['maxp'] if hasattr(maxp, "maxInstructionDefs"): maxp.maxInstructionDefs += 1 else: setattr(maxp, "maxInstructionDefs", 1) if hasattr(maxp, "maxStackElements"): maxp.maxStackElements = max(len(loc), maxp.maxStackElements) else: setattr(maxp, "maxInstructionDefs", len(loc)) if 'name' in varfont: log.info("Pruning name table") exclude = {a.axisNameID for a in fvar.axes} for i in fvar.instances: exclude.add(i.subfamilyNameID) exclude.add(i.postscriptNameID) if 'ltag' in varfont: # Drop the whole 'ltag' table if all its language tags are referenced by # name records to be pruned. # TODO: prune unused ltag tags and re-enumerate langIDs accordingly excludedUnicodeLangIDs = [ n.langID for n in varfont['name'].names if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF ] if set(excludedUnicodeLangIDs) == set(range(len((varfont['ltag'].tags)))): del varfont['ltag'] varfont['name'].names[:] = [ n for n in varfont['name'].names if n.nameID not in exclude ] if "wght" in location and "OS/2" in varfont: varfont["OS/2"].usWeightClass = otRound( max(1, min(location["wght"], 1000)) ) if "wdth" in location: wdth = location["wdth"] for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): if wdth < percent: varfont["OS/2"].usWidthClass = widthClass break else: varfont["OS/2"].usWidthClass = 9 if "slnt" in location and "post" in varfont: varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] return varfont
def test__checkFloat_overflow(self): g = GlyphCoordinates([(1, 1)], typecode="h") g.append((0x8000, 0)) assert g.array.typecode == "d" assert g.array == array.array("d", [1.0, 1.0, 32768.0, 0.0])
def generateInstance(variableFontPath, location, targetDirectory, normalize=True): u""" Instantiate an instance of a variable font at the specified location. Keyword arguments: varfilename -- a variable font file path location -- a dictionary of axis tag and value {"wght": 0.75, "wdth": -0.5} """ # make a custom file name from the location e.g. VariableFont-wghtXXX-wdthXXX.ttf instanceName = "" for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) targetFileName = '.'.join(variableFontPath.split('/')[-1].split('.')[:-1]) + instanceName + '.ttf' if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) outFile = targetDirectory + targetFileName if not os.path.exists(outFile): # Instance does not exist as file. Create it. # print("Loading GX font") varFont = TTFont(variableFontPath) # Set the instance name IDs in the name table platforms=((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = varFont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode(instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") varFont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name varFont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name varFont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix fvar = varFont['fvar'] axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes} # TODO Round to F2Dot14? if normalize: normalizedLoc = normalizeLocation(location, axes) else: normalizedLoc = location # Location is normalized now if DEBUG: print("Normalized location:", varFileName, normalizedLoc) gvar = varFont['gvar'] for glyphName, variations in gvar.variations.items(): coordinates, _ = _GetCoordinates(varFont, glyphName) for var in variations: scalar = supportScalar(normalizedLoc, var.axes) if not scalar: continue # TODO Do IUP / handle None items varCoords = [] for coord in var.coordinates: # TODO temp hack to avoid NoneType if coord is None: varCoords.append((0, 0)) else: varCoords.append(coord) coordinates += GlyphCoordinates(varCoords) * scalar # coordinates += GlyphCoordinates(var.coordinates) * scalar _SetCoordinates(varFont, glyphName, coordinates) # print("Removing GX tables") for tag in ('fvar', 'avar', 'gvar'): if tag in varFont: del varFont[tag] # Fix leading bug in drawbot by setting lineGap to 0 varFont['hhea'].lineGap = 0 if DEBUG: print("Saving instance font", outFile) varFont.save(outFile) # Installing the font in DrawBot. Answer font name and path. return installFont(outFile), outFile
def generateInstance(variableFontPath, location, targetDirectory, normalize=False, force=False): instanceName = "" for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) targetFileName = '.'.join(variableFontPath.split('/')[-1].split('.') [:-1]) + instanceName + '.ttf' if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) outFile = targetDirectory + targetFileName if force or not os.path.exists(outFile): #print location #print("Loading variable font") varFont = TTFont(variableFontPath) fvar = varFont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(location, axes) # Location is normalized now #print("Normalized location:", loc, 'from', location) # Set the instance name IDs in the name table platforms = ((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = varFont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode( instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") varFont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name varFont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name varFont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix gvar = varFont['gvar'] glyf = varFont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates, _ = _GetCoordinates(varFont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) #, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates( varFont, glyphname) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = _iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varFont, glyphname, coordinates) #print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in varFont: del varFont[tag] #print("Saving instance font", outFile) varFont.save(outFile) # Installing the font in DrawBot. Answer font name and path. return c.installFont(outFile), outFile
def generateInstance(variableFontPath, location, targetDirectory, normalize=True, cached=True, lazy=True): """ D E P R E C A T E D Use pagebot.fonttoolbox.objects.font.instantiateVariableFont instead (calling fontTools) Instantiate an instance of a variable font at the specified location. Keyword arguments: varfilename -- a variable font file path location -- a dictionary of axis tag and value {"wght": 0.75, "wdth": -0.5} """ # make a custom file name from the location e.g. VariableFont-wghtXXX-wdthXXX.ttf instanceName = "" for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) targetFileName = '.'.join(variableFontPath.split('/')[-1].split('.') [:-1]) + instanceName + '.ttf' if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) outFile = targetDirectory + targetFileName if not cached or not os.path.exists(outFile): # Instance does not exist as file. Create it. # print("Loading GX font") varfont = TTFont(variableFontPath, lazy=lazy) # Set the instance name IDs in the name table platforms = ((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = varfont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode( instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") varfont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name varfont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name varfont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix fvar = varfont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(location, axes) # Location is normalized now #print("Normalized location:", loc) gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates, _ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) #, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates( varfont, glyphname) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) # Interpolate cvt if 'cvar' in varfont: cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += int(round(delta)) #print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in varfont: del varfont[tag] #print("Saving instance font", outFile) varfont.save(outFile) # Answer the font name path. return outFile
def test__abs__(self): g = GlyphCoordinates([(-1.5, 2)]) g2 = abs(g) assert g2 == GlyphCoordinates([(1.5, 2)])
def test_compileIntermediateCoord(self): gvar = GlyphVariation({"wght": (-1.0, -1.0, 0.0), "wdth": (0.4, 0.5, 0.6)}, GlyphCoordinates.zeros(4)) self.assertEqual("C0 00 19 9A 00 00 26 66", hexencode(gvar.compileIntermediateCoord(["wght", "wdth"]))) self.assertEqual("19 9A C0 00 26 66 00 00", hexencode(gvar.compileIntermediateCoord(["wdth", "wght"]))) self.assertEqual(None, gvar.compileIntermediateCoord(["wght"])) self.assertEqual("19 9A 26 66", hexencode(gvar.compileIntermediateCoord(["wdth"])))
def test__checkFloat_overflow(self): g = GlyphCoordinates([(1, 1)], typecode="h") g.append((0x8000, 0)) assert g.array.typecode == "d" assert g.array == array.array("d", [1.0, 1.0, 32768.0, 0.0])
def makeInstance(pathOrVarFont, location, dstPath=None, normalize=True, cached=True, lazy=True, kerning=None): """Instantiate an instance of a variable font at the specified location. Keyword arguments: - varfilename -- a variable font file path - location -- a dictionary of axis tag and value {"wght": 0.75, "wdth": -0.5} >>> vf = findFont('RobotoDelta-VF') >>> print(vf) <Font RobotoDelta-VF> >>> print(len(vf)) 188 >>> instance = makeInstance(vf.path, dict(opsz=8), cached=False) >>> instance <Font RobotoDelta-VF-opsz8> >>> len(instance) 241 >>> len(instance['H'].points) 12 >>> instance['Egrave'] <PageBot Glyph Egrave Pts:0/Cnt:0/Cmp:2> >>> len(instance['Egrave'].components) 2 """ # make a custom file name from the location e.g. # VariableFont-wghtXXX-wdthXXX.ttf instanceName = "" if isinstance(pathOrVarFont, Font): pathOrVarFont = pathOrVarFont.path varFont = Font(pathOrVarFont, lazy=lazy) ttFont = varFont.ttFont for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) if dstPath is None: targetFileName = '.'.join(varFont.path.split('/')[-1].split('.') [:-1]) + instanceName + '.ttf' targetDirectory = getInstancePath() if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) dstPath = targetDirectory + targetFileName # Instance does not exist as file. Create it. if not cached or not os.path.exists(dstPath): # Set the instance name IDs in the name table platforms = ((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = ttFont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode( instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") ttFont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name ttFont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name ttFont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix fvar = ttFont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(location, axes) # Location is normalized now #print("Normalized location:", loc) gvar = ttFont['gvar'] glyf = ttFont['glyf'] # get list of glyph names in gvar sorted by component depth glyphNames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphName in glyphNames: variations = gvar.variations[glyphName] coordinates, _ = _GetCoordinates(ttFont, glyphName) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) #, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates( ttFont, glyphName) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(ttFont, glyphName, coordinates) # Interpolate cvt if 'cvar' in ttFont: cvar = ttFont['cvar'] cvt = ttFont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += int(round(delta)) #print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in ttFont: del ttFont[tag] if kerning is not None: for pair, value in kerning.items(): varFont.kerning[pair] = value #print("Saving instance font", outFile) varFont.save(dstPath) # Answer instance. return Font(dstPath, lazy=lazy)
def test_scale(self): g = GlyphCoordinates([(1, 2)]) g.scale((.5, 0)) assert g == GlyphCoordinates([(0.5, 0.0)])
def test_scale(self): g = GlyphCoordinates([(1,2)]) g.scale((.5,0)) assert g == GlyphCoordinates([(0.5,0.0)])
def test__pos__(self): g = GlyphCoordinates([(1, 2)]) g2 = +g assert g == g2
def test_compileCoord(self): gvar = GlyphVariation({"wght": (-1.0, -1.0, -1.0), "wdth": (0.4, 0.5, 0.6)}, GlyphCoordinates.zeros(4)) self.assertEqual("C0 00 20 00", hexencode(gvar.compileCoord(["wght", "wdth"]))) self.assertEqual("20 00 C0 00", hexencode(gvar.compileCoord(["wdth", "wght"]))) self.assertEqual("C0 00", hexencode(gvar.compileCoord(["wght"])))
def test__round__(self): g = GlyphCoordinates([(-1.5, 2)]) g2 = round(g) assert g2 == GlyphCoordinates([(-1, 2)])
def test_transform(self): g = GlyphCoordinates([(1,2)]) g.transform(((.5,0),(.2,.5))) assert g[0] == GlyphCoordinates([(0.9,1.0)])[0]
def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary defining the desired location along the variable font's axes. The location values must be specified as user-space coordinates, e.g.: {'wght': 400, 'wdth': 100} By default, a new TTFont object is returned. If ``inplace`` is True, the input varfont is modified and reduced to a static font. """ if not inplace: # make a copy to leave input varfont unmodified stream = BytesIO() varfont.save(stream) stream.seek(0) varfont = TTFont(stream) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} loc = normalizeLocation(location, axes) if 'avar' in varfont: maps = varfont['avar'].segments loc = {k:_DesignspaceAxis._map(v, maps[k]) for k,v in loc.items()} # Quantize to F2Dot14, to avoid surprise interpolations. loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()} # Location is normalized now log.info("Normalized location: %s", loc) log.info("Mutating glyf/gvar tables") gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) if 'cvar' in varfont: log.info("Mutating cvt/cvar tables") cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += otRound(delta) if 'MVAR' in varfont: log.info("Mutating MVAR table") mvar = varfont['MVAR'].table varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) records = mvar.ValueRecord for rec in records: mvarTag = rec.ValueTag if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, getattr(varfont[tableTag], itemName) + delta) if 'GDEF' in varfont: log.info("Mutating GDEF/GPOS/GSUB tables") merger = MutatorMerger(varfont, loc) log.info("Building interpolated tables") merger.instantiate() if 'name' in varfont: log.info("Pruning name table") exclude = {a.axisNameID for a in fvar.axes} for i in fvar.instances: exclude.add(i.subfamilyNameID) exclude.add(i.postscriptNameID) varfont['name'].names[:] = [ n for n in varfont['name'].names if n.nameID not in exclude ] if "wght" in location and "OS/2" in varfont: varfont["OS/2"].usWeightClass = otRound( max(1, min(location["wght"], 1000)) ) if "wdth" in location: wdth = location["wdth"] for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): if wdth < percent: varfont["OS/2"].usWidthClass = widthClass break else: varfont["OS/2"].usWidthClass = 9 if "slnt" in location and "post" in varfont: varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] return varfont
def test_translate(self): g = GlyphCoordinates([(1,2)]) g.translate((.5,0)) assert g == GlyphCoordinates([(1.5,2.0)])