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