| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 |
| |
|
| | |
| |
|
| |
|
| | 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: list[ImageFile._Tile], |
| | size: tuple[int, int], |
| | fp: IO[bytes], |
| | scale: int = 1, |
| | transparency: bool = False, |
| | ) -> Image.core.ImagingCore: |
| | """Render an image using Ghostscript""" |
| | global gs_binary |
| | if not has_ghostscript(): |
| | msg = "Unable to locate Ghostscript on paths" |
| | raise OSError(msg) |
| | assert isinstance(gs_binary, str) |
| |
|
| | |
| | args = tile[0].args |
| | assert isinstance(args, tuple) |
| | length, bbox = args |
| |
|
| | |
| | scale = int(scale) or 1 |
| | width = size[0] * scale |
| | height = size[1] * scale |
| | |
| | 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 |
| |
|
| | |
| | |
| | |
| | with open(infile_temp, "wb") as f: |
| | |
| | fp.seek(0, io.SEEK_END) |
| | fsize = fp.tell() |
| | |
| | |
| | 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) |
| |
|
| | if transparency: |
| | |
| | device = "pngalpha" |
| | else: |
| | |
| | |
| | device = "pnmraw" |
| |
|
| | |
| | command = [ |
| | gs_binary, |
| | "-q", |
| | f"-g{width:d}x{height:d}", |
| | f"-r{res_x:f}x{res_y:f}", |
| | "-dBATCH", |
| | "-dNOPAUSE", |
| | "-dSAFER", |
| | f"-sDEVICE={device}", |
| | f"-sOutputFile={outfile}", |
| | |
| | "-c", |
| | f"{-bbox[0]} {-bbox[1]} translate", |
| | "-f", |
| | infile, |
| | |
| | "-c", |
| | "showpage", |
| | ] |
| |
|
| | |
| | try: |
| | startupinfo = None |
| | if sys.platform.startswith("win"): |
| | startupinfo = subprocess.STARTUPINFO() |
| | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW |
| | subprocess.check_call(command, startupinfo=startupinfo) |
| | with Image.open(outfile) as out_im: |
| | out_im.load() |
| | return out_im.im.copy() |
| | finally: |
| | try: |
| | os.unlink(outfile) |
| | if infile_temp: |
| | os.unlink(infile_temp) |
| | except OSError: |
| | pass |
| |
|
| |
|
| | def _accept(prefix: bytes) -> bool: |
| | return prefix.startswith(b"%!PS") or ( |
| | len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5 |
| | ) |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | 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: |
| | assert self.fp is not None |
| | (length, offset) = self._find_offset(self.fp) |
| |
|
| | |
| | self.fp.seek(offset) |
| |
|
| | self._mode = "RGB" |
| |
|
| | |
| | |
| | bounding_box: list[int] | None = None |
| | imagedata_size: tuple[int, int] | None = 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 bounding_box, 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 bounding_box or (trailer_reached and reading_trailer_comments): |
| | try: |
| | |
| | |
| | |
| | bounding_box = [int(float(i)) for i in v.split()] |
| | except Exception: |
| | pass |
| | return True |
| |
|
| | while True: |
| | byte = self.fp.read(1) |
| | if byte == b"": |
| | |
| | if bytes_read == 0: |
| | if reading_header_comments: |
| | check_required_header_comments() |
| | break |
| | elif byte in b"\r\n": |
| | |
| | |
| | |
| | if bytes_read == 0: |
| | continue |
| | else: |
| | |
| | |
| | if bytes_read >= 255: |
| | |
| | |
| | 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 |
| | |
| | |
| | bytes_read = 0 |
| | byte_arr[bytes_read] = byte[0] |
| | bytes_read += 1 |
| | continue |
| |
|
| | if reading_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.startswith("PS-Adobe"): |
| | self.info["PS-Adobe"] = k[9:] |
| | else: |
| | self.info[k] = "" |
| | elif s[0] == "%": |
| | |
| | |
| | pass |
| | else: |
| | msg = "bad EPS header" |
| | raise OSError(msg) |
| | elif bytes_mv[:11] == b"%ImageData:": |
| | |
| | |
| |
|
| | |
| | |
| | if imagedata_size: |
| | bytes_read = 0 |
| | continue |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 |
| |
|
| | |
| | |
| | imagedata_size = columns, rows |
| | elif bytes_mv[:5] == b"%%EOF": |
| | break |
| | elif trailer_reached and reading_trailer_comments: |
| | |
| | s = str(bytes_mv[:bytes_read], "latin-1") |
| | read_comment(s) |
| | elif bytes_mv[:9] == b"%%Trailer": |
| | trailer_reached = True |
| | elif bytes_mv[:14] == b"%%BeginBinary:": |
| | bytecount = int(byte_arr[14:bytes_read]) |
| | self.fp.seek(bytecount, os.SEEK_CUR) |
| | bytes_read = 0 |
| |
|
| | |
| | |
| | if not bounding_box: |
| | msg = "cannot determine EPS bounding box" |
| | raise OSError(msg) |
| |
|
| | |
| | self._size = imagedata_size or ( |
| | bounding_box[2] - bounding_box[0], |
| | bounding_box[3] - bounding_box[1], |
| | ) |
| |
|
| | self.tile = [ |
| | ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box)) |
| | ] |
| |
|
| | def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: |
| | s = fp.read(4) |
| |
|
| | if s == b"%!PS": |
| | |
| | fp.seek(0, io.SEEK_END) |
| | length = fp.tell() |
| | offset = 0 |
| | elif i32(s) == 0xC6D3D0C5: |
| | |
| | |
| | |
| | |
| | |
| | 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: int = 1, transparency: bool = False |
| | ) -> Image.core.PixelAccess | None: |
| | |
| | if self.tile: |
| | assert self.fp is not None |
| | 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: |
| | |
| | |
| | pass |
| |
|
| |
|
| | |
| |
|
| |
|
| | def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None: |
| | """EPS Writer for the Python Imaging Library.""" |
| |
|
| | |
| | im.load() |
| |
|
| | |
| | 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: |
| | |
| | fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") |
| | fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") |
| | |
| | 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) |
| |
|
| | |
| | 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) |
| | 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, [ImageFile._Tile("eps", (0, 0) + im.size)]) |
| |
|
| | 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") |
| |
|