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