From f71456ffd78aa33a76c786c7ba488e0e4a843d5b Mon Sep 17 00:00:00 2001 From: Muguett-DBY <85372096+Muguett-DBY@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:58:51 +1100 Subject: [PATCH 1/6] Decode RGB JPEGs with conflicting JFIF and Adobe markers --- Tests/test_file_jpeg.py | 49 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/13.1.0.rst | 6 +++++ src/PIL/JpegImagePlugin.py | 23 +++++++++++++++-- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 1110262ed2d..66838c14769 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -204,6 +204,55 @@ def test_dpi_jfif_cm(self) -> None: with Image.open("Tests/images/jfif_unit_cm.jpg") as im: assert im.info["dpi"] == (2.54, 5.08) + @pytest.mark.parametrize("progressive", (False, True)) + @pytest.mark.parametrize("draft_mode", (None, "RGB", "L")) + def test_keep_rgb_with_dpi( + self, progressive: bool, draft_mode: str | None + ) -> None: + image = Image.new("RGB", (20, 20), (255, 0, 0)) + options = {"keep_rgb": True, "quality": 100, "progressive": progressive} + outputs = [] + for dpi in (None, (72, 72)): + output = BytesIO() + if dpi is None: + image.save(output, "JPEG", **options) + else: + image.save(output, "JPEG", dpi=dpi, **options) + outputs.append(output.getvalue()) + + with Image.open(BytesIO(outputs[0])) as reference: + with Image.open(BytesIO(outputs[1])) as actual: + assert actual.info["dpi"] == (72, 72) + if draft_mode: + reference.draft(draft_mode, (8, 8)) + actual.draft(draft_mode, (8, 8)) + assert_image_equal(reference, actual) + + def test_reopen_does_not_use_previous_adobe_marker(self) -> None: + output = BytesIO() + Image.new("RGB", (20, 20), "red").save( + output, "JPEG", dpi=(72, 72), quality=100, subsampling=0 + ) + data = bytearray(output.getvalue()) + # Give a JFIF YCbCr image RGB component identifiers without adding APP14. + for marker, offsets in ((b"\xff\xc0", (10, 13, 16)), (b"\xff\xda", (5, 7, 9))): + start = data.find(marker) + assert start >= 0 + for old_id, offset, new_id in zip((1, 2, 3), offsets, b"RGB"): + assert data[start + offset] == old_id + data[start + offset] = new_id + + with Image.open(BytesIO(data)) as reference: + with Image.open(BytesIO(data)) as reopened: + assert isinstance(reopened, JpegImagePlugin.JpegImageFile) + assert reopened.fp is not None + # MPO frame changes call _open on the same object and can leave + # information from the previous frame in the info dictionary. + reopened.info["adobe_transform"] = 0 + reopened.fp.seek(0) + reopened._open() + assert_image_equal(reference, reopened) + def test_icc(self, tmp_path: Path) -> None: # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst index 380154ae109..8ab3ae30a66 100644 --- a/docs/releasenotes/13.1.0.rst +++ b/docs/releasenotes/13.1.0.rst @@ -62,6 +62,12 @@ Blurring I;16 images :py:class:`~PIL.ImageFilter.GaussianBlur` and :py:class:`~PIL.ImageFilter.BoxBlur` now support I;16, I;16L, I;16B and I;16N images. +Reading RGB JPEGs saved with DPI +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +RGB JPEGs saved with both ``keep_rgb=True`` and DPI now load with the correct +colors when they contain both JFIF and Adobe markers. + Minimum C version ^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index dd0a470a736..0d90ccd2a00 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -393,9 +393,28 @@ def _open(self) -> None: handler(self, i) if i == 0xFFDA: # start of scan rawmode = self.mode + jpegmode = "" if self.mode == "CMYK": rawmode = "CMYK;I" # assume adobe conventions - self.tile = [ImageFile._Tile("jpeg", args=(rawmode, ""))] + elif ( + self.mode == "RGB" + and [component[0] for component in self.layer] == list(b"RGB") + and any( + name == "APP0" and data.startswith(b"JFIF") + for name, data in self.applist + ) + and any( + name == "APP14" + and data.startswith(b"Adobe") + and len(data) >= 12 + and data[11] == 0 + for name, data in self.applist + ) + ): + # JFIF takes precedence in libjpeg, but these component IDs + # and the Adobe marker identify untransformed RGB data. + jpegmode = "RGB" + self.tile = [ImageFile._Tile("jpeg", args=(rawmode, jpegmode))] # self.__offset = self.fp.tell() break s = self.fp.read(1) @@ -451,7 +470,7 @@ def draft( assert isinstance(a, tuple) if a[0] == "RGB" and mode in ["L", "YCbCr"]: self._mode = mode - a = mode, "" + a = mode, a[1] if size: scale = min(self.size[0] // size[0], self.size[1] // size[1]) From f2a0c219c562020856ed2fc0254326349e85f88f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:59:40 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 66838c14769..fef8e1676af 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -206,9 +206,7 @@ def test_dpi_jfif_cm(self) -> None: @pytest.mark.parametrize("progressive", (False, True)) @pytest.mark.parametrize("draft_mode", (None, "RGB", "L")) - def test_keep_rgb_with_dpi( - self, progressive: bool, draft_mode: str | None - ) -> None: + def test_keep_rgb_with_dpi(self, progressive: bool, draft_mode: str | None) -> None: image = Image.new("RGB", (20, 20), (255, 0, 0)) options = {"keep_rgb": True, "quality": 100, "progressive": progressive} outputs = [] From 8a2795dd9b2818e6124e2fcabad58e5e2016a5ab Mon Sep 17 00:00:00 2001 From: Muguett-DBY <85372096+Muguett-DBY@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:08:21 +1100 Subject: [PATCH 3/6] Respect the last Adobe transform marker --- Tests/test_file_jpeg.py | 17 ++++++++++++++++- src/PIL/JpegImagePlugin.py | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fef8e1676af..478ded667cd 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -226,7 +226,7 @@ def test_keep_rgb_with_dpi(self, progressive: bool, draft_mode: str | None) -> N actual.draft(draft_mode, (8, 8)) assert_image_equal(reference, actual) - def test_reopen_does_not_use_previous_adobe_marker(self) -> None: + def jfif_with_rgb_component_ids(self) -> bytes: output = BytesIO() Image.new("RGB", (20, 20), "red").save( output, "JPEG", dpi=(72, 72), quality=100, subsampling=0 @@ -239,7 +239,10 @@ def test_reopen_does_not_use_previous_adobe_marker(self) -> None: for old_id, offset, new_id in zip((1, 2, 3), offsets, b"RGB"): assert data[start + offset] == old_id data[start + offset] = new_id + return bytes(data) + def test_reopen_does_not_use_previous_adobe_marker(self) -> None: + data = self.jfif_with_rgb_component_ids() with Image.open(BytesIO(data)) as reference: with Image.open(BytesIO(data)) as reopened: assert isinstance(reopened, JpegImagePlugin.JpegImageFile) @@ -251,6 +254,18 @@ def test_reopen_does_not_use_previous_adobe_marker(self) -> None: reopened._open() assert_image_equal(reference, reopened) + def test_multiple_adobe_markers_use_the_last_transform(self) -> None: + data = self.jfif_with_rgb_component_ids() + adobe = b"\xff\xee\x00\x0eAdobe\x00\x64\x00\x00\x00\x00" + first = adobe + b"\x00" + last = adobe + b"\x01" + both = data[:2] + first + last + data[2:] + last_only = data[:2] + last + data[2:] + with Image.open(BytesIO(last_only)) as reference: + with Image.open(BytesIO(both)) as actual: + assert actual.info["adobe_transform"] == 1 + assert_image_equal(reference, actual) + def test_icc(self, tmp_path: Path) -> None: # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 0d90ccd2a00..d4fa6cd5df1 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -403,11 +403,11 @@ def _open(self) -> None: name == "APP0" and data.startswith(b"JFIF") for name, data in self.applist ) + and self.info.get("adobe_transform") == 0 and any( name == "APP14" and data.startswith(b"Adobe") and len(data) >= 12 - and data[11] == 0 for name, data in self.applist ) ): From 5cf20fa8982d2f753bea62c7be9231b3937cbaee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Oct 2026 22:42:47 +1100 Subject: [PATCH 4/6] If keep_rgb is True, add dpi to Exif instead of using JFIF --- Tests/test_file_jpeg.py | 69 +++++------------------------------- docs/releasenotes/13.1.0.rst | 6 ---- src/PIL/JpegImagePlugin.py | 42 +++++++++------------- 3 files changed, 25 insertions(+), 92 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 478ded667cd..d500abfec9d 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -204,67 +204,14 @@ def test_dpi_jfif_cm(self) -> None: with Image.open("Tests/images/jfif_unit_cm.jpg") as im: assert im.info["dpi"] == (2.54, 5.08) - @pytest.mark.parametrize("progressive", (False, True)) - @pytest.mark.parametrize("draft_mode", (None, "RGB", "L")) - def test_keep_rgb_with_dpi(self, progressive: bool, draft_mode: str | None) -> None: - image = Image.new("RGB", (20, 20), (255, 0, 0)) - options = {"keep_rgb": True, "quality": 100, "progressive": progressive} - outputs = [] - for dpi in (None, (72, 72)): - output = BytesIO() - if dpi is None: - image.save(output, "JPEG", **options) - else: - image.save(output, "JPEG", dpi=dpi, **options) - outputs.append(output.getvalue()) - - with Image.open(BytesIO(outputs[0])) as reference: - with Image.open(BytesIO(outputs[1])) as actual: - assert actual.info["dpi"] == (72, 72) - if draft_mode: - reference.draft(draft_mode, (8, 8)) - actual.draft(draft_mode, (8, 8)) - assert_image_equal(reference, actual) - - def jfif_with_rgb_component_ids(self) -> bytes: - output = BytesIO() - Image.new("RGB", (20, 20), "red").save( - output, "JPEG", dpi=(72, 72), quality=100, subsampling=0 - ) - data = bytearray(output.getvalue()) - # Give a JFIF YCbCr image RGB component identifiers without adding APP14. - for marker, offsets in ((b"\xff\xc0", (10, 13, 16)), (b"\xff\xda", (5, 7, 9))): - start = data.find(marker) - assert start >= 0 - for old_id, offset, new_id in zip((1, 2, 3), offsets, b"RGB"): - assert data[start + offset] == old_id - data[start + offset] = new_id - return bytes(data) - - def test_reopen_does_not_use_previous_adobe_marker(self) -> None: - data = self.jfif_with_rgb_component_ids() - with Image.open(BytesIO(data)) as reference: - with Image.open(BytesIO(data)) as reopened: - assert isinstance(reopened, JpegImagePlugin.JpegImageFile) - assert reopened.fp is not None - # MPO frame changes call _open on the same object and can leave - # information from the previous frame in the info dictionary. - reopened.info["adobe_transform"] = 0 - reopened.fp.seek(0) - reopened._open() - assert_image_equal(reference, reopened) - - def test_multiple_adobe_markers_use_the_last_transform(self) -> None: - data = self.jfif_with_rgb_component_ids() - adobe = b"\xff\xee\x00\x0eAdobe\x00\x64\x00\x00\x00\x00" - first = adobe + b"\x00" - last = adobe + b"\x01" - both = data[:2] + first + last + data[2:] - last_only = data[:2] + last + data[2:] - with Image.open(BytesIO(last_only)) as reference: - with Image.open(BytesIO(both)) as actual: - assert actual.info["adobe_transform"] == 1 - assert_image_equal(reference, actual) + def test_dpi_with_keep_rgb(self, tmp_path: Path) -> None: + im = Image.new("RGB", (20, 20), (255, 0, 0)) + f = tmp_path / "temp.jpg" + im.save(f, "JPEG", dpi=(72, 72), keep_rgb=True) + + with Image.open(f) as reloaded: + assert reloaded.info["dpi"] == (72, 72) + assert_image_equal(reloaded, im) def test_icc(self, tmp_path: Path) -> None: # Test ICC support diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst index 8ab3ae30a66..380154ae109 100644 --- a/docs/releasenotes/13.1.0.rst +++ b/docs/releasenotes/13.1.0.rst @@ -62,12 +62,6 @@ Blurring I;16 images :py:class:`~PIL.ImageFilter.GaussianBlur` and :py:class:`~PIL.ImageFilter.BoxBlur` now support I;16, I;16L, I;16B and I;16N images. -Reading RGB JPEGs saved with DPI -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -RGB JPEGs saved with both ``keep_rgb=True`` and DPI now load with the correct -colors when they contain both JFIF and Adobe markers. - Minimum C version ^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d4fa6cd5df1..0fb58f082e3 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -393,28 +393,9 @@ def _open(self) -> None: handler(self, i) if i == 0xFFDA: # start of scan rawmode = self.mode - jpegmode = "" if self.mode == "CMYK": rawmode = "CMYK;I" # assume adobe conventions - elif ( - self.mode == "RGB" - and [component[0] for component in self.layer] == list(b"RGB") - and any( - name == "APP0" and data.startswith(b"JFIF") - for name, data in self.applist - ) - and self.info.get("adobe_transform") == 0 - and any( - name == "APP14" - and data.startswith(b"Adobe") - and len(data) >= 12 - for name, data in self.applist - ) - ): - # JFIF takes precedence in libjpeg, but these component IDs - # and the Adobe marker identify untransformed RGB data. - jpegmode = "RGB" - self.tile = [ImageFile._Tile("jpeg", args=(rawmode, jpegmode))] + self.tile = [ImageFile._Tile("jpeg", args=(rawmode, ""))] # self.__offset = self.fp.tell() break s = self.fp.read(1) @@ -470,7 +451,7 @@ def draft( assert isinstance(a, tuple) if a[0] == "RGB" and mode in ["L", "YCbCr"]: self._mode = mode - a = mode, a[1] + a = mode, "" if size: scale = min(self.size[0] // size[0], self.size[1] // size[1]) @@ -701,8 +682,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: info = im.encoderinfo - dpi = [round(x) for x in info.get("dpi", (0, 0))] - quality = info.get("quality", -1) subsampling = info.get("subsampling", -1) qtables = info.get("qtables") @@ -833,7 +812,20 @@ def validate_qtables( optimize = info.get("optimize", False) + keep_rgb = info.get("keep_rgb", False) + jfif_dpi = (0, 0) exif = info.get("exif", b"") + if "dpi" in info: + dpi = tuple(round(x) for x in info["dpi"]) + if keep_rgb: + if not isinstance(exif, Image.Exif): + exif_object = Image.Exif() + exif_object.load(exif) + exif = exif_object + exif[0x011A] = dpi + else: + jfif_dpi = dpi + if isinstance(exif, Image.Exif): exif = exif.tobytes() if len(exif) > MAX_BYTES_IN_MARKER: @@ -846,9 +838,9 @@ def validate_qtables( progressive, info.get("smooth", 0), optimize, - info.get("keep_rgb", False), + keep_rgb, info.get("streamtype", 0), - dpi, + jfif_dpi, subsampling, info.get("restart_marker_blocks", 0), info.get("restart_marker_rows", 0), From 94de18b8320d3e40bca92012ebd188772af40c50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Oct 2026 17:05:13 +1100 Subject: [PATCH 5/6] Do not modify supplied Exif object --- Tests/test_file_jpeg.py | 4 ++-- src/PIL/JpegImagePlugin.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index d500abfec9d..6b0f30dbc19 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -207,10 +207,10 @@ def test_dpi_jfif_cm(self) -> None: def test_dpi_with_keep_rgb(self, tmp_path: Path) -> None: im = Image.new("RGB", (20, 20), (255, 0, 0)) f = tmp_path / "temp.jpg" - im.save(f, "JPEG", dpi=(72, 72), keep_rgb=True) + im.save(f, "JPEG", dpi=(96, 96), keep_rgb=True) with Image.open(f) as reloaded: - assert reloaded.info["dpi"] == (72, 72) + assert reloaded.info["dpi"] == (96, 96) assert_image_equal(reloaded, im) def test_icc(self, tmp_path: Path) -> None: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 0fb58f082e3..2d2fad1d025 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -818,10 +818,10 @@ def validate_qtables( if "dpi" in info: dpi = tuple(round(x) for x in info["dpi"]) if keep_rgb: - if not isinstance(exif, Image.Exif): - exif_object = Image.Exif() - exif_object.load(exif) - exif = exif_object + exif_object = Image.Exif() + exif_object.load(exif.tobytes() if isinstance(exif, Image.Exif) else exif) + exif = exif_object + exif[0x011A] = dpi else: jfif_dpi = dpi From 9a20ff030e2a524971d3d5bb5f1aa6dae4240b49 Mon Sep 17 00:00:00 2001 From: Muguett-DBY <85372096+Muguett-DBY@users.noreply.github.com> Date: Fri, 30 Oct 2026 06:49:07 +1100 Subject: [PATCH 6/6] Preserve JPEG DPI metadata across modes --- Tests/test_file_jpeg.py | 65 ++++++++++++++++++++++++++++++++++++++ src/PIL/JpegImagePlugin.py | 12 +++++-- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index db77397a1b6..9615497239c 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -213,6 +213,71 @@ def test_dpi_with_keep_rgb(self, tmp_path: Path) -> None: assert reloaded.info["dpi"] == (96, 96) assert_image_equal(reloaded, im) + @pytest.mark.parametrize("as_bytes", (False, True)) + def test_dpi_keep_rgb_existing_exif(self, as_bytes: bool) -> None: + exif = Image.Exif() + exif[0x010E] = "description" + exif[0x011A] = 300 + exif[0x011B] = 200 + exif[0x0128] = 3 + original = exif.tobytes() + output = BytesIO() + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + Image.new("RGB", (20, 20), (255, 0, 0)).save( + output, + "JPEG", + keep_rgb=True, + dpi=(96, 96), + exif=original if as_bytes else exif, + ) + assert exif.tobytes() == original + with Image.open(BytesIO(output.getvalue())) as reloaded: + stored = reloaded.getexif() + assert reloaded.info["dpi"] == (96, 96) + assert stored[0x011A] == 96 + assert stored[0x011B] == 96 + assert stored[0x0128] == 2 + assert stored[0x010E] == "description" + assert reloaded.getpixel((0, 0)) == (255, 0, 0) + + def test_dpi_keep_rgb_stores_both_axes(self) -> None: + output = BytesIO() + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + Image.new("RGB", (20, 20)).save( + output, "JPEG", keep_rgb=True, dpi=(96, 150) + ) + with Image.open(BytesIO(output.getvalue())) as reloaded: + exif = reloaded.getexif() + assert exif[0x011A] == 96 + assert exif[0x011B] == 150 + assert exif[0x0128] == 2 + + @pytest.mark.parametrize("mode", ("1", "L", "YCbCr", "CMYK")) + def test_dpi_keep_rgb_other_modes(self, mode: str) -> None: + output = BytesIO() + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + Image.new(mode, (20, 20)).save(output, "JPEG", keep_rgb=True, dpi=(96, 150)) + with Image.open(BytesIO(output.getvalue())) as reloaded: + assert reloaded.info["dpi"] == (96, 150) + + @pytest.mark.parametrize("dpi", ((0, 0), (0, 150), (96, 0))) + def test_dpi_keep_rgb_zero_density(self, dpi: tuple[int, int]) -> None: + output = BytesIO() + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + Image.new("RGB", (20, 20)).save(output, "JPEG", keep_rgb=True, dpi=dpi) + with Image.open(BytesIO(output.getvalue())) as reloaded: + assert "dpi" not in reloaded.info + assert not reloaded.getexif() + + @pytest.mark.parametrize("dpi", ((96,), (96, 96, 96))) + def test_dpi_keep_rgb_invalid_length(self, dpi: tuple[int, ...]) -> None: + with pytest.raises(TypeError): + Image.new("RGB", (20, 20)).save(BytesIO(), "JPEG", keep_rgb=True, dpi=dpi) + def test_icc(self, tmp_path: Path) -> None: # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 38f6f48071b..5f33e0aa96a 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -816,12 +816,20 @@ def validate_qtables( exif = info.get("exif", b"") if "dpi" in info: dpi = tuple(round(x) for x in info["dpi"]) - if keep_rgb: + if ( + keep_rgb + and im.mode in ("RGB", "RGBX") + and len(dpi) == 2 + and dpi[0] > 0 + and dpi[1] > 0 + ): exif_object = Image.Exif() exif_object.load(exif.tobytes() if isinstance(exif, Image.Exif) else exif) exif = exif_object - exif[0x011A] = dpi + exif[0x011A] = dpi[0] + exif[0x011B] = dpi[1] + exif[0x0128] = 2 else: jfif_dpi = dpi