def generate_and_write_autohinted_instance( instantiator: instantiator.Instantiator, instance_descriptor: fontTools.designspaceLib.InstanceDescriptor, output_dir: Path, psautohint: str, ): # 3. Generate instance UFO. instance = instantiator.generate_instance(instance_descriptor) file_stem = f"{instance.info.familyName}-{instance.info.styleName}".replace(" ", "") # 3.5. Optionally write instance UFO to disk, for debugging. # instance.save(output_dir / f"{file_stem}.ufo", overwrite=True) # 4. Compile and write instance OTF to disk. Do not optimize, because we have to # do it again after autohinting. instance_font = ufo2ft.compileOTF( instance, removeOverlaps=True, overlapsBackend="pathops", inplace=True, useProductionNames=True, optimizeCFF=ufo2ft.CFFOptimization.NONE, ) output_path = output_dir / f"{file_stem}.otf" instance_font.save(output_path) # 5. Run psautohint on it. subprocess.run([psautohint, str(output_path)]) # 6. Subroutinize (compress) it instance_font = fontTools.ttLib.TTFont(output_path) cffsubr.subroutinize(instance_font).save(output_path)
def test_keep_glyph_names(self): font = load_test_font("SourceSansPro-Regular.subset.ttx") glyph_order = font.getGlyphOrder() assert font["post"].formatType == 3.0 assert font["post"].glyphOrder is None cffsubr.subroutinize(font, cff_version=2) assert font["post"].formatType == 2.0 assert font["post"].glyphOrder == glyph_order font2 = recompile_font(font) assert font2.getGlyphOrder() == glyph_order # now convert from CFF2 to CFF1 and check post format is set to 3.0 # https://github.com/adobe-type-tools/cffsubr/issues/8 cffsubr.subroutinize(font2, cff_version=1) assert font2["post"].formatType == 3.0 assert font2["post"].glyphOrder == None font3 = recompile_font(font2) assert font3.getGlyphOrder() == glyph_order
def test_output_different_cff_version(self, testfile, cff_version): font = load_test_font(testfile) table_tag = cffsubr._sniff_cff_table_format(font) cffsubr.subroutinize(font, cff_version=cff_version) assert cffsubr._sniff_cff_table_format(font) != table_tag
def test_inplace(self, testfile): font = load_test_font(testfile) font2 = cffsubr.subroutinize(font, inplace=False) assert font is not font2 font3 = cffsubr.subroutinize(font, inplace=True) assert font3 is font
def test_output_same_cff_version(self, testfile, table_tag): font = load_test_font(testfile) assert cffsubr._sniff_cff_table_format(font) == table_tag cffsubr.subroutinize(font) assert cffsubr._sniff_cff_table_format(font) == table_tag
def test_desubroutinize(testfile): font = load_test_font(testfile) cffsubr.subroutinize(font) font2 = cffsubr.desubroutinize(font, inplace=False) assert cffsubr.has_subroutines(font) assert not cffsubr.has_subroutines(font2)
def main(args=None): """Compress OpenType Font's CFF or CFF2 table by computing subroutines.""" parser = argparse.ArgumentParser("cffsubr", description=main.__doc__) parser.add_argument( "input_file", help="input font file. Must contain either CFF or CFF2 table" ) output_group = parser.add_mutually_exclusive_group() output_group.add_argument( "-o", "--output-file", default=None, help="optional path to output file. By default, dump binary data to stdout", ) output_group.add_argument( "-i", "--inplace", action="store_true", help="whether to overwrite the input file", ) parser.add_argument( "-f", "--cff-version", default=None, type=int, choices=(1, 2), help="output CFF table format version", ) parser.add_argument( "-N", "--no-glyph-names", dest="keep_glyph_names", action="store_false", help="whether to drop postscript glyph names when converting from CFF to CFF2.", ) parser.add_argument( "-d", "--desubroutinize", action="store_true", help="Don't subroutinize, instead remove all subroutines (in any).", ) options = parser.parse_args(args) if options.inplace: options.output_file = options.input_file elif not options.output_file: options.output_file = sys.stdout.buffer # Load TTFont lazily by default assuming output != input; load non-lazily if -i # option is passed, so that fontTools let us overwrite the input file. lazy = True if not options.inplace else None with ttLib.TTFont(options.input_file, lazy=lazy) as font: if options.desubroutinize: cffsubr.desubroutinize(font) else: cffsubr.subroutinize(font, options.cff_version, options.keep_glyph_names) font.save(options.output_file)
def generateFont(options, font): generateFeatures(font, options) from datetime import datetime info = font.info major, minor = options.version.split(".") info.versionMajor, info.versionMinor = int(major), int(minor) year = datetime.now().year info.copyright = f"Copyright 2010-{year} The Amiri Project Authors (https://github.com/alif-type/amiri)." info.openTypeNameLicense = "This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://scripts.sil.org/OFL" info.openTypeNameLicenseURL = "https://scripts.sil.org/OFL" if options.output.endswith(".ttf"): from fontTools.ttLib import newTable from fontTools.ttLib.tables import ttProgram otf = compileTTF(font, inplace=True, removeOverlaps=True, overlapsBackend="pathops", featureWriters=[]) otf["DSIG"] = DSIG = newTable("DSIG") DSIG.ulVersion = 1 DSIG.usFlag = 0 DSIG.usNumSigs = 0 DSIG.signatureRecords = [] otf["prep"] = prep = newTable("prep") prep.program = ttProgram.Program() prep.program.fromAssembly( ['PUSHW[]', '511', 'SCANCTRL[]', 'PUSHB[]', '4', 'SCANTYPE[]']) else: import cffsubr otf = compileOTF(font, inplace=True, optimizeCFF=0, removeOverlaps=True, overlapsBackend="pathops", featureWriters=[]) cffsubr.subroutinize(otf) if info.styleMapStyleName and "italic" in info.styleMapStyleName: otf['name'].names = [n for n in otf['name'].names if n.nameID != 17] for name in otf['name'].names: if name.nameID == 2: name.string = info.styleName glyf = otf.get("glyf") if glyf: from fontTools.ttLib.tables._g_l_y_f import UNSCALED_COMPONENT_OFFSET for name, glyph in glyf.glyphs.items(): glyph.expand(glyf) if glyph.isComposite(): for component in glyph.components: component.flags |= UNSCALED_COMPONENT_OFFSET return otf
def test_non_standard_upem_mute_font_matrix_warning(self, caplog): # See https://github.com/adobe-type-tools/cffsubr/issues/13 font = load_test_font("FontMatrixTest.ttx") assert font["CFF "].cff[0].FontMatrix == [0.0005, 0, 0, 0.0005, 0, 0] cffsubr.subroutinize(font, cff_version=2) with caplog.at_level(logging.WARNING, logger=cffLib.log.name): font2 = recompile_font(font) assert ( "Some CFF FDArray/FontDict keys were ignored upon compile: FontMatrix" not in caplog.text)
def _optimize(self, otf, name, fmt): if fmt == Format.OTF: import cffsubr from fontTools.cffLib.specializer import specializeProgram logger.info(f"Optimizing {name}.{fmt.value}") topDict = otf["CFF "].cff.topDictIndex[0] charStrings = topDict.CharStrings for charString in charStrings.values(): charString.decompile() charString.program = specializeProgram(charString.program) logger.info(f"Subroutinizing {name}.{fmt.value}") cffsubr.subroutinize(otf, keep_glyph_names=False) return otf
def test_drop_glyph_names(self): font = load_test_font("SourceSansPro-Regular.subset.ttx") glyph_order = font.getGlyphOrder() assert font["post"].formatType == 3.0 assert font["post"].glyphOrder is None cffsubr.subroutinize(font, cff_version=2, keep_glyph_names=False) assert font["post"].formatType == 3.0 assert font["post"].glyphOrder is None buf = io.BytesIO() font.save(buf) buf.seek(0) font2 = ttLib.TTFont(buf) assert font2.getGlyphOrder() != glyph_order
def _subroutinize_with_cffsubr(cls, otf, cffVersion): import cffsubr cffInputVersion = cls._get_cff_version(otf) assert cffInputVersion is not None, "Missing required 'CFF ' or 'CFF2' table" msg = f"Subroutinizing {cffInputVersion.name} table with cffsubr" if cffInputVersion != cffVersion: msg += f" (output format: {cffVersion.name})" logger.info(msg) return cffsubr.subroutinize(otf, cff_version=cffVersion, keep_glyph_names=False)
def buildVF(opts): font = GSFont(opts.glyphs) glyphOrder = buildAltGlyphs(font) prepare(font) for instance in font.instances: print(f" MASTER {instance.name}") build(instance, opts, glyphOrder) if instance.name == "Regular": regular = instance ds = DesignSpaceDocument() for i, axisDef in enumerate(font.axes): axis = ds.newAxisDescriptor() axis.tag = axisDef.axisTag axis.name = axisDef.name axis.maximum = max(x.axes[i] for x in font.instances) axis.minimum = min(x.axes[i] for x in font.instances) axis.default = regular.axes[i] ds.addAxis(axis) for instance in font.instances: source = ds.newSourceDescriptor() source.font = instance.font source.familyName = instance.familyName source.styleName = instance.name source.name = instance.fullName source.location = { a.name: instance.axes[i] for i, a in enumerate(ds.axes) } ds.addSource(source) print(f" MERGE {font.familyName}") otf, _, _ = merge(ds) subroutinize(otf) if not opts.debug: otf["post"].formatType = 3.0 return otf
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
def test_has_subroutines(testfile): font = load_test_font(testfile) assert not cffsubr.has_subroutines(font) assert cffsubr.has_subroutines(cffsubr.subroutinize(font))
designspace.instances = [ s for s in designspace.instances if s.lib.get("com.schriftgestaltung.export", True) ] # 2. Compile variable OTF from the masters. Do not optimize, because we have to do # it again after autohinting. varfont = ufo2ft.compileVariableCFF2( designspace, inplace=True, useProductionNames=True, optimizeCFF=ufo2ft.CFFOptimization.NONE, ) # 3. Generate STAT table. stylespace = statmake.classes.Stylespace.from_file(stylespace_path) statmake.lib.apply_stylespace_to_variable_font(stylespace, varfont, {}) # 4. Save. External tools after this point. varfont.save(output_path) # 5. Autohint subprocess.check_call( [os.fspath(args.psautohint_path), os.fspath(output_path)]) # 6. Subroutinize (compress) varfont = fontTools.ttLib.TTFont(output_path) cffsubr.subroutinize(varfont).save(output_path)