Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
JpegImagePlugin.py890 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        try:
131            while s[offset : offset + 4] == b"8BIM":
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            pass  # 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    try:
665        rawmode = RAWMODE[im.mode]
666    except KeyError as e:
667        msg = f"cannot write mode {im.mode} as JPEG"
668        raise OSError(msg) from e
669
670    info = im.encoderinfo
671
672    dpi = [round(x) for x in info.get("dpi", (0, 0))]
673
674    quality = info.get("quality", -1)
675    subsampling = info.get("subsampling", -1)
676    qtables = info.get("qtables")
677
678    if quality == "keep":
679        quality = -1
680        subsampling = "keep"
681        qtables = "keep"
682    elif quality in presets:
683        preset = presets[quality]
684        quality = -1
685        subsampling = preset.get("subsampling", -1)
686        qtables = preset.get("quantization")
687    elif not isinstance(quality, int):
688        msg = "Invalid quality setting"
689        raise ValueError(msg)
690    else:
691        if subsampling in presets:
692            subsampling = presets[subsampling].get("subsampling", -1)
693        if isinstance(qtables, str) and qtables in presets:
694            qtables = presets[qtables].get("quantization")
695
696    if subsampling == "4:4:4":
697        subsampling = 0
698    elif subsampling == "4:2:2":
699        subsampling = 1
700    elif subsampling == "4:2:0":
701        subsampling = 2
702    elif subsampling == "4:1:1":
703        # For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0.
704        # Set 4:2:0 if someone is still using that value.
705        subsampling = 2
706    elif subsampling == "keep":
707        if im.format != "JPEG":
708            msg = "Cannot use 'keep' when original image is not a JPEG"
709            raise ValueError(msg)
710        subsampling = get_sampling(im)
711
712    def validate_qtables(
713        qtables: (
714            str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None
715        ),
716    ) -> list[list[int]] | None:
717        if qtables is None:
718            return qtables
719        if isinstance(qtables, str):
720            try:
721                lines = [
722                    int(num)
723                    for line in qtables.splitlines()
724                    for num in line.split("#", 1)[0].split()
725                ]
726            except ValueError as e:
727                msg = "Invalid quantization table"
728                raise ValueError(msg) from e
729            else:
730                qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)]
731        if isinstance(qtables, (tuple, list, dict)):
732            if isinstance(qtables, dict):
733                qtables = [
734                    qtables[key] for key in range(len(qtables)) if key in qtables
735                ]
736            elif isinstance(qtables, tuple):
737                qtables = list(qtables)
738            if not (0 < len(qtables) < 5):
739                msg = "None or too many quantization tables"
740                raise ValueError(msg)
741            try:
742                for idx, table in enumerate(qtables):
743                    if len(table) != 64:
744                        msg = "Invalid quantization table"
745                        raise TypeError(msg)
746                    qtables[idx] = list(array.array("H", table))
747            except TypeError as e:
748                msg = "Invalid quantization table"
749                raise ValueError(msg) from e
750            return qtables
751
752    if qtables == "keep":
753        if im.format != "JPEG":
754            msg = "Cannot use 'keep' when original image is not a JPEG"
755            raise ValueError(msg)
756        qtables = getattr(im, "quantization", None)
757    qtables = validate_qtables(qtables)
758
759    extra = info.get("extra", b"")
760
761    MAX_BYTES_IN_MARKER = 65533
762    if xmp := info.get("xmp"):
763        overhead_len = 29  # b"http://ns.adobe.com/xap/1.0/\x00"
764        max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
765        if len(xmp) > max_data_bytes_in_marker:
766            msg = "XMP data is too long"
767            raise ValueError(msg)
768        size = o16(2 + overhead_len + len(xmp))
769        extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
770
771    if icc_profile := info.get("icc_profile"):
772        overhead_len = 14  # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
773        max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
774        markers = []
775        while icc_profile:
776            markers.append(icc_profile[:max_data_bytes_in_marker])
777            icc_profile = icc_profile[max_data_bytes_in_marker:]
778        i = 1
779        for marker in markers:
780            size = o16(2 + overhead_len + len(marker))
781            extra += (
782                b"\xff\xe2"
783                + size
784                + b"ICC_PROFILE\0"
785                + o8(i)
786                + o8(len(markers))
787                + marker
788            )
789            i += 1
790
791    comment = info.get("comment", im.info.get("comment"))
792
793    # "progressive" is the official name, but older documentation
794    # says "progression"
795    # FIXME: issue a warning if the wrong form is used (post-1.1.7)
796    progressive = info.get("progressive", False) or info.get("progression", False)
797
798    optimize = info.get("optimize", False)
799
800    exif = info.get("exif", b"")
801    if isinstance(exif, Image.Exif):
802        exif = exif.tobytes()
803    if len(exif) > MAX_BYTES_IN_MARKER:
804        msg = "EXIF data is too long"
805        raise ValueError(msg)
806
807    # get keyword arguments
808    im.encoderconfig = (
809        quality,
810        progressive,
811        info.get("smooth", 0),
812        optimize,
813        info.get("keep_rgb", False),
814        info.get("streamtype", 0),
815        dpi,
816        subsampling,
817        info.get("restart_marker_blocks", 0),
818        info.get("restart_marker_rows", 0),
819        qtables,
820        comment,
821        extra,
822        exif,
823    )
824
825    # if we optimize, libjpeg needs a buffer big enough to hold the whole image
826    # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
827    # channels*size, this is a value that's been used in a django patch.
828    # https://github.com/matthewwithanm/django-imagekit/issues/50
829    if optimize or progressive:
830        # CMYK can be bigger
831        if im.mode == "CMYK":
832            bufsize = 4 * im.size[0] * im.size[1]
833        # keep sets quality to -1, but the actual value may be high.
834        elif quality >= 95 or quality == -1:
835            bufsize = 2 * im.size[0] * im.size[1]
836        else:
837            bufsize = im.size[0] * im.size[1]
838        if exif:
839            bufsize += len(exif) + 5
840        if extra:
841            bufsize += len(extra) + 1
842    else:
843        # The EXIF info needs to be written as one block, + APP1, + one spare byte.
844        # Ensure that our buffer is big enough. Same with the icc_profile block.
845        bufsize = max(len(exif) + 5, len(extra) + 1)
846
847    ImageFile._save(
848        im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize
849    )
850
851
852##
853# Factory for making JPEG and MPO instances
854def jpeg_factory(
855    fp: IO[bytes], filename: str | bytes | None = None
856) -> JpegImageFile | MpoImageFile:
857    im = JpegImageFile(fp, filename)
858    try:
859        mpheader = im._getmp()
860        if mpheader is not None and mpheader[45057] > 1:
861            for segment, content in im.applist:
862                if segment == "APP1" and b' hdrgm:Version="' in content:
863                    # Ultra HDR images are not yet supported
864                    return im
865            # It's actually an MPO
866            from .MpoImagePlugin import MpoImageFile
867
868            # Don't reload everything, just convert it.
869            im = MpoImageFile.adopt(im, mpheader)
870    except (TypeError, IndexError):
871        # It is really a JPEG
872        pass
873    except SyntaxError:
874        warnings.warn(
875            "Image appears to be a malformed MPO file, it will be "
876            "interpreted as a base JPEG file"
877        )
878    return im
879
880
881# ---------------------------------------------------------------------
882# Registry stuff
883
884Image.register_open(JpegImageFile.format, jpeg_factory, _accept)
885Image.register_save(JpegImageFile.format, _save)
886
887Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"])
888
889Image.register_mime(JpegImageFile.format, "image/jpeg")
890 
codekingpro/portable-devtools · Team Ai