a prototype for box.equalalign
[PyX/mjg.git] / pyx / text.py
blob3fcf97d16da0882fb70ad0a31f202ec6cfa8e06d
1 #!/usr/bin/env python
4 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2002 André Wobst <wobsta@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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 # this code will be part of PyX 0.3
26 import os, threading, Queue, traceback, re, struct, tempfile
27 import helper, attrlist, bbox, unit, box, base, trafo, canvas
29 ###############################################################################
30 # joergl would mainly work here ...
32 ###############################################################################
33 # this is the old stuff
34 _DVI_CHARMIN = 0 # typeset a character and move right (range min)
35 _DVI_CHARMAX = 127 # typeset a character and move right (range max)
36 _DVI_SET1234 = 128 # typeset a character and move right
37 _DVI_SETRULE = 132 # typeset a rule and move right
38 _DVI_PUT1234 = 133 # typeset a character
39 _DVI_PUTRULE = 137 # typeset a rule
40 _DVI_NOP = 138 # no operation
41 _DVI_BOP = 139 # beginning of page
42 _DVI_EOP = 140 # ending of page
43 _DVI_PUSH = 141 # save the current positions (h, v, w, x, y, z)
44 _DVI_POP = 142 # restore positions (h, v, w, x, y, z)
45 _DVI_RIGHT1234 = 143 # move right
46 _DVI_W0 = 147 # move right by w
47 _DVI_W1234 = 148 # move right and set w
48 _DVI_X0 = 152 # move right by x
49 _DVI_X1234 = 153 # move right and set x
50 _DVI_DOWN1234 = 157 # move down
51 _DVI_Y0 = 161 # move down by y
52 _DVI_Y1234 = 162 # move down and set y
53 _DVI_Z0 = 166 # move down by z
54 _DVI_Z1234 = 167 # move down and set z
55 _DVI_FNTNUMMIN = 171 # set current font (range min)
56 _DVI_FNTNUMMAX = 234 # set current font (range max)
57 _DVI_FNT1234 = 235 # set current font
58 _DVI_SPECIAL1234 = 239 # special (dvi extention)
59 _DVI_FNTDEF1234 = 243 # define the meaning of a font number
60 _DVI_PRE = 247 # preamble
61 _DVI_POST = 248 # postamble beginning
62 _DVI_POSTPOST = 249 # postamble ending
64 _DVI_VERSION = 2 # dvi version
66 # position variable indices
67 _POS_H = 0
68 _POS_V = 1
69 _POS_W = 2
70 _POS_X = 3
71 _POS_Y = 4
72 _POS_Z = 5
74 # reader states
75 _READ_PRE = 1
76 _READ_NOPAGE = 2
77 _READ_PAGE = 3
78 _READ_POST = 4
79 _READ_POSTPOST = 5
80 _READ_DONE = 6
82 class fix_word:
83 def __init__(self, word):
84 if word>=0:
85 self.sign = 1
86 else:
87 self.sign = -1
89 self.precomma = abs(word) >> 20
90 self.postcomma = abs(word) & 0xFFFFF
92 def __float__(self):
93 return self.sign * (self.precomma + 1.0*self.postcomma/0xFFFFF)
95 def __mul__(self, other):
96 # hey, it's Q&D
97 result = fix_word(0)
99 result.sign = self.sign*other.sign
100 c = self.postcomma*other.precomma + self.precomma*other.postcomma
101 result.precomma = self.precomma*other.precomma + (c >> 20)
102 result.postcomma = c & 0xFFFFF + ((self.postcomma*other.postcomma) >> 40)
103 return result
106 class char_info_word:
107 def __init__(self, word):
108 self.width_index = (word & 0xFF000000) >> 24
109 self.height_index = (word & 0x00F00000) >> 20
110 self.depth_index = (word & 0x000F0000) >> 16
111 self.italic_index = (word & 0x0000FC00) >> 10
112 self.tag = (word & 0x00000300) >> 8
113 self.remainder = (word & 0x000000FF)
115 if self.width_index==0:
116 raise TFMError, "width_index should not be zero"
118 class binfile:
120 def __init__(self, filename, mode="r"):
121 self.file = open(filename, mode)
123 def tell(self):
124 return self.file.tell()
126 def read(self, bytes):
127 return self.file.read(bytes)
129 def readint(self, bytes = 4, signed = 0):
130 first = 1
131 result = 0
132 while bytes:
133 value = ord(self.file.read(1))
134 if first and signed and value > 127:
135 value -= 256
136 first = 0
137 result = 256 * result + value
138 bytes -= 1
139 return result
141 def readint32(self):
142 return struct.unpack(">l", self.file.read(4))[0]
144 def readuint32(self):
145 return struct.unpack(">L", self.file.read(4))[0]
147 def readint24(self):
148 # XXX: checkme
149 return struct.unpack(">l", "\0"+self.file.read(3))[0]
151 def readuint24(self):
152 # XXX: checkme
153 return struct.unpack(">L", "\0"+self.file.read(3))[0]
155 def readint16(self):
156 return struct.unpack(">h", self.file.read(2))[0]
158 def readuint16(self):
159 return struct.unpack(">H", self.file.read(2))[0]
161 def readchar(self):
162 return struct.unpack("b", self.file.read(1))[0]
164 def readuchar(self):
165 return struct.unpack("B", self.file.read(1))[0]
167 def readstring(self, bytes):
168 l = self.readuchar()
169 assert l<bytes-1, "inconsistency in file: string too long"
170 return self.file.read(bytes-1)[:l]
172 class DVIError(Exception): pass
174 class TFMError(Exception): pass
176 class TFMFile:
177 def __init__(self, name):
178 self.file = binfile(name, "rb")
181 # read pre header
184 self.lf = self.file.readint16()
185 self.lh = self.file.readint16()
186 self.bc = self.file.readint16()
187 self.ec = self.file.readint16()
188 self.nw = self.file.readint16()
189 self.nh = self.file.readint16()
190 self.nd = self.file.readint16()
191 self.ni = self.file.readint16()
192 self.nl = self.file.readint16()
193 self.nk = self.file.readint16()
194 self.ne = self.file.readint16()
195 self.np = self.file.readint16()
197 if not (self.bc-1<=self.ec<=255 and
198 self.ne<=256 and
199 self.lf==6+self.lh+(self.ec-self.bc+1)+self.nw+self.nh+self.nd
200 +self.ni+self.nl+self.nk+self.ne+self.np):
201 raise TFMError, "error in TFM pre-header"
204 # read header
207 self.checksum = self.file.readint32()
208 self.designsizeraw = self.file.readint32()
209 assert self.designsizeraw>0, "invald design size"
210 self.designsize = fix_word(self.designsizeraw)
211 if self.lh>2:
212 self.charcoding = self.file.readstring(40)
213 else:
214 self.charcoding = None
216 if self.lh>12:
217 self.fontfamily = self.file.readstring(20)
218 else:
219 self.fontfamily = None
221 if self.lh>17:
222 self.sevenbitsave = self.file.readuchar()
223 # ignore the following two bytes
224 self.file.readint16()
225 facechar = self.file.readuchar()
226 # decode ugly face specification into the Knuth suggested string
227 if facechar<18:
228 if facechar>=12:
229 self.face = "E"
230 facechar -= 12
231 elif facechar>=6:
232 self.face = "C"
233 facechar -= 6
234 else:
235 self.face = "R"
237 if facechar>=4:
238 self.face = "L" + self.face
239 facechar -= 4
240 elif facechar>=2:
241 self.face = "B" + self.face
242 facechar -= 2
243 else:
244 self.face = "M" + self.face
246 if facechar==1:
247 self.face = self.face[0] + "I" + self.face[1]
248 else:
249 self.face = self.face[0] + "R" + self.face[1]
251 else:
252 self.face = None
253 else:
254 self.sevenbitsave = self.face = None
256 if self.lh>18:
257 # just ignore the rest
258 self.file.read((self.lh-18)*4)
261 # read char_info
264 self.char_info = [None for charcode in range(self.ec+1)]
266 for charcode in range(self.bc, self.ec+1):
267 self.char_info[charcode] = char_info_word(self.file.readint32())
270 # read widths
273 self.width = [None for width_index in range(self.nw)]
274 for width_index in range(self.nw):
275 # self.width[width_index] = fix_word(self.file.readint32())
276 self.width[width_index] = self.file.readint32()
279 # read heights
282 self.height = [None for height_index in range(self.nh)]
283 for height_index in range(self.nh):
284 # self.height[height_index] = fix_word(self.file.readint32())
285 self.height[height_index] = self.file.readint32()
288 # read depths
291 self.depth = [None for depth_index in range(self.nd)]
292 for depth_index in range(self.nd):
293 # self.depth[depth_index] = fix_word(self.file.readint32())
294 self.depth[depth_index] = self.file.readint32()
297 # read italic
300 self.italic = [None for italic_index in range(self.ni)]
301 for italic_index in range(self.ni):
302 # self.italic[italic_index] = fix_word(self.file.readint32())
303 self.italic[italic_index] = self.file.readint32()
306 # read lig_kern
309 # XXX decode to lig_kern_command
311 self.lig_kern = [None for lig_kern_index in range(self.nl)]
312 for lig_kern_index in range(self.nl):
313 self.lig_kern[lig_kern_index] = self.file.readint32()
316 # read kern
319 self.kern = [None for kern_index in range(self.nk)]
320 for kern_index in range(self.nk):
321 # self.kern[kern_index] = fix_word(self.file.readint32())
322 self.kern[kern_index] = self.file.readint32()
325 # read exten
328 # XXX decode to extensible_recipe
330 self.exten = [None for exten_index in range(self.ne)]
331 for exten_index in range(self.ne):
332 self.exten[exten_index] = self.file.readint32()
335 # read param
338 # XXX decode
340 self.param = [None for param_index in range(self.np)]
341 for param_index in range(self.np):
342 self.param[param_index] = self.file.readint32()
345 class Font:
346 def __init__(self, name, c, q, d, tfmconv):
347 self.name = name
348 path = os.popen("kpsewhich %s.tfm" % self.name, "r").readline()[:-1]
349 self.tfmfile = TFMFile(path)
351 if self.tfmfile.checksum!=c:
352 raise DVIError("check sums do not agree: %d vs. %d" %
353 (self.checksum, c))
355 self.tfmdesignsize = round(tfmconv*self.tfmfile.designsizeraw)
357 if abs(self.tfmdesignsize - d)>2:
358 raise DVIError("design sizes do not agree: %d vs. %d" %
359 (self.tfmdesignsize, d))
362 if q<0 or q>134217728:
363 raise DVIError("font '%s' not loaded: bad scale" % fontname)
365 if d<0 or d>134217728:
366 raise DVIError("font '%s' not loaded: bad design size" % fontname)
368 self.scale = 1.0*q/d
369 self.alpha = 16;
370 self.q = self.qorig = q
371 while self.q>=8388608:
372 self.q = self.q/2
373 self.alpha *= 2
375 self.beta = 256/self.alpha;
376 self.alpha = self.alpha*self.q;
378 def __str__(self):
379 return "Font(%s, %d)" % (self.name, self.tfmdesignsize)
381 __repr__ = __str__
383 def convert(self, width):
384 b0 = width >> 24
385 b1 = (width >> 16) & 0xff
386 b2 = (width >> 8 ) & 0xff
387 b3 = (width ) & 0xff
388 # print width*self.qorig*16/ 16777216, (((((b3*self.q)/256)+(b2*self.q))/256)+(b1*self.q))/self.beta
390 if b0==0:
391 return (((((b3*self.q)/256)+(b2*self.q))/256)+(b1*self.q))/self.beta
392 elif b0==255:
393 return (((((b3*self.q)/256)+(b2*self.q))/256)+(b1*self.q))/self.beta-self.alpha
394 else:
395 raise TFMError("error in font size")
397 def __getattr__(self, attr):
398 return self.tfmfile.__dict__[attr]
400 def getwidth(self, charcode):
401 return self.convert(self.tfmfile.width[self.char_info[charcode].width_index])
403 def getheight(self, charcode):
404 return self.convert(self.tfmfile.height[self.char_info[charcode].height_index])
406 def getdepth(self, charcode):
407 return self.convert(self.tfmfile.depth[self.char_info[charcode].depth_index])
409 def getitalic(self, charcode):
410 return self.convert(self.tfmfile.italic[self.char_info[charcode].italic_index])
413 class DVIFile:
415 def flushout(self):
416 if self.actoutstart:
417 if self.debug:
418 print "[%s]" % self.actoutstring
419 self.actpage.append(("c",
420 self.actoutstart[0], self.actoutstart[1],
421 self.actoutstring))
422 self.actoutstart = None
424 def putchar(self, char, inch=1):
425 x = self.pos[_POS_H] * self.conv / 100000
426 y = -self.pos[_POS_V] * self.conv / 100000
427 dx = inch and self.fonts[self.activefont].getwidth(char) or 0
429 if self.debug:
430 print ("%d: %schar%d h:=%d+%d=%d, hh:=%d" %
431 (self.filepos,
432 inch and "set" or "put",
433 char,
434 self.pos[_POS_H], dx, self.pos[_POS_H]+dx,
437 self.pos[_POS_H] += dx
439 ascii = (char > 32 and char < 128) and "%s" % chr(char) or "\\%03o" % char
441 if self.actoutstart is None:
442 self.actoutstart = unit.t_cm(x), unit.t_cm(y)
443 self.actoutstring = ""
444 self.actoutstring = self.actoutstring + ascii
446 if not inch:
447 # XXX: correct !?
448 self.flushout()
450 def putrule(self, height, width, inch=1):
451 self.flushout()
452 if height > 0 and width > 0:
453 x1 = self.pos[_POS_H] * self.conv * 1e-5
454 y1 = -self.pos[_POS_V] * self.conv * 1e-5
455 w = width * self.conv * 1e-5
456 h = height * self.conv * 1e-5
457 if self.debug:
458 print "%d: putrule height %d, width %d" % (self.filepos, height, width)
459 self.actpage.append(("r",
460 unit.t_cm(x1), unit.t_cm(y1),
461 unit.t_cm(w), unit.t_cm(h)))
462 if inch:
463 pass # TODO: increment h
465 def usefont(self, fontnum):
466 self.flushout()
467 self.activefont = fontnum
468 self.actpage.append(("f", self.fonts[fontnum]))
469 if self.debug:
470 print ("%d: fntnum%i current font is %s" %
471 (self.filepos,
472 self.activefont, self.fonts[fontnum].name))
474 def definefont(self, num, c, q, d, fontname):
475 # c: checksum
476 # q: scaling factor
477 # Note that q is actually s in large parts of the documentation.
478 # d: design size
480 self.fonts[num] = Font(fontname, c, q, d, self.tfmconv)
482 scale = round((1000.0*self.conv*q)/(self.trueconv*d))
483 m = 1.0*q/d
485 if self.debug:
486 print "%d: fntdefx %i: %s" % (self.filepos, num, fontname)
488 # scalestring = scale!=1000 and " scaled %d" % scale or ""
489 # print ("Font %i: %s%s---loaded at size %d DVI units" %
490 # (num, fontname, scalestring, q))
491 # if scale!=1000:
492 # print " (this font is magnified %d%%)" % round(scale/10)
494 def __init__(self, filename, debug=0):
496 self.filename = filename
497 self.debug = debug
498 file = binfile(self.filename, "rb")
499 state = _READ_PRE
500 stack = []
502 # XXX max number of fonts
503 self.fonts = [None for i in range(64)]
504 self.activefont = None
506 # here goes the result, for each page one list.
507 self.pages = []
509 # pointer to currently active page
510 self.actpage = None
512 # currently active output: position and content
513 self.actoutstart = None
514 self.actoutstring = ""
516 while state != _READ_DONE:
517 self.filepos = file.tell()
518 cmd = file.readuchar()
519 if cmd == _DVI_NOP: pass
521 elif state == _READ_PRE:
522 if cmd == _DVI_PRE:
523 if file.readuchar() != _DVI_VERSION: raise DVIError
524 num = file.readuint32()
525 den = file.readuint32()
526 mag = file.readuint32()
528 # self.trueconv = conv in DVIType docu
529 # if resolution = 254000.0
531 self.tfmconv = (25400000.0/num)*(den/473628672)/16.0;
532 self.trueconv = 1.0*num/den
533 self.conv = self.trueconv*(mag/1000.0)
535 comment = file.read(file.readuchar())
536 state = _READ_NOPAGE
537 else: raise DVIError
538 elif state == _READ_NOPAGE:
539 if cmd == _DVI_BOP:
540 self.flushout()
541 if self.debug:
542 print "%d: beginning of page" % self.filepos,
543 print file.readuint32()
544 for i in range(9): file.readuint32(),
545 else:
546 for i in range(10): file.readuint32(),
547 file.readuint32()
549 self.pos = [0, 0, 0, 0, 0, 0]
550 self.pages.append([])
551 self.actpage = self.pages[-1]
552 state = _READ_PAGE
553 elif cmd == _DVI_POST:
554 state = _READ_DONE # we skip the rest
555 else: raise DVIError
556 elif state == _READ_PAGE:
557 if cmd >= _DVI_CHARMIN and cmd <= _DVI_CHARMAX:
558 self.putchar(cmd)
559 elif cmd >= _DVI_SET1234 and cmd < _DVI_SET1234 + 4:
560 self.putchar(file.readint(cmd - _DVI_SET1234 + 1))
561 elif cmd == _DVI_SETRULE:
562 self.putrule(file.readint32(), file.readint32())
563 elif cmd >= _DVI_PUT1234 and cmd < _DVI_PUT1234 + 4:
564 self.putchar(file.readint(cmd - _DVI_PUT1234 + 1), inch=0)
565 elif cmd == _DVI_PUTRULE:
566 self.putrule(file.readint32(), file.readint32(), 0)
567 elif cmd == _DVI_EOP:
568 self.flushout()
569 state = _READ_NOPAGE
570 if self.debug:
571 print "%d: eop" % self.filepos
572 print
573 elif cmd == _DVI_PUSH:
574 stack.append(tuple(self.pos))
575 if self.debug:
576 print "%d: push" % self.filepos
577 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
578 (( len(stack)-1,)+tuple(self.pos)))
579 elif cmd == _DVI_POP:
580 self.flushout()
581 self.pos = list(stack[-1])
582 del stack[-1]
583 if self.debug:
584 print "%d: pop" % self.filepos
585 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
586 (( len(stack),)+tuple(self.pos)))
588 elif cmd >= _DVI_RIGHT1234 and cmd < _DVI_RIGHT1234 + 4:
589 self.flushout()
590 dh = file.readint(cmd - _DVI_RIGHT1234 + 1, 1)
591 if self.debug:
592 print ("%d: right%d %d h:=%d%+d=%d, hh:=" %
593 (self.filepos,
594 cmd - _DVI_RIGHT1234 + 1,
596 self.pos[_POS_H],
598 self.pos[_POS_H]+dh))
599 self.pos[_POS_H] += dh
600 elif cmd == _DVI_W0:
601 self.flushout()
602 if self.debug:
603 print ("%d: w0 %d h:=%d%+d=%d, hh:=" %
604 (self.filepos,
605 self.pos[_POS_W],
606 self.pos[_POS_H],
607 self.pos[_POS_W],
608 self.pos[_POS_H]+self.pos[_POS_W]))
609 self.pos[_POS_H] += self.pos[_POS_W]
610 elif cmd >= _DVI_W1234 and cmd < _DVI_W1234 + 4:
611 self.flushout()
612 self.pos[_POS_W] = file.readint(cmd - _DVI_W1234 + 1, 1)
613 if self.debug:
614 print ("%d: w%d %d h:=%d%+d=%d, hh:=" %
615 (self.filepos,
616 cmd - _DVI_W1234 + 1,
617 self.pos[_POS_W],
618 self.pos[_POS_H],
619 self.pos[_POS_W],
620 self.pos[_POS_H]+self.pos[_POS_W]))
621 self.pos[_POS_H] += self.pos[_POS_W]
622 elif cmd == _DVI_X0:
623 self.flushout()
624 self.pos[_POS_H] += self.pos[_POS_X]
625 elif cmd >= _DVI_X1234 and cmd < _DVI_X1234 + 4:
626 self.flushout()
627 self.pos[_POS_X] = file.readint(cmd - _DVI_X1234 + 1, 1)
628 self.pos[_POS_H] += self.pos[_POS_X]
629 elif cmd >= _DVI_DOWN1234 and cmd < _DVI_DOWN1234 + 4:
630 self.flushout()
631 dv = file.readint(cmd - _DVI_DOWN1234 + 1, 1)
632 if self.debug:
633 print ("%d: down%d %d v:=%d%+d=%d, vv:=" %
634 (self.filepos,
635 cmd - _DVI_DOWN1234 + 1,
637 self.pos[_POS_V],
639 self.pos[_POS_V]+dv))
640 self.pos[_POS_V] += dv
641 elif cmd == _DVI_Y0:
642 self.flushout()
643 self.pos[_POS_V] += self.pos[_POS_Y]
644 elif cmd >= _DVI_Y1234 and cmd < _DVI_Y1234 + 4:
645 self.flushout()
646 self.pos[_POS_Y] = file.readint(cmd - _DVI_Y1234 + 1, 1)
647 self.pos[_POS_V] += self.pos[_POS_Y]
648 elif cmd == _DVI_Z0:
649 self.flushout()
650 self.pos[_POS_V] += self.pos[_POS_Z]
651 elif cmd >= _DVI_Z1234 and cmd < _DVI_Z1234 + 4:
652 self.flushout()
653 self.pos[_POS_Z] = file.readint(cmd - _DVI_Z1234 + 1, 1)
654 self.pos[_POS_V] += self.pos[_POS_Z]
655 elif cmd >= _DVI_FNTNUMMIN and cmd <= _DVI_FNTNUMMAX:
656 self.usefont(cmd - _DVI_FNTNUMMIN)
657 elif cmd >= _DVI_FNT1234 and cmd < _DVI_FNT1234 + 4:
658 self.usefont(file.readint(cmd - _DVI_FNT1234 + 1, 1))
659 elif cmd >= _DVI_SPECIAL1234 and cmd < _DVI_SPECIAL1234 + 4:
660 print "special %s" % file.read(file.readint(cmd - _DVI_SPECIAL1234 + 1))
661 raise RuntimeError("specials are not yet handled, abort")
662 elif cmd >= _DVI_FNTDEF1234 and cmd < _DVI_FNTDEF1234 + 4:
663 if cmd==_DVI_FNTDEF1234:
664 num=file.readuchar()
665 elif cmd==_DVI_FNTDEF1234+1:
666 num=file.readuint16()
667 elif cmd==_DVI_FNTDEF1234+2:
668 num=file.readuint24()
669 elif cmd==_DVI_FNTDEF1234+3:
670 # Cool, here we have according to docu a signed int. Why?
671 num=file.readint32()
673 self.definefont(num,
674 file.readint32(),
675 file.readint32(),
676 file.readint32(),
677 file.read(file.readuchar()+file.readuchar()))
678 else: raise DVIError
680 else: raise DVIError # unexpected reader state
681 self.flushout()
683 def writeheader(self, file):
684 """write PostScript font header"""
685 for font in self.fonts:
686 if font:
687 file.write("%%%%BeginFont: %s\n" % font.name.upper())
689 tmpfilename = tempfile.mktemp(suffix=".pfa")
690 pfbfilename = os.popen("kpsewhich %s.pfb" % font.name).readlines()[-1][:-1]
691 os.system("pfb2pfa %s %s" % (pfbfilename, tmpfilename))
693 pfa = open(tmpfilename, "r")
694 file.write(pfa.read())
695 pfa.close()
696 os.unlink(tmpfilename)
698 file.write("%%EndFont\n")
700 FontSizePattern= re.compile(r"([0-9]+)$")
702 def write(self, file, page):
703 """write PostScript code for page into file"""
704 if self.debug:
705 print "dvifile(\"%s\").write() for page %s called" % (self.filename, page)
706 for el in self.pages[page-1]:
707 command, arg = el[0], el[1:]
708 if command=="c":
709 x, y, c = arg
710 file.write("%f %f moveto (%s) show\n" % (unit.topt(x), unit.topt(y), c))
711 if command=="r":
712 x1, y1, w, h = arg
713 file.write("%f %f moveto %f 0 rlineto 0 %f rlineto %f 0 rlineto closepath fill\n" %
714 (unit.topt(x1), unit.topt(y1),
715 unit.topt(w),
716 unit.topt(h),
717 -unit.topt(w)))
718 elif command=="f":
719 fontname = arg[0].name
720 fontscale = arg[0].scale
721 match = self.FontSizePattern.search(fontname)
722 if match:
723 file.write("/%s %f selectfont\n" % (fontname.upper(),
724 int(match.group(1))*fontscale))
725 else:
726 raise RuntimeError("cannot determine font size from name '%s'" % fontname)
729 #if __name__=="__main__":
730 # cmr10 = Font("cmr10")
731 # print cmr10.charcoding
732 # print cmr10.fontfamily
733 # print cmr10.face
734 # for charcode in range(cmr10.bc, cmr10.ec+1):
735 # print "%d\th=%f\tw=%f\td=%f\ti=%f" % (
736 # charcode,
737 # cmr10.getwidth(charcode),
738 # cmr10.getheight(charcode),
739 # cmr10.getdepth(charcode),
740 # cmr10.getitalic(charcode))
742 # dvifile = DVIFile("test.dvi")
743 # print [font for font in dvifile.fonts if font]
745 # end of old stuff
746 ###############################################################################
748 ###############################################################################
749 # wobsta would mainly work here ...
751 class TexResultError(Exception):
753 def __init__(self, description, texrunner):
754 self.description = description
755 self.texrunner = texrunner
757 def __str__(self):
758 return ("%s\n" % self.description +
759 "The expression passed to TeX was:\n" +
760 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
761 "The return message from TeX was:\n" +
762 " %s\n" % self.texrunner.texmsg.replace("\n", "\n ").rstrip() +
763 "After parsing this message, the following was left:\n" +
764 " %s" % self.texrunner.texmsgparsed.replace("\n", "\n ").rstrip())
767 class TexResultWarning(TexResultError): pass
770 ###############################################################################
771 # checkmsg
772 ############################################################################{{{
775 class checkmsg: pass
778 class _checkmsgstart(checkmsg):
780 startpattern = re.compile(r"This is [0-9a-zA-Z\s_]*TeX")
782 def check(self, texrunner):
783 m = self.startpattern.search(texrunner.texmsgparsed)
784 if not m:
785 raise TexResultError("TeX startup failed", texrunner)
786 texrunner.texmsgparsed = texrunner.texmsgparsed[m.end():]
787 try:
788 texrunner.texmsgparsed = texrunner.texmsgparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
789 except IndexError:
790 raise TexResultError("TeX running startup file failed", texrunner)
791 try:
792 texrunner.texmsgparsed = texrunner.texmsgparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
793 except IndexError:
794 raise TexResultError("TeX switch to scrollmode failed", texrunner)
797 class _checkmsgnoaux(checkmsg):
799 def check(self, texrunner):
800 try:
801 texrunner.texmsgparsed = texrunner.texmsgparsed.split("No file %s.aux." % texrunner.texfilename, 1)[1]
802 except IndexError:
803 pass
806 class _checkmsginputmarker(checkmsg):
808 def check(self, texrunner):
809 try:
810 s1, s2 = texrunner.texmsgparsed.split("PyXInputMarker(%s)" % texrunner.executeid, 1)
811 texrunner.texmsgparsed = s1 + s2
812 except IndexError:
813 raise TexResultError("PyXInputMarker expected", texrunner)
816 class _checkmsgpyxbox(checkmsg):
818 pattern = re.compile(r"PyXBox\(page=(?P<page>\d+),wd=-?\d*((\d\.?)|(\.?\d))\d*pt,ht=-?\d*((\d\.?)|(\.?\d))\d*pt,dp=-?\d*((\d\.?)|(\.?\d))\d*pt\)")
820 def check(self, texrunner):
821 m = self.pattern.search(texrunner.texmsgparsed)
822 if m and m.group("page") == str(texrunner.page):
823 texrunner.texmsgparsed = texrunner.texmsgparsed[:m.start()] + texrunner.texmsgparsed[m.end():]
824 else:
825 raise TexResultError("PyXBox expected", texrunner)
828 class _checkmsgpyxpageout(checkmsg):
830 def check(self, texrunner):
831 try:
832 s1, s2 = texrunner.texmsgparsed.split("[80.121.88.%s]" % texrunner.page, 1)
833 texrunner.texmsgparsed = s1 + s2
834 except IndexError:
835 raise TexResultError("PyXPageOutMarker expected", texrunner)
838 class _checkmsgtexend(checkmsg):
840 auxpattern = re.compile(r"\([^()]*\.aux\)")
841 dvipattern = re.compile(r"Output written on (?P<texfilename>[a-z]+).dvi \((?P<page>\d+) pages?, \d+ bytes\)\.")
843 def check(self, texrunner):
844 m = self.auxpattern.search(texrunner.texmsgparsed)
845 if m:
846 texrunner.texmsgparsed = texrunner.texmsgparsed[:m.start()] + texrunner.texmsgparsed[m.end():]
847 try:
848 s1, s2 = texrunner.texmsgparsed.split("(see the transcript file for additional information)", 1)
849 texrunner.texmsgparsed = s1 + s2
850 except IndexError:
851 pass
852 m = self.dvipattern.search(texrunner.texmsgparsed)
853 if texrunner.page:
854 if not m:
855 raise TexResultError("TeX dvifile messages expected", texrunner)
856 if m.group("page") != str(texrunner.page):
857 raise TexResultError("wrong number of pages reported", texrunner)
858 if m.group("texfilename") != texrunner.texfilename:
859 raise TexResultError("wrong filename of the dvifile reported", texrunner)
860 texrunner.texmsgparsed = texrunner.texmsgparsed[:m.start()] + texrunner.texmsgparsed[m.end():]
861 else:
862 try:
863 s1, s2 = texrunner.texmsgparsed.split("No pages of output.", 1)
864 texrunner.texmsgparsed = s1 + s2
865 except IndexError:
866 raise TexResultError("no dvifile expected")
867 try:
868 s1, s2 = texrunner.texmsgparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
869 texrunner.texmsgparsed = s1 + s2
870 except IndexError:
871 raise TexResultError("TeX logfile message expected")
874 class _checkmsgemptylines(checkmsg):
876 pattern = re.compile(r"^\*?\n", re.M)
878 def check(self, texrunner):
879 m = self.pattern.search(texrunner.texmsgparsed)
880 while m:
881 texrunner.texmsgparsed = texrunner.texmsgparsed[:m.start()] + texrunner.texmsgparsed[m.end():]
882 m = self.pattern.search(texrunner.texmsgparsed)
885 class _checkmsgload(checkmsg):
887 pattern = re.compile(r"\((?P<filename>[^()\s\n]+)[^()]*\)")
889 def baselevels(self, s, maxlevel=1, brackets="()"):
890 level = 0
891 highestlevel = 0
892 res = ""
893 for c in s:
894 if c == brackets[0]:
895 level += 1
896 if level > highestlevel:
897 highestlevel = level
898 if level <= maxlevel:
899 res += c
900 if c == brackets[1]:
901 level -= 1
902 if not level and highestlevel > 0:
903 return res
905 def check(self, texrunner):
906 lowestbracketlevel = self.baselevels(texrunner.texmsgparsed)
907 if lowestbracketlevel is not None:
908 m = self.pattern.search(lowestbracketlevel)
909 while m:
910 if os.access(m.group("filename"), os.R_OK):
911 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
912 else:
913 break
914 m = self.pattern.match(lowestbracketlevel)
915 else:
916 texrunner.texmsgparsed = lowestbracketlevel
919 class _checkmsggraphicsload(_checkmsgload):
921 def baselevels(self, s, brackets="<>", **args):
922 _checkmsgload.baselevels(self, s, brackets=brackets, **args)
926 class _checkmsgignore(_checkmsgload):
928 def check(self, texrunner):
929 texrunner.texmsgparsed = ""
932 checkmsg.start = _checkmsgstart()
933 checkmsg.noaux = _checkmsgnoaux()
934 checkmsg.inputmarker = _checkmsginputmarker()
935 checkmsg.pyxbox = _checkmsgpyxbox()
936 checkmsg.pyxpageout = _checkmsgpyxpageout()
937 checkmsg.texend = _checkmsgtexend()
938 checkmsg.emptylines = _checkmsgemptylines()
939 checkmsg.load = _checkmsgload()
940 checkmsg.graphicsload = _checkmsggraphicsload()
941 checkmsg.ignore = _checkmsgignore()
943 # }}}
946 ###############################################################################
947 # texsettings
948 ############################################################################{{{
951 class halign: # horizontal alignment
953 def __init__(self, hratio):
954 self.hratio = hratio
957 halign.left = halign(0)
958 halign.right = halign(1)
959 halign.center = halign(0.5)
962 class _texsetting: # generic tex settings (modifications of the tex expression)
964 def modifyexpr(self, expr):
965 return expr
968 class valign(_texsetting):
970 def __init__(self, width):
971 self.width_str = width
973 def modifyexpr(self, expr):
974 return r"\%s{\hsize%.5fpt{%s}}" % (self.vkind, unit.topt(self.width_str)*72.27/72.0, expr)
977 class _valigntopline(valign):
979 vkind = "vtop"
982 class _valignbottomline(valign):
984 vkind = "vbox"
987 class _valigncenterline(valign):
989 def __init__(self, heightstr="0", lowerratio=0.5):
990 self.heightstr = heightstr
991 self.lowerratio = lowerratio
993 def modifyexpr(self, expr):
994 return r"\setbox0\hbox{%s}\lower%.5f\ht0\hbox{%s}" % (self.heightstr, self.lowerratio, expr)
997 valign.topline = _valigntopline
998 valign.bottomline = _valignbottomline
999 valign.centerline = _valigncenterline
1002 class _mathmode(_texsetting):
1004 def modifyexpr(self, expr):
1005 return r"\hbox{$\displaystyle{%s}$}" % expr
1007 mathmode = _mathmode()
1010 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
1012 class size(_texsetting):
1014 def __init__(self, expr, sizelist=defaultsizelist):
1015 if helper.isinteger(expr):
1016 if expr >= 0 and expr < sizelist.index(None):
1017 self.size = sizelist[expr]
1018 elif expr < 0 and expr + len(sizelist) > sizelist.index(None):
1019 self.size = sizelist[expr]
1020 else:
1021 raise IndexError("index out of sizelist range")
1022 else:
1023 self.size = expr
1025 def modifyexpr(self, expr):
1026 return r"\%s{%s}" % (self.size, expr)
1028 for s in defaultsizelist:
1029 if s is not None:
1030 size.__dict__[s] = size(s)
1032 # }}}
1035 class _readpipe(threading.Thread):
1037 def __init__(self, pipe, expectqueue, gotevent, gotqueue):
1038 threading.Thread.__init__(self)
1039 self.setDaemon(1)
1040 self.pipe = pipe
1041 self.expectqueue = expectqueue
1042 self.gotevent = gotevent
1043 self.gotqueue = gotqueue
1044 self.start()
1046 def run(self):
1047 read = self.pipe.readline()
1048 while len(read):
1049 read.replace("\r", "")
1050 if not len(read) or read[-1] != "\n":
1051 read += "\n"
1052 self.gotqueue.put(read)
1053 try:
1054 self.expect = self.expectqueue.get_nowait()
1055 except Queue.Empty:
1056 pass
1057 if read.find(self.expect) != -1:
1058 self.gotevent.set()
1059 read = self.pipe.readline()
1060 if self.expect.find("PyXInputMarker") != -1:
1061 raise RuntimeError("TeX finished unexpectedly")
1065 class _textbox(box._rect, base.PSText):
1067 def __init__(self, x, y, left, right, height, depth, texrunner, page):
1068 self.texttrafo = trafo._translate(-left, 0)
1069 box._rect.__init__(self, -left, -depth, left + right, depth + height, abscenter = (left, depth), trafo=trafo._translate(x, y))
1070 self.texrunner = texrunner
1071 self.page = page
1073 def transform(self, trafo):
1074 box._rect.transform(self, trafo)
1075 self.texttrafo = trafo * self.texttrafo
1077 def writefontheader(self, file, containsfonts):
1078 self.texrunner.writefontheader(file, containsfonts)
1080 def write(self, file):
1081 canvas._gsave().write(file) # XXX: canvas?, constructor call needed?
1082 self.texttrafo.write(file)
1083 self.texrunner.write(file, self.page)
1084 canvas._grestore().write(file)
1088 class textbox(_textbox):
1090 def __init__(self, x, y, left, right, height, depth, texrunner, page):
1091 _textbox.__init__(unit.topt(x), unit.topt(y), unit.topt(left), unit.topt(right),
1092 unit.topt(height), unit.topt(depth), texrunner, page)
1096 class TexRunsError(Exception): pass
1097 class TexDoneError(Exception): pass
1098 class TexNotInDefineModeError(Exception): pass
1101 class texrunner(attrlist.attrlist):
1103 def __init__(self, mode="tex",
1104 docclass="article",
1105 docopt=None,
1106 usefiles=[],
1107 waitfortex=5,
1108 texdebug=0,
1109 dvidebug=0,
1110 checkmsgstart=checkmsg.start,
1111 checkmsgdocclass=checkmsg.load,
1112 checkmsgbegindoc=(checkmsg.load, checkmsg.noaux),
1113 checkmsgend=checkmsg.texend,
1114 checkmsgdefaultdefine=(),
1115 checkmsgdefaultrun=()):
1116 self.mode = mode
1117 self.docclass = docclass
1118 self.docopt = docopt
1119 self.usefiles = usefiles
1120 self.waitfortex = waitfortex
1121 self.texdebug = texdebug
1122 self.dvidebug = dvidebug
1123 self.checkmsgstart = helper.ensuresequence(checkmsgstart)
1124 self.checkmsgdocclass = helper.ensuresequence(checkmsgdocclass)
1125 self.checkmsgbegindoc = helper.ensuresequence(checkmsgbegindoc)
1126 self.checkmsgend = helper.ensuresequence(checkmsgend)
1127 self.checkmsgdefaultdefine = helper.ensuresequence(checkmsgdefaultdefine)
1128 self.checkmsgdefaultrun = helper.ensuresequence(checkmsgdefaultrun)
1130 self.texruns = 0
1131 self.texdone = 0
1132 self.definemode = 1
1133 self.executeid = 0
1134 self.page = 0
1135 self.texfilename = "text"
1137 def execute(self, expr, *checks):
1138 if not self.texruns:
1139 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
1140 texfile.write("\\relax\n")
1141 texfile.close()
1142 try:
1143 self.texinput, self.texoutput = os.popen4("%s %s" % (self.mode, self.texfilename), "t", 0)
1144 except ValueError:
1145 # workaround for MS Windows
1146 self.texinput, self.texoutput = os.popen4("%s %s" % (self.mode, self.texfilename), "t")
1147 self.expectqueue = Queue.Queue(1) # allow for a single entry only
1148 self.gotevent = threading.Event()
1149 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
1150 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue)
1151 self.texruns = 1
1152 olddefinemode = self.definemode
1153 self.definemode = 1
1154 self.execute("\\scrollmode\n\\raiseerror%\n" + # switch to and check scrollmode
1155 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" +
1156 "\\newbox\\PyXBox%\n" +
1157 "\\def\\ProcessPyXBox#1#2{%\n" +
1158 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" +
1159 "\\immediate\\write16{PyXBox(page=#2," +
1160 "wd=\\the\\wd\\PyXBox," +
1161 "ht=\\the\\ht\\PyXBox," +
1162 "dp=\\the\\dp\\PyXBox)}%\n" +
1163 "\\ht\\PyXBox0pt%\n" +
1164 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\copy\\PyXBox}}%\n" +
1165 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker(#1)}}",
1166 *self.checkmsgstart)
1167 os.remove("%s.tex" % self.texfilename)
1168 if self.mode == "latex":
1169 if self.docopt is not None:
1170 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass), *self.checkmsgdocclass)
1171 else:
1172 self.execute("\\documentclass{%s}" % self.docclass, *self.checkmsgdocclass)
1173 self.definemode = olddefinemode
1174 self.executeid += 1
1175 if expr is not None:
1176 self.expectqueue.put_nowait("PyXInputMarker(%i)" % self.executeid)
1177 if self.definemode:
1178 self.expr = ("%s%%\n" % expr +
1179 "\\PyXInput{%i}%%\n" % self.executeid)
1180 else:
1181 self.page += 1
1182 self.expr = ("\\ProcessPyXBox{%s}{%i}%%\n" % (expr, self.page) +
1183 "\\PyXInput{%i}%%\n" % self.executeid)
1184 else:
1185 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
1186 if self.mode == "latex":
1187 self.expr = "\\end{document}\n"
1188 else:
1189 self.expr = "\\end\n"
1190 if self.texdebug:
1191 print "pass the following expression to (La)TeX:\n %s" % self.expr.replace("\n", "\n ").rstrip()
1192 self.texinput.write(self.expr)
1193 self.gotevent.wait(self.waitfortex)
1194 nogotevent = not self.gotevent.isSet()
1195 self.gotevent.clear()
1196 try:
1197 self.texmsg = ""
1198 while 1:
1199 self.texmsg += self.gotqueue.get_nowait()
1200 except Queue.Empty:
1201 pass
1202 self.texmsgparsed = self.texmsg
1203 if nogotevent:
1204 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
1205 else:
1206 if expr is not None:
1207 checkmsg.inputmarker.check(self)
1208 if not self.definemode:
1209 checkmsg.pyxbox.check(self)
1210 checkmsg.pyxpageout.check(self)
1211 for check in checks:
1212 try:
1213 check.check(self)
1214 except TexResultWarning:
1215 traceback.print_exc()
1216 checkmsg.emptylines.check(self)
1217 if len(self.texmsgparsed):
1218 raise TexResultError("unhandled TeX response (might be an error)", self)
1219 if expr is None:
1220 self.texruns = 0
1221 self.texdone = 1
1223 def writefontheader(self, file, containsfonts):
1224 if not self.texdone:
1225 _default.execute(None, *self.checkmsgend)
1226 self.dvifile = DVIFile("%s.dvi" % self.texfilename, debug=self.dvidebug)
1227 if self not in containsfonts:
1228 self.dvifile.writeheader(file)
1229 containsfonts.append(self)
1230 # TODO: - containfonts should contain font/glyph information instead of texrunner references
1232 def write(self, file, page):
1233 if not self.texdone:
1234 _default.execute(None, *self.checkmsgend)
1235 self.dvifile = DVIFile("%s.dvi" % self.texfilename)
1236 return self.dvifile.write(file, page)
1238 def settex(self, mode=None, waitfortex=None):
1239 if self.texruns:
1240 raise TexRunsError
1241 if mode is not None:
1242 mode = mode.lower()
1243 if mode != "tex" and mode != "latex":
1244 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1245 self.mode = mode
1246 if waitfortex is not None:
1247 self.waitfortex = waitfortex
1249 def set(self, texdebug=None, dvidebug=None, **args):
1250 if self.texdone:
1251 raise TexDoneError
1252 if texdebug is not None:
1253 self.texdebug = texdebug
1254 if dvidebug is not None:
1255 self.dvidebug = dvidebug
1256 if len(args.keys()):
1257 self.settex(**args)
1259 def bracketcheck(self, expr):
1260 depth = 0
1261 esc = 0
1262 for c in expr:
1263 if c == "{" and not esc:
1264 depth = depth + 1
1265 if c == "}" and not esc:
1266 depth = depth - 1
1267 if depth < 0:
1268 raise ValueError("unmatched '}'")
1269 if c == "\\":
1270 esc = (esc + 1) % 2
1271 else:
1272 esc = 0
1273 if depth > 0:
1274 raise ValueError("unmatched '{'")
1276 def define(self, expr, *args):
1277 if self.texdone:
1278 raise TexDoneError
1279 if not self.definemode:
1280 raise TexNotInDefineModeError
1281 self.bracketcheck(expr)
1282 self.execute(expr, *self.attrgetall(args, checkmsg, default=self.checkmsgdefaultdefine))
1284 PyXBoxPattern = re.compile(r"PyXBox\(page=(?P<page>\d+),wd=(?P<wd>-?\d*((\d\.?)|(\.?\d))\d*)pt,ht=(?P<ht>-?\d*((\d\.?)|(\.?\d))\d*)pt,dp=(?P<dp>-?\d*((\d\.?)|(\.?\d))\d*)pt\)")
1286 def text(self, x, y, expr, *args):
1287 if expr is None:
1288 raise ValueError("None is invalid")
1289 if self.texdone:
1290 raise TexDoneError
1291 if self.definemode:
1292 if self.mode == "latex":
1293 self.execute("\\begin{document}", *self.checkmsgbegindoc)
1294 self.definemode = 0
1295 self.attrcheck(args, allowmulti=(halign, _texsetting, checkmsg, trafo._trafo))
1296 for texsetting in self.attrgetall(args, _texsetting, default=()):
1297 expr = texsetting.modifyexpr(expr)
1298 self.bracketcheck(expr)
1299 self.execute(expr, *self.attrgetall(args, checkmsg, default=self.checkmsgdefaultrun))
1300 match = self.PyXBoxPattern.search(self.texmsg)
1301 if not match or int(match.group("page")) != self.page:
1302 raise TexResultError("box extents not found", self)
1303 width, height, depth = map(lambda x: float(x) * 72.0 / 72.27, match.group("wd", "ht", "dp"))
1304 hratio = self.attrgetall(args, halign, default=(halign.left,))[0].hratio
1305 textbox = _textbox(unit.topt(x), unit.topt(y), hratio * width, (1 - hratio) * width, height, depth, self, self.page)
1306 for t in self.attrgetall(args, trafo._trafo, default=()):
1307 textbox.transform(t)
1308 return textbox
1311 _default = texrunner()
1312 set = _default.set
1313 define = _default.define
1314 text = _default.text
1316 # vim: fdm=marker