2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
TrueType Font parser.
|
|
|
|
|
|
|
|
Documents:
|
2023-10-07 23:04:41 +00:00
|
|
|
- "The OpenType Specification"
|
|
|
|
https://docs.microsoft.com/en-us/typography/opentype/spec/
|
2023-01-12 01:04:47 +00:00
|
|
|
- "An Introduction to TrueType Fonts: A look inside the TTF format"
|
|
|
|
written by "NRSI: Computers & Writing Systems"
|
|
|
|
http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter08
|
|
|
|
|
|
|
|
Author: Victor Stinner
|
|
|
|
Creation date: 2007-02-08
|
|
|
|
"""
|
|
|
|
|
|
|
|
from hachoir.parser import Parser
|
2023-10-07 23:04:41 +00:00
|
|
|
from hachoir.field import (
|
|
|
|
FieldSet,
|
|
|
|
ParserError,
|
|
|
|
UInt8,
|
|
|
|
UInt16,
|
|
|
|
UInt24,
|
|
|
|
UInt32,
|
|
|
|
Int16,
|
|
|
|
Bit,
|
|
|
|
Bits,
|
|
|
|
PaddingBits,
|
|
|
|
NullBytes,
|
|
|
|
String,
|
|
|
|
RawBytes,
|
|
|
|
Bytes,
|
|
|
|
Enum,
|
|
|
|
TimestampMac32,
|
|
|
|
GenericVector,
|
|
|
|
PascalString8,
|
|
|
|
)
|
2023-01-12 01:04:47 +00:00
|
|
|
from hachoir.core.endian import BIG_ENDIAN
|
|
|
|
from hachoir.core.text_handler import textHandler, hexadecimal, filesizeHandler
|
|
|
|
|
|
|
|
MAX_NAME_COUNT = 300
|
|
|
|
MIN_NB_TABLE = 3
|
|
|
|
MAX_NB_TABLE = 30
|
|
|
|
|
|
|
|
DIRECTION_NAME = {
|
|
|
|
0: "Mixed directional",
|
|
|
|
1: "Left to right",
|
|
|
|
2: "Left to right + neutrals",
|
|
|
|
-1: "Right to left",
|
|
|
|
-2: "Right to left + neutrals",
|
|
|
|
}
|
|
|
|
|
|
|
|
NAMEID_NAME = {
|
|
|
|
0: "Copyright notice",
|
|
|
|
1: "Font family name",
|
|
|
|
2: "Font subfamily name",
|
|
|
|
3: "Unique font identifier",
|
|
|
|
4: "Full font name",
|
|
|
|
5: "Version string",
|
|
|
|
6: "Postscript name",
|
|
|
|
7: "Trademark",
|
|
|
|
8: "Manufacturer name",
|
|
|
|
9: "Designer",
|
|
|
|
10: "Description",
|
|
|
|
11: "URL Vendor",
|
|
|
|
12: "URL Designer",
|
|
|
|
13: "License Description",
|
|
|
|
14: "License info URL",
|
|
|
|
16: "Preferred Family",
|
|
|
|
17: "Preferred Subfamily",
|
|
|
|
18: "Compatible Full",
|
|
|
|
19: "Sample text",
|
|
|
|
20: "PostScript CID findfont name",
|
|
|
|
}
|
|
|
|
|
|
|
|
PLATFORM_NAME = {
|
|
|
|
0: "Unicode",
|
|
|
|
1: "Macintosh",
|
|
|
|
2: "ISO",
|
|
|
|
3: "Microsoft",
|
|
|
|
4: "Custom",
|
|
|
|
}
|
|
|
|
|
|
|
|
CHARSET_MAP = {
|
|
|
|
# (platform, encoding) => charset
|
|
|
|
0: {3: "UTF-16-BE"},
|
|
|
|
1: {0: "MacRoman"},
|
|
|
|
3: {1: "UTF-16-BE"},
|
|
|
|
}
|
|
|
|
|
2023-10-07 23:04:41 +00:00
|
|
|
PERMISSIONS = {
|
|
|
|
0: "Installable embedding",
|
|
|
|
2: "Restricted License embedding",
|
|
|
|
4: "Preview & Print embedding",
|
|
|
|
8: "Editable embedding",
|
|
|
|
}
|
2023-01-12 01:04:47 +00:00
|
|
|
|
2023-10-07 23:04:41 +00:00
|
|
|
FWORD = Int16
|
|
|
|
UFWORD = UInt16
|
|
|
|
|
|
|
|
|
|
|
|
class Tag(String):
|
|
|
|
def __init__(self, parent, name, description=None):
|
|
|
|
String.__init__(self, parent, name, 4, description)
|
|
|
|
|
|
|
|
|
|
|
|
class Version16Dot16(FieldSet):
|
|
|
|
static_size = 32
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
def createFields(self):
|
2023-10-07 23:04:41 +00:00
|
|
|
yield UInt16(self, "major")
|
|
|
|
yield UInt16(self, "minor")
|
|
|
|
|
|
|
|
def createValue(self):
|
|
|
|
return float("%u.%x" % (self["major"].value, self["minor"].value))
|
|
|
|
|
|
|
|
|
|
|
|
class Fixed(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(self, "int_part")
|
|
|
|
yield UInt16(self, "float_part")
|
|
|
|
|
|
|
|
def createValue(self):
|
|
|
|
return self["int_part"].value + float(self["float_part"].value) / 65536
|
|
|
|
|
|
|
|
|
|
|
|
class Tuple(FieldSet):
|
|
|
|
def __init__(self, parent, name, axisCount):
|
|
|
|
super().__init__(parent, name, description="Tuple Record")
|
|
|
|
self.axisCount = axisCount
|
|
|
|
|
|
|
|
def createFields(self):
|
|
|
|
for _ in range(self.axisCount):
|
|
|
|
yield (Fixed(self, "coordinate[]"))
|
|
|
|
|
|
|
|
|
|
|
|
class F2DOT14(FieldSet):
|
|
|
|
static_size = 16
|
|
|
|
|
|
|
|
def createFields(self):
|
|
|
|
yield Int16(self, "int_part")
|
|
|
|
|
|
|
|
def createValue(self):
|
|
|
|
return self["int_part"].value / 16384
|
|
|
|
|
|
|
|
|
|
|
|
class TableHeader(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield Tag(self, "tag")
|
2023-01-12 01:04:47 +00:00
|
|
|
yield textHandler(UInt32(self, "checksum"), hexadecimal)
|
|
|
|
yield UInt32(self, "offset")
|
|
|
|
yield filesizeHandler(UInt32(self, "size"))
|
|
|
|
|
|
|
|
def createDescription(self):
|
|
|
|
return "Table entry: %s (%s)" % (self["tag"].display, self["size"].display)
|
|
|
|
|
|
|
|
|
|
|
|
class NameHeader(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield Enum(UInt16(self, "platformID"), PLATFORM_NAME)
|
|
|
|
yield UInt16(self, "encodingID")
|
|
|
|
yield UInt16(self, "languageID")
|
|
|
|
yield Enum(UInt16(self, "nameID"), NAMEID_NAME)
|
|
|
|
yield UInt16(self, "length")
|
|
|
|
yield UInt16(self, "offset")
|
|
|
|
|
|
|
|
def getCharset(self):
|
|
|
|
platform = self["platformID"].value
|
|
|
|
encoding = self["encodingID"].value
|
|
|
|
try:
|
|
|
|
return CHARSET_MAP[platform][encoding]
|
|
|
|
except KeyError:
|
|
|
|
self.warning("TTF: Unknown charset (%s,%s)" % (platform, encoding))
|
|
|
|
return "ISO-8859-1"
|
|
|
|
|
|
|
|
def createDescription(self):
|
|
|
|
platform = self["platformID"].display
|
|
|
|
name = self["nameID"].display
|
|
|
|
return "Name record: %s (%s)" % (name, platform)
|
|
|
|
|
|
|
|
|
|
|
|
def parseFontHeader(self):
|
|
|
|
yield UInt16(self, "maj_ver", "Major version")
|
|
|
|
yield UInt16(self, "min_ver", "Minor version")
|
|
|
|
yield UInt16(self, "font_maj_ver", "Font major version")
|
|
|
|
yield UInt16(self, "font_min_ver", "Font minor version")
|
|
|
|
yield textHandler(UInt32(self, "checksum"), hexadecimal)
|
|
|
|
yield Bytes(self, "magic", 4, r"Magic string (\x5F\x0F\x3C\xF5)")
|
|
|
|
if self["magic"].value != b"\x5F\x0F\x3C\xF5":
|
|
|
|
raise ParserError("TTF: invalid magic of font header")
|
|
|
|
|
|
|
|
# Flags
|
|
|
|
yield Bit(self, "y0", "Baseline at y=0")
|
|
|
|
yield Bit(self, "x0", "Left sidebearing point at x=0")
|
|
|
|
yield Bit(self, "instr_point", "Instructions may depend on point size")
|
|
|
|
yield Bit(self, "ppem", "Force PPEM to integer values for all")
|
|
|
|
yield Bit(self, "instr_width", "Instructions may alter advance width")
|
|
|
|
yield Bit(self, "vertical", "e laid out vertically?")
|
|
|
|
yield PaddingBits(self, "reserved[]", 1)
|
|
|
|
yield Bit(self, "linguistic", "Requires layout for correct linguistic rendering?")
|
|
|
|
yield Bit(self, "gx", "Metamorphosis effects?")
|
|
|
|
yield Bit(self, "strong", "Contains strong right-to-left glyphs?")
|
|
|
|
yield Bit(self, "indic", "contains Indic-style rearrangement effects?")
|
|
|
|
yield Bit(self, "lossless", "Data is lossless (Agfa MicroType compression)")
|
|
|
|
yield Bit(self, "converted", "Font converted (produce compatible metrics)")
|
|
|
|
yield Bit(self, "cleartype", "Optimised for ClearType")
|
|
|
|
yield Bits(self, "adobe", 2, "(used by Adobe)")
|
|
|
|
|
|
|
|
yield UInt16(self, "unit_per_em", "Units per em")
|
2023-10-07 23:04:41 +00:00
|
|
|
if not (16 <= self["unit_per_em"].value <= 16384):
|
2023-01-12 01:04:47 +00:00
|
|
|
raise ParserError("TTF: Invalid unit/em value")
|
|
|
|
yield UInt32(self, "created_high")
|
|
|
|
yield TimestampMac32(self, "created")
|
|
|
|
yield UInt32(self, "modified_high")
|
|
|
|
yield TimestampMac32(self, "modified")
|
|
|
|
yield UInt16(self, "xmin")
|
|
|
|
yield UInt16(self, "ymin")
|
|
|
|
yield UInt16(self, "xmax")
|
|
|
|
yield UInt16(self, "ymax")
|
|
|
|
|
|
|
|
# Mac style
|
|
|
|
yield Bit(self, "bold")
|
|
|
|
yield Bit(self, "italic")
|
|
|
|
yield Bit(self, "underline")
|
|
|
|
yield Bit(self, "outline")
|
|
|
|
yield Bit(self, "shadow")
|
|
|
|
yield Bit(self, "condensed", "(narrow)")
|
|
|
|
yield Bit(self, "expanded")
|
|
|
|
yield PaddingBits(self, "reserved[]", 9)
|
|
|
|
|
|
|
|
yield UInt16(self, "lowest", "Smallest readable size in pixels")
|
|
|
|
yield Enum(UInt16(self, "font_dir", "Font direction hint"), DIRECTION_NAME)
|
|
|
|
yield Enum(UInt16(self, "ofst_format"), {0: "short offsets", 1: "long"})
|
|
|
|
yield UInt16(self, "glyph_format", "(=0)")
|
|
|
|
|
|
|
|
|
2023-10-07 23:04:41 +00:00
|
|
|
class AxisValueMap(FieldSet):
|
|
|
|
static_size = 32
|
|
|
|
|
|
|
|
def createFields(self):
|
|
|
|
yield F2DOT14(self, "fromCoordinate")
|
|
|
|
yield F2DOT14(self, "toCoordinate")
|
|
|
|
|
|
|
|
|
|
|
|
class SegmentMaps(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(
|
|
|
|
self, "positionMapCount", "The number of correspondence pairs for this axis"
|
|
|
|
)
|
|
|
|
for _ in range(self["positionMapCount"].value):
|
|
|
|
yield (AxisValueMap(self, "axisValueMaps[]"))
|
|
|
|
|
|
|
|
|
|
|
|
def parseAvar(self):
|
|
|
|
yield UInt16(self, "majorVersion", "Major version")
|
|
|
|
yield UInt16(self, "minorVersion", "Minor version")
|
|
|
|
yield PaddingBits(self, "reserved[]", 16)
|
|
|
|
yield UInt16(self, "axisCount", "The number of variation axes for this font")
|
|
|
|
for _ in range(self["axisCount"].value):
|
|
|
|
yield (SegmentMaps(self, "segmentMaps[]"))
|
|
|
|
|
|
|
|
|
|
|
|
class VariationAxisRecord(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield Tag(self, "axisTag", "Tag identifying the design variation for the axis")
|
|
|
|
yield Fixed(self, "minValue", "The minimum coordinate value for the axis")
|
|
|
|
yield Fixed(self, "defaultValue", "The default coordinate value for the axis")
|
|
|
|
yield Fixed(self, "maxValue", "The maximum coordinate value for the axis")
|
|
|
|
yield PaddingBits(self, "reservedFlags", 15)
|
|
|
|
yield Bit(
|
|
|
|
self, "hidden", "The axis should not be exposed directly in user interfaces"
|
|
|
|
)
|
|
|
|
yield UInt16(
|
|
|
|
self,
|
|
|
|
"axisNameID",
|
|
|
|
"The name ID for entries in the 'name' table that provide a display name for this axis",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class InstanceRecord(FieldSet):
|
|
|
|
def __init__(self, parent, name, axisCount, hasPSNameID=False):
|
|
|
|
super().__init__(parent, name, description="Instance record")
|
|
|
|
self.axisCount = axisCount
|
|
|
|
self.hasPSNameID = hasPSNameID
|
|
|
|
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(
|
|
|
|
self, "subfamilyNameID", "Name ID for subfamily names for this instance"
|
|
|
|
)
|
|
|
|
yield PaddingBits(self, "reservedFlags", 16)
|
|
|
|
yield Tuple(self, "coordinates", axisCount=self.axisCount)
|
|
|
|
if self.hasPSNameID:
|
|
|
|
yield UInt16(
|
|
|
|
self,
|
|
|
|
"postScriptNameID",
|
|
|
|
"Name ID for PostScript names for this instance",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def parseFvar(self):
|
|
|
|
yield UInt16(self, "majorVersion", "Major version")
|
|
|
|
yield UInt16(self, "minorVersion", "Minor version")
|
|
|
|
yield UInt16(
|
|
|
|
self, "axisArrayOffset", "Offset to the start of the VariationAxisRecord array."
|
|
|
|
)
|
|
|
|
yield PaddingBits(self, "reserved[]", 16)
|
|
|
|
yield UInt16(self, "axisCount", "The number of variation axes for this font")
|
|
|
|
yield UInt16(self, "axisSize", "The size in bytes of each VariationAxisRecord")
|
|
|
|
yield UInt16(self, "instanceCount", "The number of named instances for this font")
|
|
|
|
yield UInt16(self, "instanceSize", "The size in bytes of each InstanceRecord")
|
|
|
|
if self["axisArrayOffset"].value > 16:
|
|
|
|
yield PaddingBits(self, "padding", 8 * (self["axisArrayOffset"].value - 16))
|
|
|
|
for _ in range(self["axisCount"].value):
|
|
|
|
yield (VariationAxisRecord(self, "axes[]"))
|
|
|
|
for _ in range(self["instanceCount"].value):
|
|
|
|
yield (
|
|
|
|
InstanceRecord(
|
|
|
|
self,
|
|
|
|
"instances[]",
|
|
|
|
axisCount=self["axisCount"].value,
|
|
|
|
hasPSNameID=(
|
|
|
|
self["instanceSize"].value == (2 * self["axisCount"].value + 6)
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class EncodingRecord(FieldSet):
|
|
|
|
static_size = 64
|
|
|
|
|
|
|
|
def createFields(self):
|
|
|
|
yield Enum(UInt16(self, "platformID"), PLATFORM_NAME)
|
|
|
|
yield UInt16(self, "encodingID")
|
|
|
|
self.offset = UInt32(self, "subtableOffset")
|
|
|
|
yield self.offset
|
|
|
|
|
|
|
|
|
|
|
|
class CmapTable0(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(self, "format", "Table format")
|
|
|
|
yield UInt16(self, "length", "Length in bytes")
|
|
|
|
yield UInt16(self, "language", "Language ID")
|
|
|
|
yield GenericVector(self, "mapping", 256, UInt8)
|
|
|
|
|
|
|
|
|
|
|
|
class CmapTable4(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(self, "format", "Table format")
|
|
|
|
yield UInt16(self, "length", "Length in bytes")
|
|
|
|
yield UInt16(self, "language", "Language ID")
|
|
|
|
yield UInt16(self, "segCountX2", "Twice the number of segments")
|
|
|
|
segments = self["segCountX2"].value // 2
|
|
|
|
yield UInt16(self, "searchRange")
|
|
|
|
yield UInt16(self, "entrySelector")
|
|
|
|
yield UInt16(self, "rangeShift")
|
|
|
|
yield GenericVector(self, "endCode", segments, UInt16)
|
|
|
|
yield PaddingBits(self, "reserved[]", 16)
|
|
|
|
yield GenericVector(self, "startCode", segments, UInt16)
|
|
|
|
yield GenericVector(self, "idDelta", segments, Int16)
|
|
|
|
yield GenericVector(self, "idRangeOffsets", segments, UInt16)
|
|
|
|
remainder = (self["length"].value - (self.current_size / 8)) / 2
|
|
|
|
if remainder:
|
|
|
|
yield GenericVector(self, "glyphIdArray", remainder, UInt16)
|
|
|
|
|
|
|
|
|
|
|
|
class CmapTable6(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(self, "format", "Table format")
|
|
|
|
yield UInt16(self, "length", "Length in bytes")
|
|
|
|
yield UInt16(self, "language", "Language ID")
|
|
|
|
yield UInt16(self, "firstCode", "First character code of subrange")
|
|
|
|
yield UInt16(self, "entryCount", "Number of character codes in subrange")
|
|
|
|
yield GenericVector(self, "glyphIdArray", self["entryCount"].value, UInt16)
|
|
|
|
|
|
|
|
|
|
|
|
class SequentialMapGroup(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt32(self, "startCharCode", "First character code in this group")
|
|
|
|
yield UInt32(self, "endCharCode", "First character code in this group")
|
|
|
|
yield UInt32(
|
|
|
|
self,
|
|
|
|
"startGlyphID",
|
|
|
|
"Glyph index corresponding to the starting character code",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class CmapTable12(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(self, "format", "Table format")
|
|
|
|
yield PaddingBits(self, "reserved[]", 16)
|
|
|
|
yield UInt32(self, "length", "Length in bytes")
|
|
|
|
yield UInt32(self, "language", "Language ID")
|
|
|
|
yield UInt32(self, "numGroups", "Number of groupings which follow")
|
|
|
|
for i in range(self["numGroups"].value):
|
|
|
|
yield SequentialMapGroup(self, "mapgroup[]")
|
|
|
|
|
|
|
|
|
|
|
|
class VariationSelector(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt24(self, "varSelector", "Variation selector")
|
|
|
|
yield UInt32(self, "defaultUVSOffset", "Offset to default UVS table")
|
|
|
|
yield UInt32(self, "nonDefaultUVSOffset", "Offset to non-default UVS table")
|
|
|
|
|
|
|
|
|
|
|
|
class CmapTable14(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(self, "format", "Table format")
|
|
|
|
yield UInt32(self, "length", "Length in bytes")
|
|
|
|
yield UInt32(
|
|
|
|
self, "numVarSelectorRecords", "Number of variation selector records"
|
|
|
|
)
|
|
|
|
for i in range(self["numVarSelectorRecords"].value):
|
|
|
|
yield VariationSelector(self, "variationSelector[]")
|
|
|
|
|
|
|
|
|
|
|
|
def parseCmap(self):
|
|
|
|
yield UInt16(self, "version")
|
|
|
|
numTables = UInt16(self, "numTables", "Number of encoding tables")
|
|
|
|
yield numTables
|
|
|
|
encodingRecords = []
|
|
|
|
for index in range(numTables.value):
|
|
|
|
entry = EncodingRecord(self, "encodingRecords[]")
|
|
|
|
yield entry
|
|
|
|
encodingRecords.append(entry)
|
|
|
|
encodingRecords.sort(key=lambda field: field["subtableOffset"].value)
|
|
|
|
last = None
|
|
|
|
for er in encodingRecords:
|
|
|
|
offset = er["subtableOffset"].value
|
|
|
|
if last and last == offset:
|
|
|
|
continue
|
|
|
|
last = offset
|
|
|
|
|
|
|
|
# Add padding if any
|
|
|
|
padding = self.seekByte(offset, relative=True, null=False)
|
|
|
|
if padding:
|
|
|
|
yield padding
|
|
|
|
format = UInt16(self, "format").value
|
|
|
|
if format == 0:
|
|
|
|
yield CmapTable0(self, "cmap table format 0")
|
|
|
|
elif format == 4:
|
|
|
|
yield CmapTable4(self, "cmap table format 4")
|
|
|
|
elif format == 6:
|
|
|
|
yield CmapTable6(self, "cmap table format 6")
|
|
|
|
elif format == 12:
|
|
|
|
yield CmapTable12(self, "cmap table format 12")
|
|
|
|
elif format == 14:
|
|
|
|
yield CmapTable14(self, "cmap table format 14")
|
|
|
|
|
|
|
|
|
|
|
|
class SignatureRecord(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(self, "format", "Table format")
|
|
|
|
yield UInt16(self, "length", "Length of signature")
|
|
|
|
yield UInt16(self, "signatureBlockOffset", "Offset to signature block")
|
|
|
|
|
|
|
|
|
|
|
|
class SignatureBlock(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield PaddingBits(self, "reserved[]", 32)
|
|
|
|
yield UInt32(
|
|
|
|
self,
|
|
|
|
"length",
|
|
|
|
"Length (in bytes) of the PKCS#7 packet in the signature field",
|
|
|
|
)
|
|
|
|
yield String(self, "signature", self["length"].value, "Signature block")
|
|
|
|
|
|
|
|
|
|
|
|
def parseDSIG(self):
|
|
|
|
yield UInt32(self, "version")
|
|
|
|
yield UInt16(self, "numSignatures", "Number of signatures in the table")
|
|
|
|
yield Bit(self, "flag", "Cannot be resigned")
|
|
|
|
yield PaddingBits(self, "reserved[]", 7)
|
|
|
|
entries = []
|
|
|
|
for i in range(self["numSignatures"].value):
|
|
|
|
record = SignatureRecord(self, "signatureRecords[]")
|
|
|
|
entries.append(record)
|
|
|
|
yield record
|
|
|
|
entries.sort(key=lambda field: field["signatureBlockOffset"].value)
|
|
|
|
last = None
|
|
|
|
for entry in entries:
|
|
|
|
offset = entry["signatureBlockOffset"].value
|
|
|
|
if last and last == offset:
|
|
|
|
continue
|
|
|
|
last = offset
|
|
|
|
# Add padding if any
|
|
|
|
padding = self.seekByte(offset, relative=True, null=False)
|
|
|
|
if padding:
|
|
|
|
yield padding
|
|
|
|
|
|
|
|
padding = (self.size - self.current_size) // 8
|
|
|
|
if padding:
|
|
|
|
yield NullBytes(self, "padding_end", padding)
|
|
|
|
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
def parseNames(self):
|
|
|
|
# Read header
|
|
|
|
yield UInt16(self, "format")
|
|
|
|
if self["format"].value != 0:
|
2023-10-07 23:04:41 +00:00
|
|
|
raise ParserError("TTF (names): Invalid format (%u)" % self["format"].value)
|
2023-01-12 01:04:47 +00:00
|
|
|
yield UInt16(self, "count")
|
|
|
|
yield UInt16(self, "offset")
|
|
|
|
if MAX_NAME_COUNT < self["count"].value:
|
2023-10-07 23:04:41 +00:00
|
|
|
raise ParserError("Invalid number of names (%s)" % self["count"].value)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
# Read name index
|
|
|
|
entries = []
|
|
|
|
for index in range(self["count"].value):
|
|
|
|
entry = NameHeader(self, "header[]")
|
|
|
|
yield entry
|
|
|
|
entries.append(entry)
|
|
|
|
|
|
|
|
# Sort names by their offset
|
|
|
|
entries.sort(key=lambda field: field["offset"].value)
|
|
|
|
|
|
|
|
# Read name value
|
|
|
|
last = None
|
|
|
|
for entry in entries:
|
|
|
|
# Skip duplicates values
|
|
|
|
new = (entry["offset"].value, entry["length"].value)
|
|
|
|
if last and last == new:
|
|
|
|
self.warning("Skip duplicate %s %s" % (entry.name, new))
|
|
|
|
continue
|
|
|
|
last = (entry["offset"].value, entry["length"].value)
|
|
|
|
|
|
|
|
# Skip negative offset
|
|
|
|
offset = entry["offset"].value + self["offset"].value
|
|
|
|
if offset < self.current_size // 8:
|
|
|
|
self.warning("Skip value %s (negative offset)" % entry.name)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Add padding if any
|
|
|
|
padding = self.seekByte(offset, relative=True, null=True)
|
|
|
|
if padding:
|
|
|
|
yield padding
|
|
|
|
|
|
|
|
# Read value
|
|
|
|
size = entry["length"].value
|
|
|
|
if size:
|
2023-10-07 23:04:41 +00:00
|
|
|
yield String(
|
|
|
|
self, "value[]", size, entry.description, charset=entry.getCharset()
|
|
|
|
)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
padding = (self.size - self.current_size) // 8
|
|
|
|
if padding:
|
|
|
|
yield NullBytes(self, "padding_end", padding)
|
|
|
|
|
|
|
|
|
2023-10-07 23:04:41 +00:00
|
|
|
def parseMaxp(self):
|
|
|
|
# Read header
|
|
|
|
yield Version16Dot16(self, "format", "format version")
|
|
|
|
yield UInt16(self, "numGlyphs", "Number of glyphs")
|
|
|
|
if self["format"].value >= 1:
|
|
|
|
yield UInt16(self, "maxPoints", "Maximum points in a non-composite glyph")
|
|
|
|
yield UInt16(self, "maxContours", "Maximum contours in a non-composite glyph")
|
|
|
|
yield UInt16(self, "maxCompositePoints", "Maximum points in a composite glyph")
|
|
|
|
yield UInt16(
|
|
|
|
self, "maxCompositeContours", "Maximum contours in a composite glyph"
|
|
|
|
)
|
|
|
|
yield UInt16(self, "maxZones", "Do instructions use the twilight zone?")
|
|
|
|
yield UInt16(self, "maxTwilightPoints", "Maximum points used in Z0")
|
|
|
|
yield UInt16(self, "maxStorage", "Number of Storage Area locations")
|
|
|
|
yield UInt16(self, "maxFunctionDefs", "Number of function definitions")
|
|
|
|
yield UInt16(self, "maxInstructionDefs", "Number of instruction definitions")
|
|
|
|
yield UInt16(self, "maxStackElements", "Maximum stack depth")
|
|
|
|
yield UInt16(
|
|
|
|
self, "maxSizeOfInstructions", "Maximum byte count for glyph instructions"
|
|
|
|
)
|
|
|
|
yield UInt16(
|
|
|
|
self,
|
|
|
|
"maxComponentElements",
|
|
|
|
"Maximum number of components at glyph top level",
|
|
|
|
)
|
|
|
|
yield UInt16(self, "maxComponentDepth", "Maximum level of recursion")
|
|
|
|
|
|
|
|
|
|
|
|
def parseHhea(self):
|
|
|
|
yield UInt16(self, "majorVersion", "Major version")
|
|
|
|
yield UInt16(self, "minorVersion", "Minor version")
|
|
|
|
yield FWORD(self, "ascender", "Typographic ascent")
|
|
|
|
yield FWORD(self, "descender", "Typographic descent")
|
|
|
|
yield FWORD(self, "lineGap", "Typographic linegap")
|
|
|
|
yield UFWORD(self, "advanceWidthMax", "Maximum advance width")
|
|
|
|
yield FWORD(self, "minLeftSideBearing", "Minimum left sidebearing value")
|
|
|
|
yield FWORD(self, "minRightSideBearing", "Minimum right sidebearing value")
|
|
|
|
yield FWORD(self, "xMaxExtent", "Maximum X extent")
|
|
|
|
yield Int16(self, "caretSlopeRise", "Caret slope rise")
|
|
|
|
yield Int16(self, "caretSlopeRun", "Caret slope run")
|
|
|
|
yield Int16(self, "caretOffset", "Caret offset")
|
|
|
|
yield GenericVector(self, "reserved", 4, Int16)
|
|
|
|
yield Int16(self, "metricDataFormat", "Metric data format")
|
|
|
|
yield UInt16(self, "numberOfHMetrics", "Number of horizontal metrics")
|
|
|
|
|
|
|
|
|
|
|
|
class fsType(FieldSet):
|
|
|
|
def createFields(self):
|
|
|
|
yield Enum(Bits(self, "usage_permissions", 4), PERMISSIONS)
|
|
|
|
yield PaddingBits(self, "reserved[]", 4)
|
|
|
|
yield Bit(self, "no_subsetting", "Font may not be subsetted prior to embedding")
|
|
|
|
yield Bit(
|
|
|
|
self,
|
|
|
|
"bitmap_embedding",
|
|
|
|
"Only bitmaps contained in the font may be embedded",
|
|
|
|
)
|
|
|
|
yield PaddingBits(self, "reserved[]", 6)
|
|
|
|
|
|
|
|
|
|
|
|
def parseOS2(self):
|
|
|
|
yield UInt16(self, "version", "Table version")
|
|
|
|
yield Int16(self, "xAvgCharWidth")
|
|
|
|
yield UInt16(self, "usWeightClass")
|
|
|
|
yield UInt16(self, "usWidthClass")
|
|
|
|
yield fsType(self, "fsType")
|
|
|
|
yield Int16(self, "ySubscriptXSize")
|
|
|
|
yield Int16(self, "ySubscriptYSize")
|
|
|
|
yield Int16(self, "ySubscriptXOffset")
|
|
|
|
yield Int16(self, "ySubscriptYOffset")
|
|
|
|
yield Int16(self, "ySuperscriptXSize")
|
|
|
|
yield Int16(self, "ySuperscriptYSize")
|
|
|
|
yield Int16(self, "ySuperscriptXOffset")
|
|
|
|
yield Int16(self, "ySuperscriptYOffset")
|
|
|
|
yield Int16(self, "yStrikeoutSize")
|
|
|
|
yield Int16(self, "yStrikeoutPosition")
|
|
|
|
yield Int16(self, "sFamilyClass")
|
|
|
|
yield GenericVector(self, "panose", 10, UInt8)
|
|
|
|
yield UInt32(self, "ulUnicodeRange1")
|
|
|
|
yield UInt32(self, "ulUnicodeRange2")
|
|
|
|
yield UInt32(self, "ulUnicodeRange3")
|
|
|
|
yield UInt32(self, "ulUnicodeRange4")
|
|
|
|
yield Tag(self, "achVendID", "Vendor ID")
|
|
|
|
yield UInt16(self, "fsSelection")
|
|
|
|
yield UInt16(self, "usFirstCharIndex")
|
|
|
|
yield UInt16(self, "usLastCharIndex")
|
|
|
|
yield Int16(self, "sTypoAscender")
|
|
|
|
yield Int16(self, "sTypoDescender")
|
|
|
|
yield Int16(self, "sTypoLineGap")
|
|
|
|
yield UInt16(self, "usWinAscent")
|
|
|
|
yield UInt16(self, "usWinDescent")
|
|
|
|
if self["version"].value >= 1:
|
|
|
|
yield UInt32(self, "ulCodePageRange1")
|
|
|
|
yield UInt32(self, "ulCodePageRange2")
|
|
|
|
if self["version"].value >= 2:
|
|
|
|
yield Int16(self, "sxHeight")
|
|
|
|
yield Int16(self, "sCapHeight")
|
|
|
|
yield UInt16(self, "usDefaultChar")
|
|
|
|
yield UInt16(self, "usBreakChar")
|
|
|
|
yield UInt16(self, "usMaxContext")
|
|
|
|
if self["version"].value >= 5:
|
|
|
|
yield UInt16(self, "usLowerOpticalPointSize")
|
|
|
|
yield UInt16(self, "usUpperOpticalPointSize")
|
|
|
|
|
|
|
|
|
|
|
|
def parsePost(self):
|
|
|
|
yield Version16Dot16(self, "version", "Table version")
|
|
|
|
yield Fixed(
|
|
|
|
self,
|
|
|
|
"italicAngle",
|
|
|
|
"Italic angle in counter-clockwise degrees from the vertical.",
|
|
|
|
)
|
|
|
|
yield FWORD(self, "underlinePosition", "Top of underline to baseline")
|
|
|
|
yield FWORD(self, "underlineThickness", "Suggested underline thickness")
|
|
|
|
yield UInt32(self, "isFixedPitch", "Is the font fixed pitch?")
|
|
|
|
yield UInt32(self, "minMemType42", "Minimum memory usage (OpenType)")
|
|
|
|
yield UInt32(self, "maxMemType42", "Maximum memory usage (OpenType)")
|
|
|
|
yield UInt32(self, "minMemType1", "Minimum memory usage (Type 1)")
|
|
|
|
yield UInt32(self, "maxMemType1", "Maximum memory usage (Type 1)")
|
|
|
|
if self["version"].value == 2.0:
|
|
|
|
yield UInt16(self, "numGlyphs")
|
|
|
|
indices = GenericVector(
|
|
|
|
self,
|
|
|
|
"Array of indices into the string data",
|
|
|
|
self["numGlyphs"].value,
|
|
|
|
UInt16,
|
|
|
|
"glyphNameIndex",
|
|
|
|
)
|
|
|
|
yield indices
|
|
|
|
for gid, index in enumerate(indices):
|
|
|
|
if index.value >= 258:
|
|
|
|
yield PascalString8(self, "glyphname[%i]" % gid)
|
|
|
|
elif self["version"].value == 2.0:
|
|
|
|
yield UInt16(self, "numGlyphs")
|
|
|
|
indices = GenericVector(
|
|
|
|
self,
|
|
|
|
"Difference between graphic index and standard order of glyph",
|
|
|
|
self["numGlyphs"].value,
|
|
|
|
UInt16,
|
|
|
|
"offset",
|
|
|
|
)
|
|
|
|
yield indices
|
|
|
|
|
|
|
|
|
|
|
|
# This is work-in-progress until I work out good ways to do random-access on offsets
|
|
|
|
parseScriptList = (
|
|
|
|
parseFeatureList
|
|
|
|
) = parseLookupList = parseFeatureVariationsTable = lambda x: None
|
|
|
|
|
|
|
|
|
|
|
|
def parseGSUB(self):
|
|
|
|
yield UInt16(self, "majorVersion", "Major version")
|
|
|
|
yield UInt16(self, "minorVersion", "Minor version")
|
|
|
|
SUBTABLES = [
|
|
|
|
("script list", parseScriptList),
|
|
|
|
("feature list", parseFeatureList),
|
|
|
|
("lookup list", parseLookupList),
|
|
|
|
]
|
|
|
|
offsets = []
|
|
|
|
for description, parser in SUBTABLES:
|
|
|
|
name = description.title().replace(" ", "")
|
|
|
|
offset = UInt16(
|
|
|
|
self, name[0].lower() + name[1:], "Offset to %s table" % description
|
|
|
|
)
|
|
|
|
yield offset
|
|
|
|
offsets.append((offset.value, parser))
|
|
|
|
if self["min_ver"].value == 1:
|
|
|
|
offset = UInt32(
|
|
|
|
self, "featureVariationsOffset", "Offset to feature variations table"
|
|
|
|
)
|
|
|
|
offsets.append((offset.value, parseFeatureVariationsTable))
|
|
|
|
|
|
|
|
offsets.sort(key=lambda field: field[0])
|
|
|
|
padding = self.seekByte(offsets[0][0], null=True)
|
|
|
|
if padding:
|
|
|
|
yield padding
|
|
|
|
lastOffset, first_parser = offsets[0]
|
|
|
|
for offset, parser in offsets[1:]:
|
|
|
|
# yield parser(self)
|
|
|
|
yield RawBytes(self, "content", offset - lastOffset)
|
|
|
|
lastOffset = offset
|
|
|
|
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
class Table(FieldSet):
|
|
|
|
TAG_INFO = {
|
2023-10-07 23:04:41 +00:00
|
|
|
"DSIG": ("DSIG", "Digital Signature", parseDSIG),
|
|
|
|
"GSUB": ("GSUB", "Glyph Substitutions", parseGSUB),
|
|
|
|
"avar": ("avar", "Axis variation table", parseAvar),
|
|
|
|
"cmap": ("cmap", "Character to Glyph Index Mapping", parseCmap),
|
|
|
|
"fvar": ("fvar", "Font variations table", parseFvar),
|
2023-01-12 01:04:47 +00:00
|
|
|
"head": ("header", "Font header", parseFontHeader),
|
2023-10-07 23:04:41 +00:00
|
|
|
"hhea": ("hhea", "Horizontal Header", parseHhea),
|
|
|
|
"maxp": ("maxp", "Maximum Profile", parseMaxp),
|
2023-01-12 01:04:47 +00:00
|
|
|
"name": ("names", "Names", parseNames),
|
2023-10-07 23:04:41 +00:00
|
|
|
"OS/2": ("OS_2", "OS/2 and Windows Metrics", parseOS2),
|
|
|
|
"post": ("post", "PostScript", parsePost),
|
2023-01-12 01:04:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, parent, name, table, **kw):
|
|
|
|
FieldSet.__init__(self, parent, name, **kw)
|
|
|
|
self.table = table
|
|
|
|
tag = table["tag"].value
|
|
|
|
if tag in self.TAG_INFO:
|
|
|
|
self._name, self._description, self.parser = self.TAG_INFO[tag]
|
|
|
|
else:
|
|
|
|
self.parser = None
|
|
|
|
|
|
|
|
def createFields(self):
|
|
|
|
if self.parser:
|
|
|
|
yield from self.parser(self)
|
|
|
|
else:
|
|
|
|
yield RawBytes(self, "content", self.size // 8)
|
|
|
|
|
|
|
|
def createDescription(self):
|
|
|
|
return "Table %s (%s)" % (self.table["tag"].value, self.table.path)
|
|
|
|
|
|
|
|
|
|
|
|
class TrueTypeFontFile(Parser):
|
|
|
|
endian = BIG_ENDIAN
|
|
|
|
PARSER_TAGS = {
|
|
|
|
"id": "ttf",
|
|
|
|
"category": "misc",
|
|
|
|
"file_ext": ("ttf",),
|
|
|
|
"min_size": 10 * 8, # FIXME
|
|
|
|
"description": "TrueType font",
|
|
|
|
}
|
|
|
|
|
|
|
|
def validate(self):
|
2023-10-07 23:04:41 +00:00
|
|
|
if self["maj_ver"].value == 1 and self["min_ver"].value == 0:
|
|
|
|
pass
|
|
|
|
elif self["maj_ver"].value == 0x4F54 and self["min_ver"].value == 0x544F:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
return "Invalid version (%u.%u)" % (
|
|
|
|
self["maj_ver"].value,
|
|
|
|
self["min_ver"].value,
|
|
|
|
)
|
2023-01-12 01:04:47 +00:00
|
|
|
if not (MIN_NB_TABLE <= self["nb_table"].value <= MAX_NB_TABLE):
|
|
|
|
return "Invalid number of table (%u)" % self["nb_table"].value
|
|
|
|
return True
|
|
|
|
|
|
|
|
def createFields(self):
|
|
|
|
yield UInt16(self, "maj_ver", "Major version")
|
|
|
|
yield UInt16(self, "min_ver", "Minor version")
|
|
|
|
yield UInt16(self, "nb_table")
|
|
|
|
yield UInt16(self, "search_range")
|
|
|
|
yield UInt16(self, "entry_selector")
|
|
|
|
yield UInt16(self, "range_shift")
|
|
|
|
tables = []
|
|
|
|
for index in range(self["nb_table"].value):
|
|
|
|
table = TableHeader(self, "table_hdr[]")
|
|
|
|
yield table
|
|
|
|
tables.append(table)
|
|
|
|
tables.sort(key=lambda field: field["offset"].value)
|
|
|
|
for table in tables:
|
|
|
|
padding = self.seekByte(table["offset"].value, null=True)
|
|
|
|
if padding:
|
|
|
|
yield padding
|
|
|
|
size = table["size"].value
|
|
|
|
if size:
|
|
|
|
yield Table(self, "table[]", table, size=size * 8)
|
|
|
|
padding = self.seekBit(self.size, null=True)
|
|
|
|
if padding:
|
|
|
|
yield padding
|