make some nice error message when using Python 2 accidentally
[PyX.git] / epsfile.py
blobbd7e9aed954ef289073e2963064aadc02a5e5dee
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
136 def _readbbox(file):
137 """returns bounding box of EPS file filename"""
139 file = linefilereader(file)
141 # check the %! header comment
142 if not file.readline().startswith(b"%!"):
143 raise IOError("file doesn't start with a '%!' header comment")
145 bboxatend = 0
146 # parse the header (use the first BoundingBox)
147 while True:
148 line = file.readline()
149 if not line:
150 break
151 if line.startswith(b"%%BoundingBox:") and not bboxatend:
152 values = line.split(b":", 1)[1].split()
153 if values == ["(atend)"]:
154 bboxatend = 1
155 else:
156 if len(values) != 4:
157 raise IOError("invalid number of bounding box values")
158 return bbox.bbox_pt(*list(map(int, values)))
159 elif (line.rstrip() == b"%%EndComments" or
160 (len(line) >= 2 and line[0] != "%" and line[1] not in string.whitespace)):
161 # implicit end of comments section
162 break
163 if not bboxatend:
164 raise IOError("no bounding box information found")
166 # parse the body
167 nesting = 0 # allow for nested documents
168 while True:
169 line = file.readline()
170 if line.startswith(b"%%BeginData:"):
171 values = line.split(":", 1)[1].split()
172 if len(values) > 3:
173 raise IOError("invalid number of arguments")
174 if len(values) == 3:
175 if values[2] == b"Lines":
176 for i in range(int(values[0])):
177 file.readline()
178 elif values[2] != b"Bytes":
179 raise IOError("invalid bytesorlines-value")
180 else:
181 file.read(int(values[0]))
182 else:
183 file.read(int(values[0]))
184 line = file.readline()
185 # ignore tailing whitespace/newline for binary data
186 if (len(values) < 3 or values[2] != "Lines") and not len(line.strip()):
187 line = file.readline()
188 if line.rstrip() != b"%%EndData":
189 raise IOError("missing EndData")
190 elif line.startswith(b"%%BeginBinary:"):
191 file.read(int(line.split(":", 1)[1]))
192 line = file.readline()
193 # ignore tailing whitespace/newline
194 if not len(line.strip()):
195 line = file.readline()
196 if line.rstrip() != b"%%EndBinary":
197 raise IOError("missing EndBinary")
198 elif line.startswith(b"%%BeginDocument:"):
199 nesting += 1
200 elif line.rstrip() == b"%%EndDocument":
201 if nesting < 1:
202 raise IOError("unmatched EndDocument")
203 nesting -= 1
204 elif not nesting and line.rstrip() == b"%%Trailer":
205 break
207 usebbox = None
208 # parse the trailer (use the last BoundingBox)
209 line = True
210 while line:
211 line = file.readline(EOFmsg=None)
212 if line.startswith("%%BoundingBox:"):
213 values = line.split(b":", 1)[1].split()
214 if len(values) != 4:
215 raise IOError("invalid number of bounding box values")
216 usebbox = bbox.bbox_pt(*list(map(int, values)))
217 if not usebbox:
218 raise IOError("missing bounding box information in document trailer")
219 return usebbox
222 class epsfile(baseclasses.canvasitem):
224 """class for epsfiles"""
226 def __init__(self,
227 x, y, filename,
228 width=None, height=None, scale=None, align="bl",
229 clip=1, translatebbox=1, bbox=None,
230 kpsearch=0):
231 """inserts epsfile
233 Object for an EPS file named filename at position (x,y). Width, height,
234 scale and aligment can be adjusted by the corresponding parameters. If
235 clip is set, the result gets clipped to the bbox of the EPS file. If
236 translatebbox is not set, the EPS graphics is not translated to the
237 corresponding origin. If bbox is not None, it overrides the bounding
238 box in the epsfile itself. If kpsearch is set then filename is searched
239 using the kpathsea library.
242 self.x_pt = unit.topt(x)
243 self.y_pt = unit.topt(y)
244 self.filename = filename
245 self.kpsearch = kpsearch
246 if bbox:
247 self.mybbox = bbox
248 else:
249 with self.open() as epsfile:
250 self.mybbox = _readbbox(epsfile)
252 # determine scaling in x and y direction
253 self.scalex = self.scaley = scale
255 if width is not None or height is not None:
256 if scale is not None:
257 raise ValueError("cannot set both width and/or height and scale simultaneously")
258 if height is not None:
259 self.scaley = unit.topt(height)/(self.mybbox.ury_pt-self.mybbox.lly_pt)
260 if width is not None:
261 self.scalex = unit.topt(width)/(self.mybbox.urx_pt-self.mybbox.llx_pt)
263 if self.scalex is None:
264 self.scalex = self.scaley
265 if self.scaley is None:
266 self.scaley = self.scalex
268 # set the actual width and height of the eps file (after a
269 # possible scaling)
270 self.width_pt = self.mybbox.urx_pt-self.mybbox.llx_pt
271 if self.scalex:
272 self.width_pt *= self.scalex
274 self.height_pt = self.mybbox.ury_pt-self.mybbox.lly_pt
275 if self.scaley:
276 self.height_pt *= self.scaley
278 # take alignment into account
279 self.align = align
280 if self.align[0]=="b":
281 pass
282 elif self.align[0]=="c":
283 self.y_pt -= self.height_pt/2.0
284 elif self.align[0]=="t":
285 self.y_pt -= self.height_pt
286 else:
287 raise ValueError("vertical alignment can only be b (bottom), c (center), or t (top)")
289 if self.align[1]=="l":
290 pass
291 elif self.align[1]=="c":
292 self.x_pt -= self.width_pt/2.0
293 elif self.align[1]=="r":
294 self.x_pt -= self.width_pt
295 else:
296 raise ValueError("horizontal alignment can only be l (left), c (center), or r (right)")
298 self.clip = clip
299 self.translatebbox = translatebbox
301 self.trafo = trafo.translate_pt(self.x_pt, self.y_pt)
303 if self.scalex is not None:
304 self.trafo = self.trafo * trafo.scale_pt(self.scalex, self.scaley)
306 if translatebbox:
307 self.trafo = self.trafo * trafo.translate_pt(-self.mybbox.llx_pt, -self.mybbox.lly_pt)
309 def open(self):
310 if self.kpsearch:
311 return config.open(self.filename, [config.format.pict])
312 else:
313 return open(self.filename, "rb")
315 def bbox(self):
316 return self.mybbox.transformed(self.trafo)
318 def processPS(self, file, writer, context, registry, bbox):
319 registry.add(_BeginEPSF)
320 registry.add(_EndEPSF)
321 bbox += self.bbox()
323 file.write("BeginEPSF\n")
325 if self.clip:
326 llx_pt, lly_pt, urx_pt, ury_pt = self.mybbox.transformed(self.trafo).highrestuple_pt()
327 file.write("%g %g %g %g rectclip\n" % (llx_pt, lly_pt, urx_pt-llx_pt, ury_pt-lly_pt))
329 self.trafo.processPS(file, writer, context, registry)
331 file.write("%%%%BeginDocument: %s\n" % self.filename)
333 with self.open() as epsfile:
334 file.write_bytes(epsfile.read())
336 file.write("%%EndDocument\n")
337 file.write("EndEPSF\n")
339 def processPDF(self, file, writer, context, registry, bbox):
340 warnings.warn("EPS file is included as a bitmap created using pipeGS")
341 from pyx import bitmap, canvas
342 from PIL import Image
343 c = canvas.canvas()
344 c.insert(self)
345 i = Image.open(c.pipeGS(device="pngalpha", resolution=600, seekable=True))
346 i.load()
347 b = bitmap.bitmap_pt(self.bbox().llx_pt, self.bbox().lly_pt, i)
348 # we slightly shift the bitmap to re-center it, as the bitmap might contain some additional border
349 # unfortunately we need to construct another bitmap instance for that ...
350 b = bitmap.bitmap_pt(self.bbox().llx_pt + 0.5*(self.bbox().width_pt()-b.bbox().width_pt()),
351 self.bbox().lly_pt + 0.5*(self.bbox().height_pt()-b.bbox().height_pt()), i)
352 b.processPDF(file, writer, context, registry, bbox)