Skip to content

Commit

Permalink
Merge branch 'main' into ImageStat_getextrema_opt
Browse files Browse the repository at this point in the history
  • Loading branch information
florath committed Dec 5, 2023
2 parents ac47b75 + 7d892d3 commit ed03954
Show file tree
Hide file tree
Showing 26 changed files with 122 additions and 140 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/wheels-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ JPEGTURBO_VERSION=3.0.1
OPENJPEG_VERSION=2.5.0
XZ_VERSION=5.4.5
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.15
LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then
GIFLIB_VERSION=5.1.4
else
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.4
rev: v0.1.6
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.1
rev: 23.11.0
hooks:
- id: black

Expand Down Expand Up @@ -42,12 +42,12 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/

- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.8.1
rev: v0.9.0
hooks:
- id: sphinx-lint

- repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.4.1
rev: 1.5.3
hooks:
- id: pyproject-fmt

Expand Down
14 changes: 13 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ Changelog (Pillow)
10.2.0 (unreleased)
-------------------

- Raise ValueError when TrueType font size is not greater than zero #7584
- Added support for reading DX10 BC4 DDS images #7603
[sambvfx, radarhere]

- Optimized ImageStat.Stat.count #7599
[florath]

- Correct PDF palette size when saving #7555
[radarhere]

- Fixed closing file pointer with olefile 0.47 #7594
[radarhere]

- Raise ValueError when TrueType font size is not greater than zero #7584, #7587
[akx, radarhere]

- If absent, do not try to close fp when closing image #7557
Expand Down
9 changes: 0 additions & 9 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ Released as needed privately to individual vendors for critical security-related

## Source and Binary Distributions

### macOS and Linux
* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
```bash
Expand All @@ -104,14 +103,6 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
and copy into `dist`.

### Windows
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
```bash
gh run download --dir dist
# select dist-x.y.z
```

## Publicize Release

* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010
Expand Down
Binary file added Tests/images/bc4_typeless.dds
Binary file not shown.
Binary file added Tests/images/bc4_unorm.dds
Binary file not shown.
Binary file added Tests/images/bc4_unorm.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 1 addition & 3 deletions Tests/test_file_apng.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,7 @@ def test_apng_save(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255)

with Image.open("Tests/images/apng/single_frame_default.png") as im:
frames = []
for frame_im in ImageSequence.Iterator(im):
frames.append(frame_im.copy())
frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)]
frames[0].save(
test_file, save_all=True, default_image=True, append_images=frames[1:]
)
Expand Down
23 changes: 23 additions & 0 deletions Tests/test_file_dds.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
TEST_FILE_ATI1 = "Tests/images/ati1.dds"
TEST_FILE_ATI2 = "Tests/images/ati2.dds"
TEST_FILE_DX10_BC4_TYPELESS = "Tests/images/bc4_typeless.dds"
TEST_FILE_DX10_BC4_UNORM = "Tests/images/bc4_unorm.dds"
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"
Expand Down Expand Up @@ -82,6 +84,27 @@ def test_sanity_ati1():
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))


@pytest.mark.parametrize(
"image_path",
(
TEST_FILE_DX10_BC4_UNORM,
# hexeditted to be typeless
TEST_FILE_DX10_BC4_TYPELESS,
),
)
def test_dx10_bc4(image_path):
"""Check DX10 BC4 images can be opened"""

with Image.open(image_path) as im:
im.load()

assert im.format == "DDS"
assert im.mode == "L"
assert im.size == (64, 64)

assert_image_equal_tofile(im, TEST_FILE_DX10_BC4_UNORM.replace(".dds", ".png"))


@pytest.mark.parametrize(
"image_path",
(
Expand Down
13 changes: 13 additions & 0 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:

.. versionadded:: 2.5.0

**streamtype**
Allows storing images without quantization and Huffman tables, or with
these tables but without image data. This is useful for container formats
or network protocols that handle tables separately and share them between
images.

* ``0`` (default): interchange datastream, with tables and image data
* ``1``: abbreviated table specification (tables-only) datastream

.. versionadded:: 10.2.0

* ``2``: abbreviated image (image-only) datastream

**comment**
A comment about the image.

Expand Down
11 changes: 5 additions & 6 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,10 @@ and :pypi:`olefile` for Pillow to read FPX and MIC images::

.. tab:: Windows

.. warning:: Pillow > 9.5.0 no longer includes 32-bit wheels.

We provide Pillow binaries for Windows compiled for the matrix of
supported Pythons in 64-bit versions in the wheel format. These binaries include
support for all optional libraries except libimagequant and libxcb. Raqm support
We provide Pillow binaries for Windows compiled for the matrix of supported
Pythons in the wheel format. These include x86, x86-64 and arm64 versions
(with the exception of Python 3.8 on arm64). These binaries include support
for all optional libraries except libimagequant and libxcb. Raqm support
requires FriBiDi to be installed separately::

python3 -m pip install --upgrade pip
Expand Down Expand Up @@ -176,7 +175,7 @@ Many of Pillow's features require external libraries:
* **littlecms** provides color management

* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
above uses liblcms2. Tested with **1.19** and **2.7-2.15**.
above uses liblcms2. Tested with **1.19** and **2.7-2.16**.

* **libwebp** provides the WebP format.

Expand Down
8 changes: 7 additions & 1 deletion src/PIL/DdsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@
DXGI_FORMAT_R8G8B8A8_TYPELESS = 27
DXGI_FORMAT_R8G8B8A8_UNORM = 28
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
DXGI_FORMAT_BC4_TYPELESS = 79
DXGI_FORMAT_BC4_UNORM = 80
DXGI_FORMAT_BC5_TYPELESS = 82
DXGI_FORMAT_BC5_UNORM = 83
DXGI_FORMAT_BC5_SNORM = 84
Expand Down Expand Up @@ -190,7 +192,11 @@ def _open(self):
# ignoring flags which pertain to volume textures and cubemaps
(dxgi_format,) = struct.unpack("<I", self.fp.read(4))
self.fp.read(16)
if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM):
if dxgi_format in (DXGI_FORMAT_BC4_TYPELESS, DXGI_FORMAT_BC4_UNORM):
self.pixel_format = "BC4"
n = 4
self._mode = "L"
elif dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM):
self.pixel_format = "BC5"
n = 5
self._mode = "RGB"
Expand Down
9 changes: 4 additions & 5 deletions src/PIL/FpxImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,15 @@ def _open_index(self, index=1):

s = prop[0x2000002 | id]

colors = []
bands = i32(s, 4)
if bands > 4:
msg = "Invalid number of bands"
raise OSError(msg)
for i in range(bands):
# note: for now, we ignore the "uncalibrated" flag
colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)

self._mode, self.rawmode = MODES[tuple(colors)]
# note: for now, we ignore the "uncalibrated" flag
colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands))

self._mode, self.rawmode = MODES[colors]

# load JPEG tables, if any
self.jpeg = {}
Expand Down
16 changes: 5 additions & 11 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1288,9 +1288,9 @@ def filter(self, filter):
if self.im.bands == 1 or multiband:
return self._new(filter.filter(self.im))

ims = []
for c in range(self.im.bands):
ims.append(self._new(filter.filter(self.im.getband(c))))
ims = [
self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands)
]
return merge(self.mode, ims)

def getbands(self):
Expand Down Expand Up @@ -1339,10 +1339,7 @@ def getcolors(self, maxcolors=256):
self.load()
if self.mode in ("1", "L", "P"):
h = self.im.histogram()
out = []
for i in range(256):
if h[i]:
out.append((h[i], i))
out = [(h[i], i) for i in range(256) if h[i]]
if len(out) > maxcolors:
return None
return out
Expand Down Expand Up @@ -1383,10 +1380,7 @@ def getextrema(self):

self.load()
if self.im.bands > 1:
extrema = []
for i in range(self.im.bands):
extrema.append(self.im.getband(i).getextrema())
return tuple(extrema)
return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
return self.im.getextrema()

def _getxmp(self, xmp_tags):
Expand Down
7 changes: 2 additions & 5 deletions src/PIL/ImageCms.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,11 +787,8 @@ def getProfileInfo(profile):
# info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
description = profile.profile.profile_description
cpright = profile.profile.copyright
arr = []
for elt in (description, cpright):
if elt:
arr.append(elt)
return "\r\n\r\n".join(arr) + "\r\n\r\n"
elements = [element for element in (description, cpright) if element]
return "\r\n\r\n".join(elements) + "\r\n\r\n"

except (AttributeError, OSError, TypeError, ValueError) as v:
raise PyCMSError(v) from v
Expand Down
8 changes: 4 additions & 4 deletions src/PIL/ImageFont.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ class FreeTypeFont:
def __init__(self, font=None, size=10, index=0, encoding="", layout_engine=None):
# FIXME: use service provider instead

if size <= 0:
msg = "font size must be greater than 0"
raise ValueError(msg)

self.path = font
self.size = size
self.index = index
Expand Down Expand Up @@ -791,10 +795,6 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
:exception ValueError: If the font size is not greater than zero.
"""

if size <= 0:
msg = "font size must be greater than 0"
raise ValueError(msg)

def freetype(font):
return FreeTypeFont(font, size, index, encoding, layout_engine)

Expand Down
8 changes: 2 additions & 6 deletions src/PIL/ImageOps.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,7 @@ def invert(image):
:param image: The image to invert.
:return: An image.
"""
lut = []
for i in range(256):
lut.append(255 - i)
lut = list(range(255, -1, -1))
return image.point(lut) if image.mode == "1" else _lut(image, lut)


Expand All @@ -581,10 +579,8 @@ def posterize(image, bits):
:param bits: The number of bits to keep for each channel (1-8).
:return: An image.
"""
lut = []
mask = ~(2 ** (8 - bits) - 1)
for i in range(256):
lut.append(i & mask)
lut = [i & mask for i in range(256)]
return _lut(image, lut)


Expand Down
20 changes: 6 additions & 14 deletions src/PIL/ImagePalette.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,21 +200,15 @@ def raw(rawmode, data):


def make_linear_lut(black, white):
lut = []
if black == 0:
for i in range(256):
lut.append(white * i // 255)
else:
msg = "unavailable when black is non-zero"
raise NotImplementedError(msg) # FIXME
return lut
return [white * i // 255 for i in range(256)]

msg = "unavailable when black is non-zero"
raise NotImplementedError(msg) # FIXME


def make_gamma_lut(exp):
lut = []
for i in range(256):
lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
return lut
return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]


def negative(mode="RGB"):
Expand All @@ -226,9 +220,7 @@ def negative(mode="RGB"):
def random(mode="RGB"):
from random import randint

palette = []
for i in range(256 * len(mode)):
palette.append(randint(0, 255))
palette = [randint(0, 255) for _ in range(256 * len(mode))]
return ImagePalette(mode, palette)


Expand Down
18 changes: 6 additions & 12 deletions src/PIL/ImageQt.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,10 @@ def align8to32(bytes, width, mode):
if not extra_padding:
return bytes

new_data = []
for i in range(len(bytes) // bytes_per_line):
new_data.append(
bytes[i * bytes_per_line : (i + 1) * bytes_per_line]
+ b"\x00" * extra_padding
)
new_data = [
bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding
for i in range(len(bytes) // bytes_per_line)
]

return b"".join(new_data)

Expand All @@ -131,15 +129,11 @@ def _toqclass_helper(im):
format = qt_format.Format_Mono
elif im.mode == "L":
format = qt_format.Format_Indexed8
colortable = []
for i in range(256):
colortable.append(rgb(i, i, i))
colortable = [rgb(i, i, i) for i in range(256)]
elif im.mode == "P":
format = qt_format.Format_Indexed8
colortable = []
palette = im.getpalette()
for i in range(0, len(palette), 3):
colortable.append(rgb(*palette[i : i + 3]))
colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)]
elif im.mode == "RGB":
# Populate the 4th channel with 255
im = im.convert("RGBA")
Expand Down

0 comments on commit ed03954

Please sign in to comment.