diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 000000000..d1d824335
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:base"
+ ],
+ "labels": [
+ "Dependency"
+ ],
+ "packageRules": [
+ {
+ "groupName": "github-actions",
+ "matchManagers": ["github-actions"],
+ "separateMajorMinor": "false"
+ }
+ ],
+ "schedule": ["on the 3rd day of the month"]
+}
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
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]
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
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
+
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index 604d54d88..f6860a9a4 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -58,6 +58,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/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/Tests/test_file_tga.py b/Tests/test_file_tga.py
index cbbb7df1d..7d8b5139a 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -120,6 +120,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/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/Tests/test_imageops.py b/Tests/test_imageops.py
index 01e40e6d4..550578f8f 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()
@@ -130,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/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 >> from PIL.ExifTags import TAGS
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))])
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 2e11df54c..20435fe31 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -519,9 +519,8 @@ 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 = im.palette.colors.get(source_color)
+ if index in used_palette_colors:
index = None
used_palette_colors.append(index)
for i, index in enumerate(used_palette_colors):
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index d2819e076..6611ceb3c 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1949,11 +1949,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:
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 0c3f900ca..ae43fc3bd 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)
@@ -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
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 59b89e988..cd454b755 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -193,9 +193,10 @@ 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")
+ 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
@@ -210,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)
@@ -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(
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index e289027fe..414857fc2 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -355,9 +355,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",
"license": "COPYING",
"build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),