| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | 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:
|
| | (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
|
| | 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:
|
| | 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")
|
| |
|