def test_curveTo_no_points(self): quadpen = Cu2QuPen(DummyPen(), MAX_ERR) quadpen.moveTo((0, 0)) with self.assertRaisesRegex(AssertionError, "illegal curve segment point count: 0"): quadpen.curveTo()
def test_addComponent(self): pen = DummyPen() quadpen = Cu2QuPen(pen, MAX_ERR) quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0)) # components are passed through without changes self.assertEqual( str(pen).splitlines(), [ "pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))", ])
def test_curveTo_3_points(self): pen = DummyPen() quadpen = Cu2QuPen(pen, MAX_ERR) quadpen.moveTo((0, 0)) quadpen.curveTo((1, 1), (2, 2), (3, 3)) self.assertEqual( str(pen).splitlines(), [ "pen.moveTo((0, 0))", "pen.qCurveTo((0.75, 0.75), (2.25, 2.25), (3, 3))", ])
def test_curveTo_1_point(self): pen = DummyPen() quadpen = Cu2QuPen(pen, MAX_ERR) quadpen.moveTo((0, 0)) quadpen.curveTo((1, 1)) self.assertEqual( str(pen).splitlines(), [ "pen.moveTo((0, 0))", "pen.lineTo((1, 1))", ])
def test_qCurveTo_more_than_1_point(self): pen = DummyPen() quadpen = Cu2QuPen(pen, MAX_ERR) quadpen.moveTo((0, 0)) quadpen.qCurveTo((1, 1), (2, 2)) self.assertEqual( str(pen).splitlines(), [ "pen.moveTo((0, 0))", "pen.qCurveTo((1, 1), (2, 2))", ])
def glyphs_to_quadratic( glyphs, max_err=MAX_ERR, reverse_direction=REVERSE_DIRECTION): quadGlyphs = {} for gname in glyphs.keys(): glyph = glyphs[gname] ttPen = TTGlyphPen(glyphs) cu2quPen = Cu2QuPen(ttPen, max_err, reverse_direction=reverse_direction) glyph.draw(cu2quPen) quadGlyphs[gname] = ttPen.glyph() return quadGlyphs
def test_curveTo_more_than_3_points(self): # a 'SuperBezier' as described in fontTools.basePen.AbstractPen pen = DummyPen() quadpen = Cu2QuPen(pen, MAX_ERR) quadpen.moveTo((0, 0)) quadpen.curveTo((1, 1), (2, 2), (3, 3), (4, 4)) self.assertEqual( str(pen).splitlines(), [ "pen.moveTo((0, 0))", "pen.qCurveTo((0.75, 0.75), (1.625, 1.625), (2, 2))", "pen.qCurveTo((2.375, 2.375), (3.25, 3.25), (4, 4))", ])
def test__check_contour_closed(self): msg = "closePath or endPath is required" quadpen = Cu2QuPen(DummyPen(), MAX_ERR) quadpen.moveTo((0, 0)) with self.assertRaisesRegex(AssertionError, msg): quadpen.moveTo((1, 1)) with self.assertRaisesRegex(AssertionError, msg): quadpen.addComponent("a", (1, 0, 0, 1, 0, 0)) # it works if contour is closed quadpen.closePath() quadpen.moveTo((1, 1)) quadpen.endPath() quadpen.addComponent("a", (1, 0, 0, 1, 0, 0))
def test__check_contour_is_open(self): msg = "moveTo is required" quadpen = Cu2QuPen(DummyPen(), MAX_ERR) with self.assertRaisesRegex(AssertionError, msg): quadpen.lineTo((0, 0)) with self.assertRaisesRegex(AssertionError, msg): quadpen.qCurveTo((0, 0), (1, 1)) with self.assertRaisesRegex(AssertionError, msg): quadpen.curveTo((0, 0), (1, 1), (2, 2)) with self.assertRaisesRegex(AssertionError, msg): quadpen.closePath() with self.assertRaisesRegex(AssertionError, msg): quadpen.endPath() quadpen.moveTo((0, 0)) # now it works quadpen.lineTo((1, 1)) quadpen.qCurveTo((2, 2), (3, 3)) quadpen.curveTo((4, 4), (5, 5), (6, 6)) quadpen.closePath()
def test_ignore_single_points(self): pen = DummyPen() try: logging.captureWarnings(True) with CapturingLogHandler("py.warnings", level="WARNING") as log: quadpen = Cu2QuPen(pen, MAX_ERR, ignore_single_points=True) finally: logging.captureWarnings(False) quadpen.moveTo((0, 0)) quadpen.endPath() quadpen.moveTo((1, 1)) quadpen.closePath() self.assertGreaterEqual(len(log.records), 1) if sys.version_info < (3, 11): self.assertIn("ignore_single_points is deprecated", log.records[0].args[0]) else: self.assertIn("ignore_single_points is deprecated", log.records[0].msg) # single-point contours were ignored, so the pen commands are empty self.assertFalse(pen.commands) # redraw without ignoring single points quadpen.ignore_single_points = False quadpen.moveTo((0, 0)) quadpen.endPath() quadpen.moveTo((1, 1)) quadpen.closePath() self.assertTrue(pen.commands) self.assertEqual( str(pen).splitlines(), [ "pen.moveTo((0, 0))", "pen.endPath()", "pen.moveTo((1, 1))", "pen.closePath()" ])
def _save_ttfont(bbf): fb = FontBuilder(bbf.info.unitsPerEm, isTTF=True) fb.setupGlyphOrder(bbf.lib.glyphOrder) bbf._build_maps() fb.setupCharacterMap(bbf._unicodemap) glyf = {} metrics = {} for i in [1, 2]: for g in bbf: if i == 1 and g.components: continue if i == 2 and not g.components: continue pen = Cu2QuPen(TTGlyphPen(glyf), bbf.info.unitsPerEm / 1000) try: g.draw(pen) glyf[g.name] = pen.pen.glyph() except Exception as e: pen = Cu2QuPen(TTGlyphPen(glyf), bbf.info.unitsPerEm / 1000) glyf[g.name] = pen.pen.glyph() pass bounds = g.bounds xMin = 0 if bounds: xMin = bounds[0] metrics[g.name] = (g.width, xMin) versionMajor = bbf.info.versionMajor or 1 versionMinor = bbf.info.versionMinor or 0 fb.updateHead( fontRevision=versionMajor + versionMinor / 10**len(str(versionMinor)), created=_ufo_date_to_opentime(bbf.info.openTypeHeadCreated), lowestRecPPEM=bbf.info.openTypeHeadLowestRecPPEM or fb.font["head"].lowestRecPPEM, ) fb.setupGlyf(glyf) fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader( ascent=int(bbf.info.openTypeHheaAscender or bbf.info.ascender or 0), descent=int(bbf.info.openTypeHheaDescender or bbf.info.descender or 0), ) os2 = { "sTypoAscender": bbf.info.openTypeOS2TypoAscender or bbf.info.ascender or 0, "sTypoDescender": bbf.info.openTypeOS2TypoDescender or bbf.info.descender or 0, "sxHeight": bbf.info.xHeight, "sCapHeight": bbf.info.capHeight, } for k in [ "usWidthClass", "usWeightClass", "sTypoLineGap", "usWinAscent", "usWinDescent", "ySubscriptXSize", "ySubscriptYSize", "ySubscriptXOffset", "ySubscriptYOffset", "ySuperscriptXSize", "ySuperscriptYSize", "ySuperscriptXOffset", "ySuperscriptYOffset", "yStrikeoutSize", "yStrikeoutPosition", ]: infokey = k while not infokey[0].isupper(): infokey = infokey[1:] infokey = "openTypeOS2" + infokey if hasattr(bbf.info, infokey): if getattr(bbf.info, infokey): os2[k] = getattr(bbf.info, infokey) fb.setupOS2(**os2) # Name tables nameStrings = dict( familyName=dict(en=bbf.info.familyName), styleName=dict(en=bbf.info.styleName), fullName=f"{bbf.info.familyName} {bbf.info.styleName}", psName=bbf.info.postscriptFontName, version="Version " + str(bbf.info.versionMajor + bbf.info.versionMinor / 10**len(str(bbf.info.versionMinor))), ) if not nameStrings["psName"]: del nameStrings["psName"] fb.setupNameTable(nameStrings) # Kerning # Features fb.setupPost( underlinePosition=(bbf.info.postscriptUnderlinePosition or 0), underlineThickness=(bbf.info.postscriptUnderlineThickness or 0), ) return fb.font
def build(instance, isTTF, version): font = instance.parent source = font.masters[0] fea, marks = makeFeatures(instance, source) source.blueValues = [] source.otherBlues = [] for zone in sorted(source.alignmentZones): pos = zone.position size = zone.size vals = sorted((pos, pos + size)) if pos == 0 or size >= 0: source.blueValues.extend(vals) else: source.otherBlues.extend(vals) fontinfo = f""" FontName {instance.fontName} OrigEmSqUnits {font.upm} DominantV {source.verticalStems} DominantH {source.horizontalStems} BaselineOvershoot {source.blueValues[0]} BaselineYCoord {source.blueValues[1]} LcHeight {source.blueValues[2]} LcOvershoot {source.blueValues[3] - source.blueValues[2]} CapHeight {source.blueValues[4]} CapOvershoot {source.blueValues[5] - source.blueValues[4]} AscenderHeight {source.blueValues[6]} AscenderOvershoot {source.blueValues[7] - source.blueValues[6]} Baseline5 {source.otherBlues[1]} Baseline5Overshoot {source.otherBlues[0] - source.otherBlues[1]} FlexOK true BlueFuzz 1 """ characterMap = {} glyphs = {} metrics = {} layerSet = {g.name: g.layers[source.id] for g in font.glyphs} if isTTF: from fontTools.pens.cu2quPen import Cu2QuPen from fontTools.pens.recordingPen import RecordingPen for glyph in font.glyphs: layer = glyph.layers[source.id] pen = RecordingPen() layer.draw(pen) layer.paths = [] layer.components = [] pen.replay(Cu2QuPen(layer.getPen(), 1.0, reverse_direction=True)) for glyph in font.glyphs: if not glyph.export and not isTTF: continue name = glyph.name for code in glyph.unicodes: characterMap[int(code, 16)] = name layer = glyph.layers[source.id] width = 0 if name in marks else layer.width pen = BoundsPen(layerSet) layer.draw(pen) metrics[name] = (width, pen.bounds[0] if pen.bounds else 0) if isTTF: from fontTools.pens.ttGlyphPen import TTGlyphPen pen = TTGlyphPen(layerSet) if layer.paths: # Decompose and remove overlaps. path = Path() layer.draw(DecomposePathPen(path, layerSet)) path.simplify(fix_winding=True, keep_starting_points=True) path.draw(pen) else: # Composite-only glyph, no need to decompose. layer.draw(FlattenComponentsPen(pen, layerSet)) glyphs[name] = pen.glyph() else: from fontTools.pens.t2CharStringPen import T2CharStringPen # Draw glyph and remove overlaps. path = Path() layer.draw(DecomposePathPen(path, layerSet)) path.simplify(fix_winding=True, keep_starting_points=True) # Build CharString. pen = T2CharStringPen(width, None) path.draw(pen) glyphs[name] = pen.getCharString() vendor = font.customParameters["vendorID"] names = { "copyright": font.copyright, "familyName": instance.familyName, "styleName": instance.name, "uniqueFontIdentifier": f"{version};{vendor};{instance.fontName}", "fullName": instance.fullName, "version": f"Version {version}", "psName": instance.fontName, "manufacturer": font.manufacturer, "designer": font.designer, "description": font.customParameters["description"], "vendorURL": font.manufacturerURL, "designerURL": font.designerURL, "licenseDescription": font.customParameters["license"], "licenseInfoURL": font.customParameters["licenseURL"], "sampleText": font.customParameters["sampleText"], } date = int(font.date.timestamp()) - epoch_diff fb = FontBuilder(font.upm, isTTF=isTTF) fb.updateHead(fontRevision=version, created=date, modified=date) fb.setupGlyphOrder(font.glyphOrder) fb.setupCharacterMap(characterMap) fb.setupNameTable(names, mac=False) fb.setupHorizontalHeader( ascent=source.ascender, descent=source.descender, lineGap=source.customParameters["typoLineGap"], ) if isTTF: fb.setupGlyf(glyphs) else: privateDict = { "BlueValues": source.blueValues, "OtherBlues": source.otherBlues, "StemSnapH": source.horizontalStems, "StemSnapV": source.verticalStems, "StdHW": source.horizontalStems[0], "StdVW": source.verticalStems[0], } fontInfo = { "FullName": names["fullName"], "Notice": names["copyright"].replace("©", "\(c\)"), "version": f"{version}", "Weight": instance.name, } fb.setupCFF(names["psName"], fontInfo, glyphs, privateDict) fb.setupHorizontalMetrics(metrics) codePages = [CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"]] fb.setupOS2( version=4, sTypoAscender=source.ascender, sTypoDescender=source.descender, sTypoLineGap=source.customParameters["typoLineGap"], usWinAscent=source.ascender, usWinDescent=-source.descender, sxHeight=source.xHeight, sCapHeight=source.capHeight, achVendID=vendor, fsType=calcBits(font.customParameters["fsType"], 0, 16), fsSelection=calcFsSelection(instance), ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0, 32), ulCodePageRange1=calcBits(codePages, 0, 32), ) underlineThickness = int(source.customParameters["underlineThickness"]) underlinePosition = int(source.customParameters["underlinePosition"]) fb.setupPost( keepGlyphNames=False, underlineThickness=underlineThickness, underlinePosition=underlinePosition + underlineThickness // 2, ) fb.font["meta"] = meta = newTable("meta") meta.data = {"dlng": "Arab", "slng": "Arab"} fb.addOpenTypeFeatures(fea) if isTTF: from fontTools.ttLib.tables import ttProgram fb.setupDummyDSIG() fb.font["gasp"] = gasp = newTable("gasp") gasp.gaspRange = {0xFFFF: 15} fb.font["prep"] = prep = newTable("prep") prep.program = ttProgram.Program() assembly = ["PUSHW[]", "511", "SCANCTRL[]", "PUSHB[]", "4", "SCANTYPE[]"] prep.program.fromAssembly(assembly) else: from cffsubr import subroutinize subroutinize(fb.font) return fb.font