add UnicodeEngine (MultiEngineText and axis texters returning MultiEngineText), texte...
[PyX.git] / pyx / font / t1file.py
blob4bd5e4e66b1d31a5d3f5b16989a4548089609583
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, logging, math, re
24 try:
25 import zlib
26 haszlib = True
27 except ImportError:
28 haszlib = False
30 logger = logging.getLogger("pyx")
32 from pyx import trafo, reader, writer
33 from pyx.path import path, moveto_pt, lineto_pt, curveto_pt, closepath
35 try:
36 from ._t1code import *
37 except:
38 from .t1code import *
41 adobestandardencoding = [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 None, None, None, None, None, None, None, None,
45 "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quoteright",
46 "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash",
47 "zero", "one", "two", "three", "four", "five", "six", "seven",
48 "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question",
49 "at", "A", "B", "C", "D", "E", "F", "G",
50 "H", "I", "J", "K", "L", "M", "N", "O",
51 "P", "Q", "R", "S", "T", "U", "V", "W",
52 "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore",
53 "quoteleft", "a", "b", "c", "d", "e", "f", "g",
54 "h", "i", "j", "k", "l", "m", "n", "o",
55 "p", "q", "r", "s", "t", "u", "v", "w",
56 "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", 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, None, None, None, None, None, None, None,
61 None, "exclamdown", "cent", "sterling", "fraction", "yen", "florin", "section",
62 "currency", "quotesingle", "quotedblleft", "guillemotleft", "guilsinglleft", "guilsinglright", "fi", "fl",
63 None, "endash", "dagger", "daggerdbl", "periodcentered", None, "paragraph", "bullet",
64 "quotesinglbase", "quotedblbase", "quotedblright", "guillemotright", "ellipsis", "perthousand", None, "questiondown",
65 None, "grave", "acute", "circumflex", "tilde", "macron", "breve", "dotaccent",
66 "dieresis", None, "ring", "cedilla", None, "hungarumlaut", "ogonek", "caron",
67 "emdash", None, None, None, None, None, None, None,
68 None, None, None, None, None, None, None, None,
69 None, "AE", None, "ordfeminine", None, None, None, None,
70 "Lslash", "Oslash", "OE", "ordmasculine", None, None, None, None,
71 None, "ae", None, None, None, "dotlessi", None, None,
72 "lslash", "oslash", "oe", "germandbls", None, None, None, None]
74 class T1context:
76 def __init__(self, t1font, flex=True):
77 """context for T1cmd evaluation"""
78 self.t1font = t1font
80 # state description
81 self.x = None
82 self.y = None
83 self.wx = None
84 self.wy = None
85 self.t1stack = []
86 self.psstack = []
87 self.flex = flex
90 ######################################################################
91 # T1 commands
92 # Note, that the T1 commands are variable-free except for plain number,
93 # which are stored as integers. All other T1 commands exist as a single
94 # instance only
96 T1cmds = {}
97 T1subcmds = {}
99 class T1cmd:
101 def __init__(self, code, subcmd=0):
102 self.code = code
103 self.subcmd = subcmd
104 if subcmd:
105 T1subcmds[code] = self
106 else:
107 T1cmds[code] = self
109 def __str__(self):
110 """returns a string representation of the T1 command"""
111 raise NotImplementedError
113 def updatepath(self, path, trafo, context):
114 """update path instance applying trafo to the points"""
115 raise NotImplementedError
117 def gathercalls(self, seacglyphs, subrs, context):
118 """gather dependancy information
120 subrs is the "called-subrs" dictionary. gathercalls will insert the
121 subr number as key having the value 1, i.e. subrs will become the
122 numbers of used subrs. Similar seacglyphs will contain all glyphs in
123 composite characters (subrs for those glyphs will also
124 already be included).
126 This method might will not properly update all information in the
127 context (especially consuming values from the stack) and will also skip
128 various tests for performance reasons. For most T1 commands it just
129 doesn't need to do anything.
131 pass
134 # commands for starting and finishing
136 class _T1endchar(T1cmd):
138 def __init__(self):
139 T1cmd.__init__(self, 14)
141 def __str__(self):
142 return "endchar"
144 def updatepath(self, path, trafo, context):
145 pass
147 T1endchar = _T1endchar()
150 class _T1hsbw(T1cmd):
152 def __init__(self):
153 T1cmd.__init__(self, 13)
155 def __str__(self):
156 return "hsbw"
158 def updatepath(self, path, trafo, context):
159 sbx = context.t1stack.pop(0)
160 wx = context.t1stack.pop(0)
161 path.append(moveto_pt(*trafo.apply_pt(sbx, 0)))
162 context.x = sbx
163 context.y = 0
164 context.wx = wx
165 context.wy = 0
167 T1hsbw = _T1hsbw()
170 class _T1seac(T1cmd):
172 def __init__(self):
173 T1cmd.__init__(self, 6, subcmd=1)
175 def __str__(self):
176 return "seac"
178 def updatepath(self, path, atrafo, context):
179 sab = context.t1stack.pop(0)
180 adx = context.t1stack.pop(0)
181 ady = context.t1stack.pop(0)
182 bchar = context.t1stack.pop(0)
183 achar = context.t1stack.pop(0)
184 aglyph = adobestandardencoding[achar]
185 bglyph = adobestandardencoding[bchar]
186 context.t1font.updateglyphpath(bglyph, path, atrafo, context)
187 atrafo = atrafo * trafo.translate_pt(adx-sab, ady)
188 context.t1font.updateglyphpath(aglyph, path, atrafo, context)
190 def gathercalls(self, seacglyphs, subrs, context):
191 achar = context.t1stack.pop()
192 bchar = context.t1stack.pop()
193 aglyph = adobestandardencoding[achar]
194 bglyph = adobestandardencoding[bchar]
195 seacglyphs.add(aglyph)
196 seacglyphs.add(bglyph)
197 context.t1font.gatherglyphcalls(bglyph, seacglyphs, subrs, context)
198 context.t1font.gatherglyphcalls(aglyph, seacglyphs, subrs, context)
200 T1seac = _T1seac()
203 class _T1sbw(T1cmd):
205 def __init__(self):
206 T1cmd.__init__(self, 7, subcmd=1)
208 def __str__(self):
209 return "sbw"
211 def updatepath(self, path, trafo, context):
212 sbx = context.t1stack.pop(0)
213 sby = context.t1stack.pop(0)
214 wx = context.t1stack.pop(0)
215 wy = context.t1stack.pop(0)
216 path.append(moveto_pt(*trafo.apply_pt(sbx, sby)))
217 context.x = sbx
218 context.y = sby
219 context.wx = wx
220 context.wy = wy
222 T1sbw = _T1sbw()
225 # path construction commands
227 class _T1closepath(T1cmd):
229 def __init__(self):
230 T1cmd.__init__(self, 9)
232 def __str__(self):
233 return "closepath"
235 def updatepath(self, path, trafo, context):
236 path.append(closepath())
237 # The closepath in T1 is different from PostScripts in that it does
238 # *not* modify the current position; hence we need to add an additional
239 # moveto here ...
240 path.append(moveto_pt(*trafo.apply_pt(context.x, context.y)))
242 T1closepath = _T1closepath()
245 class _T1hlineto(T1cmd):
247 def __init__(self):
248 T1cmd.__init__(self, 6)
250 def __str__(self):
251 return "hlineto"
253 def updatepath(self, path, trafo, context):
254 dx = context.t1stack.pop(0)
255 path.append(lineto_pt(*trafo.apply_pt(context.x + dx, context.y)))
256 context.x += dx
258 T1hlineto = _T1hlineto()
261 class _T1hmoveto(T1cmd):
263 def __init__(self):
264 T1cmd.__init__(self, 22)
266 def __str__(self):
267 return "hmoveto"
269 def updatepath(self, path, trafo, context):
270 dx = context.t1stack.pop(0)
271 path.append(moveto_pt(*trafo.apply_pt(context.x + dx, context.y)))
272 context.x += dx
274 T1hmoveto = _T1hmoveto()
277 class _T1hvcurveto(T1cmd):
279 def __init__(self):
280 T1cmd.__init__(self, 31)
282 def __str__(self):
283 return "hvcurveto"
285 def updatepath(self, path, trafo, context):
286 dx1 = context.t1stack.pop(0)
287 dx2 = context.t1stack.pop(0)
288 dy2 = context.t1stack.pop(0)
289 dy3 = context.t1stack.pop(0)
290 path.append(curveto_pt(*(trafo.apply_pt(context.x + dx1, context.y) +
291 trafo.apply_pt(context.x + dx1 + dx2, context.y + dy2) +
292 trafo.apply_pt(context.x + dx1 + dx2, context.y + dy2 + dy3))))
293 context.x += dx1+dx2
294 context.y += dy2+dy3
296 T1hvcurveto = _T1hvcurveto()
299 class _T1rlineto(T1cmd):
301 def __init__(self):
302 T1cmd.__init__(self, 5)
304 def __str__(self):
305 return "rlineto"
307 def updatepath(self, path, trafo, context):
308 dx = context.t1stack.pop(0)
309 dy = context.t1stack.pop(0)
310 path.append(lineto_pt(*trafo.apply_pt(context.x + dx, context.y + dy)))
311 context.x += dx
312 context.y += dy
314 T1rlineto = _T1rlineto()
317 class _T1rmoveto(T1cmd):
319 def __init__(self):
320 T1cmd.__init__(self, 21)
322 def __str__(self):
323 return "rmoveto"
325 def updatepath(self, path, trafo, context):
326 dx = context.t1stack.pop(0)
327 dy = context.t1stack.pop(0)
328 path.append(moveto_pt(*trafo.apply_pt(context.x + dx, context.y + dy)))
329 context.x += dx
330 context.y += dy
332 T1rmoveto = _T1rmoveto()
335 class _T1rrcurveto(T1cmd):
337 def __init__(self):
338 T1cmd.__init__(self, 8)
340 def __str__(self):
341 return "rrcurveto"
343 def updatepath(self, path, trafo, context):
344 dx1 = context.t1stack.pop(0)
345 dy1 = context.t1stack.pop(0)
346 dx2 = context.t1stack.pop(0)
347 dy2 = context.t1stack.pop(0)
348 dx3 = context.t1stack.pop(0)
349 dy3 = context.t1stack.pop(0)
350 path.append(curveto_pt(*(trafo.apply_pt(context.x + dx1, context.y + dy1) +
351 trafo.apply_pt(context.x + dx1 + dx2, context.y + dy1 + dy2) +
352 trafo.apply_pt(context.x + dx1 + dx2 + dx3, context.y + dy1 + dy2 + dy3))))
353 context.x += dx1+dx2+dx3
354 context.y += dy1+dy2+dy3
356 T1rrcurveto = _T1rrcurveto()
359 class _T1vlineto(T1cmd):
361 def __init__(self):
362 T1cmd.__init__(self, 7)
364 def __str__(self):
365 return "vlineto"
367 def updatepath(self, path, trafo, context):
368 dy = context.t1stack.pop(0)
369 path.append(lineto_pt(*trafo.apply_pt(context.x, context.y + dy)))
370 context.y += dy
372 T1vlineto = _T1vlineto()
375 class _T1vmoveto(T1cmd):
377 def __init__(self):
378 T1cmd.__init__(self, 4)
380 def __str__(self):
381 return "vmoveto"
383 def updatepath(self, path, trafo, context):
384 dy = context.t1stack.pop(0)
385 path.append(moveto_pt(*trafo.apply_pt(context.x, context.y + dy)))
386 context.y += dy
388 T1vmoveto = _T1vmoveto()
391 class _T1vhcurveto(T1cmd):
393 def __init__(self):
394 T1cmd.__init__(self, 30)
396 def __str__(self):
397 return "vhcurveto"
399 def updatepath(self, path, trafo, context):
400 dy1 = context.t1stack.pop(0)
401 dx2 = context.t1stack.pop(0)
402 dy2 = context.t1stack.pop(0)
403 dx3 = context.t1stack.pop(0)
404 path.append(curveto_pt(*(trafo.apply_pt(context.x, context.y + dy1) +
405 trafo.apply_pt(context.x + dx2, context.y + dy1 + dy2) +
406 trafo.apply_pt(context.x + dx2 + dx3, context.y + dy1 + dy2))))
407 context.x += dx2+dx3
408 context.y += dy1+dy2
410 T1vhcurveto = _T1vhcurveto()
413 # hint commands
415 class _T1dotsection(T1cmd):
417 def __init__(self):
418 T1cmd.__init__(self, 0, subcmd=1)
420 def __str__(self):
421 return "dotsection"
423 def updatepath(self, path, trafo, context):
424 pass
426 T1dotsection = _T1dotsection()
429 class _T1hstem(T1cmd):
431 def __init__(self):
432 T1cmd.__init__(self, 1)
434 def __str__(self):
435 return "hstem"
437 def updatepath(self, path, trafo, context):
438 y = context.t1stack.pop(0)
439 dy = context.t1stack.pop(0)
441 T1hstem = _T1hstem()
444 class _T1hstem3(T1cmd):
446 def __init__(self):
447 T1cmd.__init__(self, 2, subcmd=1)
449 def __str__(self):
450 return "hstem3"
452 def updatepath(self, path, trafo, context):
453 y0 = context.t1stack.pop(0)
454 dy0 = context.t1stack.pop(0)
455 y1 = context.t1stack.pop(0)
456 dy1 = context.t1stack.pop(0)
457 y2 = context.t1stack.pop(0)
458 dy2 = context.t1stack.pop(0)
460 T1hstem3 = _T1hstem3()
463 class _T1vstem(T1cmd):
465 def __init__(self):
466 T1cmd.__init__(self, 3)
468 def __str__(self):
469 return "vstem"
471 def updatepath(self, path, trafo, context):
472 x = context.t1stack.pop(0)
473 dx = context.t1stack.pop(0)
475 T1vstem = _T1vstem()
478 class _T1vstem3(T1cmd):
480 def __init__(self):
481 T1cmd.__init__(self, 1, subcmd=1)
483 def __str__(self):
484 return "vstem3"
486 def updatepath(self, path, trafo, context):
487 self.x0 = context.t1stack.pop(0)
488 self.dx0 = context.t1stack.pop(0)
489 self.x1 = context.t1stack.pop(0)
490 self.dx1 = context.t1stack.pop(0)
491 self.x2 = context.t1stack.pop(0)
492 self.dx2 = context.t1stack.pop(0)
494 T1vstem3 = _T1vstem3()
497 # arithmetic command
499 class _T1div(T1cmd):
501 def __init__(self):
502 T1cmd.__init__(self, 12, subcmd=1)
504 def __str__(self):
505 return "div"
507 def updatepath(self, path, trafo, context):
508 num2 = context.t1stack.pop()
509 num1 = context.t1stack.pop()
510 context.t1stack.append(divmod(num1, num2)[0])
512 def gathercalls(self, seacglyphs, subrs, context):
513 num2 = context.t1stack.pop()
514 num1 = context.t1stack.pop()
515 context.t1stack.append(divmod(num1, num2)[0])
517 T1div = _T1div()
520 # subroutine commands
522 class _T1callothersubr(T1cmd):
524 def __init__(self):
525 T1cmd.__init__(self, 16, subcmd=1)
527 def __str__(self):
528 return "callothersubr"
530 def updatepath(self, path, trafo, context):
531 othersubrnumber = context.t1stack.pop()
532 n = context.t1stack.pop()
533 for i in range(n):
534 context.psstack.append(context.t1stack.pop(0))
535 if othersubrnumber == 0:
536 flex_size, x, y = context.psstack[-3:]
537 if context.flex:
538 x1, y1, x2, y2, x3, y3 = context.psstack[2:8]
539 x1, y1 = trafo.apply_pt(x1, y1)
540 x2, y2 = trafo.apply_pt(x2, y2)
541 x3, y3 = trafo.apply_pt(x3, y3)
542 path.append(curveto_pt(x1, y1, x2, y2, x3, y3))
543 x1, y1, x2, y2, x3, y3 = context.psstack[8:14]
544 x1, y1 = trafo.apply_pt(x1, y1)
545 x2, y2 = trafo.apply_pt(x2, y2)
546 x3, y3 = trafo.apply_pt(x3, y3)
547 path.append(curveto_pt(x1, y1, x2, y2, x3, y3))
548 else:
549 path.append(lineto_pt(*trafo.apply_pt(x, y)))
550 context.psstack = [y, x]
551 elif othersubrnumber == 1:
552 pass
553 elif othersubrnumber == 2:
554 path.pathitems.pop()
555 context.psstack.append(context.x)
556 context.psstack.append(context.y)
558 def gathercalls(self, seacglyphs, subrs, context):
559 othersubrnumber = context.t1stack.pop()
560 n = context.t1stack.pop()
561 context.psstack.extend([context.t1stack.pop() for i in range(n)][::-1])
563 T1callothersubr = _T1callothersubr()
566 class _T1callsubr(T1cmd):
568 def __init__(self):
569 T1cmd.__init__(self, 10)
571 def __str__(self):
572 return "callsubr"
574 def updatepath(self, path, trafo, context):
575 subr = context.t1stack.pop()
576 context.t1font.updatesubrpath(subr, path, trafo, context)
578 def gathercalls(self, seacglyphs, subrs, context):
579 subr = context.t1stack.pop()
580 subrs.add(subr)
581 context.t1font.gathersubrcalls(subr, seacglyphs, subrs, context)
583 T1callsubr = _T1callsubr()
586 class _T1pop(T1cmd):
588 def __init__(self):
589 T1cmd.__init__(self, 17, subcmd=1)
591 def __str__(self):
592 return "pop"
594 def updatepath(self, path, trafo, context):
595 context.t1stack.append(context.psstack.pop())
597 def gathercalls(self, seacglyphs, subrs, context):
598 context.t1stack.append(context.psstack.pop())
600 T1pop = _T1pop()
603 class _T1return(T1cmd):
605 def __init__(self):
606 T1cmd.__init__(self, 11)
608 def __str__(self):
609 return "return"
611 def updatepath(self, path, trafo, context):
612 pass
614 T1return = _T1return()
617 class _T1setcurrentpoint(T1cmd):
619 def __init__(self):
620 T1cmd.__init__(self, 33, subcmd=1)
622 def __str__(self):
623 return "setcurrentpoint"
625 def updatepath(self, path, trafo, context):
626 context.x = context.t1stack.pop(0)
627 context.y = context.t1stack.pop(0)
629 T1setcurrentpoint = _T1setcurrentpoint()
632 ######################################################################
634 class FontFormatError(Exception):
635 pass
637 class T1File:
639 eexecr = 55665
640 charstringr = 4330
642 fontnamepattern = re.compile("/FontName\s+/(.*?)\s+def\s+")
643 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")
645 def __init__(self, data1, data2eexec, data3):
646 """initializes a t1font instance
648 data1 and data3 are the two clear text data parts and data2 is
649 the binary data part"""
650 self.data1 = data1
651 self._data2eexec = data2eexec
652 self.data3 = data3
654 # marker and value for decoded data
655 self._data2 = None
656 # note that data2eexec is set to none by setsubrcmds and setglyphcmds
657 # this *also* denotes, that data2 is out-of-date; hence they are both
658 # marked by an _ and getdata2 and getdata2eexec will properly resolve
659 # the current state of decoding ...
661 # marker and value for standard encoding check
662 self.encoding = None
664 self.name, = self.fontnamepattern.search(self.data1).groups()
665 m11, m12, m21, m22, v1, v2 = list(map(float, self.fontmatrixpattern.search(self.data1).groups()[:6]))
666 self.fontmatrix = trafo.trafo_pt(matrix=((m11, m12), (m21, m22)), vector=(v1, v2))
668 def _eexecdecode(self, code):
669 """eexec decoding of code"""
670 return decoder(code, self.eexecr, 4)
672 def _charstringdecode(self, code):
673 """charstring decoding of code"""
674 return decoder(code, self.charstringr, self.lenIV)
676 def _eexecencode(self, data):
677 """eexec encoding of data"""
678 return encoder(data, self.eexecr, b"PyX!")
680 def _charstringencode(self, data):
681 """eexec encoding of data"""
682 return encoder(data, self.charstringr, b"PyX!"[:self.lenIV])
684 def _encoding(self):
685 """helper method to lookup the encoding in the font"""
686 c = reader.PStokenizer(self.data1, "/Encoding")
687 token1 = c.gettoken()
688 token2 = c.gettoken()
689 if token1 == "StandardEncoding" and token2 == "def":
690 self.encoding = adobestandardencoding
691 else:
692 self.encoding = [None]*256
693 while True:
694 self.encodingstart = c.pos
695 if c.gettoken() == "dup":
696 break
697 while True:
698 i = c.getint()
699 glyph = c.gettoken()
700 if 0 <= i < 256:
701 self.encoding[i] = glyph[1:]
702 token = c.gettoken(); assert token == "put"
703 self.encodingend = c.pos
704 token = c.gettoken()
705 if token == "readonly" or token == "def":
706 break
707 assert token == "dup"
709 lenIVpattern = re.compile(b"/lenIV\s+(\d+)\s+def\s+")
710 flexhintsubrs = [[3, 0, T1callothersubr, T1pop, T1pop, T1setcurrentpoint, T1return],
711 [0, 1, T1callothersubr, T1return],
712 [0, 2, T1callothersubr, T1return],
713 [T1return]]
715 def _data2decode(self):
716 """decodes data2eexec to the data2 string and the subr and glyphs dictionary
718 It doesn't make sense to call this method twice -- check the content of
719 data2 before calling. The method also keeps the subrs and charstrings
720 start and end positions for later use."""
721 self._data2 = self._eexecdecode(self._data2eexec)
723 m = self.lenIVpattern.search(self._data2)
724 if m:
725 self.lenIV = int(m.group(1))
726 else:
727 self.lenIV = 4
729 self.emptysubr = self._charstringencode(b"\x0b") # 11, i.e. return
731 # extract Subrs
732 c = reader.PSbytes_tokenizer(self._data2, b"/Subrs")
733 self.subrsstart = c.pos
734 arraycount = c.getint()
735 token = c.gettoken(); assert token == b"array"
736 self.subrs = []
737 for i in range(arraycount):
738 token = c.gettoken(); assert token == b"dup"
739 token = c.getint(); assert token == i
740 size = c.getint()
741 if not i:
742 self.subrrdtoken = c.gettoken()
743 else:
744 token = c.gettoken(); assert token == self.subrrdtoken
745 self.subrs.append(c.getbytes(size))
746 token = c.gettoken()
747 if token == b"noaccess":
748 token = token + b" " + c.gettoken()
749 if not i:
750 self.subrnptoken = token
751 else:
752 assert token == self.subrnptoken
753 self.subrsend = c.pos
755 # hasflexhintsubrs is a boolean indicating that the font uses flex or
756 # hint replacement subrs as specified by Adobe (tm). When it does, the
757 # first 4 subrs should all be copied except when none of them are used
758 # in the stripped version of the font since we then get a font not
759 # using flex or hint replacement subrs at all.
760 self.hasflexhintsubrs = (arraycount >= len(self.flexhintsubrs) and
761 [self.getsubrcmds(i)
762 for i in range(len(self.flexhintsubrs))] == self.flexhintsubrs)
764 # extract glyphs
765 self.glyphs = {}
766 self.glyphlist = [] # we want to keep the order of the glyph names
767 c = reader.PSbytes_tokenizer(self._data2, b"/CharStrings")
768 self.charstringsstart = c.pos
769 c.getint()
770 token = c.gettoken(); assert token == b"dict"
771 token = c.gettoken(); assert token == b"dup"
772 token = c.gettoken(); assert token == b"begin"
773 first = True
774 while True:
775 chartoken = c.gettoken().decode("ascii")
776 if chartoken == "end":
777 break
778 assert chartoken[0] == "/"
779 size = c.getint()
780 if first:
781 self.glyphrdtoken = c.gettoken()
782 else:
783 token = c.gettoken(); assert token == self.glyphrdtoken
784 self.glyphlist.append(chartoken[1:])
785 self.glyphs[chartoken[1:]] = c.getbytes(size)
786 if first:
787 self.glyphndtoken = c.gettoken()
788 else:
789 token = c.gettoken(); assert token == self.glyphndtoken
790 first = False
791 self.charstringsend = c.pos
792 assert not self.subrs or self.subrrdtoken == self.glyphrdtoken
794 def _cmds(self, code):
795 """return a list of T1cmd's for encoded charstring data in code"""
796 code = array.array("B", self._charstringdecode(code))
797 cmds = []
798 while code:
799 x = code.pop(0)
800 if x == 12: # this starts an escaped cmd
801 cmds.append(T1subcmds[code.pop(0)])
802 elif 0 <= x < 32: # those are cmd's
803 cmds.append(T1cmds[x])
804 elif 32 <= x <= 246: # short ints
805 cmds.append(x-139)
806 elif 247 <= x <= 250: # mid size ints
807 cmds.append(((x - 247)*256) + code.pop(0) + 108)
808 elif 251 <= x <= 254: # mid size ints
809 cmds.append(-((x - 251)*256) - code.pop(0) - 108)
810 else: # x = 255, i.e. full size ints
811 y = ((code.pop(0)*256+code.pop(0))*256+code.pop(0))*256+code.pop(0)
812 if y > (1 << 31):
813 cmds.append(y - (1 << 32))
814 else:
815 cmds.append(y)
816 return cmds
818 def _code(self, cmds):
819 """return an encoded charstring data for list of T1cmd's in cmds"""
820 code = array.array("B")
821 for cmd in cmds:
822 try:
823 if cmd.subcmd:
824 code.append(12)
825 code.append(cmd.code)
826 except AttributeError:
827 if -107 <= cmd <= 107:
828 code.append(cmd+139)
829 elif 108 <= cmd <= 1131:
830 a, b = divmod(cmd-108, 256)
831 code.append(a+247)
832 code.append(b)
833 elif -1131 <= cmd <= -108:
834 a, b = divmod(-cmd-108, 256)
835 code.append(a+251)
836 code.append(b)
837 else:
838 if cmd < 0:
839 cmd += 1 << 32
840 cmd, x4 = divmod(cmd, 256)
841 cmd, x3 = divmod(cmd, 256)
842 x1, x2 = divmod(cmd, 256)
843 code.append(255)
844 code.append(x1)
845 code.append(x2)
846 code.append(x3)
847 code.append(x4)
848 return self._charstringencode(code.tobytes())
850 def getsubrcmds(self, subr):
851 """return a list of T1cmd's for subr subr"""
852 if not self._data2:
853 self._data2decode()
854 return self._cmds(self.subrs[subr])
856 def getglyphcmds(self, glyph):
857 """return a list of T1cmd's for glyph glyph"""
858 if not self._data2:
859 self._data2decode()
860 return self._cmds(self.glyphs[glyph])
862 def setsubrcmds(self, subr, cmds):
863 """replaces the T1cmd's by the list cmds for subr subr"""
864 if not self._data2:
865 self._data2decode()
866 self._data2eexec = None
867 self.subrs[subr] = self._code(cmds)
869 def setglyphcmds(self, glyph, cmds):
870 """replaces the T1cmd's by the list cmds for glyph glyph"""
871 if not self._data2:
872 self._data2decode()
873 self._data2eexec = None
874 self.glyphs[glyph] = self._code(cmds)
876 def updatepath(self, cmds, path, trafo, context):
877 for cmd in cmds:
878 if isinstance(cmd, T1cmd):
879 cmd.updatepath(path, trafo, context)
880 else:
881 context.t1stack.append(cmd)
883 def updatesubrpath(self, subr, path, trafo, context):
884 self.updatepath(self.getsubrcmds(subr), path, trafo, context)
886 def updateglyphpath(self, glyph, path, trafo, context):
887 self.updatepath(self.getglyphcmds(glyph), path, trafo, context)
889 def gathercalls(self, cmds, seacglyphs, subrs, context):
890 for cmd in cmds:
891 if isinstance(cmd, T1cmd):
892 cmd.gathercalls(seacglyphs, subrs, context)
893 else:
894 context.t1stack.append(cmd)
896 def gathersubrcalls(self, subr, seacglyphs, subrs, context):
897 self.gathercalls(self.getsubrcmds(subr), seacglyphs, subrs, context)
899 def gatherglyphcalls(self, glyph, seacglyphs, subrs, context):
900 self.gathercalls(self.getglyphcmds(glyph), seacglyphs, subrs, context)
902 def getglyphpath_pt(self, x_pt, y_pt, glyph, size_pt, convertcharcode=False, flex=True):
903 """return an object containing the PyX path, wx_pt and wy_pt for glyph named glyph"""
904 if convertcharcode:
905 if not self.encoding:
906 self._encoding()
907 glyph = self.encoding[glyph]
908 t = self.fontmatrix.scaled(size_pt)
909 tpath = t.translated_pt(x_pt, y_pt)
910 context = T1context(self, flex=flex)
911 p = path()
912 self.updateglyphpath(glyph, p, tpath, context)
913 class glyphpath:
914 def __init__(self, p, wx_pt, wy_pt):
915 self.path = p
916 self.wx_pt = wx_pt
917 self.wy_pt = wy_pt
918 return glyphpath(p, *t.apply_pt(context.wx, context.wy))
920 def getdata2(self, subrs=None, glyphs=None):
921 """makes a data2 string
923 subrs is a dict containing those subrs numbers as keys,
924 which are to be contained in the subrsstring to be created.
925 If subrs is None, all subrs in self.subrs will be used.
926 The subrs dict might be modified *in place*.
928 glyphs is a dict containing those glyph names as keys,
929 which are to be contained in the charstringsstring to be created.
930 If glyphs is None, all glyphs in self.glyphs will be used."""
931 w = writer.writer(io.BytesIO())
933 def addsubrs(subrs):
934 if subrs is not None:
935 # some adjustments to the subrs dict
936 if subrs:
937 subrsmin = min(subrs)
938 subrsmax = max(subrs)
939 if self.hasflexhintsubrs and subrsmin < len(self.flexhintsubrs):
940 # According to the spec we need to keep all the flex and hint subrs
941 # as long as any of it is used.
942 for subr in range(len(self.flexhintsubrs)):
943 subrs.add(subr)
944 subrsmax = max(subrs)
945 else:
946 subrsmax = -1
947 else:
948 # build a new subrs dict containing all subrs
949 subrs = dict([(subr, 1) for subr in range(len(self.subrs))])
950 subrsmax = len(self.subrs) - 1
952 # build the string from all selected subrs
953 w.write("%d array\n" % (subrsmax + 1))
954 for subr in range(subrsmax+1):
955 if subr in subrs:
956 code = self.subrs[subr]
957 else:
958 code = self.emptysubr
959 w.write("dup %d %d " % (subr, len(code)))
960 w.write_bytes(self.subrrdtoken)
961 w.write_bytes(b" ")
962 w.write_bytes(code)
963 w.write_bytes(b" ")
964 w.write_bytes(self.subrnptoken)
965 w.write_bytes(b"\n")
967 def addcharstrings(glyphs):
968 w.write("%d dict dup begin\n" % (glyphs is None and len(self.glyphlist) or len(glyphs)))
969 for glyph in self.glyphlist:
970 if glyphs is None or glyph in glyphs:
971 w.write("/%s %d " % (glyph, len(self.glyphs[glyph])))
972 w.write_bytes(self.glyphrdtoken)
973 w.write_bytes(b" ")
974 w.write_bytes(self.glyphs[glyph])
975 w.write_bytes(b" ")
976 w.write_bytes(self.glyphndtoken)
977 w.write_bytes(b"\n")
978 w.write("end\n")
980 if self.subrsstart < self.charstringsstart:
981 w.write_bytes(self._data2[:self.subrsstart])
982 addsubrs(subrs)
983 w.write_bytes(self._data2[self.subrsend:self.charstringsstart])
984 addcharstrings(glyphs)
985 w.write_bytes(self._data2[self.charstringsend:])
986 else:
987 w.write_bytes(self._data2[:self.charstringsstart])
988 addcharstrings(glyphs)
989 w.write_bytes(self._data2[self.charstringsend:self.subrsstart])
990 addsubrs(subrs)
991 w.write_bytes(self._data2[self.subrsend:])
992 return w.file.getvalue()
994 def getdata2eexec(self):
995 if self._data2eexec:
996 return self._data2eexec
997 # note that self._data2 is out-of-date here too, hence we need to call getdata2
998 return self._eexecencode(self.getdata2())
1000 newlinepattern = re.compile("\s*[\r\n]\s*")
1001 uniqueidstrpattern = re.compile("%?/UniqueID\s+\d+\s+def\s+")
1002 uniqueidbytespattern = re.compile(b"%?/UniqueID\s+\d+\s+def\s+")
1003 # when UniqueID is commented out (as in modern latin), prepare to remove the comment character as well
1005 def getstrippedfont(self, glyphs, charcodes):
1006 """create a T1File instance containing only certain glyphs
1008 glyphs is a set of the glyph names. It might be modified *in place*!
1010 if not self.encoding:
1011 self._encoding()
1012 for charcode in charcodes:
1013 glyphs.add(self.encoding[charcode])
1015 # collect information about used glyphs and subrs
1016 seacglyphs = set()
1017 subrs = set()
1018 for glyph in glyphs:
1019 self.gatherglyphcalls(glyph, seacglyphs, subrs, T1context(self))
1020 # while we have gathered all subrs for the seacglyphs alreadys, we
1021 # might have missed the glyphs themself (when they are not used stand-alone)
1022 glyphs.update(seacglyphs)
1023 glyphs.add(".notdef")
1025 # strip data1
1026 if self.encoding is adobestandardencoding:
1027 data1 = self.data1
1028 else:
1029 encodingstrings = []
1030 for char, glyph in enumerate(self.encoding):
1031 if glyph in glyphs:
1032 encodingstrings.append("dup %i /%s put\n" % (char, glyph))
1033 data1 = self.data1[:self.encodingstart] + "\n" + "".join(encodingstrings) + self.data1[self.encodingend:]
1034 data1 = self.newlinepattern.subn("\n", data1)[0]
1035 data1 = self.uniqueidstrpattern.subn("", data1)[0]
1037 # strip data2
1038 data2 = self.uniqueidbytespattern.subn(b"", self.getdata2(subrs, glyphs))[0]
1040 # strip data3
1041 data3 = self.newlinepattern.subn("\n", self.data3)[0]
1043 # create and return the new font instance
1044 return T1File(data1.rstrip() + "\n", self._eexecencode(data2), data3.rstrip() + "\n")
1046 # The following two methods, writePDFfontinfo and getglyphinfo,
1047 # extract informtion which should better be taken from the afm file.
1048 def writePDFfontinfo(self, file):
1049 try:
1050 glyphinfo_y = self.getglyphinfo("y")
1051 glyphinfo_W = self.getglyphinfo("W")
1052 glyphinfo_H = self.getglyphinfo("H")
1053 glyphinfo_h = self.getglyphinfo("h")
1054 glyphinfo_period = self.getglyphinfo("period")
1055 glyphinfo_colon = self.getglyphinfo("colon")
1056 except:
1057 logger.warning("Auto-guessing of font information for font '%s' failed. We're writing stub data instead." % self.name)
1058 file.write("/Flags 4\n")
1059 file.write("/FontBBox [0 -100 1000 1000]\n")
1060 file.write("/ItalicAngle 0\n")
1061 file.write("/Ascent 1000\n")
1062 file.write("/Descent -100\n")
1063 file.write("/CapHeight 700\n")
1064 file.write("/StemV 100\n")
1065 else:
1066 if not self.encoding:
1067 self._encoding()
1068 # As a simple heuristics we assume non-symbolic fonts if and only
1069 # if the Adobe standard encoding is used. All other font flags are
1070 # not specified here.
1071 if self.encoding is adobestandardencoding:
1072 file.write("/Flags 32\n")
1073 else:
1074 file.write("/Flags 4\n")
1075 file.write("/FontBBox [0 %d %d %d]\n" % (glyphinfo_y[3], glyphinfo_W[0], glyphinfo_H[5]))
1076 file.write("/ItalicAngle %d\n" % math.degrees(math.atan2(glyphinfo_period[4]-glyphinfo_colon[4], glyphinfo_colon[5]-glyphinfo_period[5])))
1077 file.write("/Ascent %d\n" % glyphinfo_H[5])
1078 file.write("/Descent %d\n" % glyphinfo_y[3])
1079 file.write("/CapHeight %d\n" % glyphinfo_h[5])
1080 file.write("/StemV %d\n" % (glyphinfo_period[4]-glyphinfo_period[2]))
1082 def getglyphinfo(self, glyph, flex=True):
1083 logger.warning("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)
1084 context = T1context(self, flex=flex)
1085 p = path()
1086 self.updateglyphpath(glyph, p, trafo.trafo(), context)
1087 bbox = p.bbox()
1088 return context.wx, context.wy, bbox.llx_pt, bbox.lly_pt, bbox.urx_pt, bbox.ury_pt
1090 def outputPFA(self, file, remove_UniqueID_lookup=False):
1091 """output the T1File in PFA format"""
1092 data1 = self.data1
1093 data3 = self.data3
1094 if remove_UniqueID_lookup:
1095 m1 = re.search("""FontDirectory\s*/%(name)s\s+known{/%(name)s\s+findfont\s+dup\s*/UniqueID\s+known\s*{\s*dup\s*
1096 /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*
1097 {save\s+true\s*}\s*{\s*false\s*}\s*ifelse\s*}\s*{\s*false\s*}\s*ifelse""" % {"name": self.name},
1098 data1, re.VERBOSE)
1099 m3 = re.search("\s*{restore}\s*if", data3)
1100 if m1 and m3:
1101 data1 = data1[:m1.start()] + data1[m1.end():]
1102 data3 = data3[:m3.start()] + data3[m3.end():]
1103 file.write(data1)
1104 data2eexechex = binascii.b2a_hex(self.getdata2eexec())
1105 linelength = 64
1106 for i in range((len(data2eexechex)-1)//linelength + 1):
1107 file.write_bytes(data2eexechex[i*linelength: i*linelength+linelength])
1108 file.write("\n")
1109 file.write(data3)
1111 def outputPFB(self, file):
1112 """output the T1File in PFB format"""
1113 data2eexec = self.getdata2eexec()
1114 def pfblength(data):
1115 l = len(data)
1116 l, x1 = divmod(l, 256)
1117 l, x2 = divmod(l, 256)
1118 x4, x3 = divmod(l, 256)
1119 return chr(x1) + chr(x2) + chr(x3) + chr(x4)
1120 file.write("\200\1")
1121 file.write(pfblength(self.data1))
1122 file.write(self.data1)
1123 file.write("\200\2")
1124 file.write(pfblength(data2eexec))
1125 file.write(data2eexec)
1126 file.write("\200\1")
1127 file.write(pfblength(self.data3))
1128 file.write(self.data3)
1129 file.write("\200\3")
1131 def outputPS(self, file, writer):
1132 """output the PostScript code for the T1File to the file file"""
1133 self.outputPFA(file, remove_UniqueID_lookup=True)
1135 def outputPDF(self, file, writer):
1136 data2eexec = self.getdata2eexec()
1137 data3 = self.data3
1138 # we might be allowed to skip the third part ...
1139 if (data3.replace("\n", "")
1140 .replace("\r", "")
1141 .replace("\t", "")
1142 .replace(" ", "")) == "0"*512 + "cleartomark":
1143 data3 = ""
1145 data = self.data1.encode("ascii", errors="surrogateescape") + data2eexec + data3.encode("ascii", errors="surrogateescape")
1146 if writer.compress and haszlib:
1147 data = zlib.compress(data)
1149 file.write("<<\n"
1150 "/Length %d\n"
1151 "/Length1 %d\n"
1152 "/Length2 %d\n"
1153 "/Length3 %d\n" % (len(data), len(self.data1), len(data2eexec), len(data3)))
1154 if writer.compress and haszlib:
1155 file.write("/Filter /FlateDecode\n")
1156 file.write(">>\n"
1157 "stream\n")
1158 file.write_bytes(data)
1159 file.write("\n"
1160 "endstream\n")
1162 @classmethod
1163 def from_PFA_bytes(cls, bytes):
1164 """create a T1File instance from a string of bytes corresponding to a PFA file"""
1165 try:
1166 m1 = bytes.index("eexec") + 6
1167 m2 = bytes.index("0"*40)
1168 except ValueError:
1169 raise FontFormatError
1171 data1 = bytes[:m1].decode("ascii", errors="surrogateescape")
1172 data2eexec = binascii.a2b_hex(bytes[m1: m2].replace(" ", "").replace("\r", "").replace("\n", ""))
1173 data3 = bytes[m2:].decode("ascii", errors="surrogateescape")
1174 return cls(data1, data2eexec, data3)
1176 @classmethod
1177 def from_PFA_filename(cls, filename):
1178 """create a T1File instance from PFA font file of given name"""
1179 with open(filename, "rb") as file:
1180 t1file = cls.from_PFA_bytes(file.read())
1181 return t1file
1183 @classmethod
1184 def from_PFB_bytes(cls, bytes):
1185 """create a T1File instance from a string of bytes corresponding to a PFB file"""
1187 def pfblength(s):
1188 if len(s) != 4:
1189 raise ValueError("invalid string length")
1190 return (s[0] +
1191 s[1]*256 +
1192 s[2]*256*256 +
1193 s[3]*256*256*256)
1194 class consumer:
1195 def __init__(self, bytes):
1196 self.bytes = bytes
1197 self.pos = 0
1198 def __call__(self, n):
1199 result = self.bytes[self.pos:self.pos+n]
1200 self.pos += n
1201 return result
1203 consume = consumer(bytes)
1204 mark = consume(2)
1205 if mark != b"\200\1":
1206 raise FontFormatError
1207 data1 = consume(pfblength(consume(4))).decode("ascii", errors="surrogateescape")
1208 mark = consume(2)
1209 if mark != b"\200\2":
1210 raise FontFormatError
1211 data2eexec = b""
1212 while mark == b"\200\2":
1213 data2eexec = data2eexec + consume(pfblength(consume(4)))
1214 mark = consume(2)
1215 if mark != b"\200\1":
1216 raise FontFormatError
1217 data3 = consume(pfblength(consume(4))).decode("ascii", errors="surrogateescape")
1218 mark = consume(2)
1219 if mark != b"\200\3":
1220 raise FontFormatError
1221 if consume(1):
1222 raise FontFormatError
1224 return cls(data1, data2eexec, data3)
1226 @classmethod
1227 def from_PFB_filename(cls, filename):
1228 """create a T1File instance from PFB font file of given name"""
1229 with open(filename, "rb") as file:
1230 t1file = cls.from_PFB_bytes(file.read())
1231 return t1file
1233 @classmethod
1234 def from_PF_bytes(cls, bytes):
1235 try:
1236 return cls.from_PFB_bytes(bytes)
1237 except FontFormatError:
1238 return cls.from_PFA_bytes(bytes)
1240 @classmethod
1241 def from_PF_filename(cls, filename):
1242 """create a T1File instance from PFA or PFB font file of given name"""
1243 with open(filename, "rb") as file:
1244 t1file = cls.from_PF_bytes(file.read())
1245 return t1file