CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
JpegImagePlugin.py896 linesDownload Raw Back to PIL
1#
2# The Python Imaging Library.
3# $Id$
4#
5# JPEG (JFIF) file handling
6#
7# See "Digital Compression and Coding of Continuous-Tone Still Images,
8# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1)
9#
10# History:
11# 1995-09-09 fl   Created
12# 1995-09-13 fl   Added full parser
13# 1996-03-25 fl   Added hack to use the IJG command line utilities
14# 1996-05-05 fl   Workaround Photoshop 2.5 CMYK polarity bug
15# 1996-05-28 fl   Added draft support, JFIF version (0.1)
16# 1996-12-30 fl   Added encoder options, added progression property (0.2)
17# 1997-08-27 fl   Save mode 1 images as BW (0.3)
18# 1998-07-12 fl   Added YCbCr to draft and save methods (0.4)
19# 1998-10-19 fl   Don't hang on files using 16-bit DQT's (0.4.1)
20# 2001-04-16 fl   Extract DPI settings from JFIF files (0.4.2)
21# 2002-07-01 fl   Skip pad bytes before markers; identify Exif files (0.4.3)
22# 2003-04-25 fl   Added experimental EXIF decoder (0.5)
23# 2003-06-06 fl   Added experimental EXIF GPSinfo decoder
24# 2003-09-13 fl   Extract COM markers
25# 2009-09-06 fl   Added icc_profile support (from Florian Hoech)
26# 2009-03-06 fl   Changed CMYK handling; always use Adobe polarity (0.6)
27# 2009-03-08 fl   Added subsampling support (from Justin Huff).
28#
29# Copyright (c) 1997-2003 by Secret Labs AB.
30# Copyright (c) 1995-1996 by Fredrik Lundh.
31#
32# See the README file for information on usage and redistribution.
33#
34from __future__ import annotations
35
36import array
37import io
38import math
39import os
40import struct
41import subprocess
42import sys
43import tempfile
44import warnings
45
46from . import Image, ImageFile
47from ._binary import i16be as i16
48from ._binary import i32be as i32
49from ._binary import o8
50from ._binary import o16be as o16
51from .JpegPresets import presets
52
53TYPE_CHECKING = False
54if TYPE_CHECKING:
55    from typing import IO, Any
56
57    from .MpoImagePlugin import MpoImageFile
58
59#
60# Parser
61
62
63def Skip(self: JpegImageFile, marker: int) -> None:
64    assert self.fp is not None
65    n = i16(self.fp.read(2)) - 2
66    ImageFile._safe_read(self.fp, n)
67
68
69def APP(self: JpegImageFile, marker: int) -> None:
70    #
71    # Application marker.  Store these in the APP dictionary.
72    # Also look for well-known application markers.
73
74    assert self.fp is not None
75    n = i16(self.fp.read(2)) - 2
76    s = ImageFile._safe_read(self.fp, n)
77
78    app = f"APP{marker & 15}"
79
80    self.app[app] = s  # compatibility
81    self.applist.append((app, s))
82
83    if marker == 0xFFE0 and s.startswith(b"JFIF"):
84        # extract JFIF information
85        self.info["jfif"] = version = i16(s, 5)  # version
86        self.info["jfif_version"] = divmod(version, 256)
87        # extract JFIF properties
88        try:
89            jfif_unit = s[7]
90            jfif_density = i16(s, 8), i16(s, 10)
91        except Exception:
92            pass
93        else:
94            if jfif_unit == 1:
95                self.info["dpi"] = jfif_density
96            elif jfif_unit == 2:  # cm
97                # 1 dpcm = 2.54 dpi
98                self.info["dpi"] = tuple(d * 2.54 for d in jfif_density)
99            self.info["jfif_unit"] = jfif_unit
100            self.info["jfif_density"] = jfif_density
101    elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"):
102        # extract EXIF information
103        if "exif" in self.info:
104            self.info["exif"] += s[6:]
105        else:
106            self.info["exif"] = s
107            self._exif_offset = self.fp.tell() - n + 6
108    elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"):
109        self.info["xmp"] = s.split(b"\x00", 1)[1]
110    elif marker == 0xFFE2 and s.startswith(b"FPXR\0"):
111        # extract FlashPix information (incomplete)
112        self.info["flashpix"] = s  # FIXME: value will change
113    elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"):
114        # Since an ICC profile can be larger than the maximum size of
115        # a JPEG marker (64K), we need provisions to split it into
116        # multiple markers. The format defined by the ICC specifies
117        # one or more APP2 markers containing the following data:
118        #   Identifying string      ASCII "ICC_PROFILE\0"  (12 bytes)
119        #   Marker sequence number  1, 2, etc (1 byte)
120        #   Number of markers       Total of APP2's used (1 byte)
121        #   Profile data            (remainder of APP2 data)
122        # Decoders should use the marker sequence numbers to
123        # reassemble the profile, rather than assuming that the APP2
124        # markers appear in the correct sequence.
125        self.icclist.append(s)
126    elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"):
127        # parse the image resource block
128        offset = 14
129        photoshop = self.info.setdefault("photoshop", {})
130        while s[offset : offset + 4] == b"8BIM":
131            try:
132                offset += 4
133                # resource code
134                code = i16(s, offset)
135                offset += 2
136                # resource name (usually empty)
137                name_len = s[offset]
138                # name = s[offset+1:offset+1+name_len]
139                offset += 1 + name_len
140                offset += offset & 1  # align
141                # resource data block
142                size = i32(s, offset)
143                offset += 4
144                data = s[offset : offset + size]
145                if code == 0x03ED:  # ResolutionInfo
146                    photoshop[code] = {
147                        "XResolution": i32(data, 0) / 65536,
148                        "DisplayedUnitsX": i16(data, 4),
149                        "YResolution": i32(data, 8) / 65536,
150                        "DisplayedUnitsY": i16(data, 12),
151                    }
152                else:
153                    photoshop[code] = data
154                offset += size
155                offset += offset & 1  # align
156            except struct.error:
157                break  # insufficient data
158
159    elif marker == 0xFFEE and s.startswith(b"Adobe"):
160        self.info["adobe"] = i16(s, 5)
161        # extract Adobe custom properties
162        try:
163            adobe_transform = s[11]
164        except IndexError:
165            pass
166        else:
167            self.info["adobe_transform"] = adobe_transform
168    elif marker == 0xFFE2 and s.startswith(b"MPF\0"):
169        # extract MPO information
170        self.info["mp"] = s[4:]
171        # offset is current location minus buffer size
172        # plus constant header size
173        self.info["mpoffset"] = self.fp.tell() - n + 4
174
175
176def COM(self: JpegImageFile, marker: int) -> None:
177    #
178    # Comment marker.  Store these in the APP dictionary.
179    assert self.fp is not None
180    n = i16(self.fp.read(2)) - 2
181    s = ImageFile._safe_read(self.fp, n)
182
183    self.info["comment"] = s
184    self.app["COM"] = s  # compatibility
185    self.applist.append(("COM", s))
186
187
188def SOF(self: JpegImageFile, marker: int) -> None:
189    #
190    # Start of frame marker.  Defines the size and mode of the
191    # image.  JPEG is colour blind, so we use some simple
192    # heuristics to map the number of layers to an appropriate
193    # mode.  Note that this could be made a bit brighter, by
194    # looking for JFIF and Adobe APP markers.
195
196    assert self.fp is not None
197    n = i16(self.fp.read(2)) - 2
198    s = ImageFile._safe_read(self.fp, n)
199    self._size = i16(s, 3), i16(s, 1)
200    if self._im is not None and self.size != self.im.size:
201        self._im = None
202
203    self.bits = s[0]
204    if self.bits != 8:
205        msg = f"cannot handle {self.bits}-bit layers"
206        raise SyntaxError(msg)
207
208    self.layers = s[5]
209    if self.layers == 1:
210        self._mode = "L"
211    elif self.layers == 3:
212        self._mode = "RGB"
213    elif self.layers == 4:
214        self._mode = "CMYK"
215    else:
216        msg = f"cannot handle {self.layers}-layer images"
217        raise SyntaxError(msg)
218
219    if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]:
220        self.info["progressive"] = self.info["progression"] = 1
221
222    if self.icclist:
223        # fixup icc profile
224        self.icclist.sort()  # sort by sequence number
225        if self.icclist[0][13] == len(self.icclist):
226            profile = [p[14:] for p in self.icclist]
227            icc_profile = b"".join(profile)
228        else:
229            icc_profile = None  # wrong number of fragments
230        self.info["icc_profile"] = icc_profile
231        self.icclist = []
232
233    for i in range(6, len(s), 3):
234        t = s[i : i + 3]
235        # 4-tuples: id, vsamp, hsamp, qtable
236        self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2]))
237
238
239def DQT(self: JpegImageFile, marker: int) -> None:
240    #
241    # Define quantization table.  Note that there might be more
242    # than one table in each marker.
243
244    # FIXME: The quantization tables can be used to estimate the
245    # compression quality.
246
247    assert self.fp is not None
248    n = i16(self.fp.read(2)) - 2
249    s = ImageFile._safe_read(self.fp, n)
250    while len(s):
251        v = s[0]
252        precision = 1 if (v // 16 == 0) else 2  # in bytes
253        qt_length = 1 + precision * 64
254        if len(s) < qt_length:
255            msg = "bad quantization table marker"
256            raise SyntaxError(msg)
257        data = array.array("B" if precision == 1 else "H", s[1:qt_length])
258        if sys.byteorder == "little" and precision > 1:
259            data.byteswap()  # the values are always big-endian
260        self.quantization[v & 15] = [data[i] for i in zigzag_index]
261        s = s[qt_length:]
262
263
264#
265# JPEG marker table
266
267MARKER = {
268    0xFFC0: ("SOF0", "Baseline DCT", SOF),
269    0xFFC1: ("SOF1", "Extended Sequential DCT", SOF),
270    0xFFC2: ("SOF2", "Progressive DCT", SOF),
271    0xFFC3: ("SOF3", "Spatial lossless", SOF),
272    0xFFC4: ("DHT", "Define Huffman table", Skip),
273    0xFFC5: ("SOF5", "Differential sequential DCT", SOF),
274    0xFFC6: ("SOF6", "Differential progressive DCT", SOF),
275    0xFFC7: ("SOF7", "Differential spatial", SOF),
276    0xFFC8: ("JPG", "Extension", None),
277    0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF),
278    0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF),
279    0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF),
280    0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip),
281    0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF),
282    0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF),
283    0xFFCF: ("SOF15", "Differential spatial (AC)", SOF),
284    0xFFD0: ("RST0", "Restart 0", None),
285    0xFFD1: ("RST1", "Restart 1", None),
286    0xFFD2: ("RST2", "Restart 2", None),
287    0xFFD3: ("RST3", "Restart 3", None),
288    0xFFD4: ("RST4", "Restart 4", None),
289    0xFFD5: ("RST5", "Restart 5", None),
290    0xFFD6: ("RST6", "Restart 6", None),
291    0xFFD7: ("RST7", "Restart 7", None),
292    0xFFD8: ("SOI", "Start of image", None),
293    0xFFD9: ("EOI", "End of image", None),
294    0xFFDA: ("SOS", "Start of scan", Skip),
295    0xFFDB: ("DQT", "Define quantization table", DQT),
296    0xFFDC: ("DNL", "Define number of lines", Skip),
297    0xFFDD: ("DRI", "Define restart interval", Skip),
298    0xFFDE: ("DHP", "Define hierarchical progression", SOF),
299    0xFFDF: ("EXP", "Expand reference component", Skip),
300    0xFFE0: ("APP0", "Application segment 0", APP),
301    0xFFE1: ("APP1", "Application segment 1", APP),
302    0xFFE2: ("APP2", "Application segment 2", APP),
303    0xFFE3: ("APP3", "Application segment 3", APP),
304    0xFFE4: ("APP4", "Application segment 4", APP),
305    0xFFE5: ("APP5", "Application segment 5", APP),
306    0xFFE6: ("APP6", "Application segment 6", APP),
307    0xFFE7: ("APP7", "Application segment 7", APP),
308    0xFFE8: ("APP8", "Application segment 8", APP),
309    0xFFE9: ("APP9", "Application segment 9", APP),
310    0xFFEA: ("APP10", "Application segment 10", APP),
311    0xFFEB: ("APP11", "Application segment 11", APP),
312    0xFFEC: ("APP12", "Application segment 12", APP),
313    0xFFED: ("APP13", "Application segment 13", APP),
314    0xFFEE: ("APP14", "Application segment 14", APP),
315    0xFFEF: ("APP15", "Application segment 15", APP),
316    0xFFF0: ("JPG0", "Extension 0", None),
317    0xFFF1: ("JPG1", "Extension 1", None),
318    0xFFF2: ("JPG2", "Extension 2", None),
319    0xFFF3: ("JPG3", "Extension 3", None),
320    0xFFF4: ("JPG4", "Extension 4", None),
321    0xFFF5: ("JPG5", "Extension 5", None),
322    0xFFF6: ("JPG6", "Extension 6", None),
323    0xFFF7: ("JPG7", "Extension 7", None),
324    0xFFF8: ("JPG8", "Extension 8", None),
325    0xFFF9: ("JPG9", "Extension 9", None),
326    0xFFFA: ("JPG10", "Extension 10", None),
327    0xFFFB: ("JPG11", "Extension 11", None),
328    0xFFFC: ("JPG12", "Extension 12", None),
329    0xFFFD: ("JPG13", "Extension 13", None),
330    0xFFFE: ("COM", "Comment", COM),
331}
332
333
334def _accept(prefix: bytes) -> bool:
335    # Magic number was taken from https://en.wikipedia.org/wiki/JPEG
336    return prefix.startswith(b"\xff\xd8\xff")
337
338
339##
340# Image plugin for JPEG and JFIF images.
341
342
343class JpegImageFile(ImageFile.ImageFile):
344    format = "JPEG"
345    format_description = "JPEG (ISO 10918)"
346
347    def _open(self) -> None:
348        assert self.fp is not None
349        s = self.fp.read(3)
350
351        if not _accept(s):
352            msg = "not a JPEG file"
353            raise SyntaxError(msg)
354        s = b"\xff"
355
356        # Create attributes
357        self.bits = self.layers = 0
358        self._exif_offset = 0
359
360        # JPEG specifics (internal)
361        self.layer: list[tuple[int, int, int, int]] = []
362        self._huffman_dc: dict[Any, Any] = {}
363        self._huffman_ac: dict[Any, Any] = {}
364        self.quantization: dict[int, list[int]] = {}
365        self.app: dict[str, bytes] = {}  # compatibility
366        self.applist: list[tuple[str, bytes]] = []
367        self.icclist: list[bytes] = []
368
369        while True:
370            i = s[0]
371            if i == 0xFF:
372                s = s + self.fp.read(1)
373                i = i16(s)
374            else:
375                # Skip non-0xFF junk
376                s = self.fp.read(1)
377                continue
378
379            if i in MARKER:
380                name, description, handler = MARKER[i]
381                if handler is not None:
382                    handler(self, i)
383                if i == 0xFFDA:  # start of scan
384                    rawmode = self.mode
385                    if self.mode == "CMYK":
386                        rawmode = "CMYK;I"  # assume adobe conventions
387                    self.tile = [
388                        ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, ""))
389                    ]
390                    # self.__offset = self.fp.tell()
391                    break
392                s = self.fp.read(1)
393            elif i in {0, 0xFFFF}:
394                # padded marker or junk; move on
395                s = b"\xff"
396            elif i == 0xFF00:  # Skip extraneous data (escaped 0xFF)
397                s = self.fp.read(1)
398            else:
399                msg = "no marker found"
400                raise SyntaxError(msg)
401
402        self._read_dpi_from_exif()
403
404    def __getstate__(self) -> list[Any]:
405        return super().__getstate__() + [self.layers, self.layer]
406
407    def __setstate__(self, state: list[Any]) -> None:
408        self.layers, self.layer = state[6:]
409        super().__setstate__(state)
410
411    def load_read(self, read_bytes: int) -> bytes:
412        """
413        internal: read more image data
414        For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
415        so libjpeg can finish decoding
416        """
417        assert self.fp is not None
418        s = self.fp.read(read_bytes)
419
420        if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"):
421            # Premature EOF.
422            # Pretend file is finished adding EOI marker
423            self._ended = True
424            return b"\xff\xd9"
425
426        return s
427
428    def draft(
429        self, mode: str | None, size: tuple[int, int] | None
430    ) -> tuple[str, tuple[int, int, float, float]] | None:
431        if len(self.tile) != 1:
432            return None
433
434        # Protect from second call
435        if self.decoderconfig:
436            return None
437
438        d, e, o, a = self.tile[0]
439        scale = 1
440        original_size = self.size
441
442        assert isinstance(a, tuple)
443        if a[0] == "RGB" and mode in ["L", "YCbCr"]:
444            self._mode = mode
445            a = mode, ""
446
447        if size:
448            scale = min(self.size[0] // size[0], self.size[1] // size[1])
449            for s in [8, 4, 2, 1]:
450                if scale >= s:
451                    break
452            assert e is not None
453            e = (
454                e[0],
455                e[1],
456                (e[2] - e[0] + s - 1) // s + e[0],
457                (e[3] - e[1] + s - 1) // s + e[1],
458            )
459            self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s)
460            scale = s
461
462        self.tile = [ImageFile._Tile(d, e, o, a)]
463        self.decoderconfig = (scale, 0)
464
465        box = (0, 0, original_size[0] / scale, original_size[1] / scale)
466        return self.mode, box
467
468    def load_djpeg(self) -> None:
469        # ALTERNATIVE: handle JPEGs via the IJG command line utilities
470
471        f, path = tempfile.mkstemp()
472        os.close(f)
473        if os.path.exists(self.filename):
474            subprocess.check_call(["djpeg", "-outfile", path, self.filename])
475        else:
476            try:
477                os.unlink(path)
478            except OSError:
479                pass
480
481            msg = "Invalid Filename"
482            raise ValueError(msg)
483
484        try:
485            with Image.open(path) as _im:
486                _im.load()
487                self.im = _im.im
488        finally:
489            try:
490                os.unlink(path)
491            except OSError:
492                pass
493
494        self._mode = self.im.mode
495        self._size = self.im.size
496
497        self.tile = []
498
499    def _getexif(self) -> dict[int, Any] | None:
500        return _getexif(self)
501
502    def _read_dpi_from_exif(self) -> None:
503        # If DPI isn't in JPEG header, fetch from EXIF
504        if "dpi" in self.info or "exif" not in self.info:
505            return
506        try:
507            exif = self.getexif()
508            resolution_unit = exif[0x0128]
509            x_resolution = exif[0x011A]
510            try:
511                dpi = float(x_resolution[0]) / x_resolution[1]
512            except TypeError:
513                dpi = x_resolution
514            if math.isnan(dpi):
515                msg = "DPI is not a number"
516                raise ValueError(msg)
517            if resolution_unit == 3:  # cm
518                # 1 dpcm = 2.54 dpi
519                dpi *= 2.54
520            self.info["dpi"] = dpi, dpi
521        except (
522            struct.error,  # truncated EXIF
523            KeyError,  # dpi not included
524            SyntaxError,  # invalid/unreadable EXIF
525            TypeError,  # dpi is an invalid float
526            ValueError,  # dpi is an invalid float
527            ZeroDivisionError,  # invalid dpi rational value
528        ):
529            self.info["dpi"] = 72, 72
530
531    def _getmp(self) -> dict[int, Any] | None:
532        return _getmp(self)
533
534
535def _getexif(self: JpegImageFile) -> dict[int, Any] | None:
536    if "exif" not in self.info:
537        return None
538    return self.getexif()._get_merged_dict()
539
540
541def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
542    # Extract MP information.  This method was inspired by the "highly
543    # experimental" _getexif version that's been in use for years now,
544    # itself based on the ImageFileDirectory class in the TIFF plugin.
545
546    # The MP record essentially consists of a TIFF file embedded in a JPEG
547    # application marker.
548    try:
549        data = self.info["mp"]
550    except KeyError:
551        return None
552    file_contents = io.BytesIO(data)
553    head = file_contents.read(8)
554    endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<"
555    # process dictionary
556    from . import TiffImagePlugin
557
558    try:
559        info = TiffImagePlugin.ImageFileDirectory_v2(head)
560        file_contents.seek(info.next)
561        info.load(file_contents)
562        mp = dict(info)
563    except Exception as e:
564        msg = "malformed MP Index (unreadable directory)"
565        raise SyntaxError(msg) from e
566    # it's an error not to have a number of images
567    try:
568        quant = mp[0xB001]
569    except KeyError as e:
570        msg = "malformed MP Index (no number of images)"
571        raise SyntaxError(msg) from e
572    # get MP entries
573    mpentries = []
574    try:
575        rawmpentries = mp[0xB002]
576        for entrynum in range(quant):
577            unpackedentry = struct.unpack_from(
578                f"{endianness}LLLHH", rawmpentries, entrynum * 16
579            )
580            labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2")
581            mpentry = dict(zip(labels, unpackedentry))
582            mpentryattr = {
583                "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)),
584                "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)),
585                "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)),
586                "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27,
587                "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24,
588                "MPType": mpentry["Attribute"] & 0x00FFFFFF,
589            }
590            if mpentryattr["ImageDataFormat"] == 0:
591                mpentryattr["ImageDataFormat"] = "JPEG"
592            else:
593                msg = "unsupported picture format in MPO"
594                raise SyntaxError(msg)
595            mptypemap = {
596                0x000000: "Undefined",
597                0x010001: "Large Thumbnail (VGA Equivalent)",
598                0x010002: "Large Thumbnail (Full HD Equivalent)",
599                0x020001: "Multi-Frame Image (Panorama)",
600                0x020002: "Multi-Frame Image: (Disparity)",
601                0x020003: "Multi-Frame Image: (Multi-Angle)",
602                0x030000: "Baseline MP Primary Image",
603            }
604            mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown")
605            mpentry["Attribute"] = mpentryattr
606            mpentries.append(mpentry)
607        mp[0xB002] = mpentries
608    except KeyError as e:
609        msg = "malformed MP Index (bad MP Entry)"
610        raise SyntaxError(msg) from e
611    # Next we should try and parse the individual image unique ID list;
612    # we don't because I've never seen this actually used in a real MPO
613    # file and so can't test it.
614    return mp
615
616
617# --------------------------------------------------------------------
618# stuff to save JPEG files
619
620RAWMODE = {
621    "1": "L",
622    "L": "L",
623    "RGB": "RGB",
624    "RGBX": "RGB",
625    "CMYK": "CMYK;I",  # assume adobe conventions
626    "YCbCr": "YCbCr",
627}
628
629# fmt: off
630zigzag_index = (
631    0,  1,  5,  6, 14, 15, 27, 28,
632    2,  4,  7, 13, 16, 26, 29, 42,
633    3,  8, 12, 17, 25, 30, 41, 43,
634    9, 11, 18, 24, 31, 40, 44, 53,
635    10, 19, 23, 32, 39, 45, 52, 54,
636    20, 22, 33, 38, 46, 51, 55, 60,
637    21, 34, 37, 47, 50, 56, 59, 61,
638    35, 36, 48, 49, 57, 58, 62, 63,
639)
640
641samplings = {
642    (1, 1, 1, 1, 1, 1): 0,
643    (2, 1, 1, 1, 1, 1): 1,
644    (2, 2, 1, 1, 1, 1): 2,
645}
646# fmt: on
647
648
649def get_sampling(im: Image.Image) -> int:
650    # There's no subsampling when images have only 1 layer
651    # (grayscale images) or when they are CMYK (4 layers),
652    # so set subsampling to the default value.
653    #
654    # NOTE: currently Pillow can't encode JPEG to YCCK format.
655    # If YCCK support is added in the future, subsampling code will have
656    # to be updated (here and in JpegEncode.c) to deal with 4 layers.
657    if not isinstance(im, JpegImageFile) or im.layers in (1, 4):
658        return -1
659    sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
660    return samplings.get(sampling, -1)
661
662
663def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
664    if im.width == 0 or im.height == 0:
665        msg = "cannot write empty image as JPEG"
666        raise ValueError(msg)
667
668    try:
669        rawmode = RAWMODE[im.mode]
670    except KeyError as e:
671        msg = f"cannot write mode {im.mode} as JPEG"
672        raise OSError(msg) from e
673
674    info = im.encoderinfo
675
676    dpi = [round(x) for x in info.get("dpi", (0, 0))]
677
678    quality = info.get("quality", -1)
679    subsampling = info.get("subsampling", -1)
680    qtables = info.get("qtables")
681
682    if quality == "keep":
683        quality = -1
684        subsampling = "keep"
685        qtables = "keep"
686    elif quality in presets:
687        preset = presets[quality]
688        quality = -1
689        subsampling = preset.get("subsampling", -1)
690        qtables = preset.get("quantization")
691    elif not isinstance(quality, int):
692        msg = "Invalid quality setting"
693        raise ValueError(msg)
694    else:
695        if subsampling in presets:
696            subsampling = presets[subsampling].get("subsampling", -1)
697        if isinstance(qtables, str) and qtables in presets:
698            qtables = presets[qtables].get("quantization")
699
700    if subsampling == "4:4:4":
701        subsampling = 0
702    elif subsampling == "4:2:2":
703        subsampling = 1
704    elif subsampling == "4:2:0":
705        subsampling = 2
706    elif subsampling == "4:1:1":
707        # For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0.
708        # Set 4:2:0 if someone is still using that value.
709        subsampling = 2
710    elif subsampling == "keep":
711        if im.format != "JPEG":
712            msg = "Cannot use 'keep' when original image is not a JPEG"
713            raise ValueError(msg)
714        subsampling = get_sampling(im)
715
716    def validate_qtables(
717        qtables: (
718            str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None
719        ),
720    ) -> list[list[int]] | None:
721        if qtables is None:
722            return qtables
723        if isinstance(qtables, str):
724            try:
725                lines = [
726                    int(num)
727                    for line in qtables.splitlines()
728                    for num in line.split("#", 1)[0].split()
729                ]
730            except ValueError as e:
731                msg = "Invalid quantization table"
732                raise ValueError(msg) from e
733            else:
734                qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)]
735        if isinstance(qtables, (tuple, list, dict)):
736            if isinstance(qtables, dict):
737                qtables = [
738                    qtables[key] for key in range(len(qtables)) if key in qtables
739                ]
740            elif isinstance(qtables, tuple):
741                qtables = list(qtables)
742            if not (0 < len(qtables) < 5):
743                msg = "None or too many quantization tables"
744                raise ValueError(msg)
745            for idx, table in enumerate(qtables):
746                try:
747                    if len(table) != 64:
748                        msg = "Invalid quantization table"
749                        raise TypeError(msg)
750                    table_array = array.array("H", table)
751                except TypeError as e:
752                    msg = "Invalid quantization table"
753                    raise ValueError(msg) from e
754                else:
755                    qtables[idx] = list(table_array)
756            return qtables
757
758    if qtables == "keep":
759        if im.format != "JPEG":
760            msg = "Cannot use 'keep' when original image is not a JPEG"
761            raise ValueError(msg)
762        qtables = getattr(im, "quantization", None)
763    qtables = validate_qtables(qtables)
764
765    extra = info.get("extra", b"")
766
767    MAX_BYTES_IN_MARKER = 65533
768    if xmp := info.get("xmp"):
769        overhead_len = 29  # b"http://ns.adobe.com/xap/1.0/\x00"
770        max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
771        if len(xmp) > max_data_bytes_in_marker:
772            msg = "XMP data is too long"
773            raise ValueError(msg)
774        size = o16(2 + overhead_len + len(xmp))
775        extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
776
777    if icc_profile := info.get("icc_profile"):
778        overhead_len = 14  # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
779        max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
780        markers = []
781        while icc_profile:
782            markers.append(icc_profile[:max_data_bytes_in_marker])
783            icc_profile = icc_profile[max_data_bytes_in_marker:]
784        i = 1
785        for marker in markers:
786            size = o16(2 + overhead_len + len(marker))
787            extra += (
788                b"\xff\xe2"
789                + size
790                + b"ICC_PROFILE\0"
791                + o8(i)
792                + o8(len(markers))
793                + marker
794            )
795            i += 1
796
797    comment = info.get("comment", im.info.get("comment"))
798
799    # "progressive" is the official name, but older documentation
800    # says "progression"
801    # FIXME: issue a warning if the wrong form is used (post-1.1.7)
802    progressive = info.get("progressive", False) or info.get("progression", False)
803
804    optimize = info.get("optimize", False)
805
806    exif = info.get("exif", b"")
807    if isinstance(exif, Image.Exif):
808        exif = exif.tobytes()
809    if len(exif) > MAX_BYTES_IN_MARKER:
810        msg = "EXIF data is too long"
811        raise ValueError(msg)
812
813    # get keyword arguments
814    im.encoderconfig = (
815        quality,
816        progressive,
817        info.get("smooth", 0),
818        optimize,
819        info.get("keep_rgb", False),
820        info.get("streamtype", 0),
821        dpi,
822        subsampling,
823        info.get("restart_marker_blocks", 0),
824        info.get("restart_marker_rows", 0),
825        qtables,
826        comment,
827        extra,
828        exif,
829    )
830
831    # if we optimize, libjpeg needs a buffer big enough to hold the whole image
832    # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
833    # channels*size, this is a value that's been used in a django patch.
834    # https://github.com/matthewwithanm/django-imagekit/issues/50
835    if optimize or progressive:
836        # CMYK can be bigger
837        if im.mode == "CMYK":
838            bufsize = 4 * im.size[0] * im.size[1]
839        # keep sets quality to -1, but the actual value may be high.
840        elif quality >= 95 or quality == -1:
841            bufsize = 2 * im.size[0] * im.size[1]
842        else:
843            bufsize = im.size[0] * im.size[1]
844        if exif:
845            bufsize += len(exif) + 5
846        if extra:
847            bufsize += len(extra) + 1
848    else:
849        # The EXIF info needs to be written as one block, + APP1, + one spare byte.
850        # Ensure that our buffer is big enough. Same with the icc_profile block.
851        bufsize = max(len(exif) + 5, len(extra) + 1)
852
853    ImageFile._save(
854        im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize
855    )
856
857
858##
859# Factory for making JPEG and MPO instances
860def jpeg_factory(
861    fp: IO[bytes], filename: str | bytes | None = None
862) -> JpegImageFile | MpoImageFile:
863    im = JpegImageFile(fp, filename)
864    try:
865        mpheader = im._getmp()
866        if mpheader is not None and mpheader[45057] > 1:
867            for segment, content in im.applist:
868                if segment == "APP1" and b' hdrgm:Version="' in content:
869                    # Ultra HDR images are not yet supported
870                    return im
871            # It's actually an MPO
872            from .MpoImagePlugin import MpoImageFile
873
874            # Don't reload everything, just convert it.
875            im = MpoImageFile.adopt(im, mpheader)
876    except (TypeError, IndexError):
877        # It is really a JPEG
878        pass
879    except SyntaxError:
880        warnings.warn(
881            "Image appears to be a malformed MPO file, it will be "
882            "interpreted as a base JPEG file"
883        )
884    return im
885
886
887# ---------------------------------------------------------------------
888# Registry stuff
889
890Image.register_open(JpegImageFile.format, jpeg_factory, _accept)
891Image.register_save(JpegImageFile.format, _save)
892
893Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"])
894
895Image.register_mime(JpegImageFile.format, "image/jpeg")
896 
Aluode/PerceptionLabPortable · CoolFace