Spaces:
Paused
Paused
| # | |
| # The Python Imaging Library. | |
| # $Id$ | |
| # | |
| # EPS file handling | |
| # | |
| # History: | |
| # 1995-09-01 fl Created (0.1) | |
| # 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2) | |
| # 1996-08-22 fl Don't choke on floating point BoundingBox values | |
| # 1996-08-23 fl Handle files from Macintosh (0.3) | |
| # 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) | |
| # 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5) | |
| # 2014-05-07 e Handling of EPS with binary preview and fixed resolution | |
| # resizing | |
| # | |
| # Copyright (c) 1997-2003 by Secret Labs AB. | |
| # Copyright (c) 1995-2003 by Fredrik Lundh | |
| # | |
| # See the README file for information on usage and redistribution. | |
| # | |
| from __future__ import annotations | |
| import io | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| import tempfile | |
| from typing import IO | |
| from . import Image, ImageFile | |
| from ._binary import i32le as i32 | |
| from ._deprecate import deprecate | |
| # -------------------------------------------------------------------- | |
| split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") | |
| field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") | |
| gs_binary: str | bool | None = None | |
| gs_windows_binary = None | |
| def has_ghostscript() -> bool: | |
| global gs_binary, gs_windows_binary | |
| if gs_binary is None: | |
| if sys.platform.startswith("win"): | |
| if gs_windows_binary is None: | |
| import shutil | |
| for binary in ("gswin32c", "gswin64c", "gs"): | |
| if shutil.which(binary) is not None: | |
| gs_windows_binary = binary | |
| break | |
| else: | |
| gs_windows_binary = False | |
| gs_binary = gs_windows_binary | |
| else: | |
| try: | |
| subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) | |
| gs_binary = "gs" | |
| except OSError: | |
| gs_binary = False | |
| return gs_binary is not False | |
| def Ghostscript(tile, size, fp, scale=1, transparency=False): | |
| """Render an image using Ghostscript""" | |
| global gs_binary | |
| if not has_ghostscript(): | |
| msg = "Unable to locate Ghostscript on paths" | |
| raise OSError(msg) | |
| # Unpack decoder tile | |
| decoder, tile, offset, data = tile[0] | |
| length, bbox = data | |
| # Hack to support hi-res rendering | |
| scale = int(scale) or 1 | |
| width = size[0] * scale | |
| height = size[1] * scale | |
| # resolution is dependent on bbox and size | |
| res_x = 72.0 * width / (bbox[2] - bbox[0]) | |
| res_y = 72.0 * height / (bbox[3] - bbox[1]) | |
| out_fd, outfile = tempfile.mkstemp() | |
| os.close(out_fd) | |
| infile_temp = None | |
| if hasattr(fp, "name") and os.path.exists(fp.name): | |
| infile = fp.name | |
| else: | |
| in_fd, infile_temp = tempfile.mkstemp() | |
| os.close(in_fd) | |
| infile = infile_temp | |
| # Ignore length and offset! | |
| # Ghostscript can read it | |
| # Copy whole file to read in Ghostscript | |
| with open(infile_temp, "wb") as f: | |
| # fetch length of fp | |
| fp.seek(0, io.SEEK_END) | |
| fsize = fp.tell() | |
| # ensure start position | |
| # go back | |
| fp.seek(0) | |
| lengthfile = fsize | |
| while lengthfile > 0: | |
| s = fp.read(min(lengthfile, 100 * 1024)) | |
| if not s: | |
| break | |
| lengthfile -= len(s) | |
| f.write(s) | |
| device = "pngalpha" if transparency else "ppmraw" | |
| # Build Ghostscript command | |
| command = [ | |
| gs_binary, | |
| "-q", # quiet mode | |
| f"-g{width:d}x{height:d}", # set output geometry (pixels) | |
| f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch) | |
| "-dBATCH", # exit after processing | |
| "-dNOPAUSE", # don't pause between pages | |
| "-dSAFER", # safe mode | |
| f"-sDEVICE={device}", | |
| f"-sOutputFile={outfile}", # output file | |
| # adjust for image origin | |
| "-c", | |
| f"{-bbox[0]} {-bbox[1]} translate", | |
| "-f", | |
| infile, # input file | |
| # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) | |
| "-c", | |
| "showpage", | |
| ] | |
| # push data through Ghostscript | |
| try: | |
| startupinfo = None | |
| if sys.platform.startswith("win"): | |
| startupinfo = subprocess.STARTUPINFO() | |
| startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW | |
| subprocess.check_call(command, startupinfo=startupinfo) | |
| out_im = Image.open(outfile) | |
| out_im.load() | |
| finally: | |
| try: | |
| os.unlink(outfile) | |
| if infile_temp: | |
| os.unlink(infile_temp) | |
| except OSError: | |
| pass | |
| im = out_im.im.copy() | |
| out_im.close() | |
| return im | |
| class PSFile: | |
| """ | |
| Wrapper for bytesio object that treats either CR or LF as end of line. | |
| This class is no longer used internally, but kept for backwards compatibility. | |
| """ | |
| def __init__(self, fp): | |
| deprecate( | |
| "PSFile", | |
| 11, | |
| action="If you need the functionality of this class " | |
| "you will need to implement it yourself.", | |
| ) | |
| self.fp = fp | |
| self.char = None | |
| def seek(self, offset, whence=io.SEEK_SET): | |
| self.char = None | |
| self.fp.seek(offset, whence) | |
| def readline(self) -> str: | |
| s = [self.char or b""] | |
| self.char = None | |
| c = self.fp.read(1) | |
| while (c not in b"\r\n") and len(c): | |
| s.append(c) | |
| c = self.fp.read(1) | |
| self.char = self.fp.read(1) | |
| # line endings can be 1 or 2 of \r \n, in either order | |
| if self.char in b"\r\n": | |
| self.char = None | |
| return b"".join(s).decode("latin-1") | |
| def _accept(prefix: bytes) -> bool: | |
| return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) | |
| ## | |
| # Image plugin for Encapsulated PostScript. This plugin supports only | |
| # a few variants of this format. | |
| class EpsImageFile(ImageFile.ImageFile): | |
| """EPS File Parser for the Python Imaging Library""" | |
| format = "EPS" | |
| format_description = "Encapsulated Postscript" | |
| mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} | |
| def _open(self) -> None: | |
| (length, offset) = self._find_offset(self.fp) | |
| # go to offset - start of "%!PS" | |
| self.fp.seek(offset) | |
| self._mode = "RGB" | |
| self._size = None | |
| byte_arr = bytearray(255) | |
| bytes_mv = memoryview(byte_arr) | |
| bytes_read = 0 | |
| reading_header_comments = True | |
| reading_trailer_comments = False | |
| trailer_reached = False | |
| def check_required_header_comments() -> None: | |
| """ | |
| The EPS specification requires that some headers exist. | |
| This should be checked when the header comments formally end, | |
| when image data starts, or when the file ends, whichever comes first. | |
| """ | |
| if "PS-Adobe" not in self.info: | |
| msg = 'EPS header missing "%!PS-Adobe" comment' | |
| raise SyntaxError(msg) | |
| if "BoundingBox" not in self.info: | |
| msg = 'EPS header missing "%%BoundingBox" comment' | |
| raise SyntaxError(msg) | |
| def _read_comment(s: str) -> bool: | |
| nonlocal reading_trailer_comments | |
| try: | |
| m = split.match(s) | |
| except re.error as e: | |
| msg = "not an EPS file" | |
| raise SyntaxError(msg) from e | |
| if not m: | |
| return False | |
| k, v = m.group(1, 2) | |
| self.info[k] = v | |
| if k == "BoundingBox": | |
| if v == "(atend)": | |
| reading_trailer_comments = True | |
| elif not self._size or (trailer_reached and reading_trailer_comments): | |
| try: | |
| # Note: The DSC spec says that BoundingBox | |
| # fields should be integers, but some drivers | |
| # put floating point values there anyway. | |
| box = [int(float(i)) for i in v.split()] | |
| self._size = box[2] - box[0], box[3] - box[1] | |
| self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] | |
| except Exception: | |
| pass | |
| return True | |
| while True: | |
| byte = self.fp.read(1) | |
| if byte == b"": | |
| # if we didn't read a byte we must be at the end of the file | |
| if bytes_read == 0: | |
| if reading_header_comments: | |
| check_required_header_comments() | |
| break | |
| elif byte in b"\r\n": | |
| # if we read a line ending character, ignore it and parse what | |
| # we have already read. if we haven't read any other characters, | |
| # continue reading | |
| if bytes_read == 0: | |
| continue | |
| else: | |
| # ASCII/hexadecimal lines in an EPS file must not exceed | |
| # 255 characters, not including line ending characters | |
| if bytes_read >= 255: | |
| # only enforce this for lines starting with a "%", | |
| # otherwise assume it's binary data | |
| if byte_arr[0] == ord("%"): | |
| msg = "not an EPS file" | |
| raise SyntaxError(msg) | |
| else: | |
| if reading_header_comments: | |
| check_required_header_comments() | |
| reading_header_comments = False | |
| # reset bytes_read so we can keep reading | |
| # data until the end of the line | |
| bytes_read = 0 | |
| byte_arr[bytes_read] = byte[0] | |
| bytes_read += 1 | |
| continue | |
| if reading_header_comments: | |
| # Load EPS header | |
| # if this line doesn't start with a "%", | |
| # or does start with "%%EndComments", | |
| # then we've reached the end of the header/comments | |
| if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": | |
| check_required_header_comments() | |
| reading_header_comments = False | |
| continue | |
| s = str(bytes_mv[:bytes_read], "latin-1") | |
| if not _read_comment(s): | |
| m = field.match(s) | |
| if m: | |
| k = m.group(1) | |
| if k[:8] == "PS-Adobe": | |
| self.info["PS-Adobe"] = k[9:] | |
| else: | |
| self.info[k] = "" | |
| elif s[0] == "%": | |
| # handle non-DSC PostScript comments that some | |
| # tools mistakenly put in the Comments section | |
| pass | |
| else: | |
| msg = "bad EPS header" | |
| raise OSError(msg) | |
| elif bytes_mv[:11] == b"%ImageData:": | |
| # Check for an "ImageData" descriptor | |
| # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 | |
| # Values: | |
| # columns | |
| # rows | |
| # bit depth (1 or 8) | |
| # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) | |
| # number of padding channels | |
| # block size (number of bytes per row per channel) | |
| # binary/ascii (1: binary, 2: ascii) | |
| # data start identifier (the image data follows after a single line | |
| # consisting only of this quoted value) | |
| image_data_values = byte_arr[11:bytes_read].split(None, 7) | |
| columns, rows, bit_depth, mode_id = ( | |
| int(value) for value in image_data_values[:4] | |
| ) | |
| if bit_depth == 1: | |
| self._mode = "1" | |
| elif bit_depth == 8: | |
| try: | |
| self._mode = self.mode_map[mode_id] | |
| except ValueError: | |
| break | |
| else: | |
| break | |
| self._size = columns, rows | |
| return | |
| elif bytes_mv[:5] == b"%%EOF": | |
| break | |
| elif trailer_reached and reading_trailer_comments: | |
| # Load EPS trailer | |
| s = str(bytes_mv[:bytes_read], "latin-1") | |
| _read_comment(s) | |
| elif bytes_mv[:9] == b"%%Trailer": | |
| trailer_reached = True | |
| bytes_read = 0 | |
| if not self._size: | |
| msg = "cannot determine EPS bounding box" | |
| raise OSError(msg) | |
| def _find_offset(self, fp): | |
| s = fp.read(4) | |
| if s == b"%!PS": | |
| # for HEAD without binary preview | |
| fp.seek(0, io.SEEK_END) | |
| length = fp.tell() | |
| offset = 0 | |
| elif i32(s) == 0xC6D3D0C5: | |
| # FIX for: Some EPS file not handled correctly / issue #302 | |
| # EPS can contain binary data | |
| # or start directly with latin coding | |
| # more info see: | |
| # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf | |
| s = fp.read(8) | |
| offset = i32(s) | |
| length = i32(s, 4) | |
| else: | |
| msg = "not an EPS file" | |
| raise SyntaxError(msg) | |
| return length, offset | |
| def load(self, scale=1, transparency=False): | |
| # Load EPS via Ghostscript | |
| if self.tile: | |
| self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) | |
| self._mode = self.im.mode | |
| self._size = self.im.size | |
| self.tile = [] | |
| return Image.Image.load(self) | |
| def load_seek(self, pos: int) -> None: | |
| # we can't incrementally load, so force ImageFile.parser to | |
| # use our custom load method by defining this method. | |
| pass | |
| # -------------------------------------------------------------------- | |
| def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None: | |
| """EPS Writer for the Python Imaging Library.""" | |
| # make sure image data is available | |
| im.load() | |
| # determine PostScript image mode | |
| if im.mode == "L": | |
| operator = (8, 1, b"image") | |
| elif im.mode == "RGB": | |
| operator = (8, 3, b"false 3 colorimage") | |
| elif im.mode == "CMYK": | |
| operator = (8, 4, b"false 4 colorimage") | |
| else: | |
| msg = "image mode is not supported" | |
| raise ValueError(msg) | |
| if eps: | |
| # write EPS header | |
| fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") | |
| fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") | |
| # fp.write("%%CreationDate: %s"...) | |
| fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size) | |
| fp.write(b"%%Pages: 1\n") | |
| fp.write(b"%%EndComments\n") | |
| fp.write(b"%%Page: 1 1\n") | |
| fp.write(b"%%ImageData: %d %d " % im.size) | |
| fp.write(b'%d %d 0 1 1 "%s"\n' % operator) | |
| # image header | |
| fp.write(b"gsave\n") | |
| fp.write(b"10 dict begin\n") | |
| fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1])) | |
| fp.write(b"%d %d scale\n" % im.size) | |
| fp.write(b"%d %d 8\n" % im.size) # <= bits | |
| fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) | |
| fp.write(b"{ currentfile buf readhexstring pop } bind\n") | |
| fp.write(operator[2] + b"\n") | |
| if hasattr(fp, "flush"): | |
| fp.flush() | |
| ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)]) | |
| fp.write(b"\n%%%%EndBinary\n") | |
| fp.write(b"grestore end\n") | |
| if hasattr(fp, "flush"): | |
| fp.flush() | |
| # -------------------------------------------------------------------- | |
| Image.register_open(EpsImageFile.format, EpsImageFile, _accept) | |
| Image.register_save(EpsImageFile.format, _save) | |
| Image.register_extensions(EpsImageFile.format, [".ps", ".eps"]) | |
| Image.register_mime(EpsImageFile.format, "application/postscript") | |