diff --git a/CHANGES.rst b/CHANGES.rst index c3e60acff..060569ab5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,60 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Added support for reading BMP images with RLE4 compression #6674 + [npjg, radarhere] + +- Decode JPEG compressed BLP1 data in original mode #6678 + [radarhere] + +- Added GPS TIFF tag info #6661 + [radarhere] + +- Added conversion between RGB/RGBA/RGBX and LAB #6647 + [radarhere] + +- Do not attempt normalization if mode is already normal #6644 + [radarhere] + +- Fixed seeking to an L frame in a GIF #6576 + [radarhere] + +- Consider all frames when selecting mode for PNG save_all #6610 + [radarhere] + +- Don't reassign crc on ChunkStream close #6627 + [wiredfool, radarhere] + +- Raise a warning if NumPy failed to raise an error during conversion #6594 + [radarhere] + +- Show all frames in ImageShow #6611 + [radarhere] + +- Allow FLI palette chunk to not be first #6626 + [radarhere] + +- If first GIF frame has transparency for RGB_ALWAYS loading strategy, use RGBA mode #6592 + [radarhere] + +- Round box position to integer when pasting embedded color #6517 + [radarhere, nulano] + +- Removed EXIF prefix when saving WebP #6582 + [radarhere] + +- Pad IM palette to 768 bytes when saving #6579 + [radarhere] + +- Added DDS BC6H reading #6449 + [ShadelessFox, REDxEYE, radarhere] + +- Added support for opening WhiteIsZero 16-bit integer TIFF images #6642 + [JayWiz, radarhere] + +- Raise an error when allocating translucent color to RGB palette #6654 + [jsbueno, radarhere] + - Added reading of TIFF child images #6569 [radarhere] diff --git a/Tests/helper.py b/Tests/helper.py index 13c6955e4..0d1d03ac8 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -208,12 +208,11 @@ class PillowLeakTestCase: # ru_maxrss # This is the maximum resident set size utilized (in bytes). return mem / 1024 # Kb - else: - # linux - # man 2 getrusage - # ru_maxrss (since Linux 2.6.32) - # This is the maximum resident set size used (in kilobytes). - return mem # Kb + # linux + # man 2 getrusage + # ru_maxrss (since Linux 2.6.32) + # This is the maximum resident set size used (in kilobytes). + return mem # Kb def _test_leak(self, core): start_mem = self._get_mem_usage() @@ -285,7 +284,7 @@ def magick_command(): if imagemagick and shutil.which(imagemagick[0]): return imagemagick - elif graphicsmagick and shutil.which(graphicsmagick[0]): + if graphicsmagick and shutil.which(graphicsmagick[0]): return graphicsmagick diff --git a/Tests/images/bc6h.dds b/Tests/images/bc6h.dds new file mode 100644 index 000000000..77993a0c1 Binary files /dev/null and b/Tests/images/bc6h.dds differ diff --git a/Tests/images/bc6h.png b/Tests/images/bc6h.png new file mode 100644 index 000000000..609f11489 Binary files /dev/null and b/Tests/images/bc6h.png differ diff --git a/Tests/images/bc6h_sf.dds b/Tests/images/bc6h_sf.dds new file mode 100644 index 000000000..2ab1b195b Binary files /dev/null and b/Tests/images/bc6h_sf.dds differ diff --git a/Tests/images/bc6h_sf.png b/Tests/images/bc6h_sf.png new file mode 100644 index 000000000..6a3b73d5f Binary files /dev/null and b/Tests/images/bc6h_sf.png differ diff --git a/Tests/images/blp/blp1_jpeg2.blp b/Tests/images/blp/blp1_jpeg2.blp new file mode 100644 index 000000000..890180e9b Binary files /dev/null and b/Tests/images/blp/blp1_jpeg2.blp differ diff --git a/Tests/images/bw_gradient.imt b/Tests/images/bw_gradient.imt new file mode 100644 index 000000000..d765cf95f Binary files /dev/null and b/Tests/images/bw_gradient.imt differ diff --git a/Tests/images/hopper_palette_chunk_second.fli b/Tests/images/hopper_palette_chunk_second.fli new file mode 100644 index 000000000..54447de0a Binary files /dev/null and b/Tests/images/hopper_palette_chunk_second.fli differ diff --git a/Tests/images/no_palette_after_rgb.gif b/Tests/images/no_palette_after_rgb.gif new file mode 100644 index 000000000..8704c464c Binary files /dev/null and b/Tests/images/no_palette_after_rgb.gif differ diff --git a/Tests/images/palette_not_needed_for_second_frame.gif b/Tests/images/palette_not_needed_for_second_frame.gif new file mode 100644 index 000000000..0617291d1 Binary files /dev/null and b/Tests/images/palette_not_needed_for_second_frame.gif differ diff --git a/Tests/images/text_float_coord.png b/Tests/images/text_float_coord.png new file mode 100644 index 000000000..49468698c Binary files /dev/null and b/Tests/images/text_float_coord.png differ diff --git a/Tests/images/text_float_coord_1_alt.png b/Tests/images/text_float_coord_1_alt.png new file mode 100644 index 000000000..50bdac3d8 Binary files /dev/null and b/Tests/images/text_float_coord_1_alt.png differ diff --git a/Tests/images/unimplemented_dxgi_format.dds b/Tests/images/unimplemented_dxgi_format.dds index 5ecb42006..70860f2fc 100644 Binary files a/Tests/images/unimplemented_dxgi_format.dds and b/Tests/images/unimplemented_dxgi_format.dds differ diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index cdaad5940..1f5567163 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -647,6 +647,16 @@ def test_seek_after_close(): im.seek(0) +@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) +def test_different_modes_in_later_frames(mode, tmp_path): + test_file = str(tmp_path / "temp.png") + + im = Image.new("L", (1, 1)) + im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))]) + with Image.open(test_file) as reloaded: + assert reloaded.mode == mode + + def test_constants_deprecation(): for enum, prefix in { PngImagePlugin.Disposal: "APNG_DISPOSE_", diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index c1fae44ca..ba2781820 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -14,6 +14,9 @@ def test_load_blp1(): with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") + with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im: + im.load() + def test_load_blp2_raw(): with Image.open("Tests/images/blp/blp2_raw.blp") as im: diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index f6860a9a4..5f6d52355 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -176,6 +176,11 @@ def test_rle8(): im.load() +def test_rle4(): + with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im: + assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12) + + @pytest.mark.parametrize( "file_name,length", ( diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 351001199..4b9f8949e 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -16,6 +16,8 @@ TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" TEST_FILE_BC5S = "Tests/images/bc5s.dds" +TEST_FILE_BC6H = "Tests/images/bc6h.dds" +TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" @@ -114,6 +116,20 @@ def test_dx10_bc5(image_path, expected_path): assert_image_equal_tofile(im, expected_path.replace(".dds", ".png")) +@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) +def test_dx10_bc6h(image_path): + """Check DX10 BC6H/BC6HS images can be opened""" + + with Image.open(image_path) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (128, 128) + + assert_image_equal_tofile(im, image_path.replace(".dds", ".png")) + + def test_dx10_bc7(): """Check DX10 images can be opened""" diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index a7d43d2e9..b8b999d70 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -4,7 +4,7 @@ import pytest from PIL import FliImagePlugin, Image -from .helper import assert_image_equal_tofile, is_pypy +from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. @@ -79,6 +79,12 @@ def test_invalid_file(): FliImagePlugin.FliImageFile(invalid_file) +def test_palette_chunk_second(): + with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im: + with Image.open(static_test_file) as expected: + assert_image_equal(im.convert("RGB"), expected.convert("RGB")) + + def test_n_frames(): with Image.open(static_test_file) as im: assert im.n_frames == 1 diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 637be2be4..926f5c1ee 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -83,18 +83,40 @@ def test_l_mode_transparency(): assert im.load()[0, 0] == 128 +def test_l_mode_after_rgb(): + with Image.open("Tests/images/no_palette_after_rgb.gif") as im: + im.seek(1) + assert im.mode == "RGB" + + im.seek(2) + assert im.mode == "RGB" + + +def test_palette_not_needed_for_second_frame(): + with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: + im.seek(1) + assert_image_similar(im, hopper("L").convert("RGB"), 8) + + def test_strategy(): + with Image.open("Tests/images/iss634.gif") as im: + expected_rgb_always = im.convert("RGB") + with Image.open("Tests/images/chi.gif") as im: - expected_zero = im.convert("RGB") + expected_rgb_always_rgba = im.convert("RGBA") im.seek(1) - expected_one = im.convert("RGB") + expected_different = im.convert("RGB") try: GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS - with Image.open("Tests/images/chi.gif") as im: + with Image.open("Tests/images/iss634.gif") as im: assert im.mode == "RGB" - assert_image_equal(im, expected_zero) + assert_image_equal(im, expected_rgb_always) + + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "RGBA" + assert_image_equal(im, expected_rgb_always_rgba) GifImagePlugin.LOADING_STRATEGY = ( GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY @@ -105,7 +127,7 @@ def test_strategy(): im.seek(1) assert im.mode == "P" - assert_image_equal(im.convert("RGB"), expected_one) + assert_image_equal(im.convert("RGB"), expected_different) # Change to RGB mode when a frame has an individual palette with Image.open("Tests/images/iss634.gif") as im: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index e458a197c..5cf93713b 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -86,6 +86,18 @@ def test_roundtrip(mode, tmp_path): assert_image_equal_tofile(im, out) +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 1, 2] + im.putpalette(colors) + + out = str(tmp_path / "temp.im") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + [0] * 765 + + def test_save_unsupported_mode(tmp_path): out = str(tmp_path / "temp.im") im = hopper("HSV") diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py new file mode 100644 index 000000000..f56acc429 --- /dev/null +++ b/Tests/test_file_imt.py @@ -0,0 +1,19 @@ +import io + +import pytest + +from PIL import Image, ImtImagePlugin + +from .helper import assert_image_equal_tofile + + +def test_sanity(): + with Image.open("Tests/images/bw_gradient.imt") as im: + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + + +@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n")) +def test_invalid_file(data): + with io.BytesIO(data) as fp: + with pytest.raises(SyntaxError): + ImtImagePlugin.ImtImageFile(fp) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index adbb72aa5..fa96e425b 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -30,7 +30,7 @@ from .helper import ( ) try: - import defusedxml.ElementTree as ElementTree + from defusedxml import ElementTree except ImportError: ElementTree = None diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index d9066c589..58d3aac06 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -934,7 +934,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, exif=tags, compression=compression) with Image.open(out) as reloaded: - for tag in tags.keys(): + for tag in tags: assert tag not in reloaded.getexif() def test_old_style_jpeg(self): diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 1af0223eb..37235fe6f 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -20,7 +20,7 @@ from .helper import ( ) try: - import defusedxml.ElementTree as ElementTree + from defusedxml import ElementTree except ImportError: ElementTree = None diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 5c6376caf..fbcbea6c6 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -240,8 +240,8 @@ def test_header_token_too_long(tmp_path): def test_truncated_file(tmp_path): # Test EOF in header path = str(tmp_path / "temp.pgm") - with open(path, "w") as f: - f.write("P6") + with open(path, "wb") as f: + f.write(b"P6") with pytest.raises(ValueError) as e: with Image.open(path): @@ -256,11 +256,11 @@ def test_truncated_file(tmp_path): im.load() -@pytest.mark.parametrize("maxval", (0, 65536)) +@pytest.mark.parametrize("maxval", (b"0", b"65536")) def test_invalid_maxval(maxval, tmp_path): path = str(tmp_path / "temp.ppm") - with open(path, "w") as f: - f.write("P6\n3 1 " + str(maxval)) + with open(path, "wb") as f: + f.write(b"P6\n3 1 " + maxval) with pytest.raises(ValueError) as e: with Image.open(path): @@ -283,13 +283,13 @@ def test_neg_ppm(): def test_mimetypes(tmp_path): path = str(tmp_path / "temp.pgm") - with open(path, "w") as f: - f.write("P4\n128 128\n255") + with open(path, "wb") as f: + f.write(b"P4\n128 128\n255") with Image.open(path) as im: assert im.get_format_mimetype() == "image/x-portable-bitmap" - with open(path, "w") as f: - f.write("PyCMYK\n128 128\n255") + with open(path, "wb") as f: + f.write(b"PyCMYK\n128 128\n255") with Image.open(path) as im: assert im.get_format_mimetype() == "image/x-portable-anymap" diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 1a5ba594f..a32e6a005 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -18,7 +18,7 @@ from .helper import ( ) try: - import defusedxml.ElementTree as ElementTree + from defusedxml import ElementTree except ImportError: ElementTree = None diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index e6d6fc63f..f77a245c0 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -55,9 +55,7 @@ def test_write_exif_metadata(): test_buffer.seek(0) with Image.open(test_buffer) as webp_image: webp_exif = webp_image.info.get("exif", None) - assert webp_exif - if webp_exif: - assert webp_exif == expected_exif, "WebP EXIF didn't match" + assert webp_exif == expected_exif[6:], "WebP EXIF didn't match" def test_read_icc_profile(): diff --git a/Tests/test_image.py b/Tests/test_image.py index ab945e946..e57903490 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -129,8 +129,6 @@ class TestImage: im.size = (3, 4) def test_invalid_image(self): - import io - im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): with Image.open(im): @@ -699,15 +697,15 @@ class TestImage: def test_empty_exif(self): with Image.open("Tests/images/exif.png") as im: exif = im.getexif() - assert dict(exif) != {} + assert dict(exif) # Test that exif data is cleared after another load exif.load(None) - assert dict(exif) == {} + assert not dict(exif) # Test loading just the EXIF header exif.load(b"Exif\x00\x00") - assert dict(exif) == {} + assert not dict(exif) @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index bb09a7708..955740b95 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -131,8 +131,7 @@ class TestImageGetPixel(AccessTest): bands = Image.getmodebands(mode) if bands == 1: return 1 - else: - return tuple(range(1, bands + 1)) + return tuple(range(1, bands + 1)) def check(self, mode, c=None): if not c: @@ -345,13 +344,14 @@ class TestCffi(AccessTest): @pytest.mark.parametrize("mode", ("P", "PA")) def test_p_putpixel_rgb_rgba(self, mode): - for color in [(255, 0, 0), (255, 0, 0, 127)]: + for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) access = PyAccess.new(im, False) access.putpixel((0, 0), color) - alpha = color[3] if len(color) == 4 and mode == "PA" else 255 - assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) + if len(color) == 3: + color += (255,) + assert im.convert("RGBA").getpixel((0, 0)) == color class TestImagePutPixelError(AccessTest): @@ -414,7 +414,7 @@ class TestEmbeddable: def test_embeddable(self): import ctypes - with open("embed_pil.c", "w") as fh: + with open("embed_pil.c", "w", encoding="utf-8") as fh: fh.write( """ #include "Python.h" diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 7e5fd6fe1..ae3518e44 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -35,10 +35,13 @@ def test_toarray(): test_with_dtype(numpy.float64) test_with_dtype(numpy.uint8) - if parse_version(numpy.__version__) >= parse_version("1.23"): - with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: + with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: + if parse_version(numpy.__version__) >= parse_version("1.23"): with pytest.raises(OSError): numpy.array(im_truncated) + else: + with pytest.warns(UserWarning): + numpy.array(im_truncated) def test_fromarray(): diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 1a78f8b4c..902d8bf8f 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -38,6 +38,12 @@ def test_sanity(): convert(im, output_mode) +def test_unsupported_conversion(): + im = hopper() + with pytest.raises(ValueError): + im.convert("INVALID") + + def test_default(): im = hopper("P") @@ -242,6 +248,17 @@ def test_p2pa_palette(): assert im_pa.getpalette() == im.getpalette() +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) +def test_rgb_lab(mode): + im = Image.new(mode, (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (0, 128, 128) + + im = Image.new("LAB", (1, 1), (255, 0, 0)) + converted_im = im.convert(mode) + assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) + + def test_matrix_illegal_conversion(): # Arrange im = hopper("CMYK") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index a374e24c5..e5cf8b631 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -935,7 +935,30 @@ def test_standard_embedded_color(layout_engine): d = ImageDraw.Draw(im) d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) - assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2) + assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) + + +@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) +def test_float_coord(layout_engine, fontmode): + txt = "Hello World!" + ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + + im = Image.new("RGB", (300, 64), "white") + d = ImageDraw.Draw(im) + if fontmode == "1": + d.fontmode = "1" + + embedded_color = fontmode == "RGBA" + d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=embedded_color) + try: + assert_image_similar_tofile(im, "Tests/images/text_float_coord.png", 3.9) + except AssertionError: + if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC: + assert_image_similar_tofile( + im, "Tests/images/text_float_coord_1_alt.png", 1 + ) + else: + raise def test_cbdt(layout_engine): diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 39d91eade..fe7ac9a7a 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -6,10 +6,8 @@ from PIL import Image, ImageMath def pixel(im): if hasattr(im, "im"): return f"{im.mode} {repr(im.getpixel((0, 0)))}" - else: - if isinstance(im, int): - return int(im) # hack to deal with booleans - print(im) + if isinstance(im, int): + return int(im) # hack to deal with booleans A = Image.new("L", (1, 1), 1) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 475d249ed..5bda28117 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -50,6 +50,16 @@ def test_getcolor(): palette.getcolor("unknown") +def test_getcolor_rgba_color_rgb_palette(): + palette = ImagePalette.ImagePalette("RGB") + + # Opaque RGBA colors are converted + assert palette.getcolor((0, 0, 0, 255)) == palette.getcolor((0, 0, 0)) + + with pytest.raises(ValueError): + palette.getcolor((0, 0, 0, 128)) + + @pytest.mark.parametrize( "index, palette", [ diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 66eeaf6f8..a9b33e437 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -60,7 +60,10 @@ Pillow also provides limited support for a few additional modes, including: * ``BGR;24`` (24-bit reversed true colour) * ``BGR;32`` (32-bit reversed true colour) -However, Pillow doesn’t support user-defined modes; if you need to handle band +Apart from these additional modes, Pillow doesn't yet support multichannel +images with a depth of more than 8 bits per channel. + +Pillow also doesn’t support user-defined modes; if you need to handle band combinations that are not listed above, use a sequence of Image objects. You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index dc629666c..1e79db68b 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -45,9 +45,9 @@ BMP ^^^ Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``, -or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding -is not supported. Support for reading 8-bit run-length encoding was added in Pillow -9.1.0. +or ``RGB`` data. 16-colour images are read as ``P`` images. +Support for reading 8-bit run-length encoding was added in Pillow 9.1.0. +Support for reading 4-bit run-length encoding was added in Pillow 9.3.0. Opening ~~~~~~~ @@ -56,7 +56,8 @@ The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **compression** - Set to ``bmp_rle`` if the file is run-length encoded. + Set to 1 if the file is a 256-color run-length encoded image. + Set to 2 if the file is a 16-color run-length encoded image. DDS ^^^ diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 57646e558..48ce6fef7 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -202,7 +202,7 @@ Pillow now builds binary wheels for musllinux, suitable for Linux distributions (rather than the glibc library used by manylinux wheels). See :pep:`656`. ImageShow temporary files on Unix -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`, a temporary file is created from the image. On Unix, Pillow will no longer delete these diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index 7109a09f2..5a4086748 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -4,25 +4,6 @@ Backwards Incompatible Changes ============================== -TODO -^^^^ - -Deprecations -============ - -TODO -^^^^ - -TODO - -API Changes -=========== - -TODO -^^^^ - -TODO - API Additions ============= @@ -55,15 +36,25 @@ Additional images can also be appended when saving, by combining the Security ======== -TODO -^^^^ +Decode JPEG compressed BLP1 data in original mode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Within the BLP image format, BLP1 data may use JPEG compression. Instead of +telling the JPEG library that this data is in BGRX mode, Pillow will now +decode the data in its natural CMYK mode, then convert it to RGB and rearrange +the channels afterwards. Trying to load the data in an incorrect mode could +result in a segmentation fault. Other Changes ============= -Added DDS ATI1 and ATI2 reading -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Added DDS ATI1, ATI2 and BC6H reading +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Support has been added to read the ATI1 and ATI2 formats of DDS images. +Support has been added to read the ATI1, ATI2 and BC6H formats of DDS images. + +Show all frames with ImageShow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calling :py:meth:`~PIL.Image.Image.show` or using +:py:mod:`~PIL.ImageShow`, all frames will now be shown. diff --git a/setup.py b/setup.py index aa3168aa5..14c404752 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ from setuptools.command.build_ext import build_ext def get_version(): version_file = "src/PIL/_version.py" - with open(version_file) as f: + with open(version_file, encoding="utf-8") as f: exec(compile(f.read(), version_file, "exec")) return locals()["__version__"] diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 104fbada9..533997737 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -373,8 +373,8 @@ class BLP1Decoder(_BLPBaseDecoder): data = BytesIO(data) image = JpegImageFile(data) Image._decompression_bomb_check(image.size) - image.mode = "RGB" - image.tile = [("jpeg", (0, 0) + self.size, 0, ("BGRX", ""))] + r, g, b = image.convert("RGB").split() + image = Image.merge("RGB", (b, g, r)) self.set_as_raw(image.tobytes()) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 8e69acd36..b8332062e 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -214,7 +214,7 @@ class BmpImageFile(ImageFile.ImageFile): raw_mode, self.mode = "BGRA", "RGBA" except AttributeError: pass - elif file_info["compression"] == self.RLE8: + elif file_info["compression"] in (self.RLE8, self.RLE4): decoder_name = "bmp_rle" else: raise OSError(f"Unsupported BMP compression ({file_info['compression']})") @@ -253,16 +253,18 @@ class BmpImageFile(ImageFile.ImageFile): # ---------------------------- Finally set the tile data for the plugin self.info["compression"] = file_info["compression"] + args = [raw_mode] + if decoder_name == "bmp_rle": + args.append(file_info["compression"] == self.RLE4) + else: + args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) + args.append(file_info["direction"]) self.tile = [ ( decoder_name, (0, 0, file_info["width"], file_info["height"]), offset or self.fp.tell(), - ( - raw_mode, - ((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3), - file_info["direction"], - ), + tuple(args), ) ] @@ -283,6 +285,7 @@ class BmpRleDecoder(ImageFile.PyDecoder): _pulls_fd = True def decode(self, buffer): + rle4 = self.args[1] data = bytearray() x = 0 while len(data) < self.state.xsize * self.state.ysize: @@ -296,7 +299,16 @@ class BmpRleDecoder(ImageFile.PyDecoder): if x + num_pixels > self.state.xsize: # Too much data for row num_pixels = max(0, self.state.xsize - x) - data += byte * num_pixels + if rle4: + first_pixel = o8(byte[0] >> 4) + second_pixel = o8(byte[0] & 0x0F) + for index in range(num_pixels): + if index % 2 == 0: + data += first_pixel + else: + data += second_pixel + else: + data += byte * num_pixels x += num_pixels else: if byte[0] == 0: @@ -317,9 +329,18 @@ class BmpRleDecoder(ImageFile.PyDecoder): x = len(data) % self.state.xsize else: # absolute mode - bytes_read = self.fd.read(byte[0]) - data += bytes_read - if len(bytes_read) < byte[0]: + if rle4: + # 2 pixels per byte + byte_count = byte[0] // 2 + bytes_read = self.fd.read(byte_count) + for byte_read in bytes_read: + data += o8(byte_read >> 4) + data += o8(byte_read & 0x0F) + else: + byte_count = byte[0] + bytes_read = self.fd.read(byte_count) + data += bytes_read + if len(bytes_read) < byte_count: break x += byte[0] diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index bba480161..eea6e3153 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -101,6 +101,8 @@ DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 DXGI_FORMAT_BC5_TYPELESS = 82 DXGI_FORMAT_BC5_UNORM = 83 DXGI_FORMAT_BC5_SNORM = 84 +DXGI_FORMAT_BC6H_UF16 = 95 +DXGI_FORMAT_BC6H_SF16 = 96 DXGI_FORMAT_BC7_TYPELESS = 97 DXGI_FORMAT_BC7_UNORM = 98 DXGI_FORMAT_BC7_UNORM_SRGB = 99 @@ -181,6 +183,14 @@ class DdsImageFile(ImageFile.ImageFile): self.pixel_format = "BC5S" n = 5 self.mode = "RGB" + elif dxgi_format == DXGI_FORMAT_BC6H_UF16: + self.pixel_format = "BC6H" + n = 6 + self.mode = "RGB" + elif dxgi_format == DXGI_FORMAT_BC6H_SF16: + self.pixel_format = "BC6HS" + n = 6 + self.mode = "RGB" elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM): self.pixel_format = "BC7" n = 7 diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index e13b1779c..908bed9f4 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -15,6 +15,7 @@ # See the README file for information on usage and redistribution. # +import os from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -80,11 +81,19 @@ class FliImageFile(ImageFile.ImageFile): if i16(s, 4) == 0xF1FA: # look for palette chunk - s = self.fp.read(6) - if i16(s, 4) == 11: - self._palette(palette, 2) - elif i16(s, 4) == 4: - self._palette(palette, 0) + number_of_subchunks = i16(s, 6) + chunk_size = None + for _ in range(number_of_subchunks): + if chunk_size is not None: + self.fp.seek(chunk_size - 6, os.SEEK_CUR) + s = self.fp.read(6) + chunk_type = i16(s, 4) + if chunk_type in (4, 11): + self._palette(palette, 2 if chunk_type == 11 else 0) + break + chunk_size = i32(s) + if not chunk_size: + break palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette] self.palette = ImagePalette.raw("RGB", b"".join(palette)) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 20435fe31..dd1b21f2e 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -274,6 +274,8 @@ class GifImageFile(ImageFile.ImageFile): p = self.fp.read(3 << bits) if self._is_palette_needed(p): palette = ImagePalette.raw("RGB", p) + else: + palette = False # image data bits = self.fp.read(1)[0] @@ -298,12 +300,14 @@ class GifImageFile(ImageFile.ImageFile): if self.dispose: self.im.paste(self.dispose, self.dispose_extent) - self._frame_palette = palette or self.global_palette + self._frame_palette = palette if palette is not None else self.global_palette + self._frame_transparency = frame_transparency if frame == 0: if self._frame_palette: - self.mode = ( - "RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P" - ) + if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: + self.mode = "RGBA" if frame_transparency is not None else "RGB" + else: + self.mode = "P" else: self.mode = "L" @@ -313,7 +317,6 @@ class GifImageFile(ImageFile.ImageFile): palette = copy(self.global_palette) self.palette = palette else: - self._frame_transparency = frame_transparency if self.mode == "P": if ( LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY @@ -386,7 +389,8 @@ class GifImageFile(ImageFile.ImageFile): transparency = -1 if frame_transparency is not None: if frame == 0: - self.info["transparency"] = frame_transparency + if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: + self.info["transparency"] = frame_transparency elif self.mode not in ("RGB", "RGBA"): transparency = frame_transparency self.tile = [ @@ -410,9 +414,9 @@ class GifImageFile(ImageFile.ImageFile): temp_mode = "P" if self._frame_palette else "L" self._prev_im = None if self.__frame == 0: - if "transparency" in self.info: + if self._frame_transparency is not None: self.im = Image.core.fill( - temp_mode, self.size, self.info["transparency"] + temp_mode, self.size, self._frame_transparency ) elif self.mode in ("RGB", "RGBA"): self._prev_im = self.im @@ -429,19 +433,20 @@ class GifImageFile(ImageFile.ImageFile): def load_end(self): if self.__frame == 0: if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: - self.mode = "RGB" - self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) + if self._frame_transparency is not None: + self.im.putpalettealpha(self._frame_transparency, 0) + self.mode = "RGBA" + else: + self.mode = "RGB" + self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) return - if self.mode == "P" and self._prev_im: - if self._frame_transparency is not None: - self.im.putpalettealpha(self._frame_transparency, 0) - frame_im = self.im.convert("RGBA") - else: - frame_im = self.im.convert("RGB") + if not self._prev_im: + return + if self._frame_transparency is not None: + self.im.putpalettealpha(self._frame_transparency, 0) + frame_im = self.im.convert("RGBA") else: - if not self._prev_im: - return - frame_im = self.im + frame_im = self.im.convert("RGB") frame_im = self._crop(frame_im, self.dispose_extent) self.im = self._prev_im diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 78ccfb9cf..31b0ff469 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -352,7 +352,13 @@ def _save(im, fp, filename): fp.write(b"Lut: 1\r\n") fp.write(b"\000" * (511 - fp.tell()) + b"\032") if im.mode in ["P", "PA"]: - fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes + im_palette = im.im.getpalette("RGB", "RGB;L") + colors = len(im_palette) // 3 + palette = b"" + for i in range(3): + palette += im_palette[colors * i : colors * (i + 1)] + palette += b"\x00" * (256 - colors) + fp.write(palette) # 768 bytes ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6611ceb3c..7faf0c248 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -679,12 +679,24 @@ class Image: new["shape"] = shape new["typestr"] = typestr new["version"] = 3 - if self.mode == "1": - # Binary images need to be extended from bits to bytes - # See: https://github.com/python-pillow/Pillow/issues/350 - new["data"] = self.tobytes("raw", "L") - else: - new["data"] = self.tobytes() + try: + if self.mode == "1": + # Binary images need to be extended from bits to bytes + # See: https://github.com/python-pillow/Pillow/issues/350 + new["data"] = self.tobytes("raw", "L") + else: + new["data"] = self.tobytes() + except Exception as e: + if not isinstance(e, (MemoryError, RecursionError)): + try: + import numpy + from packaging.version import parse as parse_version + except ImportError: + pass + else: + if parse_version(numpy.__version__) < parse_version("1.23"): + warnings.warn(e) + raise return new def __getstate__(self): @@ -868,7 +880,7 @@ class Image: and the palette can be represented without a palette. The current version supports all possible conversions between - "L", "RGB" and "CMYK." The ``matrix`` argument only supports "L" + "L", "RGB" and "CMYK". The ``matrix`` argument only supports "L" and "RGB". When translating a color image to greyscale (mode "L"), @@ -887,6 +899,9 @@ class Image: this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, and ``dither`` and ``palette`` are ignored. + When converting from "PA", if an "RGBA" palette is present, the alpha + channel from the image will be used instead of the values from the palette. + :param mode: The requested mode. See: :ref:`concept-modes`. :param matrix: An optional conversion matrix. If given, this should be 4- or 12-tuple containing floating point values. @@ -1027,6 +1042,19 @@ class Image: warnings.warn("Couldn't allocate palette entry for transparency") return new + if "LAB" in (self.mode, mode): + other_mode = mode if self.mode == "LAB" else self.mode + if other_mode in ("RGB", "RGBA", "RGBX"): + from . import ImageCms + + srgb = ImageCms.createProfile("sRGB") + lab = ImageCms.createProfile("LAB") + profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab] + transform = ImageCms.buildTransform( + profiles[0], profiles[1], self.mode, mode + ) + return transform.apply(self) + # colorspace conversion if dither is None: dither = Dither.FLOYDSTEINBERG @@ -1036,7 +1064,10 @@ class Image: except ValueError: try: # normalize source image and try again - im = self.im.convert(getmodebase(self.mode)) + modebase = getmodebase(self.mode) + if modebase == self.mode: + raise + im = self.im.convert(modebase) im = im.convert(mode, dither) except KeyError as e: raise ValueError("illegal conversion") from e diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e84dafb12..3a6063a6c 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -482,8 +482,8 @@ class ImageDraw: # extract mask and set text alpha color, mask = mask, mask.getband(3) color.fillband(3, (ink >> 24) & 0xFF) - coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1] - self.im.paste(color, coord + coord2, mask) + x, y = (int(c) for c in coord) + self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) else: self.draw.draw_bitmap(coord, mask, ink) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 8be7f0f10..b3a7cdb98 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -955,6 +955,11 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): encoding of any text provided in subsequent operations. :param layout_engine: Which layout engine to use, if available: :data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`. + If it is available, Raqm layout will be used by default. + Otherwise, basic layout will be used. + + Raqm layout is recommended for all non-English text. If Raqm layout + is not required, basic layout will have better performance. You can check support for Raqm layout using :py:func:`PIL.features.check_feature` with ``feature="raqm"``. diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index b73b2cd9d..fe76c86f4 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -115,7 +115,11 @@ class ImagePalette: raise ValueError("palette contains raw palette data") if isinstance(color, tuple): if self.mode == "RGB": - if len(color) == 4 and color[3] == 255: + if len(color) == 4: + if color[3] != 255: + raise ValueError( + "cannot add non-opaque RGBA color to RGB palette" + ) color = color[:3] elif self.mode == "RGBA": if len(color) == 3: diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 9f9a551fb..76f42a307 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -136,7 +136,7 @@ class WindowsViewer(Viewer): """The default viewer on Windows is the default system application for PNG files.""" format = "PNG" - options = {"compress_level": 1} + options = {"compress_level": 1, "save_all": True} def get_command(self, file, **options): return ( @@ -154,7 +154,7 @@ class MacViewer(Viewer): """The default viewer on macOS using ``Preview.app``.""" format = "PNG" - options = {"compress_level": 1} + options = {"compress_level": 1, "save_all": True} def get_command(self, file, **options): # on darwin open returns immediately resulting in the temp @@ -197,7 +197,7 @@ if sys.platform == "darwin": class UnixViewer(Viewer): format = "PNG" - options = {"compress_level": 1} + options = {"compress_level": 1, "save_all": True} def get_command(self, file, **options): command = self.get_command_ex(file, **options)[0] diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 5790acdaf..dc7078012 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -39,15 +39,19 @@ class ImtImageFile(ImageFile.ImageFile): # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. - if b"\n" not in self.fp.read(100): + buffer = self.fp.read(100) + if b"\n" not in buffer: raise SyntaxError("not an IM file") - self.fp.seek(0) xsize = ysize = 0 while True: - s = self.fp.read(1) + if buffer: + s = buffer[:1] + buffer = buffer[1:] + else: + s = self.fp.read(1) if not s: break @@ -55,7 +59,12 @@ class ImtImageFile(ImageFile.ImageFile): # image data begins self.tile = [ - ("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1)) + ( + "raw", + (0, 0) + self.size, + self.fp.tell() - len(buffer), + (self.mode, 0, 1), + ) ] break @@ -63,8 +72,11 @@ class ImtImageFile(ImageFile.ImageFile): else: # read key/value pair - # FIXME: dangerous, may read whole file - s = s + self.fp.readline() + if b"\n" not in buffer: + buffer += self.fp.read(100) + lines = buffer.split(b"\n") + s += lines.pop(0) + buffer = b"\n".join(lines) if len(s) == 1 or len(s) > 100: break if s[0] == ord(b"*"): @@ -74,13 +86,13 @@ class ImtImageFile(ImageFile.ImageFile): if not m: break k, v = m.group(1, 2) - if k == "width": + if k == b"width": xsize = int(v) self._size = xsize, ysize - elif k == "height": + elif k == b"height": ysize = int(v) self._size = xsize, ysize - elif k == "pixel" and v == "n8": + elif k == b"pixel" and v == b"n8": self.mode = "L" diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 442c65e6f..7fb468877 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -189,7 +189,7 @@ class ChunkStream: self.close() def close(self): - self.queue = self.crc = self.fp = None + self.queue = self.fp = None def push(self, cid, pos, length): @@ -224,7 +224,7 @@ class ChunkStream: ) from e def crc_skip(self, cid, data): - """Read checksum. Used if the C module is not present""" + """Read checksum""" self.fp.read(4) @@ -1089,28 +1089,28 @@ class _fdat: self.seq_num += 1 -def _write_multiple_frames(im, fp, chunk, rawmode): - default_image = im.encoderinfo.get("default_image", im.info.get("default_image")) +def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images): duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) if default_image: - chain = itertools.chain(im.encoderinfo.get("append_images", [])) + chain = itertools.chain(append_images) else: - chain = itertools.chain([im], im.encoderinfo.get("append_images", [])) + chain = itertools.chain([im], append_images) im_frames = [] frame_count = 0 for im_seq in chain: for im_frame in ImageSequence.Iterator(im_seq): - im_frame = im_frame.copy() - if im_frame.mode != im.mode: - if im.mode == "P": - im_frame = im_frame.convert(im.mode, palette=im.palette) + if im_frame.mode == rawmode: + im_frame = im_frame.copy() + else: + if rawmode == "P": + im_frame = im_frame.convert(rawmode, palette=im.palette) else: - im_frame = im_frame.convert(im.mode) + im_frame = im_frame.convert(rawmode) encoderinfo = im.encoderinfo.copy() if isinstance(duration, (list, tuple)): encoderinfo["duration"] = duration[frame_count] @@ -1221,7 +1221,26 @@ def _save_all(im, fp, filename): def _save(im, fp, filename, chunk=putchunk, save_all=False): # save an image to disk (called by the save method) - mode = im.mode + if save_all: + default_image = im.encoderinfo.get( + "default_image", im.info.get("default_image") + ) + modes = set() + append_images = im.encoderinfo.get("append_images", []) + if default_image: + chain = itertools.chain(append_images) + else: + chain = itertools.chain([im], append_images) + for im_seq in chain: + for im_frame in ImageSequence.Iterator(im_seq): + modes.add(im_frame.mode) + for mode in ("RGBA", "RGB", "P"): + if mode in modes: + break + else: + mode = modes.pop() + else: + mode = im.mode if mode == "P": @@ -1373,7 +1392,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): chunk(fp, b"eXIf", exif) if save_all: - _write_multiple_frames(im, fp, chunk, rawmode) + _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) else: ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 766d46ffb..04a63bd2b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -173,6 +173,7 @@ OPEN_INFO = { (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), + (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 3f3a1ccd2..9b5277138 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -232,7 +232,39 @@ TAGS_V2_GROUPS = { 41730: ("CFAPattern", UNDEFINED, 1), }, # GPSInfoIFD - 34853: {}, + 34853: { + 0: ("GPSVersionID", BYTE, 4), + 1: ("GPSLatitudeRef", ASCII, 2), + 2: ("GPSLatitude", RATIONAL, 3), + 3: ("GPSLongitudeRef", ASCII, 2), + 4: ("GPSLongitude", RATIONAL, 3), + 5: ("GPSAltitudeRef", BYTE, 1), + 6: ("GPSAltitude", RATIONAL, 1), + 7: ("GPSTimeStamp", RATIONAL, 3), + 8: ("GPSSatellites", ASCII, 0), + 9: ("GPSStatus", ASCII, 2), + 10: ("GPSMeasureMode", ASCII, 2), + 11: ("GPSDOP", RATIONAL, 1), + 12: ("GPSSpeedRef", ASCII, 2), + 13: ("GPSSpeed", RATIONAL, 1), + 14: ("GPSTrackRef", ASCII, 2), + 15: ("GPSTrack", RATIONAL, 1), + 16: ("GPSImgDirectionRef", ASCII, 2), + 17: ("GPSImgDirection", RATIONAL, 1), + 18: ("GPSMapDatum", ASCII, 0), + 19: ("GPSDestLatitudeRef", ASCII, 2), + 20: ("GPSDestLatitude", RATIONAL, 3), + 21: ("GPSDestLongitudeRef", ASCII, 2), + 22: ("GPSDestLongitude", RATIONAL, 3), + 23: ("GPSDestBearingRef", ASCII, 2), + 24: ("GPSDestBearing", RATIONAL, 1), + 25: ("GPSDestDistanceRef", ASCII, 2), + 26: ("GPSDestDistance", RATIONAL, 1), + 27: ("GPSProcessingMethod", UNDEFINED, 0), + 28: ("GPSAreaInformation", UNDEFINED, 0), + 29: ("GPSDateStamp", ASCII, 11), + 30: ("GPSDifferential", SHORT, 1), + }, # InteroperabilityIFD 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)}, } diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index c1f4b730f..5eaeb10cc 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -311,9 +311,11 @@ def _save(im, fp, filename): lossless = im.encoderinfo.get("lossless", False) quality = im.encoderinfo.get("quality", 80) icc_profile = im.encoderinfo.get("icc_profile") or "" - exif = im.encoderinfo.get("exif", "") + exif = im.encoderinfo.get("exif", b"") if isinstance(exif, Image.Exif): exif = exif.tobytes() + if exif.startswith(b"Exif\x00\x00"): + exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") method = im.encoderinfo.get("method", 4) diff --git a/src/decode.c b/src/decode.c index cb018a4e7..7a9b956c5 100644 --- a/src/decode.c +++ b/src/decode.c @@ -376,11 +376,8 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { actual = "L"; break; case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ - actual = "RGB"; - break; case 6: /* BC6: 3-channel 16-bit float */ - /* TODO: support 4-channel floating point images */ - actual = "RGBAF"; + actual = "RGB"; break; default: PyErr_SetString(PyExc_ValueError, "block compression type unknown"); diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 22b36eb7a..a57b74b61 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -23,10 +23,6 @@ typedef struct { UINT8 l; } lum; -typedef struct { - FLOAT32 r, g, b; -} rgb32f; - typedef struct { UINT16 c0, c1; UINT32 lut; @@ -536,53 +532,53 @@ static const bc6_mode_info bc6_modes[] = { /* Table.F, encoded as a sequence of bit indices */ static const UINT8 bc6_bit_packings[][75] = { - {116, 132, 176, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, + {116, 132, 180, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, - 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, - 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175}, - {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 172, 173, 132, 16, 17, - 18, 19, 20, 21, 22, 133, 174, 116, 32, 33, 34, 35, 36, 37, 38, - 175, 177, 176, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, + 66, 67, 68, 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, + 129, 130, 131, 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 176, 177, 132, 16, 17, + 18, 19, 20, 21, 22, 133, 178, 116, 32, 33, 34, 35, 36, 37, 38, + 179, 181, 180, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 10, 112, 113, 114, 115, 64, 65, 66, 67, 26, - 172, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175}, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 10, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, 128, 129, 130, 131, - 96, 97, 98, 99, 172, 174, 144, 145, 146, 147, 116, 175}, + 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 176, 178, 144, 145, 146, 147, 116, 179}, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 10, 132, 112, 113, 114, 115, 64, 65, 66, 67, 26, - 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, 128, 129, 130, 131, - 96, 97, 98, 99, 173, 174, 144, 145, 146, 147, 176, 175}, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, 128, 129, 130, 131, + 96, 97, 98, 99, 177, 178, 144, 145, 146, 147, 180, 179}, {0, 1, 2, 3, 4, 5, 6, 7, 8, 132, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 116, 32, 33, 34, 35, 36, 37, 38, 39, 40, 176, + 21, 22, 23, 24, 116, 32, 33, 34, 35, 36, 37, 38, 39, 40, 180, 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175}, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, {0, 1, 2, 3, 4, 5, 6, 7, 164, 132, 16, 17, 18, 19, 20, - 21, 22, 23, 174, 116, 32, 33, 34, 35, 36, 37, 38, 39, 175, 176, + 21, 22, 23, 178, 116, 32, 33, 34, 35, 36, 37, 38, 39, 179, 180, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, 129, 130, 131, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 172, 132, 16, 17, 18, 19, 20, - 21, 22, 23, 117, 116, 32, 33, 34, 35, 36, 37, 38, 39, 165, 176, + {0, 1, 2, 3, 4, 5, 6, 7, 176, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 117, 116, 32, 33, 34, 35, 36, 37, 38, 39, 165, 180, 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 173, 132, 16, 17, 18, 19, 20, - 21, 22, 23, 133, 116, 32, 33, 34, 35, 36, 37, 38, 39, 177, 176, + 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 177, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 133, 116, 32, 33, 34, 35, 36, 37, 38, 39, 181, 180, 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, - 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175}, - {0, 1, 2, 3, 4, 5, 164, 172, 173, 132, 16, 17, 18, 19, 20, - 21, 117, 133, 174, 116, 32, 33, 34, 35, 36, 37, 165, 175, 177, 176, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 164, 176, 177, 132, 16, 17, 18, 19, 20, + 21, 117, 133, 178, 116, 32, 33, 34, 35, 36, 37, 165, 179, 181, 180, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, @@ -681,20 +677,31 @@ bc6_finalize(int v, int sign) { } } +static UINT8 +bc6_clamp(float value) { + if (value < 0.0f) { + return 0; + } else if (value > 1.0f) { + return 255; + } else { + return (UINT8) (value * 255.0f); + } +} + static void -bc6_lerp(rgb32f *col, int *e0, int *e1, int s, int sign) { +bc6_lerp(rgba *col, int *e0, int *e1, int s, int sign) { int r, g, b; int t = 64 - s; r = (e0[0] * t + e1[0] * s) >> 6; g = (e0[1] * t + e1[1] * s) >> 6; b = (e0[2] * t + e1[2] * s) >> 6; - col->r = bc6_finalize(r, sign); - col->g = bc6_finalize(g, sign); - col->b = bc6_finalize(b, sign); + col->r = bc6_clamp(bc6_finalize(r, sign)); + col->g = bc6_clamp(bc6_finalize(g, sign)); + col->b = bc6_clamp(bc6_finalize(b, sign)); } static void -decode_bc6_block(rgb32f *col, const UINT8 *src, int sign) { +decode_bc6_block(rgba *col, const UINT8 *src, int sign) { UINT16 endpoints[12]; /* storage for r0, g0, b0, r1, ... */ int ueps[12]; int i, i0, ib2, di, dw, mask, numep, s; @@ -744,21 +751,16 @@ decode_bc6_block(rgb32f *col, const UINT8 *src, int sign) { } if (sign || info->tr) { /* sign-extend e1,2,3 if signed or deltas */ for (i = 3; i < numep; i += 3) { - bc6_sign_extend(&endpoints[i + 0], info->rb); + bc6_sign_extend(&endpoints[i], info->rb); bc6_sign_extend(&endpoints[i + 1], info->gb); bc6_sign_extend(&endpoints[i + 2], info->bb); } } if (info->tr) { /* apply deltas */ - for (i = 3; i < numep; i++) { + for (i = 3; i < numep; i += 3) { endpoints[i] = (endpoints[i] + endpoints[0]) & mask; - } - if (sign) { - for (i = 3; i < numep; i += 3) { - bc6_sign_extend(&endpoints[i + 0], info->rb); - bc6_sign_extend(&endpoints[i + 1], info->gb); - bc6_sign_extend(&endpoints[i + 2], info->bb); - } + endpoints[i + 1] = (endpoints[i + 1] + endpoints[1]) & mask; + endpoints[i + 2] = (endpoints[i + 2] + endpoints[2]) & mask; } } for (i = 0; i < numep; i++) { @@ -862,8 +864,8 @@ decode_bcn( break; case 6: while (bytes >= 16) { - rgb32f col[16]; - decode_bc6_block(col, ptr, (state->state >> 4) & 1); + rgba col[16]; + decode_bc6_block(col, ptr, strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0); put_block(im, state, (const char *)col, sizeof(col[0]), C); ptr += 16; bytes -= 16; diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index fafd8141e..acf5202e5 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -432,18 +432,18 @@ fill_mask_L( } } else { + int alpha_channel = strcmp(imOut->mode, "RGBa") == 0 || + strcmp(imOut->mode, "RGBA") == 0 || + strcmp(imOut->mode, "La") == 0 || + strcmp(imOut->mode, "LA") == 0 || + strcmp(imOut->mode, "PA") == 0; for (y = 0; y < ysize; y++) { UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize; UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; for (x = 0; x < xsize; x++) { for (i = 0; i < pixelsize; i++) { UINT8 channel_mask = *mask; - if ((strcmp(imOut->mode, "RGBa") == 0 || - strcmp(imOut->mode, "RGBA") == 0 || - strcmp(imOut->mode, "La") == 0 || - strcmp(imOut->mode, "LA") == 0 || - strcmp(imOut->mode, "PA") == 0) && - i != 3 && channel_mask != 0) { + if (alpha_channel && i != 3 && channel_mask != 0) { channel_mask = 255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255); } diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f4515468f..f4858b630 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -132,9 +132,9 @@ deps = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": "https://zlib.net/zlib1212.zip", - "filename": "zlib1212.zip", - "dir": "zlib-1.2.12", + "url": "https://zlib.net/zlib1213.zip", + "filename": "zlib1213.zip", + "dir": "zlib-1.2.13", "build": [ cmd_nmake(r"win32\Makefile.msc", "clean"), cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), @@ -281,9 +281,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/5.2.0.zip", - "filename": "harfbuzz-5.2.0.zip", - "dir": "harfbuzz-5.2.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.1.zip", + "filename": "harfbuzz-5.3.1.zip", + "dir": "harfbuzz-5.3.1", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"),