source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/contrib/fpdf/ttfonts.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 41.2 KB
Line 
1#******************************************************************************
2# TTFontFile class                                                             
3#                                                                             
4# This class is based on The ReportLab Open Source PDF library                 
5# written in Python - http://www.reportlab.com/software/opensource/           
6# together with ideas from the OpenOffice source code and others.             
7#                                                                             
8# Version:  1.04                                                               
9# Date:     2011-09-18                                                         
10# Author:   Ian Back <ianb@bpm1.com>                                           
11# License:  LGPL                                                               
12# Copyright (c) Ian Back, 2010                                                 
13# Ported to Python 2.7 by Mariano Reingart (reingart@gmail.com) on 2012       
14# This header must be retained in any redistribution or                       
15# modification of the file.                                                   
16#                                                                             
17#******************************************************************************
18
19from __future__ import with_statement
20
21from struct import pack, unpack, unpack_from
22import re
23import warnings
24from .php import die, substr, str_repeat, str_pad, strlen, count
25from .py3k import b, ord
26
27
28# Define the value used in the "head" table of a created TTF file
29# 0x74727565 "true" for Mac
30# 0x00010000 for Windows
31# Either seems to work for a font embedded in a PDF file
32# when read by Adobe Reader on a Windows PC(!)
33_TTF_MAC_HEADER = False
34
35
36# TrueType Font Glyph operators
37GF_WORDS = (1 << 0)
38GF_SCALE = (1 << 3)
39GF_MORE  = (1 << 5)
40GF_XYSCALE  = (1 << 6)
41GF_TWOBYTWO = (1 << 7)
42
43
44def sub32(x, y):
45    xlo = x[1]
46    xhi = x[0]
47    ylo = y[1]
48    yhi = y[0]
49    if (ylo > xlo): 
50        xlo += 1 << 16
51        yhi += 1
52    reslo = xlo-ylo
53    if (yhi > xhi): 
54        xhi += 1 << 16 
55    reshi = xhi-yhi
56    reshi = reshi & 0xFFFF
57    return (reshi, reslo)
58
59def calcChecksum(data):
60    if (strlen(data) % 4):
61        data += str_repeat(b("\0"), (4-(len(data) % 4)))
62    hi=0x0000
63    lo=0x0000
64    for i in range(0, len(data), 4):
65        hi += (ord(data[i])<<8) + ord(data[i+1])
66        lo += (ord(data[i+2])<<8) + ord(data[i+3])
67        hi += lo >> 16
68        lo = lo & 0xFFFF
69        hi = hi & 0xFFFF
70    return (hi, lo)
71
72
73class TTFontFile:
74
75    def __init__(self):
76        self.maxStrLenRead = 200000    # Maximum size of glyf table to read in as string (otherwise reads each glyph from file)
77
78    def getMetrics(self, file):
79        self.filename = file
80        with open(file,'rb') as self.fh:
81            self._pos = 0
82            self.charWidths = []
83            self.glyphPos = {}
84            self.charToGlyph = {}
85            self.tables = {}
86            self.otables = {}
87            self.ascent = 0
88            self.descent = 0
89            self.TTCFonts = {}
90            self.version = version = self.read_ulong()
91            if (version==0x4F54544F):
92                die("Postscript outlines are not supported")
93            if (version==0x74746366):
94                die("ERROR - TrueType Fonts Collections not supported")
95            if (version not in (0x00010000,0x74727565)):
96                die("Not a TrueType font: version=" + str(version))
97            self.readTableDirectory()
98            self.extractInfo()
99   
100    def readTableDirectory(self, ):
101        self.numTables = self.read_ushort()
102        self.searchRange = self.read_ushort()
103        self.entrySelector = self.read_ushort()
104        self.rangeShift = self.read_ushort()
105        self.tables = {}   
106        for i in range(self.numTables):
107            record = {}
108            record['tag'] = self.read_tag()
109            record['checksum'] = (self.read_ushort(),self.read_ushort())
110            record['offset'] = self.read_ulong()
111            record['length'] = self.read_ulong()
112            self.tables[record['tag']] = record   
113
114    def get_table_pos(self, tag):
115        offset = self.tables[tag]['offset']
116        length = self.tables[tag]['length']
117        return (offset, length)
118   
119    def seek(self, pos):
120        self._pos = pos
121        self.fh.seek(self._pos)
122   
123    def skip(self, delta):
124        self._pos = self._pos + delta
125        self.fh.seek(self._pos)
126   
127    def seek_table(self, tag, offset_in_table = 0):
128        tpos = self.get_table_pos(tag)
129        self._pos = tpos[0] + offset_in_table
130        self.fh.seek(self._pos)
131        return self._pos
132
133    def read_tag(self):
134        self._pos += 4
135        return self.fh.read(4).decode("latin1")
136
137    def read_short(self):
138        self._pos += 2
139        s = self.fh.read(2)
140        a = (ord(s[0])<<8) + ord(s[1])
141        if (a & (1 << 15) ):
142            a = (a - (1 << 16))
143        return a
144   
145    def unpack_short(self, s):
146        a = (ord(s[0])<<8) + ord(s[1])
147        if (a & (1 << 15) ):
148            a = (a - (1 << 16))     
149        return a
150   
151    def read_ushort(self):
152        self._pos += 2
153        s = self.fh.read(2)
154        return (ord(s[0])<<8) + ord(s[1])
155
156    def read_ulong(self):
157        self._pos += 4
158        s = self.fh.read(4)
159        # if large uInt32 as an integer, PHP converts it to -ve
160        return (ord(s[0])*16777216) + (ord(s[1])<<16) + (ord(s[2])<<8) + ord(s[3]) #     16777216  = 1<<24
161
162    def get_ushort(self, pos):
163        self.fh.seek(pos)
164        s = self.fh.read(2)
165        return (ord(s[0])<<8) + ord(s[1])
166
167    def get_ulong(self, pos):
168        self.fh.seek(pos)
169        s = self.fh.read(4)
170        # iF large uInt32 as an integer, PHP converts it to -ve
171        return (ord(s[0])*16777216) + (ord(s[1])<<16) + (ord(s[2])<<8) + ord(s[3]) #     16777216  = 1<<24   
172
173    def pack_short(self, val):
174        if (val<0):
175            val = abs(val)
176            val = ~val
177            val += 1
178        return pack(">H",val)
179   
180    def splice(self, stream, offset, value):
181        return substr(stream,0,offset) + value + substr(stream,offset+strlen(value))
182   
183    def _set_ushort(self, stream, offset, value):
184        up = pack(">H", value)
185        return self.splice(stream, offset, up)   
186
187    def _set_short(self, stream, offset, val):
188        if (val<0):
189            val = abs(val)
190            val = ~val
191            val += 1
192        up = pack(">H",val)
193        return self.splice(stream, offset, up)
194
195    def get_chunk(self, pos, length):
196        self.fh.seek(pos)
197        if (length <1):  return ''
198        return (self.fh.read(length))
199
200    def get_table(self, tag):
201        (pos, length) = self.get_table_pos(tag)
202        if (length == 0):
203            die('Truetype font (' + self.filename + '): error reading table: ' + tag)
204        self.fh.seek(pos)
205        return (self.fh.read(length))
206
207    def add(self, tag, data):
208        if (tag == 'head') :
209            data = self.splice(data, 8, b("\0\0\0\0"))       
210        self.otables[tag] = data
211
212############################################/
213############################################/
214
215############################################/
216
217    def extractInfo(self):
218        #################/
219        # name - Naming table
220        #################/
221        self.sFamilyClass = 0
222        self.sFamilySubClass = 0
223
224        name_offset = self.seek_table("name")
225        format = self.read_ushort()
226        if (format != 0):
227            die("Unknown name table format " + format)
228        numRecords = self.read_ushort()
229        string_data_offset = name_offset + self.read_ushort()
230        names = {1:'',2:'',3:'',4:'',6:''}
231        K = list(names.keys())
232        nameCount = len(names)
233        for i in range(numRecords):
234            platformId = self.read_ushort()
235            encodingId = self.read_ushort()
236            languageId = self.read_ushort()
237            nameId = self.read_ushort()
238            length = self.read_ushort()
239            offset = self.read_ushort()
240            if (nameId not in K): continue
241            N = ''
242            if (platformId == 3 and encodingId == 1 and languageId == 0x409):  # Microsoft, Unicode, US English, PS Name
243                opos = self._pos
244                self.seek(string_data_offset + offset)
245                if (length % 2 != 0):
246                    die("PostScript name is UTF-16BE string of odd length")
247                length //= 2
248                N = ''
249                while (length > 0):
250                    char = self.read_ushort()
251                    N += (chr(char))
252                    length -= 1
253                self._pos = opos
254                self.seek(opos)
255           
256            elif (platformId == 1 and encodingId == 0 and languageId == 0):  # Macintosh, Roman, English, PS Name
257                opos = self._pos
258                N = self.get_chunk(string_data_offset + offset, length).decode("latin1")
259                self._pos = opos
260                self.seek(opos)
261           
262            if (N and names[nameId]==''):
263                names[nameId] = N
264                nameCount -= 1
265                if (nameCount==0): break
266           
267       
268        if (names[6]):
269            psName = names[6]
270        elif (names[4]):
271            psName = re.sub(' ','-',names[4])
272        elif (names[1]):
273            psName = re.sub(' ','-',names[1])
274        else:
275            psName = ''
276        if (not psName):
277            die("Could not find PostScript font name")
278        self.name = psName
279        if (names[1]):
280            self.familyName = names[1] 
281        else: 
282            self.familyName = psName
283        if (names[2]):
284            self.styleName = names[2]
285        else:
286            self.styleName = 'Regular'
287        if (names[4]):
288            self.fullName = names[4]
289        else:
290            self.fullName = psName
291        if (names[3]):
292            self.uniqueFontID = names[3]
293        else:
294            self.uniqueFontID = psName
295        if (names[6]):
296            self.fullName = names[6]
297
298        #################/
299        # head - Font header table
300        #################/
301        self.seek_table("head")
302        self.skip(18)
303        self.unitsPerEm = unitsPerEm = self.read_ushort()
304        scale = 1000 / float(unitsPerEm)
305        self.skip(16)
306        xMin = self.read_short()
307        yMin = self.read_short()
308        xMax = self.read_short()
309        yMax = self.read_short()
310        self.bbox = [(xMin*scale), (yMin*scale), (xMax*scale), (yMax*scale)]
311        self.skip(3*2)
312        indexToLocFormat = self.read_ushort()
313        glyphDataFormat = self.read_ushort()
314        if (glyphDataFormat != 0):
315            die('Unknown glyph data format ' + glyphDataFormat)
316
317        #################/
318        # hhea metrics table
319        #################/
320        # ttf2t1 seems to use this value rather than the one in OS/2 - so put in for compatibility
321        if ("hhea" in self.tables):
322            self.seek_table("hhea")
323            self.skip(4)
324            hheaAscender = self.read_short()
325            hheaDescender = self.read_short()
326            self.ascent = (hheaAscender *scale)
327            self.descent = (hheaDescender *scale)
328       
329
330        #################/
331        # OS/2 - OS/2 and Windows metrics table
332        #################/
333        if ("OS/2" in self.tables):
334            self.seek_table("OS/2")
335            version = self.read_ushort()
336            self.skip(2)
337            usWeightClass = self.read_ushort()
338            self.skip(2)
339            fsType = self.read_ushort()
340            if (fsType == 0x0002 or (fsType & 0x0300) != 0):
341                die('ERROR - Font file ' + self.filename + ' cannot be embedded due to copyright restrictions.')
342                self.restrictedUse = True
343           
344            self.skip(20)
345            sF = self.read_short()
346            self.sFamilyClass = (sF >> 8)
347            self.sFamilySubClass = (sF & 0xFF)
348            self._pos += 10  #PANOSE = 10 byte length
349            panose = self.fh.read(10)
350            self.skip(26)
351            sTypoAscender = self.read_short()
352            sTypoDescender = self.read_short()
353            if (not self.ascent):
354                self.ascent = (sTypoAscender*scale)
355            if (not self.descent):
356                self.descent = (sTypoDescender*scale)
357            if (version > 1):
358                self.skip(16)
359                sCapHeight = self.read_short()
360                self.capHeight = (sCapHeight*scale)
361            else:
362                self.capHeight = self.ascent           
363       
364        else:
365            usWeightClass = 500
366            if (not self.ascent): self.ascent = (yMax*scale)
367            if (not self.descent): self.descent = (yMin*scale)
368            self.capHeight = self.ascent
369       
370        self.stemV = 50 + int(pow((usWeightClass / 65.0),2))
371
372        #################/
373        # post - PostScript table
374        #################/
375        self.seek_table("post")
376        self.skip(4)
377        self.italicAngle = self.read_short() + self.read_ushort() / 65536.0
378        self.underlinePosition = self.read_short() * scale
379        self.underlineThickness = self.read_short() * scale
380        isFixedPitch = self.read_ulong()
381
382        self.flags = 4
383
384        if (self.italicAngle!= 0):
385            self.flags = self.flags | 64
386        if (usWeightClass >= 600):
387            self.flags = self.flags | 262144
388        if (isFixedPitch):
389            self.flags = self.flags | 1
390
391        #################/
392        # hhea - Horizontal header table
393        #################/
394        self.seek_table("hhea")
395        self.skip(32)
396        metricDataFormat = self.read_ushort()
397        if (metricDataFormat != 0):
398            die('Unknown horizontal metric data format '.metricDataFormat)
399        numberOfHMetrics = self.read_ushort()
400        if (numberOfHMetrics == 0):
401            die('Number of horizontal metrics is 0')
402
403        #################/
404        # maxp - Maximum profile table
405        #################/
406        self.seek_table("maxp")
407        self.skip(4)
408        numGlyphs = self.read_ushort()
409
410        #################/
411        # cmap - Character to glyph index mapping table
412        #################/
413        cmap_offset = self.seek_table("cmap")
414        self.skip(2)
415        cmapTableCount = self.read_ushort()
416        unicode_cmap_offset = 0
417        unicode_cmap_offset12 = 0
418       
419        for i in range(cmapTableCount):
420            platformID = self.read_ushort()
421            encodingID = self.read_ushort()
422            offset = self.read_ulong()
423            save_pos = self._pos
424            if platformID == 3 and encodingID == 10:  # Microsoft, UCS-4
425                format = self.get_ushort(cmap_offset + offset)
426                if (format == 12):
427                    if not unicode_cmap_offset12:
428                        unicode_cmap_offset12 = cmap_offset + offset
429                    break
430            if ((platformID == 3 and encodingID == 1) or platformID == 0):  # Microsoft, Unicode
431                format = self.get_ushort(cmap_offset + offset)
432                if (format == 4):
433                    if (not unicode_cmap_offset):
434                        unicode_cmap_offset = cmap_offset + offset
435                    break
436                   
437            self.seek(save_pos)
438       
439        if not unicode_cmap_offset and not unicode_cmap_offset12:
440            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)')
441
442        glyphToChar = {}
443        charToGlyph = {}
444        if unicode_cmap_offset12:
445            self.getCMAP12(unicode_cmap_offset12, glyphToChar, charToGlyph)
446        else:   
447            self.getCMAP4(unicode_cmap_offset, glyphToChar, charToGlyph)
448
449        #################/
450        # hmtx - Horizontal metrics table
451        #################/
452        self.getHMTX(numberOfHMetrics, numGlyphs, glyphToChar, scale)
453
454
455############################################/
456############################################/
457
458    def makeSubset(self, file, subset):
459        self.filename = file
460        with open(file ,'rb') as self.fh:
461            self._pos = 0
462            self.charWidths = []
463            self.glyphPos = {}
464            self.charToGlyph = {}
465            self.tables = {}
466            self.otables = {}
467            self.ascent = 0
468            self.descent = 0
469            self.skip(4)
470            self.maxUni = 0
471            self.readTableDirectory()
472
473            #################/
474            # head - Font header table
475            #################/
476            self.seek_table("head")
477            self.skip(50)
478            indexToLocFormat = self.read_ushort()
479            glyphDataFormat = self.read_ushort()
480
481            #################/
482            # hhea - Horizontal header table
483            #################/
484            self.seek_table("hhea")
485            self.skip(32)
486            metricDataFormat = self.read_ushort()
487            orignHmetrics = numberOfHMetrics = self.read_ushort()
488
489            #################/
490            # maxp - Maximum profile table
491            #################/
492            self.seek_table("maxp")
493            self.skip(4)
494            numGlyphs = self.read_ushort()
495
496            #################/
497            # cmap - Character to glyph index mapping table
498            #################/
499            cmap_offset = self.seek_table("cmap")
500            self.skip(2)
501            cmapTableCount = self.read_ushort()
502            unicode_cmap_offset = 0
503            unicode_cmap_offset12 = 0
504            for i in range(cmapTableCount):
505                platformID = self.read_ushort()
506                encodingID = self.read_ushort()
507                offset = self.read_ulong()
508                save_pos = self._pos
509                if platformID == 3 and encodingID == 10:  # Microsoft, UCS-4
510                    format = self.get_ushort(cmap_offset + offset)
511                    if (format == 12):
512                        if not unicode_cmap_offset12:
513                            unicode_cmap_offset12 = cmap_offset + offset
514                        break
515                if ((platformID == 3 and encodingID == 1) or platformID == 0):  # Microsoft, Unicode
516                    format = self.get_ushort(cmap_offset + offset)
517                    if (format == 4):
518                        unicode_cmap_offset = cmap_offset + offset
519                        break
520               
521                self.seek(save_pos )
522           
523            if not unicode_cmap_offset and not unicode_cmap_offset12:
524                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)')
525
526            glyphToChar = {}
527            charToGlyph = {}
528            if unicode_cmap_offset12:
529                self.getCMAP12(unicode_cmap_offset12, glyphToChar, charToGlyph)
530            else:   
531                self.getCMAP4(unicode_cmap_offset, glyphToChar, charToGlyph)
532
533            self.charToGlyph = charToGlyph
534
535            #################/
536            # hmtx - Horizontal metrics table
537            #################/
538            scale = 1    # not used
539            self.getHMTX(numberOfHMetrics, numGlyphs, glyphToChar, scale)
540
541            #################/
542            # loca - Index to location
543            #################/
544            self.getLOCA(indexToLocFormat, numGlyphs)
545
546            subsetglyphs = [(0, 0)]     # special "sorted dict"!
547            subsetCharToGlyph = {}
548            for code in subset:
549                if (code in self.charToGlyph):
550                    if (self.charToGlyph[code], code) not in subsetglyphs:
551                        subsetglyphs.append((self.charToGlyph[code], code))   # Old Glyph ID => Unicode
552                    subsetCharToGlyph[code] = self.charToGlyph[code]    # Unicode to old GlyphID
553                self.maxUni = max(self.maxUni, code)
554            (start,dummy) = self.get_table_pos('glyf')
555
556            subsetglyphs.sort()
557            glyphSet = {}
558            n = 0
559            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.
560            for originalGlyphIdx, uni in subsetglyphs:
561                fsLastCharIndex = max(fsLastCharIndex , uni)
562                glyphSet[originalGlyphIdx] = n    # old glyphID to new glyphID
563                n += 1
564
565            codeToGlyph = {}
566            for uni, originalGlyphIdx in sorted(subsetCharToGlyph.items()):
567                codeToGlyph[uni] = glyphSet[originalGlyphIdx]
568           
569            self.codeToGlyph = codeToGlyph
570           
571            for originalGlyphIdx, uni in subsetglyphs:
572                nonlocals = {'start': start, 'glyphSet': glyphSet,
573                             'subsetglyphs': subsetglyphs}
574                self.getGlyphs(originalGlyphIdx, nonlocals)
575
576            numGlyphs = numberOfHMetrics = len(subsetglyphs)
577
578            #tables copied from the original
579            tags = ['name']
580            for tag in tags: 
581                self.add(tag, self.get_table(tag))
582            tags = ['cvt ', 'fpgm', 'prep', 'gasp']
583            for tag in tags:
584                if (tag in self.tables): 
585                    self.add(tag, self.get_table(tag))       
586
587            # post - PostScript
588            opost = self.get_table('post')
589            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")
590            self.add('post', post)
591
592            # Sort CID2GID map into segments of contiguous codes
593            if 0 in codeToGlyph:
594                del codeToGlyph[0]
595            #unset(codeToGlyph[65535])
596            rangeid = 0
597            range_ = {}
598            prevcid = -2
599            prevglidx = -1
600            # for each character
601            for cid, glidx in sorted(codeToGlyph.items()):
602                if (cid == (prevcid + 1) and glidx == (prevglidx + 1)):
603                    range_[rangeid].append(glidx)
604                else:
605                    # new range
606                    rangeid = cid
607                    range_[rangeid] = []
608                    range_[rangeid].append(glidx)
609                prevcid = cid
610                prevglidx = glidx
611
612            # cmap - Character to glyph mapping - Format 4 (MS / )
613            segCount = len(range_) + 1    # + 1 Last segment has missing character 0xFFFF
614            searchRange = 1
615            entrySelector = 0
616            while (searchRange * 2 <= segCount ):
617                searchRange = searchRange * 2
618                entrySelector = entrySelector + 1
619           
620            searchRange = searchRange * 2
621            rangeShift = segCount * 2 - searchRange
622            length = 16 + (8*segCount ) + (numGlyphs+1)
623            cmap = [0, 1,        # Index : version, number of encoding subtables
624                3, 1,                # Encoding Subtable : platform (MS=3), encoding (Unicode)
625                0, 12,            # Encoding Subtable : offset (hi,lo)
626                4, length, 0,         # Format 4 Mapping subtable: format, length, language
627                segCount*2,
628                searchRange,
629                entrySelector,
630                rangeShift]
631
632            range_ = sorted(range_.items())
633           
634            # endCode(s)
635            for start, subrange in range_:
636                endCode = start + (len(subrange)-1)
637                cmap.append(endCode)    # endCode(s)
638           
639            cmap.append(0xFFFF)    # endCode of last Segment
640            cmap.append(0)    # reservedPad
641
642            # startCode(s)
643            for start, subrange in range_:
644                cmap.append(start)    # startCode(s)
645           
646            cmap.append(0xFFFF)    # startCode of last Segment
647            # idDelta(s)
648            for start, subrange in range_:
649                idDelta = -(start-subrange[0])
650                n += count(subrange)
651                cmap.append(idDelta)    # idDelta(s)
652           
653            cmap.append(1)    # idDelta of last Segment
654            # idRangeOffset(s)
655            for subrange in range_:
656                cmap.append(0)    # idRangeOffset[segCount]      Offset in bytes to glyph indexArray, or 0
657           
658            cmap.append(0)    # idRangeOffset of last Segment
659            for subrange, glidx in range_:
660                cmap.extend(glidx)
661           
662            cmap.append(0)    # Mapping for last character
663            cmapstr = b('')
664            for cm in cmap:
665                if cm >= 0:
666                    cmapstr += pack(">H", cm)
667                else:
668                    try:
669                        cmapstr += pack(">h", cm)
670                    except:
671                        warnings.warn("cmap value too big/small: %s" % cm)
672                        cmapstr += pack(">H", -cm)
673            self.add('cmap', cmapstr)
674
675            # glyf - Glyph data
676            (glyfOffset,glyfLength) = self.get_table_pos('glyf')
677            if (glyfLength < self.maxStrLenRead):
678                glyphData = self.get_table('glyf')
679
680            offsets = []
681            glyf = b('')
682            pos = 0
683
684            hmtxstr = b('')
685            xMinT = 0
686            yMinT = 0
687            xMaxT = 0
688            yMaxT = 0
689            advanceWidthMax = 0
690            minLeftSideBearing = 0
691            minRightSideBearing = 0
692            xMaxExtent = 0
693            maxPoints = 0            # points in non-compound glyph
694            maxContours = 0            # contours in non-compound glyph
695            maxComponentPoints = 0    # points in compound glyph
696            maxComponentContours = 0    # contours in compound glyph
697            maxComponentElements = 0    # number of glyphs referenced at top level
698            maxComponentDepth = 0        # levels of recursion, set to 0 if font has only simple glyphs
699            self.glyphdata = {}
700
701            for originalGlyphIdx, uni in subsetglyphs:
702                # hmtx - Horizontal Metrics
703                hm = self.getHMetric(orignHmetrics, originalGlyphIdx)   
704                hmtxstr += hm
705
706                offsets.append(pos)
707                try:
708                    glyphPos = self.glyphPos[originalGlyphIdx]
709                    glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos
710                except IndexError:
711                    warnings.warn("missing glyph %s" % (originalGlyphIdx))
712                    glyphLen = 0
713
714                if (glyfLength < self.maxStrLenRead):
715                    data = substr(glyphData,glyphPos,glyphLen)
716                else:
717                    if (glyphLen > 0):
718                        data = self.get_chunk(glyfOffset+glyphPos,glyphLen)
719                    else:
720                        data = b('')
721               
722                if (glyphLen > 0):
723                    up = unpack(">H", substr(data,0,2))[0]
724                if (glyphLen > 2 and (up & (1 << 15)) ):     # If number of contours <= -1 i.e. composite glyph
725                    pos_in_glyph = 10
726                    flags = GF_MORE
727                    nComponentElements = 0
728                    while (flags & GF_MORE):
729                        nComponentElements += 1    # number of glyphs referenced at top level
730                        up = unpack(">H", substr(data,pos_in_glyph,2))
731                        flags = up[0]
732                        up = unpack(">H", substr(data,pos_in_glyph+2,2))
733                        glyphIdx = up[0]
734                        self.glyphdata.setdefault(originalGlyphIdx, {}).setdefault('compGlyphs', []).append(glyphIdx)
735                        try:
736                            data = self._set_ushort(data, pos_in_glyph + 2, glyphSet[glyphIdx])
737                        except KeyError:
738                            data = 0
739                            warnings.warn("missing glyph data %s" % glyphIdx)
740                        pos_in_glyph += 4
741                        if (flags & GF_WORDS):
742                            pos_in_glyph += 4
743                        else:
744                            pos_in_glyph += 2
745                        if (flags & GF_SCALE):
746                            pos_in_glyph += 2
747                        elif (flags & GF_XYSCALE):
748                            pos_in_glyph += 4
749                        elif (flags & GF_TWOBYTWO):
750                            pos_in_glyph += 8
751                   
752                    maxComponentElements = max(maxComponentElements, nComponentElements)
753               
754                glyf += data
755                pos += glyphLen
756                if (pos % 4 != 0):
757                    padding = 4 - (pos % 4)
758                    glyf += str_repeat(b("\0"),padding)
759                    pos += padding
760
761            offsets.append(pos)
762            self.add('glyf', glyf)
763
764            # hmtx - Horizontal Metrics
765            self.add('hmtx', hmtxstr)
766
767            # loca - Index to location
768            locastr = b('')
769            if (((pos + 1) >> 1) > 0xFFFF):
770                indexToLocFormat = 1        # long format
771                for offset in offsets:
772                    locastr += pack(">L",offset)
773            else:
774                indexToLocFormat = 0        # short format
775                for offset in offsets: 
776                    locastr += pack(">H",offset//2)
777           
778            self.add('loca', locastr)
779
780            # head - Font header
781            head = self.get_table('head')
782            head = self._set_ushort(head, 50, indexToLocFormat)
783            self.add('head', head)
784
785            # hhea - Horizontal Header
786            hhea = self.get_table('hhea')
787            hhea = self._set_ushort(hhea, 34, numberOfHMetrics)
788            self.add('hhea', hhea)
789
790            # maxp - Maximum Profile
791            maxp = self.get_table('maxp')
792            maxp = self._set_ushort(maxp, 4, numGlyphs)
793            self.add('maxp', maxp)
794
795            # OS/2 - OS/2
796            os2 = self.get_table('OS/2')
797            self.add('OS/2', os2 )
798
799        # Put the TTF file together
800        stm = self.endTTFile('')
801        return stm
802   
803
804    #########################################
805    # Recursively get composite glyph data
806    def getGlyphData(self, originalGlyphIdx, nonlocals):
807        # &maxdepth, &depth, &points, &contours
808        nonlocals['depth'] += 1
809        nonlocals['maxdepth'] = max(nonlocals['maxdepth'], nonlocals['depth'])
810        if (len(self.glyphdata[originalGlyphIdx]['compGlyphs'])):
811            for glyphIdx in self.glyphdata[originalGlyphIdx]['compGlyphs']:
812                self.getGlyphData(glyphIdx, nonlocals)           
813       
814        elif ((self.glyphdata[originalGlyphIdx]['nContours'] > 0) and nonlocals['depth'] > 0):     # simple
815            contours += self.glyphdata[originalGlyphIdx]['nContours']
816            points += self.glyphdata[originalGlyphIdx]['nPoints']
817       
818        nonlocals['depth'] -= 1
819
820
821    #########################################
822    # Recursively get composite glyphs
823    def getGlyphs(self, originalGlyphIdx, nonlocals):
824        # &start, &glyphSet, &subsetglyphs)
825       
826        try:
827            glyphPos = self.glyphPos[originalGlyphIdx]
828            glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos
829        except IndexError:
830            warnings.warn("missing glyph %s" % (originalGlyphIdx))
831            return
832
833        if (not glyphLen): 
834            return
835       
836        self.seek(nonlocals['start'] + glyphPos)
837        numberOfContours = self.read_short()
838        if (numberOfContours < 0):
839            self.skip(8)
840            flags = GF_MORE
841            while (flags & GF_MORE):
842                flags = self.read_ushort()
843                glyphIdx = self.read_ushort()
844                if (glyphIdx not in nonlocals['glyphSet']):
845                    nonlocals['glyphSet'][glyphIdx] = len(nonlocals['subsetglyphs'])    # old glyphID to new glyphID
846                    nonlocals['subsetglyphs'].append((glyphIdx, 1))
847               
848                savepos = self.fh.tell()
849                self.getGlyphs(glyphIdx, nonlocals)
850                self.seek(savepos)
851                if (flags & GF_WORDS):
852                    self.skip(4)
853                else:
854                    self.skip(2)
855                if (flags & GF_SCALE):
856                    self.skip(2)
857                elif (flags & GF_XYSCALE):
858                    self.skip(4)
859                elif (flags & GF_TWOBYTWO):
860                    self.skip(8)
861
862    #########################################
863
864    def getHMTX(self, numberOfHMetrics, numGlyphs, glyphToChar, scale):
865        start = self.seek_table("hmtx")
866        aw = 0
867        self.charWidths = []
868        def resize_cw(size, default):
869            size = (((size + 1) // 1024) + 1) * 1024
870            delta = size - len(self.charWidths)
871            if delta > 0:
872                self.charWidths += [default] * delta
873        nCharWidths = 0
874        if ((numberOfHMetrics*4) < self.maxStrLenRead):
875            data = self.get_chunk(start,(numberOfHMetrics*4))
876            arr = unpack(">%dH" % (len(data)//2), data)
877        else:
878            self.seek(start)
879        for glyph in range(numberOfHMetrics):
880            if ((numberOfHMetrics*4) < self.maxStrLenRead):
881                aw = arr[(glyph*2)] # PHP starts arrays from index 0!? +1
882            else:
883                aw = self.read_ushort()
884                lsb = self.read_ushort()
885           
886            if (glyph in glyphToChar or glyph == 0):
887                if (aw >= (1 << 15) ):
888                    aw = 0     # 1.03 Some (arabic) fonts have -ve values for width
889                    # although should be unsigned value - comes out as e.g. 65108 (intended -50)
890                if (glyph == 0):
891                    self.defaultWidth = scale*aw
892                    continue
893               
894                for char in glyphToChar[glyph]:
895                    if (char != 0 and char != 65535):
896                        w = int(round(scale*aw+0.001))   # ROUND_HALF_UP in PY3K (like php)
897                        if (w == 0):  w = 65535
898                        if (char < 196608):
899                            if char >= len(self.charWidths):
900                                resize_cw(char, self.defaultWidth)
901                            self.charWidths[char] = w
902                            nCharWidths += 1
903           
904       
905        data = self.get_chunk((start+numberOfHMetrics*4),(numGlyphs*2))
906        arr = unpack(">%dH" % (len(data)//2), data)
907        diff = numGlyphs-numberOfHMetrics
908        for pos in range(diff):
909            glyph = pos + numberOfHMetrics
910            if (glyph in glyphToChar):
911                for char in glyphToChar[glyph]:
912                    if (char != 0 and char != 65535):
913                        w = int(round(scale*aw+0.001))  # ROUND_HALF_UP in PY3K (like php)
914                        if (w == 0):  w = 65535
915                        if (char < 196608):
916                            if char >= len(self.charWidths):
917                                resize_cw(char, self.defaultWidth)
918                            self.charWidths[char] = w
919                            nCharWidths += 1
920                       
921       
922        # NB 65535 is a set width of 0
923        # First bytes define number of chars in font
924        self.charWidths[0] = nCharWidths
925   
926
927    def getHMetric(self, numberOfHMetrics, gid):
928        start = self.seek_table("hmtx")
929        if (gid < numberOfHMetrics):
930            self.seek(start+(gid*4))
931            hm = self.fh.read(4)
932        else:
933            self.seek(start+((numberOfHMetrics-1)*4))
934            hm = self.fh.read(2)
935            self.seek(start+(numberOfHMetrics*2)+(gid*2))
936            hm += self.fh.read(2)
937        return hm
938   
939
940    def getLOCA(self, indexToLocFormat, numGlyphs):
941        start = self.seek_table('loca')
942        self.glyphPos = []
943        if (indexToLocFormat == 0):
944            data = self.get_chunk(start,(numGlyphs*2)+2)
945            arr = unpack(">%dH" % (len(data)//2), data)
946            for n in range(numGlyphs):
947                self.glyphPos.append((arr[n] * 2))  # n+1 !?
948        elif (indexToLocFormat == 1):
949            data = self.get_chunk(start,(numGlyphs*4)+4)
950            arr = unpack(">%dL" % (len(data)//4), data)
951            for n in range(numGlyphs):
952                self.glyphPos.append((arr[n]))  # n+1 !?
953        else:
954            die('Unknown location table format ' + indexToLocFormat)
955
956    # CMAP Format 4
957    def getCMAP4(self, unicode_cmap_offset, glyphToChar, charToGlyph):
958        self.maxUniChar = 0
959        self.seek(unicode_cmap_offset + 2)
960        length = self.read_ushort()
961        limit = unicode_cmap_offset + length
962        self.skip(2)
963
964        segCount = self.read_ushort() // 2
965        self.skip(6)
966        endCount = []
967        for i in range(segCount):
968            endCount.append(self.read_ushort())
969        self.skip(2)
970        startCount = []
971        for i in range(segCount):
972            startCount.append(self.read_ushort())
973        idDelta = []
974        for i in range(segCount):
975            idDelta.append(self.read_short())         # ???? was unsigned short
976        idRangeOffset_start = self._pos
977        idRangeOffset = []
978        for i in range(segCount):
979            idRangeOffset.append(self.read_ushort())
980
981        for n in range(segCount):
982            endpoint = (endCount[n] + 1)
983            for unichar in range(startCount[n], endpoint, 1):
984                if (idRangeOffset[n] == 0):
985                    glyph = (unichar + idDelta[n]) & 0xFFFF
986                else:
987                    offset = (unichar - startCount[n]) * 2 + idRangeOffset[n]
988                    offset = idRangeOffset_start + 2 * n + offset
989                    if (offset >= limit):
990                        glyph = 0
991                    else:
992                        glyph = self.get_ushort(offset)
993                        if (glyph != 0):
994                           glyph = (glyph + idDelta[n]) & 0xFFFF
995                   
996                charToGlyph[unichar] = glyph
997                if (unichar < 196608):
998                    self.maxUniChar = max(unichar,self.maxUniChar)
999                glyphToChar.setdefault(glyph, []).append(unichar)
1000
1001    # CMAP Format 12
1002    def getCMAP12(self, unicode_cmap_offset, glyphToChar, charToGlyph):
1003        self.maxUniChar = 0
1004        # table (skip format version, should be 12)
1005        self.seek(unicode_cmap_offset + 2)
1006        # reserved
1007        self.skip(2)
1008        # table length
1009        length = self.read_ulong()
1010        # language (should be 0)
1011        self.skip(4)
1012        # groups count
1013        grpCount = self.read_ulong()
1014
1015        if 2 + 2 + 4 + 4 + 4 + grpCount * 3 * 4 > length:
1016            die("TTF format 12 cmap table too small") 
1017        for n in range(grpCount):
1018            startCharCode = self.read_ulong()
1019            endCharCode = self.read_ulong()
1020            glyph = self.read_ulong()
1021            for unichar in range(startCharCode, endCharCode + 1):
1022                charToGlyph[unichar] = glyph
1023                if (unichar < 196608):
1024                    self.maxUniChar = max(unichar, self.maxUniChar)
1025                glyphToChar.setdefault(glyph, []).append(unichar)
1026                glyph += 1
1027           
1028           
1029
1030    # Put the TTF file together
1031    def endTTFile(self, stm):
1032        stm = b('')
1033        numTables = count(self.otables)
1034        searchRange = 1
1035        entrySelector = 0
1036        while (searchRange * 2 <= numTables):
1037            searchRange = searchRange * 2
1038            entrySelector = entrySelector + 1
1039       
1040        searchRange = searchRange * 16
1041        rangeShift = numTables * 16 - searchRange
1042
1043        # Header
1044        if (_TTF_MAC_HEADER):
1045            stm += (pack(">LHHHH", 0x74727565, numTables, searchRange, entrySelector, rangeShift))    # Mac
1046        else:
1047            stm += (pack(">LHHHH", 0x00010000 , numTables, searchRange, entrySelector, rangeShift))    # Windows
1048
1049       
1050        # Table directory
1051        tables = self.otables
1052
1053        offset = 12 + numTables * 16
1054        sorted_tables = sorted(tables.items())
1055        for tag, data in sorted_tables:
1056            if (tag == 'head'):
1057                head_start = offset
1058            stm += tag.encode("latin1")
1059            checksum = calcChecksum(data)
1060            stm += pack(">HH", checksum[0],checksum[1])
1061            stm += pack(">LL", offset, strlen(data))
1062            paddedLength = (strlen(data)+3)&~3
1063            offset = offset + paddedLength
1064
1065        # Table data
1066        for tag, data in sorted_tables:
1067            data += b("\0\0\0")
1068            stm += substr(data,0,(strlen(data)&~3))
1069
1070        checksum = calcChecksum(stm)
1071        checksum = sub32((0xB1B0,0xAFBA), checksum)
1072        chk = pack(">HH", checksum[0],checksum[1])
1073        stm = self.splice(stm,(head_start + 8),chk)
1074        return stm
1075   
Note: See TracBrowser for help on using the repository browser.