def test_check_unique_glyphnames(): """ Font contains unique glyph names? """ import io from fontbakery.profiles.universal import com_google_fonts_check_unique_glyphnames as check test_font_path = TEST_FILE("nunito/Nunito-Regular.ttf") test_font = TTFont(test_font_path) status, _ = list(check(test_font))[-1] assert status == PASS # Fonttools renames duplicate glyphs with #1, #2, ... on load. # Code snippet from https://github.com/fonttools/fonttools/issues/149. glyph_names = test_font.getGlyphOrder() glyph_names[2] = glyph_names[3] # Load again, we changed the font directly. test_font = TTFont(test_font_path) test_font.setGlyphOrder(glyph_names) test_font['post'] # Just access the data to make fonttools generate it. test_file = io.BytesIO() test_font.save(test_file) test_font = TTFont(test_file) status, message = list(check(test_font))[-1] assert status == FAIL assert "space" in message # Upgrade to post format 3.0 and roundtrip data to update TTF object. test_font = TTFont(test_font_path) test_font.setGlyphOrder(glyph_names) test_font["post"].formatType = 3.0 test_file = io.BytesIO() test_font.save(test_file) test_font = TTFont(test_file) status, message = list(check(test_font))[-1] assert status == SKIP
def _layoutEngineOTLTablesRepresentationFactory(layoutEngine): font = layoutEngine.font ret = dict() glyphOrder = sorted(font.keys()) if font.features.text: otf = TTFont() otf.setGlyphOrder(glyphOrder) # compile with feaLib + markWriter/kernWriter try: compiler = FeatureCompiler(font, otf) compiler.postProcess = lambda: None compiler.compile() for name in ("GDEF", "GSUB", "GPOS"): if name in otf: table = otf[name].compile(otf) value = hb.Blob.create_for_array(table, HB.MEMORY_MODE_READONLY) ret[name] = value except Exception: # TODO: handle this in the UI import traceback print(traceback.format_exc(5)) # discard tables from incompletely parsed feature text ret = dict() return ret, glyphOrder
def test_check_unique_glyphnames(): """ Font contains unique glyph names? """ check = CheckTester(universal_profile, "com.google.fonts/check/unique_glyphnames") ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf")) assert_PASS(check(ttFont)) # Fonttools renames duplicate glyphs with #1, #2, ... on load. # Code snippet from https://github.com/fonttools/fonttools/issues/149. glyph_names = ttFont.getGlyphOrder() glyph_names[2] = glyph_names[3] # Load again, we changed the font directly. ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf")) ttFont.setGlyphOrder(glyph_names) ttFont['post'] # Just access the data to make fonttools generate it. _file = io.BytesIO() _file.name = ttFont.reader.file.name ttFont.save(_file) ttFont = TTFont(_file) message = assert_results_contain(check(ttFont), FAIL, "duplicated-glyph-names") assert "space" in message # Upgrade to post format 3.0 and roundtrip data to update TTF object. ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf")) ttFont.setGlyphOrder(glyph_names) ttFont["post"].formatType = 3.0 _file = io.BytesIO() _file.name = ttFont.reader.file.name ttFont.save(_file) ttFont = TTFont(_file) assert_SKIP(check(ttFont))
def makeTTFont(): 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 uni0327 uni0328 e.fina """.split() glyphs.extend("cid{:05d}".format(cid) for cid in range(800, 1001 + 1)) font = TTFont() font.setGlyphOrder(glyphs) return font
def __init__(self, ufo, ttFont=None, glyphSet=None, **kwargs): """ Args: ufo: an object representing a UFO (defcon.Font or equivalent) containing the features source data. ttFont: a fontTools TTFont object where the generated OpenType tables are added. If None, an empty TTFont is used, with the same glyph order as the ufo object. glyphSet: a (optional) dict containing pre-processed copies of the UFO glyphs. """ self.ufo = ufo if ttFont is None: from fontTools.ttLib import TTFont from ufo2ft.util import makeOfficialGlyphOrder ttFont = TTFont() ttFont.setGlyphOrder(makeOfficialGlyphOrder(ufo)) self.ttFont = ttFont glyphOrder = ttFont.getGlyphOrder() if glyphSet is not None: assert set(glyphOrder) == set(glyphSet.keys()) else: glyphSet = ufo self.glyphSet = OrderedDict((gn, glyphSet[gn]) for gn in glyphOrder)
def makeTTFont(): 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 """.split() font = TTFont() font.setGlyphOrder(glyphs) return font
def _layoutEngineOTLTablesRepresentationFactory(layoutEngine): import os import tempfile font = layoutEngine.font gdef = gsub = gpos = None if font.features.text: otf = TTFont() otf.setGlyphOrder(sorted(font.keys())) # XXX hack around fontTools only reading from disk fd, feaPath = tempfile.mkstemp() f = open(feaPath, "w") f.write(font.features.text) f.close() # compile with fontTools try: addOpenTypeFeatures(feaPath, otf) except: import traceback print(traceback.format_exc(5)) finally: os.close(fd) os.remove(feaPath) 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 check_mti_file(self, name, tableTag=None): xml_expected_path = self.getpath("%s.ttx" % name + ('.'+tableTag if tableTag is not None else '')) with open(xml_expected_path, 'rt', encoding="utf-8") as xml_expected_file: xml_expected = xml_expected_file.read() font = self.create_font() with open(self.getpath("%s.txt" % name), 'rt', encoding="utf-8") as f: table = mtiLib.build(f, font, tableTag=tableTag) if tableTag is not None: self.assertEqual(tableTag, table.tableTag) tableTag = table.tableTag # Make sure it compiles. blob = table.compile(font) # Make sure it decompiles. decompiled = table.__class__() decompiled.decompile(blob, font) # XML from built object. writer = XMLWriter(StringIO()) writer.begintag(tableTag); writer.newline() table.toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_built = writer.file.getvalue() # XML from decompiled object. writer = XMLWriter(StringIO()) writer.begintag(tableTag); writer.newline() decompiled.toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_binary = writer.file.getvalue() self.expect_ttx(xml_binary, xml_built, fromfile='decompiled', tofile='built') self.expect_ttx(xml_expected, xml_built, fromfile=xml_expected_path, tofile='built') from fontTools.misc import xmlReader f = StringIO() f.write(xml_expected) f.seek(0) font2 = TTFont() font2.setGlyphOrder(font.getGlyphOrder()) reader = xmlReader.XMLReader(f, font2) reader.read(rootless=True) # XML from object read from XML. writer = XMLWriter(StringIO()) writer.begintag(tableTag); writer.newline() font2[tableTag].toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_fromxml = writer.file.getvalue() self.expect_ttx(xml_expected, xml_fromxml, fromfile=xml_expected_path, tofile='fromxml')
def check_mti_file(self, name, tableTag=None): xml_expected_path = self.getpath("%s.ttx" % name + ('.'+tableTag if tableTag is not None else '')) with open(xml_expected_path, 'rt', encoding="utf-8") as xml_expected_file: xml_expected = xml_expected_file.read() font = self.create_font() with open(self.getpath("%s.txt" % name), 'rt', encoding="utf-8") as f: table = mtiLib.build(f, font, tableTag=tableTag) if tableTag is not None: self.assertEqual(tableTag, table.tableTag) tableTag = table.tableTag # Make sure it compiles. blob = table.compile(font) # Make sure it decompiles. decompiled = table.__class__() decompiled.decompile(blob, font) # XML from built object. writer = XMLWriter(StringIO(), newlinestr='\n') writer.begintag(tableTag); writer.newline() table.toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_built = writer.file.getvalue() # XML from decompiled object. writer = XMLWriter(StringIO(), newlinestr='\n') writer.begintag(tableTag); writer.newline() decompiled.toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_binary = writer.file.getvalue() self.expect_ttx(xml_binary, xml_built, fromfile='decompiled', tofile='built') self.expect_ttx(xml_expected, xml_built, fromfile=xml_expected_path, tofile='built') from fontTools.misc import xmlReader f = StringIO() f.write(xml_expected) f.seek(0) font2 = TTFont() font2.setGlyphOrder(font.getGlyphOrder()) reader = xmlReader.XMLReader(f, font2) reader.read(rootless=True) # XML from object read from XML. writer = XMLWriter(StringIO(), newlinestr='\n') writer.begintag(tableTag); writer.newline() font2[tableTag].toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_fromxml = writer.file.getvalue() self.expect_ttx(xml_expected, xml_fromxml, fromfile=xml_expected_path, tofile='fromxml')
def test_decompile_empty_table(self): font = TTFont() glyphNames = [".notdef", "space"] font.setGlyphOrder(glyphNames) font["loca"] = newTable("loca") font["loca"].locations = [0] * (len(glyphNames) + 1) font["glyf"] = newTable("glyf") font["glyf"].decompile(b"\x00", font) self.assertEqual(len(font["glyf"]), 2) self.assertEqual(font["glyf"][".notdef"].numberOfContours, 0) self.assertEqual(font["glyf"]["space"].numberOfContours, 0)
def test_getPhantomPoints(self): # https://github.com/fonttools/fonttools/issues/2295 font = TTFont() glyphNames = [".notdef"] font.setGlyphOrder(glyphNames) font["loca"] = newTable("loca") font["loca"].locations = [0] * (len(glyphNames) + 1) font["glyf"] = newTable("glyf") font["glyf"].decompile(b"\x00", font) font["hmtx"] = newTable("hmtx") font["hmtx"].metrics = {".notdef": (100, 0)} font["head"] = newTable("head") font["head"].unitsPerEm = 1000 self.assertEqual(font["glyf"].getPhantomPoints(".notdef", font, 0), [(0, 0), (100, 0), (0, 0), (0, -1000)])
def _ensure_groups_grouped_in_glyph_order( color_glyphs: MutableMapping[str, ColorGlyph], ttfont: ttLib.TTFont, reuse_groups: Tuple[Tuple[str, ...]], ): # svg requires glyphs in same doc have sequential gids; reshuffle to make this true glyph_order = ttfont.getGlyphOrder()[:-len(color_glyphs)] gid = len(glyph_order) for group in reuse_groups: for glyph_name in group: color_glyphs[glyph_name] = color_glyphs[glyph_name]._replace( glyph_id=gid) gid += 1 glyph_order.extend(group) ttfont.setGlyphOrder(glyph_order)
def test_setGlyphOrder_also_updates_glyf_glyphOrder(): # https://github.com/fonttools/fonttools/issues/2060#issuecomment-1063932428 font = TTFont() font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx")) current_order = font.getGlyphOrder() assert current_order == font["glyf"].glyphOrder new_order = list(current_order) while new_order == current_order: random.shuffle(new_order) font.setGlyphOrder(new_order) assert font.getGlyphOrder() == new_order assert font["glyf"].glyphOrder == new_order
def makeTTFont(): glyphs = ( ".notdef space slash fraction " "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 onehalf " "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 " "d.alt n.end s.end " "c_h c_k c_s c_t f_f f_f_i f_i f_l o_f_f_i " ).split() font = TTFont() font.setGlyphOrder(glyphs) return font
def makeTTFont(): glyphs = ( ".notdef space slash fraction " "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 onehalf " "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 " "d.alt n.end s.end " "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 " "grave acute dieresis macron circumflex cedilla umlaut ogonek caron " "damma hamza sukun kasratan lam_meem_jeem ").split() font = TTFont() font.setGlyphOrder(glyphs) return font
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 makeTTFont(): glyphs = ( ".notdef space slash fraction " "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 onehalf " "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 " "d.alt n.end s.end " "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 " "grave acute dieresis macron circumflex cedilla umlaut ogonek caron " "damma hamza sukun kasratan lam_meem_jeem " ).split() font = TTFont() font.setGlyphOrder(glyphs) return font
def _ensure_groups_grouped_in_glyph_order( color_glyphs: MutableMapping[str, ColorGlyph], color_glyph_order: Sequence[str], ttfont: ttLib.TTFont, reuse_groups: Tuple[Tuple[str, ...]], ): # svg requires glyphs in same doc have sequential gids; reshuffle to make this true. # Changing the order of glyphs in a TTFont requires that all tables that use # glyph indexes have been fully decompiled (loaded with lazy=False). # Cf. https://github.com/fonttools/fonttools/issues/2060 _ensure_ttfont_fully_decompiled(ttfont) # The glyph names in the TTFont may have been dropped (post table 3.0), so the # names we see after decompiling the TTFont are made up and likely different # from the input color glyph names. We only want to reorder the glyphs while # keeping the existing names, we can't change order and rename at the same time # or else tables that contain mappings keyed by glyph name would blow up. # Thus, we need to match the old and current names by their position in the # font's current glyph order: i.e. we assume all color glyphs are placed at the # END of the glyph order. current_glyph_order = ttfont.getGlyphOrder() current_color_glyph_names = current_glyph_order[-len(color_glyphs):] assert len(color_glyph_order) == len(current_color_glyph_names) rename_map = { color_glyph_order[i]: current_color_glyph_names[i] for i in range(len(color_glyph_order)) } glyph_order = current_glyph_order[:-len(color_glyphs)] gid = len(glyph_order) for group in reuse_groups: for glyph_name in group: color_glyphs[glyph_name] = color_glyphs[glyph_name]._replace( glyph_id=gid) gid += 1 glyph_order.extend(rename_map[g] for g in group) ttfont.setGlyphOrder(glyph_order)
class OutlineOTFCompiler(object): """ This object will create a bare-bones OTF-CFF containing outline data and not much else. The only external method is :meth:`ufo2fdk.tools.outlineOTF.compile`. When creating this object, you must provide a *font* object and a *path* indicating where the OTF should be saved. Optionally, you can provide a *glyphOrder* list of glyph names indicating the order of the glyphs in the font. """ def __init__(self, font, path, glyphOrder=None): self.ufo = font self.path = path self.log = [] # make any missing glyphs and store them locally missingRequiredGlyphs = self.makeMissingRequiredGlyphs() # make a dict of all glyphs self.allGlyphs = {} for glyph in font: self.allGlyphs[glyph.name] = glyph self.allGlyphs.update(missingRequiredGlyphs) # store the glyph order if glyphOrder is None: glyphOrder = sorted(self.allGlyphs.keys()) self.glyphOrder = self.makeOfficialGlyphOrder(glyphOrder) # make a reusable bounding box self.fontBoundingBox = tuple([_roundInt(i) for i in self.makeFontBoundingBox()]) # make a reusable character mapping self.unicodeToGlyphNameMapping = self.makeUnicodeToGlyphNameMapping() # ----------- # Main Method # ----------- def compile(self): """ Compile the OTF. """ self.otf = TTFont(sfntVersion="OTTO") # populate basic tables self.setupTable_head() self.setupTable_hhea() self.setupTable_hmtx() self.setupTable_name() self.setupTable_maxp() self.setupTable_cmap() self.setupTable_OS2() self.setupTable_post() self.setupTable_CFF() self.setupOtherTables() # write the file self.otf.save(self.path) # discard the object self.otf.close() del self.otf # ----- # Tools # ----- def makeFontBoundingBox(self): """ Make a bounding box for the font. **This should not be called externally.** Subclasses may override this method to handle the bounds creation in a different way if desired. """ return getFontBounds(self.ufo) def makeUnicodeToGlyphNameMapping(self): """ Make a ``unicode : glyph name`` mapping for the font. **This should not be called externally.** Subclasses may override this method to handle the mapping creation in a different way if desired. """ mapping = {} for glyphName, glyph in self.allGlyphs.items(): unicodes = glyph.unicodes for uni in unicodes: mapping[uni] = glyphName return mapping def makeMissingRequiredGlyphs(self): """ Add space and .notdef to the font if they are not present. **This should not be called externally.** Subclasses may override this method to handle the glyph creation in a different way if desired. """ glyphs = {} font = self.ufo unitsPerEm = _roundInt(getAttrWithFallback(font.info, "unitsPerEm")) ascender = _roundInt(getAttrWithFallback(font.info, "ascender")) descender = _roundInt(getAttrWithFallback(font.info, "descender")) defaultWidth = _roundInt(unitsPerEm * 0.5) if ".notdef" not in self.ufo: glyphs[".notdef"] = StubGlyph(name=".notdef", width=defaultWidth, unitsPerEm=unitsPerEm, ascender=ascender, descender=descender) if "space" not in self.ufo: glyphs["space"] = StubGlyph(name="space", width=defaultWidth, unitsPerEm=unitsPerEm, ascender=ascender, descender=descender, unicodes=[32]) return glyphs def makeOfficialGlyphOrder(self, glyphOrder): """ Make a the final glyph order. **This should not be called externally.** Subclasses may override this method to handle the order creation in a different way if desired. """ allGlyphs = self.allGlyphs orderedGlyphs = [".notdef", "space"] for glyphName in glyphOrder: if glyphName in [".notdef", "space"]: continue orderedGlyphs.append(glyphName) for glyphName in sorted(allGlyphs.keys()): if glyphName not in orderedGlyphs: orderedGlyphs.append(glyphName) return orderedGlyphs def getCharStringForGlyph(self, glyph, private, globalSubrs): """ Get a Type2CharString for the *glyph* **This should not be called externally.** Subclasses may override this method to handle the charstring creation in a different way if desired. """ width = glyph.width # subtract the nominal width postscriptNominalWidthX = getAttrWithFallback(self.ufo.info, "postscriptNominalWidthX") if postscriptNominalWidthX: width = width - postscriptNominalWidthX # round width = _roundInt(width) pen = T2CharStringPen(width, self.allGlyphs) glyph.draw(pen) charString = pen.getCharString(private, globalSubrs) return charString # -------------- # Table Builders # -------------- def setupTable_head(self): """ Make the head table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["head"] = head = newTable("head") font = self.ufo head.checkSumAdjustment = 0 head.tableVersion = 1.0 versionMajor = getAttrWithFallback(font.info, "versionMajor") versionMinor = getAttrWithFallback(font.info, "versionMinor") * .001 head.fontRevision = versionMajor + versionMinor head.magicNumber = 0x5F0F3CF5 # upm head.unitsPerEm = int(round(getAttrWithFallback(font.info, "unitsPerEm"))) # times head.created = dateStringToTimeValue(getAttrWithFallback(font.info, "openTypeHeadCreated")) - mac_epoch_diff head.modified = dateStringToTimeValue(dateStringForNow()) - mac_epoch_diff # bounding box xMin, yMin, xMax, yMax = self.fontBoundingBox head.xMin = int(math.floor(xMin)) head.yMin = int(math.floor(yMin)) head.xMax = int(math.ceil(xMax)) head.yMax = int(math.ceil(yMax)) # style mapping styleMapStyleName = getAttrWithFallback(font.info, "styleMapStyleName") macStyle = [] if styleMapStyleName == "bold": macStyle = [0] elif styleMapStyleName == "bold italic": macStyle = [0, 1] elif styleMapStyleName == "italic": macStyle = [1] head.macStyle = intListToNum(macStyle, 0, 16) # misc head.flags = intListToNum(getAttrWithFallback(font.info, "openTypeHeadFlags"), 0, 16) head.lowestRecPPEM = _roundInt(getAttrWithFallback(font.info, "openTypeHeadLowestRecPPEM")) head.fontDirectionHint = 2 head.indexToLocFormat = 0 head.glyphDataFormat = 0 def setupTable_name(self): """ Make the name table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["name"] = newTable("name") def setupTable_maxp(self): """ Make the maxp table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["maxp"] = maxp = newTable("maxp") maxp.tableVersion = 0x00005000 def setupTable_cmap(self): """ Make the cmap table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ from fontTools.ttLib.tables._c_m_a_p import cmap_format_4 nonBMP = dict((k, v) for k, v in self.unicodeToGlyphNameMapping.items() if k > 65535) if nonBMP: mapping = dict((k, v) for k, v in self.unicodeToGlyphNameMapping.items() if k <= 65535) else: mapping = dict(self.unicodeToGlyphNameMapping) # mac cmap4_0_3 = cmap_format_4(4) cmap4_0_3.platformID = 0 cmap4_0_3.platEncID = 3 cmap4_0_3.language = 0 cmap4_0_3.cmap = mapping # windows cmap4_3_1 = cmap_format_4(4) cmap4_3_1.platformID = 3 cmap4_3_1.platEncID = 1 cmap4_3_1.language = 0 cmap4_3_1.cmap = mapping # store self.otf["cmap"] = cmap = newTable("cmap") cmap.tableVersion = 0 cmap.tables = [cmap4_0_3, cmap4_3_1] # If we have glyphs outside Unicode BMP, we must set another # subtable that can hold longer codepoints for them. if nonBMP: from fontTools.ttLib.tables._c_m_a_p import cmap_format_12 nonBMP.update(mapping) # mac cmap12_0_4 = cmap_format_12(12) cmap12_0_4.platformID = 0 cmap12_0_4.platEncID = 4 cmap12_0_4.language = 0 cmap12_0_4.cmap = nonBMP # windows cmap12_3_10 = cmap_format_12(12) cmap12_3_10.platformID = 3 cmap12_3_10.platEncID = 10 cmap12_3_10.language = 0 cmap12_3_10.cmap = nonBMP # update tables registry cmap.tables = [cmap4_0_3, cmap4_3_1, cmap12_0_4, cmap12_3_10] cmap.tables.sort() def setupTable_OS2(self): """ Make the OS/2 table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["OS/2"] = os2 = newTable("OS/2") font = self.ufo os2.version = 0x0004 # average glyph width widths = [glyph.width for glyph in self.allGlyphs.values() if glyph.width > 0] os2.xAvgCharWidth = _roundInt(sum(widths) / len(widths)) # weight and width classes os2.usWeightClass = getAttrWithFallback(font.info, "openTypeOS2WeightClass") os2.usWidthClass = getAttrWithFallback(font.info, "openTypeOS2WidthClass") # embedding os2.fsType = intListToNum(getAttrWithFallback(font.info, "openTypeOS2Type"), 0, 16) # subscript v = getAttrWithFallback(font.info, "openTypeOS2SubscriptXSize") if v is None: v = 0 os2.ySubscriptXSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptYSize") if v is None: v = 0 os2.ySubscriptYSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptXOffset") if v is None: v = 0 os2.ySubscriptXOffset = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptYOffset") if v is None: v = 0 os2.ySubscriptYOffset = _roundInt(v) # superscript v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptXSize") if v is None: v = 0 os2.ySuperscriptXSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptYSize") if v is None: v = 0 os2.ySuperscriptYSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptXOffset") if v is None: v = 0 os2.ySuperscriptXOffset = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptYOffset") if v is None: v = 0 os2.ySuperscriptYOffset = _roundInt(v) # strikeout v = getAttrWithFallback(font.info, "openTypeOS2StrikeoutSize") if v is None: v = 0 os2.yStrikeoutSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2StrikeoutPosition") if v is None: v = 0 os2.yStrikeoutPosition = _roundInt(v) # family class os2.sFamilyClass = 0 # XXX not sure how to create the appropriate value # panose data = getAttrWithFallback(font.info, "openTypeOS2Panose") panose = Panose() panose.bFamilyType = data[0] panose.bSerifStyle = data[1] panose.bWeight = data[2] panose.bProportion = data[3] panose.bContrast = data[4] panose.bStrokeVariation = data[5] panose.bArmStyle = data[6] panose.bLetterForm = data[7] panose.bMidline = data[8] panose.bXHeight = data[9] os2.panose = panose # Unicode ranges uniRanges = getAttrWithFallback(font.info, "openTypeOS2UnicodeRanges") os2.ulUnicodeRange1 = intListToNum(uniRanges, 0, 32) os2.ulUnicodeRange2 = intListToNum(uniRanges, 32, 32) os2.ulUnicodeRange3 = intListToNum(uniRanges, 64, 32) os2.ulUnicodeRange4 = intListToNum(uniRanges, 96, 32) # codepage ranges codepageRanges = getAttrWithFallback(font.info, "openTypeOS2CodePageRanges") os2.ulCodePageRange1 = intListToNum(codepageRanges, 0, 32) os2.ulCodePageRange2 = intListToNum(codepageRanges, 32, 32) # vendor id os2.achVendID = _ignoreASCII(getAttrWithFallback(font.info, "openTypeOS2VendorID")) # vertical metrics os2.sxHeight = _roundInt(getAttrWithFallback(font.info, "xHeight")) os2.sCapHeight = _roundInt(getAttrWithFallback(font.info, "capHeight")) os2.sTypoAscender = _roundInt(getAttrWithFallback(font.info, "openTypeOS2TypoAscender")) os2.sTypoDescender = _roundInt(getAttrWithFallback(font.info, "openTypeOS2TypoDescender")) os2.sTypoLineGap = _roundInt(getAttrWithFallback(font.info, "openTypeOS2TypoLineGap")) os2.usWinAscent = _roundInt(getAttrWithFallback(font.info, "openTypeOS2WinAscent")) os2.usWinDescent = _roundInt(getAttrWithFallback(font.info, "openTypeOS2WinDescent")) # style mapping selection = list(getAttrWithFallback(font.info, "openTypeOS2Selection")) styleMapStyleName = getAttrWithFallback(font.info, "styleMapStyleName") if styleMapStyleName == "regular": selection.append(6) elif styleMapStyleName == "bold": selection.append(5) elif styleMapStyleName == "italic": selection.append(0) elif styleMapStyleName == "bold italic": selection += [0, 5] os2.fsSelection = intListToNum(selection, 0, 16) # characetr indexes unicodes = [i for i in self.unicodeToGlyphNameMapping.keys() if i is not None] if unicodes: minIndex = min(unicodes) maxIndex = max(unicodes) else: # the font may have *no* unicode values # (it really happens!) so there needs # to be a fallback. use space for this. minIndex = 0x0020 maxIndex = 0x0020 if maxIndex > 0xFFFF: # the spec says that 0xFFFF should be used # as the max if the max exceeds 0xFFFF maxIndex = 0xFFFF os2.fsFirstCharIndex = minIndex os2.fsLastCharIndex = maxIndex os2.usBreakChar = 32 os2.usDefaultChar = 0 # maximum contextual lookup length os2.usMaxContex = 0 def setupTable_hmtx(self): """ Make the hmtx table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["hmtx"] = hmtx = newTable("hmtx") hmtx.metrics = {} for glyphName, glyph in self.allGlyphs.items(): width = glyph.width left = 0 if len(glyph) or len(glyph.components): left = glyph.leftMargin if left is None: left = 0 hmtx[glyphName] = (_roundInt(width), _roundInt(left)) def setupTable_hhea(self): """ Make the hhea table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["hhea"] = hhea = newTable("hhea") font = self.ufo hhea.tableVersion = 0x00010000 # vertical metrics hhea.ascent = _roundInt(getAttrWithFallback(font.info, "openTypeHheaAscender")) hhea.descent = _roundInt(getAttrWithFallback(font.info, "openTypeHheaDescender")) hhea.lineGap = _roundInt(getAttrWithFallback(font.info, "openTypeHheaLineGap")) # horizontal metrics widths = [] lefts = [] rights = [] extents = [] for glyph in self.allGlyphs.values(): left = glyph.leftMargin right = glyph.rightMargin if left is None: left = 0 if right is None: right = 0 widths.append(glyph.width) lefts.append(left) rights.append(right) bounds = glyph.bounds if bounds is not None: xMin, yMin, xMax, yMax = bounds else: xMin = 0 xMax = 0 extent = left + (xMax - xMin) # equation from spec for calculating xMaxExtent: Max(lsb + (xMax - xMin)) extents.append(extent) hhea.advanceWidthMax = _roundInt(max(widths)) hhea.minLeftSideBearing = _roundInt(min(lefts)) hhea.minRightSideBearing = _roundInt(min(rights)) hhea.xMaxExtent = _roundInt(max(extents)) # misc hhea.caretSlopeRise = getAttrWithFallback(font.info, "openTypeHheaCaretSlopeRise") hhea.caretSlopeRun = getAttrWithFallback(font.info, "openTypeHheaCaretSlopeRun") hhea.caretOffset = _roundInt(getAttrWithFallback(font.info, "openTypeHheaCaretOffset")) hhea.reserved0 = 0 hhea.reserved1 = 0 hhea.reserved2 = 0 hhea.reserved3 = 0 hhea.metricDataFormat = 0 # glyph count hhea.numberOfHMetrics = len(self.allGlyphs) def setupTable_post(self): """ Make the post table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["post"] = post = newTable("post") font = self.ufo post.formatType = 3.0 # italic angle italicAngle = getAttrWithFallback(font.info, "italicAngle") post.italicAngle = italicAngle # underline underlinePosition = getAttrWithFallback(font.info, "postscriptUnderlinePosition") if underlinePosition is None: underlinePosition = 0 post.underlinePosition = _roundInt(underlinePosition) underlineThickness = getAttrWithFallback(font.info, "postscriptUnderlineThickness") if underlineThickness is None: underlineThickness = 0 post.underlineThickness = _roundInt(underlineThickness) # determine if the font has a fixed width post.isFixedPitch = getAttrWithFallback(font.info, "postscriptIsFixedPitch") # misc post.minMemType42 = 0 post.maxMemType42 = 0 post.minMemType1 = 0 post.maxMemType1 = 0 def setupTable_CFF(self): """ Make the CFF table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["CFF "] = cff = newTable("CFF ") cff = cff.cff # set up the basics cff.major = 1 cff.minor = 0 cff.hdrSize = 4 cff.offSize = 4 cff.fontNames = [] strings = IndexedStrings() cff.strings = strings private = PrivateDict(strings=strings) private.rawDict.update(private.defaults) globalSubrs = GlobalSubrsIndex(private=private) topDict = TopDict(GlobalSubrs=globalSubrs, strings=strings) topDict.Private = private charStrings = topDict.CharStrings = CharStrings(file=None, charset=None, globalSubrs=globalSubrs, private=private, fdSelect=None, fdArray=None) charStrings.charStringsAreIndexed = True topDict.charset = [] charStringsIndex = charStrings.charStringsIndex = SubrsIndex(private=private, globalSubrs=globalSubrs) cff.topDictIndex = topDictIndex = TopDictIndex() topDictIndex.append(topDict) topDictIndex.strings = strings cff.GlobalSubrs = globalSubrs # populate naming data info = self.ufo.info psName = getAttrWithFallback(info, "postscriptFontName") cff.fontNames.append(psName) topDict = cff.topDictIndex[0] topDict.version = "%d.%d" % (getAttrWithFallback(info, "versionMajor"), getAttrWithFallback(info, "versionMinor")) trademark = getAttrWithFallback(info, "trademark") if trademark: trademark = normalizeStringForPostscript(trademark.replace(u"\u00A9", "Copyright")) if trademark != self.ufo.info.trademark: self.log.append("[Warning] The trademark was normalized for storage in the CFF table and consequently some characters were dropped: '%s'" % trademark) if trademark is None: trademark = "" topDict.Notice = trademark copyright = getAttrWithFallback(info, "copyright") if copyright: copyright = normalizeStringForPostscript(copyright.replace(u"\u00A9", "Copyright")) if copyright != self.ufo.info.copyright: self.log.append("[Warning] The copyright was normalized for storage in the CFF table and consequently some characters were dropped: '%s'" % copyright) if copyright is None: copyright = "" topDict.Copyright = copyright topDict.FullName = getAttrWithFallback(info, "postscriptFullName") topDict.FamilyName = getAttrWithFallback(info, "openTypeNamePreferredFamilyName") topDict.Weight = getAttrWithFallback(info, "postscriptWeightName") topDict.FontName = getAttrWithFallback(info, "postscriptFontName") # populate various numbers topDict.isFixedPitch = getAttrWithFallback(info, "postscriptIsFixedPitch") topDict.ItalicAngle = getAttrWithFallback(info, "italicAngle") underlinePosition = getAttrWithFallback(info, "postscriptUnderlinePosition") if underlinePosition is None: underlinePosition = 0 topDict.UnderlinePosition = _roundInt(underlinePosition) underlineThickness = getAttrWithFallback(info, "postscriptUnderlineThickness") if underlineThickness is None: underlineThickness = 0 topDict.UnderlineThickness = _roundInt(underlineThickness) # populate font matrix unitsPerEm = _roundInt(getAttrWithFallback(info, "unitsPerEm")) topDict.FontMatrix = [1.0 / unitsPerEm, 0, 0, 1.0 / unitsPerEm, 0, 0] # populate the width values defaultWidthX = _roundInt(getAttrWithFallback(info, "postscriptDefaultWidthX")) if defaultWidthX: private.rawDict["defaultWidthX"] = defaultWidthX nominalWidthX = _roundInt(getAttrWithFallback(info, "postscriptNominalWidthX")) if nominalWidthX: private.rawDict["nominalWidthX"] = nominalWidthX # populate hint data blueFuzz = _roundInt(getAttrWithFallback(info, "postscriptBlueFuzz")) blueShift = _roundInt(getAttrWithFallback(info, "postscriptBlueShift")) blueScale = getAttrWithFallback(info, "postscriptBlueScale") forceBold = getAttrWithFallback(info, "postscriptForceBold") blueValues = getAttrWithFallback(info, "postscriptBlueValues") if isinstance(blueValues, list): blueValues = [_roundInt(i) for i in blueValues] otherBlues = getAttrWithFallback(info, "postscriptOtherBlues") if isinstance(otherBlues, list): otherBlues = [_roundInt(i) for i in otherBlues] familyBlues = getAttrWithFallback(info, "postscriptFamilyBlues") if isinstance(familyBlues, list): familyBlues = [_roundInt(i) for i in familyBlues] familyOtherBlues = getAttrWithFallback(info, "postscriptFamilyOtherBlues") if isinstance(familyOtherBlues, list): familyOtherBlues = [_roundInt(i) for i in familyOtherBlues] stemSnapH = getAttrWithFallback(info, "postscriptStemSnapH") if isinstance(stemSnapH, list): stemSnapH = [_roundInt(i) for i in stemSnapH] stemSnapV = getAttrWithFallback(info, "postscriptStemSnapV") if isinstance(stemSnapV, list): stemSnapV = [_roundInt(i) for i in stemSnapV] # only write the blues data if some blues are defined. if (blueValues or otherBlues): private.rawDict["BlueFuzz"] = blueFuzz private.rawDict["BlueShift"] = blueShift private.rawDict["BlueScale"] = blueScale private.rawDict["ForceBold"] = forceBold private.rawDict["BlueValues"] = blueValues private.rawDict["OtherBlues"] = otherBlues private.rawDict["FamilyBlues"] = familyBlues private.rawDict["FamilyOtherBlues"] = familyOtherBlues # only write the stems if both are defined. if (stemSnapH and stemSnapV): private.rawDict["StemSnapH"] = stemSnapH private.rawDict["StdHW"] = stemSnapH[0] private.rawDict["StemSnapV"] = stemSnapV private.rawDict["StdVW"] = stemSnapV[0] # populate glyphs for glyphName in self.glyphOrder: glyph = self.allGlyphs[glyphName] charString = self.getCharStringForGlyph(glyph, private, globalSubrs) # add to the font exists = glyphName in charStrings if exists: # XXX a glyph already has this name. should we choke? glyphID = charStrings.charStrings[glyphName] charStringsIndex.items[glyphID] = charString else: charStringsIndex.append(charString) glyphID = len(topDict.charset) charStrings.charStrings[glyphName] = glyphID topDict.charset.append(glyphName) topDict.FontBBox = self.fontBoundingBox # write the glyph order self.otf.setGlyphOrder(self.glyphOrder) def setupOtherTables(self): """ Make the other tables. The default implementation does nothing. **This should not be called externally.** Subclasses may override this method to add other tables to the font if desired. """ pass
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
class WOFF2Writer(SFNTWriter): flavor = "woff2" def __init__(self, file, numTables, sfntVersion="\000\001\000\000", flavor=None, flavorData=None): if not haveBrotli: print( 'The WOFF2 encoder requires the Brotli Python extension, available at:\n' 'https://github.com/google/brotli', file=sys.stderr) raise ImportError("No module named brotli") self.file = file self.numTables = numTables self.sfntVersion = Tag(sfntVersion) self.flavorData = flavorData or WOFF2FlavorData() self.directoryFormat = woff2DirectoryFormat self.directorySize = woff2DirectorySize self.DirectoryEntry = WOFF2DirectoryEntry self.signature = Tag("wOF2") self.nextTableOffset = 0 self.transformBuffer = BytesIO() self.tables = OrderedDict() # make empty TTFont to store data while normalising and transforming tables self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) def __setitem__(self, tag, data): """Associate new entry named 'tag' with raw table data.""" if tag in self.tables: raise TTLibError("cannot rewrite '%s' table" % tag) if tag == 'DSIG': # always drop DSIG table, since the encoding process can invalidate it self.numTables -= 1 return entry = self.DirectoryEntry() entry.tag = Tag(tag) entry.flags = getKnownTagIndex(entry.tag) # WOFF2 table data are written to disk only on close(), after all tags # have been specified entry.data = data self.tables[tag] = entry def close(self): """ All tags must have been specified. Now write the table data and directory. """ if len(self.tables) != self.numTables: raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(self.tables))) if self.sfntVersion in ("\x00\x01\x00\x00", "true"): isTrueType = True elif self.sfntVersion == "OTTO": isTrueType = False else: raise TTLibError( "Not a TrueType or OpenType font (bad sfntVersion)") # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned. # However, the reference WOFF2 implementation still fails to reconstruct # 'unpadded' glyf tables, therefore we need to 'normalise' them. # See: # https://github.com/khaledhosny/ots/issues/60 # https://github.com/google/woff2/issues/15 if isTrueType: self._normaliseGlyfAndLoca(padding=4) self._setHeadTransformFlag() # To pass the legacy OpenType Sanitiser currently included in browsers, # we must sort the table directory and data alphabetically by tag. # See: # https://github.com/google/woff2/pull/3 # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html # TODO(user): remove to match spec once browsers are on newer OTS self.tables = OrderedDict(sorted(self.tables.items())) self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() fontData = self._transformTables() compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT) self.totalCompressedSize = len(compressedFont) self.length = self._calcTotalSize() self.majorVersion, self.minorVersion = self._getVersion() self.reserved = 0 directory = self._packTableDirectory() self.file.seek(0) self.file.write(pad(directory + compressedFont, size=4)) self._writeFlavorData() def _normaliseGlyfAndLoca(self, padding=4): """ Recompile glyf and loca tables, aligning glyph offsets to multiples of 'padding' size. Update the head table's 'indexToLocFormat' accordingly while compiling loca. """ if self.sfntVersion == "OTTO": return # make up glyph names required to decompile glyf table self._decompileTable('maxp') numGlyphs = self.ttFont['maxp'].numGlyphs glyphOrder = ['.notdef' ] + ["glyph%.5d" % i for i in range(1, numGlyphs)] self.ttFont.setGlyphOrder(glyphOrder) for tag in ('head', 'loca', 'glyf'): self._decompileTable(tag) self.ttFont['glyf'].padding = padding for tag in ('glyf', 'loca'): self._compileTable(tag) def _setHeadTransformFlag(self): """ Set bit 11 of 'head' table flags to indicate that the font has undergone a lossless modifying transform. Re-compile head table data.""" self._decompileTable('head') self.ttFont['head'].flags |= (1 << 11) self._compileTable('head') def _decompileTable(self, tag): """ Fetch table data, decompile it, and store it inside self.ttFont. """ tag = Tag(tag) if tag not in self.tables: raise TTLibError("missing required table: %s" % tag) if self.ttFont.isLoaded(tag): return data = self.tables[tag].data if tag == 'loca': tableClass = WOFF2LocaTable elif tag == 'glyf': tableClass = WOFF2GlyfTable else: tableClass = getTableClass(tag) table = tableClass(tag) self.ttFont.tables[tag] = table table.decompile(data, self.ttFont) def _compileTable(self, tag): """ Compile table and store it in its 'data' attribute. """ self.tables[tag].data = self.ttFont[tag].compile(self.ttFont) def _calcSFNTChecksumsLengthsAndOffsets(self): """ Compute the 'original' SFNT checksums, lengths and offsets for checksum adjustment calculation. Return the total size of the uncompressed font. """ offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables) for tag, entry in self.tables.items(): data = entry.data entry.origOffset = offset entry.origLength = len(data) if tag == 'head': entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) else: entry.checkSum = calcChecksum(data) offset += (entry.origLength + 3) & ~3 return offset def _transformTables(self): """Return transformed font data.""" for tag, entry in self.tables.items(): if tag in woff2TransformedTableTags: data = self.transformTable(tag) else: data = entry.data entry.offset = self.nextTableOffset entry.saveData(self.transformBuffer, data) self.nextTableOffset += entry.length self.writeMasterChecksum() fontData = self.transformBuffer.getvalue() return fontData def transformTable(self, tag): """Return transformed table data.""" if tag not in woff2TransformedTableTags: raise TTLibError("Transform for table '%s' is unknown" % tag) if tag == "loca": data = b"" elif tag == "glyf": for tag in ('maxp', 'head', 'loca', 'glyf'): self._decompileTable(tag) glyfTable = self.ttFont['glyf'] data = glyfTable.transform(self.ttFont) else: raise NotImplementedError return data def _calcMasterChecksum(self): """Calculate checkSumAdjustment.""" tags = list(self.tables.keys()) checksums = [] for i in range(len(tags)): checksums.append(self.tables[tags[i]].checkSum) # Create a SFNT directory for checksum calculation purposes self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( self.numTables, 16) directory = sstruct.pack(sfntDirectoryFormat, self) tables = sorted(self.tables.items()) for tag, entry in tables: sfntEntry = SFNTDirectoryEntry() sfntEntry.tag = entry.tag sfntEntry.checkSum = entry.checkSum sfntEntry.offset = entry.origOffset sfntEntry.length = entry.origLength directory = directory + sfntEntry.toString() directory_end = sfntDirectorySize + len( self.tables) * sfntDirectoryEntrySize assert directory_end == len(directory) checksums.append(calcChecksum(directory)) checksum = sum(checksums) & 0xffffffff # BiboAfba! checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff return checksumadjustment def writeMasterChecksum(self): """Write checkSumAdjustment to the transformBuffer.""" checksumadjustment = self._calcMasterChecksum() self.transformBuffer.seek(self.tables['head'].offset + 8) self.transformBuffer.write(struct.pack(">L", checksumadjustment)) def _calcTotalSize(self): """Calculate total size of WOFF2 font, including any meta- and/or private data.""" offset = self.directorySize for entry in self.tables.values(): offset += len(entry.toString()) offset += self.totalCompressedSize offset = (offset + 3) & ~3 offset = self._calcFlavorDataOffsetsAndSize(offset) return offset def _calcFlavorDataOffsetsAndSize(self, start): """Calculate offsets and lengths for any meta- and/or private data.""" offset = start data = self.flavorData if data.metaData: self.metaOrigLength = len(data.metaData) self.metaOffset = offset self.compressedMetaData = brotli.compress(data.metaData, mode=brotli.MODE_TEXT) self.metaLength = len(self.compressedMetaData) offset += self.metaLength else: self.metaOffset = self.metaLength = self.metaOrigLength = 0 self.compressedMetaData = b"" if data.privData: # make sure private data is padded to 4-byte boundary offset = (offset + 3) & ~3 self.privOffset = offset self.privLength = len(data.privData) offset += self.privLength else: self.privOffset = self.privLength = 0 return offset def _getVersion(self): """Return the WOFF2 font's (majorVersion, minorVersion) tuple.""" data = self.flavorData if data.majorVersion is not None and data.minorVersion is not None: return data.majorVersion, data.minorVersion else: # if None, return 'fontRevision' from 'head' table if 'head' in self.tables: return struct.unpack(">HH", self.tables['head'].data[4:8]) else: return 0, 0 def _packTableDirectory(self): """Return WOFF2 table directory data.""" directory = sstruct.pack(self.directoryFormat, self) for entry in self.tables.values(): directory = directory + entry.toString() return directory def _writeFlavorData(self): """Write metadata and/or private data using appropiate padding.""" compressedMetaData = self.compressedMetaData privData = self.flavorData.privData if compressedMetaData and privData: compressedMetaData = pad(compressedMetaData, size=4) if compressedMetaData: self.file.seek(self.metaOffset) assert self.file.tell() == self.metaOffset self.file.write(compressedMetaData) if privData: self.file.seek(self.privOffset) assert self.file.tell() == self.privOffset self.file.write(privData) def reordersTables(self): return True
class OTFPostProcessor(object): """Does some post-processing operations on a compiled OpenType font, using info from the source UFO where necessary. """ def __init__(self, otf, ufo): self.ufo = ufo stream = BytesIO() otf.save(stream) stream.seek(0) self.otf = TTFont(stream) self._postscriptNames = ufo.lib.get('public.postscriptNames') def process(self, useProductionNames=True, optimizeCff=True): if useProductionNames: self._rename_glyphs_from_ufo() if optimizeCff and 'CFF ' in self.otf: from compreffor import compress compress(self.otf) return self.otf def _rename_glyphs_from_ufo(self): """Rename glyphs using ufo.lib.public.postscriptNames in UFO.""" rename_map = {g.name: self._build_production_name(g) for g in self.ufo} # .notdef may not be present in the original font rename_map[".notdef"] = ".notdef" rename = lambda names: [rename_map[n] for n in names] self.otf.setGlyphOrder(rename(self.otf.getGlyphOrder())) if 'CFF ' in self.otf: cff = self.otf['CFF '].cff.topDictIndex[0] char_strings = cff.CharStrings.charStrings cff.CharStrings.charStrings = { rename_map.get(n, n): v for n, v in char_strings.items() } cff.charset = rename(cff.charset) def _build_production_name(self, glyph): """Build a production name for a single glyph.""" # use PostScript names from UFO lib if available if self._postscriptNames: production_name = self._postscriptNames.get(glyph.name) return production_name if production_name else glyph.name # use name derived from unicode value unicode_val = glyph.unicode if glyph.unicode is not None: return '%s%04X' % ('u' if unicode_val > 0xffff else 'uni', unicode_val) # use production name + last (non-script) suffix if possible parts = glyph.name.rsplit('.', 1) if len(parts) == 2 and parts[0] in self.ufo: return '%s.%s' % (self._build_production_name( self.ufo[parts[0]]), parts[1]) # use ligature name, making sure to look up components with suffixes parts = glyph.name.split('.', 1) if len(parts) == 2: liga_parts = ['%s.%s' % (n, parts[1]) for n in parts[0].split('_')] else: liga_parts = glyph.name.split('_') if len(liga_parts) > 1 and all(n in self.ufo for n in liga_parts): unicode_vals = [self.ufo[n].unicode for n in liga_parts] if all(v and v <= 0xffff for v in unicode_vals): return 'uni' + ''.join('%04X' % v for v in unicode_vals) return '_'.join( self._build_production_name(self.ufo[n]) for n in liga_parts) return glyph.name
def font(): font = TTFont() font.setGlyphOrder([".notdef"] + ["glyph%05d" % i for i in range(1, 30)]) return font
class OutlineOTFCompiler(object): """ This object will create a bare-bones OTF-CFF containing outline data and not much else. The only external method is :meth:`ufo2fdk.tools.outlineOTF.compile`. When creating this object, you must provide a *font* object and a *path* indicating where the OTF should be saved. Optionally, you can provide a *glyphOrder* list of glyph names indicating the order of the glyphs in the font. """ def __init__(self, font, path, glyphOrder=None): self.ufo = font self.path = path self.log = [] # make any missing glyphs and store them locally missingRequiredGlyphs = self.makeMissingRequiredGlyphs() # make a dict of all glyphs self.allGlyphs = {} for glyph in font: self.allGlyphs[glyph.name] = glyph self.allGlyphs.update(missingRequiredGlyphs) # store the glyph order if glyphOrder is None: glyphOrder = sorted(self.allGlyphs.keys()) self.glyphOrder = self.makeOfficialGlyphOrder(glyphOrder) # make a reusable bounding box self.fontBoundingBox = tuple( [_roundInt(i) for i in self.makeFontBoundingBox()]) # make a reusable character mapping self.unicodeToGlyphNameMapping = self.makeUnicodeToGlyphNameMapping() # ----------- # Main Method # ----------- def compile(self): """ Compile the OTF. """ self.otf = TTFont(sfntVersion="OTTO") # populate basic tables self.setupTable_head() self.setupTable_hhea() self.setupTable_hmtx() self.setupTable_name() self.setupTable_maxp() self.setupTable_cmap() self.setupTable_OS2() self.setupTable_post() self.setupTable_CFF() self.setupOtherTables() # write the file self.otf.save(self.path) # discard the object self.otf.close() del self.otf # ----- # Tools # ----- def makeFontBoundingBox(self): """ Make a bounding box for the font. **This should not be called externally.** Subclasses may override this method to handle the bounds creation in a different way if desired. """ return getFontBounds(self.ufo) def makeUnicodeToGlyphNameMapping(self): """ Make a ``unicode : glyph name`` mapping for the font. **This should not be called externally.** Subclasses may override this method to handle the mapping creation in a different way if desired. """ mapping = {} for glyphName, glyph in self.allGlyphs.items(): unicodes = glyph.unicodes for uni in unicodes: mapping[uni] = glyphName return mapping def makeMissingRequiredGlyphs(self): """ Add space and .notdef to the font if they are not present. **This should not be called externally.** Subclasses may override this method to handle the glyph creation in a different way if desired. """ glyphs = {} font = self.ufo unitsPerEm = _roundInt(getAttrWithFallback(font.info, "unitsPerEm")) ascender = _roundInt(getAttrWithFallback(font.info, "ascender")) descender = _roundInt(getAttrWithFallback(font.info, "descender")) defaultWidth = _roundInt(unitsPerEm * 0.5) if ".notdef" not in self.ufo: glyphs[".notdef"] = StubGlyph(name=".notdef", width=defaultWidth, unitsPerEm=unitsPerEm, ascender=ascender, descender=descender) if "space" not in self.ufo: glyphs["space"] = StubGlyph(name="space", width=defaultWidth, unitsPerEm=unitsPerEm, ascender=ascender, descender=descender, unicodes=[32]) return glyphs def makeOfficialGlyphOrder(self, glyphOrder): """ Make a the final glyph order. **This should not be called externally.** Subclasses may override this method to handle the order creation in a different way if desired. """ allGlyphs = self.allGlyphs orderedGlyphs = [".notdef", "space"] for glyphName in glyphOrder: if glyphName in [".notdef", "space"]: continue orderedGlyphs.append(glyphName) for glyphName in sorted(self.allGlyphs.keys()): if glyphName not in orderedGlyphs: orderedGlyphs.append(glyphName) return orderedGlyphs def getCharStringForGlyph(self, glyph, private, globalSubrs): """ Get a Type2CharString for the *glyph* **This should not be called externally.** Subclasses may override this method to handle the charstring creation in a different way if desired. """ width = glyph.width # subtract the nominal width postscriptNominalWidthX = getAttrWithFallback( self.ufo.info, "postscriptNominalWidthX") if postscriptNominalWidthX: width = width - postscriptNominalWidthX # round width = _roundInt(width) pen = T2CharStringPen(width, self.allGlyphs) glyph.draw(pen) charString = pen.getCharString(private, globalSubrs) return charString # -------------- # Table Builders # -------------- def setupTable_head(self): """ Make the head table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["head"] = head = newTable("head") font = self.ufo head.checkSumAdjustment = 0 head.tableVersion = 1.0 versionMajor = getAttrWithFallback(font.info, "versionMajor") versionMinor = getAttrWithFallback(font.info, "versionMinor") * .001 head.fontRevision = versionMajor + versionMinor head.magicNumber = 0x5F0F3CF5 # upm head.unitsPerEm = getAttrWithFallback(font.info, "unitsPerEm") # times head.created = dateStringToTimeValue( getAttrWithFallback(font.info, "openTypeHeadCreated")) - mac_epoch_diff head.modified = dateStringToTimeValue( dateStringForNow()) - mac_epoch_diff # bounding box xMin, yMin, xMax, yMax = self.fontBoundingBox head.xMin = xMin head.yMin = yMin head.xMax = xMax head.yMax = yMax # style mapping styleMapStyleName = getAttrWithFallback(font.info, "styleMapStyleName") macStyle = [] if styleMapStyleName == "bold": macStyle = [0] elif styleMapStyleName == "bold italic": macStyle = [0, 1] elif styleMapStyleName == "italic": macStyle = [1] head.macStyle = intListToNum(macStyle, 0, 16) # misc head.flags = intListToNum( getAttrWithFallback(font.info, "openTypeHeadFlags"), 0, 16) head.lowestRecPPEM = _roundInt( getAttrWithFallback(font.info, "openTypeHeadLowestRecPPEM")) head.fontDirectionHint = 2 head.indexToLocFormat = 0 head.glyphDataFormat = 0 def setupTable_name(self): """ Make the name table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["name"] = newTable("name") def setupTable_maxp(self): """ Make the maxp table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["maxp"] = maxp = newTable("maxp") maxp.tableVersion = 0x00005000 def setupTable_cmap(self): """ Make the cmap table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ from fontTools.ttLib.tables._c_m_a_p import cmap_format_4 nonBMP = dict((k, v) for k, v in self.unicodeToGlyphNameMapping.items() if k > 65535) if nonBMP: mapping = dict((k, v) for k, v in self.unicodeToGlyphNameMapping.items() if k <= 65535) else: mapping = dict(self.unicodeToGlyphNameMapping) # mac cmap4_0_3 = cmap_format_4(4) cmap4_0_3.platformID = 0 cmap4_0_3.platEncID = 3 cmap4_0_3.language = 0 cmap4_0_3.cmap = mapping # windows cmap4_3_1 = cmap_format_4(4) cmap4_3_1.platformID = 3 cmap4_3_1.platEncID = 1 cmap4_3_1.language = 0 cmap4_3_1.cmap = mapping # store self.otf["cmap"] = cmap = newTable("cmap") cmap.tableVersion = 0 cmap.tables = [cmap4_0_3, cmap4_3_1] # If we have glyphs outside Unicode BMP, we must set another # subtable that can hold longer codepoints for them. if nonBMP: from fontTools.ttLib.tables._c_m_a_p import cmap_format_12 nonBMP.update(mapping) # mac cmap12_0_4 = cmap_format_12(12) cmap12_0_4.platformID = 0 cmap12_0_4.platEncID = 4 cmap12_0_4.language = 0 cmap12_0_4.cmap = nonBMP # windows cmap12_3_10 = cmap_format_12(12) cmap12_3_10.platformID = 3 cmap12_3_10.platEncID = 10 cmap12_3_10.language = 0 cmap12_3_10.cmap = nonBMP # update tables registry cmap.tables = [cmap4_0_3, cmap4_3_1, cmap12_0_4, cmap12_3_10] def setupTable_OS2(self): """ Make the OS/2 table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["OS/2"] = os2 = newTable("OS/2") font = self.ufo os2.version = 0x0004 # average glyph width widths = [ glyph.width for glyph in self.allGlyphs.values() if glyph.width > 0 ] os2.xAvgCharWidth = _roundInt(sum(widths) / len(widths)) # weight and width classes os2.usWeightClass = getAttrWithFallback(font.info, "openTypeOS2WeightClass") os2.usWidthClass = getAttrWithFallback(font.info, "openTypeOS2WidthClass") # embedding os2.fsType = intListToNum( getAttrWithFallback(font.info, "openTypeOS2Type"), 0, 16) # subscript v = getAttrWithFallback(font.info, "openTypeOS2SubscriptXSize") if v is None: v = 0 os2.ySubscriptXSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptYSize") if v is None: v = 0 os2.ySubscriptYSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptXOffset") if v is None: v = 0 os2.ySubscriptXOffset = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SubscriptYOffset") if v is None: v = 0 os2.ySubscriptYOffset = _roundInt(v) # superscript v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptXSize") if v is None: v = 0 os2.ySuperscriptXSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptYSize") if v is None: v = 0 os2.ySuperscriptYSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptXOffset") if v is None: v = 0 os2.ySuperscriptXOffset = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2SuperscriptYOffset") if v is None: v = 0 os2.ySuperscriptYOffset = _roundInt(v) # strikeout v = getAttrWithFallback(font.info, "openTypeOS2StrikeoutSize") if v is None: v = 0 os2.yStrikeoutSize = _roundInt(v) v = getAttrWithFallback(font.info, "openTypeOS2StrikeoutPosition") if v is None: v = 0 os2.yStrikeoutPosition = _roundInt(v) # family class os2.sFamilyClass = 0 # XXX not sure how to create the appropriate value # panose data = getAttrWithFallback(font.info, "openTypeOS2Panose") panose = Panose() panose.bFamilyType = data[0] panose.bSerifStyle = data[1] panose.bWeight = data[2] panose.bProportion = data[3] panose.bContrast = data[4] panose.bStrokeVariation = data[5] panose.bArmStyle = data[6] panose.bLetterForm = data[7] panose.bMidline = data[8] panose.bXHeight = data[9] os2.panose = panose # Unicode ranges uniRanges = getAttrWithFallback(font.info, "openTypeOS2UnicodeRanges") os2.ulUnicodeRange1 = intListToNum(uniRanges, 0, 32) os2.ulUnicodeRange2 = intListToNum(uniRanges, 32, 32) os2.ulUnicodeRange3 = intListToNum(uniRanges, 64, 32) os2.ulUnicodeRange4 = intListToNum(uniRanges, 96, 32) # codepage ranges codepageRanges = getAttrWithFallback(font.info, "openTypeOS2CodePageRanges") os2.ulCodePageRange1 = intListToNum(codepageRanges, 0, 32) os2.ulCodePageRange2 = intListToNum(codepageRanges, 32, 32) # vendor id os2.achVendID = str( getAttrWithFallback(font.info, "openTypeOS2VendorID").decode( "ascii", "ignore")) # vertical metrics os2.sxHeight = _roundInt(getAttrWithFallback(font.info, "xHeight")) os2.sCapHeight = _roundInt(getAttrWithFallback(font.info, "capHeight")) os2.sTypoAscender = _roundInt( getAttrWithFallback(font.info, "openTypeOS2TypoAscender")) os2.sTypoDescender = _roundInt( getAttrWithFallback(font.info, "openTypeOS2TypoDescender")) os2.sTypoLineGap = _roundInt( getAttrWithFallback(font.info, "openTypeOS2TypoLineGap")) os2.usWinAscent = _roundInt( getAttrWithFallback(font.info, "openTypeOS2WinAscent")) os2.usWinDescent = _roundInt( getAttrWithFallback(font.info, "openTypeOS2WinDescent")) # style mapping selection = list(getAttrWithFallback(font.info, "openTypeOS2Selection")) styleMapStyleName = getAttrWithFallback(font.info, "styleMapStyleName") if styleMapStyleName == "regular": selection.append(6) elif styleMapStyleName == "bold": selection.append(5) elif styleMapStyleName == "italic": selection.append(0) elif styleMapStyleName == "bold italic": selection += [0, 5] os2.fsSelection = intListToNum(selection, 0, 16) # characetr indexes unicodes = [ i for i in self.unicodeToGlyphNameMapping.keys() if i is not None ] if unicodes: minIndex = min(unicodes) maxIndex = max(unicodes) else: # the font may have *no* unicode values # (it really happens!) so there needs # to be a fallback. use space for this. minIndex = 0x0020 maxIndex = 0x0020 if maxIndex > 0xFFFF: # the spec says that 0xFFFF should be used # as the max if the max exceeds 0xFFFF maxIndex = 0xFFFF os2.fsFirstCharIndex = minIndex os2.fsLastCharIndex = maxIndex os2.usBreakChar = 32 os2.usDefaultChar = 0 # maximum contextual lookup length os2.usMaxContex = 0 def setupTable_hmtx(self): """ Make the hmtx table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["hmtx"] = hmtx = newTable("hmtx") hmtx.metrics = {} for glyphName, glyph in self.allGlyphs.items(): width = glyph.width left = 0 if len(glyph) or len(glyph.components): left = glyph.leftMargin if left is None: left = 0 hmtx[glyphName] = (_roundInt(width), _roundInt(left)) def setupTable_hhea(self): """ Make the hhea table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["hhea"] = hhea = newTable("hhea") font = self.ufo hhea.tableVersion = 1.0 # vertical metrics hhea.ascent = _roundInt( getAttrWithFallback(font.info, "openTypeHheaAscender")) hhea.descent = _roundInt( getAttrWithFallback(font.info, "openTypeHheaDescender")) hhea.lineGap = _roundInt( getAttrWithFallback(font.info, "openTypeHheaLineGap")) # horizontal metrics widths = [] lefts = [] rights = [] extents = [] for glyph in self.allGlyphs.values(): left = glyph.leftMargin right = glyph.rightMargin if left is None: left = 0 if right is None: right = 0 widths.append(glyph.width) lefts.append(left) rights.append(right) # robofab if hasattr(glyph, "box"): bounds = glyph.box # others else: bounds = glyph.bounds if bounds is not None: xMin, yMin, xMax, yMax = bounds else: xMin = 0 xMax = 0 extent = left + ( xMax - xMin ) # equation from spec for calculating xMaxExtent: Max(lsb + (xMax - xMin)) extents.append(extent) hhea.advanceWidthMax = _roundInt(max(widths)) hhea.minLeftSideBearing = _roundInt(min(lefts)) hhea.minRightSideBearing = _roundInt(min(rights)) hhea.xMaxExtent = _roundInt(max(extents)) # misc hhea.caretSlopeRise = getAttrWithFallback( font.info, "openTypeHheaCaretSlopeRise") hhea.caretSlopeRun = getAttrWithFallback(font.info, "openTypeHheaCaretSlopeRun") hhea.caretOffset = _roundInt( getAttrWithFallback(font.info, "openTypeHheaCaretOffset")) hhea.reserved0 = 0 hhea.reserved1 = 0 hhea.reserved2 = 0 hhea.reserved3 = 0 hhea.metricDataFormat = 0 # glyph count hhea.numberOfHMetrics = len(self.allGlyphs) def setupTable_post(self): """ Make the post table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["post"] = post = newTable("post") font = self.ufo post.formatType = 3.0 # italic angle italicAngle = getAttrWithFallback(font.info, "italicAngle") post.italicAngle = italicAngle # underline underlinePosition = getAttrWithFallback(font.info, "postscriptUnderlinePosition") if underlinePosition is None: underlinePosition = 0 post.underlinePosition = _roundInt(underlinePosition) underlineThickness = getAttrWithFallback( font.info, "postscriptUnderlineThickness") if underlineThickness is None: underlineThickness = 0 post.underlineThickness = _roundInt(underlineThickness) # determine if the font has a fixed width post.isFixedPitch = getAttrWithFallback(font.info, "postscriptIsFixedPitch") # misc post.minMemType42 = 0 post.maxMemType42 = 0 post.minMemType1 = 0 post.maxMemType1 = 0 def setupTable_CFF(self): """ Make the CFF table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["CFF "] = cff = newTable("CFF ") cff = cff.cff # set up the basics cff.major = 1 cff.minor = 0 cff.hdrSize = 4 cff.offSize = 4 cff.fontNames = [] strings = IndexedStrings() cff.strings = strings private = PrivateDict(strings=strings) private.rawDict.update(private.defaults) globalSubrs = GlobalSubrsIndex(private=private) topDict = TopDict(GlobalSubrs=globalSubrs, strings=strings) topDict.Private = private charStrings = topDict.CharStrings = CharStrings( file=None, charset=None, globalSubrs=globalSubrs, private=private, fdSelect=None, fdArray=None) charStrings.charStringsAreIndexed = True topDict.charset = [] charStringsIndex = charStrings.charStringsIndex = SubrsIndex( private=private, globalSubrs=globalSubrs) cff.topDictIndex = topDictIndex = TopDictIndex() topDictIndex.append(topDict) topDictIndex.strings = strings cff.GlobalSubrs = globalSubrs # populate naming data info = self.ufo.info psName = getAttrWithFallback(info, "postscriptFontName") cff.fontNames.append(psName) topDict = cff.topDictIndex[0] topDict.version = "%d.%d" % (getAttrWithFallback( info, "versionMajor"), getAttrWithFallback(info, "versionMinor")) trademark = getAttrWithFallback(info, "trademark") if trademark: trademark = normalizeStringForPostscript( trademark.replace(u"\u00A9", "Copyright")) if trademark != self.ufo.info.trademark: self.log.append( "[Warning] The trademark was normalized for storage in the CFF table and consequently some characters were dropped: '%s'" % trademark) if trademark is None: trademark = "" topDict.Notice = trademark copyright = getAttrWithFallback(info, "copyright") if copyright: copyright = normalizeStringForPostscript( copyright.replace(u"\u00A9", "Copyright")) if copyright != self.ufo.info.copyright: self.log.append( "[Warning] The copyright was normalized for storage in the CFF table and consequently some characters were dropped: '%s'" % copyright) if copyright is None: copyright = "" topDict.Copyright = copyright topDict.FullName = getAttrWithFallback(info, "postscriptFullName") topDict.FamilyName = getAttrWithFallback( info, "openTypeNamePreferredFamilyName") topDict.Weight = getAttrWithFallback(info, "postscriptWeightName") topDict.FontName = getAttrWithFallback(info, "postscriptFontName") # populate various numbers topDict.isFixedPitch = getAttrWithFallback(info, "postscriptIsFixedPitch") topDict.ItalicAngle = getAttrWithFallback(info, "italicAngle") underlinePosition = getAttrWithFallback(info, "postscriptUnderlinePosition") if underlinePosition is None: underlinePosition = 0 topDict.UnderlinePosition = _roundInt(underlinePosition) underlineThickness = getAttrWithFallback( info, "postscriptUnderlineThickness") if underlineThickness is None: underlineThickness = 0 topDict.UnderlineThickness = _roundInt(underlineThickness) # populate font matrix unitsPerEm = _roundInt(getAttrWithFallback(info, "unitsPerEm")) topDict.FontMatrix = [1.0 / unitsPerEm, 0, 0, 1.0 / unitsPerEm, 0, 0] # populate the width values defaultWidthX = _roundInt( getAttrWithFallback(info, "postscriptDefaultWidthX")) if defaultWidthX: private.rawDict["defaultWidthX"] = defaultWidthX nominalWidthX = _roundInt( getAttrWithFallback(info, "postscriptNominalWidthX")) if nominalWidthX: private.rawDict["nominalWidthX"] = nominalWidthX # populate hint data blueFuzz = _roundInt(getAttrWithFallback(info, "postscriptBlueFuzz")) blueShift = _roundInt(getAttrWithFallback(info, "postscriptBlueShift")) blueScale = getAttrWithFallback(info, "postscriptBlueScale") forceBold = getAttrWithFallback(info, "postscriptForceBold") blueValues = getAttrWithFallback(info, "postscriptBlueValues") if isinstance(blueValues, list): blueValues = [_roundInt(i) for i in blueValues] otherBlues = getAttrWithFallback(info, "postscriptOtherBlues") if isinstance(otherBlues, list): otherBlues = [_roundInt(i) for i in otherBlues] familyBlues = getAttrWithFallback(info, "postscriptFamilyBlues") if isinstance(familyBlues, list): familyBlues = [_roundInt(i) for i in familyBlues] familyOtherBlues = getAttrWithFallback(info, "postscriptFamilyOtherBlues") if isinstance(familyOtherBlues, list): familyOtherBlues = [_roundInt(i) for i in familyOtherBlues] stemSnapH = getAttrWithFallback(info, "postscriptStemSnapH") if isinstance(stemSnapH, list): stemSnapH = [_roundInt(i) for i in stemSnapH] stemSnapV = getAttrWithFallback(info, "postscriptStemSnapV") if isinstance(stemSnapV, list): stemSnapV = [_roundInt(i) for i in stemSnapV] # only write the blues data if some blues are defined. if (blueValues or otherBlues): private.rawDict["BlueFuzz"] = blueFuzz private.rawDict["BlueShift"] = blueShift private.rawDict["BlueScale"] = blueScale private.rawDict["ForceBold"] = forceBold private.rawDict["BlueValues"] = blueValues private.rawDict["OtherBlues"] = otherBlues private.rawDict["FamilyBlues"] = familyBlues private.rawDict["FamilyOtherBlues"] = familyOtherBlues # only write the stems if both are defined. if (stemSnapH and stemSnapV): private.rawDict["StemSnapH"] = stemSnapH private.rawDict["StdHW"] = stemSnapH[0] private.rawDict["StemSnapV"] = stemSnapV private.rawDict["StdVW"] = stemSnapV[0] # populate glyphs for glyphName in self.glyphOrder: glyph = self.allGlyphs[glyphName] unicodes = glyph.unicodes charString = self.getCharStringForGlyph(glyph, private, globalSubrs) # add to the font exists = charStrings.has_key(glyphName) if exists: # XXX a glyph already has this name. should we choke? glyphID = charStrings.charStrings[glyphName] charStringsIndex.items[glyphID] = charString else: charStringsIndex.append(charString) glyphID = len(topDict.charset) charStrings.charStrings[glyphName] = glyphID topDict.charset.append(glyphName) topDict.FontBBox = self.fontBoundingBox # write the glyph order self.otf.setGlyphOrder(self.glyphOrder) def setupOtherTables(self): """ Make the other tables. The default implementation does nothing. **This should not be called externally.** Subclasses may override this method to add other tables to the font if desired. """ pass
def makeLookup1(): # make a variation of the shell TTX data f = open(shellSourcePath) ttxData = f.read() f.close() ttxData = ttxData.replace("__familyName__", "gsubtest-lookup1") tempShellSourcePath = shellSourcePath + ".temp" f = open(tempShellSourcePath, "wb") f.write(ttxData) f.close() # compile the shell shell = TTFont(sfntVersion="OTTO") shell.importXML(tempShellSourcePath) shell.save(shellTempPath) os.remove(tempShellSourcePath) # load the shell shell = TTFont(shellTempPath) # grab the PASS and FAIL data hmtx = shell["hmtx"] glyphSet = shell.getGlyphSet() failGlyph = glyphSet["F"] failGlyph.decompile() failGlyphProgram = list(failGlyph.program) failGlyphMetrics = hmtx["F"] passGlyph = glyphSet["P"] passGlyph.decompile() passGlyphProgram = list(passGlyph.program) passGlyphMetrics = hmtx["P"] # grab some tables hmtx = shell["hmtx"] cmap = shell["cmap"] # start the glyph order existingGlyphs = [".notdef", "space", "F", "P"] glyphOrder = list(existingGlyphs) # start the CFF cff = shell["CFF "].cff globalSubrs = cff.GlobalSubrs topDict = cff.topDictIndex[0] topDict.charset = existingGlyphs private = topDict.Private charStrings = topDict.CharStrings charStringsIndex = charStrings.charStringsIndex features = sorted(mapping) # build the outline, hmtx and cmap data cp = baseCodepoint for index, tag in enumerate(features): # tag.pass glyphName = "{0!s}.pass".format(tag) glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=passGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings ) hmtx[glyphName] = passGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError, "Unsupported cmap table format: {0:d}".format(table.format) cp += 1 # tag.fail glyphName = "{0!s}.fail".format(tag) glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=failGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings ) hmtx[glyphName] = failGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError, "Unsupported cmap table format: {0:d}".format(table.format) # bump this up so that the sequence is the same as the lookup 3 font cp += 3 # set the glyph order shell.setGlyphOrder(glyphOrder) # start the GSUB shell["GSUB"] = newTable("GSUB") gsub = shell["GSUB"].table = GSUB() gsub.Version = 1.0 # make a list of all the features we will make featureCount = len(features) # set up the script list scriptList = gsub.ScriptList = ScriptList() scriptList.ScriptCount = 1 scriptList.ScriptRecord = [] scriptRecord = ScriptRecord() scriptList.ScriptRecord.append(scriptRecord) scriptRecord.ScriptTag = "DFLT" script = scriptRecord.Script = Script() defaultLangSys = script.DefaultLangSys = DefaultLangSys() defaultLangSys.FeatureCount = featureCount defaultLangSys.FeatureIndex = range(defaultLangSys.FeatureCount) defaultLangSys.ReqFeatureIndex = 65535 defaultLangSys.LookupOrder = None script.LangSysCount = 0 script.LangSysRecord = [] # set up the feature list featureList = gsub.FeatureList = FeatureList() featureList.FeatureCount = featureCount featureList.FeatureRecord = [] for index, tag in enumerate(features): # feature record featureRecord = FeatureRecord() featureRecord.FeatureTag = tag feature = featureRecord.Feature = Feature() featureList.FeatureRecord.append(featureRecord) # feature feature.FeatureParams = None feature.LookupCount = 1 feature.LookupListIndex = [index] # write the lookups lookupList = gsub.LookupList = LookupList() lookupList.LookupCount = featureCount lookupList.Lookup = [] for tag in features: # lookup lookup = Lookup() lookup.LookupType = 1 lookup.LookupFlag = 0 lookup.SubTableCount = 1 lookup.SubTable = [] lookupList.Lookup.append(lookup) # subtable subtable = SingleSubst() subtable.Format = 2 subtable.LookupType = 1 subtable.mapping = { "{0!s}.pass".format(tag) : "{0!s}.fail".format(tag), "{0!s}.fail".format(tag) : "{0!s}.pass".format(tag), } lookup.SubTable.append(subtable) path = outputPath % 1 + ".otf" if os.path.exists(path): os.remove(path) shell.save(path) # get rid of the shell if os.path.exists(shellTempPath): os.remove(shellTempPath)
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
class OTFPostProcessor(object): """Does some post-processing operations on a compiled OpenType font, using info from the source UFO where necessary. """ def __init__(self, otf, ufo): self.ufo = ufo stream = BytesIO() otf.save(stream) stream.seek(0) self.otf = TTFont(stream) def process(self, useProductionNames=True, optimizeCff=True): if useProductionNames: self._rename_glyphs_from_ufo() if optimizeCff and 'CFF ' in self.otf: from compreffor import Compreffor comp = Compreffor(self.otf) comp.compress() return self.otf def _rename_glyphs_from_ufo(self): """Rename glyphs using glif.lib.public.postscriptNames in UFO.""" rename_map = { g.name: self._build_production_name(g) for g in self.ufo} # .notdef may not be present in the original font rename_map[".notdef"] = ".notdef" rename = lambda names: [rename_map[n] for n in names] self.otf.setGlyphOrder(rename(self.otf.getGlyphOrder())) if 'CFF ' in self.otf: cff = self.otf['CFF '].cff.topDictIndex[0] char_strings = cff.CharStrings.charStrings cff.CharStrings.charStrings = { rename_map.get(n, n): v for n, v in char_strings.items()} cff.charset = rename(cff.charset) def _build_production_name(self, glyph): """Build a production name for a single glyph.""" # use name from Glyphs source if available production_name = glyph.lib.get('public.postscriptName') if production_name: return production_name # use name derived from unicode value unicode_val = glyph.unicode if glyph.unicode is not None: return '%s%04X' % ( 'u' if unicode_val > 0xffff else 'uni', unicode_val) # use production name + last (non-script) suffix if possible parts = glyph.name.rsplit('.', 1) if len(parts) == 2 and parts[0] in self.ufo: return '%s.%s' % ( self._build_production_name(self.ufo[parts[0]]), parts[1]) # use ligature name, making sure to look up components with suffixes parts = glyph.name.split('.', 1) if len(parts) == 2: liga_parts = ['%s.%s' % (n, parts[1]) for n in parts[0].split('_')] else: liga_parts = glyph.name.split('_') if len(liga_parts) > 1 and all(n in self.ufo for n in liga_parts): unicode_vals = [self.ufo[n].unicode for n in liga_parts] if all(v and v <= 0xffff for v in unicode_vals): return 'uni' + ''.join('%04X' % v for v in unicode_vals) return '_'.join( self._build_production_name(self.ufo[n]) for n in liga_parts) return glyph.name
def test_max_ctx_calc_no_features(): font = TTFont() assert maxCtxFont(font) == 0 font.setGlyphOrder(['.notdef']) addOpenTypeFeaturesFromString(font, '') assert maxCtxFont(font) == 0
class WOFF2Writer(SFNTWriter): flavor = "woff2" def __init__(self, file, numTables, sfntVersion="\000\001\000\000", flavor=None, flavorData=None): if not haveBrotli: print('The WOFF2 encoder requires the Brotli Python extension, available at:\n' 'https://github.com/google/brotli', file=sys.stderr) raise ImportError("No module named brotli") self.file = file self.numTables = numTables self.sfntVersion = Tag(sfntVersion) self.flavorData = flavorData or WOFF2FlavorData() self.directoryFormat = woff2DirectoryFormat self.directorySize = woff2DirectorySize self.DirectoryEntry = WOFF2DirectoryEntry self.signature = Tag("wOF2") self.nextTableOffset = 0 self.transformBuffer = BytesIO() self.tables = OrderedDict() # make empty TTFont to store data while normalising and transforming tables self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) def __setitem__(self, tag, data): """Associate new entry named 'tag' with raw table data.""" if tag in self.tables: raise TTLibError("cannot rewrite '%s' table" % tag) if tag == 'DSIG': # always drop DSIG table, since the encoding process can invalidate it self.numTables -= 1 return entry = self.DirectoryEntry() entry.tag = Tag(tag) entry.flags = getKnownTagIndex(entry.tag) # WOFF2 table data are written to disk only on close(), after all tags # have been specified entry.data = data self.tables[tag] = entry def close(self): """ All tags must have been specified. Now write the table data and directory. """ if len(self.tables) != self.numTables: raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(self.tables))) if self.sfntVersion in ("\x00\x01\x00\x00", "true"): isTrueType = True elif self.sfntVersion == "OTTO": isTrueType = False else: raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned. # However, the reference WOFF2 implementation still fails to reconstruct # 'unpadded' glyf tables, therefore we need to 'normalise' them. # See: # https://github.com/khaledhosny/ots/issues/60 # https://github.com/google/woff2/issues/15 if isTrueType: self._normaliseGlyfAndLoca(padding=4) self._setHeadTransformFlag() # To pass the legacy OpenType Sanitiser currently included in browsers, # we must sort the table directory and data alphabetically by tag. # See: # https://github.com/google/woff2/pull/3 # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html # TODO(user): remove to match spec once browsers are on newer OTS self.tables = OrderedDict(sorted(self.tables.items())) self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() fontData = self._transformTables() compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT) self.totalCompressedSize = len(compressedFont) self.length = self._calcTotalSize() self.majorVersion, self.minorVersion = self._getVersion() self.reserved = 0 directory = self._packTableDirectory() self.file.seek(0) self.file.write(pad(directory + compressedFont, size=4)) self._writeFlavorData() def _normaliseGlyfAndLoca(self, padding=4): """ Recompile glyf and loca tables, aligning glyph offsets to multiples of 'padding' size. Update the head table's 'indexToLocFormat' accordingly while compiling loca. """ if self.sfntVersion == "OTTO": return # make up glyph names required to decompile glyf table self._decompileTable('maxp') numGlyphs = self.ttFont['maxp'].numGlyphs glyphOrder = ['.notdef'] + ["glyph%.5d" % i for i in range(1, numGlyphs)] self.ttFont.setGlyphOrder(glyphOrder) for tag in ('head', 'loca', 'glyf'): self._decompileTable(tag) self.ttFont['glyf'].padding = padding for tag in ('glyf', 'loca'): self._compileTable(tag) def _setHeadTransformFlag(self): """ Set bit 11 of 'head' table flags to indicate that the font has undergone a lossless modifying transform. Re-compile head table data.""" self._decompileTable('head') self.ttFont['head'].flags |= (1 << 11) self._compileTable('head') def _decompileTable(self, tag): """ Fetch table data, decompile it, and store it inside self.ttFont. """ tag = Tag(tag) if tag not in self.tables: raise TTLibError("missing required table: %s" % tag) if self.ttFont.isLoaded(tag): return data = self.tables[tag].data if tag == 'loca': tableClass = WOFF2LocaTable elif tag == 'glyf': tableClass = WOFF2GlyfTable else: tableClass = getTableClass(tag) table = tableClass(tag) self.ttFont.tables[tag] = table table.decompile(data, self.ttFont) def _compileTable(self, tag): """ Compile table and store it in its 'data' attribute. """ self.tables[tag].data = self.ttFont[tag].compile(self.ttFont) def _calcSFNTChecksumsLengthsAndOffsets(self): """ Compute the 'original' SFNT checksums, lengths and offsets for checksum adjustment calculation. Return the total size of the uncompressed font. """ offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables) for tag, entry in self.tables.items(): data = entry.data entry.origOffset = offset entry.origLength = len(data) if tag == 'head': entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) else: entry.checkSum = calcChecksum(data) offset += (entry.origLength + 3) & ~3 return offset def _transformTables(self): """Return transformed font data.""" for tag, entry in self.tables.items(): if tag in woff2TransformedTableTags: data = self.transformTable(tag) else: data = entry.data entry.offset = self.nextTableOffset entry.saveData(self.transformBuffer, data) self.nextTableOffset += entry.length self.writeMasterChecksum() fontData = self.transformBuffer.getvalue() return fontData def transformTable(self, tag): """Return transformed table data.""" if tag not in woff2TransformedTableTags: raise TTLibError("Transform for table '%s' is unknown" % tag) if tag == "loca": data = b"" elif tag == "glyf": for tag in ('maxp', 'head', 'loca', 'glyf'): self._decompileTable(tag) glyfTable = self.ttFont['glyf'] data = glyfTable.transform(self.ttFont) else: raise NotImplementedError return data def _calcMasterChecksum(self): """Calculate checkSumAdjustment.""" tags = list(self.tables.keys()) checksums = [] for i in range(len(tags)): checksums.append(self.tables[tags[i]].checkSum) # Create a SFNT directory for checksum calculation purposes self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16) directory = sstruct.pack(sfntDirectoryFormat, self) tables = sorted(self.tables.items()) for tag, entry in tables: sfntEntry = SFNTDirectoryEntry() sfntEntry.tag = entry.tag sfntEntry.checkSum = entry.checkSum sfntEntry.offset = entry.origOffset sfntEntry.length = entry.origLength directory = directory + sfntEntry.toString() directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize assert directory_end == len(directory) checksums.append(calcChecksum(directory)) checksum = sum(checksums) & 0xffffffff # BiboAfba! checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff return checksumadjustment def writeMasterChecksum(self): """Write checkSumAdjustment to the transformBuffer.""" checksumadjustment = self._calcMasterChecksum() self.transformBuffer.seek(self.tables['head'].offset + 8) self.transformBuffer.write(struct.pack(">L", checksumadjustment)) def _calcTotalSize(self): """Calculate total size of WOFF2 font, including any meta- and/or private data.""" offset = self.directorySize for entry in self.tables.values(): offset += len(entry.toString()) offset += self.totalCompressedSize offset = (offset + 3) & ~3 offset = self._calcFlavorDataOffsetsAndSize(offset) return offset def _calcFlavorDataOffsetsAndSize(self, start): """Calculate offsets and lengths for any meta- and/or private data.""" offset = start data = self.flavorData if data.metaData: self.metaOrigLength = len(data.metaData) self.metaOffset = offset self.compressedMetaData = brotli.compress( data.metaData, mode=brotli.MODE_TEXT) self.metaLength = len(self.compressedMetaData) offset += self.metaLength else: self.metaOffset = self.metaLength = self.metaOrigLength = 0 self.compressedMetaData = b"" if data.privData: # make sure private data is padded to 4-byte boundary offset = (offset + 3) & ~3 self.privOffset = offset self.privLength = len(data.privData) offset += self.privLength else: self.privOffset = self.privLength = 0 return offset def _getVersion(self): """Return the WOFF2 font's (majorVersion, minorVersion) tuple.""" data = self.flavorData if data.majorVersion is not None and data.minorVersion is not None: return data.majorVersion, data.minorVersion else: # if None, return 'fontRevision' from 'head' table if 'head' in self.tables: return struct.unpack(">HH", self.tables['head'].data[4:8]) else: return 0, 0 def _packTableDirectory(self): """Return WOFF2 table directory data.""" directory = sstruct.pack(self.directoryFormat, self) for entry in self.tables.values(): directory = directory + entry.toString() return directory def _writeFlavorData(self): """Write metadata and/or private data using appropiate padding.""" compressedMetaData = self.compressedMetaData privData = self.flavorData.privData if compressedMetaData and privData: compressedMetaData = pad(compressedMetaData, size=4) if compressedMetaData: self.file.seek(self.metaOffset) assert self.file.tell() == self.metaOffset self.file.write(compressedMetaData) if privData: self.file.seek(self.privOffset) assert self.file.tell() == self.privOffset self.file.write(privData) def reordersTables(self): return True
def create_font(celf): font = TTFont() font.setGlyphOrder(celf.GLYPH_ORDER) return font
fonts.append(TTFont(sys.argv[index], lazy=False)) if fonts[0].getGlyphOrder() != fonts[-1].getGlyphOrder(): glyphs1 = fonts[0].getGlyphOrder() glyphs2 = fonts[-1].getGlyphOrder() missing = [glyph for glyph in glyphs1 if glyph not in glyphs2] extra = [glyph for glyph in glyphs2 if glyph not in glyphs1] print("all fonts must have the same glyph order!") print("missing from {}:".format(sys.argv[index])) print(missing) print("extra from {}:".format(sys.argv[index])) print(extra) sys.exit(1) out_font = TTFont() out_font.setGlyphOrder(fonts[0].getGlyphOrder()) # Most tables are just copied from the first input file # TODO: em values and PPEM will vary, does this matter? for tag in ("head", "hhea", "maxp", "OS/2", "hmtx", "cmap", "name", "post"): out_font[tag] = fonts[0][tag] out_font["EBLC"] = eblc = table_E_B_L_C_() out_font["EBDT"] = ebdt = table_E_B_D_T_() eblc.version = fonts[0]["EBLC"].version ebdt.version = fonts[0]["EBDT"].version eblc.strikes = [] ebdt.strikeData = []
def makeLookup1(): # make a variation of the shell TTX data f = open(shellSourcePath) ttxData = f.read() f.close() ttxData = ttxData.replace("__familyName__", "gsubtest-lookup1") tempShellSourcePath = shellSourcePath + ".temp" f = open(tempShellSourcePath, "wb") f.write(ttxData) f.close() # compile the shell shell = TTFont(sfntVersion="OTTO") shell.importXML(tempShellSourcePath) shell.save(shellTempPath) os.remove(tempShellSourcePath) # load the shell shell = TTFont(shellTempPath) # grab the PASS and FAIL data hmtx = shell["hmtx"] glyphSet = shell.getGlyphSet() failGlyph = glyphSet["F"] failGlyph.decompile() failGlyphProgram = list(failGlyph.program) failGlyphMetrics = hmtx["F"] passGlyph = glyphSet["P"] passGlyph.decompile() passGlyphProgram = list(passGlyph.program) passGlyphMetrics = hmtx["P"] # grab some tables hmtx = shell["hmtx"] cmap = shell["cmap"] # start the glyph order existingGlyphs = [".notdef", "space", "F", "P"] glyphOrder = list(existingGlyphs) # start the CFF cff = shell["CFF "].cff globalSubrs = cff.GlobalSubrs topDict = cff.topDictIndex[0] topDict.charset = existingGlyphs private = topDict.Private charStrings = topDict.CharStrings charStringsIndex = charStrings.charStringsIndex features = sorted(mapping) # build the outline, hmtx and cmap data cp = baseCodepoint for index, tag in enumerate(features): # tag.pass glyphName = "%s.pass" % tag glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=passGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings, ) hmtx[glyphName] = passGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError("Unsupported cmap table format: %d" % table.format) cp += 1 # tag.fail glyphName = "%s.fail" % tag glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=failGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings, ) hmtx[glyphName] = failGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError("Unsupported cmap table format: %d" % table.format) # bump this up so that the sequence is the same as the lookup 3 font cp += 3 # set the glyph order shell.setGlyphOrder(glyphOrder) # start the GSUB shell["GSUB"] = newTable("GSUB") gsub = shell["GSUB"].table = GSUB() gsub.Version = 1.0 # make a list of all the features we will make featureCount = len(features) # set up the script list scriptList = gsub.ScriptList = ScriptList() scriptList.ScriptCount = 1 scriptList.ScriptRecord = [] scriptRecord = ScriptRecord() scriptList.ScriptRecord.append(scriptRecord) scriptRecord.ScriptTag = "DFLT" script = scriptRecord.Script = Script() defaultLangSys = script.DefaultLangSys = DefaultLangSys() defaultLangSys.FeatureCount = featureCount defaultLangSys.FeatureIndex = range(defaultLangSys.FeatureCount) defaultLangSys.ReqFeatureIndex = 65535 defaultLangSys.LookupOrder = None script.LangSysCount = 0 script.LangSysRecord = [] # set up the feature list featureList = gsub.FeatureList = FeatureList() featureList.FeatureCount = featureCount featureList.FeatureRecord = [] for index, tag in enumerate(features): # feature record featureRecord = FeatureRecord() featureRecord.FeatureTag = tag feature = featureRecord.Feature = Feature() featureList.FeatureRecord.append(featureRecord) # feature feature.FeatureParams = None feature.LookupCount = 1 feature.LookupListIndex = [index] # write the lookups lookupList = gsub.LookupList = LookupList() lookupList.LookupCount = featureCount lookupList.Lookup = [] for tag in features: # lookup lookup = Lookup() lookup.LookupType = 1 lookup.LookupFlag = 0 lookup.SubTableCount = 1 lookup.SubTable = [] lookupList.Lookup.append(lookup) # subtable subtable = SingleSubst() subtable.Format = 2 subtable.LookupType = 1 subtable.mapping = { "%s.pass" % tag: "%s.fail" % tag, "%s.fail" % tag: "%s.pass" % tag, } lookup.SubTable.append(subtable) path = outputPath % 1 + ".otf" if os.path.exists(path): os.remove(path) shell.save(path) # get rid of the shell if os.path.exists(shellTempPath): os.remove(shellTempPath)
def __init__(self, ufo): self.ufo = ufo otf = TTFont() otf.setGlyphOrder(ufo.glyphOrder) super(CompositorUFOFont, self).__init__(otf)