Merge branch 'main' into tiff

This commit is contained in:
Andrew Murray 2024-01-01 13:41:17 +11:00 committed by GitHub
commit 99760f4c59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 242 additions and 132 deletions

18
.github/problem-matchers/gcc.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"__comment": "Based on vscode-cpptools' Extension/package.json gcc rule",
"problemMatcher": [
{
"owner": "gcc-problem-matcher",
"pattern": [
{
"regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

View File

@ -13,6 +13,8 @@ categories:
label: "Removal" label: "Removal"
- title: "Testing" - title: "Testing"
label: "Testing" label: "Testing"
- title: "Type hints"
label: "Type hints"
exclude-labels: exclude-labels:
- "changelog: skip" - "changelog: skip"

View File

@ -86,6 +86,10 @@ jobs:
env: env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build - name: Build
run: | run: |
.ci/build.sh .ci/build.sh

View File

@ -5,6 +5,18 @@ Changelog (Pillow)
10.2.0 (unreleased) 10.2.0 (unreleased)
------------------- -------------------
- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662
[radarhere]
- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657
[hugovk]
- Restricted environment keys for ImageMath.eval() #7655
[wiredfool, radarhere]
- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641
[hugovk, radarhere]
- Fix incorrect color blending for overlapping glyphs #7497 - Fix incorrect color blending for overlapping glyphs #7497
[ZachNagengast, nulano, radarhere] [ZachNagengast, nulano, radarhere]

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is Pillow is the friendly PIL fork. It is
Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors. Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
Like PIL, Pillow is licensed under the open source HPND License: Like PIL, Pillow is licensed under the open source HPND License:

View File

@ -20,12 +20,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
git tag 5.2.0 git tag 5.2.0
git push --tags git push --tags
``` ```
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check and upload all source and binary distributions e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.0*
```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
@ -55,12 +50,7 @@ Released as needed for security, installation or critical bug fixes.
```bash ```bash
make sdist make sdist
``` ```
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check and upload all source and binary distributions e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.1*
```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash ```bash
git push git push
@ -82,11 +72,7 @@ Released as needed privately to individual vendors for critical security-related
git tag 2.5.3 git tag 2.5.3
git push origin --tags git push origin --tags
``` ```
* [ ] Create and check source distribution: * [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
```bash
make sdist
```
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash ```bash
git push origin 2.5.x git push origin 2.5.x
@ -94,14 +80,15 @@ Released as needed privately to individual vendors for critical security-related
## Source and Binary Distributions ## Source and Binary Distributions
* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) * [ ] Check 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): has passed, including the "Upload release to PyPI" job. This will have been triggered
```bash by the new tag.
gh run download --dir dist
# select dist
```
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) * [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
and copy into `dist`. and copy into `dist`. Check and upload them e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.0*
```
## Publicize Release ## Publicize Release

BIN
Tests/images/bgr15.dds Normal file

Binary file not shown.

BIN
Tests/images/bgr15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

View File

@ -1,7 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
from __future__ import annotations
# Copyright 2020 Google LLC # Copyright 2020 Google LLC
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -1,7 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
from __future__ import annotations
# Copyright 2020 Google LLC # Copyright 2020 Google LLC
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -32,6 +32,7 @@ TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SR
TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds" TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
@ -249,6 +250,7 @@ def test_dx10_r8g8b8a8_unorm_srgb():
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB), ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15),
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
], ],
) )
@ -341,16 +343,9 @@ def test_palette():
assert_image_equal_tofile(im, "Tests/images/transparent.gif") assert_image_equal_tofile(im, "Tests/images/transparent.gif")
@pytest.mark.parametrize( def test_unsupported_bitcount():
"test_file",
(
"Tests/images/unsupported_bitcount_rgb.dds",
"Tests/images/unsupported_bitcount_luminance.dds",
),
)
def test_unsupported_bitcount(test_file):
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open(test_file): with Image.open("Tests/images/unsupported_bitcount.dds"):
pass pass

View File

@ -11,6 +11,15 @@ from .helper import hopper
TEST_FILE = "Tests/images/iptc.jpg" TEST_FILE = "Tests/images/iptc.jpg"
def test_open():
f = BytesIO(
b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c"
b"\x03\x14\x00\x01\x01\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x00"
)
with Image.open(f) as im:
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
def test_getiptcinfo_jpg_none(): def test_getiptcinfo_jpg_none():
# Arrange # Arrange
with hopper() as im: with hopper() as im:

View File

@ -840,6 +840,10 @@ class TestFileJpeg:
# Act / Assert # Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09" assert im._getexif()[306] == "2017:03:13 23:03:09"
def test_multiple_exif(self):
with Image.open("Tests/images/multiple_exif.jpg") as im:
assert im.info["exif"] == b"Exif\x00\x00firstsecond"
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )

View File

@ -612,6 +612,14 @@ class TestFileTiff:
assert_image_equal_tofile(im, tmpfile) assert_image_equal_tofile(im, tmpfile)
def test_rowsperstrip(self, tmp_path):
outfile = str(tmp_path / "temp.tif")
im = hopper()
im.save(outfile, tiffinfo={278: 256})
with Image.open(outfile) as im:
assert im.tag_v2[278] == 256
def test_strip_raw(self): def test_strip_raw(self):
infile = "Tests/images/tiff_strip_raw.tif" infile = "Tests/images/tiff_strip_raw.tif"
with Image.open(infile) as im: with Image.open(infile) as im:

View File

@ -123,6 +123,7 @@ def test_write_metadata(tmp_path):
"""Test metadata writing through the python code""" """Test metadata writing through the python code"""
with Image.open("Tests/images/hopper.tif") as img: with Image.open("Tests/images/hopper.tif") as img:
f = str(tmp_path / "temp.tiff") f = str(tmp_path / "temp.tiff")
del img.tag[278]
img.save(f, tiffinfo=img.tag) img.save(f, tiffinfo=img.tag)
original = img.tag_v2.named() original = img.tag_v2.named()
@ -159,6 +160,7 @@ def test_change_stripbytecounts_tag_type(tmp_path):
out = str(tmp_path / "temp.tiff") out = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
info = im.tag_v2 info = im.tag_v2
del info[278]
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
im = im.resize((500, 500)) im = im.resize((500, 500))

View File

@ -1016,6 +1016,11 @@ class TestImage:
except OSError as e: except OSError as e:
assert str(e) == "buffer overrun when reading image file" assert str(e) == "buffer overrun when reading image file"
def test_exit_fp(self):
with Image.new("L", (1, 1)) as im:
pass
assert not hasattr(im, "fp")
def test_close_graceful(self, caplog): def test_close_graceful(self, caplog):
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
copy = im.copy() copy = im.copy()

View File

@ -1053,11 +1053,13 @@ def test_too_many_characters(font):
with pytest.raises(ValueError): with pytest.raises(ValueError):
transposed_font.getlength("A" * 1_000_001) transposed_font.getlength("A" * 1_000_001)
default_font = ImageFont.load_default() imagefont = ImageFont.ImageFont()
with pytest.raises(ValueError): with pytest.raises(ValueError):
default_font.getlength("A" * 1_000_001) imagefont.getlength("A" * 1_000_001)
with pytest.raises(ValueError): with pytest.raises(ValueError):
default_font.getbbox("A" * 1_000_001) imagefont.getbbox("A" * 1_000_001)
with pytest.raises(ValueError):
imagefont.getmask("A" * 1_000_001)
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -64,6 +64,16 @@ def test_prevent_exec(expression):
ImageMath.eval(expression) ImageMath.eval(expression)
def test_prevent_double_underscores():
with pytest.raises(ValueError):
ImageMath.eval("1", {"__": None})
def test_prevent_builtins():
with pytest.raises(ValueError):
ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None})
def test_logical(): def test_logical():
assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("not A", images)) == 0
assert pixel(ImageMath.eval("A and B", images)) == "L 2" assert pixel(ImageMath.eval("A and B", images)) == "L 2"

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is Pillow is the friendly PIL fork. It is
Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors
Like PIL, Pillow is licensed under the open source PIL Like PIL, Pillow is licensed under the open source PIL
Software License: Software License:

View File

@ -54,7 +54,7 @@ master_doc = "index"
# General information about the project. # General information about the project.
project = "Pillow (PIL Fork)" project = "Pillow (PIL Fork)"
copyright = ( copyright = (
"1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" "1995-2011 Fredrik Lundh, 2010-2024 Jeffrey A. Clark (Alex) and contributors"
) )
author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors" author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors"

View File

@ -62,10 +62,24 @@ output only the quantization and Huffman tables for the image.
Security Security
======== ========
TODO ImageFont.getmask: Applied ImageFont.MAX_STRING_LENGTH
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO To protect against potential DOS attacks when using arbitrary strings as text input,
Pillow will now raise a :py:exc:`ValueError` if the number of characters passed into
:py:meth:`PIL.ImageFont.ImageFont.getmask` is over a certain limit,
:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.
This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It
can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
ImageMath.eval: Restricted environment keys
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:cve:`2023-50447`: If an attacker has control over the keys passed to the
``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute
arbitrary code. To prevent this, keys matching the names of builtins and keys
containing double underscores will now raise a :py:exc:`ValueError`.
Other Changes Other Changes
============= =============

View File

@ -117,6 +117,8 @@ extend-ignore = [
[tool.ruff.per-file-ignores] [tool.ruff.per-file-ignores]
"Tests/*.py" = ["I001"] "Tests/*.py" = ["I001"]
"Tests/oss-fuzz/fuzz_font.py" = ["I002"]
"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"]
[tool.ruff.isort] [tool.ruff.isort]
known-first-party = ["PIL"] known-first-party = ["PIL"]

View File

@ -16,15 +16,16 @@
from __future__ import annotations from __future__ import annotations
import io import io
from typing import IO, AnyStr, Generic, Literal
class ContainerIO: class ContainerIO(Generic[AnyStr]):
""" """
A file object that provides read access to a part of an existing A file object that provides read access to a part of an existing
file (for example a TAR file). file (for example a TAR file).
""" """
def __init__(self, file, offset, length) -> None: def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None:
""" """
Create file object. Create file object.
@ -32,7 +33,7 @@ class ContainerIO:
:param offset: Start of region, in bytes. :param offset: Start of region, in bytes.
:param length: Size of region, in bytes. :param length: Size of region, in bytes.
""" """
self.fh = file self.fh: IO[AnyStr] = file
self.pos = 0 self.pos = 0
self.offset = offset self.offset = offset
self.length = length self.length = length
@ -41,10 +42,10 @@ class ContainerIO:
## ##
# Always false. # Always false.
def isatty(self): def isatty(self) -> bool:
return False return False
def seek(self, offset, mode=io.SEEK_SET): def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None:
""" """
Move file pointer. Move file pointer.
@ -63,7 +64,7 @@ class ContainerIO:
self.pos = max(0, min(self.pos, self.length)) self.pos = max(0, min(self.pos, self.length))
self.fh.seek(self.offset + self.pos) self.fh.seek(self.offset + self.pos)
def tell(self): def tell(self) -> int:
""" """
Get current file pointer. Get current file pointer.
@ -71,7 +72,7 @@ class ContainerIO:
""" """
return self.pos return self.pos
def read(self, n=0): def read(self, n: int = 0) -> AnyStr:
""" """
Read data. Read data.
@ -84,17 +85,17 @@ class ContainerIO:
else: else:
n = self.length - self.pos n = self.length - self.pos
if not n: # EOF if not n: # EOF
return b"" if "b" in self.fh.mode else "" return b"" if "b" in self.fh.mode else "" # type: ignore[return-value]
self.pos = self.pos + n self.pos = self.pos + n
return self.fh.read(n) return self.fh.read(n)
def readline(self): def readline(self) -> AnyStr:
""" """
Read a line of text. Read a line of text.
:returns: An 8-bit string. :returns: An 8-bit string.
""" """
s = b"" if "b" in self.fh.mode else "" s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment]
newline_character = b"\n" if "b" in self.fh.mode else "\n" newline_character = b"\n" if "b" in self.fh.mode else "\n"
while True: while True:
c = self.read(1) c = self.read(1)
@ -105,7 +106,7 @@ class ContainerIO:
break break
return s return s
def readlines(self): def readlines(self) -> list[AnyStr]:
""" """
Read multiple lines of text. Read multiple lines of text.

View File

@ -18,6 +18,7 @@ from enum import IntEnum, IntFlag
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i32le as i32 from ._binary import i32le as i32
from ._binary import o8
from ._binary import o32le as o32 from ._binary import o32le as o32
# Magic ("DDS ") # Magic ("DDS ")
@ -341,6 +342,7 @@ class DdsImageFile(ImageFile.ImageFile):
flags, height, width = struct.unpack("<3I", header.read(12)) flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height) self._size = (width, height)
extents = (0, 0) + self.size
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved struct.unpack("<11I", header.read(44)) # reserved
@ -351,22 +353,16 @@ class DdsImageFile(ImageFile.ImageFile):
rawmode = None rawmode = None
if pfflags & DDPF.RGB: if pfflags & DDPF.RGB:
# Texture contains uncompressed RGB data # Texture contains uncompressed RGB data
masks = struct.unpack("<4I", header.read(16)) if pfflags & DDPF.ALPHAPIXELS:
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
if bitcount == 24:
self._mode = "RGB"
rawmode = masks[0x000000FF] + masks[0x0000FF00] + masks[0x00FF0000]
elif bitcount == 32 and pfflags & DDPF.ALPHAPIXELS:
self._mode = "RGBA" self._mode = "RGBA"
rawmode = ( mask_count = 4
masks[0x000000FF]
+ masks[0x0000FF00]
+ masks[0x00FF0000]
+ masks[0xFF000000]
)
else: else:
msg = f"Unsupported bitcount {bitcount} for {pfflags}" self._mode = "RGB"
raise OSError(msg) mask_count = 3
masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
return
elif pfflags & DDPF.LUMINANCE: elif pfflags & DDPF.LUMINANCE:
if bitcount == 8: if bitcount == 8:
self._mode = "L" self._mode = "L"
@ -464,7 +460,6 @@ class DdsImageFile(ImageFile.ImageFile):
msg = f"Unknown pixel format flags {pfflags}" msg = f"Unknown pixel format flags {pfflags}"
raise NotImplementedError(msg) raise NotImplementedError(msg)
extents = (0, 0) + self.size
if n: if n:
self.tile = [ self.tile = [
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format)) ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
@ -476,6 +471,39 @@ class DdsImageFile(ImageFile.ImageFile):
pass pass
class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
bitcount, masks = self.args
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
# Calculate how many zeros each mask is padded with
mask_offsets = []
# And the maximum value of each channel without the padding
mask_totals = []
for mask in masks:
offset = 0
if mask != 0:
while mask >> (offset + 1) << (offset + 1) == mask:
offset += 1
mask_offsets.append(offset)
mask_totals.append(mask >> offset)
data = bytearray()
bytecount = bitcount // 8
while len(data) < self.state.xsize * self.state.ysize * len(masks):
value = int.from_bytes(self.fd.read(bytecount), "little")
for i, mask in enumerate(masks):
masked_value = value & mask
# Remove the zero padding, and scale it to 8 bits
data += o8(
int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
)
self.set_as_raw(bytes(data))
return -1, 0
def _save(im, fp, filename): def _save(im, fp, filename):
if im.mode not in ("RGB", "RGBA", "L", "LA"): if im.mode not in ("RGB", "RGBA", "L", "LA"):
msg = f"cannot write mode {im.mode} as DDS" msg = f"cannot write mode {im.mode} as DDS"
@ -533,5 +561,6 @@ def _accept(prefix):
Image.register_open(DdsImageFile.format, DdsImageFile, _accept) Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
Image.register_decoder("dds_rgb", DdsRgbDecoder)
Image.register_save(DdsImageFile.format, _save) Image.register_save(DdsImageFile.format, _save)
Image.register_extension(DdsImageFile.format, ".dds") Image.register_extension(DdsImageFile.format, ".dds")

View File

@ -530,15 +530,19 @@ class Image:
def __enter__(self): def __enter__(self):
return self return self
def _close_fp(self):
if getattr(self, "_fp", False):
if self._fp != self.fp:
self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp:
self.fp.close()
def __exit__(self, *args): def __exit__(self, *args):
if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False): if hasattr(self, "fp"):
if getattr(self, "_fp", False): if getattr(self, "_exclusive_fp", False):
if self._fp != self.fp: self._close_fp()
self._fp.close() self.fp = None
self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp:
self.fp.close()
self.fp = None
def close(self): def close(self):
""" """
@ -554,12 +558,7 @@ class Image:
""" """
if hasattr(self, "fp"): if hasattr(self, "fp"):
try: try:
if getattr(self, "_fp", False): self._close_fp()
if self._fp != self.fp:
self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp:
self.fp.close()
self.fp = None self.fp = None
except Exception as msg: except Exception as msg:
logger.debug("Error closing: %s", msg) logger.debug("Error closing: %s", msg)

View File

@ -19,10 +19,12 @@
from __future__ import annotations from __future__ import annotations
import re import re
from functools import lru_cache
from . import Image from . import Image
@lru_cache
def getrgb(color): def getrgb(color):
""" """
Convert a color string to an RGB or RGBA tuple. If the string cannot be Convert a color string to an RGB or RGBA tuple. If the string cannot be
@ -121,6 +123,7 @@ def getrgb(color):
raise ValueError(msg) raise ValueError(msg)
@lru_cache
def getcolor(color, mode): def getcolor(color, mode):
""" """
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if

View File

@ -149,6 +149,7 @@ class ImageFont:
:return: An internal PIL storage memory instance as defined by the :return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module. :py:mod:`PIL.Image.core` interface module.
""" """
_string_length_check(text)
return self.font.getmask(text, mode) return self.font.getmask(text, mode)
def getbbox(self, text, *args, **kwargs): def getbbox(self, text, *args, **kwargs):

View File

@ -234,6 +234,11 @@ def eval(expression, _dict={}, **kw):
# build execution namespace # build execution namespace
args = ops.copy() args = ops.copy()
for k in list(_dict.keys()) + list(kw.keys()):
if "__" in k or hasattr(builtins, k):
msg = f"'{k}' not allowed"
raise ValueError(msg)
args.update(_dict) args.update(_dict)
args.update(kw) args.update(kw)
for k, v in args.items(): for k, v in args.items():

View File

@ -20,26 +20,29 @@ import os
import tempfile import tempfile
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i8, o8
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
COMPRESSION = {1: "raw", 5: "jpeg"} COMPRESSION = {1: "raw", 5: "jpeg"}
PAD = o8(0) * 4 PAD = b"\0\0\0\0"
# #
# Helpers # Helpers
def _i8(c: int | bytes) -> int:
return c if isinstance(c, int) else c[0]
def i(c): def i(c):
return i32((PAD + c)[-4:]) return i32((PAD + c)[-4:])
def dump(c): def dump(c):
for i in c: for i in c:
print("%02x" % i8(i), end=" ") print("%02x" % _i8(i), end=" ")
print() print()
@ -103,10 +106,10 @@ class IptcImageFile(ImageFile.ImageFile):
self.info[tag] = tagdata self.info[tag] = tagdata
# mode # mode
layers = i8(self.info[(3, 60)][0]) layers = self.info[(3, 60)][0]
component = i8(self.info[(3, 60)][1]) component = self.info[(3, 60)][1]
if (3, 65) in self.info: if (3, 65) in self.info:
id = i8(self.info[(3, 65)][0]) - 1 id = self.info[(3, 65)][0] - 1
else: else:
id = 0 id = 0
if layers == 1 and not component: if layers == 1 and not component:
@ -128,24 +131,20 @@ class IptcImageFile(ImageFile.ImageFile):
# tile # tile
if tag == (8, 10): if tag == (8, 10):
self.tile = [ self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
("iptc", (compression, offset), (0, 0, self.size[0], self.size[1]))
]
def load(self): def load(self):
if len(self.tile) != 1 or self.tile[0][0] != "iptc": if len(self.tile) != 1 or self.tile[0][0] != "iptc":
return ImageFile.ImageFile.load(self) return ImageFile.ImageFile.load(self)
type, tile, box = self.tile[0] offset, compression = self.tile[0][2:]
encoding, offset = tile
self.fp.seek(offset) self.fp.seek(offset)
# Copy image data to temporary file # Copy image data to temporary file
o_fd, outfile = tempfile.mkstemp(text=False) o_fd, outfile = tempfile.mkstemp(text=False)
o = os.fdopen(o_fd) o = os.fdopen(o_fd)
if encoding == "raw": if compression == "raw":
# To simplify access to the extracted file, # To simplify access to the extracted file,
# prepend a PPM header # prepend a PPM header
o.write("P5\n%d %d\n255\n" % self.size) o.write("P5\n%d %d\n255\n" % self.size)

View File

@ -87,10 +87,12 @@ def APP(self, marker):
self.info["dpi"] = jfif_density self.info["dpi"] = jfif_density
self.info["jfif_unit"] = jfif_unit self.info["jfif_unit"] = jfif_unit
self.info["jfif_density"] = jfif_density self.info["jfif_density"] = jfif_density
elif marker == 0xFFE1 and s[:5] == b"Exif\0": elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
if "exif" not in self.info: # extract EXIF information
# extract EXIF information (incomplete) if "exif" in self.info:
self.info["exif"] = s # FIXME: value will change self.info["exif"] += s[6:]
else:
self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6 self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE2 and s[:5] == b"FPXR\0": elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete) # extract FlashPix information (incomplete)

View File

@ -21,7 +21,7 @@ from types import TracebackType
from . import ContainerIO from . import ContainerIO
class TarIO(ContainerIO.ContainerIO): class TarIO(ContainerIO.ContainerIO[bytes]):
"""A file object that provides read access to a given member of a TAR file.""" """A file object that provides read access to a given member of a TAR file."""
def __init__(self, tarfile: str, file: str) -> None: def __init__(self, tarfile: str, file: str) -> None:

View File

@ -1706,20 +1706,21 @@ def _save(im, fp, filename):
# data orientation # data orientation
w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
stride = len(bits) * ((w * bits[0] + 7) // 8) stride = len(bits) * ((w * bits[0] + 7) // 8)
# aim for given strip size (64 KB by default) when using libtiff writer if ROWSPERSTRIP not in ifd:
if libtiff: # aim for given strip size (64 KB by default) when using libtiff writer
im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) if libtiff:
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
# JPEG encoder expects multiple of 8 rows rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
if compression == "jpeg": # JPEG encoder expects multiple of 8 rows
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) if compression == "jpeg":
else: rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
rows_per_strip = h else:
if rows_per_strip == 0: rows_per_strip = h
rows_per_strip = 1 if rows_per_strip == 0:
strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip rows_per_strip = 1
strips_per_image = (h + rows_per_strip - 1) // rows_per_strip ifd[ROWSPERSTRIP] = rows_per_strip
ifd[ROWSPERSTRIP] = rows_per_strip strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP]
strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
if strip_byte_counts >= 2**16: if strip_byte_counts >= 2**16:
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (

View File

@ -18,16 +18,16 @@ from __future__ import annotations
from struct import pack, unpack_from from struct import pack, unpack_from
def i8(c) -> int: def i8(c: bytes) -> int:
return c if c.__class__ is int else c[0] return c[0]
def o8(i): def o8(i: int) -> bytes:
return bytes((i & 255,)) return bytes((i & 255,))
# Input, le = little endian, be = big endian # Input, le = little endian, be = big endian
def i16le(c, o=0): def i16le(c: bytes, o: int = 0) -> int:
""" """
Converts a 2-bytes (16 bits) string to an unsigned integer. Converts a 2-bytes (16 bits) string to an unsigned integer.
@ -37,7 +37,7 @@ def i16le(c, o=0):
return unpack_from("<H", c, o)[0] return unpack_from("<H", c, o)[0]
def si16le(c, o=0): def si16le(c: bytes, o: int = 0) -> int:
""" """
Converts a 2-bytes (16 bits) string to a signed integer. Converts a 2-bytes (16 bits) string to a signed integer.
@ -47,7 +47,7 @@ def si16le(c, o=0):
return unpack_from("<h", c, o)[0] return unpack_from("<h", c, o)[0]
def si16be(c, o=0): def si16be(c: bytes, o: int = 0) -> int:
""" """
Converts a 2-bytes (16 bits) string to a signed integer, big endian. Converts a 2-bytes (16 bits) string to a signed integer, big endian.
@ -57,7 +57,7 @@ def si16be(c, o=0):
return unpack_from(">h", c, o)[0] return unpack_from(">h", c, o)[0]
def i32le(c, o=0) -> int: def i32le(c: bytes, o: int = 0) -> int:
""" """
Converts a 4-bytes (32 bits) string to an unsigned integer. Converts a 4-bytes (32 bits) string to an unsigned integer.
@ -67,7 +67,7 @@ def i32le(c, o=0) -> int:
return unpack_from("<I", c, o)[0] return unpack_from("<I", c, o)[0]
def si32le(c, o=0): def si32le(c: bytes, o: int = 0) -> int:
""" """
Converts a 4-bytes (32 bits) string to a signed integer. Converts a 4-bytes (32 bits) string to a signed integer.
@ -77,26 +77,26 @@ def si32le(c, o=0):
return unpack_from("<i", c, o)[0] return unpack_from("<i", c, o)[0]
def i16be(c, o=0): def i16be(c: bytes, o: int = 0) -> int:
return unpack_from(">H", c, o)[0] return unpack_from(">H", c, o)[0]
def i32be(c, o=0): def i32be(c: bytes, o: int = 0) -> int:
return unpack_from(">I", c, o)[0] return unpack_from(">I", c, o)[0]
# Output, le = little endian, be = big endian # Output, le = little endian, be = big endian
def o16le(i): def o16le(i: int) -> bytes:
return pack("<H", i) return pack("<H", i)
def o32le(i): def o32le(i: int) -> bytes:
return pack("<I", i) return pack("<I", i)
def o16be(i) -> bytes: def o16be(i: int) -> bytes:
return pack(">H", i) return pack(">H", i)
def o32be(i): def o32be(i: int) -> bytes:
return pack(">I", i) return pack(">I", i)