fix rational test: itruediv was missing
[PyX.git] / epsfile.py
blob34aca0e2cba3c645914649c333e455a5bcf10f06
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2011 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2002-2011 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 import os, string, tempfile, warnings
24 from . import baseclasses, bbox, config, unit, trafo, pswriter
26 # PostScript-procedure definitions (cf. 5002.EPSF_Spec_v3.0.pdf)
27 # with important correction in EndEPSF:
28 # end operator is missing in the spec!
30 _BeginEPSF = pswriter.PSdefinition("BeginEPSF", b"""{
31 /b4_Inc_state save def
32 /dict_count countdictstack def
33 /op_count count 1 sub def
34 userdict begin
35 /showpage { } def
36 0 setgray 0 setlinecap
37 1 setlinewidth 0 setlinejoin
38 10 setmiterlimit [ ] 0 setdash newpath
39 /languagelevel where
40 {pop languagelevel
41 1 ne
42 {false setstrokeadjust false setoverprint
43 } if
44 } if
45 } bind""")
47 _EndEPSF = pswriter.PSdefinition("EndEPSF", b"""{
48 end
49 count op_count sub {pop} repeat
50 countdictstack dict_count sub {end} repeat
51 b4_Inc_state restore
52 } bind""")
55 class linefilereader:
56 """a line by line file reader
58 This line by line file reader allows for '\n', '\r' and
59 '\r\n' as line separation characters. Line separation
60 characters are not modified (binary mode). It implements
61 a readline, a read and a close method similar to a regular
62 file object."""
64 # note: '\n\r' is not considered to be a linebreak as its documented
65 # in the DSC spec #5001, while '\n\r' *is* a *single* linebreak
66 # according to the EPSF spec #5002
68 def __init__(self, file, typicallinelen=257):
69 """Opens the file filename for reading.
71 typicallinelen defines the default buffer increase
72 to find the next linebreak."""
74 # note: The maximal line size in an EPS is 255 plus the
75 # linebreak characters. However, we also handle
76 # lines longer than that.
77 self.file = file
78 self.buffer = b""
79 self.typicallinelen = typicallinelen
81 def read(self, count=None, EOFmsg="unexpected end of file"):
82 """read bytes from the file
84 count is the number of bytes to be read when set. Then count
85 is unset, the rest of the file is returned. EOFmsg is used
86 to raise a IOError, when the end of the file is reached while
87 reading count bytes or when the rest of the file is empty when
88 count is unset. When EOFmsg is set to None, less than the
89 requested number of bytes might be returned."""
90 if count is not None:
91 if count > len(self.buffer):
92 self.buffer += self.file.read(count - len(self.buffer))
93 if EOFmsg is not None and len(self.buffer) < count:
94 raise IOError(EOFmsg)
95 result = self.buffer[:count]
96 self.buffer = self.buffer[count:]
97 return result
98 else:
99 self.buffer += self.file.read()
100 if EOFmsg is not None and not len(self.buffer):
101 raise IOError(EOFmsg)
102 result = self.buffer
103 self.buffer = ""
104 return result
106 def readline(self, EOFmsg="unexpected end of file"):
107 """reads a line from the file
109 Lines are separated by '\n', '\r' or '\r\n'. The line separation
110 strings are included in the return value. The last line might not
111 end with an line separation string. Reading beyond the file generates
112 an IOError with the EOFmsg message. When EOFmsg is None, an empty
113 string is returned when reading beyond the end of the file."""
114 EOF = 0
115 while True:
116 crpos = self.buffer.find(b"\r")
117 nlpos = self.buffer.find(b"\n")
118 if nlpos == -1 and (crpos == -1 or crpos == len(self.buffer) - 1) and not EOF:
119 newbuffer = self.file.read(self.typicallinelen)
120 if not len(newbuffer):
121 EOF = 1
122 self.buffer += newbuffer
123 else:
124 eol = len(self.buffer)
125 if not eol and EOFmsg is not None:
126 raise IOError(EOFmsg)
127 if nlpos != -1:
128 eol = nlpos + 1
129 if crpos != -1 and (nlpos == -1 or crpos < nlpos - 1):
130 eol = crpos + 1
131 result = self.buffer[:eol]
132 self.buffer = self.buffer[eol:]
133 return result
135 def close(self):
136 self.file.close()
139 def _readbbox(file):
140 """returns bounding box of EPS file filename"""
142 file = linefilereader(file)
144 # check the %! header comment
145 if not file.readline().startswith(b"%!"):
146 raise IOError("file doesn't start with a '%!' header comment")
148 bboxatend = 0
149 # parse the header (use the first BoundingBox)
150 while True:
151 line = file.readline()
152 if not line:
153 break
154 if line.startswith(b"%%BoundingBox:") and not bboxatend:
155 values = line.split(b":", 1)[1].split()
156 if values == ["(atend)"]:
157 bboxatend = 1
158 else:
159 if len(values) != 4:
160 raise IOError("invalid number of bounding box values")
161 return bbox.bbox_pt(*list(map(int, values)))
162 elif (line.rstrip() == b"%%EndComments" or
163 (len(line) >= 2 and line[0] != "%" and line[1] not in string.whitespace)):
164 # implicit end of comments section
165 break
166 if not bboxatend:
167 raise IOError("no bounding box information found")
169 # parse the body
170 nesting = 0 # allow for nested documents
171 while True:
172 line = file.readline()
173 if line.startswith(b"%%BeginData:"):
174 values = line.split(":", 1)[1].split()
175 if len(values) > 3:
176 raise IOError("invalid number of arguments")
177 if len(values) == 3:
178 if values[2] == b"Lines":
179 for i in range(int(values[0])):
180 file.readline()
181 elif values[2] != b"Bytes":
182 raise IOError("invalid bytesorlines-value")
183 else:
184 file.read(int(values[0]))
185 else:
186 file.read(int(values[0]))
187 line = file.readline()
188 # ignore tailing whitespace/newline for binary data
189 if (len(values) < 3 or values[2] != "Lines") and not len(line.strip()):
190 line = file.readline()
191 if line.rstrip() != b"%%EndData":
192 raise IOError("missing EndData")
193 elif line.startswith(b"%%BeginBinary:"):
194 file.read(int(line.split(":", 1)[1]))
195 line = file.readline()
196 # ignore tailing whitespace/newline
197 if not len(line.strip()):
198 line = file.readline()
199 if line.rstrip() != b"%%EndBinary":
200 raise IOError("missing EndBinary")
201 elif line.startswith(b"%%BeginDocument:"):
202 nesting += 1
203 elif line.rstrip() == b"%%EndDocument":
204 if nesting < 1:
205 raise IOError("unmatched EndDocument")
206 nesting -= 1
207 elif not nesting and line.rstrip() == b"%%Trailer":
208 break
210 usebbox = None
211 # parse the trailer (use the last BoundingBox)
212 line = True
213 while line:
214 line = file.readline(EOFmsg=None)
215 if line.startswith("%%BoundingBox:"):
216 values = line.split(b":", 1)[1].split()
217 if len(values) != 4:
218 raise IOError("invalid number of bounding box values")
219 usebbox = bbox.bbox_pt(*list(map(int, values)))
220 if not usebbox:
221 raise IOError("missing bounding box information in document trailer")
222 return usebbox
225 class epsfile(baseclasses.canvasitem):
227 """class for epsfiles"""
229 def __init__(self,
230 x, y, filename,
231 width=None, height=None, scale=None, align="bl",
232 clip=1, translatebbox=1, bbox=None,
233 kpsearch=0):
234 """inserts epsfile
236 Object for an EPS file named filename at position (x,y). Width, height,
237 scale and aligment can be adjusted by the corresponding parameters. If
238 clip is set, the result gets clipped to the bbox of the EPS file. If
239 translatebbox is not set, the EPS graphics is not translated to the
240 corresponding origin. If bbox is not None, it overrides the bounding
241 box in the epsfile itself. If kpsearch is set then filename is searched
242 using the kpathsea library.
245 self.x_pt = unit.topt(x)
246 self.y_pt = unit.topt(y)
247 self.filename = filename
248 self.kpsearch = kpsearch
249 if bbox:
250 self.mybbox = bbox
251 else:
252 epsfile = self.open()
253 self.mybbox = _readbbox(epsfile)
254 epsfile.close()
256 # determine scaling in x and y direction
257 self.scalex = self.scaley = scale
259 if width is not None or height is not None:
260 if scale is not None:
261 raise ValueError("cannot set both width and/or height and scale simultaneously")
262 if height is not None:
263 self.scaley = unit.topt(height)/(self.mybbox.ury_pt-self.mybbox.lly_pt)
264 if width is not None:
265 self.scalex = unit.topt(width)/(self.mybbox.urx_pt-self.mybbox.llx_pt)
267 if self.scalex is None:
268 self.scalex = self.scaley
269 if self.scaley is None:
270 self.scaley = self.scalex
272 # set the actual width and height of the eps file (after a
273 # possible scaling)
274 self.width_pt = self.mybbox.urx_pt-self.mybbox.llx_pt
275 if self.scalex:
276 self.width_pt *= self.scalex
278 self.height_pt = self.mybbox.ury_pt-self.mybbox.lly_pt
279 if self.scaley:
280 self.height_pt *= self.scaley
282 # take alignment into account
283 self.align = align
284 if self.align[0]=="b":
285 pass
286 elif self.align[0]=="c":
287 self.y_pt -= self.height_pt/2.0
288 elif self.align[0]=="t":
289 self.y_pt -= self.height_pt
290 else:
291 raise ValueError("vertical alignment can only be b (bottom), c (center), or t (top)")
293 if self.align[1]=="l":
294 pass
295 elif self.align[1]=="c":
296 self.x_pt -= self.width_pt/2.0
297 elif self.align[1]=="r":
298 self.x_pt -= self.width_pt
299 else:
300 raise ValueError("horizontal alignment can only be l (left), c (center), or r (right)")
302 self.clip = clip
303 self.translatebbox = translatebbox
305 self.trafo = trafo.translate_pt(self.x_pt, self.y_pt)
307 if self.scalex is not None:
308 self.trafo = self.trafo * trafo.scale_pt(self.scalex, self.scaley)
310 if translatebbox:
311 self.trafo = self.trafo * trafo.translate_pt(-self.mybbox.llx_pt, -self.mybbox.lly_pt)
313 def open(self):
314 if self.kpsearch:
315 return config.open(self.filename, [config.format.pict])
316 else:
317 return open(self.filename, "rb")
319 def bbox(self):
320 return self.mybbox.transformed(self.trafo)
322 def processPS(self, file, writer, context, registry, bbox):
323 registry.add(_BeginEPSF)
324 registry.add(_EndEPSF)
325 bbox += self.bbox()
327 file.write("BeginEPSF\n")
329 if self.clip:
330 llx_pt, lly_pt, urx_pt, ury_pt = self.mybbox.transformed(self.trafo).highrestuple_pt()
331 file.write("%g %g %g %g rectclip\n" % (llx_pt, lly_pt, urx_pt-llx_pt, ury_pt-lly_pt))
333 self.trafo.processPS(file, writer, context, registry)
335 file.write("%%%%BeginDocument: %s\n" % self.filename)
337 epsfile = self.open()
338 file.write_bytes(epsfile.read())
339 epsfile.close()
341 file.write("%%EndDocument\n")
342 file.write("EndEPSF\n")
344 def processPDF(self, file, writer, context, registry, bbox):
345 warnings.warn("EPS file is included as a bitmap created using pipeGS")
346 from pyx import bitmap, canvas
347 from PIL import Image
348 c = canvas.canvas()
349 c.insert(self)
350 i = Image.open(c.pipeGS(device="pngalpha", resolution=600, seekable=True))
351 i.load()
352 b = bitmap.bitmap_pt(self.bbox().llx_pt, self.bbox().lly_pt, i)
353 # we slightly shift the bitmap to re-center it, as the bitmap might contain some additional border
354 # unfortunately we need to construct another bitmap instance for that ...
355 b = bitmap.bitmap_pt(self.bbox().llx_pt + 0.5*(self.bbox().width_pt()-b.bbox().width_pt()),
356 self.bbox().lly_pt + 0.5*(self.bbox().height_pt()-b.bbox().height_pt()), i)
357 b.processPDF(file, writer, context, registry, bbox)