Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
EpsImagePlugin.py482 linesDownload Raw Back to PIL
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
23
24import io
25import os
26import re
27import subprocess
28import sys
29import tempfile
30from typing import IO
31
32from . import Image, ImageFile
33from ._binary import i32le as i32
34
35# --------------------------------------------------------------------
36
37
38split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
39field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
40
41gs_binary: str | bool | None = None
42gs_windows_binary = None
43
44
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
51
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
66
67
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)
81
82    # Unpack decoder tile
83    args = tile[0].args
84    assert isinstance(args, tuple)
85    length, bbox = args
86
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])
94
95    out_fd, outfile = tempfile.mkstemp()
96    os.close(out_fd)
97
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
105
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)
123
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"
131
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    ]
152
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
170
171
172def _accept(prefix: bytes) -> bool:
173    return prefix.startswith(b"%!PS") or (
174        len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5
175    )
176
177
178##
179# Image plugin for Encapsulated PostScript. This plugin supports only
180# a few variants of this format.
181
182
183class EpsImageFile(ImageFile.ImageFile):
184    """EPS File Parser for the Python Imaging Library"""
185
186    format = "EPS"
187    format_description = "Encapsulated Postscript"
188
189    mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
190
191    def _open(self) -> None:
192        assert self.fp is not None
193        length, offset = self._find_offset(self.fp)
194
195        # go to offset - start of "%!PS"
196        self.fp.seek(offset)
197
198        self._mode = "RGB"
199
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
204
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
211
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)
224
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
232
233            if not m:
234                return False
235
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
250
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
284
285            if reading_header_comments:
286                # Load EPS header
287
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
295
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
315
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
321
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                )
336
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
346
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
362
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)
368
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        )
374
375        self.tile = [
376            ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
377        ]
378
379    def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
380        s = fp.read(4)
381
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)
399
400        return length, offset
401
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)
413
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
418
419
420# --------------------------------------------------------------------
421
422
423def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
424    """EPS Writer for the Python Imaging Library."""
425
426    # make sure image data is available
427    im.load()
428
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)
439
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)
451
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()
463
464    ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])
465
466    fp.write(b"\n%%%%EndBinary\n")
467    fp.write(b"grestore end\n")
468    if hasattr(fp, "flush"):
469        fp.flush()
470
471
472# --------------------------------------------------------------------
473
474
475Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
476
477Image.register_save(EpsImageFile.format, _save)
478
479Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
480
481Image.register_mime(EpsImageFile.format, "application/postscript")
482 
codekingpro/portable-devtools · Team Ai