Skip to content

Commit 4b62012

Browse files
committed
Merge branch 'main' into infer-target-version
2 parents c3fb0de + aab5a2f commit 4b62012

File tree

8 files changed

+676
-26
lines changed

8 files changed

+676
-26
lines changed

CHANGES.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ Changelog (Pillow)
55
10.2.0 (unreleased)
66
-------------------
77

8+
- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491
9+
[bgilbert, radarhere]
10+
11+
- If save_all PNG only has one frame, do not create animated image #7522
12+
[radarhere]
13+
814
- Fixed frombytes() for images with a zero dimension #7493
915
[radarhere]
1016

Tests/test_file_apng.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ def test_apng_save(tmp_path):
350350
im.load()
351351
assert not im.is_animated
352352
assert im.n_frames == 1
353-
assert im.get_format_mimetype() == "image/apng"
353+
assert im.get_format_mimetype() == "image/png"
354354
assert im.info.get("default_image") is None
355355
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
356356
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@@ -450,26 +450,29 @@ def test_apng_save_duration_loop(tmp_path):
450450
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
451451
)
452452
with Image.open(test_file) as im:
453-
im.load()
454453
assert im.n_frames == 1
455-
assert im.info.get("duration") == 750
454+
assert "duration" not in im.info
456455

457-
# test info duration
458-
frame.info["duration"] = 750
459-
frame.save(test_file, save_all=True)
456+
different_frame = Image.new("RGBA", (128, 64))
457+
frame.save(
458+
test_file,
459+
save_all=True,
460+
append_images=[frame, different_frame],
461+
duration=[500, 100, 150],
462+
)
460463
with Image.open(test_file) as im:
461-
assert im.info.get("duration") == 750
462-
464+
assert im.n_frames == 2
465+
assert im.info["duration"] == 600
463466

464-
def test_apng_save_duplicate_duration(tmp_path):
465-
test_file = str(tmp_path / "temp.png")
466-
frame = Image.new("RGB", (1, 1))
467+
im.seek(1)
468+
assert im.info["duration"] == 150
467469

468-
# Test a single duration is correctly combined across duplicate frames
469-
frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
470+
# test info duration
471+
frame.info["duration"] = 300
472+
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
470473
with Image.open(test_file) as im:
471-
assert im.n_frames == 1
472-
assert im.info.get("duration") == 1500
474+
assert im.n_frames == 2
475+
assert im.info["duration"] == 600
473476

474477

475478
def test_apng_save_disposal(tmp_path):
@@ -674,15 +677,16 @@ def test_seek_after_close():
674677

675678
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
676679
@pytest.mark.parametrize("default_image", (True, False))
677-
def test_different_modes_in_later_frames(mode, default_image, tmp_path):
680+
@pytest.mark.parametrize("duplicate", (True, False))
681+
def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):
678682
test_file = str(tmp_path / "temp.png")
679683

680684
im = Image.new("L", (1, 1))
681685
im.save(
682686
test_file,
683687
save_all=True,
684688
default_image=default_image,
685-
append_images=[Image.new(mode, (1, 1))],
689+
append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],
686690
)
687691
with Image.open(test_file) as reloaded:
688692
assert reloaded.mode == mode

Tests/test_file_jpeg.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,28 @@ def closure(mode, *args):
961961
im.load()
962962
ImageFile.LOAD_TRUNCATED_IMAGES = False
963963

964+
def test_separate_tables(self):
965+
im = hopper()
966+
data = [] # [interchange, tables-only, image-only]
967+
for streamtype in range(3):
968+
out = BytesIO()
969+
im.save(out, format="JPEG", streamtype=streamtype)
970+
data.append(out.getvalue())
971+
972+
# SOI, EOI
973+
for marker in b"\xff\xd8", b"\xff\xd9":
974+
assert marker in data[1] and marker in data[2]
975+
# DHT, DQT
976+
for marker in b"\xff\xc4", b"\xff\xdb":
977+
assert marker in data[1] and marker not in data[2]
978+
# SOF0, SOS, APP0 (JFIF header)
979+
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
980+
assert marker not in data[1] and marker in data[2]
981+
982+
with Image.open(BytesIO(data[0])) as interchange_im:
983+
with Image.open(BytesIO(data[1] + data[2])) as combined_im:
984+
assert_image_equal(interchange_im, combined_im)
985+
964986
def test_repr_jpeg(self):
965987
im = hopper()
966988

src/PIL/PngImagePlugin.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,9 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
11561156
encoderinfo["duration"] = duration
11571157
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
11581158

1159+
if len(im_frames) == 1 and not default_image:
1160+
return im_frames[0]["im"]
1161+
11591162
# animation control
11601163
chunk(
11611164
fp,
@@ -1391,8 +1394,10 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
13911394
chunk(fp, b"eXIf", exif)
13921395

13931396
if save_all:
1394-
_write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
1395-
else:
1397+
im = _write_multiple_frames(
1398+
im, fp, chunk, rawmode, default_image, append_images
1399+
)
1400+
if im:
13961401
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
13971402

13981403
if info:

src/libImaging/JpegEncode.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
218218
}
219219
switch (context->streamtype) {
220220
case 1:
221-
/* tables only -- not yet implemented */
222-
state->errcode = IMAGING_CODEC_CONFIG;
223-
return -1;
221+
/* tables only */
222+
jpeg_write_tables(&context->cinfo);
223+
goto cleanup;
224224
case 2:
225225
/* image only */
226226
jpeg_suppress_tables(&context->cinfo, TRUE);
@@ -316,6 +316,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
316316
}
317317
jpeg_finish_compress(&context->cinfo);
318318

319+
cleanup:
319320
/* Clean up */
320321
if (context->comment) {
321322
free(context->comment);

wheels/config.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ HARFBUZZ_VERSION=8.2.1
99
LIBPNG_VERSION=1.6.40
1010
JPEGTURBO_VERSION=3.0.1
1111
OPENJPEG_VERSION=2.5.0
12-
XZ_VERSION=5.4.4
12+
XZ_VERSION=5.4.5
1313
TIFF_VERSION=4.6.0
1414
LCMS2_VERSION=2.15
1515
if [[ -n "$IS_MACOS" ]]; then

0 commit comments

Comments
 (0)