~openerp/openobject-server/web-dashboard

« back to all changes in this revision

Viewing changes to bin/reportlab/pdfbase/cidfonts.py

  • Committer: pinky
  • Date: 2006-12-07 13:41:40 UTC
  • Revision ID: pinky-3f10ee12cea3c4c75cef44ab04ad33ef47432907
New trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#Copyright ReportLab Europe Ltd. 2000-2004
 
2
#see license.txt for license details
 
3
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfbase/cidfonts.py
 
4
#$Header $
 
5
__version__=''' $Id$ '''
 
6
__doc__="""CID (Asian multi-byte) font support.
 
7
 
 
8
This defines classes to represent CID fonts.  They know how to calculate
 
9
their own width and how to write themselves into PDF files."""
 
10
 
 
11
import os
 
12
from types import ListType, TupleType, DictType
 
13
from string import find, split, strip
 
14
import marshal
 
15
import md5
 
16
import time
 
17
 
 
18
import reportlab
 
19
from reportlab.pdfbase import pdfmetrics
 
20
from reportlab.pdfbase._cidfontdata import allowedTypeFaces, allowedEncodings, CIDFontInfo
 
21
from reportlab.pdfgen.canvas import Canvas
 
22
from reportlab.pdfbase import pdfdoc
 
23
from reportlab.rl_config import CMapSearchPath
 
24
 
 
25
 
 
26
def findCMapFile(name):
 
27
    "Returns full filename, or raises error"
 
28
    for dirname in CMapSearchPath:
 
29
        cmapfile = dirname + os.sep + name
 
30
        if os.path.isfile(cmapfile):
 
31
            return cmapfile
 
32
    raise IOError, 'CMAP file for encodings "%s" not found!' % name
 
33
 
 
34
def structToPDF(structure):
 
35
    "Converts deeply nested structure to PDFdoc dictionary/array objects"
 
36
    if type(structure) is DictType:
 
37
        newDict = {}
 
38
        for k, v in structure.items():
 
39
            newDict[k] = structToPDF(v)
 
40
        return pdfdoc.PDFDictionary(newDict)
 
41
    elif type(structure) in (ListType, TupleType):
 
42
        newList = []
 
43
        for elem in structure:
 
44
            newList.append(structToPDF(elem))
 
45
        return pdfdoc.PDFArray(newList)
 
46
    else:
 
47
        return structure
 
48
 
 
49
class CIDEncoding(pdfmetrics.Encoding):
 
50
    """Multi-byte encoding.  These are loaded from CMAP files.
 
51
 
 
52
    A CMAP file is like a mini-codec.  It defines the correspondence
 
53
    between code points in the (multi-byte) input data and Character
 
54
    IDs. """
 
55
    # aims to do similar things to Brian Hooper's CMap class,
 
56
    # but I could not get it working and had to rewrite.
 
57
    # also, we should really rearrange our current encoding
 
58
    # into a SingleByteEncoding since many of its methods
 
59
    # should not apply here.
 
60
 
 
61
    def __init__(self, name, useCache=1):
 
62
        self.name = name
 
63
        self._mapFileHash = None
 
64
        self._codeSpaceRanges = []
 
65
        self._notDefRanges = []
 
66
        self._cmap = {}
 
67
        self.source = None
 
68
        if useCache:
 
69
            from reportlab.lib.utils import get_rl_tempdir
 
70
            fontmapdir = get_rl_tempdir('FastCMAPS')
 
71
            if os.path.isfile(fontmapdir + os.sep + name + '.fastmap'):
 
72
                self.fastLoad(fontmapdir)
 
73
                self.source = fontmapdir + os.sep + name + '.fastmap'
 
74
            else:
 
75
                self.parseCMAPFile(name)
 
76
                self.source = 'CMAP: ' + name
 
77
                self.fastSave(fontmapdir)
 
78
        else:
 
79
            self.parseCMAPFile(name)
 
80
 
 
81
    def _hash(self, text):
 
82
        hasher = md5.new()
 
83
        hasher.update(text)
 
84
        return hasher.digest()
 
85
 
 
86
    def parseCMAPFile(self, name):
 
87
        """This is a tricky one as CMAP files are Postscript
 
88
        ones.  Some refer to others with a 'usecmap'
 
89
        command"""
 
90
        started = time.clock()
 
91
        cmapfile = findCMapFile(name)
 
92
        # this will CRAWL with the unicode encodings...
 
93
        rawdata = open(cmapfile, 'r').read()
 
94
 
 
95
        self._mapFileHash = self._hash(rawdata)
 
96
        #if it contains the token 'usecmap', parse the other
 
97
        #cmap file first....
 
98
        usecmap_pos = find(rawdata, 'usecmap')
 
99
        if  usecmap_pos > -1:
 
100
            #they tell us to look in another file
 
101
            #for the code space ranges. The one
 
102
            # to use will be the previous word.
 
103
            chunk = rawdata[0:usecmap_pos]
 
104
            words = split(chunk)
 
105
            otherCMAPName = words[-1]
 
106
            #print 'referred to another CMAP %s' % otherCMAPName
 
107
            self.parseCMAPFile(otherCMAPName)
 
108
            # now continue parsing this, as it may
 
109
            # override some settings
 
110
 
 
111
 
 
112
        words = split(rawdata)
 
113
        while words <> []:
 
114
            if words[0] == 'begincodespacerange':
 
115
                words = words[1:]
 
116
                while words[0] <> 'endcodespacerange':
 
117
                    strStart, strEnd, words = words[0], words[1], words[2:]
 
118
                    start = int(strStart[1:-1], 16)
 
119
                    end = int(strEnd[1:-1], 16)
 
120
                    self._codeSpaceRanges.append((start, end),)
 
121
            elif words[0] == 'beginnotdefrange':
 
122
                words = words[1:]
 
123
                while words[0] <> 'endnotdefrange':
 
124
                    strStart, strEnd, strValue = words[0:3]
 
125
                    start = int(strStart[1:-1], 16)
 
126
                    end = int(strEnd[1:-1], 16)
 
127
                    value = int(strValue)
 
128
                    self._notDefRanges.append((start, end, value),)
 
129
                    words = words[3:]
 
130
            elif words[0] == 'begincidrange':
 
131
                words = words[1:]
 
132
                while words[0] <> 'endcidrange':
 
133
                    strStart, strEnd, strValue = words[0:3]
 
134
                    start = int(strStart[1:-1], 16)
 
135
                    end = int(strEnd[1:-1], 16)
 
136
                    value = int(strValue)
 
137
                    # this means that 'start' corresponds to 'value',
 
138
                    # start+1 corresponds to value+1 and so on up
 
139
                    # to end
 
140
                    offset = 0
 
141
                    while start + offset <= end:
 
142
                        self._cmap[start + offset] = value + offset
 
143
                        offset = offset + 1
 
144
                    words = words[3:]
 
145
 
 
146
            else:
 
147
                words = words[1:]
 
148
        finished = time.clock()
 
149
        print 'parsed CMAP %s in %0.4f seconds' % (self.name, finished - started)
 
150
 
 
151
    def translate(self, text):
 
152
        "Convert a string into a list of CIDs"
 
153
        output = []
 
154
        cmap = self._cmap
 
155
        lastChar = ''
 
156
        for char in text:
 
157
            if lastChar <> '':
 
158
                #print 'convert character pair "%s"' % (lastChar + char)
 
159
                num = ord(lastChar) * 256 + ord(char)
 
160
            else:
 
161
                #print 'convert character "%s"' % char
 
162
                num = ord(char)
 
163
            lastChar = char
 
164
            found = 0
 
165
            for low, high in self._codeSpaceRanges:
 
166
                if low < num < high:
 
167
                    try:
 
168
                        cid = cmap[num]
 
169
                        #print '%d -> %d' % (num, cid)
 
170
                    except KeyError:
 
171
                        #not defined.  Try to find the appropriate
 
172
                        # notdef character, or failing that return
 
173
                        # zero
 
174
                        cid = 0
 
175
                        for low2, high2, notdef in self._notDefRanges:
 
176
                            if low2 < num < high2:
 
177
                                cid = notdef
 
178
                                break
 
179
                    output.append(cid)
 
180
                    found = 1
 
181
                    break
 
182
            if found:
 
183
                lastChar = ''
 
184
            else:
 
185
                lastChar = char
 
186
        return output
 
187
 
 
188
    def fastSave(self, directory):
 
189
        f = open(os.path.join(directory, self.name + '.fastmap'), 'wb')
 
190
        marshal.dump(self._mapFileHash, f)
 
191
        marshal.dump(self._codeSpaceRanges, f)
 
192
        marshal.dump(self._notDefRanges, f)
 
193
        marshal.dump(self._cmap, f)
 
194
        f.close()
 
195
 
 
196
    def fastLoad(self, directory):
 
197
        started = time.clock()
 
198
        f = open(os.path.join(directory, self.name + '.fastmap'), 'rb')
 
199
        self._mapFileHash = marshal.load(f)
 
200
        self._codeSpaceRanges = marshal.load(f)
 
201
        self._notDefRanges = marshal.load(f)
 
202
        self._cmap = marshal.load(f)
 
203
        f.close()
 
204
        finished = time.clock()
 
205
        #print 'loaded %s in %0.4f seconds' % (self.name, finished - started)
 
206
 
 
207
 
 
208
class CIDTypeFace(pdfmetrics.TypeFace):
 
209
    """Multi-byte type face.
 
210
 
 
211
    Conceptually similar to a single byte typeface,
 
212
    but the glyphs are identified by a numeric Character
 
213
    ID (CID) and not a glyph name. """
 
214
    def __init__(self, name):
 
215
        """Initialised from one of the canned dictionaries in allowedEncodings
 
216
 
 
217
        Or rather, it will be shortly..."""
 
218
        pdfmetrics.TypeFace.__init__(self, name)
 
219
        self._extractDictInfo(name)
 
220
    def _extractDictInfo(self, name):
 
221
        try:
 
222
            fontDict = CIDFontInfo[name]
 
223
        except KeyError:
 
224
            raise KeyError, ("Unable to find information on CID typeface '%s'" % name +
 
225
                            "Only the following font names work:" + repr(allowedTypeFaces)
 
226
                             )
 
227
        descFont = fontDict['DescendantFonts'][0]
 
228
        self.ascent = descFont['FontDescriptor']['Ascent']
 
229
        self.descent = descFont['FontDescriptor']['Descent']
 
230
        self._defaultWidth = descFont['DW']
 
231
        self._explicitWidths = self._expandWidths(descFont['W'])
 
232
 
 
233
        # should really support self.glyphWidths, self.glyphNames
 
234
        # but not done yet.
 
235
 
 
236
 
 
237
    def _expandWidths(self, compactWidthArray):
 
238
        """Expands Adobe nested list structure to get a dictionary of widths.
 
239
 
 
240
        Here is an example of such a structure.
 
241
        (
 
242
            # starting at character ID 1, next n  characters have the widths given.
 
243
            1,  (277,305,500,668,668,906,727,305,445,445,508,668,305,379,305,539),
 
244
            # all Characters from ID 17 to 26 are 668 em units wide
 
245
            17, 26, 668,
 
246
            27, (305, 305, 668, 668, 668, 566, 871, 727, 637, 652, 699, 574, 555,
 
247
                 676, 687, 242, 492, 664, 582, 789, 707, 734, 582, 734, 605, 605,
 
248
                 641, 668, 727, 945, 609, 609, 574, 445, 668, 445, 668, 668, 590,
 
249
                 555, 609, 547, 602, 574, 391, 609, 582, 234, 277, 539, 234, 895,
 
250
                 582, 605, 602, 602, 387, 508, 441, 582, 562, 781, 531, 570, 555,
 
251
                 449, 246, 449, 668),
 
252
            # these must be half width katakana and the like.
 
253
            231, 632, 500
 
254
        )
 
255
        """
 
256
        data = compactWidthArray[:]
 
257
        widths = {}
 
258
        while data:
 
259
            start, data = data[0], data[1:]
 
260
            if type(data[0]) in (ListType, TupleType):
 
261
                items, data = data[0], data[1:]
 
262
                for offset in range(len(items)):
 
263
                    widths[start + offset] = items[offset]
 
264
            else:
 
265
                end, width, data = data[0], data[1], data[2:]
 
266
                for idx in range(start, end+1):
 
267
                    widths[idx] = width
 
268
        return widths
 
269
 
 
270
    def getCharWidth(self, characterId):
 
271
        return self._explicitWidths.get(characterId, self._defaultWidth)
 
272
 
 
273
class CIDFont(pdfmetrics.Font):
 
274
    "Represents a built-in multi-byte font"
 
275
    def __init__(self, face, encoding):
 
276
 
 
277
        self._multiByte = 1
 
278
        assert face in allowedTypeFaces, "TypeFace '%s' not supported! Use any of these instead: %s" % (face, allowedTypeFaces)
 
279
        self.faceName = face
 
280
        #should cache in registry...
 
281
        self.face = CIDTypeFace(face)
 
282
 
 
283
        assert encoding in allowedEncodings, "Encoding '%s' not supported!  Use any of these instead: %s" % (encoding, allowedEncodings)
 
284
        self.encodingName = encoding
 
285
        self.encoding = CIDEncoding(encoding)
 
286
 
 
287
        #legacy hack doing quick cut and paste.
 
288
        self.fontName = self.faceName + '-' + self.encodingName
 
289
        self.name = self.fontName
 
290
 
 
291
        # need to know if it is vertical or horizontal
 
292
        self.isVertical = (self.encodingName[-1] == 'V')
 
293
 
 
294
 
 
295
    def stringWidth(self, text, size):
 
296
        cidlist = self.encoding.translate(text)
 
297
        if self.isVertical:
 
298
            #this part is "not checked!" but seems to work.
 
299
            #assume each is 1000 ems high
 
300
            return len(cidlist) * size
 
301
        else:
 
302
            w = 0
 
303
            for cid in cidlist:
 
304
                w = w + self.face.getCharWidth(cid)
 
305
            return 0.001 * w * size
 
306
 
 
307
 
 
308
    def addObjects(self, doc):
 
309
        """The explicit code in addMinchoObjects and addGothicObjects
 
310
        will be replaced by something that pulls the data from
 
311
        _cidfontdata.py in the next few days."""
 
312
        internalName = 'F' + repr(len(doc.fontMapping)+1)
 
313
 
 
314
        bigDict = CIDFontInfo[self.face.name]
 
315
        bigDict['Name'] = '/' + internalName
 
316
        bigDict['Encoding'] = '/' + self.encodingName
 
317
 
 
318
        #convert to PDF dictionary/array objects
 
319
        cidObj = structToPDF(bigDict)
 
320
 
 
321
        # link into document, and add to font map
 
322
        r = doc.Reference(cidObj, internalName)
 
323
        fontDict = doc.idToObject['BasicFonts'].dict
 
324
        fontDict[internalName] = r
 
325
        doc.fontMapping[self.name] = '/' + internalName
 
326
 
 
327
 
 
328
 
 
329
def precalculate(cmapdir):
 
330
    # crunches through all, making 'fastmap' files
 
331
    import os
 
332
    files = os.listdir(cmapdir)
 
333
    for file in files:
 
334
        if os.path.isfile(cmapdir + os.sep + self.name + '.fastmap'):
 
335
            continue
 
336
        try:
 
337
            enc = CIDEncoding(file)
 
338
        except:
 
339
            print 'cannot parse %s, skipping' % enc
 
340
            continue
 
341
        enc.fastSave(cmapdir)
 
342
        print 'saved %s.fastmap' % file
 
343
 
 
344
def test():
 
345
    # only works if you have cirrect encodings on your box!
 
346
    c = Canvas('test_japanese.pdf')
 
347
    c.setFont('Helvetica', 30)
 
348
    c.drawString(100,700, 'Japanese Font Support')
 
349
 
 
350
    pdfmetrics.registerFont(CIDFont('HeiseiMin-W3','90ms-RKSJ-H'))
 
351
    pdfmetrics.registerFont(CIDFont('HeiseiKakuGo-W5','90ms-RKSJ-H'))
 
352
 
 
353
 
 
354
    # the two typefaces
 
355
    c.setFont('HeiseiMin-W3-90ms-RKSJ-H', 16)
 
356
    # this says "This is HeiseiMincho" in shift-JIS.  Not all our readers
 
357
    # have a Japanese PC, so I escaped it. On a Japanese-capable
 
358
    # system, print the string to see Kanji
 
359
    message1 = '\202\261\202\352\202\315\225\275\220\254\226\276\222\251\202\305\202\267\201B'
 
360
    c.drawString(100, 675, message1)
 
361
    c.save()
 
362
    print 'saved test_japanese.pdf'
 
363
 
 
364
 
 
365
##    print 'CMAP_DIR = ', CMAP_DIR
 
366
##    tf1 = CIDTypeFace('HeiseiMin-W3')
 
367
##    print 'ascent = ',tf1.ascent
 
368
##    print 'descent = ',tf1.descent
 
369
##    for cid in [1,2,3,4,5,18,19,28,231,1742]:
 
370
##        print 'width of cid %d = %d' % (cid, tf1.getCharWidth(cid))
 
371
 
 
372
    encName = '90ms-RKSJ-H'
 
373
    enc = CIDEncoding(encName)
 
374
    print message1, '->', enc.translate(message1)
 
375
 
 
376
    f = CIDFont('HeiseiMin-W3','90ms-RKSJ-H')
 
377
    print 'width = %0.2f' % f.stringWidth(message1, 10)
 
378
 
 
379
 
 
380
    #testing all encodings
 
381
##    import time
 
382
##    started = time.time()
 
383
##    import glob
 
384
##    for encName in _cidfontdata.allowedEncodings:
 
385
##    #encName = '90ms-RKSJ-H'
 
386
##        enc = CIDEncoding(encName)
 
387
##        print 'encoding %s:' % encName
 
388
##        print '    codeSpaceRanges = %s' % enc._codeSpaceRanges
 
389
##        print '    notDefRanges = %s' % enc._notDefRanges
 
390
##        print '    mapping size = %d' % len(enc._cmap)
 
391
##    finished = time.time()
 
392
##    print 'constructed all encodings in %0.2f seconds' % (finished - started)
 
393
 
 
394
if __name__=='__main__':
 
395
    test()
 
396
 
 
397
 
 
398
 
 
399