woff2.py 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680
  1. from io import BytesIO
  2. import sys
  3. import array
  4. import struct
  5. from collections import OrderedDict
  6. from fontTools.misc import sstruct
  7. from fontTools.misc.arrayTools import calcIntBounds
  8. from fontTools.misc.textTools import Tag, bytechr, byteord, bytesjoin, pad
  9. from fontTools.ttLib import (
  10. TTFont,
  11. TTLibError,
  12. getTableModule,
  13. getTableClass,
  14. getSearchRange,
  15. )
  16. from fontTools.ttLib.sfnt import (
  17. SFNTReader,
  18. SFNTWriter,
  19. DirectoryEntry,
  20. WOFFFlavorData,
  21. sfntDirectoryFormat,
  22. sfntDirectorySize,
  23. SFNTDirectoryEntry,
  24. sfntDirectoryEntrySize,
  25. calcChecksum,
  26. )
  27. from fontTools.ttLib.tables import ttProgram, _g_l_y_f
  28. import logging
  29. log = logging.getLogger("fontTools.ttLib.woff2")
  30. haveBrotli = False
  31. try:
  32. try:
  33. import brotlicffi as brotli
  34. except ImportError:
  35. import brotli
  36. haveBrotli = True
  37. except ImportError:
  38. pass
  39. class WOFF2Reader(SFNTReader):
  40. flavor = "woff2"
  41. def __init__(self, file, checkChecksums=0, fontNumber=-1):
  42. if not haveBrotli:
  43. log.error(
  44. "The WOFF2 decoder requires the Brotli Python extension, available at: "
  45. "https://github.com/google/brotli"
  46. )
  47. raise ImportError("No module named brotli")
  48. self.file = file
  49. signature = Tag(self.file.read(4))
  50. if signature != b"wOF2":
  51. raise TTLibError("Not a WOFF2 font (bad signature)")
  52. self.file.seek(0)
  53. self.DirectoryEntry = WOFF2DirectoryEntry
  54. data = self.file.read(woff2DirectorySize)
  55. if len(data) != woff2DirectorySize:
  56. raise TTLibError("Not a WOFF2 font (not enough data)")
  57. sstruct.unpack(woff2DirectoryFormat, data, self)
  58. self.tables = OrderedDict()
  59. offset = 0
  60. for i in range(self.numTables):
  61. entry = self.DirectoryEntry()
  62. entry.fromFile(self.file)
  63. tag = Tag(entry.tag)
  64. self.tables[tag] = entry
  65. entry.offset = offset
  66. offset += entry.length
  67. totalUncompressedSize = offset
  68. compressedData = self.file.read(self.totalCompressedSize)
  69. decompressedData = brotli.decompress(compressedData)
  70. if len(decompressedData) != totalUncompressedSize:
  71. raise TTLibError(
  72. "unexpected size for decompressed font data: expected %d, found %d"
  73. % (totalUncompressedSize, len(decompressedData))
  74. )
  75. self.transformBuffer = BytesIO(decompressedData)
  76. self.file.seek(0, 2)
  77. if self.length != self.file.tell():
  78. raise TTLibError("reported 'length' doesn't match the actual file size")
  79. self.flavorData = WOFF2FlavorData(self)
  80. # make empty TTFont to store data while reconstructing tables
  81. self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
  82. def __getitem__(self, tag):
  83. """Fetch the raw table data. Reconstruct transformed tables."""
  84. entry = self.tables[Tag(tag)]
  85. if not hasattr(entry, "data"):
  86. if entry.transformed:
  87. entry.data = self.reconstructTable(tag)
  88. else:
  89. entry.data = entry.loadData(self.transformBuffer)
  90. return entry.data
  91. def reconstructTable(self, tag):
  92. """Reconstruct table named 'tag' from transformed data."""
  93. entry = self.tables[Tag(tag)]
  94. rawData = entry.loadData(self.transformBuffer)
  95. if tag == "glyf":
  96. # no need to pad glyph data when reconstructing
  97. padding = self.padding if hasattr(self, "padding") else None
  98. data = self._reconstructGlyf(rawData, padding)
  99. elif tag == "loca":
  100. data = self._reconstructLoca()
  101. elif tag == "hmtx":
  102. data = self._reconstructHmtx(rawData)
  103. else:
  104. raise TTLibError("transform for table '%s' is unknown" % tag)
  105. return data
  106. def _reconstructGlyf(self, data, padding=None):
  107. """Return recostructed glyf table data, and set the corresponding loca's
  108. locations. Optionally pad glyph offsets to the specified number of bytes.
  109. """
  110. self.ttFont["loca"] = WOFF2LocaTable()
  111. glyfTable = self.ttFont["glyf"] = WOFF2GlyfTable()
  112. glyfTable.reconstruct(data, self.ttFont)
  113. if padding:
  114. glyfTable.padding = padding
  115. data = glyfTable.compile(self.ttFont)
  116. return data
  117. def _reconstructLoca(self):
  118. """Return reconstructed loca table data."""
  119. if "loca" not in self.ttFont:
  120. # make sure glyf is reconstructed first
  121. self.tables["glyf"].data = self.reconstructTable("glyf")
  122. locaTable = self.ttFont["loca"]
  123. data = locaTable.compile(self.ttFont)
  124. if len(data) != self.tables["loca"].origLength:
  125. raise TTLibError(
  126. "reconstructed 'loca' table doesn't match original size: "
  127. "expected %d, found %d" % (self.tables["loca"].origLength, len(data))
  128. )
  129. return data
  130. def _reconstructHmtx(self, data):
  131. """Return reconstructed hmtx table data."""
  132. # Before reconstructing 'hmtx' table we need to parse other tables:
  133. # 'glyf' is required for reconstructing the sidebearings from the glyphs'
  134. # bounding box; 'hhea' is needed for the numberOfHMetrics field.
  135. if "glyf" in self.flavorData.transformedTables:
  136. # transformed 'glyf' table is self-contained, thus 'loca' not needed
  137. tableDependencies = ("maxp", "hhea", "glyf")
  138. else:
  139. # decompiling untransformed 'glyf' requires 'loca', which requires 'head'
  140. tableDependencies = ("maxp", "head", "hhea", "loca", "glyf")
  141. for tag in tableDependencies:
  142. self._decompileTable(tag)
  143. hmtxTable = self.ttFont["hmtx"] = WOFF2HmtxTable()
  144. hmtxTable.reconstruct(data, self.ttFont)
  145. data = hmtxTable.compile(self.ttFont)
  146. return data
  147. def _decompileTable(self, tag):
  148. """Decompile table data and store it inside self.ttFont."""
  149. data = self[tag]
  150. if self.ttFont.isLoaded(tag):
  151. return self.ttFont[tag]
  152. tableClass = getTableClass(tag)
  153. table = tableClass(tag)
  154. self.ttFont.tables[tag] = table
  155. table.decompile(data, self.ttFont)
  156. class WOFF2Writer(SFNTWriter):
  157. flavor = "woff2"
  158. def __init__(
  159. self,
  160. file,
  161. numTables,
  162. sfntVersion="\000\001\000\000",
  163. flavor=None,
  164. flavorData=None,
  165. ):
  166. if not haveBrotli:
  167. log.error(
  168. "The WOFF2 encoder requires the Brotli Python extension, available at: "
  169. "https://github.com/google/brotli"
  170. )
  171. raise ImportError("No module named brotli")
  172. self.file = file
  173. self.numTables = numTables
  174. self.sfntVersion = Tag(sfntVersion)
  175. self.flavorData = WOFF2FlavorData(data=flavorData)
  176. self.directoryFormat = woff2DirectoryFormat
  177. self.directorySize = woff2DirectorySize
  178. self.DirectoryEntry = WOFF2DirectoryEntry
  179. self.signature = Tag("wOF2")
  180. self.nextTableOffset = 0
  181. self.transformBuffer = BytesIO()
  182. self.tables = OrderedDict()
  183. # make empty TTFont to store data while normalising and transforming tables
  184. self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
  185. def __setitem__(self, tag, data):
  186. """Associate new entry named 'tag' with raw table data."""
  187. if tag in self.tables:
  188. raise TTLibError("cannot rewrite '%s' table" % tag)
  189. if tag == "DSIG":
  190. # always drop DSIG table, since the encoding process can invalidate it
  191. self.numTables -= 1
  192. return
  193. entry = self.DirectoryEntry()
  194. entry.tag = Tag(tag)
  195. entry.flags = getKnownTagIndex(entry.tag)
  196. # WOFF2 table data are written to disk only on close(), after all tags
  197. # have been specified
  198. entry.data = data
  199. self.tables[tag] = entry
  200. def close(self):
  201. """All tags must have been specified. Now write the table data and directory."""
  202. if len(self.tables) != self.numTables:
  203. raise TTLibError(
  204. "wrong number of tables; expected %d, found %d"
  205. % (self.numTables, len(self.tables))
  206. )
  207. if self.sfntVersion in ("\x00\x01\x00\x00", "true"):
  208. isTrueType = True
  209. elif self.sfntVersion == "OTTO":
  210. isTrueType = False
  211. else:
  212. raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
  213. # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned.
  214. # However, the reference WOFF2 implementation still fails to reconstruct
  215. # 'unpadded' glyf tables, therefore we need to 'normalise' them.
  216. # See:
  217. # https://github.com/khaledhosny/ots/issues/60
  218. # https://github.com/google/woff2/issues/15
  219. if (
  220. isTrueType
  221. and "glyf" in self.flavorData.transformedTables
  222. and "glyf" in self.tables
  223. ):
  224. self._normaliseGlyfAndLoca(padding=4)
  225. self._setHeadTransformFlag()
  226. # To pass the legacy OpenType Sanitiser currently included in browsers,
  227. # we must sort the table directory and data alphabetically by tag.
  228. # See:
  229. # https://github.com/google/woff2/pull/3
  230. # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html
  231. #
  232. # 2023: We rely on this in _transformTables where we expect that
  233. # "loca" comes after "glyf" table.
  234. self.tables = OrderedDict(sorted(self.tables.items()))
  235. self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets()
  236. fontData = self._transformTables()
  237. compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT)
  238. self.totalCompressedSize = len(compressedFont)
  239. self.length = self._calcTotalSize()
  240. self.majorVersion, self.minorVersion = self._getVersion()
  241. self.reserved = 0
  242. directory = self._packTableDirectory()
  243. self.file.seek(0)
  244. self.file.write(pad(directory + compressedFont, size=4))
  245. self._writeFlavorData()
  246. def _normaliseGlyfAndLoca(self, padding=4):
  247. """Recompile glyf and loca tables, aligning glyph offsets to multiples of
  248. 'padding' size. Update the head table's 'indexToLocFormat' accordingly while
  249. compiling loca.
  250. """
  251. if self.sfntVersion == "OTTO":
  252. return
  253. for tag in ("maxp", "head", "loca", "glyf", "fvar"):
  254. if tag in self.tables:
  255. self._decompileTable(tag)
  256. self.ttFont["glyf"].padding = padding
  257. for tag in ("glyf", "loca"):
  258. self._compileTable(tag)
  259. def _setHeadTransformFlag(self):
  260. """Set bit 11 of 'head' table flags to indicate that the font has undergone
  261. a lossless modifying transform. Re-compile head table data."""
  262. self._decompileTable("head")
  263. self.ttFont["head"].flags |= 1 << 11
  264. self._compileTable("head")
  265. def _decompileTable(self, tag):
  266. """Fetch table data, decompile it, and store it inside self.ttFont."""
  267. tag = Tag(tag)
  268. if tag not in self.tables:
  269. raise TTLibError("missing required table: %s" % tag)
  270. if self.ttFont.isLoaded(tag):
  271. return
  272. data = self.tables[tag].data
  273. if tag == "loca":
  274. tableClass = WOFF2LocaTable
  275. elif tag == "glyf":
  276. tableClass = WOFF2GlyfTable
  277. elif tag == "hmtx":
  278. tableClass = WOFF2HmtxTable
  279. else:
  280. tableClass = getTableClass(tag)
  281. table = tableClass(tag)
  282. self.ttFont.tables[tag] = table
  283. table.decompile(data, self.ttFont)
  284. def _compileTable(self, tag):
  285. """Compile table and store it in its 'data' attribute."""
  286. self.tables[tag].data = self.ttFont[tag].compile(self.ttFont)
  287. def _calcSFNTChecksumsLengthsAndOffsets(self):
  288. """Compute the 'original' SFNT checksums, lengths and offsets for checksum
  289. adjustment calculation. Return the total size of the uncompressed font.
  290. """
  291. offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables)
  292. for tag, entry in self.tables.items():
  293. data = entry.data
  294. entry.origOffset = offset
  295. entry.origLength = len(data)
  296. if tag == "head":
  297. entry.checkSum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:])
  298. else:
  299. entry.checkSum = calcChecksum(data)
  300. offset += (entry.origLength + 3) & ~3
  301. return offset
  302. def _transformTables(self):
  303. """Return transformed font data."""
  304. transformedTables = self.flavorData.transformedTables
  305. for tag, entry in self.tables.items():
  306. data = None
  307. if tag in transformedTables:
  308. data = self.transformTable(tag)
  309. if data is not None:
  310. entry.transformed = True
  311. if data is None:
  312. if tag == "glyf":
  313. # Currently we always sort table tags so
  314. # 'loca' comes after 'glyf'.
  315. transformedTables.discard("loca")
  316. # pass-through the table data without transformation
  317. data = entry.data
  318. entry.transformed = False
  319. entry.offset = self.nextTableOffset
  320. entry.saveData(self.transformBuffer, data)
  321. self.nextTableOffset += entry.length
  322. self.writeMasterChecksum()
  323. fontData = self.transformBuffer.getvalue()
  324. return fontData
  325. def transformTable(self, tag):
  326. """Return transformed table data, or None if some pre-conditions aren't
  327. met -- in which case, the non-transformed table data will be used.
  328. """
  329. if tag == "loca":
  330. data = b""
  331. elif tag == "glyf":
  332. for tag in ("maxp", "head", "loca", "glyf"):
  333. self._decompileTable(tag)
  334. glyfTable = self.ttFont["glyf"]
  335. data = glyfTable.transform(self.ttFont)
  336. elif tag == "hmtx":
  337. if "glyf" not in self.tables:
  338. return
  339. for tag in ("maxp", "head", "hhea", "loca", "glyf", "hmtx"):
  340. self._decompileTable(tag)
  341. hmtxTable = self.ttFont["hmtx"]
  342. data = hmtxTable.transform(self.ttFont) # can be None
  343. else:
  344. raise TTLibError("Transform for table '%s' is unknown" % tag)
  345. return data
  346. def _calcMasterChecksum(self):
  347. """Calculate checkSumAdjustment."""
  348. checksums = []
  349. for tag in self.tables.keys():
  350. checksums.append(self.tables[tag].checkSum)
  351. # Create a SFNT directory for checksum calculation purposes
  352. self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(
  353. self.numTables, 16
  354. )
  355. directory = sstruct.pack(sfntDirectoryFormat, self)
  356. tables = sorted(self.tables.items())
  357. for tag, entry in tables:
  358. sfntEntry = SFNTDirectoryEntry()
  359. sfntEntry.tag = entry.tag
  360. sfntEntry.checkSum = entry.checkSum
  361. sfntEntry.offset = entry.origOffset
  362. sfntEntry.length = entry.origLength
  363. directory = directory + sfntEntry.toString()
  364. directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
  365. assert directory_end == len(directory)
  366. checksums.append(calcChecksum(directory))
  367. checksum = sum(checksums) & 0xFFFFFFFF
  368. # BiboAfba!
  369. checksumadjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF
  370. return checksumadjustment
  371. def writeMasterChecksum(self):
  372. """Write checkSumAdjustment to the transformBuffer."""
  373. checksumadjustment = self._calcMasterChecksum()
  374. self.transformBuffer.seek(self.tables["head"].offset + 8)
  375. self.transformBuffer.write(struct.pack(">L", checksumadjustment))
  376. def _calcTotalSize(self):
  377. """Calculate total size of WOFF2 font, including any meta- and/or private data."""
  378. offset = self.directorySize
  379. for entry in self.tables.values():
  380. offset += len(entry.toString())
  381. offset += self.totalCompressedSize
  382. offset = (offset + 3) & ~3
  383. offset = self._calcFlavorDataOffsetsAndSize(offset)
  384. return offset
  385. def _calcFlavorDataOffsetsAndSize(self, start):
  386. """Calculate offsets and lengths for any meta- and/or private data."""
  387. offset = start
  388. data = self.flavorData
  389. if data.metaData:
  390. self.metaOrigLength = len(data.metaData)
  391. self.metaOffset = offset
  392. self.compressedMetaData = brotli.compress(
  393. data.metaData, mode=brotli.MODE_TEXT
  394. )
  395. self.metaLength = len(self.compressedMetaData)
  396. offset += self.metaLength
  397. else:
  398. self.metaOffset = self.metaLength = self.metaOrigLength = 0
  399. self.compressedMetaData = b""
  400. if data.privData:
  401. # make sure private data is padded to 4-byte boundary
  402. offset = (offset + 3) & ~3
  403. self.privOffset = offset
  404. self.privLength = len(data.privData)
  405. offset += self.privLength
  406. else:
  407. self.privOffset = self.privLength = 0
  408. return offset
  409. def _getVersion(self):
  410. """Return the WOFF2 font's (majorVersion, minorVersion) tuple."""
  411. data = self.flavorData
  412. if data.majorVersion is not None and data.minorVersion is not None:
  413. return data.majorVersion, data.minorVersion
  414. else:
  415. # if None, return 'fontRevision' from 'head' table
  416. if "head" in self.tables:
  417. return struct.unpack(">HH", self.tables["head"].data[4:8])
  418. else:
  419. return 0, 0
  420. def _packTableDirectory(self):
  421. """Return WOFF2 table directory data."""
  422. directory = sstruct.pack(self.directoryFormat, self)
  423. for entry in self.tables.values():
  424. directory = directory + entry.toString()
  425. return directory
  426. def _writeFlavorData(self):
  427. """Write metadata and/or private data using appropiate padding."""
  428. compressedMetaData = self.compressedMetaData
  429. privData = self.flavorData.privData
  430. if compressedMetaData and privData:
  431. compressedMetaData = pad(compressedMetaData, size=4)
  432. if compressedMetaData:
  433. self.file.seek(self.metaOffset)
  434. assert self.file.tell() == self.metaOffset
  435. self.file.write(compressedMetaData)
  436. if privData:
  437. self.file.seek(self.privOffset)
  438. assert self.file.tell() == self.privOffset
  439. self.file.write(privData)
  440. def reordersTables(self):
  441. return True
  442. # -- woff2 directory helpers and cruft
  443. woff2DirectoryFormat = """
  444. > # big endian
  445. signature: 4s # "wOF2"
  446. sfntVersion: 4s
  447. length: L # total woff2 file size
  448. numTables: H # number of tables
  449. reserved: H # set to 0
  450. totalSfntSize: L # uncompressed size
  451. totalCompressedSize: L # compressed size
  452. majorVersion: H # major version of WOFF file
  453. minorVersion: H # minor version of WOFF file
  454. metaOffset: L # offset to metadata block
  455. metaLength: L # length of compressed metadata
  456. metaOrigLength: L # length of uncompressed metadata
  457. privOffset: L # offset to private data block
  458. privLength: L # length of private data block
  459. """
  460. woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat)
  461. woff2KnownTags = (
  462. "cmap",
  463. "head",
  464. "hhea",
  465. "hmtx",
  466. "maxp",
  467. "name",
  468. "OS/2",
  469. "post",
  470. "cvt ",
  471. "fpgm",
  472. "glyf",
  473. "loca",
  474. "prep",
  475. "CFF ",
  476. "VORG",
  477. "EBDT",
  478. "EBLC",
  479. "gasp",
  480. "hdmx",
  481. "kern",
  482. "LTSH",
  483. "PCLT",
  484. "VDMX",
  485. "vhea",
  486. "vmtx",
  487. "BASE",
  488. "GDEF",
  489. "GPOS",
  490. "GSUB",
  491. "EBSC",
  492. "JSTF",
  493. "MATH",
  494. "CBDT",
  495. "CBLC",
  496. "COLR",
  497. "CPAL",
  498. "SVG ",
  499. "sbix",
  500. "acnt",
  501. "avar",
  502. "bdat",
  503. "bloc",
  504. "bsln",
  505. "cvar",
  506. "fdsc",
  507. "feat",
  508. "fmtx",
  509. "fvar",
  510. "gvar",
  511. "hsty",
  512. "just",
  513. "lcar",
  514. "mort",
  515. "morx",
  516. "opbd",
  517. "prop",
  518. "trak",
  519. "Zapf",
  520. "Silf",
  521. "Glat",
  522. "Gloc",
  523. "Feat",
  524. "Sill",
  525. )
  526. woff2FlagsFormat = """
  527. > # big endian
  528. flags: B # table type and flags
  529. """
  530. woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat)
  531. woff2UnknownTagFormat = """
  532. > # big endian
  533. tag: 4s # 4-byte tag (optional)
  534. """
  535. woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat)
  536. woff2UnknownTagIndex = 0x3F
  537. woff2Base128MaxSize = 5
  538. woff2DirectoryEntryMaxSize = (
  539. woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize
  540. )
  541. woff2TransformedTableTags = ("glyf", "loca")
  542. woff2GlyfTableFormat = """
  543. > # big endian
  544. version: H # = 0x0000
  545. optionFlags: H # Bit 0: we have overlapSimpleBitmap[], Bits 1-15: reserved
  546. numGlyphs: H # Number of glyphs
  547. indexFormat: H # Offset format for loca table
  548. nContourStreamSize: L # Size of nContour stream
  549. nPointsStreamSize: L # Size of nPoints stream
  550. flagStreamSize: L # Size of flag stream
  551. glyphStreamSize: L # Size of glyph stream
  552. compositeStreamSize: L # Size of composite stream
  553. bboxStreamSize: L # Comnined size of bboxBitmap and bboxStream
  554. instructionStreamSize: L # Size of instruction stream
  555. """
  556. woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat)
  557. bboxFormat = """
  558. > # big endian
  559. xMin: h
  560. yMin: h
  561. xMax: h
  562. yMax: h
  563. """
  564. woff2OverlapSimpleBitmapFlag = 0x0001
  565. def getKnownTagIndex(tag):
  566. """Return index of 'tag' in woff2KnownTags list. Return 63 if not found."""
  567. try:
  568. return woff2KnownTags.index(tag)
  569. except ValueError:
  570. return woff2UnknownTagIndex
  571. class WOFF2DirectoryEntry(DirectoryEntry):
  572. def fromFile(self, file):
  573. pos = file.tell()
  574. data = file.read(woff2DirectoryEntryMaxSize)
  575. left = self.fromString(data)
  576. consumed = len(data) - len(left)
  577. file.seek(pos + consumed)
  578. def fromString(self, data):
  579. if len(data) < 1:
  580. raise TTLibError("can't read table 'flags': not enough data")
  581. dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self)
  582. if self.flags & 0x3F == 0x3F:
  583. # if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value
  584. if len(data) < woff2UnknownTagSize:
  585. raise TTLibError("can't read table 'tag': not enough data")
  586. dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self)
  587. else:
  588. # otherwise, tag is derived from a fixed 'Known Tags' table
  589. self.tag = woff2KnownTags[self.flags & 0x3F]
  590. self.tag = Tag(self.tag)
  591. self.origLength, data = unpackBase128(data)
  592. self.length = self.origLength
  593. if self.transformed:
  594. self.length, data = unpackBase128(data)
  595. if self.tag == "loca" and self.length != 0:
  596. raise TTLibError("the transformLength of the 'loca' table must be 0")
  597. # return left over data
  598. return data
  599. def toString(self):
  600. data = bytechr(self.flags)
  601. if (self.flags & 0x3F) == 0x3F:
  602. data += struct.pack(">4s", self.tag.tobytes())
  603. data += packBase128(self.origLength)
  604. if self.transformed:
  605. data += packBase128(self.length)
  606. return data
  607. @property
  608. def transformVersion(self):
  609. """Return bits 6-7 of table entry's flags, which indicate the preprocessing
  610. transformation version number (between 0 and 3).
  611. """
  612. return self.flags >> 6
  613. @transformVersion.setter
  614. def transformVersion(self, value):
  615. assert 0 <= value <= 3
  616. self.flags |= value << 6
  617. @property
  618. def transformed(self):
  619. """Return True if the table has any transformation, else return False."""
  620. # For all tables in a font, except for 'glyf' and 'loca', the transformation
  621. # version 0 indicates the null transform (where the original table data is
  622. # passed directly to the Brotli compressor). For 'glyf' and 'loca' tables,
  623. # transformation version 3 indicates the null transform
  624. if self.tag in {"glyf", "loca"}:
  625. return self.transformVersion != 3
  626. else:
  627. return self.transformVersion != 0
  628. @transformed.setter
  629. def transformed(self, booleanValue):
  630. # here we assume that a non-null transform means version 0 for 'glyf' and
  631. # 'loca' and 1 for every other table (e.g. hmtx); but that may change as
  632. # new transformation formats are introduced in the future (if ever).
  633. if self.tag in {"glyf", "loca"}:
  634. self.transformVersion = 3 if not booleanValue else 0
  635. else:
  636. self.transformVersion = int(booleanValue)
  637. class WOFF2LocaTable(getTableClass("loca")):
  638. """Same as parent class. The only difference is that it attempts to preserve
  639. the 'indexFormat' as encoded in the WOFF2 glyf table.
  640. """
  641. def __init__(self, tag=None):
  642. self.tableTag = Tag(tag or "loca")
  643. def compile(self, ttFont):
  644. try:
  645. max_location = max(self.locations)
  646. except AttributeError:
  647. self.set([])
  648. max_location = 0
  649. if "glyf" in ttFont and hasattr(ttFont["glyf"], "indexFormat"):
  650. # copile loca using the indexFormat specified in the WOFF2 glyf table
  651. indexFormat = ttFont["glyf"].indexFormat
  652. if indexFormat == 0:
  653. if max_location >= 0x20000:
  654. raise TTLibError("indexFormat is 0 but local offsets > 0x20000")
  655. if not all(l % 2 == 0 for l in self.locations):
  656. raise TTLibError(
  657. "indexFormat is 0 but local offsets not multiples of 2"
  658. )
  659. locations = array.array("H")
  660. for location in self.locations:
  661. locations.append(location // 2)
  662. else:
  663. locations = array.array("I", self.locations)
  664. if sys.byteorder != "big":
  665. locations.byteswap()
  666. data = locations.tobytes()
  667. else:
  668. # use the most compact indexFormat given the current glyph offsets
  669. data = super(WOFF2LocaTable, self).compile(ttFont)
  670. return data
  671. class WOFF2GlyfTable(getTableClass("glyf")):
  672. """Decoder/Encoder for WOFF2 'glyf' table transform."""
  673. subStreams = (
  674. "nContourStream",
  675. "nPointsStream",
  676. "flagStream",
  677. "glyphStream",
  678. "compositeStream",
  679. "bboxStream",
  680. "instructionStream",
  681. )
  682. def __init__(self, tag=None):
  683. self.tableTag = Tag(tag or "glyf")
  684. def reconstruct(self, data, ttFont):
  685. """Decompile transformed 'glyf' data."""
  686. inputDataSize = len(data)
  687. if inputDataSize < woff2GlyfTableFormatSize:
  688. raise TTLibError("not enough 'glyf' data")
  689. dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self)
  690. offset = woff2GlyfTableFormatSize
  691. for stream in self.subStreams:
  692. size = getattr(self, stream + "Size")
  693. setattr(self, stream, data[:size])
  694. data = data[size:]
  695. offset += size
  696. hasOverlapSimpleBitmap = self.optionFlags & woff2OverlapSimpleBitmapFlag
  697. self.overlapSimpleBitmap = None
  698. if hasOverlapSimpleBitmap:
  699. overlapSimpleBitmapSize = (self.numGlyphs + 7) >> 3
  700. self.overlapSimpleBitmap = array.array("B", data[:overlapSimpleBitmapSize])
  701. offset += overlapSimpleBitmapSize
  702. if offset != inputDataSize:
  703. raise TTLibError(
  704. "incorrect size of transformed 'glyf' table: expected %d, received %d bytes"
  705. % (offset, inputDataSize)
  706. )
  707. bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
  708. bboxBitmap = self.bboxStream[:bboxBitmapSize]
  709. self.bboxBitmap = array.array("B", bboxBitmap)
  710. self.bboxStream = self.bboxStream[bboxBitmapSize:]
  711. self.nContourStream = array.array("h", self.nContourStream)
  712. if sys.byteorder != "big":
  713. self.nContourStream.byteswap()
  714. assert len(self.nContourStream) == self.numGlyphs
  715. if "head" in ttFont:
  716. ttFont["head"].indexToLocFormat = self.indexFormat
  717. try:
  718. self.glyphOrder = ttFont.getGlyphOrder()
  719. except:
  720. self.glyphOrder = None
  721. if self.glyphOrder is None:
  722. self.glyphOrder = [".notdef"]
  723. self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)])
  724. else:
  725. if len(self.glyphOrder) != self.numGlyphs:
  726. raise TTLibError(
  727. "incorrect glyphOrder: expected %d glyphs, found %d"
  728. % (len(self.glyphOrder), self.numGlyphs)
  729. )
  730. glyphs = self.glyphs = {}
  731. for glyphID, glyphName in enumerate(self.glyphOrder):
  732. glyph = self._decodeGlyph(glyphID)
  733. glyphs[glyphName] = glyph
  734. def transform(self, ttFont):
  735. """Return transformed 'glyf' data"""
  736. self.numGlyphs = len(self.glyphs)
  737. assert len(self.glyphOrder) == self.numGlyphs
  738. if "maxp" in ttFont:
  739. ttFont["maxp"].numGlyphs = self.numGlyphs
  740. self.indexFormat = ttFont["head"].indexToLocFormat
  741. for stream in self.subStreams:
  742. setattr(self, stream, b"")
  743. bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
  744. self.bboxBitmap = array.array("B", [0] * bboxBitmapSize)
  745. self.overlapSimpleBitmap = array.array("B", [0] * ((self.numGlyphs + 7) >> 3))
  746. for glyphID in range(self.numGlyphs):
  747. try:
  748. self._encodeGlyph(glyphID)
  749. except NotImplementedError:
  750. return None
  751. hasOverlapSimpleBitmap = any(self.overlapSimpleBitmap)
  752. self.bboxStream = self.bboxBitmap.tobytes() + self.bboxStream
  753. for stream in self.subStreams:
  754. setattr(self, stream + "Size", len(getattr(self, stream)))
  755. self.version = 0
  756. self.optionFlags = 0
  757. if hasOverlapSimpleBitmap:
  758. self.optionFlags |= woff2OverlapSimpleBitmapFlag
  759. data = sstruct.pack(woff2GlyfTableFormat, self)
  760. data += bytesjoin([getattr(self, s) for s in self.subStreams])
  761. if hasOverlapSimpleBitmap:
  762. data += self.overlapSimpleBitmap.tobytes()
  763. return data
  764. def _decodeGlyph(self, glyphID):
  765. glyph = getTableModule("glyf").Glyph()
  766. glyph.numberOfContours = self.nContourStream[glyphID]
  767. if glyph.numberOfContours == 0:
  768. return glyph
  769. elif glyph.isComposite():
  770. self._decodeComponents(glyph)
  771. else:
  772. self._decodeCoordinates(glyph)
  773. self._decodeOverlapSimpleFlag(glyph, glyphID)
  774. self._decodeBBox(glyphID, glyph)
  775. return glyph
  776. def _decodeComponents(self, glyph):
  777. data = self.compositeStream
  778. glyph.components = []
  779. more = 1
  780. haveInstructions = 0
  781. while more:
  782. component = getTableModule("glyf").GlyphComponent()
  783. more, haveInstr, data = component.decompile(data, self)
  784. haveInstructions = haveInstructions | haveInstr
  785. glyph.components.append(component)
  786. self.compositeStream = data
  787. if haveInstructions:
  788. self._decodeInstructions(glyph)
  789. def _decodeCoordinates(self, glyph):
  790. data = self.nPointsStream
  791. endPtsOfContours = []
  792. endPoint = -1
  793. for i in range(glyph.numberOfContours):
  794. ptsOfContour, data = unpack255UShort(data)
  795. endPoint += ptsOfContour
  796. endPtsOfContours.append(endPoint)
  797. glyph.endPtsOfContours = endPtsOfContours
  798. self.nPointsStream = data
  799. self._decodeTriplets(glyph)
  800. self._decodeInstructions(glyph)
  801. def _decodeOverlapSimpleFlag(self, glyph, glyphID):
  802. if self.overlapSimpleBitmap is None or glyph.numberOfContours <= 0:
  803. return
  804. byte = glyphID >> 3
  805. bit = glyphID & 7
  806. if self.overlapSimpleBitmap[byte] & (0x80 >> bit):
  807. glyph.flags[0] |= _g_l_y_f.flagOverlapSimple
  808. def _decodeInstructions(self, glyph):
  809. glyphStream = self.glyphStream
  810. instructionStream = self.instructionStream
  811. instructionLength, glyphStream = unpack255UShort(glyphStream)
  812. glyph.program = ttProgram.Program()
  813. glyph.program.fromBytecode(instructionStream[:instructionLength])
  814. self.glyphStream = glyphStream
  815. self.instructionStream = instructionStream[instructionLength:]
  816. def _decodeBBox(self, glyphID, glyph):
  817. haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7)))
  818. if glyph.isComposite() and not haveBBox:
  819. raise TTLibError("no bbox values for composite glyph %d" % glyphID)
  820. if haveBBox:
  821. dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph)
  822. else:
  823. glyph.recalcBounds(self)
  824. def _decodeTriplets(self, glyph):
  825. def withSign(flag, baseval):
  826. assert 0 <= baseval and baseval < 65536, "integer overflow"
  827. return baseval if flag & 1 else -baseval
  828. nPoints = glyph.endPtsOfContours[-1] + 1
  829. flagSize = nPoints
  830. if flagSize > len(self.flagStream):
  831. raise TTLibError("not enough 'flagStream' data")
  832. flagsData = self.flagStream[:flagSize]
  833. self.flagStream = self.flagStream[flagSize:]
  834. flags = array.array("B", flagsData)
  835. triplets = array.array("B", self.glyphStream)
  836. nTriplets = len(triplets)
  837. assert nPoints <= nTriplets
  838. x = 0
  839. y = 0
  840. glyph.coordinates = getTableModule("glyf").GlyphCoordinates.zeros(nPoints)
  841. glyph.flags = array.array("B")
  842. tripletIndex = 0
  843. for i in range(nPoints):
  844. flag = flags[i]
  845. onCurve = not bool(flag >> 7)
  846. flag &= 0x7F
  847. if flag < 84:
  848. nBytes = 1
  849. elif flag < 120:
  850. nBytes = 2
  851. elif flag < 124:
  852. nBytes = 3
  853. else:
  854. nBytes = 4
  855. assert (tripletIndex + nBytes) <= nTriplets
  856. if flag < 10:
  857. dx = 0
  858. dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex])
  859. elif flag < 20:
  860. dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex])
  861. dy = 0
  862. elif flag < 84:
  863. b0 = flag - 20
  864. b1 = triplets[tripletIndex]
  865. dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4))
  866. dy = withSign(flag >> 1, 1 + ((b0 & 0x0C) << 2) + (b1 & 0x0F))
  867. elif flag < 120:
  868. b0 = flag - 84
  869. dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex])
  870. dy = withSign(
  871. flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1]
  872. )
  873. elif flag < 124:
  874. b2 = triplets[tripletIndex + 1]
  875. dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4))
  876. dy = withSign(
  877. flag >> 1, ((b2 & 0x0F) << 8) + triplets[tripletIndex + 2]
  878. )
  879. else:
  880. dx = withSign(
  881. flag, (triplets[tripletIndex] << 8) + triplets[tripletIndex + 1]
  882. )
  883. dy = withSign(
  884. flag >> 1,
  885. (triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3],
  886. )
  887. tripletIndex += nBytes
  888. x += dx
  889. y += dy
  890. glyph.coordinates[i] = (x, y)
  891. glyph.flags.append(int(onCurve))
  892. bytesConsumed = tripletIndex
  893. self.glyphStream = self.glyphStream[bytesConsumed:]
  894. def _encodeGlyph(self, glyphID):
  895. glyphName = self.getGlyphName(glyphID)
  896. glyph = self[glyphName]
  897. self.nContourStream += struct.pack(">h", glyph.numberOfContours)
  898. if glyph.numberOfContours == 0:
  899. return
  900. elif glyph.isComposite():
  901. self._encodeComponents(glyph)
  902. else:
  903. self._encodeCoordinates(glyph)
  904. self._encodeOverlapSimpleFlag(glyph, glyphID)
  905. self._encodeBBox(glyphID, glyph)
  906. def _encodeComponents(self, glyph):
  907. lastcomponent = len(glyph.components) - 1
  908. more = 1
  909. haveInstructions = 0
  910. for i, component in enumerate(glyph.components):
  911. if i == lastcomponent:
  912. haveInstructions = hasattr(glyph, "program")
  913. more = 0
  914. self.compositeStream += component.compile(more, haveInstructions, self)
  915. if haveInstructions:
  916. self._encodeInstructions(glyph)
  917. def _encodeCoordinates(self, glyph):
  918. lastEndPoint = -1
  919. if _g_l_y_f.flagCubic in glyph.flags:
  920. raise NotImplementedError
  921. for endPoint in glyph.endPtsOfContours:
  922. ptsOfContour = endPoint - lastEndPoint
  923. self.nPointsStream += pack255UShort(ptsOfContour)
  924. lastEndPoint = endPoint
  925. self._encodeTriplets(glyph)
  926. self._encodeInstructions(glyph)
  927. def _encodeOverlapSimpleFlag(self, glyph, glyphID):
  928. if glyph.numberOfContours <= 0:
  929. return
  930. if glyph.flags[0] & _g_l_y_f.flagOverlapSimple:
  931. byte = glyphID >> 3
  932. bit = glyphID & 7
  933. self.overlapSimpleBitmap[byte] |= 0x80 >> bit
  934. def _encodeInstructions(self, glyph):
  935. instructions = glyph.program.getBytecode()
  936. self.glyphStream += pack255UShort(len(instructions))
  937. self.instructionStream += instructions
  938. def _encodeBBox(self, glyphID, glyph):
  939. assert glyph.numberOfContours != 0, "empty glyph has no bbox"
  940. if not glyph.isComposite():
  941. # for simple glyphs, compare the encoded bounding box info with the calculated
  942. # values, and if they match omit the bounding box info
  943. currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax
  944. calculatedBBox = calcIntBounds(glyph.coordinates)
  945. if currentBBox == calculatedBBox:
  946. return
  947. self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7)
  948. self.bboxStream += sstruct.pack(bboxFormat, glyph)
  949. def _encodeTriplets(self, glyph):
  950. assert len(glyph.coordinates) == len(glyph.flags)
  951. coordinates = glyph.coordinates.copy()
  952. coordinates.absoluteToRelative()
  953. flags = array.array("B")
  954. triplets = array.array("B")
  955. for i, (x, y) in enumerate(coordinates):
  956. onCurve = glyph.flags[i] & _g_l_y_f.flagOnCurve
  957. absX = abs(x)
  958. absY = abs(y)
  959. onCurveBit = 0 if onCurve else 128
  960. xSignBit = 0 if (x < 0) else 1
  961. ySignBit = 0 if (y < 0) else 1
  962. xySignBits = xSignBit + 2 * ySignBit
  963. if x == 0 and absY < 1280:
  964. flags.append(onCurveBit + ((absY & 0xF00) >> 7) + ySignBit)
  965. triplets.append(absY & 0xFF)
  966. elif y == 0 and absX < 1280:
  967. flags.append(onCurveBit + 10 + ((absX & 0xF00) >> 7) + xSignBit)
  968. triplets.append(absX & 0xFF)
  969. elif absX < 65 and absY < 65:
  970. flags.append(
  971. onCurveBit
  972. + 20
  973. + ((absX - 1) & 0x30)
  974. + (((absY - 1) & 0x30) >> 2)
  975. + xySignBits
  976. )
  977. triplets.append((((absX - 1) & 0xF) << 4) | ((absY - 1) & 0xF))
  978. elif absX < 769 and absY < 769:
  979. flags.append(
  980. onCurveBit
  981. + 84
  982. + 12 * (((absX - 1) & 0x300) >> 8)
  983. + (((absY - 1) & 0x300) >> 6)
  984. + xySignBits
  985. )
  986. triplets.append((absX - 1) & 0xFF)
  987. triplets.append((absY - 1) & 0xFF)
  988. elif absX < 4096 and absY < 4096:
  989. flags.append(onCurveBit + 120 + xySignBits)
  990. triplets.append(absX >> 4)
  991. triplets.append(((absX & 0xF) << 4) | (absY >> 8))
  992. triplets.append(absY & 0xFF)
  993. else:
  994. flags.append(onCurveBit + 124 + xySignBits)
  995. triplets.append(absX >> 8)
  996. triplets.append(absX & 0xFF)
  997. triplets.append(absY >> 8)
  998. triplets.append(absY & 0xFF)
  999. self.flagStream += flags.tobytes()
  1000. self.glyphStream += triplets.tobytes()
  1001. class WOFF2HmtxTable(getTableClass("hmtx")):
  1002. def __init__(self, tag=None):
  1003. self.tableTag = Tag(tag or "hmtx")
  1004. def reconstruct(self, data, ttFont):
  1005. (flags,) = struct.unpack(">B", data[:1])
  1006. data = data[1:]
  1007. if flags & 0b11111100 != 0:
  1008. raise TTLibError("Bits 2-7 of '%s' flags are reserved" % self.tableTag)
  1009. # When bit 0 is _not_ set, the lsb[] array is present
  1010. hasLsbArray = flags & 1 == 0
  1011. # When bit 1 is _not_ set, the leftSideBearing[] array is present
  1012. hasLeftSideBearingArray = flags & 2 == 0
  1013. if hasLsbArray and hasLeftSideBearingArray:
  1014. raise TTLibError(
  1015. "either bits 0 or 1 (or both) must set in transformed '%s' flags"
  1016. % self.tableTag
  1017. )
  1018. glyfTable = ttFont["glyf"]
  1019. headerTable = ttFont["hhea"]
  1020. glyphOrder = glyfTable.glyphOrder
  1021. numGlyphs = len(glyphOrder)
  1022. numberOfHMetrics = min(int(headerTable.numberOfHMetrics), numGlyphs)
  1023. assert len(data) >= 2 * numberOfHMetrics
  1024. advanceWidthArray = array.array("H", data[: 2 * numberOfHMetrics])
  1025. if sys.byteorder != "big":
  1026. advanceWidthArray.byteswap()
  1027. data = data[2 * numberOfHMetrics :]
  1028. if hasLsbArray:
  1029. assert len(data) >= 2 * numberOfHMetrics
  1030. lsbArray = array.array("h", data[: 2 * numberOfHMetrics])
  1031. if sys.byteorder != "big":
  1032. lsbArray.byteswap()
  1033. data = data[2 * numberOfHMetrics :]
  1034. else:
  1035. # compute (proportional) glyphs' lsb from their xMin
  1036. lsbArray = array.array("h")
  1037. for i, glyphName in enumerate(glyphOrder):
  1038. if i >= numberOfHMetrics:
  1039. break
  1040. glyph = glyfTable[glyphName]
  1041. xMin = getattr(glyph, "xMin", 0)
  1042. lsbArray.append(xMin)
  1043. numberOfSideBearings = numGlyphs - numberOfHMetrics
  1044. if hasLeftSideBearingArray:
  1045. assert len(data) >= 2 * numberOfSideBearings
  1046. leftSideBearingArray = array.array("h", data[: 2 * numberOfSideBearings])
  1047. if sys.byteorder != "big":
  1048. leftSideBearingArray.byteswap()
  1049. data = data[2 * numberOfSideBearings :]
  1050. else:
  1051. # compute (monospaced) glyphs' leftSideBearing from their xMin
  1052. leftSideBearingArray = array.array("h")
  1053. for i, glyphName in enumerate(glyphOrder):
  1054. if i < numberOfHMetrics:
  1055. continue
  1056. glyph = glyfTable[glyphName]
  1057. xMin = getattr(glyph, "xMin", 0)
  1058. leftSideBearingArray.append(xMin)
  1059. if data:
  1060. raise TTLibError("too much '%s' table data" % self.tableTag)
  1061. self.metrics = {}
  1062. for i in range(numberOfHMetrics):
  1063. glyphName = glyphOrder[i]
  1064. advanceWidth, lsb = advanceWidthArray[i], lsbArray[i]
  1065. self.metrics[glyphName] = (advanceWidth, lsb)
  1066. lastAdvance = advanceWidthArray[-1]
  1067. for i in range(numberOfSideBearings):
  1068. glyphName = glyphOrder[i + numberOfHMetrics]
  1069. self.metrics[glyphName] = (lastAdvance, leftSideBearingArray[i])
  1070. def transform(self, ttFont):
  1071. glyphOrder = ttFont.getGlyphOrder()
  1072. glyf = ttFont["glyf"]
  1073. hhea = ttFont["hhea"]
  1074. numberOfHMetrics = hhea.numberOfHMetrics
  1075. # check if any of the proportional glyphs has left sidebearings that
  1076. # differ from their xMin bounding box values.
  1077. hasLsbArray = False
  1078. for i in range(numberOfHMetrics):
  1079. glyphName = glyphOrder[i]
  1080. lsb = self.metrics[glyphName][1]
  1081. if lsb != getattr(glyf[glyphName], "xMin", 0):
  1082. hasLsbArray = True
  1083. break
  1084. # do the same for the monospaced glyphs (if any) at the end of hmtx table
  1085. hasLeftSideBearingArray = False
  1086. for i in range(numberOfHMetrics, len(glyphOrder)):
  1087. glyphName = glyphOrder[i]
  1088. lsb = self.metrics[glyphName][1]
  1089. if lsb != getattr(glyf[glyphName], "xMin", 0):
  1090. hasLeftSideBearingArray = True
  1091. break
  1092. # if we need to encode both sidebearings arrays, then no transformation is
  1093. # applicable, and we must use the untransformed hmtx data
  1094. if hasLsbArray and hasLeftSideBearingArray:
  1095. return
  1096. # set bit 0 and 1 when the respective arrays are _not_ present
  1097. flags = 0
  1098. if not hasLsbArray:
  1099. flags |= 1 << 0
  1100. if not hasLeftSideBearingArray:
  1101. flags |= 1 << 1
  1102. data = struct.pack(">B", flags)
  1103. advanceWidthArray = array.array(
  1104. "H",
  1105. [
  1106. self.metrics[glyphName][0]
  1107. for i, glyphName in enumerate(glyphOrder)
  1108. if i < numberOfHMetrics
  1109. ],
  1110. )
  1111. if sys.byteorder != "big":
  1112. advanceWidthArray.byteswap()
  1113. data += advanceWidthArray.tobytes()
  1114. if hasLsbArray:
  1115. lsbArray = array.array(
  1116. "h",
  1117. [
  1118. self.metrics[glyphName][1]
  1119. for i, glyphName in enumerate(glyphOrder)
  1120. if i < numberOfHMetrics
  1121. ],
  1122. )
  1123. if sys.byteorder != "big":
  1124. lsbArray.byteswap()
  1125. data += lsbArray.tobytes()
  1126. if hasLeftSideBearingArray:
  1127. leftSideBearingArray = array.array(
  1128. "h",
  1129. [
  1130. self.metrics[glyphOrder[i]][1]
  1131. for i in range(numberOfHMetrics, len(glyphOrder))
  1132. ],
  1133. )
  1134. if sys.byteorder != "big":
  1135. leftSideBearingArray.byteswap()
  1136. data += leftSideBearingArray.tobytes()
  1137. return data
  1138. class WOFF2FlavorData(WOFFFlavorData):
  1139. Flavor = "woff2"
  1140. def __init__(self, reader=None, data=None, transformedTables=None):
  1141. """Data class that holds the WOFF2 header major/minor version, any
  1142. metadata or private data (as bytes strings), and the set of
  1143. table tags that have transformations applied (if reader is not None),
  1144. or will have once the WOFF2 font is compiled.
  1145. Args:
  1146. reader: an SFNTReader (or subclass) object to read flavor data from.
  1147. data: another WOFFFlavorData object to initialise data from.
  1148. transformedTables: set of strings containing table tags to be transformed.
  1149. Raises:
  1150. ImportError if the brotli module is not installed.
  1151. NOTE: The 'reader' argument, on the one hand, and the 'data' and
  1152. 'transformedTables' arguments, on the other hand, are mutually exclusive.
  1153. """
  1154. if not haveBrotli:
  1155. raise ImportError("No module named brotli")
  1156. if reader is not None:
  1157. if data is not None:
  1158. raise TypeError("'reader' and 'data' arguments are mutually exclusive")
  1159. if transformedTables is not None:
  1160. raise TypeError(
  1161. "'reader' and 'transformedTables' arguments are mutually exclusive"
  1162. )
  1163. if transformedTables is not None and (
  1164. "glyf" in transformedTables
  1165. and "loca" not in transformedTables
  1166. or "loca" in transformedTables
  1167. and "glyf" not in transformedTables
  1168. ):
  1169. raise ValueError("'glyf' and 'loca' must be transformed (or not) together")
  1170. super(WOFF2FlavorData, self).__init__(reader=reader)
  1171. if reader:
  1172. transformedTables = [
  1173. tag for tag, entry in reader.tables.items() if entry.transformed
  1174. ]
  1175. elif data:
  1176. self.majorVersion = data.majorVersion
  1177. self.majorVersion = data.minorVersion
  1178. self.metaData = data.metaData
  1179. self.privData = data.privData
  1180. if transformedTables is None and hasattr(data, "transformedTables"):
  1181. transformedTables = data.transformedTables
  1182. if transformedTables is None:
  1183. transformedTables = woff2TransformedTableTags
  1184. self.transformedTables = set(transformedTables)
  1185. def _decompress(self, rawData):
  1186. return brotli.decompress(rawData)
  1187. def unpackBase128(data):
  1188. r"""Read one to five bytes from UIntBase128-encoded input string, and return
  1189. a tuple containing the decoded integer plus any leftover data.
  1190. >>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00")
  1191. True
  1192. >>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295
  1193. True
  1194. >>> unpackBase128(b'\x80\x80\x3f') # doctest: +IGNORE_EXCEPTION_DETAIL
  1195. Traceback (most recent call last):
  1196. File "<stdin>", line 1, in ?
  1197. TTLibError: UIntBase128 value must not start with leading zeros
  1198. >>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0] # doctest: +IGNORE_EXCEPTION_DETAIL
  1199. Traceback (most recent call last):
  1200. File "<stdin>", line 1, in ?
  1201. TTLibError: UIntBase128-encoded sequence is longer than 5 bytes
  1202. >>> unpackBase128(b'\x90\x80\x80\x80\x00')[0] # doctest: +IGNORE_EXCEPTION_DETAIL
  1203. Traceback (most recent call last):
  1204. File "<stdin>", line 1, in ?
  1205. TTLibError: UIntBase128 value exceeds 2**32-1
  1206. """
  1207. if len(data) == 0:
  1208. raise TTLibError("not enough data to unpack UIntBase128")
  1209. result = 0
  1210. if byteord(data[0]) == 0x80:
  1211. # font must be rejected if UIntBase128 value starts with 0x80
  1212. raise TTLibError("UIntBase128 value must not start with leading zeros")
  1213. for i in range(woff2Base128MaxSize):
  1214. if len(data) == 0:
  1215. raise TTLibError("not enough data to unpack UIntBase128")
  1216. code = byteord(data[0])
  1217. data = data[1:]
  1218. # if any of the top seven bits are set then we're about to overflow
  1219. if result & 0xFE000000:
  1220. raise TTLibError("UIntBase128 value exceeds 2**32-1")
  1221. # set current value = old value times 128 bitwise-or (byte bitwise-and 127)
  1222. result = (result << 7) | (code & 0x7F)
  1223. # repeat until the most significant bit of byte is false
  1224. if (code & 0x80) == 0:
  1225. # return result plus left over data
  1226. return result, data
  1227. # make sure not to exceed the size bound
  1228. raise TTLibError("UIntBase128-encoded sequence is longer than 5 bytes")
  1229. def base128Size(n):
  1230. """Return the length in bytes of a UIntBase128-encoded sequence with value n.
  1231. >>> base128Size(0)
  1232. 1
  1233. >>> base128Size(24567)
  1234. 3
  1235. >>> base128Size(2**32-1)
  1236. 5
  1237. """
  1238. assert n >= 0
  1239. size = 1
  1240. while n >= 128:
  1241. size += 1
  1242. n >>= 7
  1243. return size
  1244. def packBase128(n):
  1245. r"""Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of
  1246. bytes using UIntBase128 variable-length encoding. Produce the shortest possible
  1247. encoding.
  1248. >>> packBase128(63) == b"\x3f"
  1249. True
  1250. >>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f'
  1251. True
  1252. """
  1253. if n < 0 or n >= 2**32:
  1254. raise TTLibError("UIntBase128 format requires 0 <= integer <= 2**32-1")
  1255. data = b""
  1256. size = base128Size(n)
  1257. for i in range(size):
  1258. b = (n >> (7 * (size - i - 1))) & 0x7F
  1259. if i < size - 1:
  1260. b |= 0x80
  1261. data += struct.pack("B", b)
  1262. return data
  1263. def unpack255UShort(data):
  1264. """Read one to three bytes from 255UInt16-encoded input string, and return a
  1265. tuple containing the decoded integer plus any leftover data.
  1266. >>> unpack255UShort(bytechr(252))[0]
  1267. 252
  1268. Note that some numbers (e.g. 506) can have multiple encodings:
  1269. >>> unpack255UShort(struct.pack("BB", 254, 0))[0]
  1270. 506
  1271. >>> unpack255UShort(struct.pack("BB", 255, 253))[0]
  1272. 506
  1273. >>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0]
  1274. 506
  1275. """
  1276. code = byteord(data[:1])
  1277. data = data[1:]
  1278. if code == 253:
  1279. # read two more bytes as an unsigned short
  1280. if len(data) < 2:
  1281. raise TTLibError("not enough data to unpack 255UInt16")
  1282. (result,) = struct.unpack(">H", data[:2])
  1283. data = data[2:]
  1284. elif code == 254:
  1285. # read another byte, plus 253 * 2
  1286. if len(data) == 0:
  1287. raise TTLibError("not enough data to unpack 255UInt16")
  1288. result = byteord(data[:1])
  1289. result += 506
  1290. data = data[1:]
  1291. elif code == 255:
  1292. # read another byte, plus 253
  1293. if len(data) == 0:
  1294. raise TTLibError("not enough data to unpack 255UInt16")
  1295. result = byteord(data[:1])
  1296. result += 253
  1297. data = data[1:]
  1298. else:
  1299. # leave as is if lower than 253
  1300. result = code
  1301. # return result plus left over data
  1302. return result, data
  1303. def pack255UShort(value):
  1304. r"""Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring
  1305. using 255UInt16 variable-length encoding.
  1306. >>> pack255UShort(252) == b'\xfc'
  1307. True
  1308. >>> pack255UShort(506) == b'\xfe\x00'
  1309. True
  1310. >>> pack255UShort(762) == b'\xfd\x02\xfa'
  1311. True
  1312. """
  1313. if value < 0 or value > 0xFFFF:
  1314. raise TTLibError("255UInt16 format requires 0 <= integer <= 65535")
  1315. if value < 253:
  1316. return struct.pack(">B", value)
  1317. elif value < 506:
  1318. return struct.pack(">BB", 255, value - 253)
  1319. elif value < 762:
  1320. return struct.pack(">BB", 254, value - 506)
  1321. else:
  1322. return struct.pack(">BH", 253, value)
  1323. def compress(input_file, output_file, transform_tables=None):
  1324. """Compress OpenType font to WOFF2.
  1325. Args:
  1326. input_file: a file path, file or file-like object (open in binary mode)
  1327. containing an OpenType font (either CFF- or TrueType-flavored).
  1328. output_file: a file path, file or file-like object where to save the
  1329. compressed WOFF2 font.
  1330. transform_tables: Optional[Iterable[str]]: a set of table tags for which
  1331. to enable preprocessing transformations. By default, only 'glyf'
  1332. and 'loca' tables are transformed. An empty set means disable all
  1333. transformations.
  1334. """
  1335. log.info("Processing %s => %s" % (input_file, output_file))
  1336. font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
  1337. font.flavor = "woff2"
  1338. if transform_tables is not None:
  1339. font.flavorData = WOFF2FlavorData(
  1340. data=font.flavorData, transformedTables=transform_tables
  1341. )
  1342. font.save(output_file, reorderTables=False)
  1343. def decompress(input_file, output_file):
  1344. """Decompress WOFF2 font to OpenType font.
  1345. Args:
  1346. input_file: a file path, file or file-like object (open in binary mode)
  1347. containing a compressed WOFF2 font.
  1348. output_file: a file path, file or file-like object where to save the
  1349. decompressed OpenType font.
  1350. """
  1351. log.info("Processing %s => %s" % (input_file, output_file))
  1352. font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
  1353. font.flavor = None
  1354. font.flavorData = None
  1355. font.save(output_file, reorderTables=True)
  1356. def main(args=None):
  1357. """Compress and decompress WOFF2 fonts"""
  1358. import argparse
  1359. from fontTools import configLogger
  1360. from fontTools.ttx import makeOutputFileName
  1361. class _HelpAction(argparse._HelpAction):
  1362. def __call__(self, parser, namespace, values, option_string=None):
  1363. subparsers_actions = [
  1364. action
  1365. for action in parser._actions
  1366. if isinstance(action, argparse._SubParsersAction)
  1367. ]
  1368. for subparsers_action in subparsers_actions:
  1369. for choice, subparser in subparsers_action.choices.items():
  1370. print(subparser.format_help())
  1371. parser.exit()
  1372. class _NoGlyfTransformAction(argparse.Action):
  1373. def __call__(self, parser, namespace, values, option_string=None):
  1374. namespace.transform_tables.difference_update({"glyf", "loca"})
  1375. class _HmtxTransformAction(argparse.Action):
  1376. def __call__(self, parser, namespace, values, option_string=None):
  1377. namespace.transform_tables.add("hmtx")
  1378. parser = argparse.ArgumentParser(
  1379. prog="fonttools ttLib.woff2", description=main.__doc__, add_help=False
  1380. )
  1381. parser.add_argument(
  1382. "-h", "--help", action=_HelpAction, help="show this help message and exit"
  1383. )
  1384. parser_group = parser.add_subparsers(title="sub-commands")
  1385. parser_compress = parser_group.add_parser(
  1386. "compress", description="Compress a TTF or OTF font to WOFF2"
  1387. )
  1388. parser_decompress = parser_group.add_parser(
  1389. "decompress", description="Decompress a WOFF2 font to OTF"
  1390. )
  1391. for subparser in (parser_compress, parser_decompress):
  1392. group = subparser.add_mutually_exclusive_group(required=False)
  1393. group.add_argument(
  1394. "-v",
  1395. "--verbose",
  1396. action="store_true",
  1397. help="print more messages to console",
  1398. )
  1399. group.add_argument(
  1400. "-q",
  1401. "--quiet",
  1402. action="store_true",
  1403. help="do not print messages to console",
  1404. )
  1405. parser_compress.add_argument(
  1406. "input_file",
  1407. metavar="INPUT",
  1408. help="the input OpenType font (.ttf or .otf)",
  1409. )
  1410. parser_decompress.add_argument(
  1411. "input_file",
  1412. metavar="INPUT",
  1413. help="the input WOFF2 font",
  1414. )
  1415. parser_compress.add_argument(
  1416. "-o",
  1417. "--output-file",
  1418. metavar="OUTPUT",
  1419. help="the output WOFF2 font",
  1420. )
  1421. parser_decompress.add_argument(
  1422. "-o",
  1423. "--output-file",
  1424. metavar="OUTPUT",
  1425. help="the output OpenType font",
  1426. )
  1427. transform_group = parser_compress.add_argument_group()
  1428. transform_group.add_argument(
  1429. "--no-glyf-transform",
  1430. dest="transform_tables",
  1431. nargs=0,
  1432. action=_NoGlyfTransformAction,
  1433. help="Do not transform glyf (and loca) tables",
  1434. )
  1435. transform_group.add_argument(
  1436. "--hmtx-transform",
  1437. dest="transform_tables",
  1438. nargs=0,
  1439. action=_HmtxTransformAction,
  1440. help="Enable optional transformation for 'hmtx' table",
  1441. )
  1442. parser_compress.set_defaults(
  1443. subcommand=compress,
  1444. transform_tables={"glyf", "loca"},
  1445. )
  1446. parser_decompress.set_defaults(subcommand=decompress)
  1447. options = vars(parser.parse_args(args))
  1448. subcommand = options.pop("subcommand", None)
  1449. if not subcommand:
  1450. parser.print_help()
  1451. return
  1452. quiet = options.pop("quiet")
  1453. verbose = options.pop("verbose")
  1454. configLogger(
  1455. level=("ERROR" if quiet else "DEBUG" if verbose else "INFO"),
  1456. )
  1457. if not options["output_file"]:
  1458. if subcommand is compress:
  1459. extension = ".woff2"
  1460. elif subcommand is decompress:
  1461. # choose .ttf/.otf file extension depending on sfntVersion
  1462. with open(options["input_file"], "rb") as f:
  1463. f.seek(4) # skip 'wOF2' signature
  1464. sfntVersion = f.read(4)
  1465. assert len(sfntVersion) == 4, "not enough data"
  1466. extension = ".otf" if sfntVersion == b"OTTO" else ".ttf"
  1467. else:
  1468. raise AssertionError(subcommand)
  1469. options["output_file"] = makeOutputFileName(
  1470. options["input_file"], outputDir=None, extension=extension
  1471. )
  1472. try:
  1473. subcommand(**options)
  1474. except TTLibError as e:
  1475. parser.error(e)
  1476. if __name__ == "__main__":
  1477. sys.exit(main())