CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
PpmImagePlugin.py376 linesDownload Raw Back to PIL
1#
2# The Python Imaging Library.
3# $Id$
4#
5# PPM support for PIL
6#
7# History:
8#       96-03-24 fl     Created
9#       98-03-06 fl     Write RGBA images (as RGB, that is)
10#
11# Copyright (c) Secret Labs AB 1997-98.
12# Copyright (c) Fredrik Lundh 1996.
13#
14# See the README file for information on usage and redistribution.
15#
16from __future__ import annotations
17
18import math
19from typing import IO
20
21from . import Image, ImageFile
22from ._binary import i16be as i16
23from ._binary import o8
24from ._binary import o32le as o32
25
26#
27# --------------------------------------------------------------------
28
29b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d"
30
31MODES = {
32    # standard
33    b"P1": "1",
34    b"P2": "L",
35    b"P3": "RGB",
36    b"P4": "1",
37    b"P5": "L",
38    b"P6": "RGB",
39    # extensions
40    b"P0CMYK": "CMYK",
41    b"Pf": "F",
42    # PIL extensions (for test purposes only)
43    b"PyP": "P",
44    b"PyRGBA": "RGBA",
45    b"PyCMYK": "CMYK",
46}
47
48
49def _accept(prefix: bytes) -> bool:
50    return len(prefix) >= 2 and prefix.startswith(b"P") and prefix[1] in b"0123456fy"
51
52
53##
54# Image plugin for PBM, PGM, and PPM images.
55
56
57class PpmImageFile(ImageFile.ImageFile):
58    format = "PPM"
59    format_description = "Pbmplus image"
60
61    def _read_magic(self) -> bytes:
62        assert self.fp is not None
63
64        magic = b""
65        # read until whitespace or longest available magic number
66        for _ in range(6):
67            c = self.fp.read(1)
68            if not c or c in b_whitespace:
69                break
70            magic += c
71        return magic
72
73    def _read_token(self) -> bytes:
74        assert self.fp is not None
75
76        token = b""
77        while len(token) <= 10:  # read until next whitespace or limit of 10 characters
78            c = self.fp.read(1)
79            if not c:
80                break
81            elif c in b_whitespace:  # token ended
82                if not token:
83                    # skip whitespace at start
84                    continue
85                break
86            elif c == b"#":
87                # ignores rest of the line; stops at CR, LF or EOF
88                while self.fp.read(1) not in b"\r\n":
89                    pass
90                continue
91            token += c
92        if not token:
93            # Token was not even 1 byte
94            msg = "Reached EOF while reading header"
95            raise ValueError(msg)
96        elif len(token) > 10:
97            msg_too_long = b"Token too long in file header: %s" % token
98            raise ValueError(msg_too_long)
99        return token
100
101    def _open(self) -> None:
102        assert self.fp is not None
103
104        magic_number = self._read_magic()
105        try:
106            mode = MODES[magic_number]
107        except KeyError:
108            msg = "not a PPM file"
109            raise SyntaxError(msg)
110        self._mode = mode
111
112        if magic_number in (b"P1", b"P4"):
113            self.custom_mimetype = "image/x-portable-bitmap"
114        elif magic_number in (b"P2", b"P5"):
115            self.custom_mimetype = "image/x-portable-graymap"
116        elif magic_number in (b"P3", b"P6"):
117            self.custom_mimetype = "image/x-portable-pixmap"
118
119        self._size = int(self._read_token()), int(self._read_token())
120
121        decoder_name = "raw"
122        if magic_number in (b"P1", b"P2", b"P3"):
123            decoder_name = "ppm_plain"
124
125        args: str | tuple[str | int, ...]
126        if mode == "1":
127            args = "1;I"
128        elif mode == "F":
129            scale = float(self._read_token())
130            if scale == 0.0 or not math.isfinite(scale):
131                msg = "scale must be finite and non-zero"
132                raise ValueError(msg)
133            self.info["scale"] = abs(scale)
134
135            rawmode = "F;32F" if scale < 0 else "F;32BF"
136            args = (rawmode, 0, -1)
137        else:
138            maxval = int(self._read_token())
139            if not 0 < maxval < 65536:
140                msg = "maxval must be greater than 0 and less than 65536"
141                raise ValueError(msg)
142            if maxval > 255 and mode == "L":
143                self._mode = "I"
144
145            rawmode = mode
146            if decoder_name != "ppm_plain":
147                # If maxval matches a bit depth, use the raw decoder directly
148                if maxval == 65535 and mode == "L":
149                    rawmode = "I;16B"
150                elif maxval != 255:
151                    decoder_name = "ppm"
152
153            args = rawmode if decoder_name == "raw" else (rawmode, maxval)
154        self.tile = [
155            ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args)
156        ]
157
158
159#
160# --------------------------------------------------------------------
161
162
163class PpmPlainDecoder(ImageFile.PyDecoder):
164    _pulls_fd = True
165    _comment_spans: bool
166
167    def _read_block(self) -> bytes:
168        assert self.fd is not None
169
170        return self.fd.read(ImageFile.SAFEBLOCK)
171
172    def _find_comment_end(self, block: bytes, start: int = 0) -> int:
173        a = block.find(b"\n", start)
174        b = block.find(b"\r", start)
175        return min(a, b) if a * b > 0 else max(a, b)  # lowest nonnegative index (or -1)
176
177    def _ignore_comments(self, block: bytes) -> bytes:
178        if self._comment_spans:
179            # Finish current comment
180            while block:
181                comment_end = self._find_comment_end(block)
182                if comment_end != -1:
183                    # Comment ends in this block
184                    # Delete tail of comment
185                    block = block[comment_end + 1 :]
186                    break
187                else:
188                    # Comment spans whole block
189                    # So read the next block, looking for the end
190                    block = self._read_block()
191
192        # Search for any further comments
193        self._comment_spans = False
194        while True:
195            comment_start = block.find(b"#")
196            if comment_start == -1:
197                # No comment found
198                break
199            comment_end = self._find_comment_end(block, comment_start)
200            if comment_end != -1:
201                # Comment ends in this block
202                # Delete comment
203                block = block[:comment_start] + block[comment_end + 1 :]
204            else:
205                # Comment continues to next block(s)
206                block = block[:comment_start]
207                self._comment_spans = True
208                break
209        return block
210
211    def _decode_bitonal(self) -> bytearray:
212        """
213        This is a separate method because in the plain PBM format, all data tokens are
214        exactly one byte, so the inter-token whitespace is optional.
215        """
216        data = bytearray()
217        total_bytes = self.state.xsize * self.state.ysize
218
219        while len(data) != total_bytes:
220            block = self._read_block()  # read next block
221            if not block:
222                # eof
223                break
224
225            block = self._ignore_comments(block)
226
227            tokens = b"".join(block.split())
228            for token in tokens:
229                if token not in (48, 49):
230                    msg = b"Invalid token for this mode: %s" % bytes([token])
231                    raise ValueError(msg)
232            data = (data + tokens)[:total_bytes]
233        invert = bytes.maketrans(b"01", b"\xff\x00")
234        return data.translate(invert)
235
236    def _decode_blocks(self, maxval: int) -> bytearray:
237        data = bytearray()
238        max_len = 10
239        out_byte_count = 4 if self.mode == "I" else 1
240        out_max = 65535 if self.mode == "I" else 255
241        bands = Image.getmodebands(self.mode)
242        total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count
243
244        half_token = b""
245        while len(data) != total_bytes:
246            block = self._read_block()  # read next block
247            if not block:
248                if half_token:
249                    block = bytearray(b" ")  # flush half_token
250                else:
251                    # eof
252                    break
253
254            block = self._ignore_comments(block)
255
256            if half_token:
257                block = half_token + block  # stitch half_token to new block
258                half_token = b""
259
260            tokens = block.split()
261
262            if block and not block[-1:].isspace():  # block might split token
263                half_token = tokens.pop()  # save half token for later
264                if len(half_token) > max_len:  # prevent buildup of half_token
265                    msg = (
266                        b"Token too long found in data: %s" % half_token[: max_len + 1]
267                    )
268                    raise ValueError(msg)
269
270            for token in tokens:
271                if len(token) > max_len:
272                    msg = b"Token too long found in data: %s" % token[: max_len + 1]
273                    raise ValueError(msg)
274                value = int(token)
275                if value < 0:
276                    msg_str = f"Channel value is negative: {value}"
277                    raise ValueError(msg_str)
278                if value > maxval:
279                    msg_str = f"Channel value too large for this mode: {value}"
280                    raise ValueError(msg_str)
281                value = round(value / maxval * out_max)
282                data += o32(value) if self.mode == "I" else o8(value)
283                if len(data) == total_bytes:  # finished!
284                    break
285        return data
286
287    def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
288        self._comment_spans = False
289        if self.mode == "1":
290            data = self._decode_bitonal()
291            rawmode = "1;8"
292        else:
293            maxval = self.args[-1]
294            data = self._decode_blocks(maxval)
295            rawmode = "I;32" if self.mode == "I" else self.mode
296        self.set_as_raw(bytes(data), rawmode)
297        return -1, 0
298
299
300class PpmDecoder(ImageFile.PyDecoder):
301    _pulls_fd = True
302
303    def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
304        assert self.fd is not None
305
306        data = bytearray()
307        maxval = self.args[-1]
308        in_byte_count = 1 if maxval < 256 else 2
309        out_byte_count = 4 if self.mode == "I" else 1
310        out_max = 65535 if self.mode == "I" else 255
311        bands = Image.getmodebands(self.mode)
312        dest_length = self.state.xsize * self.state.ysize * bands * out_byte_count
313        while len(data) < dest_length:
314            pixels = self.fd.read(in_byte_count * bands)
315            if len(pixels) < in_byte_count * bands:
316                # eof
317                break
318            for b in range(bands):
319                value = (
320                    pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count)
321                )
322                value = min(out_max, round(value / maxval * out_max))
323                data += o32(value) if self.mode == "I" else o8(value)
324        rawmode = "I;32" if self.mode == "I" else self.mode
325        self.set_as_raw(bytes(data), rawmode)
326        return -1, 0
327
328
329#
330# --------------------------------------------------------------------
331
332
333def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
334    if im.mode == "1":
335        rawmode, head = "1;I", b"P4"
336    elif im.mode == "L":
337        rawmode, head = "L", b"P5"
338    elif im.mode in ("I", "I;16"):
339        rawmode, head = "I;16B", b"P5"
340    elif im.mode in ("RGB", "RGBA"):
341        rawmode, head = "RGB", b"P6"
342    elif im.mode == "F":
343        rawmode, head = "F;32F", b"Pf"
344    else:
345        msg = f"cannot write mode {im.mode} as PPM"
346        raise OSError(msg)
347    fp.write(head + b"\n%d %d\n" % im.size)
348    if head == b"P6":
349        fp.write(b"255\n")
350    elif head == b"P5":
351        if rawmode == "L":
352            fp.write(b"255\n")
353        else:
354            fp.write(b"65535\n")
355    elif head == b"Pf":
356        fp.write(b"-1.0\n")
357    row_order = -1 if im.mode == "F" else 1
358    ImageFile._save(
359        im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]
360    )
361
362
363#
364# --------------------------------------------------------------------
365
366
367Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
368Image.register_save(PpmImageFile.format, _save)
369
370Image.register_decoder("ppm", PpmDecoder)
371Image.register_decoder("ppm_plain", PpmPlainDecoder)
372
373Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"])
374
375Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")
376 
Aluode/PerceptionLabPortable · CoolFace