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