Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/EpsImagePlugin.py: 43%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# The Python Imaging Library.
3# $Id$
4#
5# EPS file handling
6#
7# History:
8# 1995-09-01 fl Created (0.1)
9# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2)
10# 1996-08-22 fl Don't choke on floating point BoundingBox values
11# 1996-08-23 fl Handle files from Macintosh (0.3)
12# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4)
13# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5)
14# 2014-05-07 e Handling of EPS with binary preview and fixed resolution
15# resizing
16#
17# Copyright (c) 1997-2003 by Secret Labs AB.
18# Copyright (c) 1995-2003 by Fredrik Lundh
19#
20# See the README file for information on usage and redistribution.
21#
22from __future__ import annotations
24import io
25import os
26import re
27import subprocess
28import sys
29import tempfile
30from typing import IO
32from . import Image, ImageFile
33from ._binary import i32le as i32
35# --------------------------------------------------------------------
38split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
39field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
41gs_binary: str | bool | None = None
42gs_windows_binary = None
45def has_ghostscript() -> bool:
46 global gs_binary, gs_windows_binary
47 if gs_binary is None:
48 if sys.platform.startswith("win"):
49 if gs_windows_binary is None:
50 import shutil
52 for binary in ("gswin32c", "gswin64c", "gs"):
53 if shutil.which(binary) is not None:
54 gs_windows_binary = binary
55 break
56 else:
57 gs_windows_binary = False
58 gs_binary = gs_windows_binary
59 else:
60 try:
61 subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
62 gs_binary = "gs"
63 except OSError:
64 gs_binary = False
65 return gs_binary is not False
68def Ghostscript(
69 tile: list[ImageFile._Tile],
70 size: tuple[int, int],
71 fp: IO[bytes],
72 scale: int = 1,
73 transparency: bool = False,
74) -> Image.core.ImagingCore:
75 """Render an image using Ghostscript"""
76 global gs_binary
77 if not has_ghostscript():
78 msg = "Unable to locate Ghostscript on paths"
79 raise OSError(msg)
80 assert isinstance(gs_binary, str)
82 # Unpack decoder tile
83 args = tile[0].args
84 assert isinstance(args, tuple)
85 length, bbox = args
87 # Hack to support hi-res rendering
88 scale = int(scale) or 1
89 width = size[0] * scale
90 height = size[1] * scale
91 # resolution is dependent on bbox and size
92 res_x = 72.0 * width / (bbox[2] - bbox[0])
93 res_y = 72.0 * height / (bbox[3] - bbox[1])
95 out_fd, outfile = tempfile.mkstemp()
96 os.close(out_fd)
98 infile_temp = None
99 if hasattr(fp, "name") and os.path.exists(fp.name):
100 infile = fp.name
101 else:
102 in_fd, infile_temp = tempfile.mkstemp()
103 os.close(in_fd)
104 infile = infile_temp
106 # Ignore length and offset!
107 # Ghostscript can read it
108 # Copy whole file to read in Ghostscript
109 with open(infile_temp, "wb") as f:
110 # fetch length of fp
111 fp.seek(0, io.SEEK_END)
112 fsize = fp.tell()
113 # ensure start position
114 # go back
115 fp.seek(0)
116 lengthfile = fsize
117 while lengthfile > 0:
118 s = fp.read(min(lengthfile, 100 * 1024))
119 if not s:
120 break
121 lengthfile -= len(s)
122 f.write(s)
124 if transparency:
125 # "RGBA"
126 device = "pngalpha"
127 else:
128 # "pnmraw" automatically chooses between
129 # PBM ("1"), PGM ("L"), and PPM ("RGB").
130 device = "pnmraw"
132 # Build Ghostscript command
133 command = [
134 gs_binary,
135 "-q", # quiet mode
136 f"-g{width:d}x{height:d}", # set output geometry (pixels)
137 f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch)
138 "-dBATCH", # exit after processing
139 "-dNOPAUSE", # don't pause between pages
140 "-dSAFER", # safe mode
141 f"-sDEVICE={device}",
142 f"-sOutputFile={outfile}", # output file
143 # adjust for image origin
144 "-c",
145 f"{-bbox[0]} {-bbox[1]} translate",
146 "-f",
147 infile, # input file
148 # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
149 "-c",
150 "showpage",
151 ]
153 # push data through Ghostscript
154 try:
155 startupinfo = None
156 if sys.platform.startswith("win"):
157 startupinfo = subprocess.STARTUPINFO()
158 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
159 subprocess.check_call(command, startupinfo=startupinfo)
160 with Image.open(outfile) as out_im:
161 out_im.load()
162 return out_im.im.copy()
163 finally:
164 try:
165 os.unlink(outfile)
166 if infile_temp:
167 os.unlink(infile_temp)
168 except OSError:
169 pass
172def _accept(prefix: bytes) -> bool:
173 return prefix.startswith(b"%!PS") or (
174 len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5
175 )
178##
179# Image plugin for Encapsulated PostScript. This plugin supports only
180# a few variants of this format.
183class EpsImageFile(ImageFile.ImageFile):
184 """EPS File Parser for the Python Imaging Library"""
186 format = "EPS"
187 format_description = "Encapsulated Postscript"
189 mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
191 def _open(self) -> None:
192 assert self.fp is not None
193 length, offset = self._find_offset(self.fp)
195 # go to offset - start of "%!PS"
196 self.fp.seek(offset)
198 self._mode = "RGB"
200 # When reading header comments, the first comment is used.
201 # When reading trailer comments, the last comment is used.
202 bounding_box: list[int] | None = None
203 imagedata_size: tuple[int, int] | None = None
205 byte_arr = bytearray(255)
206 bytes_mv = memoryview(byte_arr)
207 bytes_read = 0
208 reading_header_comments = True
209 reading_trailer_comments = False
210 trailer_reached = False
212 def check_required_header_comments() -> None:
213 """
214 The EPS specification requires that some headers exist.
215 This should be checked when the header comments formally end,
216 when image data starts, or when the file ends, whichever comes first.
217 """
218 if "PS-Adobe" not in self.info:
219 msg = 'EPS header missing "%!PS-Adobe" comment'
220 raise SyntaxError(msg)
221 if "BoundingBox" not in self.info:
222 msg = 'EPS header missing "%%BoundingBox" comment'
223 raise SyntaxError(msg)
225 def read_comment(s: str) -> bool:
226 nonlocal bounding_box, reading_trailer_comments
227 try:
228 m = split.match(s)
229 except re.error as e:
230 msg = "not an EPS file"
231 raise SyntaxError(msg) from e
233 if not m:
234 return False
236 k, v = m.group(1, 2)
237 self.info[k] = v
238 if k == "BoundingBox":
239 if v == "(atend)":
240 reading_trailer_comments = True
241 elif not bounding_box or (trailer_reached and reading_trailer_comments):
242 try:
243 # Note: The DSC spec says that BoundingBox
244 # fields should be integers, but some drivers
245 # put floating point values there anyway.
246 bounding_box = [int(float(i)) for i in v.split()]
247 except Exception:
248 pass
249 return True
251 while True:
252 byte = self.fp.read(1)
253 if byte == b"":
254 # if we didn't read a byte we must be at the end of the file
255 if bytes_read == 0:
256 if reading_header_comments:
257 check_required_header_comments()
258 break
259 elif byte in b"\r\n":
260 # if we read a line ending character, ignore it and parse what
261 # we have already read. if we haven't read any other characters,
262 # continue reading
263 if bytes_read == 0:
264 continue
265 else:
266 # ASCII/hexadecimal lines in an EPS file must not exceed
267 # 255 characters, not including line ending characters
268 if bytes_read >= 255:
269 # only enforce this for lines starting with a "%",
270 # otherwise assume it's binary data
271 if byte_arr[0] == ord("%"):
272 msg = "not an EPS file"
273 raise SyntaxError(msg)
274 else:
275 if reading_header_comments:
276 check_required_header_comments()
277 reading_header_comments = False
278 # reset bytes_read so we can keep reading
279 # data until the end of the line
280 bytes_read = 0
281 byte_arr[bytes_read] = byte[0]
282 bytes_read += 1
283 continue
285 if reading_header_comments:
286 # Load EPS header
288 # if this line doesn't start with a "%",
289 # or does start with "%%EndComments",
290 # then we've reached the end of the header/comments
291 if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
292 check_required_header_comments()
293 reading_header_comments = False
294 continue
296 s = str(bytes_mv[:bytes_read], "latin-1")
297 if not read_comment(s):
298 m = field.match(s)
299 if m:
300 k = m.group(1)
301 if k.startswith("PS-Adobe"):
302 self.info["PS-Adobe"] = k[9:]
303 else:
304 self.info[k] = ""
305 elif s[0] == "%":
306 # handle non-DSC PostScript comments that some
307 # tools mistakenly put in the Comments section
308 pass
309 else:
310 msg = "bad EPS header"
311 raise OSError(msg)
312 elif bytes_mv[:11] == b"%ImageData:":
313 # Check for an "ImageData" descriptor
314 # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
316 # If we've already read an "ImageData" descriptor,
317 # don't read another one.
318 if imagedata_size:
319 bytes_read = 0
320 continue
322 # Values:
323 # columns
324 # rows
325 # bit depth (1 or 8)
326 # mode (1: L, 2: LAB, 3: RGB, 4: CMYK)
327 # number of padding channels
328 # block size (number of bytes per row per channel)
329 # binary/ascii (1: binary, 2: ascii)
330 # data start identifier (the image data follows after a single line
331 # consisting only of this quoted value)
332 image_data_values = byte_arr[11:bytes_read].split(None, 7)
333 columns, rows, bit_depth, mode_id = (
334 int(value) for value in image_data_values[:4]
335 )
337 if bit_depth == 1:
338 self._mode = "1"
339 elif bit_depth == 8:
340 try:
341 self._mode = self.mode_map[mode_id]
342 except ValueError:
343 break
344 else:
345 break
347 # Parse the columns and rows after checking the bit depth and mode
348 # in case the bit depth and/or mode are invalid.
349 imagedata_size = columns, rows
350 elif bytes_mv[:5] == b"%%EOF":
351 break
352 elif trailer_reached and reading_trailer_comments:
353 # Load EPS trailer
354 s = str(bytes_mv[:bytes_read], "latin-1")
355 read_comment(s)
356 elif bytes_mv[:9] == b"%%Trailer":
357 trailer_reached = True
358 elif bytes_mv[:14] == b"%%BeginBinary:":
359 bytecount = int(byte_arr[14:bytes_read])
360 self.fp.seek(bytecount, os.SEEK_CUR)
361 bytes_read = 0
363 # A "BoundingBox" is always required,
364 # even if an "ImageData" descriptor size exists.
365 if not bounding_box:
366 msg = "cannot determine EPS bounding box"
367 raise OSError(msg)
369 # An "ImageData" size takes precedence over the "BoundingBox".
370 self._size = imagedata_size or (
371 bounding_box[2] - bounding_box[0],
372 bounding_box[3] - bounding_box[1],
373 )
375 self.tile = [
376 ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
377 ]
379 def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
380 s = fp.read(4)
382 if s == b"%!PS":
383 # for HEAD without binary preview
384 fp.seek(0, io.SEEK_END)
385 length = fp.tell()
386 offset = 0
387 elif i32(s) == 0xC6D3D0C5:
388 # FIX for: Some EPS file not handled correctly / issue #302
389 # EPS can contain binary data
390 # or start directly with latin coding
391 # more info see:
392 # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
393 s = fp.read(8)
394 offset = i32(s)
395 length = i32(s, 4)
396 else:
397 msg = "not an EPS file"
398 raise SyntaxError(msg)
400 return length, offset
402 def load(
403 self, scale: int = 1, transparency: bool = False
404 ) -> Image.core.PixelAccess | None:
405 # Load EPS via Ghostscript
406 if self.tile:
407 assert self.fp is not None
408 self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
409 self._mode = self.im.mode
410 self._size = self.im.size
411 self.tile = []
412 return Image.Image.load(self)
414 def load_seek(self, pos: int) -> None:
415 # we can't incrementally load, so force ImageFile.parser to
416 # use our custom load method by defining this method.
417 pass
420# --------------------------------------------------------------------
423def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
424 """EPS Writer for the Python Imaging Library."""
426 # make sure image data is available
427 im.load()
429 # determine PostScript image mode
430 if im.mode == "L":
431 operator = (8, 1, b"image")
432 elif im.mode == "RGB":
433 operator = (8, 3, b"false 3 colorimage")
434 elif im.mode == "CMYK":
435 operator = (8, 4, b"false 4 colorimage")
436 else:
437 msg = "image mode is not supported"
438 raise ValueError(msg)
440 if eps:
441 # write EPS header
442 fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
443 fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
444 # fp.write("%%CreationDate: %s"...)
445 fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
446 fp.write(b"%%Pages: 1\n")
447 fp.write(b"%%EndComments\n")
448 fp.write(b"%%Page: 1 1\n")
449 fp.write(b"%%ImageData: %d %d " % im.size)
450 fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
452 # image header
453 fp.write(b"gsave\n")
454 fp.write(b"10 dict begin\n")
455 fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
456 fp.write(b"%d %d scale\n" % im.size)
457 fp.write(b"%d %d 8\n" % im.size) # <= bits
458 fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
459 fp.write(b"{ currentfile buf readhexstring pop } bind\n")
460 fp.write(operator[2] + b"\n")
461 if hasattr(fp, "flush"):
462 fp.flush()
464 ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])
466 fp.write(b"\n%%%%EndBinary\n")
467 fp.write(b"grestore end\n")
468 if hasattr(fp, "flush"):
469 fp.flush()
472# --------------------------------------------------------------------
475Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
477Image.register_save(EpsImageFile.format, _save)
479Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
481Image.register_mime(EpsImageFile.format, "application/postscript")