From 99e401123bab56bf9c64314b506750a4ea6a9e79 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 19:46:07 +1000 Subject: [PATCH 01/21] Corrected palette size when saving --- Tests/test_file_tga.py | 12 ++++++++++++ src/PIL/TgaImagePlugin.py | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 0c8c9f304..fff127421 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -123,6 +123,18 @@ def test_save(tmp_path): assert test_im.size == (100, 100) +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 0, 0] + im.putpalette(colors) + + out = str(tmp_path / "temp.tga") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + + def test_save_wrong_mode(tmp_path): im = hopper("PA") out = str(tmp_path / "temp.tga") diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 59b89e988..7f5075317 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -193,7 +193,8 @@ def _save(im, fp, filename): warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: - colormapfirst, colormaplength, colormapentry = 0, 256, 24 + palette = im.im.getpalette("RGB", "BGR") + colormapfirst, colormaplength, colormapentry = 0, len(palette) // 3, 24 else: colormapfirst, colormaplength, colormapentry = 0, 0, 0 @@ -225,7 +226,7 @@ def _save(im, fp, filename): fp.write(id_section) if colormaptype: - fp.write(im.im.getpalette("RGB", "BGR")) + fp.write(palette) if rle: ImageFile._save( From 5d4fbdfab4fa5dc05f4b3de3304fffdeddb9ff4f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 19:46:46 +1000 Subject: [PATCH 02/21] Simplified code --- src/PIL/TgaImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 7f5075317..cd454b755 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -194,9 +194,9 @@ def _save(im, fp, filename): if colormaptype: palette = im.im.getpalette("RGB", "BGR") - colormapfirst, colormaplength, colormapentry = 0, len(palette) // 3, 24 + colormaplength, colormapentry = len(palette) // 3, 24 else: - colormapfirst, colormaplength, colormapentry = 0, 0, 0 + colormaplength, colormapentry = 0, 0 if im.mode in ("LA", "RGBA"): flags = 8 @@ -211,7 +211,7 @@ def _save(im, fp, filename): o8(id_len) + o8(colormaptype) + o8(imagetype) - + o16(colormapfirst) + + o16(0) # colormapfirst + o16(colormaplength) + o8(colormapentry) + o16(0) From be9224f28525211d88e9e769a32bed80a6480cd0 Mon Sep 17 00:00:00 2001 From: Bibin Hashley Date: Tue, 23 Aug 2022 02:57:03 +0530 Subject: [PATCH 03/21] ImageOps.contain function finding new size issue --- src/PIL/ImageOps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 0c3f900ca..61de3b696 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -255,11 +255,11 @@ def contain(image, size, method=Image.Resampling.BICUBIC): if im_ratio != dest_ratio: if im_ratio > dest_ratio: - new_height = int(image.height / image.width * size[0]) + new_height = round(image.height / image.width * size[0]) if new_height != size[1]: size = (size[0], new_height) else: - new_width = int(image.width / image.height * size[1]) + new_width = round(image.width / image.height * size[1]) if new_width != size[0]: size = (new_width, size[1]) return image.resize(size, resample=method) From df4bb3460000d222b3ac077dad925c32093f6b32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 22:32:42 +1000 Subject: [PATCH 04/21] Added test --- Tests/test_imageops.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 01e40e6d4..e3d413651 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -110,6 +110,16 @@ def test_contain(new_size): assert new_im.size == (256, 256) +def test_contain_round(): + im = Image.new("1", (43, 63), 1) + new_im = ImageOps.contain(im, (5, 7)) + assert new_im.width == 5 + + im = Image.new("1", (63, 43), 1) + new_im = ImageOps.contain(im, (7, 5)) + assert new_im.height == 5 + + def test_pad(): # Same ratio im = hopper() From f9d3ee0f4888f7618071c0a5315c916062e78854 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 22:56:19 +1000 Subject: [PATCH 05/21] Round position in pad() --- Tests/test_imageops.py | 9 +++++++++ src/PIL/ImageOps.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index e3d413651..550578f8f 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -140,6 +140,15 @@ def test_pad(): ) +def test_pad_round(): + im = Image.new("1", (1, 1), 1) + new_im = ImageOps.pad(im, (4, 1)) + assert new_im.load()[2, 0] == 1 + + new_im = ImageOps.pad(im, (1, 4)) + assert new_im.load()[0, 2] == 1 + + def test_pil163(): # Division by zero in equalize if < 255 pixels in image (@PIL163) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 61de3b696..ae43fc3bd 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -292,10 +292,10 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 else: out = Image.new(image.mode, size, color) if resized.width != size[0]: - x = int((size[0] - resized.width) * max(0, min(centering[0], 1))) + x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) out.paste(resized, (x, 0)) else: - y = int((size[1] - resized.height) * max(0, min(centering[1], 1))) + y = round((size[1] - resized.height) * max(0, min(centering[1], 1))) out.paste(resized, (0, y)) return out From 9fa421923c7a46827f5f79bca788c44cb57f14c7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Aug 2022 15:58:30 +1000 Subject: [PATCH 06/21] Removed requirement for 256 palette entries --- Tests/test_image.py | 1 + src/PIL/Image.py | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 7cebed127..ab945e946 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -620,6 +620,7 @@ class TestImage: im_remapped = im.remap_palette([1, 0]) assert im_remapped.info["transparency"] == 1 + assert len(im_remapped.getpalette()) == 6 # Test unused transparency im.info["transparency"] = 2 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4eb2dead6..e197c0182 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1944,11 +1944,7 @@ class Image: m_im = m_im.convert("L") - # Internally, we require 256 palette entries. - new_palette_bytes = ( - palette_bytes + ((256 * bands) - len(palette_bytes)) * b"\x00" - ) - m_im.putpalette(new_palette_bytes, palette_mode) + m_im.putpalette(palette_bytes, palette_mode) m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) if "transparency" in self.info: From e7fab6abf44faf3bdf45b99c239bf38569a1ece4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Aug 2022 23:20:31 +1000 Subject: [PATCH 07/21] Fixed remapping to palette with duplicate entries --- Tests/test_file_gif.py | 13 +++++++++++++ src/PIL/GifImagePlugin.py | 2 ++ 2 files changed, 15 insertions(+) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 68cb8a36e..4e967faec 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1087,6 +1087,19 @@ def test_palette_save_P(tmp_path): assert_image_equal(reloaded, im) +def test_palette_save_duplicate_entries(tmp_path): + im = Image.new("P", (1, 2)) + im.putpixel((0, 1), 1) + + im.putpalette((0, 0, 0, 0, 0, 0)) + + out = str(tmp_path / "temp.gif") + im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) + + with Image.open(out) as reloaded: + assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) + + def test_palette_save_all_P(tmp_path): frames = [] colors = ((255, 0, 0), (0, 255, 0)) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2e11df54c..40fbaa9b5 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -523,6 +523,8 @@ def _normalize_palette(im, palette, info): index = im.palette.colors[source_color] except KeyError: index = None + if index in used_palette_colors: + index = None used_palette_colors.append(index) for i, index in enumerate(used_palette_colors): if index is None: From 841ba4a940b2b09b5c07a43c7a75ce1266d0f2c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Aug 2022 08:08:01 +1000 Subject: [PATCH 08/21] Simplified code --- src/PIL/GifImagePlugin.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 40fbaa9b5..20435fe31 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -519,10 +519,7 @@ def _normalize_palette(im, palette, info): used_palette_colors = [] for i in range(0, len(source_palette), 3): source_color = tuple(source_palette[i : i + 3]) - try: - index = im.palette.colors[source_color] - except KeyError: - index = None + index = im.palette.colors.get(source_color) if index in used_palette_colors: index = None used_palette_colors.append(index) From 9ebf44f8b482954ea3e4c97f4dfcc230bde5c08a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Sep 2022 14:24:11 +0000 Subject: [PATCH 09/21] Add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..f9c2c3270 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} From bc069ec93901d9879b6768094529e3804d39d063 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Sep 2022 08:37:45 +1000 Subject: [PATCH 10/21] Added renovate.json to MANIFEST --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 26f9401f2..08f6dfc08 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -25,6 +25,7 @@ exclude .coveragerc exclude .editorconfig exclude .readthedocs.yml exclude codecov.yml +exclude renovate.json global-exclude .git* global-exclude *.pyc global-exclude *.so From 01657d128d063d5a848ef68d28df5d3ea5579208 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Sep 2022 20:19:13 +0300 Subject: [PATCH 11/21] Add label to Dependabot PRs --- renovate.json => .github/renovate.json | 3 +++ .pre-commit-config.yaml | 1 + 2 files changed, 4 insertions(+) rename renovate.json => .github/renovate.json (72%) diff --git a/renovate.json b/.github/renovate.json similarity index 72% rename from renovate.json rename to .github/renovate.json index f9c2c3270..9fd3341b4 100644 --- a/renovate.json +++ b/.github/renovate.json @@ -2,5 +2,8 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" + ], + "labels": [ + "Dependency" ] } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eeb4b391e..f81bcb956 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,6 +40,7 @@ repos: rev: v4.3.0 hooks: - id: check-merge-conflict + - id: check-json - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint From ae833dd62de4196ec82b5a10ce4daec798cd4a99 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Sep 2022 20:19:37 +0300 Subject: [PATCH 12/21] Group GHA updates into a single PR --- .github/renovate.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 9fd3341b4..cc8f0225f 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -5,5 +5,12 @@ ], "labels": [ "Dependency" - ] + ], + "packageRules": [ + { + "groupName": "github-actions", + "matchManagers": ["github-actions"], + "separateMajorMinor": "false" + } + ] } From a7471e9b843f267befea41d64bb1e1c4cce9a554 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Sep 2022 20:19:56 +0300 Subject: [PATCH 13/21] Create update PRs on the first day of the month --- .github/renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index cc8f0225f..4341752c3 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -12,5 +12,6 @@ "matchManagers": ["github-actions"], "separateMajorMinor": "false" } - ] + ], + "schedule": ["on the first day of the month"] } From f21bc40b236281b69192d90f215b543b032841ff Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 9 Sep 2022 16:24:11 +0300 Subject: [PATCH 14/21] Avoid release days to keep the CI free Can be 1st, 2nd or 15th: https://github.com/python-pillow/Pillow/blob/main/RELEASING.md --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 4341752c3..e378ffc78 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -13,5 +13,5 @@ "separateMajorMinor": "false" } ], - "schedule": ["on the first day of the month"] + "schedule": ["on the third day of the month"] } From 8b2d70d17a4791d68df6c10d8337d769290c6528 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Sep 2022 09:10:03 +1000 Subject: [PATCH 15/21] Corrected BMP palette size when saving --- Tests/test_file_bmp.py | 12 ++++++++++++ src/PIL/BmpImagePlugin.py | 20 ++++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index d58666b44..4c964fbea 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -51,6 +51,18 @@ def test_save_to_bytes(): assert reloaded.format == "BMP" +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] + im.putpalette(colors) + + out = str(tmp_path / "temp.bmp") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + + def test_save_too_large(tmp_path): outfile = str(tmp_path / "temp.bmp") with Image.new("RGB", (1, 1)) as im: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 7bb73fc93..1041ab763 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -375,6 +375,16 @@ def _save(im, fp, filename, bitmap_header=True): header = 40 # or 64 for OS/2 version 2 image = stride * im.size[1] + if im.mode == "1": + palette = b"".join(o8(i) * 4 for i in (0, 255)) + elif im.mode == "L": + palette = b"".join(o8(i) * 4 for i in range(256)) + elif im.mode == "P": + palette = im.im.getpalette("RGB", "BGRX") + colors = len(palette) // 4 + else: + palette = None + # bitmap header if bitmap_header: offset = 14 + header + colors * 4 @@ -405,14 +415,8 @@ def _save(im, fp, filename, bitmap_header=True): fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) - if im.mode == "1": - for i in (0, 255): - fp.write(o8(i) * 4) - elif im.mode == "L": - for i in range(256): - fp.write(o8(i) * 4) - elif im.mode == "P": - fp.write(im.im.getpalette("RGB", "BGRX")) + if palette: + fp.write(palette) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) From 964e0aa0790a7d3d9dadb03b3045de6c7e124a6e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Sep 2022 23:32:58 +1000 Subject: [PATCH 16/21] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1d4103a7c..8c2993abc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Corrected BMP and TGA palette size when saving #6500 + [radarhere] + - Do not call load() before draft() in Image.thumbnail #6539 [radarhere] From 8b90588b9712f8105618504026646847c8814e75 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Sep 2022 16:03:23 +1000 Subject: [PATCH 17/21] Updated harfbuzz to 5.2.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 94e5dd871..f4515468f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -281,9 +281,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/5.1.0.zip", - "filename": "harfbuzz-5.1.0.zip", - "dir": "harfbuzz-5.1.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/5.2.0.zip", + "filename": "harfbuzz-5.2.0.zip", + "dir": "harfbuzz-5.2.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), From 0cafaca7e8c5845272a2994f025aca201b479556 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Sep 2022 18:52:16 +1000 Subject: [PATCH 18/21] Corrected dictionary name --- docs/reference/ExifTags.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 4567d4d3e..794fa238f 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -10,7 +10,7 @@ provide constants and clear-text names for various well-known EXIF tags. .. py:data:: TAGS :type: dict - The TAG dictionary maps 16-bit integer EXIF tag enumerations to + The TAGS dictionary maps 16-bit integer EXIF tag enumerations to descriptive string names. For instance: >>> from PIL.ExifTags import TAGS From 1bdf6ef7203e97c8c406d074624a000f49b28a6a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 19 Sep 2022 10:36:46 +0300 Subject: [PATCH 19/21] Add OpenSSF Best Practices badge --- README.md | 3 +++ docs/index.rst | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 5e9adaf7e..e7c0ebc5a 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,9 @@ As of 2019, Pillow development is Number of PyPI downloads + OpenSSF Best Practices diff --git a/docs/index.rst b/docs/index.rst index c731e2746..45af4c571 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,6 +69,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Mon, 19 Sep 2022 17:22:39 +0300 Subject: [PATCH 20/21] Fix Renovate config --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index e378ffc78..ec3ccc8a6 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -13,5 +13,5 @@ "separateMajorMinor": "false" } ], - "schedule": ["on the third day of the month"] + "schedule": ["the 3rd day of the month"] } From 291c23f25014355953b3ad63ad85235a996ac8b3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 20 Sep 2022 07:35:39 +0300 Subject: [PATCH 21/21] Fix Renovate config Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index ec3ccc8a6..d1d824335 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -13,5 +13,5 @@ "separateMajorMinor": "false" } ], - "schedule": ["the 3rd day of the month"] + "schedule": ["on the 3rd day of the month"] }