# ****************************************************************************** # TTFontFile class # # This class is based on The ReportLab Open Source PDF library # written in Python - http://www.reportlab.com/software/opensource/ # together with ideas from the OpenOffice source code and others. # # Version: 1.04 # Date: 2011-09-18 # Author: Ian Back # License: LGPL # Copyright (c) Ian Back, 2010 # Ported to Python 2.7 by Mariano Reingart (reingart@gmail.com) on 2012 # This header must be retained in any redistribution or # modification of the file. # # ****************************************************************************** import re import warnings from struct import error as StructError, pack, unpack from .util import b, substr # Define the value used in the "head" table of a created TTF file # 0x74727565 "true" for Mac # 0x00010000 for Windows # Either seems to work for a font embedded in a PDF file # when read by Adobe Reader on a Windows PC(!) _TTF_MAC_HEADER = False # TrueType Font Glyph operators GF_WORDS = 1 << 0 GF_SCALE = 1 << 3 GF_MORE = 1 << 5 GF_XYSCALE = 1 << 6 GF_TWOBYTWO = 1 << 7 def sub32(x, y): xlo = x[1] xhi = x[0] ylo = y[1] yhi = y[0] if ylo > xlo: xlo += 1 << 16 yhi += 1 reslo = xlo - ylo if yhi > xhi: xhi += 1 << 16 reshi = xhi - yhi reshi = reshi & 0xFFFF return reshi, reslo def calcChecksum(data): if len(data) % 4: data += b("\0") * (4 - (len(data) % 4)) hi = 0x0000 lo = 0x0000 for i in range(0, len(data), 4): hi += (data[i] << 8) + data[i + 1] lo += (data[i + 2] << 8) + data[i + 3] hi += lo >> 16 lo &= 0xFFFF hi &= 0xFFFF return hi, lo class TTFontFile: def __init__(self): # Maximum size of glyph table to read in as string # (otherwise reads each glyph from file) self.maxStrLenRead = 200000 def getMetrics(self, file): self.filename = file with open(file, "rb") as self.fh: self._pos = 0 self.charWidths = [] self.glyphPos = {} self.charToGlyph = {} self.tables = {} self.otables = {} self.ascent = 0 self.descent = 0 self.version = version = self.read_ulong() if version == 0x4F54544F: raise RuntimeError("Postscript outlines are not supported") if version == 0x74746366: raise RuntimeError("ERROR - TrueType Fonts Collections not supported") if version not in (0x00010000, 0x74727565): raise RuntimeError(f"Not a TrueType font: version=0x{version:x}") self.readTableDirectory() self.extractInfo() def readTableDirectory( self, ): self.numTables = self.read_ushort() self.searchRange = self.read_ushort() self.entrySelector = self.read_ushort() self.rangeShift = self.read_ushort() self.tables = {} for _ in range(self.numTables): record = { "tag": self.read_tag(), "checksum": (self.read_ushort(), self.read_ushort()), "offset": self.read_ulong(), "length": self.read_ulong(), } self.tables[record["tag"]] = record def get_table_pos(self, tag): offset = self.tables[tag]["offset"] length = self.tables[tag]["length"] return offset, length def seek(self, pos): self._pos = pos self.fh.seek(self._pos) def skip(self, delta): self._pos = self._pos + delta self.fh.seek(self._pos) def seek_table(self, tag, offset_in_table=0): tpos = self.get_table_pos(tag) self._pos = tpos[0] + offset_in_table self.fh.seek(self._pos) return self._pos def read_tag(self): self._pos += 4 return self.fh.read(4).decode("latin1") def read_short(self): self._pos += 2 s = self.fh.read(2) a = (s[0] << 8) + s[1] if a & (1 << 15): a = a - (1 << 16) return a def read_ushort(self): self._pos += 2 s = self.fh.read(2) return (s[0] << 8) + s[1] def read_ulong(self): self._pos += 4 s = self.fh.read(4) # if large uInt32 as an integer, PHP converts it to -ve return s[0] * 16777216 + (s[1] << 16) + (s[2] << 8) + s[3] # 16777216 = 1<<24 def get_ushort(self, pos): self.fh.seek(pos) s = self.fh.read(2) return (s[0] << 8) + s[1] @staticmethod def splice(stream, offset, value): return substr(stream, 0, offset) + value + substr(stream, offset + len(value)) def _set_ushort(self, stream, offset, value): up = pack(">H", value) return self.splice(stream, offset, up) def get_chunk(self, pos, length): self.fh.seek(pos) if length < 1: return "" return self.fh.read(length) def get_table(self, tag): (pos, length) = self.get_table_pos(tag) if length == 0: raise RuntimeError( f"Truetype font ({self.filename}): error reading table: {tag}" ) self.fh.seek(pos) return self.fh.read(length) def add(self, tag, data): if tag == "head": data = self.splice(data, 8, b("\0\0\0\0")) self.otables[tag] = data def extractInfo(self): # name - Naming table self.sFamilyClass = 0 self.sFamilySubClass = 0 name_offset = self.seek_table("name") fmt = self.read_ushort() if fmt != 0: raise RuntimeError(f"Unknown name table format {fmt}") numRecords = self.read_ushort() string_data_offset = name_offset + self.read_ushort() names = {1: "", 2: "", 3: "", 4: "", 6: ""} K = list(names) nameCount = len(names) for _ in range(numRecords): platformId = self.read_ushort() encodingId = self.read_ushort() languageId = self.read_ushort() nameId = self.read_ushort() length = self.read_ushort() offset = self.read_ushort() if nameId not in K: continue N = "" if ( platformId == 3 and encodingId == 1 and languageId == 0x409 ): # Microsoft, Unicode, US English, PS Name opos = self._pos self.seek(string_data_offset + offset) if length % 2 != 0: raise RuntimeError( "PostScript name is UTF-16BE string of odd length" ) length //= 2 N = "" while length > 0: char = self.read_ushort() N += chr(char) length -= 1 self._pos = opos self.seek(opos) elif ( platformId == 1 and encodingId == 0 and languageId == 0 ): # Macintosh, Roman, English, PS Name opos = self._pos N = self.get_chunk(string_data_offset + offset, length).decode("latin1") self._pos = opos self.seek(opos) if N and names[nameId] == "": names[nameId] = N nameCount -= 1 if nameCount == 0: break if names[6]: psName = names[6] elif names[4]: psName = re.sub(" ", "-", names[4]) elif names[1]: psName = re.sub(" ", "-", names[1]) else: psName = "" if not psName: raise RuntimeError("Could not find PostScript font name") self.name = psName self.familyName = names[1] or psName self.styleName = names[2] or "Regular" self.fullName = names[4] or psName self.uniqueFontID = names[3] or psName if names[6]: self.fullName = names[6] # head - Font header table self.seek_table("head") self.skip(18) self.unitsPerEm = unitsPerEm = self.read_ushort() scale = 1000 / unitsPerEm self.skip(16) xMin = self.read_short() yMin = self.read_short() xMax = self.read_short() yMax = self.read_short() self.bbox = [(xMin * scale), (yMin * scale), (xMax * scale), (yMax * scale)] self.skip(3 * 2) # pylint: disable=unused-variable indexToLocFormat = self.read_ushort() glyphDataFormat = self.read_ushort() if glyphDataFormat != 0: raise RuntimeError(f"Unknown glyph data format {glyphDataFormat}") # hhea metrics table # ttf2t1 seems to use this value rather than the one in OS/2 - so put in for # compatibility if "hhea" in self.tables: self.seek_table("hhea") self.skip(4) hheaAscender = self.read_short() hheaDescender = self.read_short() self.ascent = hheaAscender * scale self.descent = hheaDescender * scale # OS/2 - OS/2 and Windows metrics table if "OS/2" in self.tables: self.seek_table("OS/2") version = self.read_ushort() self.skip(2) usWeightClass = self.read_ushort() self.skip(2) fsType = self.read_ushort() if fsType == 0x0002 or (fsType & 0x0300) != 0: raise RuntimeError( "ERROR - Font file " + self.filename + " cannot be embedded due to copyright restrictions." ) self.skip(20) sF = self.read_short() self.sFamilyClass = sF >> 8 self.sFamilySubClass = sF & 0xFF self._pos += 10 # PANOSE = 10 byte length panose = self.fh.read(10) self.skip(26) sTypoAscender = self.read_short() sTypoDescender = self.read_short() if not self.ascent: self.ascent = sTypoAscender * scale if not self.descent: self.descent = sTypoDescender * scale if version > 1: self.skip(16) sCapHeight = self.read_short() self.capHeight = sCapHeight * scale else: self.capHeight = self.ascent else: usWeightClass = 500 if not self.ascent: self.ascent = yMax * scale if not self.descent: self.descent = yMin * scale self.capHeight = self.ascent self.stemV = 50 + int(pow((usWeightClass / 65), 2)) # post - PostScript table self.seek_table("post") self.skip(4) self.italicAngle = self.read_short() + self.read_ushort() / 65536 self.underlinePosition = self.read_short() * scale self.underlineThickness = self.read_short() * scale isFixedPitch = self.read_ulong() self.flags = 4 if self.italicAngle != 0: self.flags |= 64 if usWeightClass >= 600: self.flags |= 262144 if isFixedPitch: self.flags |= 1 # hhea - Horizontal header table self.seek_table("hhea") self.skip(32) metricDataFormat = self.read_ushort() if metricDataFormat != 0: raise RuntimeError( f"Unknown horizontal metric data format: {metricDataFormat}" ) numberOfHMetrics = self.read_ushort() if numberOfHMetrics == 0: raise RuntimeError("Number of horizontal metrics is 0") # maxp - Maximum profile table self.seek_table("maxp") self.skip(4) numGlyphs = self.read_ushort() # cmap - Character to glyph index mapping table cmap_offset = self.seek_table("cmap") self.skip(2) cmapTableCount = self.read_ushort() unicode_cmap_offset = 0 unicode_cmap_offset12 = 0 for _ in range(cmapTableCount): platformID = self.read_ushort() encodingID = self.read_ushort() offset = self.read_ulong() save_pos = self._pos if platformID == 3 and encodingID == 10: # Microsoft, UCS-4 fmt = self.get_ushort(cmap_offset + offset) if fmt == 12: if not unicode_cmap_offset12: unicode_cmap_offset12 = cmap_offset + offset break if ( platformID == 3 and encodingID == 1 ) or platformID == 0: # Microsoft, Unicode fmt = self.get_ushort(cmap_offset + offset) if fmt == 4: if not unicode_cmap_offset: unicode_cmap_offset = cmap_offset + offset # Don't break here since we might later get # unicode_cmap_offset12 which is needed for # characters => 0x10000 (CMAP12) # # break self.seek(save_pos) if not unicode_cmap_offset and not unicode_cmap_offset12: raise RuntimeError( f"Font ({self.filename}) does not have cmap for Unicode (platform 3, " f"encoding 1, format 4, or platform 3, encoding 10, format 12, or " f"platform 0, any encoding, format 4)" ) glyphToChar = {} charToGlyph = {} if unicode_cmap_offset12: self.getCMAP12(unicode_cmap_offset12, glyphToChar, charToGlyph) else: self.getCMAP4(unicode_cmap_offset, glyphToChar, charToGlyph) # hmtx - Horizontal metrics table self.getHMTX(numberOfHMetrics, numGlyphs, glyphToChar, scale) def makeSubset(self, file, subset): self.filename = file with open(file, "rb") as self.fh: self._pos = 0 self.charWidths = [] self.glyphPos = {} self.charToGlyph = {} self.tables = {} self.otables = {} self.ascent = 0 self.descent = 0 self.skip(4) self.maxUni = 0 self.readTableDirectory() # head - Font header table self.seek_table("head") self.skip(50) indexToLocFormat = self.read_ushort() # pylint: disable=unused-variable glyphDataFormat = self.read_ushort() # hhea - Horizontal header table self.seek_table("hhea") self.skip(32) metricDataFormat = self.read_ushort() orignHmetrics = numberOfHMetrics = self.read_ushort() # maxp - Maximum profile table self.seek_table("maxp") self.skip(4) numGlyphs = self.read_ushort() # cmap - Character to glyph index mapping table cmap_offset = self.seek_table("cmap") self.skip(2) cmapTableCount = self.read_ushort() unicode_cmap_offset = 0 unicode_cmap_offset12 = 0 for _ in range(cmapTableCount): platformID = self.read_ushort() encodingID = self.read_ushort() offset = self.read_ulong() save_pos = self._pos if platformID == 3 and encodingID == 10: # Microsoft, UCS-4 fmt = self.get_ushort(cmap_offset + offset) if fmt == 12: if not unicode_cmap_offset12: unicode_cmap_offset12 = cmap_offset + offset break if ( platformID == 3 and encodingID == 1 ) or platformID == 0: # Microsoft, Unicode fmt = self.get_ushort(cmap_offset + offset) if fmt == 4: unicode_cmap_offset = cmap_offset + offset # Don't break here since we might later get # unicode_cmap_offset12 which is needed for # characters => 0x10000 (CMAP12) # # break self.seek(save_pos) if not unicode_cmap_offset and not unicode_cmap_offset12: raise RuntimeError( f"Font ({self.filename}) does not have cmap for Unicode " f"(platform 3, encoding 1, format 4, or platform 3, encoding 10, " f"format 12, or platform 0, any encoding, format 4)" ) glyphToChar = {} charToGlyph = {} if unicode_cmap_offset12: self.getCMAP12(unicode_cmap_offset12, glyphToChar, charToGlyph) else: self.getCMAP4(unicode_cmap_offset, glyphToChar, charToGlyph) self.charToGlyph = charToGlyph # hmtx - Horizontal metrics table scale = 1 # not used self.getHMTX(numberOfHMetrics, numGlyphs, glyphToChar, scale) # loca - Index to location self.getLOCA(indexToLocFormat, numGlyphs) subsetglyphs = [(0, 0)] # special "sorted dict"! subsetCharToGlyph = {} for code in subset: target = subset[code] if isinstance(subset, dict) else code if target > 65535: raise Exception( f"Character U+{target:X} must be remapped since it cannot be indexed in CMAP4 table" ) if code in self.charToGlyph: if (self.charToGlyph[code], target) not in subsetglyphs: subsetglyphs.append( (self.charToGlyph[code], target) ) # Old Glyph ID => Unicode subsetCharToGlyph[target] = self.charToGlyph[ code ] # Unicode to old GlyphID self.maxUni = max(self.maxUni, code) (start, _) = self.get_table_pos("glyf") subsetglyphs.sort() glyphSet = {} n = 0 # maximum Unicode index (character code) in this font, according to the cmap # subtable for platform ID 3 and platform- specific encoding ID 0 or 1. fsLastCharIndex = 0 for originalGlyphIdx, uni in subsetglyphs: fsLastCharIndex = max(fsLastCharIndex, uni) glyphSet[originalGlyphIdx] = n # old glyphID to new glyphID n += 1 codeToGlyph = {} for uni, originalGlyphIdx in sorted(subsetCharToGlyph.items()): codeToGlyph[uni] = glyphSet[originalGlyphIdx] self.codeToGlyph = codeToGlyph for originalGlyphIdx, uni in subsetglyphs: nonlocals = { "start": start, "glyphSet": glyphSet, "subsetglyphs": subsetglyphs, } self.getGlyphs(originalGlyphIdx, nonlocals) numGlyphs = numberOfHMetrics = len(subsetglyphs) # tables copied from the original tags = ["name"] for tag in tags: self.add(tag, self.get_table(tag)) tags = ["cvt ", "fpgm", "prep", "gasp"] for tag in tags: if tag in self.tables: self.add(tag, self.get_table(tag)) # post - PostScript opost = self.get_table("post") post = ( b("\x00\x03\x00\x00") + substr(opost, 4, 12) + b("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") ) self.add("post", post) # Sort CID2GID map into segments of contiguous codes if 0 in codeToGlyph: del codeToGlyph[0] # unset(codeToGlyph[65535]) rangeid = 0 range_ = {} prevcid = -2 prevglidx = -1 # for each character for cid, glidx in sorted(codeToGlyph.items()): if cid == (prevcid + 1) and glidx == (prevglidx + 1): range_[rangeid].append(glidx) else: # new range rangeid = cid range_[rangeid] = [] range_[rangeid].append(glidx) prevcid = cid prevglidx = glidx # cmap - Character to glyph mapping - Format 4 (MS / ) segCount = len(range_) + 1 # + 1 Last segment has missing character 0xFFFF searchRange = 1 entrySelector = 0 while searchRange * 2 <= segCount: searchRange = searchRange * 2 entrySelector = entrySelector + 1 searchRange = searchRange * 2 rangeShift = segCount * 2 - searchRange length = 16 + (8 * segCount) + (numGlyphs + 1) cmap = [ 0, 1, # Index : version, number of encoding subtables 3, 1, # Encoding Subtable : platform (MS=3), encoding (Unicode) 0, 12, # Encoding Subtable : offset (hi,lo) 4, length, 0, # Format 4 Mapping subtable: format, length, language segCount * 2, searchRange, entrySelector, rangeShift, ] range_ = sorted(range_.items()) # endCode(s) for start, subrange in range_: endCode = start + (len(subrange) - 1) cmap.append(endCode) # endCode(s) cmap.append(0xFFFF) # endCode of last Segment cmap.append(0) # reservedPad # startCode(s) for start, subrange in range_: cmap.append(start) # startCode(s) cmap.append(0xFFFF) # startCode of last Segment # idDelta(s) for start, subrange in range_: idDelta = -(start - subrange[0]) n += len(subrange) cmap.append(idDelta) # idDelta(s) cmap.append(1) # idDelta of last Segment # idRangeOffset(s) for subrange in range_: # idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 cmap.append(0) cmap.append(0) # idRangeOffset of last Segment for subrange, glidx in range_: cmap.extend(glidx) cmap.append(0) # Mapping for last character cmapstr = b("") for cm in cmap: if cm >= 0: cmapstr += pack(">H", cm) else: try: cmapstr += pack(">h", cm) except StructError: # cmap value too big to fit in a short (h), # putting it in an unsigned short (H): cmapstr += pack(">H", -cm) self.add("cmap", cmapstr) # glyf - Glyph data (glyfOffset, glyfLength) = self.get_table_pos("glyf") if glyfLength < self.maxStrLenRead: glyphData = self.get_table("glyf") offsets = [] glyf = b("") pos = 0 hmtxstr = b("") maxComponentElements = 0 # number of glyphs referenced at top level self.glyphdata = {} for originalGlyphIdx, uni in subsetglyphs: # hmtx - Horizontal Metrics hm = self.getHMetric(orignHmetrics, originalGlyphIdx) hmtxstr += hm offsets.append(pos) try: glyphPos = self.glyphPos[originalGlyphIdx] glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos except IndexError: warnings.warn(f"Missing glyph {originalGlyphIdx} in {file}") glyphLen = 0 if glyfLength < self.maxStrLenRead: data = substr(glyphData, glyphPos, glyphLen) else: if glyphLen > 0: data = self.get_chunk(glyfOffset + glyphPos, glyphLen) else: data = b("") if glyphLen > 0: up = unpack(">H", substr(data, 0, 2))[0] if glyphLen > 2 and ( up & (1 << 15) ): # If number of contours <= -1 i.e. composite glyph pos_in_glyph = 10 flags = GF_MORE nComponentElements = 0 while flags & GF_MORE: nComponentElements += ( 1 # number of glyphs referenced at top level ) up = unpack(">H", substr(data, pos_in_glyph, 2)) flags = up[0] up = unpack(">H", substr(data, pos_in_glyph + 2, 2)) glyphIdx = up[0] self.glyphdata.setdefault(originalGlyphIdx, {}).setdefault( "compGlyphs", [] ).append(glyphIdx) try: data = self._set_ushort( data, pos_in_glyph + 2, glyphSet[glyphIdx] ) except KeyError: data = 0 warnings.warn(f"Missing glyph data {glyphIdx} in {file}") pos_in_glyph += 4 if flags & GF_WORDS: pos_in_glyph += 4 else: pos_in_glyph += 2 if flags & GF_SCALE: pos_in_glyph += 2 elif flags & GF_XYSCALE: pos_in_glyph += 4 elif flags & GF_TWOBYTWO: pos_in_glyph += 8 maxComponentElements = max(maxComponentElements, nComponentElements) glyf += data pos += glyphLen if pos % 4 != 0: padding = 4 - (pos % 4) glyf += b("\0") * padding pos += padding offsets.append(pos) self.add("glyf", glyf) # hmtx - Horizontal Metrics self.add("hmtx", hmtxstr) # loca - Index to location locastr = b("") if ((pos + 1) >> 1) > 0xFFFF: indexToLocFormat = 1 # long format for offset in offsets: locastr += pack(">L", offset) else: indexToLocFormat = 0 # short format for offset in offsets: locastr += pack(">H", offset // 2) self.add("loca", locastr) # head - Font header head = self.get_table("head") head = self._set_ushort(head, 50, indexToLocFormat) self.add("head", head) # hhea - Horizontal Header hhea = self.get_table("hhea") hhea = self._set_ushort(hhea, 34, numberOfHMetrics) self.add("hhea", hhea) # maxp - Maximum Profile maxp = self.get_table("maxp") maxp = self._set_ushort(maxp, 4, numGlyphs) self.add("maxp", maxp) # OS/2 - OS/2 os2 = self.get_table("OS/2") self.add("OS/2", os2) # Put the TTF file together stm = self.endTTFile("") return stm # Recursively get composite glyphs def getGlyphs(self, originalGlyphIdx, nonlocals): # &start, &glyphSet, &subsetglyphs) try: glyphPos = self.glyphPos[originalGlyphIdx] glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos except IndexError: return if not glyphLen: return self.seek(nonlocals["start"] + glyphPos) numberOfContours = self.read_short() if numberOfContours < 0: self.skip(8) flags = GF_MORE while flags & GF_MORE: flags = self.read_ushort() glyphIdx = self.read_ushort() if glyphIdx not in nonlocals["glyphSet"]: nonlocals["glyphSet"][glyphIdx] = len( nonlocals["subsetglyphs"] ) # old glyphID to new glyphID nonlocals["subsetglyphs"].append((glyphIdx, 1)) savepos = self.fh.tell() self.getGlyphs(glyphIdx, nonlocals) self.seek(savepos) if flags & GF_WORDS: self.skip(4) else: self.skip(2) if flags & GF_SCALE: self.skip(2) elif flags & GF_XYSCALE: self.skip(4) elif flags & GF_TWOBYTWO: self.skip(8) def getHMTX(self, numberOfHMetrics, numGlyphs, glyphToChar, scale): start = self.seek_table("hmtx") aw = 0 self.charWidths = [] def resize_cw(size, default): size = (((size + 1) // 1024) + 1) * 1024 delta = size - len(self.charWidths) if delta > 0: self.charWidths += [default] * delta nCharWidths = 0 if (numberOfHMetrics * 4) < self.maxStrLenRead: data = self.get_chunk(start, (numberOfHMetrics * 4)) arr = unpack(f">{len(data) // 2}H", data) else: self.seek(start) for glyph in range(numberOfHMetrics): if (numberOfHMetrics * 4) < self.maxStrLenRead: aw = arr[(glyph * 2)] # PHP starts arrays from index 0!? +1 else: aw = self.read_ushort() # pylint: disable=unused-variable lsb = self.read_ushort() if glyph in glyphToChar or glyph == 0: if aw >= (1 << 15): aw = 0 # 1.03 Some (arabic) fonts have -ve values for width # although should be unsigned value # - comes out as e.g. 65108 (intended -50) if glyph == 0: self.defaultWidth = scale * aw continue for char in glyphToChar[glyph]: if char not in (0, 65535): w = round(scale * aw + 0.001) # ROUND_HALF_UP if w == 0: w = 65535 if char < 196608: if char >= len(self.charWidths): resize_cw(char, self.defaultWidth) self.charWidths[char] = w nCharWidths += 1 data = self.get_chunk((start + numberOfHMetrics * 4), (numGlyphs * 2)) arr = unpack(f">{len(data) // 2}H", data) diff = numGlyphs - numberOfHMetrics for pos in range(diff): glyph = pos + numberOfHMetrics if glyph in glyphToChar: for char in glyphToChar[glyph]: if char not in (0, 65535): w = round(scale * aw + 0.001) # ROUND_HALF_UP if w == 0: w = 65535 if char < 196608: if char >= len(self.charWidths): resize_cw(char, self.defaultWidth) self.charWidths[char] = w nCharWidths += 1 # NB 65535 is a set width of 0 # First bytes define number of chars in font self.charWidths[0] = nCharWidths def getHMetric(self, numberOfHMetrics, gid): start = self.seek_table("hmtx") if gid < numberOfHMetrics: self.seek(start + (gid * 4)) hm = self.fh.read(4) else: self.seek(start + ((numberOfHMetrics - 1) * 4)) hm = self.fh.read(2) self.seek(start + (numberOfHMetrics * 2) + (gid * 2)) hm += self.fh.read(2) return hm def getLOCA(self, indexToLocFormat, numGlyphs): try: start = self.seek_table("loca") except KeyError: # pylint: disable=raise-missing-from raise RuntimeError( f"Unknown location table format, index={indexToLocFormat}" ) self.glyphPos = [] if indexToLocFormat == 0: data = self.get_chunk(start, (numGlyphs * 2) + 2) arr = unpack(f">{len(data) // 2}H", data) for n in range(numGlyphs): self.glyphPos.append(arr[n] * 2) # n+1 !? elif indexToLocFormat == 1: data = self.get_chunk(start, (numGlyphs * 4) + 4) arr = unpack(f">{len(data) // 4}L", data) for n in range(numGlyphs): self.glyphPos.append(arr[n]) # n+1 !? else: raise RuntimeError( f"Unknown location table format, index={indexToLocFormat}" ) # CMAP Format 4 def getCMAP4(self, unicode_cmap_offset, glyphToChar, charToGlyph): self.maxUniChar = 0 self.seek(unicode_cmap_offset + 2) length = self.read_ushort() limit = unicode_cmap_offset + length self.skip(2) segCount = self.read_ushort() // 2 self.skip(6) endCount = [] for _ in range(segCount): endCount.append(self.read_ushort()) self.skip(2) startCount = [] for _ in range(segCount): startCount.append(self.read_ushort()) idDelta = [] for _ in range(segCount): idDelta.append(self.read_short()) # ???? was unsigned short idRangeOffset_start = self._pos idRangeOffset = [] for _ in range(segCount): idRangeOffset.append(self.read_ushort()) for n in range(segCount): endpoint = endCount[n] + 1 for unichar in range(startCount[n], endpoint, 1): if idRangeOffset[n] == 0: glyph = (unichar + idDelta[n]) & 0xFFFF else: offset = (unichar - startCount[n]) * 2 + idRangeOffset[n] offset = idRangeOffset_start + 2 * n + offset if offset >= limit: glyph = 0 else: glyph = self.get_ushort(offset) if glyph != 0: glyph = (glyph + idDelta[n]) & 0xFFFF charToGlyph[unichar] = glyph if unichar < 196608: self.maxUniChar = max(unichar, self.maxUniChar) glyphToChar.setdefault(glyph, []).append(unichar) # CMAP Format 12 def getCMAP12(self, unicode_cmap_offset, glyphToChar, charToGlyph): self.maxUniChar = 0 # table (skip format version, should be 12) self.seek(unicode_cmap_offset + 2) # reserved self.skip(2) # table length length = self.read_ulong() # language (should be 0) self.skip(4) # groups count grpCount = self.read_ulong() if 2 + 2 + 4 + 4 + 4 + grpCount * 3 * 4 > length: raise RuntimeError("TTF format 12 cmap table too small") for _ in range(grpCount): startCharCode = self.read_ulong() endCharCode = self.read_ulong() glyph = self.read_ulong() for unichar in range(startCharCode, endCharCode + 1): charToGlyph[unichar] = glyph if unichar < 196608: self.maxUniChar = max(unichar, self.maxUniChar) glyphToChar.setdefault(glyph, []).append(unichar) glyph += 1 # Put the TTF file together def endTTFile(self, stm): stm = b("") numTables = len(self.otables) searchRange = 1 entrySelector = 0 while searchRange * 2 <= numTables: searchRange *= 2 entrySelector += 1 searchRange *= 16 rangeShift = numTables * 16 - searchRange # Header if _TTF_MAC_HEADER: stm += pack( ">LHHHH", 0x74727565, numTables, searchRange, entrySelector, rangeShift ) # Mac else: stm += pack( ">LHHHH", 0x00010000, numTables, searchRange, entrySelector, rangeShift ) # Windows # Table directory tables = self.otables offset = 12 + numTables * 16 sorted_tables = sorted(tables.items()) for tag, data in sorted_tables: if tag == "head": head_start = offset stm += tag.encode("latin1") checksum = calcChecksum(data) stm += pack(">HH", checksum[0], checksum[1]) stm += pack(">LL", offset, len(data)) paddedLength = (len(data) + 3) & ~3 offset = offset + paddedLength # Table data for tag, data in sorted_tables: data += b("\0\0\0") stm += substr(data, 0, (len(data) & ~3)) checksum = calcChecksum(stm) checksum = sub32((0xB1B0, 0xAFBA), checksum) chk = pack(">HH", checksum[0], checksum[1]) stm = self.splice(stm, (head_start + 8), chk) return stm