def test_main(tmpdir: Path): """Check that calling the main function on an input TTF works.""" glyphs = ".notdef space A Aacute B D".split() features = """ @A = [A Aacute]; @B = [B D]; feature kern { pos @A @B -50; } kern; """ fb = FontBuilder(1000) fb.setupGlyphOrder(glyphs) addOpenTypeFeaturesFromString(fb.font, features) input = tmpdir / "in.ttf" fb.save(str(input)) output = tmpdir / "out.ttf" run( [ "fonttools", "otlLib.optimize", "--gpos-compact-mode", "5", str(input), "-o", str(output), ], check=True, ) assert output.exists()
def test_optimization_mode( caplog, blocks: List[Tuple[int, int]], mode: Optional[int], expected_subtables: int, expected_bytes: int, ): """Check that the optimizations are off by default, and that increasing the optimization level creates more subtables and a smaller byte size. """ caplog.set_level(logging.DEBUG) glyphs, features = get_kerning_by_blocks(blocks) glyphs = [".notdef space"] + glyphs env = {} if mode is not None: # NOTE: activating this optimization via the environment variable is # experimental and may not be supported once an alternative mechanism # is in place. See: https://github.com/fonttools/fonttools/issues/2349 env["FONTTOOLS_GPOS_COMPACT_MODE"] = str(mode) with set_env(**env): fb = FontBuilder(1000) fb.setupGlyphOrder(glyphs) addOpenTypeFeaturesFromString(fb.font, features) assert expected_subtables == count_pairpos_subtables(fb.font) assert expected_bytes == count_pairpos_bytes(fb.font)
def buildTables(self): """ Compile OpenType feature tables from the source. Raises a FeaLibError if the feature compilation was unsuccessful. **This should not be called externally.** Subclasses may override this method to handle the table compilation in a different way if desired. """ if not self.features: return # the path is used by the lexer to follow 'include' statements; # if we generated some automatic features, includes have already been # resolved, and we work from a string which does't exist on disk path = self.ufo.path if not self.featureWriters else None try: addOpenTypeFeaturesFromString(self.ttFont, self.features, filename=path) except FeatureLibError: if path is None: # if compilation fails, create temporary file for inspection data = self.features.encode("utf-8") with NamedTemporaryFile(delete=False) as tmp: tmp.write(data) logger.error("Compilation failed! Inspect temporary file: %r", tmp.name) raise
def compile_font(self, path, suffix, temp_dir, features=None): ttx_filename = os.path.basename(path) savepath = os.path.join(temp_dir, ttx_filename.replace('.ttx', suffix)) font = TTFont(recalcBBoxes=False, recalcTimestamp=False) font.importXML(path) if features: addOpenTypeFeaturesFromString(font, features) font.save(savepath, reorderTables=None) return font, savepath
def compile_font(self, path, suffix, temp_dir, features=None): ttx_filename = os.path.basename(path) savepath = os.path.join(temp_dir, ttx_filename.replace('.ttx', suffix)) font = TTFont(recalcBBoxes=False, recalcTimestamp=False) font.importXML(path) if features: addOpenTypeFeaturesFromString(font, features) font.save(savepath, reorderTables=None) return font, savepath
def generateFont(options, font, feastring): if os.environ.get("SOURCE_DATE_EPOCH") is None: os.environ["SOURCE_DATE_EPOCH"] = "0" fea = generateFeatures(font, args.features) fea += feastring flags = [] if args.output.endswith(".ttf"): flags += ["opentype", "dummy-dsig", "omit-instructions"] font.selection.all() font.correctReferences() font.selection.none() # fix some common font issues validateGlyphs(font) updateInfo(font, args.version) font.generate(args.output, flags=flags) try: ttfont = TTFont(args.output) addOpenTypeFeaturesFromString(ttfont, fea) # Filter-out useless Macintosh names name = ttfont["name"] name.names = [n for n in name.names if n.platformID != 1] # https://github.com/fontforge/fontforge/pull/3235 head = ttfont["head"] # fontDirectionHint is deprecated and must be set to 2 head.fontDirectionHint = 2 # unset bits 6..10 head.flags &= ~0x7e0 # Drop useless table with timestamp if "FFTM" in ttfont: del ttfont["FFTM"] ttfont.save(args.output) except: with NamedTemporaryFile(delete=False) as tmp: tmp.write(fea.encode("utf-8")) print("Failed! Inspect temporary file: %r" % tmp.name) os.remove(args.output) raise
def setupFile_featureTables(self): """ Compile and return OpenType feature tables from the source. Raises a FeaLibError if the feature compilation was unsuccessful. **This should not be called externally.** Subclasses may override this method to handle the table compilation in a different way if desired. """ if self.mtiFeatures is not None: for tag, features in self.mtiFeatures.items(): table = mtiLib.build(features.splitlines(), self.outline) assert table.tableTag == tag self.outline[tag] = table elif self.features.strip(): # the path to features.fea is only used by the lexer to resolve # the relative "include" statements if self.font.path is not None: feapath = os.path.join(self.font.path, "features.fea") else: # in-memory UFO has no path, can't do 'include' either feapath = None # save generated features to a temp file if things go wrong... data = tobytes(self.features, encoding="utf-8") with NamedTemporaryFile(delete=False) as tmp: tmp.write(data) # if compilation succedes or fails for unrelated reasons, clean # up the temporary file try: addOpenTypeFeaturesFromString(self.outline, self.features, filename=feapath) except feaLib.error.FeatureLibError: logger.error("Compilation failed! Inspect temporary file: %r", tmp.name) raise except: os.remove(tmp.name) raise else: os.remove(tmp.name)
def _layoutEngineOTLTablesRepresentationFactory(layoutEngine): font = layoutEngine.font gdef = gsub = gpos = None if font.features.text: otf = TTFont() otf.setGlyphOrder(sorted(font.keys())) # compile with fontTools try: addOpenTypeFeaturesFromString(otf, font.features.text) except: import traceback print(traceback.format_exc(5)) if "GDEF" in otf: gdef = otf["GDEF"] if "GSUB" in otf: gsub = otf["GSUB"] if "GPOS" in otf: gpos = otf["GPOS"] return gdef, gsub, gpos
def test_max_ctx_calc_features(): glyphs = '.notdef space A B C a b c'.split() features = """ lookup GSUB_EXT useExtension { sub a by b; } GSUB_EXT; lookup GPOS_EXT useExtension { pos a b -10; } GPOS_EXT; feature sub1 { sub A by a; sub A B by b; sub A B C by c; sub [A B] C by c; sub [A B] C [A B] by c; sub A by A B; sub A' C by A B; sub a' by b; sub a' b by c; sub a from [A B C]; rsub a by b; rsub a' by b; rsub a b' by c; rsub a b' c by A; rsub [a b] c' by A; rsub [a b] c' [a b] by B; lookup GSUB_EXT; } sub1; feature pos1 { pos A 20; pos A B -50; pos A B' 10 C; lookup GPOS_EXT; } pos1; """ font = TTFont() font.setGlyphOrder(glyphs) addOpenTypeFeaturesFromString(font, features) assert maxCtxFont(font) == 3
def test_max_ctx_calc_features(): glyphs = '.notdef space A B C a b c'.split() features = """ lookup GSUB_EXT useExtension { sub a by b; } GSUB_EXT; lookup GPOS_EXT useExtension { pos a b -10; } GPOS_EXT; feature sub1 { sub A by a; sub A B by b; sub A B C by c; sub [A B] C by c; sub [A B] C [A B] by c; sub A by A B; sub A' C by A B; sub a' by b; sub a' b by c; sub a from [A B C]; rsub a by b; rsub a' by b; rsub a b' by c; rsub a b' c by A; rsub [a b] c' by A; rsub [a b] c' [a b] by B; lookup GSUB_EXT; } sub1; feature pos1 { pos A 20; pos A B -50; pos A B' 10 C; lookup GPOS_EXT; } pos1; """ font = TTFont() font.setGlyphOrder(glyphs) addOpenTypeFeaturesFromString(font, features) assert maxCtxFont(font) == 3
def _layoutEngineOTLTablesRepresentationFactory(layoutEngine): font = layoutEngine.font gdef = gsub = gpos = None if font.features.text: otf = TTFont() otf.setGlyphOrder(sorted(font.keys())) # compile with fontTools try: addOpenTypeFeaturesFromString(otf, font.features.text) except: import traceback print(traceback.format_exc(5)) if "GDEF" in otf: gdef = otf["GDEF"] if "GSUB" in otf: gsub = otf["GSUB"] if "GPOS" in otf: gpos = otf["GPOS"] return gdef, gsub, gpos
def add_rclt(font, savepath): features = """ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem latn NLD; feature rclt { script latn; language NLD; lookup A { sub uni0041 by uni0061; } A; language dflt; lookup B { sub uni0041 by uni0061; } B; } rclt; """ addOpenTypeFeaturesFromString(font, features) font.save(savepath)
def setupFile_featureTables(self): """ Compile and return OpenType feature tables from the source. Raises a FeaLibError if the feature compilation was unsuccessful. **This should not be called externally.** Subclasses may override this method to handle the table compilation in a different way if desired. """ if self.mtiFeaFiles is not None: for tag, feapath in self.mtiFeaFiles.items(): with open(feapath) as feafile: table = mtiLib.build(feafile, self.outline) assert table.tableTag == tag self.outline[tag] = table elif self.features.strip(): feapath = os.path.join(self.font.path, "features.fea") if self.font.path is not None else None addOpenTypeFeaturesFromString(self.outline, self.features, filename=feapath)
def setupFile_featureTables(self): """ Compile and return OpenType feature tables from the source. Raises a FeaLibError if the feature compilation was unsuccessful. **This should not be called externally.** Subclasses may override this method to handle the table compilation in a different way if desired. """ if self.mtiFeaFiles is not None: for tag, feapath in self.mtiFeaFiles.items(): with open(feapath) as feafile: table = mtiLib.build(feafile, self.outline) assert table.tableTag == tag self.outline[tag] = table elif self.features.strip(): feapath = os.path.join(self.font.path, "features.fea") addOpenTypeFeaturesFromString(self.outline, self.features, filename=feapath)
def test_optimization_mode( caplog, blocks: List[Tuple[int, int]], level: Optional[int], expected_subtables: int, expected_bytes: int, ): """Check that the optimizations are off by default, and that increasing the optimization level creates more subtables and a smaller byte size. """ caplog.set_level(logging.DEBUG) glyphs, features = get_kerning_by_blocks(blocks) glyphs = [".notdef space"] + glyphs fb = FontBuilder(1000) if level is not None: fb.font.cfg["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"] = level fb.setupGlyphOrder(glyphs) addOpenTypeFeaturesFromString(fb.font, features) assert expected_subtables == count_pairpos_subtables(fb.font) assert expected_bytes == count_pairpos_bytes(fb.font)
def build(self, featureFile, tables=None): font = makeTTFont() addOpenTypeFeaturesFromString(font, featureFile, tables=tables) return font
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString from fontTools.ttLib import TTFont import sys font = TTFont(sys.argv[1]) destination = sys.argv[2] print(f'Writing on {destination}') while True: length = int(sys.stdin.buffer.readline().decode("utf-8")) print(f'Reading {length} bytes') feature = sys.stdin.buffer.read(length).decode("utf-8") print(f'Read |{feature}|') try: addOpenTypeFeaturesFromString(font, feature) font.save(destination) print("OK") except Exception as e: print(f'{e}')
def make_font(feature_source, fea_type='fea'): """Return font with GSUB compiled from given source. Adds a bunch of filler tables so the font can be saved if needed, for debugging purposes. """ # copied from fontTools' feaLib/builder_test. glyphs = """ .notdef space slash fraction semicolon period comma ampersand quotedblleft quotedblright quoteleft quoteright zero one two three four five six seven eight nine zero.oldstyle one.oldstyle two.oldstyle three.oldstyle four.oldstyle five.oldstyle six.oldstyle seven.oldstyle eight.oldstyle nine.oldstyle onequarter onehalf threequarters onesuperior twosuperior threesuperior ordfeminine ordmasculine A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc A.alt1 A.alt2 A.alt3 B.alt1 B.alt2 B.alt3 C.alt1 C.alt2 C.alt3 a.alt1 a.alt2 a.alt3 a.end b.alt c.mid d.alt d.mid e.begin e.mid e.end m.begin n.end s.end z.end Eng Eng.alt1 Eng.alt2 Eng.alt3 A.swash B.swash C.swash D.swash E.swash F.swash G.swash H.swash I.swash J.swash K.swash L.swash M.swash N.swash O.swash P.swash Q.swash R.swash S.swash T.swash U.swash V.swash W.swash X.swash Y.swash Z.swash f_l c_h c_k c_s c_t f_f f_f_i f_f_l f_i o_f_f_i s_t f_i.begin a_n_d T_h T_h.swash germandbls ydieresis yacute breve grave acute dieresis macron circumflex cedilla umlaut ogonek caron damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial by feature lookup sub table """.split() font = TTFont() font.setGlyphOrder(glyphs) glyph_order = font.getGlyphOrder() font['cmap'] = cmap = newTable('cmap') table = cmap_format_4(4) table.platformID = 3 table.platEncID = 1 table.language = 0 table.cmap = {AGL2UV[n]: n for n in glyph_order if n in AGL2UV} cmap.tableVersion = 0 cmap.tables = [table] font['glyf'] = glyf = newTable('glyf') glyf.glyphs = {} glyf.glyphOrder = glyph_order for name in glyph_order: pen = TTGlyphPen(None) glyf[name] = pen.glyph() font['head'] = head = newTable('head') head.tableVersion = 1.0 head.fontRevision = 1.0 head.flags = head.checkSumAdjustment = head.magicNumber =\ head.created = head.modified = head.macStyle = head.lowestRecPPEM =\ head.fontDirectionHint = head.indexToLocFormat =\ head.glyphDataFormat =\ head.xMin = head.xMax = head.yMin = head.yMax = 0 head.unitsPerEm = 1000 font['hhea'] = hhea = newTable('hhea') hhea.tableVersion = 0x00010000 hhea.ascent = hhea.descent = hhea.lineGap =\ hhea.caretSlopeRise = hhea.caretSlopeRun = hhea.caretOffset =\ hhea.reserved0 = hhea.reserved1 = hhea.reserved2 = hhea.reserved3 =\ hhea.metricDataFormat = hhea.advanceWidthMax = hhea.xMaxExtent =\ hhea.minLeftSideBearing = hhea.minRightSideBearing =\ hhea.numberOfHMetrics = 0 font['hmtx'] = hmtx = newTable('hmtx') hmtx.metrics = {} for name in glyph_order: hmtx[name] = (600, 50) font['loca'] = newTable('loca') font['maxp'] = maxp = newTable('maxp') maxp.tableVersion = 0x00005000 maxp.numGlyphs = 0 font['post'] = post = newTable('post') post.formatType = 2.0 post.extraNames = [] post.mapping = {} post.glyphOrder = glyph_order post.italicAngle = post.underlinePosition = post.underlineThickness =\ post.isFixedPitch = post.minMemType42 = post.maxMemType42 =\ post.minMemType1 = post.maxMemType1 = 0 if fea_type == 'fea': addOpenTypeFeaturesFromString(font, feature_source) elif fea_type == 'mti': font['GSUB'] = mtiLib.build(UnicodeIO(feature_source), font) return font
def make_font(feature_source, fea_type="fea"): """Return font with GSUB compiled from given source. Adds a bunch of filler tables so the font can be saved if needed, for debugging purposes. """ # copied from fontTools' feaLib/builder_test. glyphs = """ .notdef space slash fraction semicolon period comma ampersand quotedblleft quotedblright quoteleft quoteright zero one two three four five six seven eight nine zero.oldstyle one.oldstyle two.oldstyle three.oldstyle four.oldstyle five.oldstyle six.oldstyle seven.oldstyle eight.oldstyle nine.oldstyle onequarter onehalf threequarters onesuperior twosuperior threesuperior ordfeminine ordmasculine A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc A.alt1 A.alt2 A.alt3 B.alt1 B.alt2 B.alt3 C.alt1 C.alt2 C.alt3 a.alt1 a.alt2 a.alt3 a.end b.alt c.mid d.alt d.mid e.begin e.mid e.end m.begin n.end s.end z.end Eng Eng.alt1 Eng.alt2 Eng.alt3 A.swash B.swash C.swash D.swash E.swash F.swash G.swash H.swash I.swash J.swash K.swash L.swash M.swash N.swash O.swash P.swash Q.swash R.swash S.swash T.swash U.swash V.swash W.swash X.swash Y.swash Z.swash f_l c_h c_k c_s c_t f_f f_f_i f_f_l f_i o_f_f_i s_t f_i.begin a_n_d T_h T_h.swash germandbls ydieresis yacute breve grave acute dieresis macron circumflex cedilla umlaut ogonek caron damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial by feature lookup sub table """.split() font = TTFont() font.setGlyphOrder(glyphs) glyph_order = font.getGlyphOrder() font["cmap"] = cmap = newTable("cmap") table = cmap_format_4(4) table.platformID = 3 table.platEncID = 1 table.language = 0 table.cmap = {AGL2UV[n]: n for n in glyph_order if n in AGL2UV} cmap.tableVersion = 0 cmap.tables = [table] font["glyf"] = glyf = newTable("glyf") glyf.glyphs = {} glyf.glyphOrder = glyph_order for name in glyph_order: pen = TTGlyphPen(None) glyf[name] = pen.glyph() font["head"] = head = newTable("head") head.tableVersion = 1.0 head.fontRevision = 1.0 head.flags = ( head.checkSumAdjustment ) = ( head.magicNumber ) = ( head.created ) = ( head.modified ) = ( head.macStyle ) = ( head.lowestRecPPEM ) = ( head.fontDirectionHint ) = ( head.indexToLocFormat ) = head.glyphDataFormat = head.xMin = head.xMax = head.yMin = head.yMax = 0 head.unitsPerEm = 1000 font["hhea"] = hhea = newTable("hhea") hhea.tableVersion = 0x00010000 hhea.ascent = ( hhea.descent ) = ( hhea.lineGap ) = ( hhea.caretSlopeRise ) = ( hhea.caretSlopeRun ) = ( hhea.caretOffset ) = ( hhea.reserved0 ) = ( hhea.reserved1 ) = ( hhea.reserved2 ) = ( hhea.reserved3 ) = ( hhea.metricDataFormat ) = ( hhea.advanceWidthMax ) = ( hhea.xMaxExtent ) = hhea.minLeftSideBearing = hhea.minRightSideBearing = hhea.numberOfHMetrics = 0 font["hmtx"] = hmtx = newTable("hmtx") hmtx.metrics = {} for name in glyph_order: hmtx[name] = (600, 50) font["loca"] = newTable("loca") font["maxp"] = maxp = newTable("maxp") maxp.tableVersion = 0x00005000 maxp.numGlyphs = 0 font["post"] = post = newTable("post") post.formatType = 2.0 post.extraNames = [] post.mapping = {} post.glyphOrder = glyph_order post.italicAngle = ( post.underlinePosition ) = ( post.underlineThickness ) = ( post.isFixedPitch ) = post.minMemType42 = post.maxMemType42 = post.minMemType1 = post.maxMemType1 = 0 if fea_type == "fea": addOpenTypeFeaturesFromString(font, feature_source) elif fea_type == "mti": font["GSUB"] = mtiLib.build(UnicodeIO(feature_source), font) return font
print(' Renaming features glyphs') with open(Path(INDEX_DIR / f'{config["inputFile"]}.json'), 'r') as file: glyph_id_map = json.load(file) glyph_id_map_keys = sorted(glyph_id_map.keys(), reverse=True) for glyph_name in glyph_id_map_keys: regex = rf'(^|\s|\{{|\[|\\)({glyph_name})(\s|\}}|\]|\'|\;|$)' if re.search(regex, final_string) is not None: glyph_id = glyph_id_map[glyph_name] new_glyph_name = input_font.getGlyphName(glyph_id) # Run twice for instances that are sandwiched between two other instances for i in range(2): final_string = re.sub(regex, f'\g<1>{new_glyph_name}\g<3>', final_string) with open(Path(TEMP_DIR / 'features.fea'), 'w') as features_file: features_file.write(final_string) # Set up substitution functionality print(' Adding features set to output font') addOpenTypeFeaturesFromString(output_font, final_string) # Save font print(' Saving output font to TTF file') output_font.save(output_file_path) output_font.close() print(f'Completed font "{output_file_path}"') else: print(f'Skipping missing input file "{input_file_path}"') # Perform cleanup tasks print('Cleaning up directories') rmtree(TEMP_DIR) print('All build tasks are complete')
def test_max_ctx_calc_no_features(): font = TTFont() assert maxCtxFont(font) == 0 font.setGlyphOrder(['.notdef']) addOpenTypeFeaturesFromString(font, '') assert maxCtxFont(font) == 0
def test_ensureDecompiled(lazy): # test that no matter the lazy value, ensureDecompiled decompiles all tables font = TTFont() font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx")) # test font has no OTL so we add some, as an example of otData-driven tables addOpenTypeFeaturesFromString( font, """ feature calt { sub period' period' period' space by ellipsis; } calt; feature dist { pos period period -30; } dist; """ ) # also add an additional cmap subtable that will be lazily-loaded cm = CmapSubtable.newSubtable(14) cm.platformID = 0 cm.platEncID = 5 cm.language = 0 cm.cmap = {} cm.uvsDict = {0xFE00: [(0x002e, None)]} font["cmap"].tables.append(cm) # save and reload, potentially lazily buf = io.BytesIO() font.save(buf) buf.seek(0) font = TTFont(buf, lazy=lazy) # check no table is loaded until/unless requested, no matter the laziness for tag in font.keys(): assert not font.isLoaded(tag) if lazy is not False: # additional cmap doesn't get decompiled automatically unless lazy=False; # can't use hasattr or else cmap's maginc __getattr__ kicks in... cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14) assert cm.data is not None assert "uvsDict" not in cm.__dict__ # glyf glyphs are not expanded unless lazy=False assert font["glyf"].glyphs["period"].data is not None assert not hasattr(font["glyf"].glyphs["period"], "coordinates") if lazy is True: # OTL tables hold a 'reader' to lazily load when lazy=True assert "reader" in font["GSUB"].table.LookupList.__dict__ assert "reader" in font["GPOS"].table.LookupList.__dict__ font.ensureDecompiled() # all tables are decompiled now for tag in font.keys(): assert font.isLoaded(tag) # including the additional cmap cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14) assert cm.data is None assert "uvsDict" in cm.__dict__ # expanded glyf glyphs lost the 'data' attribute assert not hasattr(font["glyf"].glyphs["period"], "data") assert hasattr(font["glyf"].glyphs["period"], "coordinates") # and OTL tables have read their 'reader' assert "reader" not in font["GSUB"].table.LookupList.__dict__ assert "Lookup" in font["GSUB"].table.LookupList.__dict__ assert "reader" not in font["GPOS"].table.LookupList.__dict__ assert "Lookup" in font["GPOS"].table.LookupList.__dict__
def test_max_ctx_calc_no_features(): font = TTFont() assert maxCtxFont(font) == 0 font.setGlyphOrder(['.notdef']) addOpenTypeFeaturesFromString(font, '') assert maxCtxFont(font) == 0
def build(self, featureFile, tables=None): font = makeTTFont() addOpenTypeFeaturesFromString(font, featureFile, tables=tables) return font