Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
ImImagePlugin.py391 linesDownload Raw Back to PIL
1#
2# The Python Imaging Library.
3# $Id$
4#
5# IFUNC IM file handling for PIL
6#
7# history:
8# 1995-09-01 fl   Created.
9# 1997-01-03 fl   Save palette images
10# 1997-01-08 fl   Added sequence support
11# 1997-01-23 fl   Added P and RGB save support
12# 1997-05-31 fl   Read floating point images
13# 1997-06-22 fl   Save floating point images
14# 1997-08-27 fl   Read and save 1-bit images
15# 1998-06-25 fl   Added support for RGB+LUT images
16# 1998-07-02 fl   Added support for YCC images
17# 1998-07-15 fl   Renamed offset attribute to avoid name clash
18# 1998-12-29 fl   Added I;16 support
19# 2001-02-17 fl   Use 're' instead of 'regex' (Python 2.1) (0.7)
20# 2003-09-26 fl   Added LA/PA support
21#
22# Copyright (c) 1997-2003 by Secret Labs AB.
23# Copyright (c) 1995-2001 by Fredrik Lundh.
24#
25# See the README file for information on usage and redistribution.
26#
27from __future__ import annotations
28
29import os
30import re
31from typing import IO, Any
32
33from . import Image, ImageFile, ImagePalette
34from ._util import DeferredError
35
36# --------------------------------------------------------------------
37# Standard tags
38
39COMMENT = "Comment"
40DATE = "Date"
41EQUIPMENT = "Digitalization equipment"
42FRAMES = "File size (no of images)"
43LUT = "Lut"
44NAME = "Name"
45SCALE = "Scale (x,y)"
46SIZE = "Image size (x*y)"
47MODE = "Image type"
48
49TAGS = {
50    COMMENT: 0,
51    DATE: 0,
52    EQUIPMENT: 0,
53    FRAMES: 0,
54    LUT: 0,
55    NAME: 0,
56    SCALE: 0,
57    SIZE: 0,
58    MODE: 0,
59}
60
61OPEN = {
62    # ifunc93/p3cfunc formats
63    "0 1 image": ("1", "1"),
64    "L 1 image": ("1", "1"),
65    "Greyscale image": ("L", "L"),
66    "Grayscale image": ("L", "L"),
67    "RGB image": ("RGB", "RGB;L"),
68    "RLB image": ("RGB", "RLB"),
69    "RYB image": ("RGB", "RLB"),
70    "B1 image": ("1", "1"),
71    "B2 image": ("P", "P;2"),
72    "B4 image": ("P", "P;4"),
73    "X 24 image": ("RGB", "RGB"),
74    "L 32 S image": ("I", "I;32"),
75    "L 32 F image": ("F", "F;32"),
76    # old p3cfunc formats
77    "RGB3 image": ("RGB", "RGB;T"),
78    "RYB3 image": ("RGB", "RYB;T"),
79    # extensions
80    "LA image": ("LA", "LA;L"),
81    "PA image": ("LA", "PA;L"),
82    "RGBA image": ("RGBA", "RGBA;L"),
83    "RGBX image": ("RGB", "RGBX;L"),
84    "CMYK image": ("CMYK", "CMYK;L"),
85    "YCC image": ("YCbCr", "YCbCr;L"),
86}
87
88# ifunc95 extensions
89for i in ["8", "8S", "16", "16S", "32", "32F"]:
90    OPEN[f"L {i} image"] = ("F", f"F;{i}")
91    OPEN[f"L*{i} image"] = ("F", f"F;{i}")
92for i in ["16", "16L", "16B"]:
93    OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}")
94    OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}")
95for i in ["32S"]:
96    OPEN[f"L {i} image"] = ("I", f"I;{i}")
97    OPEN[f"L*{i} image"] = ("I", f"I;{i}")
98for j in range(2, 33):
99    OPEN[f"L*{j} image"] = ("F", f"F;{j}")
100
101
102# --------------------------------------------------------------------
103# Read IM directory
104
105split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
106
107
108def number(s: Any) -> float:
109    try:
110        return int(s)
111    except ValueError:
112        return float(s)
113
114
115##
116# Image plugin for the IFUNC IM file format.
117
118
119class ImImageFile(ImageFile.ImageFile):
120    format = "IM"
121    format_description = "IFUNC Image Memory"
122    _close_exclusive_fp_after_loading = False
123
124    def _open(self) -> None:
125        # Quick rejection: if there's not an LF among the first
126        # 100 bytes, this is (probably) not a text header.
127
128        assert self.fp is not None
129        if b"\n" not in self.fp.read(100):
130            msg = "not an IM file"
131            raise SyntaxError(msg)
132        self.fp.seek(0)
133
134        n = 0
135
136        # Default values
137        self.info[MODE] = "L"
138        self.info[SIZE] = (512, 512)
139        self.info[FRAMES] = 1
140
141        self.rawmode = "L"
142
143        while True:
144            s = self.fp.read(1)
145
146            # Some versions of IFUNC uses \n\r instead of \r\n...
147            if s == b"\r":
148                continue
149
150            if not s or s == b"\0" or s == b"\x1a":
151                break
152
153            # FIXME: this may read whole file if not a text file
154            s = s + self.fp.readline()
155
156            if len(s) > 100:
157                msg = "not an IM file"
158                raise SyntaxError(msg)
159
160            if s.endswith(b"\r\n"):
161                s = s[:-2]
162            elif s.endswith(b"\n"):
163                s = s[:-1]
164
165            try:
166                m = split.match(s)
167            except re.error as e:
168                msg = "not an IM file"
169                raise SyntaxError(msg) from e
170
171            if m:
172                k, v = m.group(1, 2)
173
174                # Don't know if this is the correct encoding,
175                # but a decent guess (I guess)
176                k = k.decode("latin-1", "replace")
177                v = v.decode("latin-1", "replace")
178
179                # Convert value as appropriate
180                if k in [FRAMES, SCALE, SIZE]:
181                    v = v.replace("*", ",")
182                    v = tuple(map(number, v.split(",")))
183                    if len(v) == 1:
184                        v = v[0]
185                elif k == MODE and v in OPEN:
186                    v, self.rawmode = OPEN[v]
187
188                # Add to dictionary. Note that COMMENT tags are
189                # combined into a list of strings.
190                if k == COMMENT:
191                    if k in self.info:
192                        self.info[k].append(v)
193                    else:
194                        self.info[k] = [v]
195                else:
196                    self.info[k] = v
197
198                if k in TAGS:
199                    n += 1
200
201            else:
202                msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}"
203                raise SyntaxError(msg)
204
205        if not n:
206            msg = "Not an IM file"
207            raise SyntaxError(msg)
208
209        # Basic attributes
210        self._size = self.info[SIZE]
211        self._mode = self.info[MODE]
212
213        # Skip forward to start of image data
214        while s and not s.startswith(b"\x1a"):
215            s = self.fp.read(1)
216        if not s:
217            msg = "File truncated"
218            raise SyntaxError(msg)
219
220        if LUT in self.info:
221            # convert lookup table to palette or lut attribute
222            palette = self.fp.read(768)
223            greyscale = 1  # greyscale palette
224            linear = 1  # linear greyscale palette
225            for i in range(256):
226                if palette[i] == palette[i + 256] == palette[i + 512]:
227                    if palette[i] != i:
228                        linear = 0
229                else:
230                    greyscale = 0
231            if self.mode in ["L", "LA", "P", "PA"]:
232                if greyscale:
233                    if not linear:
234                        self.lut = list(palette[:256])
235                else:
236                    if self.mode in ["L", "P"]:
237                        self._mode = self.rawmode = "P"
238                    elif self.mode in ["LA", "PA"]:
239                        self._mode = "PA"
240                        self.rawmode = "PA;L"
241                    self.palette = ImagePalette.raw("RGB;L", palette)
242            elif self.mode == "RGB":
243                if not greyscale or not linear:
244                    self.lut = list(palette)
245
246        self.frame = 0
247
248        self.__offset = offs = self.fp.tell()
249
250        self._fp = self.fp  # FIXME: hack
251
252        if self.rawmode.startswith("F;"):
253            # ifunc95 formats
254            try:
255                # use bit decoder (if necessary)
256                bits = int(self.rawmode[2:])
257                if bits not in [8, 16, 32]:
258                    self.tile = [
259                        ImageFile._Tile(
260                            "bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1)
261                        )
262                    ]
263                    return
264            except ValueError:
265                pass
266
267        if self.rawmode in ["RGB;T", "RYB;T"]:
268            # Old LabEye/3PC files.  Would be very surprised if anyone
269            # ever stumbled upon such a file ;-)
270            size = self.size[0] * self.size[1]
271            self.tile = [
272                ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
273                ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
274                ImageFile._Tile(
275                    "raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)
276                ),
277            ]
278        else:
279            # LabEye/IFUNC files
280            self.tile = [
281                ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
282            ]
283
284    @property
285    def n_frames(self) -> int:
286        return self.info[FRAMES]
287
288    @property
289    def is_animated(self) -> bool:
290        return self.info[FRAMES] > 1
291
292    def seek(self, frame: int) -> None:
293        if not self._seek_check(frame):
294            return
295        if isinstance(self._fp, DeferredError):
296            raise self._fp.ex
297
298        self.frame = frame
299
300        if self.mode == "1":
301            bits = 1
302        else:
303            bits = 8 * len(self.mode)
304
305        size = ((self.size[0] * bits + 7) // 8) * self.size[1]
306        offs = self.__offset + frame * size
307
308        self.fp = self._fp
309
310        self.tile = [
311            ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
312        ]
313
314    def tell(self) -> int:
315        return self.frame
316
317
318#
319# --------------------------------------------------------------------
320# Save IM files
321
322
323SAVE = {
324    # mode: (im type, raw mode)
325    "1": ("0 1", "1"),
326    "L": ("Greyscale", "L"),
327    "LA": ("LA", "LA;L"),
328    "P": ("Greyscale", "P"),
329    "PA": ("LA", "PA;L"),
330    "I": ("L 32S", "I;32S"),
331    "I;16": ("L 16", "I;16"),
332    "I;16L": ("L 16L", "I;16L"),
333    "I;16B": ("L 16B", "I;16B"),
334    "F": ("L 32F", "F;32F"),
335    "RGB": ("RGB", "RGB;L"),
336    "RGBA": ("RGBA", "RGBA;L"),
337    "RGBX": ("RGBX", "RGBX;L"),
338    "CMYK": ("CMYK", "CMYK;L"),
339    "YCbCr": ("YCC", "YCbCr;L"),
340}
341
342
343def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
344    try:
345        image_type, rawmode = SAVE[im.mode]
346    except KeyError as e:
347        msg = f"Cannot save {im.mode} images as IM"
348        raise ValueError(msg) from e
349
350    frames = im.encoderinfo.get("frames", 1)
351
352    fp.write(f"Image type: {image_type} image\r\n".encode("ascii"))
353    if filename:
354        # Each line must be 100 characters or less,
355        # or: SyntaxError("not an IM file")
356        # 8 characters are used for "Name: " and "\r\n"
357        # Keep just the filename, ditch the potentially overlong path
358        if isinstance(filename, bytes):
359            filename = filename.decode("ascii")
360        name, ext = os.path.splitext(os.path.basename(filename))
361        name = "".join([name[: 92 - len(ext)], ext])
362
363        fp.write(f"Name: {name}\r\n".encode("ascii"))
364    fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii"))
365    fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
366    if im.mode in ["P", "PA"]:
367        fp.write(b"Lut: 1\r\n")
368    fp.write(b"\000" * (511 - fp.tell()) + b"\032")
369    if im.mode in ["P", "PA"]:
370        im_palette = im.im.getpalette("RGB", "RGB;L")
371        colors = len(im_palette) // 3
372        palette = b""
373        for i in range(3):
374            palette += im_palette[colors * i : colors * (i + 1)]
375            palette += b"\x00" * (256 - colors)
376        fp.write(palette)  # 768 bytes
377    ImageFile._save(
378        im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]
379    )
380
381
382#
383# --------------------------------------------------------------------
384# Registry
385
386
387Image.register_open(ImImageFile.format, ImImageFile)
388Image.register_save(ImImageFile.format, _save)
389
390Image.register_extension(ImImageFile.format, ".im")
391 
codekingpro/portable-devtools · Team Ai