various documentation fixes
[PyX.git] / pyx / epsfile.py
blob4c5fda5d412e06c795f81b99e2ded90c9273c7f8
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 import canvasitem, bbox, filelocator, 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", """{
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", """{
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, filename, 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.
78 self.file = open(filename, "rb")
79 self.buffer = ""
80 self.typicallinelen = typicallinelen
82 def read(self, count=None, EOFmsg="unexpected end of file"):
83 """read bytes from the file
85 count is the number of bytes to be read when set. Then count
86 is unset, the rest of the file is returned. EOFmsg is used
87 to raise a IOError, when the end of the file is reached while
88 reading count bytes or when the rest of the file is empty when
89 count is unset. When EOFmsg is set to None, less than the
90 requested number of bytes might be returned."""
91 if count is not None:
92 if count > len(self.buffer):
93 self.buffer += self.file.read(count - len(self.buffer))
94 if EOFmsg is not None and len(self.buffer) < count:
95 raise IOError(EOFmsg)
96 result = self.buffer[:count]
97 self.buffer = self.buffer[count:]
98 return result
99 else:
100 self.buffer += self.file.read()
101 if EOFmsg is not None and not len(self.buffer):
102 raise IOError(EOFmsg)
103 result = self.buffer
104 self.buffer = ""
105 return result
107 def readline(self, EOFmsg="unexpected end of file"):
108 """reads a line from the file
110 Lines are separated by '\n', '\r' or '\r\n'. The line separation
111 strings are included in the return value. The last line might not
112 end with an line separation string. Reading beyond the file generates
113 an IOError with the EOFmsg message. When EOFmsg is None, an empty
114 string is returned when reading beyond the end of the file."""
115 EOF = 0
116 while 1:
117 crpos = self.buffer.find("\r")
118 nlpos = self.buffer.find("\n")
119 if nlpos == -1 and (crpos == -1 or crpos == len(self.buffer) - 1) and not EOF:
120 newbuffer = self.file.read(self.typicallinelen)
121 if not len(newbuffer):
122 EOF = 1
123 self.buffer += newbuffer
124 else:
125 eol = len(self.buffer)
126 if not eol and EOFmsg is not None:
127 raise IOError(EOFmsg)
128 if nlpos != -1:
129 eol = nlpos + 1
130 if crpos != -1 and (nlpos == -1 or crpos < nlpos - 1):
131 eol = crpos + 1
132 result = self.buffer[:eol]
133 self.buffer = self.buffer[eol:]
134 return result
136 def close(self):
137 "closes the file"
138 self.file.close()
141 def _readbbox(filename):
142 """returns bounding box of EPS file filename"""
144 file = linefilereader(filename)
146 # check the %! header comment
147 if not file.readline().startswith("%!"):
148 raise IOError("file doesn't start with a '%!' header comment")
150 bboxatend = 0
151 # parse the header (use the first BoundingBox)
152 while 1:
153 line = file.readline()
154 if not line:
155 break
156 if line.startswith("%%BoundingBox:") and not bboxatend:
157 values = line.split(":", 1)[1].split()
158 if values == ["(atend)"]:
159 bboxatend = 1
160 else:
161 if len(values) != 4:
162 raise IOError("invalid number of bounding box values")
163 return bbox.bbox_pt(*map(int, values))
164 elif (line.rstrip() == "%%EndComments" or
165 (len(line) >= 2 and line[0] != "%" and line[1] not in string.whitespace)):
166 # implicit end of comments section
167 break
168 if not bboxatend:
169 raise IOError("no bounding box information found")
171 # parse the body
172 nesting = 0 # allow for nested documents
173 while 1:
174 line = file.readline()
175 if line.startswith("%%BeginData:"):
176 values = line.split(":", 1)[1].split()
177 if len(values) > 3:
178 raise IOError("invalid number of arguments")
179 if len(values) == 3:
180 if values[2] == "Lines":
181 for i in xrange(int(values[0])):
182 file.readline()
183 elif values[2] != "Bytes":
184 raise IOError("invalid bytesorlines-value")
185 else:
186 file.read(int(values[0]))
187 else:
188 file.read(int(values[0]))
189 line = file.readline()
190 # ignore tailing whitespace/newline for binary data
191 if (len(values) < 3 or values[2] != "Lines") and not len(line.strip()):
192 line = file.readline()
193 if line.rstrip() != "%%EndData":
194 raise IOError("missing EndData")
195 elif line.startswith("%%BeginBinary:"):
196 file.read(int(line.split(":", 1)[1]))
197 line = file.readline()
198 # ignore tailing whitespace/newline
199 if not len(line.strip()):
200 line = file.readline()
201 if line.rstrip() != "%%EndBinary":
202 raise IOError("missing EndBinary")
203 elif line.startswith("%%BeginDocument:"):
204 nesting += 1
205 elif line.rstrip() == "%%EndDocument":
206 if nesting < 1:
207 raise IOError("unmatched EndDocument")
208 nesting -= 1
209 elif not nesting and line.rstrip() == "%%Trailer":
210 break
212 usebbox = None
213 # parse the trailer (use the last BoundingBox)
214 line = True
215 while line:
216 line = file.readline(EOFmsg=None)
217 if line.startswith("%%BoundingBox:"):
218 values = line.split(":", 1)[1].split()
219 if len(values) != 4:
220 raise IOError("invalid number of bounding box values")
221 usebbox = bbox.bbox_pt(*map(int, values))
222 if not usebbox:
223 raise IOError("missing bounding box information in document trailer")
224 return usebbox
227 class epsfile(canvasitem.canvasitem):
229 """class for epsfiles"""
231 def __init__(self,
232 x, y, filename,
233 width=None, height=None, scale=None, align="bl",
234 clip=1, translatebbox=1, bbox=None,
235 kpsearch=0):
236 """inserts epsfile
238 Object for an EPS file named filename at position (x,y). Width, height,
239 scale and aligment can be adjusted by the corresponding parameters. If
240 clip is set, the result gets clipped to the bbox of the EPS file. If
241 translatebbox is not set, the EPS graphics is not translated to the
242 corresponding origin. If bbox is not None, it overrides the bounding
243 box in the epsfile itself. If kpsearch is set then filename is searched
244 using the kpathsea library.
247 self.x_pt = unit.topt(x)
248 self.y_pt = unit.topt(y)
249 self.filename = filename
250 self.kpsearch = kpsearch
251 self.mybbox = bbox or _readbbox(self.filename)
253 # determine scaling in x and y direction
254 self.scalex = self.scaley = scale
256 if width is not None or height is not None:
257 if scale is not None:
258 raise ValueError("cannot set both width and/or height and scale simultaneously")
259 if height is not None:
260 self.scaley = unit.topt(height)/(self.mybbox.ury_pt-self.mybbox.lly_pt)
261 if width is not None:
262 self.scalex = unit.topt(width)/(self.mybbox.urx_pt-self.mybbox.llx_pt)
264 if self.scalex is None:
265 self.scalex = self.scaley
266 if self.scaley is None:
267 self.scaley = self.scalex
269 # set the actual width and height of the eps file (after a
270 # possible scaling)
271 self.width_pt = self.mybbox.urx_pt-self.mybbox.llx_pt
272 if self.scalex:
273 self.width_pt *= self.scalex
275 self.height_pt = self.mybbox.ury_pt-self.mybbox.lly_pt
276 if self.scaley:
277 self.height_pt *= self.scaley
279 # take alignment into account
280 self.align = align
281 if self.align[0]=="b":
282 pass
283 elif self.align[0]=="c":
284 self.y_pt -= self.height_pt/2.0
285 elif self.align[0]=="t":
286 self.y_pt -= self.height_pt
287 else:
288 raise ValueError("vertical alignment can only be b (bottom), c (center), or t (top)")
290 if self.align[1]=="l":
291 pass
292 elif self.align[1]=="c":
293 self.x_pt -= self.width_pt/2.0
294 elif self.align[1]=="r":
295 self.x_pt -= self.width_pt
296 else:
297 raise ValueError("horizontal alignment can only be l (left), c (center), or r (right)")
299 self.clip = clip
300 self.translatebbox = translatebbox
302 self.trafo = trafo.translate_pt(self.x_pt, self.y_pt)
304 if self.scalex is not None:
305 self.trafo = self.trafo * trafo.scale_pt(self.scalex, self.scaley)
307 if translatebbox:
308 self.trafo = self.trafo * trafo.translate_pt(-self.mybbox.llx_pt, -self.mybbox.lly_pt)
310 def bbox(self):
311 return self.mybbox.transformed(self.trafo)
313 def processPS(self, file, writer, context, registry, bbox):
314 registry.add(_BeginEPSF)
315 registry.add(_EndEPSF)
316 bbox += self.bbox()
318 if self.kpsearch:
319 epsfile = filelocator.open(self.filename, [filelocator.format.pict], "rb")
320 else:
321 epsfile = open(self.filename, "rb")
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, bbox)
331 file.write("%%%%BeginDocument: %s\n" % self.filename)
332 file.write(epsfile.read())
333 file.write("%%EndDocument\n")
334 file.write("EndEPSF\n")
335 epsfile.close()
337 def processPDF(self, file, writer, context, registry, bbox):
338 warnings.warn("EPS file is included as a bitmap created using pipeGS")
339 from pyx import bitmap, canvas
340 import Image
341 c = canvas.canvas()
342 c.insert(self)
343 fd, fname = tempfile.mkstemp()
344 f = os.fdopen(fd, "wb")
345 f.close()
346 c.pipeGS(fname, device="pngalpha", resolution=600)
347 i = Image.open(fname)
348 i.load()
349 os.unlink(fname)
350 b = bitmap.bitmap_pt(self.bbox().llx_pt, self.bbox().lly_pt, i)
351 # we slightly shift the bitmap to re-center it, as the bitmap might contain some additional border
352 # unfortunately we need to construct another bitmap instance for that ...
353 b = bitmap.bitmap_pt(self.bbox().llx_pt + 0.5*(self.bbox().width_pt()-b.bbox().width_pt()),
354 self.bbox().lly_pt + 0.5*(self.bbox().height_pt()-b.bbox().height_pt()), i)
355 b.processPDF(file, writer, context, registry, bbox)