#****************************************************************************** # 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. # #****************************************************************************** from __future__ import with_statement from struct import pack, unpack, unpack_from import re import warnings from .php import die, substr, str_repeat, str_pad, strlen, count from .py3k import b, ord # 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 (strlen(data) % 4): data += str_repeat(b("\0"), (4-(len(data) % 4))) hi=0x0000 lo=0x0000 for i in range(0, len(data), 4): hi += (ord(data[i])<<8) + ord(data[i+1]) lo += (ord(data[i+2])<<8) + ord(data[i+3]) hi += lo >> 16 lo = lo & 0xFFFF hi = hi & 0xFFFF return (hi, lo) class TTFontFile: def __init__(self): self.maxStrLenRead = 200000 # Maximum size of glyf table to read in as string (otherwise reads each glyph from file) 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.TTCFonts = {} self.version = version = self.read_ulong() if (version==0x4F54544F): die("Postscript outlines are not supported") if (version==0x74746366): die("ERROR - TrueType Fonts Collections not supported") if (version not in (0x00010000,0x74727565)): die("Not a TrueType font: version=" + str(version)) 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 i in range(self.numTables): record = {} record['tag'] = self.read_tag() record['checksum'] = (self.read_ushort(),self.read_ushort()) record['offset'] = self.read_ulong() record['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 = (ord(s[0])<<8) + ord(s[1]) if (a & (1 << 15) ): a = (a - (1 << 16)) return a def unpack_short(self, s): a = (ord(s[0])<<8) + ord(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 (ord(s[0])<<8) + ord(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 (ord(s[0])*16777216) + (ord(s[1])<<16) + (ord(s[2])<<8) + ord(s[3]) # 16777216 = 1<<24 def get_ushort(self, pos): self.fh.seek(pos) s = self.fh.read(2) return (ord(s[0])<<8) + ord(s[1]) def get_ulong(self, pos): self.fh.seek(pos) s = self.fh.read(4) # iF large uInt32 as an integer, PHP converts it to -ve return (ord(s[0])*16777216) + (ord(s[1])<<16) + (ord(s[2])<<8) + ord(s[3]) # 16777216 = 1<<24 def pack_short(self, val): if (val<0): val = abs(val) val = ~val val += 1 return pack(">H",val) def splice(self, stream, offset, value): return substr(stream,0,offset) + value + substr(stream,offset+strlen(value)) def _set_ushort(self, stream, offset, value): up = pack(">H", value) return self.splice(stream, offset, up) def _set_short(self, stream, offset, val): if (val<0): val = abs(val) val = ~val val += 1 up = pack(">H",val) 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): die('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") format = self.read_ushort() if (format != 0): die("Unknown name table format " + format) numRecords = self.read_ushort() string_data_offset = name_offset + self.read_ushort() names = {1:'',2:'',3:'',4:'',6:''} K = list(names.keys()) nameCount = len(names) for i 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): die("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): die("Could not find PostScript font name") self.name = psName if (names[1]): self.familyName = names[1] else: self.familyName = psName if (names[2]): self.styleName = names[2] else: self.styleName = 'Regular' if (names[4]): self.fullName = names[4] else: self.fullName = psName if (names[3]): self.uniqueFontID = names[3] else: self.uniqueFontID = 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 / float(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) indexToLocFormat = self.read_ushort() glyphDataFormat = self.read_ushort() if (glyphDataFormat != 0): die('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): die('ERROR - Font file ' + self.filename + ' cannot be embedded due to copyright restrictions.') self.restrictedUse = True 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.0),2)) #################/ # post - PostScript table #################/ self.seek_table("post") self.skip(4) self.italicAngle = self.read_short() + self.read_ushort() / 65536.0 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 = self.flags | 64 if (usWeightClass >= 600): self.flags = self.flags | 262144 if (isFixedPitch): self.flags = self.flags | 1 #################/ # hhea - Horizontal header table #################/ self.seek_table("hhea") self.skip(32) metricDataFormat = self.read_ushort() if (metricDataFormat != 0): die('Unknown horizontal metric data format '.metricDataFormat) numberOfHMetrics = self.read_ushort() if (numberOfHMetrics == 0): die('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 i 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 format = self.get_ushort(cmap_offset + offset) if (format == 12): if not unicode_cmap_offset12: unicode_cmap_offset12 = cmap_offset + offset break if ((platformID == 3 and encodingID == 1) or platformID == 0): # Microsoft, Unicode format = self.get_ushort(cmap_offset + offset) if (format == 4): if (not unicode_cmap_offset): unicode_cmap_offset = cmap_offset + offset break self.seek(save_pos) if not unicode_cmap_offset and not unicode_cmap_offset12: die('Font (' + self.filename + ') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 3, encoding 10, 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) #################/ # 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() 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 i 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 format = self.get_ushort(cmap_offset + offset) if (format == 12): if not unicode_cmap_offset12: unicode_cmap_offset12 = cmap_offset + offset break if ((platformID == 3 and encodingID == 1) or platformID == 0): # Microsoft, Unicode format = self.get_ushort(cmap_offset + offset) if (format == 4): unicode_cmap_offset = cmap_offset + offset break self.seek(save_pos ) if not unicode_cmap_offset and not unicode_cmap_offset12: die('Font (' + self.filename + ') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 3, encoding 10, 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: if (code in self.charToGlyph): if (self.charToGlyph[code], code) not in subsetglyphs: subsetglyphs.append((self.charToGlyph[code], code)) # Old Glyph ID => Unicode subsetCharToGlyph[code] = self.charToGlyph[code] # Unicode to old GlyphID self.maxUni = max(self.maxUni, code) (start,dummy) = self.get_table_pos('glyf') subsetglyphs.sort() glyphSet = {} n = 0 fsLastCharIndex = 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. 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 += count(subrange) cmap.append(idDelta) # idDelta(s) cmap.append(1) # idDelta of last Segment # idRangeOffset(s) for subrange in range_: cmap.append(0) # idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 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: warnings.warn("cmap value too big/small: %s" % cm) 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('') xMinT = 0 yMinT = 0 xMaxT = 0 yMaxT = 0 advanceWidthMax = 0 minLeftSideBearing = 0 minRightSideBearing = 0 xMaxExtent = 0 maxPoints = 0 # points in non-compound glyph maxContours = 0 # contours in non-compound glyph maxComponentPoints = 0 # points in compound glyph maxComponentContours = 0 # contours in compound glyph maxComponentElements = 0 # number of glyphs referenced at top level maxComponentDepth = 0 # levels of recursion, set to 0 if font has only simple glyphs 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("missing glyph %s" % (originalGlyphIdx)) 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("missing glyph data %s" % glyphIdx) 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 += str_repeat(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 glyph data def getGlyphData(self, originalGlyphIdx, nonlocals): # &maxdepth, &depth, &points, &contours nonlocals['depth'] += 1 nonlocals['maxdepth'] = max(nonlocals['maxdepth'], nonlocals['depth']) if (len(self.glyphdata[originalGlyphIdx]['compGlyphs'])): for glyphIdx in self.glyphdata[originalGlyphIdx]['compGlyphs']: self.getGlyphData(glyphIdx, nonlocals) elif ((self.glyphdata[originalGlyphIdx]['nContours'] > 0) and nonlocals['depth'] > 0): # simple contours += self.glyphdata[originalGlyphIdx]['nContours'] points += self.glyphdata[originalGlyphIdx]['nPoints'] nonlocals['depth'] -= 1 ######################################### # 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: warnings.warn("missing glyph %s" % (originalGlyphIdx)) 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(">%dH" % (len(data)//2), 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() 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 != 0 and char != 65535): w = int(round(scale*aw+0.001)) # ROUND_HALF_UP in PY3K (like php) 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(">%dH" % (len(data)//2), data) diff = numGlyphs-numberOfHMetrics for pos in range(diff): glyph = pos + numberOfHMetrics if (glyph in glyphToChar): for char in glyphToChar[glyph]: if (char != 0 and char != 65535): w = int(round(scale*aw+0.001)) # ROUND_HALF_UP in PY3K (like php) 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): start = self.seek_table('loca') self.glyphPos = [] if (indexToLocFormat == 0): data = self.get_chunk(start,(numGlyphs*2)+2) arr = unpack(">%dH" % (len(data)//2), 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(">%dL" % (len(data)//4), data) for n in range(numGlyphs): self.glyphPos.append((arr[n])) # n+1 !? else: die('Unknown location table format ' + 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 i in range(segCount): endCount.append(self.read_ushort()) self.skip(2) startCount = [] for i in range(segCount): startCount.append(self.read_ushort()) idDelta = [] for i in range(segCount): idDelta.append(self.read_short()) # ???? was unsigned short idRangeOffset_start = self._pos idRangeOffset = [] for i 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: die("TTF format 12 cmap table too small") for n 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 = count(self.otables) searchRange = 1 entrySelector = 0 while (searchRange * 2 <= numTables): searchRange = searchRange * 2 entrySelector = entrySelector + 1 searchRange = 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, strlen(data)) paddedLength = (strlen(data)+3)&~3 offset = offset + paddedLength # Table data for tag, data in sorted_tables: data += b("\0\0\0") stm += substr(data,0,(strlen(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