about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/PIL/PsdImagePlugin.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/PIL/PsdImagePlugin.py')
-rw-r--r--.venv/lib/python3.12/site-packages/PIL/PsdImagePlugin.py332
1 files changed, 332 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/PIL/PsdImagePlugin.py b/.venv/lib/python3.12/site-packages/PIL/PsdImagePlugin.py
new file mode 100644
index 00000000..8ff5e390
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/PIL/PsdImagePlugin.py
@@ -0,0 +1,332 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# Adobe PSD 2.5/3.0 file handling
+#
+# History:
+# 1995-09-01 fl   Created
+# 1997-01-03 fl   Read most PSD images
+# 1997-01-18 fl   Fixed P and CMYK support
+# 2001-10-21 fl   Added seek/tell support (for layers)
+#
+# Copyright (c) 1997-2001 by Secret Labs AB.
+# Copyright (c) 1995-2001 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import io
+from functools import cached_property
+from typing import IO
+
+from . import Image, ImageFile, ImagePalette
+from ._binary import i8
+from ._binary import i16be as i16
+from ._binary import i32be as i32
+from ._binary import si16be as si16
+from ._binary import si32be as si32
+
+MODES = {
+    # (photoshop mode, bits) -> (pil mode, required channels)
+    (0, 1): ("1", 1),
+    (0, 8): ("L", 1),
+    (1, 8): ("L", 1),
+    (2, 8): ("P", 1),
+    (3, 8): ("RGB", 3),
+    (4, 8): ("CMYK", 4),
+    (7, 8): ("L", 1),  # FIXME: multilayer
+    (8, 8): ("L", 1),  # duotone
+    (9, 8): ("LAB", 3),
+}
+
+
+# --------------------------------------------------------------------.
+# read PSD images
+
+
+def _accept(prefix: bytes) -> bool:
+    return prefix[:4] == b"8BPS"
+
+
+##
+# Image plugin for Photoshop images.
+
+
+class PsdImageFile(ImageFile.ImageFile):
+    format = "PSD"
+    format_description = "Adobe Photoshop"
+    _close_exclusive_fp_after_loading = False
+
+    def _open(self) -> None:
+        read = self.fp.read
+
+        #
+        # header
+
+        s = read(26)
+        if not _accept(s) or i16(s, 4) != 1:
+            msg = "not a PSD file"
+            raise SyntaxError(msg)
+
+        psd_bits = i16(s, 22)
+        psd_channels = i16(s, 12)
+        psd_mode = i16(s, 24)
+
+        mode, channels = MODES[(psd_mode, psd_bits)]
+
+        if channels > psd_channels:
+            msg = "not enough channels"
+            raise OSError(msg)
+        if mode == "RGB" and psd_channels == 4:
+            mode = "RGBA"
+            channels = 4
+
+        self._mode = mode
+        self._size = i32(s, 18), i32(s, 14)
+
+        #
+        # color mode data
+
+        size = i32(read(4))
+        if size:
+            data = read(size)
+            if mode == "P" and size == 768:
+                self.palette = ImagePalette.raw("RGB;L", data)
+
+        #
+        # image resources
+
+        self.resources = []
+
+        size = i32(read(4))
+        if size:
+            # load resources
+            end = self.fp.tell() + size
+            while self.fp.tell() < end:
+                read(4)  # signature
+                id = i16(read(2))
+                name = read(i8(read(1)))
+                if not (len(name) & 1):
+                    read(1)  # padding
+                data = read(i32(read(4)))
+                if len(data) & 1:
+                    read(1)  # padding
+                self.resources.append((id, name, data))
+                if id == 1039:  # ICC profile
+                    self.info["icc_profile"] = data
+
+        #
+        # layer and mask information
+
+        self._layers_position = None
+
+        size = i32(read(4))
+        if size:
+            end = self.fp.tell() + size
+            size = i32(read(4))
+            if size:
+                self._layers_position = self.fp.tell()
+                self._layers_size = size
+            self.fp.seek(end)
+        self._n_frames: int | None = None
+
+        #
+        # image descriptor
+
+        self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels)
+
+        # keep the file open
+        self._fp = self.fp
+        self.frame = 1
+        self._min_frame = 1
+
+    @cached_property
+    def layers(
+        self,
+    ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
+        layers = []
+        if self._layers_position is not None:
+            self._fp.seek(self._layers_position)
+            _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size))
+            layers = _layerinfo(_layer_data, self._layers_size)
+        self._n_frames = len(layers)
+        return layers
+
+    @property
+    def n_frames(self) -> int:
+        if self._n_frames is None:
+            self._n_frames = len(self.layers)
+        return self._n_frames
+
+    @property
+    def is_animated(self) -> bool:
+        return len(self.layers) > 1
+
+    def seek(self, layer: int) -> None:
+        if not self._seek_check(layer):
+            return
+
+        # seek to given layer (1..max)
+        try:
+            _, mode, _, tile = self.layers[layer - 1]
+            self._mode = mode
+            self.tile = tile
+            self.frame = layer
+            self.fp = self._fp
+        except IndexError as e:
+            msg = "no such layer"
+            raise EOFError(msg) from e
+
+    def tell(self) -> int:
+        # return layer number (0=image, 1..max=layers)
+        return self.frame
+
+
+def _layerinfo(
+    fp: IO[bytes], ct_bytes: int
+) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
+    # read layerinfo block
+    layers = []
+
+    def read(size: int) -> bytes:
+        return ImageFile._safe_read(fp, size)
+
+    ct = si16(read(2))
+
+    # sanity check
+    if ct_bytes < (abs(ct) * 20):
+        msg = "Layer block too short for number of layers requested"
+        raise SyntaxError(msg)
+
+    for _ in range(abs(ct)):
+        # bounding box
+        y0 = si32(read(4))
+        x0 = si32(read(4))
+        y1 = si32(read(4))
+        x1 = si32(read(4))
+
+        # image info
+        bands = []
+        ct_types = i16(read(2))
+        if ct_types > 4:
+            fp.seek(ct_types * 6 + 12, io.SEEK_CUR)
+            size = i32(read(4))
+            fp.seek(size, io.SEEK_CUR)
+            continue
+
+        for _ in range(ct_types):
+            type = i16(read(2))
+
+            if type == 65535:
+                b = "A"
+            else:
+                b = "RGBA"[type]
+
+            bands.append(b)
+            read(4)  # size
+
+        # figure out the image mode
+        bands.sort()
+        if bands == ["R"]:
+            mode = "L"
+        elif bands == ["B", "G", "R"]:
+            mode = "RGB"
+        elif bands == ["A", "B", "G", "R"]:
+            mode = "RGBA"
+        else:
+            mode = ""  # unknown
+
+        # skip over blend flags and extra information
+        read(12)  # filler
+        name = ""
+        size = i32(read(4))  # length of the extra data field
+        if size:
+            data_end = fp.tell() + size
+
+            length = i32(read(4))
+            if length:
+                fp.seek(length - 16, io.SEEK_CUR)
+
+            length = i32(read(4))
+            if length:
+                fp.seek(length, io.SEEK_CUR)
+
+            length = i8(read(1))
+            if length:
+                # Don't know the proper encoding,
+                # Latin-1 should be a good guess
+                name = read(length).decode("latin-1", "replace")
+
+            fp.seek(data_end)
+        layers.append((name, mode, (x0, y0, x1, y1)))
+
+    # get tiles
+    layerinfo = []
+    for i, (name, mode, bbox) in enumerate(layers):
+        tile = []
+        for m in mode:
+            t = _maketile(fp, m, bbox, 1)
+            if t:
+                tile.extend(t)
+        layerinfo.append((name, mode, bbox, tile))
+
+    return layerinfo
+
+
+def _maketile(
+    file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int
+) -> list[ImageFile._Tile]:
+    tiles = []
+    read = file.read
+
+    compression = i16(read(2))
+
+    xsize = bbox[2] - bbox[0]
+    ysize = bbox[3] - bbox[1]
+
+    offset = file.tell()
+
+    if compression == 0:
+        #
+        # raw compression
+        for channel in range(channels):
+            layer = mode[channel]
+            if mode == "CMYK":
+                layer += ";I"
+            tiles.append(ImageFile._Tile("raw", bbox, offset, layer))
+            offset = offset + xsize * ysize
+
+    elif compression == 1:
+        #
+        # packbits compression
+        i = 0
+        bytecount = read(channels * ysize * 2)
+        offset = file.tell()
+        for channel in range(channels):
+            layer = mode[channel]
+            if mode == "CMYK":
+                layer += ";I"
+            tiles.append(ImageFile._Tile("packbits", bbox, offset, layer))
+            for y in range(ysize):
+                offset = offset + i16(bytecount, i)
+                i += 2
+
+    file.seek(offset)
+
+    if offset & 1:
+        read(1)  # padding
+
+    return tiles
+
+
+# --------------------------------------------------------------------
+# registry
+
+
+Image.register_open(PsdImageFile.format, PsdImageFile, _accept)
+
+Image.register_extension(PsdImageFile.format, ".psd")
+
+Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop")