Merge branch 'main' into winbuild-update

This commit is contained in:
Andrew Murray 2022-09-21 20:27:24 +10:00 committed by GitHub
commit 8053772a2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 114 additions and 28 deletions

17
.github/renovate.json vendored Normal file
View File

@ -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"]
}

View File

@ -40,6 +40,7 @@ repos:
rev: v4.3.0 rev: v4.3.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json
- id: check-yaml - id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint

View File

@ -5,6 +5,9 @@ Changelog (Pillow)
9.3.0 (unreleased) 9.3.0 (unreleased)
------------------ ------------------
- Corrected BMP and TGA palette size when saving #6500
[radarhere]
- Do not call load() before draft() in Image.thumbnail #6539 - Do not call load() before draft() in Image.thumbnail #6539
[radarhere] [radarhere]

View File

@ -25,6 +25,7 @@ exclude .coveragerc
exclude .editorconfig exclude .editorconfig
exclude .readthedocs.yml exclude .readthedocs.yml
exclude codecov.yml exclude codecov.yml
exclude renovate.json
global-exclude .git* global-exclude .git*
global-exclude *.pyc global-exclude *.pyc
global-exclude *.so global-exclude *.so

View File

@ -74,6 +74,9 @@ As of 2019, Pillow development is
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/Pillow/"><img
alt="Number of PyPI downloads" alt="Number of PyPI downloads"
src="https://img.shields.io/pypi/dm/pillow.svg"></a> src="https://img.shields.io/pypi/dm/pillow.svg"></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/6331"><img
alt="OpenSSF Best Practices"
src="https://bestpractices.coreinfrastructure.org/projects/6331/badge"></a>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -58,6 +58,18 @@ def test_save_to_bytes():
assert reloaded.format == "BMP" 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): def test_save_too_large(tmp_path):
outfile = str(tmp_path / "temp.bmp") outfile = str(tmp_path / "temp.bmp")
with Image.new("RGB", (1, 1)) as im: with Image.new("RGB", (1, 1)) as im:

View File

@ -1087,6 +1087,19 @@ def test_palette_save_P(tmp_path):
assert_image_equal(reloaded, im) 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): def test_palette_save_all_P(tmp_path):
frames = [] frames = []
colors = ((255, 0, 0), (0, 255, 0)) colors = ((255, 0, 0), (0, 255, 0))

View File

@ -120,6 +120,18 @@ def test_save(tmp_path):
assert test_im.size == (100, 100) 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): def test_save_wrong_mode(tmp_path):
im = hopper("PA") im = hopper("PA")
out = str(tmp_path / "temp.tga") out = str(tmp_path / "temp.tga")

View File

@ -620,6 +620,7 @@ class TestImage:
im_remapped = im.remap_palette([1, 0]) im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1 assert im_remapped.info["transparency"] == 1
assert len(im_remapped.getpalette()) == 6
# Test unused transparency # Test unused transparency
im.info["transparency"] = 2 im.info["transparency"] = 2

View File

@ -110,6 +110,16 @@ def test_contain(new_size):
assert new_im.size == (256, 256) 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(): def test_pad():
# Same ratio # Same ratio
im = hopper() 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(): def test_pil163():
# Division by zero in equalize if < 255 pixels in image (@PIL163) # Division by zero in equalize if < 255 pixels in image (@PIL163)

View File

@ -69,6 +69,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://pypi.org/project/Pillow/ :target: https://pypi.org/project/Pillow/
:alt: Number of PyPI downloads :alt: Number of PyPI downloads
.. image:: https://bestpractices.coreinfrastructure.org/projects/6331/badge
:target: https://bestpractices.coreinfrastructure.org/projects/6331
:alt: OpenSSF Best Practices
Overview Overview
======== ========

View File

@ -10,7 +10,7 @@ provide constants and clear-text names for various well-known EXIF tags.
.. py:data:: TAGS .. py:data:: TAGS
:type: dict :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: descriptive string names. For instance:
>>> from PIL.ExifTags import TAGS >>> from PIL.ExifTags import TAGS

View File

@ -375,6 +375,16 @@ def _save(im, fp, filename, bitmap_header=True):
header = 40 # or 64 for OS/2 version 2 header = 40 # or 64 for OS/2 version 2
image = stride * im.size[1] 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 # bitmap header
if bitmap_header: if bitmap_header:
offset = 14 + header + colors * 4 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) fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
if im.mode == "1": if palette:
for i in (0, 255): fp.write(palette)
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"))
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))])

View File

@ -519,9 +519,8 @@ def _normalize_palette(im, palette, info):
used_palette_colors = [] used_palette_colors = []
for i in range(0, len(source_palette), 3): for i in range(0, len(source_palette), 3):
source_color = tuple(source_palette[i : i + 3]) source_color = tuple(source_palette[i : i + 3])
try: index = im.palette.colors.get(source_color)
index = im.palette.colors[source_color] if index in used_palette_colors:
except KeyError:
index = None index = None
used_palette_colors.append(index) used_palette_colors.append(index)
for i, index in enumerate(used_palette_colors): for i, index in enumerate(used_palette_colors):

View File

@ -1949,11 +1949,7 @@ class Image:
m_im = m_im.convert("L") m_im = m_im.convert("L")
# Internally, we require 256 palette entries. m_im.putpalette(palette_bytes, palette_mode)
new_palette_bytes = (
palette_bytes + ((256 * bands) - len(palette_bytes)) * b"\x00"
)
m_im.putpalette(new_palette_bytes, palette_mode)
m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes)
if "transparency" in self.info: if "transparency" in self.info:

View File

@ -255,11 +255,11 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
if im_ratio != dest_ratio: if im_ratio != dest_ratio:
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]: if new_height != size[1]:
size = (size[0], new_height) size = (size[0], new_height)
else: else:
new_width = int(image.width / image.height * size[1]) new_width = round(image.width / image.height * size[1])
if new_width != size[0]: if new_width != size[0]:
size = (new_width, size[1]) size = (new_width, size[1])
return image.resize(size, resample=method) return image.resize(size, resample=method)
@ -292,10 +292,10 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5
else: else:
out = Image.new(image.mode, size, color) out = Image.new(image.mode, size, color)
if resized.width != size[0]: 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)) out.paste(resized, (x, 0))
else: 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)) out.paste(resized, (0, y))
return out return out

View File

@ -193,9 +193,10 @@ def _save(im, fp, filename):
warnings.warn("id_section has been trimmed to 255 characters") warnings.warn("id_section has been trimmed to 255 characters")
if colormaptype: if colormaptype:
colormapfirst, colormaplength, colormapentry = 0, 256, 24 palette = im.im.getpalette("RGB", "BGR")
colormaplength, colormapentry = len(palette) // 3, 24
else: else:
colormapfirst, colormaplength, colormapentry = 0, 0, 0 colormaplength, colormapentry = 0, 0
if im.mode in ("LA", "RGBA"): if im.mode in ("LA", "RGBA"):
flags = 8 flags = 8
@ -210,7 +211,7 @@ def _save(im, fp, filename):
o8(id_len) o8(id_len)
+ o8(colormaptype) + o8(colormaptype)
+ o8(imagetype) + o8(imagetype)
+ o16(colormapfirst) + o16(0) # colormapfirst
+ o16(colormaplength) + o16(colormaplength)
+ o8(colormapentry) + o8(colormapentry)
+ o16(0) + o16(0)
@ -225,7 +226,7 @@ def _save(im, fp, filename):
fp.write(id_section) fp.write(id_section)
if colormaptype: if colormaptype:
fp.write(im.im.getpalette("RGB", "BGR")) fp.write(palette)
if rle: if rle:
ImageFile._save( ImageFile._save(

View File

@ -355,9 +355,9 @@ deps = {
"libs": [r"imagequant.lib"], "libs": [r"imagequant.lib"],
}, },
"harfbuzz": { "harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/5.1.0.zip", "url": "https://github.com/harfbuzz/harfbuzz/archive/5.2.0.zip",
"filename": "harfbuzz-5.1.0.zip", "filename": "harfbuzz-5.2.0.zip",
"dir": "harfbuzz-5.1.0", "dir": "harfbuzz-5.2.0",
"license": "COPYING", "license": "COPYING",
"build": [ "build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),