diff --git a/Lib/fontTools/misc/xmlReader.py b/Lib/fontTools/misc/xmlReader.py index a8931bb10..438549d7d 100644 --- a/Lib/fontTools/misc/xmlReader.py +++ b/Lib/fontTools/misc/xmlReader.py @@ -17,7 +17,8 @@ BUFSIZE = 0x4000 class XMLReader(object): - def __init__(self, fileOrPath, ttFont, progress=None, quiet=None): + def __init__(self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False): + if fileOrPath == '-': fileOrPath = sys.stdin if not hasattr(fileOrPath, "read"): @@ -35,6 +36,7 @@ class XMLReader(object): self.quiet = quiet self.root = None self.contentStack = [] + self.contentOnly = contentOnly self.stackSize = 0 def read(self, rootless=False): @@ -73,8 +75,24 @@ class XMLReader(object): parser.Parse(chunk, 0) def _startElementHandler(self, name, attrs): + if self.stackSize == 1 and self.contentOnly: + # We already know the table we're parsing, skip + # parsing the table tag and continue to + # stack '2' which begins parsing content + self.contentStack.append([]) + self.stackSize = 2 + return stackSize = self.stackSize self.stackSize = stackSize + 1 + subFile = attrs.get("src") + if subFile is not None: + if hasattr(self.file, 'name'): + # if file has a name, get its parent directory + dirname = os.path.dirname(self.file.name) + else: + # else fall back to using the current working directory + dirname = os.getcwd() + subFile = os.path.join(dirname, subFile) if not stackSize: if name != "ttFont": raise TTXParseError("illegal root tag: %s" % name) @@ -85,15 +103,7 @@ class XMLReader(object): self.ttFont.sfntVersion = sfntVersion self.contentStack.append([]) elif stackSize == 1: - subFile = attrs.get("src") if subFile is not None: - if hasattr(self.file, 'name'): - # if file has a name, get its parent directory - dirname = os.path.dirname(self.file.name) - else: - # else fall back to using the current working directory - dirname = os.getcwd() - subFile = os.path.join(dirname, subFile) subReader = XMLReader(subFile, self.ttFont, self.progress) subReader.read() self.contentStack.append([]) @@ -119,6 +129,11 @@ class XMLReader(object): self.currentTable = tableClass(tag) self.ttFont[tag] = self.currentTable self.contentStack.append([]) + elif stackSize == 2 and subFile is not None: + subReader = XMLReader(subFile, self.ttFont, self.progress, contentOnly=True) + subReader.read() + self.contentStack.append([]) + self.root = subReader.root elif stackSize == 2: self.contentStack.append([]) self.root = (name, attrs, self.contentStack[-1]) @@ -134,12 +149,13 @@ class XMLReader(object): def _endElementHandler(self, name): self.stackSize = self.stackSize - 1 del self.contentStack[-1] - if self.stackSize == 1: - self.root = None - elif self.stackSize == 2: - name, attrs, content = self.root - self.currentTable.fromXML(name, attrs, content, self.ttFont) - self.root = None + if not self.contentOnly: + if self.stackSize == 1: + self.root = None + elif self.stackSize == 2: + name, attrs, content = self.root + self.currentTable.fromXML(name, attrs, content, self.ttFont) + self.root = None class ProgressPrinter(object): diff --git a/Lib/fontTools/ttLib/__init__.py b/Lib/fontTools/ttLib/__init__.py index a9db4bba0..100903f9a 100644 --- a/Lib/fontTools/ttLib/__init__.py +++ b/Lib/fontTools/ttLib/__init__.py @@ -247,7 +247,8 @@ class TTFont(object): def saveXML(self, fileOrPath, progress=None, quiet=None, tables=None, skipTables=None, splitTables=False, disassembleInstructions=True, - bitmapGlyphDataFormat='raw', newlinestr=None): + splitGlyphs=False, bitmapGlyphDataFormat='raw', newlinestr=None): + """Export the font as TTX (an XML-based text file), or as a series of text files when splitTables is true. In the latter case, the 'fileOrPath' argument should be a path to a directory. @@ -290,7 +291,7 @@ class TTFont(object): if not splitTables: writer.newline() - else: + if splitTables or splitGlyphs: # 'fileOrPath' must now be a path path, ext = os.path.splitext(fileOrPath) fileNameTemplate = path + ".%s" + ext @@ -299,8 +300,11 @@ class TTFont(object): if progress: progress.set(i) tag = tables[i] - if splitTables: + if splitTables or (splitGlyphs and tag == 'glyf'): tablePath = fileNameTemplate % tagToIdentifier(tag) + else: + tablePath = None + if splitTables: tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc, newlinestr=newlinestr) tableWriter.begintag("ttFont", ttLibVersion=version) @@ -310,7 +314,7 @@ class TTFont(object): writer.newline() else: tableWriter = writer - self._tableToXML(tableWriter, tag, progress) + self._tableToXML(tableWriter, tag, progress, splitGlyphs=splitGlyphs) if splitTables: tableWriter.endtag("ttFont") tableWriter.newline() @@ -324,7 +328,7 @@ class TTFont(object): if not hasattr(fileOrPath, "write") and fileOrPath != "-": writer.close() - def _tableToXML(self, writer, tag, progress, quiet=None): + def _tableToXML(self, writer, tag, progress, splitGlyphs=False, quiet=None): if quiet is not None: deprecateArgument("quiet", "configure logging instead") if tag in self: @@ -346,7 +350,9 @@ class TTFont(object): attrs['raw'] = True writer.begintag(xmlTag, **attrs) writer.newline() - if tag in ("glyf", "CFF "): + if tag == "glyf": + table.toXML(writer, self, progress, splitGlyphs) + elif tag == "CFF ": table.toXML(writer, self, progress) else: table.toXML(writer, self) @@ -873,8 +879,8 @@ def _escapechar(c): return hex(byteord(c))[2:] -def tagToIdentifier(tag): - """Convert a table tag to a valid (but UGLY) python identifier, +def nameToIdentifier(name): + """Convert a name to a valid (but UGLY) python identifier, as well as a filename that's guaranteed to be unique even on a caseless file system. Each character is mapped to two characters. Lowercase letters get an underscore before the letter, uppercase @@ -887,19 +893,25 @@ def tagToIdentifier(tag): 'OS/2' -> 'O_S_2f_2' """ import re - tag = Tag(tag) - if tag == "GlyphOrder": - return tag - assert len(tag) == 4, "tag should be 4 characters long" - while len(tag) > 1 and tag[-1] == ' ': - tag = tag[:-1] + while len(name) > 1 and name[-1] == ' ': + name = name[:-1] ident = "" - for c in tag: + for c in name: ident = ident + _escapechar(c) if re.match("[0-9]", ident): ident = "_" + ident return ident +def tagToIdentifier(tag): + """This performs the same conversion which nameToIdentifiier does + with the additional assertion that the source tag is 4 characters + long which is criteria for a valid tag name. + """ + if tag == "GlyphOrder": + return tag + ret = nameToIdentifier(tag) + assert len(tag) == 4, "tag should be 4 characters long" + return ret def identifierToTag(ident): """the opposite of tagToIdentifier()""" diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 52117129f..966e77ea2 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -5,6 +5,7 @@ from collections import namedtuple from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools import ttLib +from fontTools import version from fontTools.misc.textTools import safeEval, pad from fontTools.misc.arrayTools import calcBounds, calcIntBounds, pointInRect from fontTools.misc.bezierTools import calcQuadraticBounds @@ -16,10 +17,17 @@ import sys import struct import array import logging - +import os +from fontTools.misc import xmlWriter +from fontTools.ttLib import nameToIdentifier log = logging.getLogger(__name__) +# We compute the version the same as is computed in ttlib/__init__ +# so that we can write 'ttLibVersion' attribute of the glyf TTX files +# when glyf is written to separate files. +version = ".".join(version.split('.')[:2]) + # # The Apple and MS rasterizers behave differently for # scaled composite components: one does scale first and then translate @@ -110,7 +118,8 @@ class table__g_l_y_f(DefaultTable.DefaultTable): ttFont['maxp'].numGlyphs = len(self.glyphs) return data - def toXML(self, writer, ttFont, progress=None): + def toXML(self, writer, ttFont, progress=None, splitGlyphs=False): + writer.newline() glyphNames = ttFont.getGlyphNames() writer.comment("The xMin, yMin, xMax and yMax values\nwill be recalculated by the compiler.") @@ -126,17 +135,39 @@ class table__g_l_y_f(DefaultTable.DefaultTable): counter = counter + 1 glyph = self[glyphName] if glyph.numberOfContours: - writer.begintag('TTGlyph', [ - ("name", glyphName), - ("xMin", glyph.xMin), - ("yMin", glyph.yMin), - ("xMax", glyph.xMax), - ("yMax", glyph.yMax), - ]) - writer.newline() - glyph.toXML(writer, ttFont) - writer.endtag('TTGlyph') - writer.newline() + if splitGlyphs: + path, ext = os.path.splitext(writer.file.name) + fileNameTemplate = path + ".%s" + ext + glyphPath = fileNameTemplate % nameToIdentifier(glyphName) + glyphWriter = xmlWriter.XMLWriter(glyphPath, idlefunc=writer.idlefunc, + newlinestr=writer.newlinestr) + glyphWriter.begintag("ttFont", ttLibVersion=version) + glyphWriter.newline() + glyphWriter.newline() + glyphWriter.begintag("glyf") + glyphWriter.newline() + glyphWriter.newline() + writer.simpletag("TTGlyph", src=os.path.basename(glyphPath)) + writer.newline() + else: + glyphWriter = writer + glyphWriter.begintag('TTGlyph', [ + ("name", glyphName), + ("xMin", glyph.xMin), + ("yMin", glyph.yMin), + ("xMax", glyph.xMax), + ("yMax", glyph.yMax), + ]) + glyphWriter.newline() + glyph.toXML(glyphWriter, ttFont) + glyphWriter.endtag('TTGlyph') + glyphWriter.newline() + if splitGlyphs: + glyphWriter.endtag("glyf") + glyphWriter.newline() + glyphWriter.endtag("ttFont") + glyphWriter.newline() + glyphWriter.close() else: writer.simpletag('TTGlyph', name=glyphName) writer.comment("contains no outline data") diff --git a/Lib/fontTools/ttx.py b/Lib/fontTools/ttx.py index 002215ce5..6c7fca8c4 100644 --- a/Lib/fontTools/ttx.py +++ b/Lib/fontTools/ttx.py @@ -38,6 +38,9 @@ usage: ttx [options] inputfile1 [... inputfileN] to the individual table dumps. This file can be used as input to ttx, as long as the table files are in the same directory. + -g Split glyf table: Save the glyf data into separate TTX files + per glyph and write a small TTX for the glyf table which + contains references to the individual TTGlyph elements. -i Do NOT disassemble TT instructions: when this option is given, all TrueType programs (glyph programs, the font program and the pre-program) will be written to the TTX file as hex data @@ -110,6 +113,7 @@ class Options(object): verbose = False quiet = False splitTables = False + splitGlyphs = False disassembleInstructions = True mergeFile = None recalcBBoxes = True @@ -160,6 +164,8 @@ class Options(object): self.skipTables.append(value) elif option == "-s": self.splitTables = True + elif option == "-g": + self.splitGlyphs = True elif option == "-i": self.disassembleInstructions = False elif option == "-z": @@ -255,6 +261,7 @@ def ttDump(input, output, options): tables=options.onlyTables, skipTables=options.skipTables, splitTables=options.splitTables, + splitGlyphs=options.splitGlyphs, disassembleInstructions=options.disassembleInstructions, bitmapGlyphDataFormat=options.bitmapGlyphDataFormat, newlinestr=options.newlinestr) @@ -318,7 +325,7 @@ def guessFileType(fileName): def parseOptions(args): - rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sim:z:baey:", + rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sgim:z:baey:", ['unicodedata=', "recalc-timestamp", 'flavor=', 'version', 'with-zopfli', 'newline=']) diff --git a/Tests/misc/xmlReader_test.py b/Tests/misc/xmlReader_test.py index 622abf5cd..93afd8956 100644 --- a/Tests/misc/xmlReader_test.py +++ b/Tests/misc/xmlReader_test.py @@ -142,7 +142,48 @@ class TestXMLReader(unittest.TestCase): self.assertTrue(reader.file.closed) os.remove(tmp.name) + def test_read_sub_file(self): + # Verifies that sub-file content is able to be read to a table. + expectedContent = u'testContent' + expectedNameID = '1' + expectedPlatform = '3' + expectedLangId = '0x409' + with tempfile.NamedTemporaryFile(delete=False) as tmp: + + subFileData = ( + '' + '' + '' + '%s' + '' + '' + '' + )%(expectedNameID, expectedPlatform, expectedLangId, expectedContent) + tmp.write(subFileData.encode("utf-8")) + with tempfile.NamedTemporaryFile(delete=False) as tmp2: + fileData = ( + '' + '' + '' + '' + '' + )%(tmp.name) + tmp2.write(fileData.encode('utf-8')) + + ttf = TTFont() + with open(tmp2.name, "rb") as f: + reader = XMLReader(f, ttf) + reader.read() + reader.close() + nameTable = ttf['name'] + self.assertTrue(int(expectedNameID) == nameTable.names[0].nameID) + self.assertTrue(int(expectedLangId, 16) == nameTable.names[0].langID) + self.assertTrue(int(expectedPlatform) == nameTable.names[0].platformID) + self.assertEqual(expectedContent, nameTable.names[0].string.decode(nameTable.names[0].getEncoding())) + + os.remove(tmp.name) + os.remove(tmp2.name) if __name__ == '__main__': import sys