def flattenGlyphs(input, fontNumber, output): font = TTFont(input, fontNumber=fontNumber) font.recalcBBoxes = False if "glyf" in font: for glyphName in font.getGlyphOrder(): glyph = font["glyf"][glyphName] coordinates, endPtsOfContours, flags = glyph.getCoordinates( font["glyf"]) glyph.numberOfContours = len(endPtsOfContours) glyph.coordinates = coordinates glyph.endPtsOfContours = endPtsOfContours glyph.flags = flags glyph.program = ttProgram.Program() font["glyf"][glyphName] = glyph elif "CFF " in font: cff = font["CFF "] fontName = cff.cff.fontNames[0] topDict = cff.cff[fontName] for glyphID in range(len(font.getGlyphOrder())): charString = topDict.CharStrings.charStringsIndex[glyphID] charString.decompile() localSubrs = getattr(charString.private, "Subrs", []) globalSubrs = charString.globalSubrs inlinedProgram = inlineProgram(localSubrs, globalSubrs, charString.program) charString.program = inlinedProgram if "Private" in topDict.rawDict and "Subrs" in topDict.Private.rawDict: topDict.Private.Subrs del topDict.Private.rawDict["Subrs"] del topDict.Private.Subrs topDict.GlobalSubrs.items = [] else: raise FlattenError("Could not flatten glyphs.") font.save(output)
def reorderFont(input, fontNumber, desiredGlyphOrder, output): font = TTFont(input, fontNumber=fontNumber, lazy=False) font = fullyLoadFont(font) if "CFF " in font: cff = font["CFF "] fontName = cff.cff.fontNames[0] topDict = cff.cff[fontName] topDict.compilerClass = ReorderedTopDictCompiler glyphOrder = font.getGlyphOrder() reorderedGlyphs = reorderGlyphs(glyphOrder, desiredGlyphOrder) if reorderedGlyphs is None: return False font.setGlyphOrder(reorderedGlyphs) # Glyph order is cached in a few places, clear those out. if "glyf" in font: del font["glyf"].glyphOrder if hasattr(font, "_reverseGlyphOrderDict"): del font._reverseGlyphOrderDict # CFF stores glyph order internally, set it: if "CFF " in font: cff = font["CFF "] fontName = cff.cff.fontNames[0] topDict = cff.cff[fontName] topDict.charset = reorderedGlyphs fixLayoutCoverage(font) tmp = BytesIO() font.save(tmp) tableOrder = font.reader.keys() success = computeTableOrder(tableOrder) if not success: tmp.close() return False outputStream = open(output, "wb") reorderFontTables(tmp, outputStream, tableOrder) tmp.close() outputStream.close() return True
class VTT_updater: def __init__(self, path_old: Union[Path, str], path_new: Union[Path, str], log: bool = True) -> None: self.font_old = TTFont(str(path_old)) self.font_old.cmap = { v: k for k, v in self.path_old.getBestCmap().items() } self.font_old.go = self.path_old.getGlyphOrder() self.font_old.path = path_old self.font_new = TTFont(str(path_new)) self.font_new.cmap = self.path_new.getBestCmap() self.font_new.go = self.path_new.getGlyphOrder() self.font_new.path = path_new self.log = True self.name_map = { k: self.font_new.cmap.get(v) for k, v in self.font_old.cmap.items() } self.name_map.update({ i: i for i in filter(lambda x: x in self.font_new.go, self.font_old.go) }) self.incompatible_glyphs = self.get_incompatible_glyphs() def get_glyph_map(self) -> dict: glyph_map = {} for i, g_new in enumerate(self.font_new.go): if g_new in self.font_old.go: glyph_map[self.font_old.index(g_new)] = i return glyph_map def get_incompatible_glyphs(self) -> list: incompatible_glyphs = list( filter(lambda x: not self.name_map[x] in self.font_new.go, self.font_old.go)) for g_name in self.font_old.go: if g_name not in incompatible_glyphs: g_old = self.font_old["glyf"][g_name] g_new = self.font_new["glyf"][self.name_map[g_name]] pen_old = RecordingPen() g_old.draw(pen_old, self.font_old["glyf"]) pen_new = RecordingPen() g_new.draw(pen_new, self.font_new["glyf"]) for (pt_old, *_), (pt_new, *_) in zip_longest(pen_old.value, pen_new.value, fillvalue=[None, None]): if pt_old != pt_new: if self.log: print( f"{g_name}/{self.name_map[g_name]} has an incompatible contour" ) break else: if hasattr(g_old, "components") and hasattr( g_new, "components"): for comp_old, comp_new in zip_longest( g_old.components, g_new.components): if comp_old.glyphName != comp_new.glyphName: if self.log: print( f"{g_name}/{self.name_map[g_name]} has incompatible components" ) break continue incompatible_glyphs.append(g_name) return incompatible_glyphs def update_assembly(self) -> None: for g_name in self.font_old.go: if g_name not in self.incompatible_glyphs: g_old = self.font_old["glyf"][g_name] if hasattr(g_old, "program"): g_new = self.font_new["glyf"][self.name_map[g_name]] g_new.program = g_old.program return None def update_glyph_programs(self, font) -> None: pattern = r"(.*OFFSET\[[r,R]\]\ ?),.*" for key in font["TSI1"].glyphPrograms: glyph_program = font["TSI1"].glyphPrograms[key].replace("\r", "\n") matches = list(re.finditer(pattern, glyph_program)) if matches: components = self.font_new["glyf"][ self.name_map[key]].components for match, component in zip(matches[::-1], components[::-1]): left, right = match.span(0) command = match.group(1) g_name, (*transformations, x, y) = component.getComponentInfo() assembly = [x, y] if transformations != [1, 0, 0, 1]: assembly.extend(transformations) assembly = list(map(str, assembly)) gid = self.font_new.go.index(g_name) new_command = f"{command}, {gid}, {', '.join(assembly)}" glyph_program = (glyph_program[:left] + new_command + glyph_program[right:]) font["TSI1"].glyphPrograms[key] = glyph_program.replace( "\n", "\r") return None def _filter_glyphs(self, dict_data) -> dict: new_dict_data = {} keys = [i for i in dict_data.keys()] for key in keys: if key not in self.incompatible_glyphs: new_dict_data[self.name_map[key]] = dict_data[key] return new_dict_data def update_TSI_tables(self) -> None: self.font_new["TSI0"] = self.font_old["TSI0"] # empty... self.font_new["TSI1"] = self.font_old["TSI1"] # assembly self.font_new["TSI2"] = self.font_old["TSI2"] # empty... self.font_new["TSI3"] = self.font_old["TSI3"] # VTT talks self.font_new["TSI5"] = self.font_old["TSI5"] # glyph groups self.font_new["cvt "] = self.font_old["cvt "] # cvts self.font_new["prep"] = self.font_old["prep"] # prep self.font_new["fpgm"] = self.font_old["fpgm"] # fpgm self.font_new["TSI1"].glyphPrograms = self._filter_glyphs( self.font_new["TSI1"].glyphPrograms) self.font_new["TSI3"].glyphPrograms = self._filter_glyphs( self.font_new["TSI3"].glyphPrograms) return None def update_table_entries(self) -> None: update = dict( maxp=[ "maxSizeOfInstructions", "maxFunctionDefs", "maxStorage", "maxStackElements", "maxZones", "maxTwilightPoints", ], head=["checkSumAdjustment"], ) for table, attributes in update.items(): for attribute in attributes: setattr( self.font_new[table], attribute, getattr(self.font_old[table], attribute), ) self.font_new["head"].flags |= 1 << 3 return None def update(self) -> None: self.update_assembly() self.update_glyph_programs(self.font_old) self.update_TSI_tables() self.update_table_entries() return None def write(self, save_as: Union[Path, str, bool] = None) -> None: if save_as: self.font_new.save(str(save_as)) else: self.font_new.save(str(self.font_new.path)) return None
class InstanceWorker(QRunnable): def __init__( self, outpath=None, font_model=None, axis_model=None, name_model=None, bit_model=None, ): super().__init__() self.signals = InstanceWorkerSignals() self.outpath = outpath self.font_model = font_model self.axis_model = axis_model self.name_model = name_model self.bit_model = bit_model self.ttfont = None @pyqtSlot() def run(self): try: # Debugging in stdout print(f"\n\n{datetime.datetime.now()}") self.ttfont = TTFont(self.font_model.fontpath) # instantiate self.instantiate_variable_font() # edit name table records self.edit_name_table() # edit bit flags self.edit_bit_flags() # write to disk self.ttfont.save(self.outpath) except Exception as e: self.signals.error.emit(f"{e}") sys.stderr.write(f"{traceback.format_exc()}\n") else: # returns the file out file path on success self.signals.result.emit(self.outpath) self.signals.finished.emit() def instantiate_variable_font(self): axis_instance_data = self.axis_model.get_instance_data() instantiateVariableFont(self.ttfont, axis_instance_data, inplace=True) print("\nAXIS INSTANCE VALUES") print( f"Instantiated variable font with axis definitions:\n{axis_instance_data}" ) def edit_name_table(self): # string, nameID, platformID, platEncID, langID name_record_plat_enc_lang = (3, 1, 1033) name_instance_data = self.name_model.get_instance_data() name_table = self.ttfont["name"] # set 3, 1, 1033 name records (only!) # mandatory writes name_table.setName(name_instance_data["nameID1"], 1, *name_record_plat_enc_lang) name_table.setName(name_instance_data["nameID2"], 2, *name_record_plat_enc_lang) name_table.setName(name_instance_data["nameID3"], 3, *name_record_plat_enc_lang) name_table.setName(name_instance_data["nameID4"], 4, *name_record_plat_enc_lang) name_table.setName(name_instance_data["nameID6"], 6, *name_record_plat_enc_lang) # optional writes # Approach: # (1) if user text data exists, write it # (2) if user text data does not exist but record does, delete it # (3) otherwise do nothing if name_instance_data["nameID16"] != "": name_table.setName( name_instance_data["nameID16"], 16, *name_record_plat_enc_lang ) elif name_table.getName(16, *name_record_plat_enc_lang): name_table.removeNames(16, *name_record_plat_enc_lang) if name_instance_data["nameID17"] != "": name_table.setName( name_instance_data["nameID17"], 17, *name_record_plat_enc_lang ) elif name_table.getName(17, *name_record_plat_enc_lang): name_table.removeNames(17, *name_record_plat_enc_lang) if name_instance_data["nameID21"] != "": name_table.setName( name_instance_data["nameID21"], 21, *name_record_plat_enc_lang ) elif name_table.getName(21, *name_record_plat_enc_lang): name_table.removeNames(21, *name_record_plat_enc_lang) if name_instance_data["nameID22"] != "": name_table.setName( name_instance_data["nameID22"], 22, *name_record_plat_enc_lang ) elif name_table.getName(22, *name_record_plat_enc_lang): name_table.removeNames(22, *name_record_plat_enc_lang) # update name table data self.ttfont["name"] = name_table # print name table report print("\nNAME TABLE EDITS") print("Name records at write time:\n") print(f"nameID1: {self.ttfont['name'].getName(1, *name_record_plat_enc_lang)}") print(f"nameID2: {self.ttfont['name'].getName(2, *name_record_plat_enc_lang)}") print(f"nameID3: {self.ttfont['name'].getName(3, *name_record_plat_enc_lang)}") print(f"nameID4: {self.ttfont['name'].getName(4, *name_record_plat_enc_lang)}") print(f"nameID6: {self.ttfont['name'].getName(6, *name_record_plat_enc_lang)}") print( f"nameID16: {self.ttfont['name'].getName(16, *name_record_plat_enc_lang)}" ) print( f"nameID17: {self.ttfont['name'].getName(17, *name_record_plat_enc_lang)}" ) print( f"nameID21: {self.ttfont['name'].getName(21, *name_record_plat_enc_lang)}" ) print( f"nameID22: {self.ttfont['name'].getName(22, *name_record_plat_enc_lang)}" ) def edit_bit_flags(self): # edit the OS/2.fsSelection bit flag pre_os2_fsselection_int = self.ttfont["OS/2"].fsSelection edited_os2_fsselection_int = self.bit_model.edit_os2_fsselection_bits( pre_os2_fsselection_int ) # edit OS/2.fsSelection in the TTFont attribute self.ttfont["OS/2"].fsSelection = edited_os2_fsselection_int # edit head.macstyle bit flag pre_head_macstyle_int = self.ttfont["head"].macStyle edited_head_macstyle_int = self.bit_model.edit_head_macstyle_bits( pre_head_macstyle_int ) self.ttfont["head"].macStyle = edited_head_macstyle_int # bit flag debugging stdout report print("\nBIT FLAGS") print( f"\nOS/2.fsSelection updated with the following data:\n" f"{self.bit_model.get_os2_instance_data()}" ) print(f"Pre OS/2.fsSelection: {num2binary(pre_os2_fsselection_int, bits=16)}") print( f"Post OS/2.fsSelection: {num2binary(self.ttfont['OS/2'].fsSelection, bits=16)}" ) print( f"\nhead.macStyle bit flag updated with the following data:\n" f"{self.bit_model.get_head_instance_data()}" ) print(f"Pre head.macStyle: {num2binary(pre_head_macstyle_int, bits=16)}") print( f"Post head.macStyle: {num2binary(self.ttfont['head'].macStyle, bits=16)}" )