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