some attr reorganization
[PyX/mjg.git] / pyx / text.py
blob3ec67eb817b79e86edf57e808353baf08355c941
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2003 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002-2003 André Wobst <wobsta@users.sourceforge.net>
7 # Copyright (C) 2003 Michael Schindler <m-schindler@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 import copy, cStringIO, exceptions, glob, os, threading, Queue, traceback, re, struct, string, tempfile, sys, atexit, time
26 import config, helper, unit, bbox, box, base, canvas, color, trafo, path, prolog, pykpathsea, version, style, attr
28 class fix_word:
29 def __init__(self, word):
30 if word >= 0:
31 self.sign = 1
32 else:
33 self.sign = -1
35 self.precomma = abs(word) >> 20
36 self.postcomma = abs(word) & 0xFFFFF
38 def __float__(self):
39 return self.sign * (self.precomma + 1.0*self.postcomma/0xFFFFF)
41 def __mul__(self, other):
42 # hey, it's Q&D
43 result = fix_word(0)
45 result.sign = self.sign*other.sign
46 c = self.postcomma*other.precomma + self.precomma*other.postcomma
47 result.precomma = self.precomma*other.precomma + (c >> 20)
48 result.postcomma = c & 0xFFFFF + ((self.postcomma*other.postcomma) >> 40)
49 return result
52 class binfile:
54 def __init__(self, filename, mode="r"):
55 self.file = open(filename, mode)
57 def close(self):
58 self.file.close()
60 def tell(self):
61 return self.file.tell()
63 def eof(self):
64 return self.file.eof()
66 def read(self, bytes):
67 return self.file.read(bytes)
69 def readint(self, bytes=4, signed=0):
70 first = 1
71 result = 0
72 while bytes:
73 value = ord(self.file.read(1))
74 if first and signed and value > 127:
75 value -= 256
76 first = 0
77 result = 256 * result + value
78 bytes -= 1
79 return result
81 def readint32(self):
82 return struct.unpack(">l", self.file.read(4))[0]
84 def readuint32(self):
85 return struct.unpack(">L", self.file.read(4))[0]
87 def readint24(self):
88 # XXX: checkme
89 return struct.unpack(">l", "\0"+self.file.read(3))[0]
91 def readuint24(self):
92 # XXX: checkme
93 return struct.unpack(">L", "\0"+self.file.read(3))[0]
95 def readint16(self):
96 return struct.unpack(">h", self.file.read(2))[0]
98 def readuint16(self):
99 return struct.unpack(">H", self.file.read(2))[0]
101 def readchar(self):
102 return struct.unpack("b", self.file.read(1))[0]
104 def readuchar(self):
105 return struct.unpack("B", self.file.read(1))[0]
107 def readstring(self, bytes):
108 l = self.readuchar()
109 assert l <= bytes-1, "inconsistency in file: string too long"
110 return self.file.read(bytes-1)[:l]
112 class stringbinfile(binfile):
114 def __init__(self, s):
115 self.file = cStringIO.StringIO(s)
118 # class tokenfile:
119 # """ ascii file containing tokens separated by spaces.
121 # Comments beginning with % are ignored. Strings containing spaces
122 # are not handled correctly
123 # """
125 # def __init__(self, filename):
126 # self.file = open(filename, "r")
127 # self.line = None
129 # def gettoken(self):
130 # """ return next token or None if EOF """
131 # while not self.line:
132 # line = self.file.readline()
133 # if line == "":
134 # return None
135 # self.line = line.split("%")[0].split()
136 # token = self.line[0]
137 # self.line = self.line[1:]
138 # return token
140 # def close(self):
141 # self.file.close()
144 ##############################################################################
145 # TFM file handling
146 ##############################################################################
148 class TFMError(exceptions.Exception): pass
151 class char_info_word:
152 def __init__(self, word):
153 self.width_index = int((word & 0xFF000000L) >> 24) #make sign-safe
154 self.height_index = (word & 0x00F00000) >> 20
155 self.depth_index = (word & 0x000F0000) >> 16
156 self.italic_index = (word & 0x0000FC00) >> 10
157 self.tag = (word & 0x00000300) >> 8
158 self.remainder = (word & 0x000000FF)
161 class tfmfile:
162 def __init__(self, name, debug=0):
163 self.file = binfile(name, "rb")
164 self.debug = debug
167 # read pre header
170 self.lf = self.file.readint16()
171 self.lh = self.file.readint16()
172 self.bc = self.file.readint16()
173 self.ec = self.file.readint16()
174 self.nw = self.file.readint16()
175 self.nh = self.file.readint16()
176 self.nd = self.file.readint16()
177 self.ni = self.file.readint16()
178 self.nl = self.file.readint16()
179 self.nk = self.file.readint16()
180 self.ne = self.file.readint16()
181 self.np = self.file.readint16()
183 if not (self.bc-1 <= self.ec <= 255 and
184 self.ne <= 256 and
185 self.lf == 6+self.lh+(self.ec-self.bc+1)+self.nw+self.nh+self.nd
186 +self.ni+self.nl+self.nk+self.ne+self.np):
187 raise TFMError, "error in TFM pre-header"
189 if debug:
190 print "lh=%d" % self.lh
193 # read header
196 self.checksum = self.file.readint32()
197 self.designsizeraw = self.file.readint32()
198 assert self.designsizeraw > 0, "invald design size"
199 self.designsize = fix_word(self.designsizeraw)
200 if self.lh > 2:
201 assert self.lh > 11, "inconsistency in TFM file: incomplete field"
202 self.charcoding = self.file.readstring(40)
203 else:
204 self.charcoding = None
206 if self.lh > 12:
207 assert self.lh > 16, "inconsistency in TFM file: incomplete field"
208 self.fontfamily = self.file.readstring(20)
209 else:
210 self.fontfamily = None
212 if self.debug:
213 print "(FAMILY %s)" % self.fontfamily
214 print "(CODINGSCHEME %s)" % self.charcoding
215 print "(DESINGSIZE R %f)" % self.designsize
217 if self.lh > 17:
218 self.sevenbitsave = self.file.readuchar()
219 # ignore the following two bytes
220 self.file.readint16()
221 facechar = self.file.readuchar()
222 # decode ugly face specification into the Knuth suggested string
223 if facechar < 18:
224 if facechar >= 12:
225 self.face = "E"
226 facechar -= 12
227 elif facechar >= 6:
228 self.face = "C"
229 facechar -= 6
230 else:
231 self.face = "R"
233 if facechar >= 4:
234 self.face = "L" + self.face
235 facechar -= 4
236 elif facechar >= 2:
237 self.face = "B" + self.face
238 facechar -= 2
239 else:
240 self.face = "M" + self.face
242 if facechar == 1:
243 self.face = self.face[0] + "I" + self.face[1]
244 else:
245 self.face = self.face[0] + "R" + self.face[1]
247 else:
248 self.face = None
249 else:
250 self.sevenbitsave = self.face = None
252 if self.lh > 18:
253 # just ignore the rest
254 print self.file.read((self.lh-18)*4)
257 # read char_info
260 self.char_info = [None]*(self.ec+1)
262 for charcode in range(self.bc, self.ec+1):
263 self.char_info[charcode] = char_info_word(self.file.readint32())
264 if self.char_info[charcode].width_index == 0:
265 # disable character if width_index is zero
266 self.char_info[charcode] = None
269 # read widths
272 self.width = [None for width_index in range(self.nw)]
273 for width_index in range(self.nw):
274 # self.width[width_index] = fix_word(self.file.readint32())
275 self.width[width_index] = self.file.readint32()
278 # read heights
281 self.height = [None for height_index in range(self.nh)]
282 for height_index in range(self.nh):
283 # self.height[height_index] = fix_word(self.file.readint32())
284 self.height[height_index] = self.file.readint32()
287 # read depths
290 self.depth = [None for depth_index in range(self.nd)]
291 for depth_index in range(self.nd):
292 # self.depth[depth_index] = fix_word(self.file.readint32())
293 self.depth[depth_index] = self.file.readint32()
296 # read italic
299 self.italic = [None for italic_index in range(self.ni)]
300 for italic_index in range(self.ni):
301 # self.italic[italic_index] = fix_word(self.file.readint32())
302 self.italic[italic_index] = self.file.readint32()
305 # read lig_kern
308 # XXX decode to lig_kern_command
310 self.lig_kern = [None for lig_kern_index in range(self.nl)]
311 for lig_kern_index in range(self.nl):
312 self.lig_kern[lig_kern_index] = self.file.readint32()
315 # read kern
318 self.kern = [None for kern_index in range(self.nk)]
319 for kern_index in range(self.nk):
320 # self.kern[kern_index] = fix_word(self.file.readint32())
321 self.kern[kern_index] = self.file.readint32()
324 # read exten
327 # XXX decode to extensible_recipe
329 self.exten = [None for exten_index in range(self.ne)]
330 for exten_index in range(self.ne):
331 self.exten[exten_index] = self.file.readint32()
334 # read param
337 # XXX decode
339 self.param = [None for param_index in range(self.np)]
340 for param_index in range(self.np):
341 self.param[param_index] = self.file.readint32()
343 self.file.close()
346 # class FontEncoding:
348 # def __init__(self, filename):
349 # """ font encoding contained in filename """
350 # encpath = pykpathsea.find_file(filename, pykpathsea.kpse_tex_ps_header_format)
351 # encfile = tokenfile(encpath)
353 # # name of encoding
354 # self.encname = encfile.gettoken()
355 # token = encfile.gettoken()
356 # if token != "[":
357 # raise RuntimeError("cannot parse encoding file '%s', expecting '[' got '%s'" % (filename, token))
358 # self.encvector = []
359 # for i in range(256):
360 # token = encfile.gettoken()
361 # if token is None or token=="]":
362 # raise RuntimeError("not enough charcodes in encoding file '%s'" % filename)
363 # self.encvector.append(token)
364 # if encfile.gettoken() != "]":
365 # raise RuntimeError("too many charcodes in encoding file '%s'" % filename)
366 # token = encfile.gettoken()
367 # if token != "def":
368 # raise RuntimeError("cannot parse encoding file '%s', expecting 'def' got '%s'" % (filename, token))
369 # token = encfile.gettoken()
370 # if token != None:
371 # raise RuntimeError("encoding file '%s' too long" % filename)
372 # encfile.close()
374 # def encode(self, charcode):
375 # return self.encvector[charcode]
377 ##############################################################################
378 # Font handling
379 ##############################################################################
381 _ReEncodeFont = prolog.definition("ReEncodeFont", """{
382 5 dict
383 begin
384 /newencoding exch def
385 /newfontname exch def
386 /basefontname exch def
387 /basefontdict basefontname findfont def
388 /newfontdict basefontdict maxlength dict def
389 basefontdict {
390 exch dup dup /FID ne exch /Encoding ne and
391 { exch newfontdict 3 1 roll put }
392 { pop pop }
393 ifelse
394 } forall
395 newfontdict /FontName newfontname put
396 newfontdict /Encoding newencoding put
397 newfontname newfontdict definefont pop
399 }""")
402 # PostScript font selection and output primitives
405 class _selectfont(base.PSOp):
406 def __init__(self, name, size):
407 self.name = name
408 self.size = size
410 def write(self, file):
411 file.write("/%s %f selectfont\n" % (self.name, self.size))
414 class selectfont(_selectfont):
415 def __init__(self, font):
416 _selectfont.__init__(self, font.getpsname(), font.getsize())
417 self.font = font
419 def prolog(self):
420 result = [prolog.fontdefinition(self.font.getbasepsname(),
421 self.font.getfontfile(),
422 self.font.getencodingfile(),
423 self.font.usedchars)]
424 if self.font.getencoding():
425 result.append(_ReEncodeFont)
426 result.append(prolog.fontencoding(self.font.getencoding(), self.font.getencodingfile()))
427 result.append(prolog.fontreencoding(self.font.getpsname(), self.font.getbasepsname(), self.font.getencoding()))
428 return result
431 class _show(base.PSOp):
432 def __init__(self, x, y, s):
433 self.x = x
434 self.y = y
435 self.s = s
437 def write(self, file):
438 file.write("%f %f moveto (%s) show\n" % (self.x, self.y, self.s))
441 class fontmapping:
443 tokenpattern = re.compile(r'"(.*?)("\s+|"$|$)|(.*?)(\s+|$)')
445 def __init__(self, s):
446 """ construct font mapping from line s of font mapping file """
447 self.texname = self.basepsname = self.fontfile = None
449 # standard encoding
450 self.encodingfile = None
452 # supported postscript fragments occuring in psfonts.map
453 self.reencodefont = self.extendfont = self.slantfont = None
455 tokens = []
456 while len(s):
457 match = self.tokenpattern.match(s)
458 if match:
459 if match.groups()[0]:
460 tokens.append('"%s"' % match.groups()[0])
461 else:
462 tokens.append(match.groups()[2])
463 s = s[match.end():]
464 else:
465 raise RuntimeError("wrong syntax")
467 for token in tokens:
468 if token.startswith("<"):
469 if token.startswith("<<"):
470 # XXX: support non-partial download here
471 self.fontfile = token[2:]
472 elif token.startswith("<["):
473 self.encodingfile = token[2:]
474 elif token.endswith(".pfa") or token.endswith(".pfb"):
475 self.fontfile = token[1:]
476 elif token.endswith(".enc"):
477 self.encodingfile = token[1:]
478 else:
479 raise RuntimeError("wrong syntax")
480 elif token.startswith('"'):
481 pscode = token[1:-1].split()
482 # parse standard postscript code fragments
483 while pscode:
484 try:
485 arg, cmd = pscode[:2]
486 except:
487 raise RuntimeError("Unsupported Postscript fragment '%s'" % pscode)
488 pscode = pscode[2:]
489 if cmd == "ReEncodeFont":
490 self.reencodefont = arg
491 elif cmd == "ExtendFont":
492 self.extendfont = arg
493 elif cmd == "SlantFont":
494 self.slantfont = arg
495 else:
496 raise RuntimeError("Unsupported Postscript fragment '%s %s'" % (arg, cmd))
497 else:
498 if self.texname is None:
499 self.texname = token
500 else:
501 self.basepsname = token
502 if self.basepsname is None:
503 self.basepsname = self.texname
505 def __str__(self):
506 return ("'%s' is '%s' read from '%s' encoded as '%s'" %
507 (self.texname, self.basepsname, self.fontfile, repr(self.encodingfile)))
509 # generate fontmap
511 def readfontmap(filenames):
512 """ read font map from filename (without path) """
513 fontmap = {}
514 for filename in filenames:
515 mappath = pykpathsea.find_file(filename, pykpathsea.kpse_dvips_config_format)
516 if not mappath:
517 raise RuntimeError("cannot find font mapping file '%s'" % filename)
518 mapfile = open(mappath, "r")
519 lineno = 0
520 for line in mapfile.readlines():
521 lineno += 1
522 line = line.rstrip()
523 if not (line=="" or line[0] in (" ", "%", "*", ";" , "#")):
524 try:
525 fm = fontmapping(line)
526 except RuntimeError, e:
527 sys.stderr.write("*** PyX Warning: Ignoring line %i in mapping file '%s': %s\n" % (lineno, filename, e))
528 else:
529 fontmap[fm.texname] = fm
530 mapfile.close()
531 return fontmap
534 class font:
535 def __init__(self, name, c, q, d, debug=0):
536 self.name = name
537 self.q = q # desired size of font (fix_word) in tex points
538 self.d = d # design size of font (fix_word) in tex points
539 tfmpath = pykpathsea.find_file("%s.tfm" % self.name, pykpathsea.kpse_tfm_format)
540 if not tfmpath:
541 raise TFMError("cannot find %s.tfm" % self.name)
542 self.tfmfile = tfmfile(tfmpath, debug)
544 # We only check for equality of font checksums if none of them
545 # is zero. The case c == 0 happend in some VF files and
546 # according to the VFtoVP documentation, paragraph 40, a check
547 # is only performed if tfmfile.checksum > 0. Anyhow, begin
548 # more generous here seems to be reasonable
549 if self.tfmfile.checksum != c and self.tfmfile.checksum != 0 and c !=0:
550 raise DVIError("check sums do not agree: %d vs. %d" %
551 (self.tfmfile.checksum, c))
553 # tfmfile.designsizeraw is the design size of the font as a fix_word
554 if abs(self.tfmfile.designsizeraw - d) > 2:
555 raise DVIError("design sizes do not agree: %d vs. %d" % (tfmdesignsize, d))
556 if q < 0 or q > 134217728:
557 raise DVIError("font '%s' not loaded: bad scale" % self.name)
558 if d < 0 or d > 134217728:
559 raise DVIError("font '%s' not loaded: bad design size" % self.name)
561 self.scale = 1.0*q/d
563 # for bookkeeping of used characters
564 self.usedchars = [0] * 256
566 def __str__(self):
567 return "font %s designed at %g tex pts used at %g tex pts" % (self.name,
568 16.0*self.d/16777216L,
569 16.0*self.q/16777216L)
572 __repr__ = __str__
574 def getsize(self):
575 """ return size of font in (PS) points """
576 # The factor 16L/16777216L=2**(-20) converts a fix_word (here self.q)
577 # to the corresponding float. Furthermore, we have to convert from TeX
578 # points to points, hence the factor 72/72.27.
579 return 16L*self.q/16777216L*72/72.27
581 def _convert(self, width):
582 return 16L*width*self.q/16777216L
584 def getwidth(self, charcode):
585 return self._convert(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])
587 def getheight(self, charcode):
588 return self._convert(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])
590 def getdepth(self, charcode):
591 return self._convert(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])
593 def getitalic(self, charcode):
594 return self._convert(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])
596 def markcharused(self, charcode):
597 self.usedchars[charcode] = 1
599 def mergeusedchars(self, otherfont):
600 for i in range(len(self.usedchars)):
601 self.usedchars[i] = self.usedchars[i] or otherfont.usedchars[i]
603 def clearusedchars(self):
604 self.usedchars = [0] * 256
607 class type1font(font):
608 def __init__(self, name, c, q, d, fontmap, debug=0):
609 font.__init__(self, name, c, q, d, debug)
610 self.fontmapping = fontmap.get(name)
611 if self.fontmapping is None:
612 raise RuntimeError("no information for font '%s' found in font mapping file, aborting" % name)
614 def getbasepsname(self):
615 return self.fontmapping.basepsname
617 def getpsname(self):
618 if self.fontmapping.reencodefont:
619 return "%s-%s" % (self.fontmapping.basepsname, self.fontmapping.reencodefont)
620 else:
621 return self.fontmapping.basepsname
623 def getfontfile(self):
624 return self.fontmapping.fontfile
626 def getencoding(self):
627 return self.fontmapping.reencodefont
629 def getencodingfile(self):
630 return self.fontmapping.encodingfile
633 class virtualfont(font):
634 def __init__(self, name, c, q, d, fontmap, debug=0):
635 font.__init__(self, name, c, q, d, debug)
636 fontpath = pykpathsea.find_file(name, pykpathsea.kpse_vf_format)
637 if fontpath is None or not len(fontpath):
638 raise RuntimeError
639 self.vffile = vffile(fontpath, self.scale, fontmap, debug > 1)
641 def getfonts(self):
642 """ return fonts used in virtual font itself """
643 return self.vffile.getfonts()
645 def getchar(self, cc):
646 """ return dvi chunk corresponding to char code cc """
647 return self.vffile.getchar(cc)
650 ##############################################################################
651 # DVI file handling
652 ##############################################################################
654 _DVI_CHARMIN = 0 # typeset a character and move right (range min)
655 _DVI_CHARMAX = 127 # typeset a character and move right (range max)
656 _DVI_SET1234 = 128 # typeset a character and move right
657 _DVI_SETRULE = 132 # typeset a rule and move right
658 _DVI_PUT1234 = 133 # typeset a character
659 _DVI_PUTRULE = 137 # typeset a rule
660 _DVI_NOP = 138 # no operation
661 _DVI_BOP = 139 # beginning of page
662 _DVI_EOP = 140 # ending of page
663 _DVI_PUSH = 141 # save the current positions (h, v, w, x, y, z)
664 _DVI_POP = 142 # restore positions (h, v, w, x, y, z)
665 _DVI_RIGHT1234 = 143 # move right
666 _DVI_W0 = 147 # move right by w
667 _DVI_W1234 = 148 # move right and set w
668 _DVI_X0 = 152 # move right by x
669 _DVI_X1234 = 153 # move right and set x
670 _DVI_DOWN1234 = 157 # move down
671 _DVI_Y0 = 161 # move down by y
672 _DVI_Y1234 = 162 # move down and set y
673 _DVI_Z0 = 166 # move down by z
674 _DVI_Z1234 = 167 # move down and set z
675 _DVI_FNTNUMMIN = 171 # set current font (range min)
676 _DVI_FNTNUMMAX = 234 # set current font (range max)
677 _DVI_FNT1234 = 235 # set current font
678 _DVI_SPECIAL1234 = 239 # special (dvi extention)
679 _DVI_FNTDEF1234 = 243 # define the meaning of a font number
680 _DVI_PRE = 247 # preamble
681 _DVI_POST = 248 # postamble beginning
682 _DVI_POSTPOST = 249 # postamble ending
684 _DVI_VERSION = 2 # dvi version
686 # position variable indices
687 _POS_H = 0
688 _POS_V = 1
689 _POS_W = 2
690 _POS_X = 3
691 _POS_Y = 4
692 _POS_Z = 5
694 # reader states
695 _READ_PRE = 1
696 _READ_NOPAGE = 2
697 _READ_PAGE = 3
698 _READ_POST = 4 # XXX not used
699 _READ_POSTPOST = 5 # XXX not used
700 _READ_DONE = 6
703 class DVIError(exceptions.Exception): pass
705 # save and restore colors
707 class _savecolor(base.PSOp):
708 def write(self, file):
709 file.write("currentcolor currentcolorspace\n")
712 class _restorecolor(base.PSOp):
713 def write(self, file):
714 file.write("setcolorspace setcolor\n")
716 class _savetrafo(base.PSOp):
717 def write(self, file):
718 file.write("matrix currentmatrix\n")
721 class _restoretrafo(base.PSOp):
722 def write(self, file):
723 file.write("setmatrix\n")
726 class dvifile:
728 def __init__(self, filename, fontmap, debug=0):
729 self.filename = filename
730 self.fontmap = fontmap
731 self.debug = debug
733 # helper routines
735 def flushout(self):
736 """ flush currently active string """
737 if self.actoutstart:
738 x = self.actoutstart[0] * self.conv
739 y = -self.actoutstart[1] * self.conv
740 if self.debug:
741 print "[%s]" % self.actoutstring
742 self.actpage.insert(_show(x, y, self.actoutstring))
743 self.actoutstart = None
745 def putrule(self, height, width, inch=1):
746 self.flushout()
747 x1 = self.pos[_POS_H] * self.conv
748 y1 = -self.pos[_POS_V] * self.conv
749 w = width * self.conv
750 h = height * self.conv
752 if height > 0 and width > 0:
753 if self.debug:
754 pixelw = int(width*self.trueconv*self.mag/1000.0)
755 if pixelw < width*self.conv: pixelw += 1
756 pixelh = int(height*self.trueconv*self.mag/1000.0)
757 if pixelh < height*self.conv: pixelh += 1
759 print ("%d: %srule height %d, width %d (%dx%d pixels)" %
760 (self.filepos, inch and "set" or "put", height, width, pixelh, pixelw))
761 self.actpage.fill(path._rect(x1, y1, w, h))
762 else:
763 if self.debug:
764 print ("%d: %srule height %d, width %d (invisible)" %
765 (self.filepos, inch and "set" or "put", height, width))
767 if inch:
768 if self.debug:
769 print (" h:=%d+%d=%d, hh:=%d" %
770 (self.pos[_POS_H], width, self.pos[_POS_H]+width, 0))
771 self.pos[_POS_H] += width
773 def putchar(self, char, inch=1):
774 dx = inch and int(round(self.activefont.getwidth(char)*self.tfmconv)) or 0
776 if self.debug:
777 print ("%d: %schar%d h:=%d+%d=%d, hh:=%d" %
778 (self.filepos,
779 inch and "set" or "put",
780 char,
781 self.pos[_POS_H], dx, self.pos[_POS_H]+dx,
784 if isinstance(self.activefont, type1font):
785 if self.actoutstart is None:
786 self.actoutstart = self.pos[_POS_H], self.pos[_POS_V]
787 self.actoutstring = ""
788 if char > 32 and char < 127 and chr(char) not in "()[]<>":
789 ascii = "%s" % chr(char)
790 else:
791 ascii = "\\%03o" % char
792 self.actoutstring = self.actoutstring + ascii
794 self.activefont.markcharused(char)
795 self.pos[_POS_H] += dx
796 else:
797 # virtual font handling
798 afterpos = list(self.pos)
799 afterpos[_POS_H] += dx
800 self._push_dvistring(self.activefont.getchar(char), self.activefont.getfonts(), afterpos,
801 self.activefont.getsize())
804 if not inch:
805 # XXX: correct !?
806 self.flushout()
808 def usefont(self, fontnum):
809 if self.debug:
810 print ("%d: fntnum%i current font is %s" %
811 (self.filepos,
812 fontnum, self.fonts[fontnum].name))
814 self.activefont = self.fonts[fontnum]
816 # if the new font is a type 1 font and if it is not already the
817 # font being currently used for the output, we have to flush the
818 # output and insert a selectfont statement in the PS code
819 if isinstance(self.activefont, type1font) and self.actoutfont!=self.activefont:
820 self.flushout()
821 self.actpage.insert(selectfont(self.activefont))
822 self.actoutfont = self.activefont
824 def definefont(self, cmdnr, num, c, q, d, fontname):
825 # cmdnr: type of fontdef command (only used for debugging output)
826 # c: checksum
827 # q: scaling factor (fix_word)
828 # Note that q is actually s in large parts of the documentation.
829 # d: design size (fix_word)
831 try:
832 font = virtualfont(fontname, c, q/self.tfmconv, d/self.tfmconv, self.fontmap, self.debug > 1)
833 except (TypeError, RuntimeError):
834 font = type1font(fontname, c, q/self.tfmconv, d/self.tfmconv, self.fontmap, self.debug > 1)
836 self.fonts[num] = font
838 if self.debug:
839 print "%d: fntdef%d %i: %s" % (self.filepos, cmdnr, num, fontname)
841 # scale = round((1000.0*self.conv*q)/(self.trueconv*d))
842 # m = 1.0*q/d
843 # scalestring = scale!=1000 and " scaled %d" % scale or ""
844 # print ("Font %i: %s%s---loaded at size %d DVI units" %
845 # (num, fontname, scalestring, q))
846 # if scale!=1000:
847 # print " (this font is magnified %d%%)" % round(scale/10)
849 def special(self, s):
850 self.flushout()
852 # it is in general not safe to continue using the currently active font because
853 # the specials may involve some gsave/grestore operations
854 # XXX: reset actoutfont only where strictly needed
855 self.actoutfont = None
857 x = self.pos[_POS_H] * self.conv
858 y = -self.pos[_POS_V] * self.conv
859 if self.debug:
860 print "%d: xxx '%s'" % (self.filepos, s)
861 if not s.startswith("PyX:"):
862 if s.startswith("Warning:"):
863 sys.stderr.write("*** PyX Warning: ignoring special '%s'\n" % s)
864 return
865 else:
866 raise RuntimeError("the special '%s' cannot be handled by PyX, aborting" % s)
867 command, args = s[4:].split()[0], s[4:].split()[1:]
868 if command=="color_begin":
869 if args[0]=="cmyk":
870 c = color.cmyk(float(args[1]), float(args[2]), float(args[3]), float(args[4]))
871 elif args[0]=="gray":
872 c = color.gray(float(args[1]))
873 elif args[0]=="hsb":
874 c = color.hsb(float(args[1]), float(args[2]), float(args[3]))
875 elif args[0]=="rgb":
876 c = color.rgb(float(args[1]), float(args[2]), float(args[3]))
877 elif args[0]=="RGB":
878 c = color.rgb(int(args[1])/255.0, int(args[2])/255.0, int(args[3])/255.0)
879 elif args[0]=="texnamed":
880 try:
881 c = getattr(color.cmyk, args[1])
882 except AttributeError:
883 raise RuntimeError("unknown TeX color '%s', aborting" % args[1])
884 else:
885 raise RuntimeError("color model '%s' cannot be handled by PyX, aborting" % args[0])
886 self.actpage.insert(_savecolor())
887 self.actpage.insert(c)
888 elif command=="color_end":
889 self.actpage.insert(_restorecolor())
890 elif command=="rotate_begin":
891 self.actpage.insert(_savetrafo())
892 self.actpage.insert(trafo._rotate(float(args[0]), x, y))
893 elif command=="rotate_end":
894 self.actpage.insert(_restoretrafo())
895 elif command=="scale_begin":
896 self.actpage.insert(_savetrafo())
897 self.actpage.insert(trafo._scale(float(args[0]), float(args[1]), x, y))
898 elif command=="scale_end":
899 self.actpage.insert(_restoretrafo())
900 elif command=="epsinclude":
901 # XXX: we cannot include epsfile in the header because this would
902 # generate a cyclic import with the canvas and text modules
903 import epsfile
905 # parse arguments
906 argdict = {}
907 for arg in args:
908 name, value = arg.split("=")
909 argdict[name] = value
911 # construct kwargs for epsfile constructor
912 epskwargs = {}
913 epskwargs["filename"] = argdict["file"]
914 epskwargs["bbox"] = bbox._bbox(float(argdict["llx"]), float(argdict["lly"]),
915 float(argdict["urx"]), float(argdict["ury"]))
916 if argdict.has_key("width"):
917 epskwargs["width"] = unit.t_pt(float(argdict["width"]))
918 if argdict.has_key("height"):
919 epskwargs["height"] = unit.t_pt(float(argdict["height"]))
920 if argdict.has_key("clip"):
921 epskwargs["clip"] = int(argdict["clip"])
922 self.actpage.insert(epsfile.epsfile(unit.t_pt(x), unit.t_pt(y), **epskwargs))
923 elif command=="marker":
924 if len(args) != 1:
925 raise RuntimeError("marker contains spaces")
926 for c in args[0]:
927 if c not in string.digits + string.letters + "@":
928 raise RuntimeError("marker contains invalid characters")
929 if self.actpage.markers.has_key(args[0]):
930 raise RuntimeError("marker name occurred several times")
931 self.actpage.markers[args[0]] = unit.t_pt(x), unit.t_pt(y)
932 else:
933 raise RuntimeError("unknown PyX special '%s', aborting" % command)
935 # routines for pushing and popping different dvi chunks on the reader
937 def _push_dvistring(self, dvi, fonts, afterpos, fontsize):
938 """ push dvi string with defined fonts on top of reader
939 stack. Every positions gets scaled relatively by the factor
940 scale. When finished with interpreting the dvi chunk, we
941 continue with self.pos=afterpos. The designsize of the virtual
942 font is passed as a fix_word
946 if self.debug:
947 print "executing new dvi chunk"
948 self.statestack.append((self.file, self.fonts, self.activefont, afterpos, self.stack, self.conv, self.tfmconv))
950 # units in vf files are relative to the size of the font and given as fix_words
951 # which can be converted to floats by diving by 2**20
952 oldconv = self.conv
953 self.conv = fontsize/2**20
954 rescale = self.conv/oldconv
956 self.file = stringbinfile(dvi)
957 self.fonts = fonts
958 self.stack = []
959 self.filepos = 0
961 # we have to rescale self.pos in order to be consistent with the new scaling
962 self.pos = map(lambda x, rescale=rescale:1.0*x/rescale, self.pos)
964 # since tfmconv converts from tfm units to dvi units, we have to rescale it as well
965 self.tfmconv /= rescale
967 self.usefont(0)
969 def _pop_dvistring(self):
970 self.flushout()
971 if self.debug:
972 print "finished executing dvi chunk"
973 self.file.close()
974 self.file, self.fonts, self.activefont, self.pos, self.stack, self.conv, self.tfmconv = self.statestack.pop()
976 # routines corresponding to the different reader states of the dvi maschine
978 def _read_pre(self):
979 file = self.file
980 while 1:
981 self.filepos = file.tell()
982 cmd = file.readuchar()
983 if cmd == _DVI_NOP:
984 pass
985 elif cmd == _DVI_PRE:
986 if self.file.readuchar() != _DVI_VERSION: raise DVIError
987 num = file.readuint32()
988 den = file.readuint32()
989 self.mag = file.readuint32()
991 # for the interpretation of all quantities, two conversion factors
992 # are relevant:
993 # - self.tfmconv (tfm units->dvi units)
994 # - self.conv (dvi units-> (PostScript) points)
996 self.tfmconv = (25400000.0/num)*(den/473628672.0)/16.0
998 # calculate self.conv as described in the DVIType docu
1000 # resolution in dpi
1001 self.resolution = 300.0
1002 # self.trueconv = conv in DVIType docu
1003 self.trueconv = (num/254000.0)*(self.resolution/den)
1005 # self.conv is the conversion factor from the dvi units
1006 # to (PostScript) points. It consists of
1007 # - self.mag/1000.0: magstep scaling
1008 # - self.trueconv: conversion from dvi units to pixels
1009 # - 1/self.resolution: conversion from pixels to inch
1010 # - 72 : conversion from inch to points
1011 self.conv = self.mag/1000.0*self.trueconv/self.resolution*72
1013 comment = file.read(file.readuchar())
1014 return _READ_NOPAGE
1015 else:
1016 raise DVIError
1018 def _read_nopage(self):
1019 file = self.file
1020 while 1:
1021 self.filepos = file.tell()
1022 cmd = file.readuchar()
1023 if cmd == _DVI_NOP:
1024 pass
1025 elif cmd == _DVI_BOP:
1026 self.flushout()
1027 pagenos = [file.readuint32() for i in range(10)]
1028 if pagenos[:3] != [ord("P"), ord("y"), ord("X")] or pagenos[4:] != [0, 0, 0, 0, 0, 0]:
1029 raise DVIError("Page in dvi file is not a PyX page.")
1030 if self.debug:
1031 print "%d: beginning of page %i" % (self.filepos, pagenos[0])
1032 file.readuint32()
1033 return _READ_PAGE
1034 elif cmd == _DVI_POST:
1035 return _READ_DONE # we skip the rest
1036 else:
1037 raise DVIError
1039 def _read_page(self):
1040 self.pages.append(canvas.canvas())
1041 self.actpage = self.pages[-1]
1042 self.actpage.markers = {}
1043 self.pos = [0, 0, 0, 0, 0, 0]
1044 self.actoutfont = None
1046 # Since we do not know, which dvi pages the actual PS file contains later on,
1047 # we have to keep track of used char informations separately for each dvi page.
1048 # In order to do so, the already defined fonts have to be copied and their
1049 # used char informations have to be reset
1050 for nr in self.fonts.keys():
1051 self.fonts[nr] = copy.copy(self.fonts[nr])
1052 self.fonts[nr].clearusedchars()
1054 while 1:
1055 file = self.file
1056 self.filepos = file.tell()
1057 try:
1058 cmd = file.readuchar()
1059 except struct.error:
1060 # we most probably (if the dvi file is not corrupt) hit the end of a dvi chunk,
1061 # so we have to continue with the rest of the dvi file
1062 self._pop_dvistring()
1063 continue
1064 if cmd == _DVI_NOP:
1065 pass
1066 if cmd >= _DVI_CHARMIN and cmd <= _DVI_CHARMAX:
1067 self.putchar(cmd)
1068 elif cmd >= _DVI_SET1234 and cmd < _DVI_SET1234 + 4:
1069 self.putchar(file.readint(cmd - _DVI_SET1234 + 1))
1070 elif cmd == _DVI_SETRULE:
1071 self.putrule(file.readint32(), file.readint32())
1072 elif cmd >= _DVI_PUT1234 and cmd < _DVI_PUT1234 + 4:
1073 self.putchar(file.readint(cmd - _DVI_PUT1234 + 1), inch=0)
1074 elif cmd == _DVI_PUTRULE:
1075 self.putrule(file.readint32(), file.readint32(), 0)
1076 elif cmd == _DVI_EOP:
1077 self.flushout()
1078 if self.debug:
1079 print "%d: eop" % self.filepos
1080 print
1081 return _READ_NOPAGE
1082 elif cmd == _DVI_PUSH:
1083 self.stack.append(list(self.pos))
1084 if self.debug:
1085 print "%d: push" % self.filepos
1086 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
1087 (( len(self.stack)-1,)+tuple(self.pos)))
1088 elif cmd == _DVI_POP:
1089 self.flushout()
1090 self.pos = self.stack.pop()
1091 if self.debug:
1092 print "%d: pop" % self.filepos
1093 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
1094 (( len(self.stack),)+tuple(self.pos)))
1095 elif cmd >= _DVI_RIGHT1234 and cmd < _DVI_RIGHT1234 + 4:
1096 self.flushout()
1097 dh = file.readint(cmd - _DVI_RIGHT1234 + 1, 1)
1098 if self.debug:
1099 print ("%d: right%d %d h:=%d%+d=%d, hh:=" %
1100 (self.filepos,
1101 cmd - _DVI_RIGHT1234 + 1,
1103 self.pos[_POS_H],
1105 self.pos[_POS_H]+dh))
1106 self.pos[_POS_H] += dh
1107 elif cmd == _DVI_W0:
1108 self.flushout()
1109 if self.debug:
1110 print ("%d: w0 %d h:=%d%+d=%d, hh:=" %
1111 (self.filepos,
1112 self.pos[_POS_W],
1113 self.pos[_POS_H],
1114 self.pos[_POS_W],
1115 self.pos[_POS_H]+self.pos[_POS_W]))
1116 self.pos[_POS_H] += self.pos[_POS_W]
1117 elif cmd >= _DVI_W1234 and cmd < _DVI_W1234 + 4:
1118 self.flushout()
1119 self.pos[_POS_W] = file.readint(cmd - _DVI_W1234 + 1, 1)
1120 if self.debug:
1121 print ("%d: w%d %d h:=%d%+d=%d, hh:=" %
1122 (self.filepos,
1123 cmd - _DVI_W1234 + 1,
1124 self.pos[_POS_W],
1125 self.pos[_POS_H],
1126 self.pos[_POS_W],
1127 self.pos[_POS_H]+self.pos[_POS_W]))
1128 self.pos[_POS_H] += self.pos[_POS_W]
1129 elif cmd == _DVI_X0:
1130 self.flushout()
1131 self.pos[_POS_H] += self.pos[_POS_X]
1132 elif cmd >= _DVI_X1234 and cmd < _DVI_X1234 + 4:
1133 self.flushout()
1134 self.pos[_POS_X] = file.readint(cmd - _DVI_X1234 + 1, 1)
1135 self.pos[_POS_H] += self.pos[_POS_X]
1136 elif cmd >= _DVI_DOWN1234 and cmd < _DVI_DOWN1234 + 4:
1137 self.flushout()
1138 dv = file.readint(cmd - _DVI_DOWN1234 + 1, 1)
1139 if self.debug:
1140 print ("%d: down%d %d v:=%d%+d=%d, vv:=" %
1141 (self.filepos,
1142 cmd - _DVI_DOWN1234 + 1,
1144 self.pos[_POS_V],
1146 self.pos[_POS_V]+dv))
1147 self.pos[_POS_V] += dv
1148 elif cmd == _DVI_Y0:
1149 self.flushout()
1150 if self.debug:
1151 print ("%d: y0 %d v:=%d%+d=%d, vv:=" %
1152 (self.filepos,
1153 self.pos[_POS_Y],
1154 self.pos[_POS_V],
1155 self.pos[_POS_Y],
1156 self.pos[_POS_V]+self.pos[_POS_Y]))
1157 self.pos[_POS_V] += self.pos[_POS_Y]
1158 elif cmd >= _DVI_Y1234 and cmd < _DVI_Y1234 + 4:
1159 self.flushout()
1160 self.pos[_POS_Y] = file.readint(cmd - _DVI_Y1234 + 1, 1)
1161 if self.debug:
1162 print ("%d: y%d %d v:=%d%+d=%d, vv:=" %
1163 (self.filepos,
1164 cmd - _DVI_Y1234 + 1,
1165 self.pos[_POS_Y],
1166 self.pos[_POS_V],
1167 self.pos[_POS_Y],
1168 self.pos[_POS_V]+self.pos[_POS_Y]))
1169 self.pos[_POS_V] += self.pos[_POS_Y]
1170 elif cmd == _DVI_Z0:
1171 self.flushout()
1172 self.pos[_POS_V] += self.pos[_POS_Z]
1173 elif cmd >= _DVI_Z1234 and cmd < _DVI_Z1234 + 4:
1174 self.flushout()
1175 self.pos[_POS_Z] = file.readint(cmd - _DVI_Z1234 + 1, 1)
1176 self.pos[_POS_V] += self.pos[_POS_Z]
1177 elif cmd >= _DVI_FNTNUMMIN and cmd <= _DVI_FNTNUMMAX:
1178 self.usefont(cmd - _DVI_FNTNUMMIN)
1179 elif cmd >= _DVI_FNT1234 and cmd < _DVI_FNT1234 + 4:
1180 self.usefont(file.readint(cmd - _DVI_FNT1234 + 1, 1))
1181 elif cmd >= _DVI_SPECIAL1234 and cmd < _DVI_SPECIAL1234 + 4:
1182 self.special(file.read(file.readint(cmd - _DVI_SPECIAL1234 + 1)))
1183 elif cmd >= _DVI_FNTDEF1234 and cmd < _DVI_FNTDEF1234 + 4:
1184 if cmd == _DVI_FNTDEF1234:
1185 num = file.readuchar()
1186 elif cmd == _DVI_FNTDEF1234+1:
1187 num = file.readuint16()
1188 elif cmd == _DVI_FNTDEF1234+2:
1189 num = file.readuint24()
1190 elif cmd == _DVI_FNTDEF1234+3:
1191 # Cool, here we have according to docu a signed int. Why?
1192 num = file.readint32()
1193 self.definefont(cmd-_DVI_FNTDEF1234+1,
1194 num,
1195 file.readint32(),
1196 file.readint32(),
1197 file.readint32(),
1198 file.read(file.readuchar()+file.readuchar()))
1199 else: raise DVIError
1201 def readpreamble(self):
1202 """ opens the dvi file and reads the preamble """
1203 # XXX shouldn't we put this into the constructor?
1204 self.fonts = {}
1205 self.activefont = None
1207 # stack of fonts and fontscale currently used (used for VFs)
1208 self.fontstack = []
1210 self.stack = []
1212 # here goes the result, for each page one list.
1213 self.pages = []
1215 # pointer to currently active page
1216 self.actpage = None
1218 # currently active output: position, content and type 1 font
1219 self.actoutstart = None
1220 self.actoutstring = ""
1221 self.actoutfont = None
1223 # stack for self.file, self.fonts and self.stack, needed for VF inclusion
1224 self.statestack = []
1226 self.file = binfile(self.filename, "rb")
1228 # currently read byte in file (for debugging output)
1229 self.filepos = None
1231 if self._read_pre() != _READ_NOPAGE:
1232 raise DVIError
1234 def readpage(self):
1235 # XXX shouldn't return this the next page (insted of appending to self.pages)
1236 """ reads a page from the dvi file
1238 This routine reads a page from the dvi file. The page
1239 is appended to the list self.pages. Each page consists
1240 of a list of PSCommands equivalent to the content of
1241 the dvi file. Furthermore, the list of used fonts
1242 can be extracted from the array self.fonts. """
1244 if self._read_nopage() == _READ_PAGE:
1245 state = self._read_page()
1246 else:
1247 raise DVIError
1249 def readpostamble(self):
1250 """ finish reading of the dvi file """
1251 self.file.close()
1253 def readfile(self):
1254 """ reads and parses the hole dvi file """
1255 self.readpreamble()
1256 # XXX its unknown how often readpage should be called so we
1257 # XXX do it differently ... :-(
1258 # XXX
1259 # XXX do we realy need this method at all?
1260 # XXX why can't we just return the page on readpage? Isn't this all we need in the end?
1262 state = _READ_NOPAGE
1263 while state != _READ_DONE:
1264 if state == _READ_NOPAGE:
1265 state = self._read_nopage()
1266 elif state == _READ_PAGE:
1267 state = self._read_page()
1268 else:
1269 raise DVIError
1270 self.file.close()
1272 def marker(self, page, marker):
1273 """return marker from page"""
1274 return self.pages[page-1].markers[marker]
1276 def prolog(self, page):
1277 """ return prolog corresponding to contents of dvi file """
1278 # XXX: can nearly be remove (see write below) but:
1279 # XXX: we still have to separate the glyphs used for the different pages
1280 # XXX: maybe we just clear the markused arrays of the fonts after each page
1281 return self.pages[page-1].prolog()
1283 def write(self, file, page):
1284 """write PostScript output for page into file"""
1285 # XXX: remove this method by return canvas to TexRunner
1286 # XXX: we should do this by removing readfile and changing readpage to return
1287 # XXX: the canvas
1288 if self.debug:
1289 print "dvifile(\"%s\").write() for page %s called" % (self.filename, page)
1290 self.pages[page-1].write(file)
1292 ##############################################################################
1293 # VF file handling
1294 ##############################################################################
1296 _VF_LONG_CHAR = 242 # character packet (long version)
1297 _VF_FNTDEF1234 = _DVI_FNTDEF1234 # font definition
1298 _VF_PRE = _DVI_PRE # preamble
1299 _VF_POST = _DVI_POST # postamble
1301 _VF_ID = 202 # VF id byte
1303 class VFError(exceptions.Exception): pass
1305 class vffile:
1306 def __init__(self, filename, scale, fontmap, debug=0):
1307 self.filename = filename
1308 self.scale = scale
1309 self.fontmap = fontmap
1310 self.debug = debug
1311 self.fonts = {} # used fonts
1312 self.widths = {} # widths of defined chars
1313 self.chardefs = {} # dvi chunks for defined chars
1315 file = binfile(self.filename, "rb")
1317 cmd = file.readuchar()
1318 if cmd == _VF_PRE:
1319 if file.readuchar() != _VF_ID: raise VFError
1320 comment = file.read(file.readuchar())
1321 self.cs = file.readuint32()
1322 self.ds = file.readuint32()
1323 else:
1324 raise VFError
1326 while 1:
1327 cmd = file.readuchar()
1328 if cmd >= _VF_FNTDEF1234 and cmd < _VF_FNTDEF1234 + 4:
1329 # font definition
1330 if cmd == _VF_FNTDEF1234:
1331 num = file.readuchar()
1332 elif cmd == _VF_FNTDEF1234+1:
1333 num = file.readuint16()
1334 elif cmd == _VF_FNTDEF1234+2:
1335 num = file.readuint24()
1336 elif cmd == _VF_FNTDEF1234+3:
1337 num = file.readint32()
1338 c = file.readint32()
1339 s = file.readint32() # relative scaling used for font (fix_word)
1340 d = file.readint32() # design size of font
1341 fontname = file.read(file.readuchar()+file.readuchar())
1343 # rescaled size of font: s is relative to the scaling
1344 # of the virtual font itself. Note that realscale has
1345 # to be a fix_word (like s)
1346 # Furthermore we have to correct for self.tfmconv
1348 reals = int(self.scale * float(fix_word(self.ds))*s)
1349 # print ("defining font %s -- VF scale: %g, VF design size: %g, relative font size: %g => real size: %g" %
1350 # (fontname, self.scale, fix_word(self.ds), fix_word(s), fix_word(reals))
1352 # reald = int(d)
1354 # XXX allow for virtual fonts here too
1355 self.fonts[num] = type1font(fontname, c, reals, d, self.fontmap, self.debug > 1)
1356 elif cmd == _VF_LONG_CHAR:
1357 # character packet (long form)
1358 pl = file.readuint32() # packet length
1359 cc = file.readuint32() # char code (assumed unsigned, but anyhow only 0 <= cc < 255 is actually used)
1360 tfm = file.readuint24() # character width
1361 dvi = file.read(pl) # dvi code of character
1362 self.widths[cc] = tfm
1363 self.chardefs[cc] = dvi
1364 elif cmd < _VF_LONG_CHAR:
1365 # character packet (short form)
1366 cc = file.readuchar() # char code
1367 tfm = file.readuint24() # character width
1368 dvi = file.read(cmd)
1369 self.widths[cc] = tfm
1370 self.chardefs[cc] = dvi
1371 elif cmd == _VF_POST:
1372 break
1373 else:
1374 raise VFError
1376 file.close()
1378 def getfonts(self):
1379 return self.fonts
1381 def getchar(self, cc):
1382 return self.chardefs[cc]
1385 ###############################################################################
1386 # texmessages
1387 # - please don't get confused:
1388 # - there is a texmessage (and a texmessageparsed) attribute within the
1389 # texrunner; it contains TeX/LaTeX response from the last command execution
1390 # - instances of classes derived from the class texmessage are used to
1391 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
1392 # attribute of a texrunner instance
1393 # - the multiple usage of the name texmessage might be removed in the future
1394 # - texmessage instances should implement _Itexmessage
1395 ###############################################################################
1397 class TexResultError(Exception):
1398 """specialized texrunner exception class
1399 - it is raised by texmessage instances, when a texmessage indicates an error
1400 - it is raised by the texrunner itself, whenever there is a texmessage left
1401 after all parsing of this message (by texmessage instances)"""
1403 def __init__(self, description, texrunner):
1404 self.description = description
1405 self.texrunner = texrunner
1407 def __str__(self):
1408 """prints a detailed report about the problem
1409 - the verbose level is controlled by texrunner.errordebug"""
1410 if self.texrunner.errordebug >= 2:
1411 return ("%s\n" % self.description +
1412 "The expression passed to TeX was:\n"
1413 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1414 "The return message from TeX was:\n"
1415 " %s\n" % self.texrunner.texmessage.replace("\n", "\n ").rstrip() +
1416 "After parsing this message, the following was left:\n"
1417 " %s" % self.texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
1418 elif self.texrunner.errordebug == 1:
1419 firstlines = self.texrunner.texmessageparsed.split("\n")
1420 if len(firstlines) > 5:
1421 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
1422 return ("%s\n" % self.description +
1423 "The expression passed to TeX was:\n"
1424 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1425 "After parsing the return message from TeX, the following was left:\n" +
1426 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
1427 else:
1428 return self.description
1431 class TexResultWarning(TexResultError):
1432 """as above, but with different handling of the exception
1433 - when this exception is raised by a texmessage instance,
1434 the information just get reported and the execution continues"""
1435 pass
1438 class _Itexmessage:
1439 """validates/invalidates TeX/LaTeX response"""
1441 def check(self, texrunner):
1442 """check a Tex/LaTeX response and respond appropriate
1443 - read the texrunners texmessageparsed attribute
1444 - if there is an problem found, raise an appropriate
1445 exception (TexResultError or TexResultWarning)
1446 - remove any valid and identified TeX/LaTeX response
1447 from the texrunners texmessageparsed attribute
1448 -> finally, there should be nothing left in there,
1449 otherwise it is interpreted as an error"""
1452 class texmessage: pass
1455 class _texmessagestart(texmessage):
1456 """validates TeX/LaTeX startup"""
1458 __implements__ = _Itexmessage
1460 startpattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
1462 def check(self, texrunner):
1463 m = self.startpattern.search(texrunner.texmessageparsed)
1464 if not m:
1465 raise TexResultError("TeX startup failed", texrunner)
1466 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
1467 try:
1468 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
1469 except (IndexError, ValueError):
1470 raise TexResultError("TeX running startup file failed", texrunner)
1471 try:
1472 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
1473 except (IndexError, ValueError):
1474 raise TexResultError("TeX scrollmode check failed", texrunner)
1477 class _texmessagenoaux(texmessage):
1478 """allows for LaTeXs no-aux-file warning"""
1480 __implements__ = _Itexmessage
1482 def check(self, texrunner):
1483 try:
1484 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
1485 texrunner.texmessageparsed = s1 + s2
1486 except (IndexError, ValueError):
1487 try:
1488 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
1489 os.sep,
1490 texrunner.texfilename), 1)
1491 texrunner.texmessageparsed = s1 + s2
1492 except (IndexError, ValueError):
1493 pass
1496 class _texmessageinputmarker(texmessage):
1497 """validates the PyXInputMarker"""
1499 __implements__ = _Itexmessage
1501 def check(self, texrunner):
1502 try:
1503 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
1504 texrunner.texmessageparsed = s1 + s2
1505 except (IndexError, ValueError):
1506 raise TexResultError("PyXInputMarker expected", texrunner)
1509 class _texmessagepyxbox(texmessage):
1510 """validates the PyXBox output"""
1512 __implements__ = _Itexmessage
1514 pattern = re.compile(r"PyXBox:page=(?P<page>\d+),lt=-?\d*((\d\.?)|(\.?\d))\d*pt,rt=-?\d*((\d\.?)|(\.?\d))\d*pt,ht=-?\d*((\d\.?)|(\.?\d))\d*pt,dp=-?\d*((\d\.?)|(\.?\d))\d*pt:")
1516 def check(self, texrunner):
1517 m = self.pattern.search(texrunner.texmessageparsed)
1518 if m and m.group("page") == str(texrunner.page):
1519 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1520 else:
1521 raise TexResultError("PyXBox expected", texrunner)
1524 class _texmessagepyxpageout(texmessage):
1525 """validates the dvi shipout message (writing a page to the dvi file)"""
1527 __implements__ = _Itexmessage
1529 def check(self, texrunner):
1530 try:
1531 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
1532 texrunner.texmessageparsed = s1 + s2
1533 except (IndexError, ValueError):
1534 raise TexResultError("PyXPageOutMarker expected", texrunner)
1537 class _texmessagetexend(texmessage):
1538 """validates TeX/LaTeX finish"""
1540 __implements__ = _Itexmessage
1542 def check(self, texrunner):
1543 try:
1544 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
1545 texrunner.texmessageparsed = s1 + s2
1546 except (IndexError, ValueError):
1547 try:
1548 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
1549 os.sep,
1550 texrunner.texfilename), 1)
1551 texrunner.texmessageparsed = s1 + s2
1552 except (IndexError, ValueError):
1553 pass
1554 try:
1555 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
1556 texrunner.texmessageparsed = s1 + s2
1557 except (IndexError, ValueError):
1558 pass
1559 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
1560 m = dvipattern.search(texrunner.texmessageparsed)
1561 if texrunner.page:
1562 if not m:
1563 raise TexResultError("TeX dvifile messages expected", texrunner)
1564 if m.group("page") != str(texrunner.page):
1565 raise TexResultError("wrong number of pages reported", texrunner)
1566 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1567 else:
1568 try:
1569 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
1570 texrunner.texmessageparsed = s1 + s2
1571 except (IndexError, ValueError):
1572 raise TexResultError("no dvifile expected")
1573 try:
1574 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
1575 texrunner.texmessageparsed = s1 + s2
1576 except (IndexError, ValueError):
1577 raise TexResultError("TeX logfile message expected")
1580 class _texmessageemptylines(texmessage):
1581 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines"""
1583 __implements__ = _Itexmessage
1585 def check(self, texrunner):
1586 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
1587 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
1590 class _texmessageload(texmessage):
1591 """validates inclusion of arbitrary files
1592 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
1593 <fielname> is a readable file and other stuff can be anything
1594 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
1595 - this is not always wanted, but we just assume that file inclusion is fine"""
1597 __implements__ = _Itexmessage
1599 pattern = re.compile(r" *\((?P<filename>[^()\s\n]+)[^()]*\) *")
1601 def baselevels(self, s, maxlevel=1, brackets="()"):
1602 """strip parts of a string above a given bracket level
1603 - return a modified (some parts might be removed) version of the string s
1604 where all parts inside brackets with level higher than maxlevel are
1605 removed
1606 - if brackets do not match (number of left and right brackets is wrong
1607 or at some points there were more right brackets than left brackets)
1608 just return the unmodified string"""
1609 level = 0
1610 highestlevel = 0
1611 res = ""
1612 for c in s:
1613 if c == brackets[0]:
1614 level += 1
1615 if level > highestlevel:
1616 highestlevel = level
1617 if level <= maxlevel:
1618 res += c
1619 if c == brackets[1]:
1620 level -= 1
1621 if level == 0 and highestlevel > 0:
1622 return res
1624 def check(self, texrunner):
1625 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
1626 if lowestbracketlevel is not None:
1627 m = self.pattern.search(lowestbracketlevel)
1628 while m:
1629 if os.access(m.group("filename"), os.R_OK):
1630 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
1631 else:
1632 break
1633 m = self.pattern.search(lowestbracketlevel)
1634 else:
1635 texrunner.texmessageparsed = lowestbracketlevel
1638 class _texmessageloadfd(_texmessageload):
1639 """validates the inclusion of font description files (fd-files)
1640 - works like _texmessageload
1641 - filename must end with .fd and no further text is allowed"""
1643 pattern = re.compile(r" *\((?P<filename>[^)]+.fd)\) *")
1646 class _texmessagegraphicsload(_texmessageload):
1647 """validates the inclusion of files as the graphics packages writes it
1648 - works like _texmessageload, but using "<" and ">" as delimiters
1649 - filename must end with .eps and no further text is allowed"""
1651 pattern = re.compile(r" *<(?P<filename>[^>]+.eps)> *")
1653 def baselevels(self, s, brackets="<>", **args):
1654 return _texmessageload.baselevels(self, s, brackets=brackets, **args)
1657 #class _texmessagepdfmapload(_texmessageload):
1658 # """validates the inclusion of files as the graphics packages writes it
1659 # - works like _texmessageload, but using "{" and "}" as delimiters
1660 # - filename must end with .map and no further text is allowed"""
1662 # pattern = re.compile(r"{(?P<filename>[^}]+.map)}")
1664 # def baselevels(self, s, brackets="{}", **args):
1665 # return _texmessageload.baselevels(self, s, brackets=brackets, **args)
1668 class _texmessageignore(_texmessageload):
1669 """validates any TeX/LaTeX response
1670 - this might be used, when the expression is ok, but no suitable texmessage
1671 parser is available
1672 - PLEASE: - consider writing suitable tex message parsers
1673 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
1675 __implements__ = _Itexmessage
1677 def check(self, texrunner):
1678 texrunner.texmessageparsed = ""
1681 texmessage.start = _texmessagestart()
1682 texmessage.noaux = _texmessagenoaux()
1683 texmessage.inputmarker = _texmessageinputmarker()
1684 texmessage.pyxbox = _texmessagepyxbox()
1685 texmessage.pyxpageout = _texmessagepyxpageout()
1686 texmessage.texend = _texmessagetexend()
1687 texmessage.emptylines = _texmessageemptylines()
1688 texmessage.load = _texmessageload()
1689 texmessage.loadfd = _texmessageloadfd()
1690 texmessage.graphicsload = _texmessagegraphicsload()
1691 texmessage.ignore = _texmessageignore()
1694 ###############################################################################
1695 # texsettings
1696 # - texsettings are used to modify a TeX/LaTeX expression
1697 # to fit the users need
1698 # - texsettings have an order attribute (id), because the order is usually
1699 # important (e.g. you can not change the fontsize in mathmode in LaTeX)
1700 # - lower id's get applied later (are more outside -> mathmode has a higher
1701 # id than fontsize)
1702 # - order attributes are used to exclude complementary settings (with the
1703 # same id)
1704 # - texsettings might (in rare cases) depend on each other (e.g. parbox and
1705 # valign)
1706 ###############################################################################
1708 class _Itexsetting:
1709 """tex setting
1710 - modifies a TeX/LaTeX expression"""
1712 id = 0
1713 """order attribute for TeX settings
1714 - higher id's will be applied first (most inside)"""
1716 exclusive = 0
1717 """marks exclusive effect of the setting
1718 - when set, settings with this id exclude each other
1719 - when unset, settings with this id do not exclude each other"""
1721 def modifyexpr(self, expr, texsettings, texrunner):
1722 """modifies the TeX/LaTeX expression
1723 - expr is the original expression
1724 - the return value is the modified expression
1725 - texsettings contains a list of all texsettings (in case a tex setting
1726 depends on another texsetting)
1727 - texrunner contains the texrunner in case the texsetting depends
1728 on it"""
1730 def __cmp__(self, other):
1731 """compare texsetting with other
1732 - other is a texsetting as well
1733 - performs an id comparison (NOTE: higher id's come first!!!)"""
1736 # preamble settings for texsetting macros
1737 _texsettingpreamble = ""
1739 class _texsetting:
1741 exclusive = 1
1743 def __cmp__(self, other):
1744 return -cmp(self.id, other.id) # note the sign!!!
1747 class halign(_texsetting):
1748 """horizontal alignment
1749 the left/right splitting is performed within the PyXBox routine"""
1751 __implements__ = _Itexsetting
1753 id = 1000
1755 def __init__(self, hratio):
1756 self.hratio = hratio
1758 def modifyexpr(self, expr, texsettings, texrunner):
1759 return r"\gdef\PyXHAlign{%.5f}%s" % (self.hratio, expr)
1761 halign.left = halign(0)
1762 halign.center = halign(0.5)
1763 halign.right = halign(1)
1766 _texsettingpreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
1768 class valign(_texsetting):
1769 "vertical alignment"
1771 id = 7000
1774 class _valigntop(valign):
1776 __implements__ = _Itexsetting
1778 def modifyexpr(self, expr, texsettings, texrunner):
1779 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1782 class _valignmiddle(valign):
1784 __implements__ = _Itexsetting
1786 def modifyexpr(self, expr, texsettings, texrunner):
1787 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
1790 class _valignbottom(valign):
1792 __implements__ = _Itexsetting
1794 def modifyexpr(self, expr, texsettings, texrunner):
1795 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1798 class _valignbaseline(valign):
1800 __implements__ = _Itexsetting
1802 def modifyexpr(self, expr, texsettings, texrunner):
1803 for texsetting in texsettings:
1804 if isinstance(texsetting, parbox):
1805 raise RuntimeError("valign.baseline: specify top/middle/bottom baseline for parbox")
1806 return expr
1809 class _valignxxxbaseline(valign):
1811 def modifyexpr(self, expr, texsettings, texrunner):
1812 for texsetting in texsettings:
1813 if isinstance(texsetting, parbox):
1814 break
1815 else:
1816 raise RuntimeError(self.noparboxmessage)
1817 return expr
1820 class _valigntopbaseline(_valignxxxbaseline):
1822 __implements__ = _Itexsetting
1824 noparboxmessage = "valign.topbaseline: no parbox defined"
1827 class _valignmiddlebaseline(_valignxxxbaseline):
1829 __implements__ = _Itexsetting
1831 noparboxmessage = "valign.middlebaseline: no parbox defined"
1834 class _valignbottombaseline(_valignxxxbaseline):
1836 __implements__ = _Itexsetting
1838 noparboxmessage = "valign.bottombaseline: no parbox defined"
1841 valign.top = _valigntop()
1842 valign.middle = _valignmiddle()
1843 valign.center = valign.middle
1844 valign.bottom = _valignbottom()
1845 valign.baseline = _valignbaseline()
1846 valign.topbaseline = _valigntopbaseline()
1847 valign.middlebaseline = _valignmiddlebaseline()
1848 valign.centerbaseline = valign.middlebaseline
1849 valign.bottombaseline = _valignbottombaseline()
1852 _texsettingpreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
1855 class _parbox(_texsetting):
1856 "goes into the vertical mode"
1858 __implements__ = _Itexsetting
1860 id = 7100
1862 def __init__(self, width):
1863 self.width = width
1865 def modifyexpr(self, expr, texsettings, texrunner):
1866 boxkind = "vtop"
1867 for texsetting in texsettings:
1868 if isinstance(texsetting, valign):
1869 if (not isinstance(texsetting, _valigntop) and
1870 not isinstance(texsetting, _valignmiddle) and
1871 not isinstance(texsetting, _valignbottom) and
1872 not isinstance(texsetting, _valigntopbaseline)):
1873 if isinstance(texsetting, _valignmiddlebaseline):
1874 boxkind = "vcenter"
1875 elif isinstance(texsetting, _valignbottombaseline):
1876 boxkind = "vbox"
1877 else:
1878 raise RuntimeError("parbox couldn'd identify the valign instance")
1879 if boxkind == "vcenter":
1880 return r"\linewidth%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize\linewidth{%s}}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize\linewidth{%s}}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self.width, expr, expr)
1881 else:
1882 return r"\linewidth%.5ftruept\%s{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, boxkind, expr)
1885 class parbox(_parbox):
1887 def __init__(self, width):
1888 _parbox.__init__(self, unit.topt(width))
1891 class vshift(_texsetting):
1893 exclusive = 0
1895 id = 5000
1898 class _vshiftchar(vshift):
1899 "vertical down shift by a fraction of a character height"
1901 def __init__(self, lowerratio, heightstr="0"):
1902 self.lowerratio = lowerratio
1903 self.heightstr = heightstr
1905 def modifyexpr(self, expr, texsettings, texrunner):
1906 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
1909 class _vshiftmathaxis(vshift):
1910 "vertical down shift by the height of the math axis"
1912 def modifyexpr(self, expr, texsettings, texrunner):
1913 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
1916 vshift.char = _vshiftchar
1917 vshift.bottomzero = vshift.char(0)
1918 vshift.middlezero = vshift.char(0.5)
1919 vshift.centerzero = vshift.middlezero
1920 vshift.topzero = vshift.char(1)
1921 vshift.mathaxis = _vshiftmathaxis()
1924 class _mathmode(_texsetting):
1925 "math mode"
1927 __implements__ = _Itexsetting
1929 id = 9000
1931 def modifyexpr(self, expr, texsettings, texrunner):
1932 return r"$\displaystyle{%s}$" % expr
1934 mathmode = _mathmode()
1937 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
1939 class size(_texsetting):
1940 "font size"
1942 __implements__ = _Itexsetting
1944 id = 3000
1946 def __init__(self, expr, sizelist=defaultsizelist):
1947 if helper.isinteger(expr):
1948 if expr >= 0 and expr < sizelist.index(None):
1949 self.size = sizelist[expr]
1950 elif expr < 0 and expr + len(sizelist) > sizelist.index(None):
1951 self.size = sizelist[expr]
1952 else:
1953 raise IndexError("index out of sizelist range")
1954 else:
1955 self.size = expr
1957 def modifyexpr(self, expr, texsettings, texrunner):
1958 return r"\%s{%s}" % (self.size, expr)
1960 for s in defaultsizelist:
1961 if s is not None:
1962 size.__dict__[s] = size(s)
1965 ###############################################################################
1966 # texrunner
1967 ###############################################################################
1970 class _readpipe(threading.Thread):
1971 """threaded reader of TeX/LaTeX output
1972 - sets an event, when a specific string in the programs output is found
1973 - sets an event, when the terminal ends"""
1975 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
1976 """initialize the reader
1977 - pipe: file to be read from
1978 - expectqueue: keeps the next InputMarker to be wait for
1979 - gotevent: the "got InputMarker" event
1980 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
1981 - quitevent: the "end of terminal" event"""
1982 threading.Thread.__init__(self)
1983 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
1984 self.pipe = pipe
1985 self.expectqueue = expectqueue
1986 self.gotevent = gotevent
1987 self.gotqueue = gotqueue
1988 self.quitevent = quitevent
1989 self.expect = None
1990 self.start()
1992 def run(self):
1993 """thread routine"""
1994 read = self.pipe.readline() # read, what comes in
1995 try:
1996 self.expect = self.expectqueue.get_nowait() # read, what should be expected
1997 except Queue.Empty:
1998 pass
1999 while len(read):
2000 # universal EOL handling (convert everything into unix like EOLs)
2001 read.replace("\r", "")
2002 if not len(read) or read[-1] != "\n":
2003 read += "\n"
2004 self.gotqueue.put(read) # report, whats readed
2005 if self.expect is not None and read.find(self.expect) != -1:
2006 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
2007 read = self.pipe.readline() # read again
2008 try:
2009 self.expect = self.expectqueue.get_nowait()
2010 except Queue.Empty:
2011 pass
2012 # EOF reached
2013 self.pipe.close()
2014 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
2015 raise RuntimeError("TeX/LaTeX finished unexpectedly")
2016 self.quitevent.set()
2020 class _textbox(box._rect, base.PSCmd):
2021 """basically a box.rect, but it contains a text created by the texrunner
2022 - texrunner._text and texrunner.text return such an object
2023 - _textbox instances can be inserted into a canvas
2024 - the output is contained in a page of the dvifile available thru the texrunner"""
2026 def __init__(self, x, y, left, right, height, depth, texrunner, dvinumber, page, *styles):
2027 self.texttrafo = trafo._translate(x, y)
2028 box._rect.__init__(self, x - left, y - depth,
2029 left + right, depth + height,
2030 abscenter = (left, depth))
2031 self.texrunner = texrunner
2032 self.dvinumber = dvinumber
2033 self.page = page
2034 self.styles = styles
2036 def transform(self, *trafos):
2037 box._rect.transform(self, *trafos)
2038 for trafo in trafos:
2039 self.texttrafo = trafo * self.texttrafo
2041 def marker(self, marker):
2042 return self.texttrafo.apply(*self.texrunner.marker(self.dvinumber, self.page, marker))
2044 def prolog(self):
2045 result = []
2046 for cmd in self.styles:
2047 result.extend(cmd.prolog())
2048 return result + self.texrunner.prolog(self.dvinumber, self.page)
2050 def write(self, file):
2051 canvas._gsave().write(file) # XXX: canvas?, constructor call needed?
2052 self.texttrafo.write(file)
2053 for style in self.styles:
2054 style.write(file)
2055 self.texrunner.write(file, self.dvinumber, self.page)
2056 canvas._grestore().write(file)
2060 class textbox(_textbox):
2062 def __init__(self, x, y, left, right, height, depth, texrunner, dvinumber, page, *styles):
2063 _textbox.__init__(self, unit.topt(x), unit.topt(y), unit.topt(left), unit.topt(right),
2064 unit.topt(height), unit.topt(depth), texrunner, dvinumber, page, *styles)
2067 def _cleantmp(texrunner):
2068 """get rid of temporary files
2069 - function to be registered by atexit
2070 - files contained in usefiles are kept"""
2071 if texrunner.texruns: # cleanup while TeX is still running?
2072 texrunner.texruns = 0
2073 texrunner.texdone = 1
2074 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
2075 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
2076 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
2077 else:
2078 texrunner.texinput.write("\n\\end\n")
2079 texrunner.texinput.close() # close the input queue and
2080 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
2081 return # didn't got a quit from TeX -> we can't do much more
2082 for usefile in texrunner.usefiles:
2083 extpos = usefile.rfind(".")
2084 try:
2085 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
2086 except OSError:
2087 pass
2088 for file in glob.glob("%s.*" % texrunner.texfilename):
2089 try:
2090 os.unlink(file)
2091 except OSError:
2092 pass
2093 if texrunner.texdebug is not None:
2094 try:
2095 texrunner.texdebug.close()
2096 texrunner.texdebug = None
2097 except IOError:
2098 pass
2101 # texrunner state exceptions
2102 class TexRunsError(Exception): pass
2103 class TexDoneError(Exception): pass
2104 class TexNotInPreambleModeError(Exception): pass
2107 class texrunner:
2108 """TeX/LaTeX interface
2109 - runs TeX/LaTeX expressions instantly
2110 - checks TeX/LaTeX response
2111 - the instance variable texmessage stores the last TeX
2112 response as a string
2113 - the instance variable texmessageparsed stores a parsed
2114 version of texmessage; it should be empty after
2115 texmessage.check was called, otherwise a TexResultError
2116 is raised
2117 - the instance variable errordebug controls the verbose
2118 level of TexResultError"""
2120 def __init__(self, mode="tex",
2121 lfs="10pt",
2122 docclass="article",
2123 docopt=None,
2124 usefiles=None,
2125 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
2126 waitfortex=config.getint("text", "waitfortex", 60),
2127 showwaitfortex=config.getint("text", "showwaitfortex", 5),
2128 texipc=config.getboolean("text", "texipc", 0),
2129 texdebug=None,
2130 dvidebug=0,
2131 errordebug=1,
2132 dvicopy=0,
2133 pyxgraphics=1,
2134 texmessagestart=texmessage.start,
2135 texmessagedocclass=texmessage.load,
2136 texmessagebegindoc=(texmessage.load, texmessage.noaux),
2137 texmessageend=texmessage.texend,
2138 texmessagedefaultpreamble=texmessage.load,
2139 texmessagedefaultrun=texmessage.loadfd):
2140 mode = mode.lower()
2141 if mode != "tex" and mode != "latex":
2142 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
2143 self.mode = mode
2144 self.lfs = lfs
2145 self.docclass = docclass
2146 self.docopt = docopt
2147 self.usefiles = helper.ensurelist(usefiles)
2148 self.fontmap = readfontmap(fontmaps.split())
2149 self.waitfortex = waitfortex
2150 self.showwaitfortex = showwaitfortex
2151 self.texipc = texipc
2152 if texdebug is not None:
2153 if texdebug[-4:] == ".tex":
2154 self.texdebug = open(texdebug, "w")
2155 else:
2156 self.texdebug = open("%s.tex" % texdebug, "w")
2157 else:
2158 self.texdebug = None
2159 self.dvidebug = dvidebug
2160 self.errordebug = errordebug
2161 self.dvicopy = dvicopy
2162 self.pyxgraphics = pyxgraphics
2163 texmessagestart = helper.ensuresequence(texmessagestart)
2164 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
2165 self.texmessagestart = texmessagestart
2166 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
2167 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
2168 self.texmessagedocclass = texmessagedocclass
2169 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
2170 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
2171 self.texmessagebegindoc = texmessagebegindoc
2172 texmessageend = helper.ensuresequence(texmessageend)
2173 helper.checkattr(texmessageend, allowmulti=(texmessage,))
2174 self.texmessageend = texmessageend
2175 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
2176 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
2177 self.texmessagedefaultpreamble = texmessagedefaultpreamble
2178 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
2179 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
2180 self.texmessagedefaultrun = texmessagedefaultrun
2182 self.texruns = 0
2183 self.texdone = 0
2184 self.preamblemode = 1
2185 self.executeid = 0
2186 self.page = 0
2187 self.dvinumber = 0
2188 self.dvifiles = []
2189 self.preambles = []
2190 savetempdir = tempfile.tempdir
2191 tempfile.tempdir = os.curdir
2192 self.texfilename = os.path.basename(tempfile.mktemp())
2193 tempfile.tempdir = savetempdir
2195 def waitforevent(self, event):
2196 """waits verbosely with an timeout for an event
2197 - observes an event while periodly while printing messages
2198 - returns the status of the event (isSet)
2199 - does not clear the event"""
2200 if self.showwaitfortex:
2201 waited = 0
2202 hasevent = 0
2203 while waited < self.waitfortex and not hasevent:
2204 if self.waitfortex - waited > self.showwaitfortex:
2205 event.wait(self.showwaitfortex)
2206 waited += self.showwaitfortex
2207 else:
2208 event.wait(self.waitfortex - waited)
2209 waited += self.waitfortex - waited
2210 hasevent = event.isSet()
2211 if not hasevent:
2212 if waited < self.waitfortex:
2213 sys.stderr.write("*** PyX INFO: still waiting for %s after %i seconds...\n" % (self.mode, waited))
2214 else:
2215 sys.stderr.write("*** PyX ERROR: the timeout of %i seconds expired and %s did not respond.\n" % (waited, self.mode))
2216 return hasevent
2217 else:
2218 event.wait(self.waitfortex)
2219 return event.isSet()
2221 def execute(self, expr, *checks):
2222 """executes expr within TeX/LaTeX
2223 - if self.texruns is not yet set, TeX/LaTeX is initialized,
2224 self.texruns is set and self.preamblemode is set
2225 - the method must not be called, when self.texdone is already set
2226 - expr should be a string or None
2227 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
2228 while self.texdone becomes set
2229 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
2230 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
2232 if not self.texruns:
2233 if self.texdebug is not None:
2234 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
2235 self.texdebug.write("%% mode: %s\n" % self.mode)
2236 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
2237 for usefile in self.usefiles:
2238 extpos = usefile.rfind(".")
2239 try:
2240 os.rename(usefile, self.texfilename + usefile[extpos:])
2241 except OSError:
2242 pass
2243 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
2244 texfile.write("\\relax%\n")
2245 texfile.close()
2246 if self.texipc:
2247 ipcflag = " --ipc"
2248 else:
2249 ipcflag = ""
2250 try:
2251 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
2252 except ValueError:
2253 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
2254 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
2255 atexit.register(_cleantmp, self)
2256 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
2257 self.gotevent = threading.Event() # keeps the got inputmarker event
2258 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
2259 self.quitevent = threading.Event() # keeps for end of terminal event
2260 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
2261 self.texruns = 1
2262 oldpreamblemode = self.preamblemode
2263 self.preamblemode = 1
2264 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
2265 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
2266 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
2267 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
2268 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
2269 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
2270 "\\newdimen\\PyXDimenHAlignRT%\n" +
2271 _texsettingpreamble + # insert preambles for texsetting macros
2272 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
2273 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
2274 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
2275 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
2276 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
2277 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
2278 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
2279 "lt=\\the\\PyXDimenHAlignLT,"
2280 "rt=\\the\\PyXDimenHAlignRT,"
2281 "ht=\\the\\ht\\PyXBox,"
2282 "dp=\\the\\dp\\PyXBox:}%\n"
2283 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
2284 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
2285 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
2286 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
2287 "\\def\\PyXMarker#1{\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
2288 *self.texmessagestart)
2289 os.remove("%s.tex" % self.texfilename)
2290 if self.mode == "tex":
2291 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
2292 lfsname = self.lfs
2293 else:
2294 lfsname = "%s.lfs" % self.lfs
2295 for fulllfsname in [lfsname,
2296 os.path.join(sys.prefix, "share", "pyx", lfsname),
2297 os.path.join(os.path.dirname(__file__), "lfs", lfsname)]:
2298 try:
2299 lfsfile = open(fulllfsname, "r")
2300 lfsdef = lfsfile.read()
2301 lfsfile.close()
2302 break
2303 except IOError:
2304 pass
2305 else:
2306 allfiles = (glob.glob("*.lfs") +
2307 glob.glob(os.path.join(sys.prefix, "share", "pyx", "*.lfs")) +
2308 glob.glob(os.path.join(os.path.dirname(__file__), "lfs", "*.lfs")))
2309 lfsnames = []
2310 for f in allfiles:
2311 try:
2312 open(f, "r").close()
2313 lfsnames.append(os.path.basename(f)[:-4])
2314 except IOError:
2315 pass
2316 lfsnames.sort()
2317 if len(lfsnames):
2318 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname, lfsnames))
2319 else:
2320 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname)
2321 self.execute(lfsdef)
2322 self.execute("\\normalsize%\n")
2323 self.execute("\\newdimen\\linewidth%\n")
2324 elif self.mode == "latex":
2325 if self.pyxgraphics:
2326 for pyxdef in ["pyx.def",
2327 os.path.join(sys.prefix, "share", "pyx", "pyx.def"),
2328 os.path.join(os.path.dirname(__file__), "..", "contrib", "pyx.def")]:
2329 try:
2330 open(pyxdef, "r").close()
2331 break
2332 except IOError:
2333 pass
2334 else:
2335 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
2336 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
2337 self.execute("\\makeatletter%\n"
2338 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
2339 "\\def\\ProcessOptions{%\n"
2340 "\\def\\Gin@driver{" + pyxdef + "}%\n"
2341 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
2342 "\\saveProcessOptions}%\n"
2343 "\\makeatother")
2344 if self.docopt is not None:
2345 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass), *self.texmessagedocclass)
2346 else:
2347 self.execute("\\documentclass{%s}" % self.docclass, *self.texmessagedocclass)
2348 self.preamblemode = oldpreamblemode
2349 self.executeid += 1
2350 if expr is not None: # TeX/LaTeX should process expr
2351 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
2352 if self.preamblemode:
2353 self.expr = ("%s%%\n" % expr +
2354 "\\PyXInput{%i}%%\n" % self.executeid)
2355 else:
2356 self.page += 1
2357 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
2358 "\\PyXInput{%i}%%\n" % self.executeid)
2359 else: # TeX/LaTeX should be finished
2360 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
2361 if self.mode == "latex":
2362 self.expr = "\\end{document}%\n"
2363 else:
2364 self.expr = "\\end%\n"
2365 if self.texdebug is not None:
2366 self.texdebug.write(self.expr)
2367 self.texinput.write(self.expr)
2368 gotevent = self.waitforevent(self.gotevent)
2369 self.gotevent.clear()
2370 if expr is None and gotevent: # TeX/LaTeX should have finished
2371 self.texruns = 0
2372 self.texdone = 1
2373 self.texinput.close() # close the input queue and
2374 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
2375 try:
2376 self.texmessage = ""
2377 while 1:
2378 self.texmessage += self.gotqueue.get_nowait()
2379 except Queue.Empty:
2380 pass
2381 self.texmessageparsed = self.texmessage
2382 if gotevent:
2383 if expr is not None:
2384 texmessage.inputmarker.check(self)
2385 if not self.preamblemode:
2386 texmessage.pyxbox.check(self)
2387 texmessage.pyxpageout.check(self)
2388 for check in checks:
2389 try:
2390 check.check(self)
2391 except TexResultWarning:
2392 traceback.print_exc()
2393 texmessage.emptylines.check(self)
2394 if len(self.texmessageparsed):
2395 raise TexResultError("unhandled TeX response (might be an error)", self)
2396 else:
2397 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
2399 def finishdvi(self):
2400 "finish TeX/LaTeX and read the dvifile"
2401 self.execute(None, *self.texmessageend)
2402 if self.dvicopy:
2403 os.system("dvicopy %(t)s.dvi %(t)s.dvicopy > %(t)s.dvicopyout 2> %(t)s.dvicopyerr" % {"t": self.texfilename})
2404 dvifilename = "%s.dvicopy" % self.texfilename
2405 else:
2406 dvifilename = "%s.dvi" % self.texfilename
2407 if self.texipc:
2408 self.dvifiles[-1].readpostamble()
2409 else:
2410 advifile = dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
2411 self.dvifiles.append(advifile)
2412 self.dvifiles[-1].readfile()
2413 self.dvinumber += 1
2415 def marker(self, dvinumber, page, marker):
2416 "return the marker position"
2417 if not self.texipc and not self.texdone:
2418 self.finishdvi()
2419 return self.dvifiles[dvinumber].marker(page, marker)
2421 def prolog(self, dvinumber, page):
2422 "return the dvifile prolog"
2423 if not self.texipc and not self.texdone:
2424 self.finishdvi()
2425 return self.dvifiles[dvinumber].prolog(page)
2427 def write(self, file, dvinumber, page):
2428 "write a page from the dvifile"
2429 if not self.texipc and not self.texdone:
2430 self.finishdvi()
2431 return self.dvifiles[dvinumber].write(file, page)
2433 def reset(self, reinit=0):
2434 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
2435 if self.texruns:
2436 self.finishdvi()
2437 if self.texdebug is not None:
2438 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
2439 self.executeid = 0
2440 self.page = 0
2441 self.texdone = 0
2442 if reinit:
2443 self.preamblemode = 1
2444 for expr, args in self.preambles:
2445 self.execute(expr, *args)
2446 if self.mode == "latex":
2447 self.execute("\\begin{document}", *self.texmessagebegindoc)
2448 self.preamblemode = 0
2449 else:
2450 self.preambles = []
2451 self.preamblemode = 1
2453 def set(self, mode=None,
2454 lfs=None,
2455 docclass=None,
2456 docopt=None,
2457 usefiles=None,
2458 fontmaps=None,
2459 waitfortex=None,
2460 showwaitfortex=None,
2461 texipc=None,
2462 texdebug=None,
2463 dvidebug=None,
2464 errordebug=None,
2465 dvicopy=None,
2466 pyxgraphics=None,
2467 texmessagestart=None,
2468 texmessagedocclass=None,
2469 texmessagebegindoc=None,
2470 texmessageend=None,
2471 texmessagedefaultpreamble=None,
2472 texmessagedefaultrun=None):
2473 """provide a set command for TeX/LaTeX settings
2474 - TeX/LaTeX must not yet been started
2475 - especially needed for the defaultrunner, where no access to
2476 the constructor is available"""
2477 if self.texruns:
2478 raise TexRunsError
2479 if mode is not None:
2480 mode = mode.lower()
2481 if mode != "tex" and mode != "latex":
2482 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
2483 self.mode = mode
2484 if lfs is not None:
2485 self.lfs = lfs
2486 if docclass is not None:
2487 self.docclass = docclass
2488 if docopt is not None:
2489 self.docopt = docopt
2490 if usefiles is not None:
2491 self.usefiles = helper.ensurelist(usefiles)
2492 if fontmaps is not None:
2493 self.fontmap = readfontmap(fontmaps.split())
2494 if waitfortex is not None:
2495 self.waitfortex = waitfortex
2496 if showwaitfortex is not None:
2497 self.showwaitfortex = showwaitfortex
2498 if texipc is not None:
2499 self.texipc = texipc
2500 if texdebug is not None:
2501 if self.texdebug is not None:
2502 self.texdebug.close()
2503 if texdebug[-4:] == ".tex":
2504 self.texdebug = open(texdebug, "w")
2505 else:
2506 self.texdebug = open("%s.tex" % texdebug, "w")
2507 if dvidebug is not None:
2508 self.dvidebug = dvidebug
2509 if errordebug is not None:
2510 self.errordebug = errordebug
2511 if dvicopy is not None:
2512 self.dvicopy = dvicopy
2513 if pyxgraphics is not None:
2514 self.pyxgraphics = pyxgraphics
2515 if errordebug is not None:
2516 self.errordebug = errordebug
2517 if texmessagestart is not None:
2518 texmessagestart = helper.ensuresequence(texmessagestart)
2519 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
2520 self.texmessagestart = texmessagestart
2521 if texmessagedocclass is not None:
2522 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
2523 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
2524 self.texmessagedocclass = texmessagedocclass
2525 if texmessagebegindoc is not None:
2526 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
2527 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
2528 self.texmessagebegindoc = texmessagebegindoc
2529 if texmessageend is not None:
2530 texmessageend = helper.ensuresequence(texmessageend)
2531 helper.checkattr(texmessageend, allowmulti=(texmessage,))
2532 self.texmessageend = texmessageend
2533 if texmessagedefaultpreamble is not None:
2534 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
2535 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
2536 self.texmessagedefaultpreamble = texmessagedefaultpreamble
2537 if texmessagedefaultrun is not None:
2538 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
2539 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
2540 self.texmessagedefaultrun = texmessagedefaultrun
2542 def preamble(self, expr, *args):
2543 r"""put something into the TeX/LaTeX preamble
2544 - in LaTeX, this is done before the \begin{document}
2545 (you might use \AtBeginDocument, when you're in need for)
2546 - it is not allowed to call preamble after calling the
2547 text method for the first time (for LaTeX this is needed
2548 due to \begin{document}; in TeX it is forced for compatibility
2549 (you should be able to switch from TeX to LaTeX, if you want,
2550 without breaking something
2551 - preamble expressions must not create any dvi output
2552 - args might contain texmessage instances"""
2553 if self.texdone or not self.preamblemode:
2554 raise TexNotInPreambleModeError
2555 helper.checkattr(args, allowmulti=(texmessage,))
2556 args = helper.getattrs(args, texmessage, default=self.texmessagedefaultpreamble)
2557 self.execute(expr, *args)
2558 self.preambles.append((expr, args))
2560 PyXBoxPattern = re.compile(r"PyXBox:page=(?P<page>\d+),lt=(?P<lt>-?\d*((\d\.?)|(\.?\d))\d*)pt,rt=(?P<rt>-?\d*((\d\.?)|(\.?\d))\d*)pt,ht=(?P<ht>-?\d*((\d\.?)|(\.?\d))\d*)pt,dp=(?P<dp>-?\d*((\d\.?)|(\.?\d))\d*)pt:")
2562 def _text(self, x, y, expr, *args):
2563 """create text by passing expr to TeX/LaTeX
2564 - returns a textbox containing the result from running expr thru TeX/LaTeX
2565 - the box center is set to x, y
2566 - *args may contain style parameters, namely:
2567 - an halign instance
2568 - _texsetting instances
2569 - texmessage instances
2570 - trafo._trafo instances
2571 - base.PathStyle instances"""
2572 if expr is None:
2573 raise ValueError("None expression is invalid")
2574 if self.texdone:
2575 self.reset(reinit=1)
2576 first = 0
2577 if self.preamblemode:
2578 if self.mode == "latex":
2579 self.execute("\\begin{document}", *self.texmessagebegindoc)
2580 self.preamblemode = 0
2581 first = 1
2582 if self.texipc and self.dvicopy:
2583 raise RuntimeError("texipc and dvicopy can't be mixed up")
2584 helper.checkattr(args, allowmulti=(_texsetting, texmessage, trafo._trafo, base.fillattr))
2585 texsettings = helper.getattrs(args, _texsetting, default=[])
2586 exclusive = []
2587 for texsetting in texsettings:
2588 if texsetting.exclusive:
2589 if texsetting.id not in exclusive:
2590 exclusive.append(texsetting.id)
2591 else:
2592 raise RuntimeError("multiple occurance of exclusive texsetting with id=%i" % texsetting.id)
2593 texsettings.sort()
2594 for texsetting in texsettings:
2595 expr = texsetting.modifyexpr(expr, texsettings, self)
2596 self.execute(expr, *helper.getattrs(args, texmessage, default=self.texmessagedefaultrun))
2597 if self.texipc:
2598 if first:
2599 self.dvifiles.append(dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug))
2600 self.dvifiles[-1].readpreamble()
2601 self.dvifiles[-1].readpage()
2602 match = self.PyXBoxPattern.search(self.texmessage)
2603 if not match or int(match.group("page")) != self.page:
2604 raise TexResultError("box extents not found", self)
2605 left, right, height, depth = map(lambda x: float(x) * 72.0 / 72.27, match.group("lt", "rt", "ht", "dp"))
2606 box = _textbox(x, y, left, right, height, depth, self, self.dvinumber, self.page,
2607 *helper.getattrs(args, base.fillattr, default=[]))
2608 for t in helper.getattrs(args, trafo._trafo, default=()):
2609 box.reltransform(t)
2610 return box
2612 def text(self, x, y, expr, *args):
2613 return self._text(unit.topt(x), unit.topt(y), expr, *args)
2616 # the module provides an default texrunner and methods for direct access
2617 defaulttexrunner = texrunner()
2618 reset = defaulttexrunner.reset
2619 set = defaulttexrunner.set
2620 preamble = defaulttexrunner.preamble
2621 text = defaulttexrunner.text
2622 _text = defaulttexrunner._text