Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,80 @@ 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)

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=(96, 96), keep_rgb=True)

with Image.open(f) as reloaded:
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:
Expand Down
27 changes: 23 additions & 4 deletions src/PIL/JpegImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,8 +681,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")
Expand Down Expand Up @@ -813,7 +811,28 @@ 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
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[0]
exif[0x011B] = dpi[1]
exif[0x0128] = 2
else:
jfif_dpi = dpi

if isinstance(exif, Image.Exif):
exif = exif.tobytes()
if len(exif) > MAX_BYTES_IN_MARKER:
Expand All @@ -826,9 +845,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),
Expand Down
Loading