font.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. from lxml.etree import Element, tostring
  2. from math import log2, floor
  3. from transform.bytes import calculateTableChecksum, generateOffsets
  4. import struct
  5. import log
  6. from format import formats
  7. from data import Tag
  8. import tables.tableRecord
  9. import tables.glyphOrder
  10. import tables.head
  11. import tables.os2
  12. import tables.post
  13. import tables.name
  14. import tables.maxp
  15. import tables.gasp
  16. import tables.loca
  17. import tables.dsig
  18. import tables.hhea
  19. import tables.hmtx
  20. import tables.vhea
  21. import tables.vmtx
  22. import tables.cmap
  23. # import tables.gdef
  24. # import tables.gpos
  25. import tables.gsub
  26. # import tables.morx
  27. import tables.glyf
  28. import tables.svg
  29. import tables.sbix
  30. import tables.cbdt
  31. import tables.cblc
  32. class TTFont:
  33. """
  34. Class representing a TrueType/OpenType font.
  35. """
  36. def __init__(self, chosenFormat, m, glyphs, flags):
  37. """
  38. Covers the entire routine for assembling a TrueType/OpenType font with forc input data.
  39. """
  40. glyphFormat = formats[chosenFormat]["imageTables"]
  41. self.tables = {}
  42. try:
  43. # not actually tables
  44. # ---------------------------------------------
  45. self.glyphOrder = tables.glyphOrder.GlyphOrder(glyphs)
  46. # headers and other weird crap
  47. # ---------------------------------------------
  48. log.out('[head] ', 90, newline=False)
  49. self.tables["head"] = tables.head.head(m)
  50. log.out('[OS/2] ', 90, newline=False)
  51. self.tables["OS/2"] = tables.os2.OS2(m, glyphs)
  52. #log.out('[post] ', 90, newline=False)
  53. #self.tables.append(tables.post.post(glyphs))
  54. # maxp is a semi-placeholder table.
  55. log.out('[maxp] ', 90, newline=False)
  56. self.tables["maxp"] = tables.maxp.maxp(glyphs)
  57. log.out('[gasp] ', 90, newline=False)
  58. self.tables["gasp"] = tables.gasp.gasp()
  59. # loca is a placeholder to make macOS happy.
  60. #
  61. # CBDT/CBLC either doesn't use loca or TTX doesn't want
  62. # an empty loca table if there's no glyf table (CBDT/CBLC
  63. # fonts shouldnt have glyf tables.)
  64. if glyphFormat is not "CBx":
  65. log.out('[loca] ', 36, newline=False)
  66. self.tables["loca"] = tables.loca.loca()
  67. # placeholder table that makes Google's font validation happy.
  68. log.out('[DSIG]', 90)
  69. self.tables["DSIG"] = tables.dsig.DSIG()
  70. # horizontal and vertical metrics tables
  71. # ---------------------------------------------
  72. log.out('[hhea] ', 90, newline=False)
  73. self.tables["hhea"] = tables.hhea.hhea(m)
  74. log.out('[hmtx] ', 90, newline=False)
  75. self.tables["hmtx"] = tables.hmtx.hmtx(m, glyphs)
  76. log.out('[vhea] ', 90, newline=False)
  77. self.tables["vhea"] = tables.vhea.vhea(m)
  78. log.out('[vmtx]', 90)
  79. self.tables["vmtx"] = tables.vmtx.vmtx(m, glyphs)
  80. # glyph-code mappings
  81. # ---------------------------------------------
  82. # single glyphs
  83. log.out('[cmap] ', 90, newline=False)
  84. self.tables["cmap"] = tables.cmap.cmap(glyphs, flags["no_vs16"])
  85. # ligatures
  86. ligatures = False
  87. # check for presence of ligatures
  88. for g in glyphs:
  89. if len(g) > 1:
  90. ligatures = True
  91. if ligatures:
  92. if formats[chosenFormat]["ligatureFormat"] == "OpenType":
  93. log.out('[GSUB] ', 36, newline=False)
  94. self.tables["GSUB"] = tables.gsub.GSUB(glyphs)
  95. # glyf
  96. # ---------------------------------------------
  97. # glyf is used as a placeholde to please font validation,
  98. # table dependencies and the TTX compiler.
  99. #
  100. # CBDT/CBLC doesn't use glyf at all
  101. if glyphFormat is not "CBx":
  102. log.out('[glyf] ', 36, newline=False)
  103. self.tables["glyf"] = tables.glyf.glyf(m, glyphs)
  104. # actual glyph picture data
  105. # ---------------------------------------------
  106. if glyphFormat == "SVG":
  107. log.out('[SVG ]', 36)
  108. self.tables["SVG "] = tables.svg.SVG(m, glyphs)
  109. elif glyphFormat == "sbix":
  110. log.out('[sbix]', 36)
  111. self.tables["sbix"] = tables.sbix.sbix(glyphs)
  112. elif glyphFormat == "CBx":
  113. log.out('[CBLC] ', 36, newline=False)
  114. self.tables["CBLC"] = tables.cblc.CBLC(m, glyphs)
  115. log.out('[CBDT]', 36)
  116. self.tables["CBDT"] = tables.cbdt.CBDT(m, glyphs)
  117. # human-readable metadata
  118. # ---------------------------------------------
  119. log.out('[name]', 90)
  120. self.tables["name"] = tables.name.name(chosenFormat, m)
  121. except ValueError as e:
  122. ValueError(f"Something went wrong with building the font class. -> {e}")
  123. def test(self):
  124. """
  125. A series of tests determining the validity of the font, checking certain
  126. variables between font tables that must agree with each other (as opposed to
  127. checking issues that exist solely within a certain table).
  128. """
  129. # certain bits in head.macStyle and OS/2.fsSelection must agree with each other.
  130. # ------------------------------------------------------------------------------
  131. macStyleBold = self.tables["head"].macStyle.toList()[0]
  132. fsSelectionBold = self.tables["OS/2"].fsSelection.toList()[5]
  133. macStyleItalic = self.tables["head"].macStyle.toList()[1]
  134. fsSelectionItalic = self.tables["OS/2"].fsSelection.toList()[0]
  135. if macStyleBold != fsSelectionBold:
  136. log.out(f"💢 The Bold bit in head.macStyle (bit 0: {macStyleBold}) does not agree with the Bold bit in OS/2.fsSelection (bit 5: {fsSelectionBold})", 91)
  137. if macStyleItalic != fsSelectionItalic:
  138. log.out(f"💢 The Italic bit in head.macStyle (bit 1: {macStyleItalic}) does not agree with the Italic bit in OS/2.fsSelection (bit 0: {fsSelectionItalic})", 91)
  139. # number of glyphs in an sbix strike must be equal to maxp.numGlyphs.
  140. # ------------------------------------------------------------------------------
  141. maxpNumGlyphs = self.tables["maxp"].numGlyphs
  142. if "sbix" in self.tables:
  143. strikes = self.tables["sbix"].strikes
  144. for num, s in enumerate(strikes):
  145. if len(s.bitmaps) != self.tables["maxp"].numGlyphs:
  146. log.out(f"💢 the number of bitmaps inside sbix strike index {num} (ppem: {s.ppem}, ppi: {s.ppi}) doesn't match maxp.numGlyphs. (sbix strike: {len(s.bitmaps)}, maxp.numGlyphs: {self.tables['maxp'].numGlyphs}).", 91)
  147. def toTTX(self, asString=False):
  148. """
  149. Compiles font class to a TTX-formatted string.
  150. """
  151. # start the TTX file
  152. # ---------------------------------------------
  153. root = Element('ttFont', {'sfntVersion': '\\x00\\x01\\x00\\x00', 'ttLibVersion': '3.28'}) # hard-coded attrs.
  154. # get all of this font's tables' TTX representations and append them to the file.
  155. root.append(self.glyphOrder.toTTX())
  156. for tableName, t in self.tables.items():
  157. root.append(t.toTTX())
  158. # the TTX is now done! (as long as something didn't go wrong)
  159. # choose whether to get the result as a formatted string or as an lxml Element.
  160. if asString:
  161. return tostring(root, pretty_print=True, method="xml", xml_declaration=True, encoding="UTF-8")
  162. else:
  163. return root
  164. def bytesPass(self):
  165. """
  166. Represents a single compile pass to bytes.
  167. (Just a WIP/placeholder right now.)
  168. """
  169. # offset table (ie. the font header)
  170. # --------------------------------------------------------------
  171. # (this should be fine and complete)
  172. numTables = len(self.tables)
  173. searchRange = (2 ** floor(log2(numTables))) * 16
  174. entrySelector = int(log2(floor(log2(numTables))))
  175. rangeShift = numTables * 16 - searchRange
  176. offsetTable = struct.pack( ">IHHHH"
  177. , 0x00010000 # sfntVersion, UInt32
  178. , numTables # UInt16
  179. , searchRange # UInt16
  180. , entrySelector # UInt16
  181. , rangeShift # UInt16
  182. )
  183. # table record entries
  184. # -------------------------------------------------------------
  185. initialTables = []
  186. originalLengths = []
  187. checkSums = []
  188. tags = []
  189. # get all of the table data
  190. for tableName, t in self.tables.items():
  191. #print(f"converting {tableName} to bytes...")
  192. # convert to bytes
  193. try:
  194. tableOutput = t.toBytes()
  195. except ValueError as e:
  196. raise ValueError(f"Something has gone wrong with converting the {tableName} table to bytes. -> {e}")
  197. initialTables.append(tableOutput[0])
  198. originalLengths.append(tableOutput[1])
  199. # get a checksum on that data
  200. try:
  201. checkSums.append(calculateTableChecksum(tableOutput[0]))
  202. except ValueError as e:
  203. raise ValueError(f"Something has gone wrong with calculating the checksum for {tableName}. -> {e}")
  204. # also add a tag.
  205. tags.append(tableName)
  206. # calculate offsets for each table
  207. initialOffset = (len(self.tables) * 16) + 12 # 16 = tableRecord length, 12 = offset table length.
  208. tableOffsets = generateOffsets(initialTables, 32, initialOffset, usingClasses=False)
  209. tableRecordsList = []
  210. for n, t in enumerate(initialTables):
  211. tableRecordsList.append(tables.tableRecord.TableRecord( tags[n]
  212. , checkSums[n]
  213. , tableOffsets["offsetInts"][n]
  214. , originalLengths[n]
  215. ))
  216. #print(tableRecordsList)
  217. tableRecordsList.sort()
  218. tableRecords = b''
  219. for t in tableRecordsList:
  220. tableRecords += t.toBytes()
  221. return offsetTable + tableRecords + tableOffsets["bytes"]
  222. def toBytes(self):
  223. """
  224. Compiles font class into a fully formed TrueType/OpenType font.
  225. (WIP)
  226. """
  227. log.out('first compilation pass...', 90)
  228. firstPass = self.bytesPass()
  229. log.out('calculating checksum...', 90)
  230. initialCS = calculateTableChecksum(firstPass)
  231. checkSumAdjustment = (0xB1B0AFBA - initialCS) % 0x100000000
  232. self.tables["head"].checkSumAdjustment = checkSumAdjustment
  233. log.out('last compilation pass...', 90)
  234. lastPass = self.bytesPass()
  235. return lastPass