use with statement for file operations (where appropriate)
[PyX.git] / pyx / font / t1file.py
blob1f8d45dd8150e728190d766b2960392034aaad7c
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2005-2011 André Wobst <wobsta@users.sourceforge.net>
5 # Copyright (C) 2006-2011 Jörg Lehmann <joergl@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 import array, binascii, io, math, re, warnings
24 try:
25 import zlib
26 haszlib = True
27 except ImportError:
28 haszlib = False
31 from pyx import trafo, reader, writer
32 from pyx.path import path, moveto_pt, lineto_pt, curveto_pt, closepath
34 try:
35 from _t1code import *
36 except:
37 from .t1code import *
40 adobestandardencoding = [None, None, None, None, None, None, None, None,
41 None, None, None, None, None, None, None, None,
42 None, None, None, None, None, None, None, None,
43 None, None, None, None, None, None, None, None,
44 "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quoteright",
45 "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash",
46 "zero", "one", "two", "three", "four", "five", "six", "seven",
47 "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question",
48 "at", "A", "B", "C", "D", "E", "F", "G",
49 "H", "I", "J", "K", "L", "M", "N", "O",
50 "P", "Q", "R", "S", "T", "U", "V", "W",
51 "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore",
52 "quoteleft", "a", "b", "c", "d", "e", "f", "g",
53 "h", "i", "j", "k", "l", "m", "n", "o",
54 "p", "q", "r", "s", "t", "u", "v", "w",
55 "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", None,
56 None, None, None, None, None, None, None, None,
57 None, None, None, None, None, None, None, None,
58 None, None, None, None, None, None, None, None,
59 None, None, None, None, None, None, None, None,
60 None, "exclamdown", "cent", "sterling", "fraction", "yen", "florin", "section",
61 "currency", "quotesingle", "quotedblleft", "guillemotleft", "guilsinglleft", "guilsinglright", "fi", "fl",
62 None, "endash", "dagger", "daggerdbl", "periodcentered", None, "paragraph", "bullet",
63 "quotesinglbase", "quotedblbase", "quotedblright", "guillemotright", "ellipsis", "perthousand", None, "questiondown",
64 None, "grave", "acute", "circumflex", "tilde", "macron", "breve", "dotaccent",
65 "dieresis", None, "ring", "cedilla", None, "hungarumlaut", "ogonek", "caron",
66 "emdash", None, None, None, None, None, None, None,
67 None, None, None, None, None, None, None, None,
68 None, "AE", None, "ordfeminine", None, None, None, None,
69 "Lslash", "Oslash", "OE", "ordmasculine", None, None, None, None,
70 None, "ae", None, None, None, "dotlessi", None, None,
71 "lslash", "oslash", "oe", "germandbls", None, None, None, None]
73 class T1context:
75 def __init__(self, t1font, flex=True):
76 """context for T1cmd evaluation"""
77 self.t1font = t1font
79 # state description
80 self.x = None
81 self.y = None
82 self.wx = None
83 self.wy = None
84 self.t1stack = []
85 self.psstack = []
86 self.flex = flex
89 ######################################################################
90 # T1 commands
91 # Note, that the T1 commands are variable-free except for plain number,
92 # which are stored as integers. All other T1 commands exist as a single
93 # instance only
95 T1cmds = {}
96 T1subcmds = {}
98 class T1cmd:
100 def __init__(self, code, subcmd=0):
101 self.code = code
102 self.subcmd = subcmd
103 if subcmd:
104 T1subcmds[code] = self
105 else:
106 T1cmds[code] = self
108 def __str__(self):
109 """returns a string representation of the T1 command"""
110 raise NotImplementedError
112 def updatepath(self, path, trafo, context):
113 """update path instance applying trafo to the points"""
114 raise NotImplementedError
116 def gathercalls(self, seacglyphs, subrs, context):
117 """gather dependancy information
119 subrs is the "called-subrs" dictionary. gathercalls will insert the
120 subr number as key having the value 1, i.e. subrs will become the
121 numbers of used subrs. Similar seacglyphs will contain all glyphs in
122 composite characters (subrs for those glyphs will also
123 already be included).
125 This method might will not properly update all information in the
126 context (especially consuming values from the stack) and will also skip
127 various tests for performance reasons. For most T1 commands it just
128 doesn't need to do anything.
130 pass
133 # commands for starting and finishing
135 class _T1endchar(T1cmd):
137 def __init__(self):
138 T1cmd.__init__(self, 14)
140 def __str__(self):
141 return "endchar"
143 def updatepath(self, path, trafo, context):
144 pass
146 T1endchar = _T1endchar()
149 class _T1hsbw(T1cmd):
151 def __init__(self):
152 T1cmd.__init__(self, 13)
154 def __str__(self):
155 return "hsbw"
157 def updatepath(self, path, trafo, context):
158 sbx = context.t1stack.pop(0)
159 wx = context.t1stack.pop(0)
160 path.append(moveto_pt(*trafo.apply_pt(sbx, 0)))
161 context.x = sbx
162 context.y = 0
163 context.wx = wx
164 context.wy = 0
166 T1hsbw = _T1hsbw()
169 class _T1seac(T1cmd):
171 def __init__(self):
172 T1cmd.__init__(self, 6, subcmd=1)
174 def __str__(self):
175 return "seac"
177 def updatepath(self, path, atrafo, context):
178 sab = context.t1stack.pop(0)
179 adx = context.t1stack.pop(0)
180 ady = context.t1stack.pop(0)
181 bchar = context.t1stack.pop(0)
182 achar = context.t1stack.pop(0)
183 aglyph = adobestandardencoding[achar]
184 bglyph = adobestandardencoding[bchar]
185 context.t1font.updateglyphpath(bglyph, path, atrafo, context)
186 atrafo = atrafo * trafo.translate_pt(adx-sab, ady)
187 context.t1font.updateglyphpath(aglyph, path, atrafo, context)
189 def gathercalls(self, seacglyphs, subrs, context):
190 achar = context.t1stack.pop()
191 bchar = context.t1stack.pop()
192 aglyph = adobestandardencoding[achar]
193 bglyph = adobestandardencoding[bchar]
194 seacglyphs.add(aglyph)
195 seacglyphs.add(bglyph)
196 context.t1font.gatherglyphcalls(bglyph, seacglyphs, subrs, context)
197 context.t1font.gatherglyphcalls(aglyph, seacglyphs, subrs, context)
199 T1seac = _T1seac()
202 class _T1sbw(T1cmd):
204 def __init__(self):
205 T1cmd.__init__(self, 7, subcmd=1)
207 def __str__(self):
208 return "sbw"
210 def updatepath(self, path, trafo, context):
211 sbx = context.t1stack.pop(0)
212 sby = context.t1stack.pop(0)
213 wx = context.t1stack.pop(0)
214 wy = context.t1stack.pop(0)
215 path.append(moveto_pt(*trafo.apply_pt(sbx, sby)))
216 context.x = sbx
217 context.y = sby
218 context.wx = wx
219 context.wy = wy
221 T1sbw = _T1sbw()
224 # path construction commands
226 class _T1closepath(T1cmd):
228 def __init__(self):
229 T1cmd.__init__(self, 9)
231 def __str__(self):
232 return "closepath"
234 def updatepath(self, path, trafo, context):
235 path.append(closepath())
236 # The closepath in T1 is different from PostScripts in that it does
237 # *not* modify the current position; hence we need to add an additional
238 # moveto here ...
239 path.append(moveto_pt(*trafo.apply_pt(context.x, context.y)))
241 T1closepath = _T1closepath()
244 class _T1hlineto(T1cmd):
246 def __init__(self):
247 T1cmd.__init__(self, 6)
249 def __str__(self):
250 return "hlineto"
252 def updatepath(self, path, trafo, context):
253 dx = context.t1stack.pop(0)
254 path.append(lineto_pt(*trafo.apply_pt(context.x + dx, context.y)))
255 context.x += dx
257 T1hlineto = _T1hlineto()
260 class _T1hmoveto(T1cmd):
262 def __init__(self):
263 T1cmd.__init__(self, 22)
265 def __str__(self):
266 return "hmoveto"
268 def updatepath(self, path, trafo, context):
269 dx = context.t1stack.pop(0)
270 path.append(moveto_pt(*trafo.apply_pt(context.x + dx, context.y)))
271 context.x += dx
273 T1hmoveto = _T1hmoveto()
276 class _T1hvcurveto(T1cmd):
278 def __init__(self):
279 T1cmd.__init__(self, 31)
281 def __str__(self):
282 return "hvcurveto"
284 def updatepath(self, path, trafo, context):
285 dx1 = context.t1stack.pop(0)
286 dx2 = context.t1stack.pop(0)
287 dy2 = context.t1stack.pop(0)
288 dy3 = context.t1stack.pop(0)
289 path.append(curveto_pt(*(trafo.apply_pt(context.x + dx1, context.y) +
290 trafo.apply_pt(context.x + dx1 + dx2, context.y + dy2) +
291 trafo.apply_pt(context.x + dx1 + dx2, context.y + dy2 + dy3))))
292 context.x += dx1+dx2
293 context.y += dy2+dy3
295 T1hvcurveto = _T1hvcurveto()
298 class _T1rlineto(T1cmd):
300 def __init__(self):
301 T1cmd.__init__(self, 5)
303 def __str__(self):
304 return "rlineto"
306 def updatepath(self, path, trafo, context):
307 dx = context.t1stack.pop(0)
308 dy = context.t1stack.pop(0)
309 path.append(lineto_pt(*trafo.apply_pt(context.x + dx, context.y + dy)))
310 context.x += dx
311 context.y += dy
313 T1rlineto = _T1rlineto()
316 class _T1rmoveto(T1cmd):
318 def __init__(self):
319 T1cmd.__init__(self, 21)
321 def __str__(self):
322 return "rmoveto"
324 def updatepath(self, path, trafo, context):
325 dx = context.t1stack.pop(0)
326 dy = context.t1stack.pop(0)
327 path.append(moveto_pt(*trafo.apply_pt(context.x + dx, context.y + dy)))
328 context.x += dx
329 context.y += dy
331 T1rmoveto = _T1rmoveto()
334 class _T1rrcurveto(T1cmd):
336 def __init__(self):
337 T1cmd.__init__(self, 8)
339 def __str__(self):
340 return "rrcurveto"
342 def updatepath(self, path, trafo, context):
343 dx1 = context.t1stack.pop(0)
344 dy1 = context.t1stack.pop(0)
345 dx2 = context.t1stack.pop(0)
346 dy2 = context.t1stack.pop(0)
347 dx3 = context.t1stack.pop(0)
348 dy3 = context.t1stack.pop(0)
349 path.append(curveto_pt(*(trafo.apply_pt(context.x + dx1, context.y + dy1) +
350 trafo.apply_pt(context.x + dx1 + dx2, context.y + dy1 + dy2) +
351 trafo.apply_pt(context.x + dx1 + dx2 + dx3, context.y + dy1 + dy2 + dy3))))
352 context.x += dx1+dx2+dx3
353 context.y += dy1+dy2+dy3
355 T1rrcurveto = _T1rrcurveto()
358 class _T1vlineto(T1cmd):
360 def __init__(self):
361 T1cmd.__init__(self, 7)
363 def __str__(self):
364 return "vlineto"
366 def updatepath(self, path, trafo, context):
367 dy = context.t1stack.pop(0)
368 path.append(lineto_pt(*trafo.apply_pt(context.x, context.y + dy)))
369 context.y += dy
371 T1vlineto = _T1vlineto()
374 class _T1vmoveto(T1cmd):
376 def __init__(self):
377 T1cmd.__init__(self, 4)
379 def __str__(self):
380 return "vmoveto"
382 def updatepath(self, path, trafo, context):
383 dy = context.t1stack.pop(0)
384 path.append(moveto_pt(*trafo.apply_pt(context.x, context.y + dy)))
385 context.y += dy
387 T1vmoveto = _T1vmoveto()
390 class _T1vhcurveto(T1cmd):
392 def __init__(self):
393 T1cmd.__init__(self, 30)
395 def __str__(self):
396 return "vhcurveto"
398 def updatepath(self, path, trafo, context):
399 dy1 = context.t1stack.pop(0)
400 dx2 = context.t1stack.pop(0)
401 dy2 = context.t1stack.pop(0)
402 dx3 = context.t1stack.pop(0)
403 path.append(curveto_pt(*(trafo.apply_pt(context.x, context.y + dy1) +
404 trafo.apply_pt(context.x + dx2, context.y + dy1 + dy2) +
405 trafo.apply_pt(context.x + dx2 + dx3, context.y + dy1 + dy2))))
406 context.x += dx2+dx3
407 context.y += dy1+dy2
409 T1vhcurveto = _T1vhcurveto()
412 # hint commands
414 class _T1dotsection(T1cmd):
416 def __init__(self):
417 T1cmd.__init__(self, 0, subcmd=1)
419 def __str__(self):
420 return "dotsection"
422 def updatepath(self, path, trafo, context):
423 pass
425 T1dotsection = _T1dotsection()
428 class _T1hstem(T1cmd):
430 def __init__(self):
431 T1cmd.__init__(self, 1)
433 def __str__(self):
434 return "hstem"
436 def updatepath(self, path, trafo, context):
437 y = context.t1stack.pop(0)
438 dy = context.t1stack.pop(0)
440 T1hstem = _T1hstem()
443 class _T1hstem3(T1cmd):
445 def __init__(self):
446 T1cmd.__init__(self, 2, subcmd=1)
448 def __str__(self):
449 return "hstem3"
451 def updatepath(self, path, trafo, context):
452 y0 = context.t1stack.pop(0)
453 dy0 = context.t1stack.pop(0)
454 y1 = context.t1stack.pop(0)
455 dy1 = context.t1stack.pop(0)
456 y2 = context.t1stack.pop(0)
457 dy2 = context.t1stack.pop(0)
459 T1hstem3 = _T1hstem3()
462 class _T1vstem(T1cmd):
464 def __init__(self):
465 T1cmd.__init__(self, 3)
467 def __str__(self):
468 return "vstem"
470 def updatepath(self, path, trafo, context):
471 x = context.t1stack.pop(0)
472 dx = context.t1stack.pop(0)
474 T1vstem = _T1vstem()
477 class _T1vstem3(T1cmd):
479 def __init__(self):
480 T1cmd.__init__(self, 1, subcmd=1)
482 def __str__(self):
483 return "vstem3"
485 def updatepath(self, path, trafo, context):
486 self.x0 = context.t1stack.pop(0)
487 self.dx0 = context.t1stack.pop(0)
488 self.x1 = context.t1stack.pop(0)
489 self.dx1 = context.t1stack.pop(0)
490 self.x2 = context.t1stack.pop(0)
491 self.dx2 = context.t1stack.pop(0)
493 T1vstem3 = _T1vstem3()
496 # arithmetic command
498 class _T1div(T1cmd):
500 def __init__(self):
501 T1cmd.__init__(self, 12, subcmd=1)
503 def __str__(self):
504 return "div"
506 def updatepath(self, path, trafo, context):
507 num2 = context.t1stack.pop()
508 num1 = context.t1stack.pop()
509 context.t1stack.append(divmod(num1, num2)[0])
511 def gathercalls(self, seacglyphs, subrs, context):
512 num2 = context.t1stack.pop()
513 num1 = context.t1stack.pop()
514 context.t1stack.append(divmod(num1, num2)[0])
516 T1div = _T1div()
519 # subroutine commands
521 class _T1callothersubr(T1cmd):
523 def __init__(self):
524 T1cmd.__init__(self, 16, subcmd=1)
526 def __str__(self):
527 return "callothersubr"
529 def updatepath(self, path, trafo, context):
530 othersubrnumber = context.t1stack.pop()
531 n = context.t1stack.pop()
532 for i in range(n):
533 context.psstack.append(context.t1stack.pop(0))
534 if othersubrnumber == 0:
535 flex_size, x, y = context.psstack[-3:]
536 if context.flex:
537 x1, y1, x2, y2, x3, y3 = context.psstack[2:8]
538 x1, y1 = trafo.apply_pt(x1, y1)
539 x2, y2 = trafo.apply_pt(x2, y2)
540 x3, y3 = trafo.apply_pt(x3, y3)
541 path.append(curveto_pt(x1, y1, x2, y2, x3, y3))
542 x1, y1, x2, y2, x3, y3 = context.psstack[8:14]
543 x1, y1 = trafo.apply_pt(x1, y1)
544 x2, y2 = trafo.apply_pt(x2, y2)
545 x3, y3 = trafo.apply_pt(x3, y3)
546 path.append(curveto_pt(x1, y1, x2, y2, x3, y3))
547 else:
548 path.append(lineto_pt(*trafo.apply_pt(x, y)))
549 context.psstack = [y, x]
550 elif othersubrnumber == 1:
551 pass
552 elif othersubrnumber == 2:
553 path.pathitems.pop()
554 context.psstack.append(context.x)
555 context.psstack.append(context.y)
557 def gathercalls(self, seacglyphs, subrs, context):
558 othersubrnumber = context.t1stack.pop()
559 n = context.t1stack.pop()
560 context.psstack.extend([context.t1stack.pop() for i in range(n)][::-1])
562 T1callothersubr = _T1callothersubr()
565 class _T1callsubr(T1cmd):
567 def __init__(self):
568 T1cmd.__init__(self, 10)
570 def __str__(self):
571 return "callsubr"
573 def updatepath(self, path, trafo, context):
574 subr = context.t1stack.pop()
575 context.t1font.updatesubrpath(subr, path, trafo, context)
577 def gathercalls(self, seacglyphs, subrs, context):
578 subr = context.t1stack.pop()
579 subrs.add(subr)
580 context.t1font.gathersubrcalls(subr, seacglyphs, subrs, context)
582 T1callsubr = _T1callsubr()
585 class _T1pop(T1cmd):
587 def __init__(self):
588 T1cmd.__init__(self, 17, subcmd=1)
590 def __str__(self):
591 return "pop"
593 def updatepath(self, path, trafo, context):
594 context.t1stack.append(context.psstack.pop())
596 def gathercalls(self, seacglyphs, subrs, context):
597 context.t1stack.append(context.psstack.pop())
599 T1pop = _T1pop()
602 class _T1return(T1cmd):
604 def __init__(self):
605 T1cmd.__init__(self, 11)
607 def __str__(self):
608 return "return"
610 def updatepath(self, path, trafo, context):
611 pass
613 T1return = _T1return()
616 class _T1setcurrentpoint(T1cmd):
618 def __init__(self):
619 T1cmd.__init__(self, 33, subcmd=1)
621 def __str__(self):
622 return "setcurrentpoint"
624 def updatepath(self, path, trafo, context):
625 context.x = context.t1stack.pop(0)
626 context.y = context.t1stack.pop(0)
628 T1setcurrentpoint = _T1setcurrentpoint()
631 ######################################################################
633 class T1file:
635 eexecr = 55665
636 charstringr = 4330
638 fontnamepattern = re.compile("/FontName\s+/(.*?)\s+def\s+")
639 fontmatrixpattern = re.compile("/FontMatrix\s*\[\s*(-?[0-9.]+)\s+(-?[0-9.]+)\s+(-?[0-9.]+)\s+(-?[0-9.]+)\s+(-?[0-9.]+)\s+(-?[0-9.]+)\s*\]\s*(readonly\s+)?def")
641 def __init__(self, data1, data2eexec, data3):
642 """initializes a t1font instance
644 data1 and data3 are the two clear text data parts and data2 is
645 the binary data part"""
646 self.data1 = data1
647 self._data2eexec = data2eexec
648 self.data3 = data3
650 # marker and value for decoded data
651 self._data2 = None
652 # note that data2eexec is set to none by setsubrcmds and setglyphcmds
653 # this *also* denotes, that data2 is out-of-date; hence they are both
654 # marked by an _ and getdata2 and getdata2eexec will properly resolve
655 # the current state of decoding ...
657 # marker and value for standard encoding check
658 self.encoding = None
660 self.name, = self.fontnamepattern.search(self.data1).groups()
661 m11, m12, m21, m22, v1, v2 = list(map(float, self.fontmatrixpattern.search(self.data1).groups()[:6]))
662 self.fontmatrix = trafo.trafo_pt(matrix=((m11, m12), (m21, m22)), vector=(v1, v2))
664 def _eexecdecode(self, code):
665 """eexec decoding of code"""
666 return decoder(code, self.eexecr, 4)
668 def _charstringdecode(self, code):
669 """charstring decoding of code"""
670 return decoder(code, self.charstringr, self.lenIV)
672 def _eexecencode(self, data):
673 """eexec encoding of data"""
674 return encoder(data, self.eexecr, b"PyX!")
676 def _charstringencode(self, data):
677 """eexec encoding of data"""
678 return encoder(data, self.charstringr, b"PyX!"[:self.lenIV])
680 def _encoding(self):
681 """helper method to lookup the encoding in the font"""
682 c = reader.PStokenizer(self.data1, "/Encoding")
683 token1 = c.gettoken()
684 token2 = c.gettoken()
685 if token1 == "StandardEncoding" and token2 == "def":
686 self.encoding = adobestandardencoding
687 else:
688 self.encoding = [None]*256
689 while True:
690 self.encodingstart = c.pos
691 if c.gettoken() == "dup":
692 break
693 while True:
694 i = c.getint()
695 glyph = c.gettoken()
696 if 0 <= i < 256:
697 self.encoding[i] = glyph[1:]
698 token = c.gettoken(); assert token == "put"
699 self.encodingend = c.pos
700 token = c.gettoken()
701 if token == "readonly" or token == "def":
702 break
703 assert token == "dup"
705 lenIVpattern = re.compile(b"/lenIV\s+(\d+)\s+def\s+")
706 flexhintsubrs = [[3, 0, T1callothersubr, T1pop, T1pop, T1setcurrentpoint, T1return],
707 [0, 1, T1callothersubr, T1return],
708 [0, 2, T1callothersubr, T1return],
709 [T1return]]
711 def _data2decode(self):
712 """decodes data2eexec to the data2 string and the subr and glyphs dictionary
714 It doesn't make sense to call this method twice -- check the content of
715 data2 before calling. The method also keeps the subrs and charstrings
716 start and end positions for later use."""
717 self._data2 = self._eexecdecode(self._data2eexec)
719 m = self.lenIVpattern.search(self._data2)
720 if m:
721 self.lenIV = int(m.group(1))
722 else:
723 self.lenIV = 4
725 self.emptysubr = self._charstringencode(b"\x0b") # 11, i.e. return
727 # extract Subrs
728 c = reader.PSbytes_tokenizer(self._data2, b"/Subrs")
729 self.subrsstart = c.pos
730 arraycount = c.getint()
731 token = c.gettoken(); assert token == b"array"
732 self.subrs = []
733 for i in range(arraycount):
734 token = c.gettoken(); assert token == b"dup"
735 token = c.getint(); assert token == i
736 size = c.getint()
737 if not i:
738 self.subrrdtoken = c.gettoken()
739 else:
740 token = c.gettoken(); assert token == self.subrrdtoken
741 self.subrs.append(c.getbytes(size))
742 token = c.gettoken()
743 if token == b"noaccess":
744 token = token + b" " + c.gettoken()
745 if not i:
746 self.subrnptoken = token
747 else:
748 assert token == self.subrnptoken
749 self.subrsend = c.pos
751 # hasflexhintsubrs is a boolean indicating that the font uses flex or
752 # hint replacement subrs as specified by Adobe (tm). When it does, the
753 # first 4 subrs should all be copied except when none of them are used
754 # in the stripped version of the font since we then get a font not
755 # using flex or hint replacement subrs at all.
756 self.hasflexhintsubrs = (arraycount >= len(self.flexhintsubrs) and
757 [self.getsubrcmds(i)
758 for i in range(len(self.flexhintsubrs))] == self.flexhintsubrs)
760 # extract glyphs
761 self.glyphs = {}
762 self.glyphlist = [] # we want to keep the order of the glyph names
763 c = reader.PSbytes_tokenizer(self._data2, b"/CharStrings")
764 self.charstringsstart = c.pos
765 c.getint()
766 token = c.gettoken(); assert token == b"dict"
767 token = c.gettoken(); assert token == b"dup"
768 token = c.gettoken(); assert token == b"begin"
769 first = True
770 while True:
771 chartoken = c.gettoken().decode("ascii")
772 if chartoken == "end":
773 break
774 assert chartoken[0] == "/"
775 size = c.getint()
776 if first:
777 self.glyphrdtoken = c.gettoken()
778 else:
779 token = c.gettoken(); assert token == self.glyphrdtoken
780 self.glyphlist.append(chartoken[1:])
781 self.glyphs[chartoken[1:]] = c.getbytes(size)
782 if first:
783 self.glyphndtoken = c.gettoken()
784 else:
785 token = c.gettoken(); assert token == self.glyphndtoken
786 first = False
787 self.charstringsend = c.pos
788 assert not self.subrs or self.subrrdtoken == self.glyphrdtoken
790 def _cmds(self, code):
791 """return a list of T1cmd's for encoded charstring data in code"""
792 code = array.array("B", self._charstringdecode(code))
793 cmds = []
794 while code:
795 x = code.pop(0)
796 if x == 12: # this starts an escaped cmd
797 cmds.append(T1subcmds[code.pop(0)])
798 elif 0 <= x < 32: # those are cmd's
799 cmds.append(T1cmds[x])
800 elif 32 <= x <= 246: # short ints
801 cmds.append(x-139)
802 elif 247 <= x <= 250: # mid size ints
803 cmds.append(((x - 247)*256) + code.pop(0) + 108)
804 elif 251 <= x <= 254: # mid size ints
805 cmds.append(-((x - 251)*256) - code.pop(0) - 108)
806 else: # x = 255, i.e. full size ints
807 y = ((code.pop(0)*256+code.pop(0))*256+code.pop(0))*256+code.pop(0)
808 if y > (1 << 31):
809 cmds.append(y - (1 << 32))
810 else:
811 cmds.append(y)
812 return cmds
814 def _code(self, cmds):
815 """return an encoded charstring data for list of T1cmd's in cmds"""
816 code = array.array("B")
817 for cmd in cmds:
818 try:
819 if cmd.subcmd:
820 code.append(12)
821 code.append(cmd.code)
822 except AttributeError:
823 if -107 <= cmd <= 107:
824 code.append(cmd+139)
825 elif 108 <= cmd <= 1131:
826 a, b = divmod(cmd-108, 256)
827 code.append(a+247)
828 code.append(b)
829 elif -1131 <= cmd <= -108:
830 a, b = divmod(-cmd-108, 256)
831 code.append(a+251)
832 code.append(b)
833 else:
834 if cmd < 0:
835 cmd += 1 << 32
836 cmd, x4 = divmod(cmd, 256)
837 cmd, x3 = divmod(cmd, 256)
838 x1, x2 = divmod(cmd, 256)
839 code.append(255)
840 code.append(x1)
841 code.append(x2)
842 code.append(x3)
843 code.append(x4)
844 return self._charstringencode(code.tobytes())
846 def getsubrcmds(self, subr):
847 """return a list of T1cmd's for subr subr"""
848 if not self._data2:
849 self._data2decode()
850 return self._cmds(self.subrs[subr])
852 def getglyphcmds(self, glyph):
853 """return a list of T1cmd's for glyph glyph"""
854 if not self._data2:
855 self._data2decode()
856 return self._cmds(self.glyphs[glyph])
858 def setsubrcmds(self, subr, cmds):
859 """replaces the T1cmd's by the list cmds for subr subr"""
860 if not self._data2:
861 self._data2decode()
862 self._data2eexec = None
863 self.subrs[subr] = self._code(cmds)
865 def setglyphcmds(self, glyph, cmds):
866 """replaces the T1cmd's by the list cmds for glyph glyph"""
867 if not self._data2:
868 self._data2decode()
869 self._data2eexec = None
870 self.glyphs[glyph] = self._code(cmds)
872 def updatepath(self, cmds, path, trafo, context):
873 for cmd in cmds:
874 if isinstance(cmd, T1cmd):
875 cmd.updatepath(path, trafo, context)
876 else:
877 context.t1stack.append(cmd)
879 def updatesubrpath(self, subr, path, trafo, context):
880 self.updatepath(self.getsubrcmds(subr), path, trafo, context)
882 def updateglyphpath(self, glyph, path, trafo, context):
883 self.updatepath(self.getglyphcmds(glyph), path, trafo, context)
885 def gathercalls(self, cmds, seacglyphs, subrs, context):
886 for cmd in cmds:
887 if isinstance(cmd, T1cmd):
888 cmd.gathercalls(seacglyphs, subrs, context)
889 else:
890 context.t1stack.append(cmd)
892 def gathersubrcalls(self, subr, seacglyphs, subrs, context):
893 self.gathercalls(self.getsubrcmds(subr), seacglyphs, subrs, context)
895 def gatherglyphcalls(self, glyph, seacglyphs, subrs, context):
896 self.gathercalls(self.getglyphcmds(glyph), seacglyphs, subrs, context)
898 def getglyphpath_pt(self, x_pt, y_pt, glyph, size_pt, convertcharcode=False, flex=True):
899 """return an object containing the PyX path, wx_pt and wy_pt for glyph named glyph"""
900 if convertcharcode:
901 if not self.encoding:
902 self._encoding()
903 glyph = self.encoding[glyph]
904 t = self.fontmatrix.scaled(size_pt)
905 tpath = t.translated_pt(x_pt, y_pt)
906 context = T1context(self, flex=flex)
907 p = path()
908 self.updateglyphpath(glyph, p, tpath, context)
909 class glyphpath:
910 def __init__(self, p, wx_pt, wy_pt):
911 self.path = p
912 self.wx_pt = wx_pt
913 self.wy_pt = wy_pt
914 return glyphpath(p, *t.apply_pt(context.wx, context.wy))
916 def getdata2(self, subrs=None, glyphs=None):
917 """makes a data2 string
919 subrs is a dict containing those subrs numbers as keys,
920 which are to be contained in the subrsstring to be created.
921 If subrs is None, all subrs in self.subrs will be used.
922 The subrs dict might be modified *in place*.
924 glyphs is a dict containing those glyph names as keys,
925 which are to be contained in the charstringsstring to be created.
926 If glyphs is None, all glyphs in self.glyphs will be used."""
927 w = writer.writer(io.BytesIO())
929 def addsubrs(subrs):
930 if subrs is not None:
931 # some adjustments to the subrs dict
932 if subrs:
933 subrsmin = min(subrs)
934 subrsmax = max(subrs)
935 if self.hasflexhintsubrs and subrsmin < len(self.flexhintsubrs):
936 # According to the spec we need to keep all the flex and hint subrs
937 # as long as any of it is used.
938 for subr in range(len(self.flexhintsubrs)):
939 subrs.add(subr)
940 else:
941 subrsmax = -1
942 else:
943 # build a new subrs dict containing all subrs
944 subrs = dict([(subr, 1) for subr in range(len(self.subrs))])
945 subrsmax = len(self.subrs) - 1
947 # build the string from all selected subrs
948 w.write("%d array\n" % (subrsmax + 1))
949 for subr in range(subrsmax+1):
950 if subr in subrs:
951 code = self.subrs[subr]
952 else:
953 code = self.emptysubr
954 w.write("dup %d %d " % (subr, len(code)))
955 w.write_bytes(self.subrrdtoken)
956 w.write_bytes(b" ")
957 w.write_bytes(code)
958 w.write_bytes(b" ")
959 w.write_bytes(self.subrnptoken)
960 w.write_bytes(b"\n")
962 def addcharstrings(glyphs):
963 w.write("%d dict dup begin\n" % (glyphs is None and len(self.glyphlist) or len(glyphs)))
964 for glyph in self.glyphlist:
965 if glyphs is None or glyph in glyphs:
966 w.write("/%s %d " % (glyph, len(self.glyphs[glyph])))
967 w.write_bytes(self.glyphrdtoken)
968 w.write_bytes(b" ")
969 w.write_bytes(self.glyphs[glyph])
970 w.write_bytes(b" ")
971 w.write_bytes(self.glyphndtoken)
972 w.write_bytes(b"\n")
973 w.write("end\n")
975 if self.subrsstart < self.charstringsstart:
976 w.write_bytes(self._data2[:self.subrsstart])
977 addsubrs(subrs)
978 w.write_bytes(self._data2[self.subrsend:self.charstringsstart])
979 addcharstrings(glyphs)
980 w.write_bytes(self._data2[self.charstringsend:])
981 else:
982 w.write_bytes(self._data2[:self.charstringsstart])
983 addcharstrings(glyphs)
984 w.write_bytes(self._data2[self.charstringsend:self.subrsstart])
985 addsubrs(subrs)
986 w.write_bytes(self._data2[self.subrsend:])
987 return w.file.getvalue()
989 def getdata2eexec(self):
990 if self._data2eexec:
991 return self._data2eexec
992 # note that self._data2 is out-of-date here too, hence we need to call getdata2
993 return self._eexecencode(self.getdata2())
995 newlinepattern = re.compile("\s*[\r\n]\s*")
996 uniqueidstrpattern = re.compile("%?/UniqueID\s+\d+\s+def\s+")
997 uniqueidbytespattern = re.compile(b"%?/UniqueID\s+\d+\s+def\s+")
998 # when UniqueID is commented out (as in modern latin), prepare to remove the comment character as well
1000 def getstrippedfont(self, glyphs, charcodes):
1001 """create a T1file instance containing only certain glyphs
1003 glyphs is a set of the glyph names. It might be modified *in place*!
1005 if not self.encoding:
1006 self._encoding()
1007 for charcode in charcodes:
1008 glyphs.add(self.encoding[charcode])
1010 # collect information about used glyphs and subrs
1011 seacglyphs = set()
1012 subrs = set()
1013 for glyph in glyphs:
1014 self.gatherglyphcalls(glyph, seacglyphs, subrs, T1context(self))
1015 # while we have gathered all subrs for the seacglyphs alreadys, we
1016 # might have missed the glyphs themself (when they are not used stand-alone)
1017 glyphs.update(seacglyphs)
1018 glyphs.add(".notdef")
1020 # strip data1
1021 if self.encoding is adobestandardencoding:
1022 data1 = self.data1
1023 else:
1024 encodingstrings = []
1025 for char, glyph in enumerate(self.encoding):
1026 if glyph in glyphs:
1027 encodingstrings.append("dup %i /%s put\n" % (char, glyph))
1028 data1 = self.data1[:self.encodingstart] + "\n" + "".join(encodingstrings) + self.data1[self.encodingend:]
1029 data1 = self.newlinepattern.subn("\n", data1)[0]
1030 data1 = self.uniqueidstrpattern.subn("", data1)[0]
1032 # strip data2
1033 data2 = self.uniqueidbytespattern.subn(b"", self.getdata2(subrs, glyphs))[0]
1035 # strip data3
1036 data3 = self.newlinepattern.subn("\n", self.data3)[0]
1038 # create and return the new font instance
1039 return T1file(data1.rstrip() + "\n", self._eexecencode(data2), data3.rstrip() + "\n")
1041 # The following two methods, writePDFfontinfo and getglyphinfo,
1042 # extract informtion which should better be taken from the afm file.
1043 def writePDFfontinfo(self, file):
1044 try:
1045 glyphinfo_y = self.getglyphinfo("y")
1046 glyphinfo_W = self.getglyphinfo("W")
1047 glyphinfo_H = self.getglyphinfo("H")
1048 glyphinfo_h = self.getglyphinfo("h")
1049 glyphinfo_period = self.getglyphinfo("period")
1050 glyphinfo_colon = self.getglyphinfo("colon")
1051 except:
1052 warnings.warn("Auto-guessing of font information for font '%s' failed. We're writing stub data instead." % self.name)
1053 file.write("/Flags 4\n")
1054 file.write("/FontBBox [0 -100 1000 1000]\n")
1055 file.write("/ItalicAngle 0\n")
1056 file.write("/Ascent 1000\n")
1057 file.write("/Descent -100\n")
1058 file.write("/CapHeight 700\n")
1059 file.write("/StemV 100\n")
1060 else:
1061 if not self.encoding:
1062 self._encoding()
1063 # As a simple heuristics we assume non-symbolic fonts if and only
1064 # if the Adobe standard encoding is used. All other font flags are
1065 # not specified here.
1066 if self.encoding is adobestandardencoding:
1067 file.write("/Flags 32\n")
1068 else:
1069 file.write("/Flags 4\n")
1070 file.write("/FontBBox [0 %f %f %f]\n" % (glyphinfo_y[3], glyphinfo_W[0], glyphinfo_H[5]))
1071 file.write("/ItalicAngle %f\n" % math.degrees(math.atan2(glyphinfo_period[4]-glyphinfo_colon[4], glyphinfo_colon[5]-glyphinfo_period[5])))
1072 file.write("/Ascent %f\n" % glyphinfo_H[5])
1073 file.write("/Descent %f\n" % glyphinfo_y[3])
1074 file.write("/CapHeight %f\n" % glyphinfo_h[5])
1075 file.write("/StemV %f\n" % (glyphinfo_period[4]-glyphinfo_period[2]))
1077 def getglyphinfo(self, glyph, flex=True):
1078 warnings.warn("We are about to extract font information for the Type 1 font '%s' from its pfb file. This is bad practice (and it's slow). You should use an afm file instead." % self.name)
1079 context = T1context(self, flex=flex)
1080 p = path()
1081 self.updateglyphpath(glyph, p, trafo.trafo(), context)
1082 bbox = p.bbox()
1083 return context.wx, context.wy, bbox.llx_pt, bbox.lly_pt, bbox.urx_pt, bbox.ury_pt
1085 def outputPFA(self, file, remove_UniqueID_lookup=False):
1086 """output the T1file in PFA format"""
1087 data1 = self.data1
1088 data3 = self.data3
1089 if remove_UniqueID_lookup:
1090 m1 = re.search("""FontDirectory\s*/%(name)s\s+known{/%(name)s\s+findfont\s+dup\s*/UniqueID\s+known\s*{\s*dup\s*
1091 /UniqueID\s+get\s+\d+\s+eq\s+exch\s*/FontType\s+get\s+1\s+eq\s+and\s*}\s*{\s*pop\s+false\s*}\s*ifelse\s*
1092 {save\s+true\s*}\s*{\s*false\s*}\s*ifelse\s*}\s*{\s*false\s*}\s*ifelse""" % {"name": self.name},
1093 data1, re.VERBOSE)
1094 m3 = re.search("\s*{restore}\s*if", data3)
1095 if m1 and m3:
1096 data1 = data1[:m1.start()] + data1[m1.end():]
1097 data3 = data3[:m3.start()] + data3[m3.end():]
1098 file.write(data1)
1099 data2eexechex = binascii.b2a_hex(self.getdata2eexec())
1100 linelength = 64
1101 for i in range((len(data2eexechex)-1)//linelength + 1):
1102 file.write_bytes(data2eexechex[i*linelength: i*linelength+linelength])
1103 file.write("\n")
1104 file.write(data3)
1106 def outputPFB(self, file):
1107 """output the T1file in PFB format"""
1108 data2eexec = self.getdata2eexec()
1109 def pfblength(data):
1110 l = len(data)
1111 l, x1 = divmod(l, 256)
1112 l, x2 = divmod(l, 256)
1113 x4, x3 = divmod(l, 256)
1114 return chr(x1) + chr(x2) + chr(x3) + chr(x4)
1115 file.write("\200\1")
1116 file.write(pfblength(self.data1))
1117 file.write(self.data1)
1118 file.write("\200\2")
1119 file.write(pfblength(data2eexec))
1120 file.write(data2eexec)
1121 file.write("\200\1")
1122 file.write(pfblength(self.data3))
1123 file.write(self.data3)
1124 file.write("\200\3")
1126 def outputPS(self, file, writer):
1127 """output the PostScript code for the T1file to the file file"""
1128 self.outputPFA(file, remove_UniqueID_lookup=True)
1130 def outputPDF(self, file, writer):
1131 data2eexec = self.getdata2eexec()
1132 data3 = self.data3
1133 # we might be allowed to skip the third part ...
1134 if (data3.replace("\n", "")
1135 .replace("\r", "")
1136 .replace("\t", "")
1137 .replace(" ", "")) == "0"*512 + "cleartomark":
1138 data3 = ""
1140 data = self.data1.encode("ascii", errors="surrogateescape") + data2eexec + data3.encode("ascii", errors="surrogateescape")
1141 if writer.compress and haszlib:
1142 data = zlib.compress(data)
1144 file.write("<<\n"
1145 "/Length %d\n"
1146 "/Length1 %d\n"
1147 "/Length2 %d\n"
1148 "/Length3 %d\n" % (len(data), len(self.data1), len(data2eexec), len(data3)))
1149 if writer.compress and haszlib:
1150 file.write("/Filter /FlateDecode\n")
1151 file.write(">>\n"
1152 "stream\n")
1153 file.write_bytes(data)
1154 file.write("\n"
1155 "endstream\n")
1157 # factory functions
1159 class FontFormatError(Exception):
1160 pass
1162 def from_PFA_bytes(bytes):
1163 """create a T1file instance from a string of bytes corresponding to a PFA file"""
1164 try:
1165 m1 = bytes.index("eexec") + 6
1166 m2 = bytes.index("0"*40)
1167 except ValueError:
1168 raise FontFormatError
1170 data1 = bytes[:m1].decode("ascii", errors="surrogateescape")
1171 data2eexec = binascii.a2b_hex(bytes[m1: m2].replace(" ", "").replace("\r", "").replace("\n", ""))
1172 data3 = bytes[m2:].decode("ascii", errors="surrogateescape")
1173 return T1file(data1, data2eexec, data3)
1175 def from_PFA_filename(filename):
1176 """create a T1file instance from PFA font file of given name"""
1177 with open(filename, "rb") as file:
1178 t1file = from_PFA_bytes(file.read())
1179 return t1file
1181 def from_PFB_bytes(bytes):
1182 """create a T1file instance from a string of bytes corresponding to a PFB file"""
1184 def pfblength(s):
1185 if len(s) != 4:
1186 raise ValueError("invalid string length")
1187 return (s[0] +
1188 s[1]*256 +
1189 s[2]*256*256 +
1190 s[3]*256*256*256)
1191 class consumer:
1192 def __init__(self, bytes):
1193 self.bytes = bytes
1194 self.pos = 0
1195 def __call__(self, n):
1196 result = self.bytes[self.pos:self.pos+n]
1197 self.pos += n
1198 return result
1200 consume = consumer(bytes)
1201 mark = consume(2)
1202 if mark != b"\200\1":
1203 raise FontFormatError
1204 data1 = consume(pfblength(consume(4))).decode("ascii", errors="surrogateescape")
1205 mark = consume(2)
1206 if mark != b"\200\2":
1207 raise FontFormatError
1208 data2eexec = b""
1209 while mark == b"\200\2":
1210 data2eexec = data2eexec + consume(pfblength(consume(4)))
1211 mark = consume(2)
1212 if mark != b"\200\1":
1213 raise FontFormatError
1214 data3 = consume(pfblength(consume(4))).decode("ascii", errors="surrogateescape")
1215 mark = consume(2)
1216 if mark != b"\200\3":
1217 raise FontFormatError
1218 if consume(1):
1219 raise FontFormatError
1221 return T1file(data1, data2eexec, data3)
1223 def from_PFB_filename(filename):
1224 """create a T1file instance from PFB font file of given name"""
1225 with open(filename, "rb") as file:
1226 t1file = from_PFB_bytes(file.read())
1227 return t1file
1229 def from_PF_bytes(bytes):
1230 #try:
1231 return from_PFB_bytes(bytes)
1232 #except FontFormatError:
1233 # return from_PFA_bytes(bytes)
1235 def from_PF_filename(filename):
1236 """create a T1file instance from PFA or PFB font file of given name"""
1237 with open(filename, "rb") as file:
1238 t1file = from_PF_bytes(file.read())
1239 return t1file