mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-06-29 09:23:11 +03:00
Merge branch 'main' into context_manager
This commit is contained in:
commit
66fe28956b
|
@ -2,8 +2,4 @@
|
||||||
|
|
||||||
# gather the coverage data
|
# gather the coverage data
|
||||||
python3 -m pip install coverage
|
python3 -m pip install coverage
|
||||||
if [[ $MATRIX_DOCKER ]]; then
|
python3 -m coverage xml
|
||||||
python3 -m coverage xml --ignore-errors
|
|
||||||
else
|
|
||||||
python3 -m coverage xml
|
|
||||||
fi
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
mypy==1.14.0
|
mypy==1.14.1
|
||||||
IceSpringPySideStubs-PyQt6
|
IceSpringPySideStubs-PyQt6
|
||||||
IceSpringPySideStubs-PySide6
|
IceSpringPySideStubs-PySide6
|
||||||
ipython
|
ipython
|
||||||
|
|
2
.github/workflows/test-cygwin.yml
vendored
2
.github/workflows/test-cygwin.yml
vendored
|
@ -52,7 +52,7 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Cygwin
|
- name: Install Cygwin
|
||||||
uses: cygwin/cygwin-install-action@v4
|
uses: cygwin/cygwin-install-action@v5
|
||||||
with:
|
with:
|
||||||
packages: >
|
packages: >
|
||||||
gcc-g++
|
gcc-g++
|
||||||
|
|
7
.github/workflows/test-docker.yml
vendored
7
.github/workflows/test-docker.yml
vendored
|
@ -44,6 +44,7 @@ jobs:
|
||||||
amazon-2023-amd64,
|
amazon-2023-amd64,
|
||||||
arch,
|
arch,
|
||||||
centos-stream-9-amd64,
|
centos-stream-9-amd64,
|
||||||
|
centos-stream-10-amd64,
|
||||||
debian-12-bookworm-x86,
|
debian-12-bookworm-x86,
|
||||||
debian-12-bookworm-amd64,
|
debian-12-bookworm-amd64,
|
||||||
fedora-40-amd64,
|
fedora-40-amd64,
|
||||||
|
@ -89,15 +90,15 @@ jobs:
|
||||||
|
|
||||||
- name: After success
|
- name: After success
|
||||||
run: |
|
run: |
|
||||||
PATH="$PATH:~/.local/bin"
|
|
||||||
docker start pillow_container
|
docker start pillow_container
|
||||||
|
sudo docker cp pillow_container:/Pillow /Pillow
|
||||||
|
sudo chown -R runner /Pillow
|
||||||
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
|
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
|
||||||
docker stop pillow_container
|
docker stop pillow_container
|
||||||
sudo mkdir -p $pil_path
|
sudo mkdir -p $pil_path
|
||||||
sudo cp src/PIL/*.py $pil_path
|
sudo cp src/PIL/*.py $pil_path
|
||||||
|
cd /Pillow
|
||||||
.ci/after_success.sh
|
.ci/after_success.sh
|
||||||
env:
|
|
||||||
MATRIX_DOCKER: ${{ matrix.docker }}
|
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
|
|
17
.github/workflows/wheels-dependencies.sh
vendored
17
.github/workflows/wheels-dependencies.sh
vendored
|
@ -37,20 +37,15 @@ fi
|
||||||
ARCHIVE_SDIR=pillow-depends-main
|
ARCHIVE_SDIR=pillow-depends-main
|
||||||
|
|
||||||
# Package versions for fresh source builds
|
# Package versions for fresh source builds
|
||||||
FREETYPE_VERSION=2.13.2
|
FREETYPE_VERSION=2.13.3
|
||||||
HARFBUZZ_VERSION=10.1.0
|
HARFBUZZ_VERSION=10.1.0
|
||||||
LIBPNG_VERSION=1.6.44
|
LIBPNG_VERSION=1.6.45
|
||||||
JPEGTURBO_VERSION=3.1.0
|
JPEGTURBO_VERSION=3.1.0
|
||||||
OPENJPEG_VERSION=2.5.3
|
OPENJPEG_VERSION=2.5.3
|
||||||
XZ_VERSION=5.6.3
|
XZ_VERSION=5.6.3
|
||||||
TIFF_VERSION=4.6.0
|
TIFF_VERSION=4.6.0
|
||||||
LCMS2_VERSION=2.16
|
LCMS2_VERSION=2.16
|
||||||
if [[ -n "$IS_MACOS" ]]; then
|
ZLIB_NG_VERSION=2.2.3
|
||||||
GIFLIB_VERSION=5.2.2
|
|
||||||
else
|
|
||||||
GIFLIB_VERSION=5.2.1
|
|
||||||
fi
|
|
||||||
ZLIB_NG_VERSION=2.2.2
|
|
||||||
LIBWEBP_VERSION=1.5.0
|
LIBWEBP_VERSION=1.5.0
|
||||||
BZIP2_VERSION=1.0.8
|
BZIP2_VERSION=1.0.8
|
||||||
LIBXCB_VERSION=1.17.0
|
LIBXCB_VERSION=1.17.0
|
||||||
|
@ -103,7 +98,7 @@ function build_harfbuzz {
|
||||||
|
|
||||||
function build {
|
function build {
|
||||||
build_xz
|
build_xz
|
||||||
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
|
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||||
yum remove -y zlib-devel
|
yum remove -y zlib-devel
|
||||||
fi
|
fi
|
||||||
build_zlib_ng
|
build_zlib_ng
|
||||||
|
@ -140,7 +135,9 @@ function build {
|
||||||
if [[ -n "$IS_MACOS" ]]; then
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
|
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
|
||||||
fi
|
fi
|
||||||
build_libwebp
|
build_simple libwebp $LIBWEBP_VERSION \
|
||||||
|
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
|
||||||
|
--enable-libwebpmux --enable-libwebpdemux
|
||||||
CFLAGS=$ORIGINAL_CFLAGS
|
CFLAGS=$ORIGINAL_CFLAGS
|
||||||
|
|
||||||
build_brotli
|
build_brotli
|
||||||
|
|
4
.github/workflows/wheels.yml
vendored
4
.github/workflows/wheels.yml
vendored
|
@ -13,6 +13,7 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- ".ci/requirements-cibw.txt"
|
- ".ci/requirements-cibw.txt"
|
||||||
- ".github/workflows/wheel*"
|
- ".github/workflows/wheel*"
|
||||||
|
- "pyproject.toml"
|
||||||
- "setup.py"
|
- "setup.py"
|
||||||
- "wheels/*"
|
- "wheels/*"
|
||||||
- "winbuild/build_prepare.py"
|
- "winbuild/build_prepare.py"
|
||||||
|
@ -23,6 +24,7 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- ".ci/requirements-cibw.txt"
|
- ".ci/requirements-cibw.txt"
|
||||||
- ".github/workflows/wheel*"
|
- ".github/workflows/wheel*"
|
||||||
|
- "pyproject.toml"
|
||||||
- "setup.py"
|
- "setup.py"
|
||||||
- "wheels/*"
|
- "wheels/*"
|
||||||
- "winbuild/build_prepare.py"
|
- "winbuild/build_prepare.py"
|
||||||
|
@ -263,8 +265,6 @@ jobs:
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
cache: pip
|
|
||||||
cache-dependency-path: "Makefile"
|
|
||||||
|
|
||||||
- run: make sdist
|
- run: make sdist
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.8.1
|
rev: v0.8.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--exit-non-zero-on-fix]
|
args: [--exit-non-zero-on-fix]
|
||||||
|
@ -24,7 +24,7 @@ repos:
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||||
rev: v19.1.4
|
rev: v19.1.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: clang-format
|
- id: clang-format
|
||||||
types: [c]
|
types: [c]
|
||||||
|
@ -56,6 +56,11 @@ repos:
|
||||||
- id: check-readthedocs
|
- id: check-readthedocs
|
||||||
- id: check-renovate
|
- id: check-renovate
|
||||||
|
|
||||||
|
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||||
|
rev: v1.0.0
|
||||||
|
hooks:
|
||||||
|
- id: zizmor
|
||||||
|
|
||||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||||
rev: v1.0.0
|
rev: v1.0.0
|
||||||
hooks:
|
hooks:
|
||||||
|
|
BIN
Tests/images/jfif_unit_cm.jpg
Normal file
BIN
Tests/images/jfif_unit_cm.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 391 B |
|
@ -4,7 +4,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import BlpImagePlugin, Image
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -19,6 +19,7 @@ def test_load_blp1() -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
|
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
|
||||||
|
|
||||||
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
|
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
|
||||||
|
assert im.mode == "RGBA"
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +38,13 @@ def test_load_blp2_dxt1a() -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
|
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_file() -> None:
|
||||||
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
with pytest.raises(BlpImagePlugin.BLPFormatError):
|
||||||
|
BlpImagePlugin.BlpImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_save(tmp_path: Path) -> None:
|
def test_save(tmp_path: Path) -> None:
|
||||||
f = str(tmp_path / "temp.blp")
|
f = str(tmp_path / "temp.blp")
|
||||||
|
|
||||||
|
|
|
@ -186,6 +186,10 @@ class TestFileJpeg:
|
||||||
assert test(100, 200) == (100, 200)
|
assert test(100, 200) == (100, 200)
|
||||||
assert test(0) is None # square pixels
|
assert test(0) is None # square pixels
|
||||||
|
|
||||||
|
def test_dpi_jfif_cm(self) -> None:
|
||||||
|
with Image.open("Tests/images/jfif_unit_cm.jpg") as im:
|
||||||
|
assert im.info["dpi"] == (2.54, 5.08)
|
||||||
|
|
||||||
@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"
|
||||||
)
|
)
|
||||||
|
@ -282,7 +286,10 @@ class TestFileJpeg:
|
||||||
assert not im2.info.get("progressive")
|
assert not im2.info.get("progressive")
|
||||||
assert im3.info.get("progressive")
|
assert im3.info.get("progressive")
|
||||||
|
|
||||||
assert_image_equal(im1, im3)
|
if features.check_feature("mozjpeg"):
|
||||||
|
assert_image_similar(im1, im3, 9.39)
|
||||||
|
else:
|
||||||
|
assert_image_equal(im1, im3)
|
||||||
assert im1_bytes >= im3_bytes
|
assert im1_bytes >= im3_bytes
|
||||||
|
|
||||||
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
|
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
|
||||||
|
@ -360,7 +367,6 @@ class TestFileJpeg:
|
||||||
assert exif.get_ifd(0x8825) == {}
|
assert exif.get_ifd(0x8825) == {}
|
||||||
|
|
||||||
transposed = ImageOps.exif_transpose(im)
|
transposed = ImageOps.exif_transpose(im)
|
||||||
assert transposed is not None
|
|
||||||
exif = transposed.getexif()
|
exif = transposed.getexif()
|
||||||
assert exif.get_ifd(0x8825) == {}
|
assert exif.get_ifd(0x8825) == {}
|
||||||
|
|
||||||
|
@ -437,8 +443,12 @@ class TestFileJpeg:
|
||||||
|
|
||||||
im2 = self.roundtrip(hopper(), progressive=1)
|
im2 = self.roundtrip(hopper(), progressive=1)
|
||||||
im3 = self.roundtrip(hopper(), progression=1) # compatibility
|
im3 = self.roundtrip(hopper(), progression=1) # compatibility
|
||||||
assert_image_equal(im1, im2)
|
if features.check_feature("mozjpeg"):
|
||||||
assert_image_equal(im1, im3)
|
assert_image_similar(im1, im2, 9.39)
|
||||||
|
assert_image_similar(im1, im3, 9.39)
|
||||||
|
else:
|
||||||
|
assert_image_equal(im1, im2)
|
||||||
|
assert_image_equal(im1, im3)
|
||||||
assert im2.info.get("progressive")
|
assert im2.info.get("progressive")
|
||||||
assert im2.info.get("progression")
|
assert im2.info.get("progression")
|
||||||
assert im3.info.get("progressive")
|
assert im3.info.get("progressive")
|
||||||
|
|
|
@ -328,6 +328,18 @@ def test_cmyk() -> None:
|
||||||
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
|
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||||
|
)
|
||||||
|
@skip_unless_feature_version("jpg_2000", "2.5.3")
|
||||||
|
def test_cmyk_save() -> None:
|
||||||
|
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2:
|
||||||
|
assert jp2.mode == "CMYK"
|
||||||
|
|
||||||
|
im = roundtrip(jp2)
|
||||||
|
assert_image_equal(im, jp2)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||||
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
|
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
|
||||||
with Image.open("Tests/images/16bit.cropped" + ext) as im:
|
with Image.open("Tests/images/16bit.cropped" + ext) as im:
|
||||||
|
|
|
@ -1177,7 +1177,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
# Assert that the error code is IMAGING_CODEC_MEMORY
|
# Assert that the error code is IMAGING_CODEC_MEMORY
|
||||||
assert str(e.value) == "-9"
|
assert str(e.value) == "decoder error -9"
|
||||||
|
|
||||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||||
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
||||||
|
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageSequence, SpiderImagePlugin
|
from PIL import Image, SpiderImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_equal, hopper, is_pypy
|
from .helper import assert_image_equal, hopper, is_pypy
|
||||||
|
|
||||||
|
@ -154,8 +154,8 @@ def test_nonstack_file() -> None:
|
||||||
|
|
||||||
def test_nonstack_dos() -> None:
|
def test_nonstack_dos() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
for i, frame in enumerate(ImageSequence.Iterator(im)):
|
with pytest.raises(EOFError):
|
||||||
assert i <= 1, "Non-stack DOS file test failed"
|
im.seek(0)
|
||||||
|
|
||||||
|
|
||||||
# for issue #4093
|
# for issue #4093
|
||||||
|
|
|
@ -123,6 +123,19 @@ class TestFileTiff:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||||
|
|
||||||
|
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
||||||
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
im = hopper()
|
||||||
|
im.save(outfile, big_tiff=True)
|
||||||
|
|
||||||
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert reloaded.tag_v2._bigtiff is True
|
||||||
|
|
||||||
|
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
|
||||||
|
|
||||||
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert reloaded.tag_v2._bigtiff is True
|
||||||
|
|
||||||
def test_seek_too_large(self) -> None:
|
def test_seek_too_large(self) -> None:
|
||||||
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
||||||
Image.open("Tests/images/seek_too_large.tif")
|
Image.open("Tests/images/seek_too_large.tif")
|
||||||
|
@ -768,7 +781,7 @@ class TestFileTiff:
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
def test_fixoffsets(self) -> None:
|
def test_fixoffsets(self) -> None:
|
||||||
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
|
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
|
||||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||||
b.seek(0)
|
b.seek(0)
|
||||||
a.fixOffsets(1, isShort=True)
|
a.fixOffsets(1, isShort=True)
|
||||||
|
@ -781,6 +794,37 @@ class TestFileTiff:
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
a.fixOffsets(1)
|
a.fixOffsets(1)
|
||||||
|
|
||||||
|
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
|
||||||
|
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||||
|
a.offsetOfNewPage = 2**16
|
||||||
|
|
||||||
|
b.seek(0)
|
||||||
|
a.fixOffsets(1, isShort=True)
|
||||||
|
|
||||||
|
b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||||
|
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||||
|
a.offsetOfNewPage = 2**32
|
||||||
|
|
||||||
|
b.seek(0)
|
||||||
|
a.fixOffsets(1, isShort=True)
|
||||||
|
|
||||||
|
b.seek(0)
|
||||||
|
a.fixOffsets(1, isLong=True)
|
||||||
|
|
||||||
|
def test_appending_tiff_writer_writelong(self) -> None:
|
||||||
|
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||||
|
b = BytesIO(data)
|
||||||
|
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||||
|
a.writeLong(2**32 - 1)
|
||||||
|
assert b.getvalue() == data + b"\xff\xff\xff\xff"
|
||||||
|
|
||||||
|
def test_appending_tiff_writer_rewritelastshorttolong(self) -> None:
|
||||||
|
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||||
|
b = BytesIO(data)
|
||||||
|
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||||
|
a.rewriteLastShortToLong(2**32 - 1)
|
||||||
|
assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff"
|
||||||
|
|
||||||
def test_saving_icc_profile(self, tmp_path: Path) -> None:
|
def test_saving_icc_profile(self, tmp_path: Path) -> None:
|
||||||
# Tests saving TIFF with icc_profile set.
|
# Tests saving TIFF with icc_profile set.
|
||||||
# At the time of writing this will only work for non-compressed tiffs
|
# At the time of writing this will only work for non-compressed tiffs
|
||||||
|
|
|
@ -795,6 +795,10 @@ class TestImage:
|
||||||
ifd[36864] = b"0220"
|
ifd[36864] = b"0220"
|
||||||
assert exif.get_ifd(0x8769) == {36864: b"0220"}
|
assert exif.get_ifd(0x8769) == {36864: b"0220"}
|
||||||
|
|
||||||
|
reloaded_exif = Image.Exif()
|
||||||
|
reloaded_exif.load(exif.tobytes())
|
||||||
|
assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"}
|
||||||
|
|
||||||
@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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -405,7 +405,6 @@ def test_exif_transpose() -> None:
|
||||||
else:
|
else:
|
||||||
original_exif = im.info["exif"]
|
original_exif = im.info["exif"]
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
assert transposed_im is not None
|
|
||||||
assert_image_similar(base_im, transposed_im, 17)
|
assert_image_similar(base_im, transposed_im, 17)
|
||||||
if orientation_im is base_im:
|
if orientation_im is base_im:
|
||||||
assert "exif" not in im.info
|
assert "exif" not in im.info
|
||||||
|
@ -417,7 +416,6 @@ def test_exif_transpose() -> None:
|
||||||
|
|
||||||
# Repeat the operation to test that it does not keep transposing
|
# Repeat the operation to test that it does not keep transposing
|
||||||
transposed_im2 = ImageOps.exif_transpose(transposed_im)
|
transposed_im2 = ImageOps.exif_transpose(transposed_im)
|
||||||
assert transposed_im2 is not None
|
|
||||||
assert_image_equal(transposed_im2, transposed_im)
|
assert_image_equal(transposed_im2, transposed_im)
|
||||||
|
|
||||||
check(base_im)
|
check(base_im)
|
||||||
|
@ -434,7 +432,6 @@ def test_exif_transpose() -> None:
|
||||||
assert im.getexif()[0x0112] == 3
|
assert im.getexif()[0x0112] == 3
|
||||||
|
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
assert transposed_im is not None
|
|
||||||
assert 0x0112 not in transposed_im.getexif()
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
transposed_im._reload_exif()
|
transposed_im._reload_exif()
|
||||||
|
@ -454,7 +451,6 @@ def test_exif_transpose() -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
im.getexif()[0x0112] = 3
|
im.getexif()[0x0112] = 3
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
assert transposed_im is not None
|
|
||||||
assert 0x0112 not in transposed_im.getexif()
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
|
||||||
|
@ -465,7 +461,6 @@ def test_exif_transpose_xml_without_xmp() -> None:
|
||||||
|
|
||||||
del im.info["xmp"]
|
del im.info["xmp"]
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
assert transposed_im is not None
|
|
||||||
assert 0x0112 not in transposed_im.getexif()
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,14 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt
|
||||||
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
|
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
|
||||||
``Image.Image.getim()``, which returns a ``Capsule`` object.
|
``Image.Image.getim()``, which returns a ``Capsule`` object.
|
||||||
|
|
||||||
|
ExifTags.IFD.Makernote
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. deprecated:: 11.1.0
|
||||||
|
|
||||||
|
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
|
||||||
|
``ExifTags.IFD.MakerNote``.
|
||||||
|
|
||||||
Removed features
|
Removed features
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
@ -572,10 +572,19 @@ JPEG 2000
|
||||||
Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
|
Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
|
||||||
``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
|
``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
|
||||||
``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel.
|
``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel.
|
||||||
Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``,
|
|
||||||
``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports
|
.. versionadded:: 8.3.0
|
||||||
JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files
|
Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with
|
||||||
(``.jp2`` or ``.jpx`` files).
|
subsampled components.
|
||||||
|
|
||||||
|
.. versionadded:: 10.4.0
|
||||||
|
Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later.
|
||||||
|
|
||||||
|
.. versionadded:: 11.1.0
|
||||||
|
Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later.
|
||||||
|
|
||||||
|
Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed
|
||||||
|
JPEG 2000 files (``.jp2`` or ``.jpx`` files).
|
||||||
|
|
||||||
When loading, if you set the ``mode`` on the image prior to the
|
When loading, if you set the ``mode`` on the image prior to the
|
||||||
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to
|
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to
|
||||||
|
@ -1199,6 +1208,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
|
||||||
|
|
||||||
.. versionadded:: 8.4.0
|
.. versionadded:: 8.4.0
|
||||||
|
|
||||||
|
**big_tiff**
|
||||||
|
If true, the image will be saved as a BigTIFF.
|
||||||
|
|
||||||
|
.. versionadded:: 11.1.0
|
||||||
|
|
||||||
**compression**
|
**compression**
|
||||||
A string containing the desired compression method for the
|
A string containing the desired compression method for the
|
||||||
file. (valid only with libtiff installed) Valid compression
|
file. (valid only with libtiff installed) Valid compression
|
||||||
|
|
|
@ -27,6 +27,8 @@ These platforms are built and tested for every change.
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| CentOS Stream 9 | 3.9 | x86-64 |
|
| CentOS Stream 9 | 3.9 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
|
| CentOS Stream 10 | 3.12 | x86-64 |
|
||||||
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Fedora 40 | 3.12 | x86-64 |
|
| Fedora 40 | 3.12 | x86-64 |
|
||||||
|
@ -75,7 +77,7 @@ These platforms have been reported to work at the versions mentioned.
|
||||||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||||
| | | versions | | Pillow version | | processors |
|
| | | versions | | Pillow version | | processors |
|
||||||
+==================================+============================+==================+==============+
|
+==================================+============================+==================+==============+
|
||||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm |
|
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
|
||||||
| +----------------------------+------------------+ |
|
| +----------------------------+------------------+ |
|
||||||
| | 3.8 | 10.4.0 | |
|
| | 3.8 | 10.4.0 | |
|
||||||
+----------------------------------+----------------------------+------------------+--------------+
|
+----------------------------------+----------------------------+------------------+--------------+
|
||||||
|
|
|
@ -1,40 +1,38 @@
|
||||||
11.1.0
|
11.1.0
|
||||||
------
|
------
|
||||||
|
|
||||||
Security
|
|
||||||
========
|
|
||||||
|
|
||||||
TODO
|
|
||||||
^^^^
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
:cve:`YYYY-XXXXX`: TODO
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
Backwards Incompatible Changes
|
|
||||||
==============================
|
|
||||||
|
|
||||||
TODO
|
|
||||||
^^^^
|
|
||||||
|
|
||||||
Deprecations
|
Deprecations
|
||||||
============
|
============
|
||||||
|
|
||||||
TODO
|
ExifTags.IFD.Makernote
|
||||||
^^^^
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
|
||||||
|
``ExifTags.IFD.MakerNote``.
|
||||||
|
|
||||||
API Changes
|
API Changes
|
||||||
===========
|
===========
|
||||||
|
|
||||||
TODO
|
Writing XMP bytes to JPEG and MPO
|
||||||
^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
Pillow 11.0.0 added writing XMP data to JPEG and MPO images::
|
||||||
|
|
||||||
|
im.info["xmp"] = b"test"
|
||||||
|
im.save("out.jpg")
|
||||||
|
|
||||||
|
However, this meant that XMP data was automatically kept from an opened image,
|
||||||
|
which is inconsistent with the rest of Pillow's behaviour. This functionality
|
||||||
|
has been removed. To write XMP data, the ``xmp`` argument can still be used for
|
||||||
|
JPEG files::
|
||||||
|
|
||||||
|
im.save("out.jpg", xmp=b"test")
|
||||||
|
|
||||||
|
To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now
|
||||||
|
be used::
|
||||||
|
|
||||||
|
second_im.encoderinfo = {"xmp": b"test"}
|
||||||
|
im.save("out.mpo", save_all=True, append_images=[second_im])
|
||||||
|
|
||||||
API Additions
|
API Additions
|
||||||
=============
|
=============
|
||||||
|
@ -49,6 +47,13 @@ zlib library, and what version of zlib-ng is being used::
|
||||||
features.check_feature("zlib_ng") # True or False
|
features.check_feature("zlib_ng") # True or False
|
||||||
features.version_feature("zlib_ng") # "2.2.2" for example, or None
|
features.version_feature("zlib_ng") # "2.2.2" for example, or None
|
||||||
|
|
||||||
|
Saving TIFF as BigTIFF
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument::
|
||||||
|
|
||||||
|
im.save("out.tiff", big_tiff=True)
|
||||||
|
|
||||||
Other Changes
|
Other Changes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
@ -58,6 +63,16 @@ Reading JPEG 2000 comments
|
||||||
When opening a JPEG 2000 image, the comment may now be read into
|
When opening a JPEG 2000 image, the comment may now be read into
|
||||||
:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images.
|
:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images.
|
||||||
|
|
||||||
|
Saving JPEG 2000 CMYK images
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files.
|
||||||
|
|
||||||
|
Minimum C version
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
C99 is now the minimum version of C required to compile Pillow from source.
|
||||||
|
|
||||||
zlib-ng in wheels
|
zlib-ng in wheels
|
||||||
^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -260,21 +260,36 @@ class BlpImageFile(ImageFile.ImageFile):
|
||||||
def _open(self) -> None:
|
def _open(self) -> None:
|
||||||
assert self.fp is not None
|
assert self.fp is not None
|
||||||
self.magic = self.fp.read(4)
|
self.magic = self.fp.read(4)
|
||||||
|
if not _accept(self.magic):
|
||||||
self.fp.seek(5, os.SEEK_CUR)
|
|
||||||
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
|
|
||||||
|
|
||||||
self.fp.seek(2, os.SEEK_CUR)
|
|
||||||
self._size = struct.unpack("<II", self.fp.read(8))
|
|
||||||
|
|
||||||
if self.magic in (b"BLP1", b"BLP2"):
|
|
||||||
decoder = self.magic.decode()
|
|
||||||
else:
|
|
||||||
msg = f"Bad BLP magic {repr(self.magic)}"
|
msg = f"Bad BLP magic {repr(self.magic)}"
|
||||||
raise BLPFormatError(msg)
|
raise BLPFormatError(msg)
|
||||||
|
|
||||||
self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
|
compression = struct.unpack("<i", self.fp.read(4))[0]
|
||||||
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)]
|
if self.magic == b"BLP1":
|
||||||
|
alpha = struct.unpack("<I", self.fp.read(4))[0] != 0
|
||||||
|
else:
|
||||||
|
encoding = struct.unpack("<b", self.fp.read(1))[0]
|
||||||
|
alpha = struct.unpack("<b", self.fp.read(1))[0] != 0
|
||||||
|
alpha_encoding = struct.unpack("<b", self.fp.read(1))[0]
|
||||||
|
self.fp.seek(1, os.SEEK_CUR) # mips
|
||||||
|
|
||||||
|
self._size = struct.unpack("<II", self.fp.read(8))
|
||||||
|
|
||||||
|
args: tuple[int, int, bool] | tuple[int, int, bool, int]
|
||||||
|
if self.magic == b"BLP1":
|
||||||
|
encoding = struct.unpack("<i", self.fp.read(4))[0]
|
||||||
|
self.fp.seek(4, os.SEEK_CUR) # subtype
|
||||||
|
|
||||||
|
args = (compression, encoding, alpha)
|
||||||
|
offset = 28
|
||||||
|
else:
|
||||||
|
args = (compression, encoding, alpha, alpha_encoding)
|
||||||
|
offset = 20
|
||||||
|
|
||||||
|
decoder = self.magic.decode()
|
||||||
|
|
||||||
|
self._mode = "RGBA" if alpha else "RGB"
|
||||||
|
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
|
||||||
|
|
||||||
|
|
||||||
class _BLPBaseDecoder(ImageFile.PyDecoder):
|
class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
|
@ -282,7 +297,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
|
|
||||||
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
||||||
try:
|
try:
|
||||||
self._read_blp_header()
|
self._read_header()
|
||||||
self._load()
|
self._load()
|
||||||
except struct.error as e:
|
except struct.error as e:
|
||||||
msg = "Truncated BLP file"
|
msg = "Truncated BLP file"
|
||||||
|
@ -293,25 +308,9 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _read_blp_header(self) -> None:
|
def _read_header(self) -> None:
|
||||||
assert self.fd is not None
|
self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||||
self.fd.seek(4)
|
self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||||
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
|
|
||||||
|
|
||||||
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
|
|
||||||
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
|
|
||||||
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
|
|
||||||
self.fd.seek(1, os.SEEK_CUR) # mips
|
|
||||||
|
|
||||||
self.size = struct.unpack("<II", self._safe_read(8))
|
|
||||||
|
|
||||||
if isinstance(self, BLP1Decoder):
|
|
||||||
# Only present for BLP1
|
|
||||||
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
|
|
||||||
self.fd.seek(4, os.SEEK_CUR) # subtype
|
|
||||||
|
|
||||||
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
|
||||||
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
|
||||||
|
|
||||||
def _safe_read(self, length: int) -> bytes:
|
def _safe_read(self, length: int) -> bytes:
|
||||||
assert self.fd is not None
|
assert self.fd is not None
|
||||||
|
@ -327,9 +326,11 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
ret.append((b, g, r, a))
|
ret.append((b, g, r, a))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
|
def _read_bgra(
|
||||||
|
self, palette: list[tuple[int, int, int, int]], alpha: bool
|
||||||
|
) -> bytearray:
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
|
_data = BytesIO(self._safe_read(self._lengths[0]))
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
(offset,) = struct.unpack("<B", _data.read(1))
|
(offset,) = struct.unpack("<B", _data.read(1))
|
||||||
|
@ -337,7 +338,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
break
|
break
|
||||||
b, g, r, a = palette[offset]
|
b, g, r, a = palette[offset]
|
||||||
d: tuple[int, ...] = (r, g, b)
|
d: tuple[int, ...] = (r, g, b)
|
||||||
if self._blp_alpha_depth:
|
if alpha:
|
||||||
d += (a,)
|
d += (a,)
|
||||||
data.extend(d)
|
data.extend(d)
|
||||||
return data
|
return data
|
||||||
|
@ -345,19 +346,21 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
|
|
||||||
class BLP1Decoder(_BLPBaseDecoder):
|
class BLP1Decoder(_BLPBaseDecoder):
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
if self._blp_compression == Format.JPEG:
|
self._compression, self._encoding, alpha = self.args
|
||||||
|
|
||||||
|
if self._compression == Format.JPEG:
|
||||||
self._decode_jpeg_stream()
|
self._decode_jpeg_stream()
|
||||||
|
|
||||||
elif self._blp_compression == 1:
|
elif self._compression == 1:
|
||||||
if self._blp_encoding in (4, 5):
|
if self._encoding in (4, 5):
|
||||||
palette = self._read_palette()
|
palette = self._read_palette()
|
||||||
data = self._read_bgra(palette)
|
data = self._read_bgra(palette, alpha)
|
||||||
self.set_as_raw(data)
|
self.set_as_raw(data)
|
||||||
else:
|
else:
|
||||||
msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
|
msg = f"Unsupported BLP encoding {repr(self._encoding)}"
|
||||||
raise BLPFormatError(msg)
|
raise BLPFormatError(msg)
|
||||||
else:
|
else:
|
||||||
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
|
msg = f"Unsupported BLP compression {repr(self._encoding)}"
|
||||||
raise BLPFormatError(msg)
|
raise BLPFormatError(msg)
|
||||||
|
|
||||||
def _decode_jpeg_stream(self) -> None:
|
def _decode_jpeg_stream(self) -> None:
|
||||||
|
@ -366,65 +369,61 @@ class BLP1Decoder(_BLPBaseDecoder):
|
||||||
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
|
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
|
||||||
jpeg_header = self._safe_read(jpeg_header_size)
|
jpeg_header = self._safe_read(jpeg_header_size)
|
||||||
assert self.fd is not None
|
assert self.fd is not None
|
||||||
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
|
self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
|
||||||
data = self._safe_read(self._blp_lengths[0])
|
data = self._safe_read(self._lengths[0])
|
||||||
data = jpeg_header + data
|
data = jpeg_header + data
|
||||||
image = JpegImageFile(BytesIO(data))
|
image = JpegImageFile(BytesIO(data))
|
||||||
Image._decompression_bomb_check(image.size)
|
Image._decompression_bomb_check(image.size)
|
||||||
if image.mode == "CMYK":
|
if image.mode == "CMYK":
|
||||||
decoder_name, extents, offset, args = image.tile[0]
|
args = image.tile[0].args
|
||||||
assert isinstance(args, tuple)
|
assert isinstance(args, tuple)
|
||||||
image.tile = [
|
image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
|
||||||
ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK"))
|
self.set_as_raw(image.convert("RGB").tobytes(), "BGR")
|
||||||
]
|
|
||||||
r, g, b = image.convert("RGB").split()
|
|
||||||
reversed_image = Image.merge("RGB", (b, g, r))
|
|
||||||
self.set_as_raw(reversed_image.tobytes())
|
|
||||||
|
|
||||||
|
|
||||||
class BLP2Decoder(_BLPBaseDecoder):
|
class BLP2Decoder(_BLPBaseDecoder):
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
|
self._compression, self._encoding, alpha, self._alpha_encoding = self.args
|
||||||
|
|
||||||
palette = self._read_palette()
|
palette = self._read_palette()
|
||||||
|
|
||||||
assert self.fd is not None
|
assert self.fd is not None
|
||||||
self.fd.seek(self._blp_offsets[0])
|
self.fd.seek(self._offsets[0])
|
||||||
|
|
||||||
if self._blp_compression == 1:
|
if self._compression == 1:
|
||||||
# Uncompressed or DirectX compression
|
# Uncompressed or DirectX compression
|
||||||
|
|
||||||
if self._blp_encoding == Encoding.UNCOMPRESSED:
|
if self._encoding == Encoding.UNCOMPRESSED:
|
||||||
data = self._read_bgra(palette)
|
data = self._read_bgra(palette, alpha)
|
||||||
|
|
||||||
elif self._blp_encoding == Encoding.DXT:
|
elif self._encoding == Encoding.DXT:
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
if self._blp_alpha_encoding == AlphaEncoding.DXT1:
|
if self._alpha_encoding == AlphaEncoding.DXT1:
|
||||||
linesize = (self.size[0] + 3) // 4 * 8
|
linesize = (self.state.xsize + 3) // 4 * 8
|
||||||
for yb in range((self.size[1] + 3) // 4):
|
for yb in range((self.state.ysize + 3) // 4):
|
||||||
for d in decode_dxt1(
|
for d in decode_dxt1(self._safe_read(linesize), alpha):
|
||||||
self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
|
|
||||||
):
|
|
||||||
data += d
|
data += d
|
||||||
|
|
||||||
elif self._blp_alpha_encoding == AlphaEncoding.DXT3:
|
elif self._alpha_encoding == AlphaEncoding.DXT3:
|
||||||
linesize = (self.size[0] + 3) // 4 * 16
|
linesize = (self.state.xsize + 3) // 4 * 16
|
||||||
for yb in range((self.size[1] + 3) // 4):
|
for yb in range((self.state.ysize + 3) // 4):
|
||||||
for d in decode_dxt3(self._safe_read(linesize)):
|
for d in decode_dxt3(self._safe_read(linesize)):
|
||||||
data += d
|
data += d
|
||||||
|
|
||||||
elif self._blp_alpha_encoding == AlphaEncoding.DXT5:
|
elif self._alpha_encoding == AlphaEncoding.DXT5:
|
||||||
linesize = (self.size[0] + 3) // 4 * 16
|
linesize = (self.state.xsize + 3) // 4 * 16
|
||||||
for yb in range((self.size[1] + 3) // 4):
|
for yb in range((self.state.ysize + 3) // 4):
|
||||||
for d in decode_dxt5(self._safe_read(linesize)):
|
for d in decode_dxt5(self._safe_read(linesize)):
|
||||||
data += d
|
data += d
|
||||||
else:
|
else:
|
||||||
msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
|
msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
|
||||||
raise BLPFormatError(msg)
|
raise BLPFormatError(msg)
|
||||||
else:
|
else:
|
||||||
msg = f"Unknown BLP encoding {repr(self._blp_encoding)}"
|
msg = f"Unknown BLP encoding {repr(self._encoding)}"
|
||||||
raise BLPFormatError(msg)
|
raise BLPFormatError(msg)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
msg = f"Unknown BLP compression {repr(self._blp_compression)}"
|
msg = f"Unknown BLP compression {repr(self._compression)}"
|
||||||
raise BLPFormatError(msg)
|
raise BLPFormatError(msg)
|
||||||
|
|
||||||
self.set_as_raw(data)
|
self.set_as_raw(data)
|
||||||
|
@ -473,10 +472,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
|
||||||
assert im.palette is not None
|
assert im.palette is not None
|
||||||
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
|
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
|
||||||
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
|
|
||||||
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0))
|
alpha_depth = 1 if im.palette.mode == "RGBA" else 0
|
||||||
fp.write(struct.pack("<b", 0)) # alpha encoding
|
if magic == b"BLP1":
|
||||||
fp.write(struct.pack("<b", 0)) # mips
|
fp.write(struct.pack("<L", alpha_depth))
|
||||||
|
else:
|
||||||
|
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
|
||||||
|
fp.write(struct.pack("<b", alpha_depth))
|
||||||
|
fp.write(struct.pack("<b", 0)) # alpha encoding
|
||||||
|
fp.write(struct.pack("<b", 0)) # mips
|
||||||
fp.write(struct.pack("<II", *im.size))
|
fp.write(struct.pack("<II", *im.size))
|
||||||
if magic == b"BLP1":
|
if magic == b"BLP1":
|
||||||
fp.write(struct.pack("<i", 5))
|
fp.write(struct.pack("<i", 5))
|
||||||
|
|
|
@ -353,6 +353,7 @@ class IFD(IntEnum):
|
||||||
Exif = 0x8769
|
Exif = 0x8769
|
||||||
GPSInfo = 0x8825
|
GPSInfo = 0x8825
|
||||||
MakerNote = 0x927C
|
MakerNote = 0x927C
|
||||||
|
Makernote = 0x927C # Deprecated
|
||||||
Interop = 0xA005
|
Interop = 0xA005
|
||||||
IFD1 = -1
|
IFD1 = -1
|
||||||
|
|
||||||
|
|
|
@ -3963,6 +3963,9 @@ class Exif(_ExifBase):
|
||||||
|
|
||||||
head = self._get_head()
|
head = self._get_head()
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
|
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
|
||||||
|
for tag, ifd_dict in self._ifds.items():
|
||||||
|
if tag not in self:
|
||||||
|
ifd[tag] = ifd_dict
|
||||||
for tag, value in self.items():
|
for tag, value in self.items():
|
||||||
if tag in [
|
if tag in [
|
||||||
ExifTags.IFD.Exif,
|
ExifTags.IFD.Exif,
|
||||||
|
|
|
@ -22,7 +22,7 @@ import functools
|
||||||
import operator
|
import operator
|
||||||
import re
|
import re
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Protocol, cast
|
from typing import Literal, Protocol, cast, overload
|
||||||
|
|
||||||
from . import ExifTags, Image, ImagePalette
|
from . import ExifTags, Image, ImagePalette
|
||||||
|
|
||||||
|
@ -673,6 +673,16 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
|
||||||
return _lut(image, lut)
|
return _lut(image, lut)
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def exif_transpose(
|
||||||
|
image: Image.Image, *, in_place: Literal[False] = False
|
||||||
|
) -> Image.Image: ...
|
||||||
|
|
||||||
|
|
||||||
def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
|
def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
|
||||||
"""
|
"""
|
||||||
If an image has an EXIF Orientation tag, other than 1, transpose the image
|
If an image has an EXIF Orientation tag, other than 1, transpose the image
|
||||||
|
|
|
@ -92,6 +92,9 @@ def APP(self: JpegImageFile, marker: int) -> None:
|
||||||
else:
|
else:
|
||||||
if jfif_unit == 1:
|
if jfif_unit == 1:
|
||||||
self.info["dpi"] = jfif_density
|
self.info["dpi"] = jfif_density
|
||||||
|
elif jfif_unit == 2: # cm
|
||||||
|
# 1 dpcm = 2.54 dpi
|
||||||
|
self.info["dpi"] = tuple(d * 2.54 for d in 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[:6] == b"Exif\0\0":
|
elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
|
||||||
|
|
|
@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ifh: bytes = b"II\052\0\0\0\0\0",
|
ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00",
|
||||||
prefix: bytes | None = None,
|
prefix: bytes | None = None,
|
||||||
group: int | None = None,
|
group: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -949,12 +949,25 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
||||||
warnings.warn(str(msg))
|
warnings.warn(str(msg))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _get_ifh(self) -> bytes:
|
||||||
|
ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
|
||||||
|
if self._bigtiff:
|
||||||
|
ifh += self._pack("HH", 8, 0)
|
||||||
|
ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8)
|
||||||
|
|
||||||
|
return ifh
|
||||||
|
|
||||||
def tobytes(self, offset: int = 0) -> bytes:
|
def tobytes(self, offset: int = 0) -> bytes:
|
||||||
# FIXME What about tagdata?
|
# FIXME What about tagdata?
|
||||||
result = self._pack("H", len(self._tags_v2))
|
result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
|
||||||
|
|
||||||
entries: list[tuple[int, int, int, bytes, bytes]] = []
|
entries: list[tuple[int, int, int, bytes, bytes]] = []
|
||||||
offset = offset + len(result) + len(self._tags_v2) * 12 + 4
|
|
||||||
|
fmt = "Q" if self._bigtiff else "L"
|
||||||
|
fmt_size = 8 if self._bigtiff else 4
|
||||||
|
offset += (
|
||||||
|
len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
|
||||||
|
)
|
||||||
stripoffsets = None
|
stripoffsets = None
|
||||||
|
|
||||||
# pass 1: convert tags to binary format
|
# pass 1: convert tags to binary format
|
||||||
|
@ -966,11 +979,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
||||||
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
|
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
|
||||||
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
|
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
|
||||||
if is_ifd:
|
if is_ifd:
|
||||||
if self._endian == "<":
|
ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
|
||||||
ifh = b"II\x2A\x00\x08\x00\x00\x00"
|
|
||||||
else:
|
|
||||||
ifh = b"MM\x00\x2A\x00\x00\x00\x08"
|
|
||||||
ifd = ImageFileDirectory_v2(ifh, group=tag)
|
|
||||||
values = self._tags_v2[tag]
|
values = self._tags_v2[tag]
|
||||||
for ifd_tag, ifd_value in values.items():
|
for ifd_tag, ifd_value in values.items():
|
||||||
ifd[ifd_tag] = ifd_value
|
ifd[ifd_tag] = ifd_value
|
||||||
|
@ -993,10 +1002,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
||||||
else:
|
else:
|
||||||
count = len(values)
|
count = len(values)
|
||||||
# figure out if data fits into the entry
|
# figure out if data fits into the entry
|
||||||
if len(data) <= 4:
|
if len(data) <= fmt_size:
|
||||||
entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
|
entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
|
||||||
else:
|
else:
|
||||||
entries.append((tag, typ, count, self._pack("L", offset), data))
|
entries.append((tag, typ, count, self._pack(fmt, offset), data))
|
||||||
offset += (len(data) + 1) // 2 * 2 # pad to word
|
offset += (len(data) + 1) // 2 * 2 # pad to word
|
||||||
|
|
||||||
# update strip offset data to point beyond auxiliary data
|
# update strip offset data to point beyond auxiliary data
|
||||||
|
@ -1007,16 +1016,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
||||||
values = [val + offset for val in handler(self, data, self.legacy_api)]
|
values = [val + offset for val in handler(self, data, self.legacy_api)]
|
||||||
data = self._write_dispatch[typ](self, *values)
|
data = self._write_dispatch[typ](self, *values)
|
||||||
else:
|
else:
|
||||||
value = self._pack("L", self._unpack("L", value)[0] + offset)
|
value = self._pack(fmt, self._unpack(fmt, value)[0] + offset)
|
||||||
entries[stripoffsets] = tag, typ, count, value, data
|
entries[stripoffsets] = tag, typ, count, value, data
|
||||||
|
|
||||||
# pass 2: write entries to file
|
# pass 2: write entries to file
|
||||||
for tag, typ, count, value, data in entries:
|
for tag, typ, count, value, data in entries:
|
||||||
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
|
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
|
||||||
result += self._pack("HHL4s", tag, typ, count, value)
|
result += self._pack(
|
||||||
|
"HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value
|
||||||
|
)
|
||||||
|
|
||||||
# -- overwrite here for multi-page --
|
# -- overwrite here for multi-page --
|
||||||
result += b"\0\0\0\0" # end of entries
|
result += self._pack(fmt, 0) # end of entries
|
||||||
|
|
||||||
# pass 3: write auxiliary data to file
|
# pass 3: write auxiliary data to file
|
||||||
for tag, typ, count, value, data in entries:
|
for tag, typ, count, value, data in entries:
|
||||||
|
@ -1028,8 +1039,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
||||||
|
|
||||||
def save(self, fp: IO[bytes]) -> int:
|
def save(self, fp: IO[bytes]) -> int:
|
||||||
if fp.tell() == 0: # skip TIFF header on subsequent pages
|
if fp.tell() == 0: # skip TIFF header on subsequent pages
|
||||||
# tiff header -- PIL always starts the first IFD at offset 8
|
fp.write(self._get_ifh())
|
||||||
fp.write(self._prefix + self._pack("HL", 42, 8))
|
|
||||||
|
|
||||||
offset = fp.tell()
|
offset = fp.tell()
|
||||||
result = self.tobytes(offset)
|
result = self.tobytes(offset)
|
||||||
|
@ -1403,7 +1413,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
self.fp = None # might be shared
|
self.fp = None # might be shared
|
||||||
|
|
||||||
if err < 0:
|
if err < 0:
|
||||||
raise OSError(err)
|
msg = f"decoder error {err}"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
return Image.Image.load(self)
|
return Image.Image.load(self)
|
||||||
|
|
||||||
|
@ -1563,17 +1574,6 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
# fillorder==2 modes have a corresponding
|
# fillorder==2 modes have a corresponding
|
||||||
# fillorder=1 mode
|
# fillorder=1 mode
|
||||||
self._mode, rawmode = OPEN_INFO[key]
|
self._mode, rawmode = OPEN_INFO[key]
|
||||||
# libtiff always returns the bytes in native order.
|
|
||||||
# we're expecting image byte order. So, if the rawmode
|
|
||||||
# contains I;16, we need to convert from native to image
|
|
||||||
# byte order.
|
|
||||||
if rawmode == "I;16":
|
|
||||||
rawmode = "I;16N"
|
|
||||||
if ";16B" in rawmode:
|
|
||||||
rawmode = rawmode.replace(";16B", ";16N")
|
|
||||||
if ";16L" in rawmode:
|
|
||||||
rawmode = rawmode.replace(";16L", ";16N")
|
|
||||||
|
|
||||||
# YCbCr images with new jpeg compression with pixels in one plane
|
# YCbCr images with new jpeg compression with pixels in one plane
|
||||||
# unpacked straight into RGB values
|
# unpacked straight into RGB values
|
||||||
if (
|
if (
|
||||||
|
@ -1582,6 +1582,14 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
and self._planar_configuration == 1
|
and self._planar_configuration == 1
|
||||||
):
|
):
|
||||||
rawmode = "RGB"
|
rawmode = "RGB"
|
||||||
|
# libtiff always returns the bytes in native order.
|
||||||
|
# we're expecting image byte order. So, if the rawmode
|
||||||
|
# contains I;16, we need to convert from native to image
|
||||||
|
# byte order.
|
||||||
|
elif rawmode == "I;16":
|
||||||
|
rawmode = "I;16N"
|
||||||
|
elif rawmode.endswith(";16B") or rawmode.endswith(";16L"):
|
||||||
|
rawmode = rawmode[:-1] + "N"
|
||||||
|
|
||||||
# Offset in the tile tuple is 0, we go from 0,0 to
|
# Offset in the tile tuple is 0, we go from 0,0 to
|
||||||
# w,h, and we only do this once -- eds
|
# w,h, and we only do this once -- eds
|
||||||
|
@ -1687,10 +1695,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
msg = f"cannot write mode {im.mode} as TIFF"
|
msg = f"cannot write mode {im.mode} as TIFF"
|
||||||
raise OSError(msg) from e
|
raise OSError(msg) from e
|
||||||
|
|
||||||
ifd = ImageFileDirectory_v2(prefix=prefix)
|
|
||||||
|
|
||||||
encoderinfo = im.encoderinfo
|
encoderinfo = im.encoderinfo
|
||||||
encoderconfig = im.encoderconfig
|
encoderconfig = im.encoderconfig
|
||||||
|
|
||||||
|
ifd = ImageFileDirectory_v2(prefix=prefix)
|
||||||
|
if encoderinfo.get("big_tiff"):
|
||||||
|
ifd._bigtiff = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
compression = encoderinfo["compression"]
|
compression = encoderinfo["compression"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -2040,20 +2051,21 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
self.offsetOfNewPage = 0
|
self.offsetOfNewPage = 0
|
||||||
|
|
||||||
self.IIMM = iimm = self.f.read(4)
|
self.IIMM = iimm = self.f.read(4)
|
||||||
|
self._bigtiff = b"\x2B" in iimm
|
||||||
if not iimm:
|
if not iimm:
|
||||||
# empty file - first page
|
# empty file - first page
|
||||||
self.isFirst = True
|
self.isFirst = True
|
||||||
return
|
return
|
||||||
|
|
||||||
self.isFirst = False
|
self.isFirst = False
|
||||||
if iimm == b"II\x2a\x00":
|
if iimm not in PREFIXES:
|
||||||
self.setEndian("<")
|
|
||||||
elif iimm == b"MM\x00\x2a":
|
|
||||||
self.setEndian(">")
|
|
||||||
else:
|
|
||||||
msg = "Invalid TIFF file header"
|
msg = "Invalid TIFF file header"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
self.setEndian("<" if iimm.startswith(II) else ">")
|
||||||
|
|
||||||
|
if self._bigtiff:
|
||||||
|
self.f.seek(4, os.SEEK_CUR)
|
||||||
self.skipIFDs()
|
self.skipIFDs()
|
||||||
self.goToEnd()
|
self.goToEnd()
|
||||||
|
|
||||||
|
@ -2073,11 +2085,13 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
msg = "IIMM of new page doesn't match IIMM of first page"
|
msg = "IIMM of new page doesn't match IIMM of first page"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
ifd_offset = self.readLong()
|
if self._bigtiff:
|
||||||
|
self.f.seek(4, os.SEEK_CUR)
|
||||||
|
ifd_offset = self._read(8 if self._bigtiff else 4)
|
||||||
ifd_offset += self.offsetOfNewPage
|
ifd_offset += self.offsetOfNewPage
|
||||||
assert self.whereToWriteNewIFDOffset is not None
|
assert self.whereToWriteNewIFDOffset is not None
|
||||||
self.f.seek(self.whereToWriteNewIFDOffset)
|
self.f.seek(self.whereToWriteNewIFDOffset)
|
||||||
self.writeLong(ifd_offset)
|
self._write(ifd_offset, 8 if self._bigtiff else 4)
|
||||||
self.f.seek(ifd_offset)
|
self.f.seek(ifd_offset)
|
||||||
self.fixIFD()
|
self.fixIFD()
|
||||||
|
|
||||||
|
@ -2123,18 +2137,20 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
self.endian = endian
|
self.endian = endian
|
||||||
self.longFmt = f"{self.endian}L"
|
self.longFmt = f"{self.endian}L"
|
||||||
self.shortFmt = f"{self.endian}H"
|
self.shortFmt = f"{self.endian}H"
|
||||||
self.tagFormat = f"{self.endian}HHL"
|
self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
|
||||||
|
|
||||||
def skipIFDs(self) -> None:
|
def skipIFDs(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
ifd_offset = self.readLong()
|
ifd_offset = self._read(8 if self._bigtiff else 4)
|
||||||
if ifd_offset == 0:
|
if ifd_offset == 0:
|
||||||
self.whereToWriteNewIFDOffset = self.f.tell() - 4
|
self.whereToWriteNewIFDOffset = self.f.tell() - (
|
||||||
|
8 if self._bigtiff else 4
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
self.f.seek(ifd_offset)
|
self.f.seek(ifd_offset)
|
||||||
num_tags = self.readShort()
|
num_tags = self._read(8 if self._bigtiff else 2)
|
||||||
self.f.seek(num_tags * 12, os.SEEK_CUR)
|
self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
|
||||||
|
|
||||||
def write(self, data: Buffer, /) -> int:
|
def write(self, data: Buffer, /) -> int:
|
||||||
return self.f.write(data)
|
return self.f.write(data)
|
||||||
|
@ -2164,17 +2180,19 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
msg = f"wrote only {bytes_written} bytes but wanted {expected}"
|
msg = f"wrote only {bytes_written} bytes but wanted {expected}"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
def rewriteLastShortToLong(self, value: int) -> None:
|
def _rewriteLast(
|
||||||
self.f.seek(-2, os.SEEK_CUR)
|
self, value: int, field_size: int, new_field_size: int = 0
|
||||||
bytes_written = self.f.write(struct.pack(self.longFmt, value))
|
) -> None:
|
||||||
self._verify_bytes_written(bytes_written, 4)
|
|
||||||
|
|
||||||
def _rewriteLast(self, value: int, field_size: int) -> None:
|
|
||||||
self.f.seek(-field_size, os.SEEK_CUR)
|
self.f.seek(-field_size, os.SEEK_CUR)
|
||||||
|
if not new_field_size:
|
||||||
|
new_field_size = field_size
|
||||||
bytes_written = self.f.write(
|
bytes_written = self.f.write(
|
||||||
struct.pack(self.endian + self._fmt(field_size), value)
|
struct.pack(self.endian + self._fmt(new_field_size), value)
|
||||||
)
|
)
|
||||||
self._verify_bytes_written(bytes_written, field_size)
|
self._verify_bytes_written(bytes_written, new_field_size)
|
||||||
|
|
||||||
|
def rewriteLastShortToLong(self, value: int) -> None:
|
||||||
|
self._rewriteLast(value, 2, 4)
|
||||||
|
|
||||||
def rewriteLastShort(self, value: int) -> None:
|
def rewriteLastShort(self, value: int) -> None:
|
||||||
return self._rewriteLast(value, 2)
|
return self._rewriteLast(value, 2)
|
||||||
|
@ -2182,13 +2200,17 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
def rewriteLastLong(self, value: int) -> None:
|
def rewriteLastLong(self, value: int) -> None:
|
||||||
return self._rewriteLast(value, 4)
|
return self._rewriteLast(value, 4)
|
||||||
|
|
||||||
|
def _write(self, value: int, field_size: int) -> None:
|
||||||
|
bytes_written = self.f.write(
|
||||||
|
struct.pack(self.endian + self._fmt(field_size), value)
|
||||||
|
)
|
||||||
|
self._verify_bytes_written(bytes_written, field_size)
|
||||||
|
|
||||||
def writeShort(self, value: int) -> None:
|
def writeShort(self, value: int) -> None:
|
||||||
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
|
self._write(value, 2)
|
||||||
self._verify_bytes_written(bytes_written, 2)
|
|
||||||
|
|
||||||
def writeLong(self, value: int) -> None:
|
def writeLong(self, value: int) -> None:
|
||||||
bytes_written = self.f.write(struct.pack(self.longFmt, value))
|
self._write(value, 4)
|
||||||
self._verify_bytes_written(bytes_written, 4)
|
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
self.finalize()
|
self.finalize()
|
||||||
|
@ -2196,24 +2218,27 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
self.f.close()
|
self.f.close()
|
||||||
|
|
||||||
def fixIFD(self) -> None:
|
def fixIFD(self) -> None:
|
||||||
num_tags = self.readShort()
|
num_tags = self._read(8 if self._bigtiff else 2)
|
||||||
|
|
||||||
for i in range(num_tags):
|
for i in range(num_tags):
|
||||||
tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))
|
tag, field_type, count = struct.unpack(
|
||||||
|
self.tagFormat, self.f.read(12 if self._bigtiff else 8)
|
||||||
|
)
|
||||||
|
|
||||||
field_size = self.fieldSizes[field_type]
|
field_size = self.fieldSizes[field_type]
|
||||||
total_size = field_size * count
|
total_size = field_size * count
|
||||||
is_local = total_size <= 4
|
fmt_size = 8 if self._bigtiff else 4
|
||||||
|
is_local = total_size <= fmt_size
|
||||||
if not is_local:
|
if not is_local:
|
||||||
offset = self.readLong() + self.offsetOfNewPage
|
offset = self._read(fmt_size) + self.offsetOfNewPage
|
||||||
self.rewriteLastLong(offset)
|
self._rewriteLast(offset, fmt_size)
|
||||||
|
|
||||||
if tag in self.Tags:
|
if tag in self.Tags:
|
||||||
cur_pos = self.f.tell()
|
cur_pos = self.f.tell()
|
||||||
|
|
||||||
if is_local:
|
if is_local:
|
||||||
self._fixOffsets(count, field_size)
|
self._fixOffsets(count, field_size)
|
||||||
self.f.seek(cur_pos + 4)
|
self.f.seek(cur_pos + fmt_size)
|
||||||
else:
|
else:
|
||||||
self.f.seek(offset)
|
self.f.seek(offset)
|
||||||
self._fixOffsets(count, field_size)
|
self._fixOffsets(count, field_size)
|
||||||
|
@ -2221,24 +2246,33 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
|
|
||||||
elif is_local:
|
elif is_local:
|
||||||
# skip the locally stored value that is not an offset
|
# skip the locally stored value that is not an offset
|
||||||
self.f.seek(4, os.SEEK_CUR)
|
self.f.seek(fmt_size, os.SEEK_CUR)
|
||||||
|
|
||||||
def _fixOffsets(self, count: int, field_size: int) -> None:
|
def _fixOffsets(self, count: int, field_size: int) -> None:
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
offset = self._read(field_size)
|
offset = self._read(field_size)
|
||||||
offset += self.offsetOfNewPage
|
offset += self.offsetOfNewPage
|
||||||
if field_size == 2 and offset >= 65536:
|
|
||||||
# offset is now too large - we must convert shorts to longs
|
new_field_size = 0
|
||||||
|
if self._bigtiff and field_size in (2, 4) and offset >= 2**32:
|
||||||
|
# offset is now too large - we must convert long to long8
|
||||||
|
new_field_size = 8
|
||||||
|
elif field_size == 2 and offset >= 2**16:
|
||||||
|
# offset is now too large - we must convert short to long
|
||||||
|
new_field_size = 4
|
||||||
|
if new_field_size:
|
||||||
if count != 1:
|
if count != 1:
|
||||||
msg = "not implemented"
|
msg = "not implemented"
|
||||||
raise RuntimeError(msg) # XXX TODO
|
raise RuntimeError(msg) # XXX TODO
|
||||||
|
|
||||||
# simple case - the offset is just one and therefore it is
|
# simple case - the offset is just one and therefore it is
|
||||||
# local (not referenced with another offset)
|
# local (not referenced with another offset)
|
||||||
self.rewriteLastShortToLong(offset)
|
self._rewriteLast(offset, field_size, new_field_size)
|
||||||
self.f.seek(-10, os.SEEK_CUR)
|
# Move back past the new offset, past 'count', and before 'field_type'
|
||||||
self.writeShort(TiffTags.LONG) # rewrite the type to LONG
|
rewind = -new_field_size - 4 - 2
|
||||||
self.f.seek(8, os.SEEK_CUR)
|
self.f.seek(rewind, os.SEEK_CUR)
|
||||||
|
self.writeShort(new_field_size) # rewrite the type
|
||||||
|
self.f.seek(2 - rewind, os.SEEK_CUR)
|
||||||
else:
|
else:
|
||||||
self._rewriteLast(offset, field_size)
|
self._rewriteLast(offset, field_size)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Master version for Pillow
|
# Master version for Pillow
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__version__ = "11.1.0.dev0"
|
__version__ = "11.2.0.dev0"
|
||||||
|
|
|
@ -127,6 +127,7 @@ features: dict[str, tuple[str, str | bool, str | None]] = {
|
||||||
"fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
|
"fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
|
||||||
"harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"),
|
"harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"),
|
||||||
"libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"),
|
"libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"),
|
||||||
|
"mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"),
|
||||||
"zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"),
|
"zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"),
|
||||||
"libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"),
|
"libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"),
|
||||||
"xcb": ("PIL._imaging", "HAVE_XCB", None),
|
"xcb": ("PIL._imaging", "HAVE_XCB", None),
|
||||||
|
@ -300,7 +301,8 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
|
||||||
if name == "jpg":
|
if name == "jpg":
|
||||||
libjpeg_turbo_version = version_feature("libjpeg_turbo")
|
libjpeg_turbo_version = version_feature("libjpeg_turbo")
|
||||||
if libjpeg_turbo_version is not None:
|
if libjpeg_turbo_version is not None:
|
||||||
v = "libjpeg-turbo " + libjpeg_turbo_version
|
v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo"
|
||||||
|
v += " " + libjpeg_turbo_version
|
||||||
if v is None:
|
if v is None:
|
||||||
v = version(name)
|
v = version(name)
|
||||||
if v is not None:
|
if v is not None:
|
||||||
|
|
|
@ -76,6 +76,13 @@
|
||||||
|
|
||||||
#ifdef HAVE_LIBJPEG
|
#ifdef HAVE_LIBJPEG
|
||||||
#include "jconfig.h"
|
#include "jconfig.h"
|
||||||
|
#ifdef LIBJPEG_TURBO_VERSION
|
||||||
|
#define JCONFIG_INCLUDED
|
||||||
|
#ifdef __CYGWIN__
|
||||||
|
#define _BASETSD_H
|
||||||
|
#endif
|
||||||
|
#include "jpeglib.h"
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_LIBZ
|
#ifdef HAVE_LIBZ
|
||||||
|
@ -4367,6 +4374,15 @@ setup_module(PyObject *m) {
|
||||||
Py_INCREF(have_libjpegturbo);
|
Py_INCREF(have_libjpegturbo);
|
||||||
PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo);
|
PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo);
|
||||||
|
|
||||||
|
PyObject *have_mozjpeg;
|
||||||
|
#ifdef JPEG_C_PARAM_SUPPORTED
|
||||||
|
have_mozjpeg = Py_True;
|
||||||
|
#else
|
||||||
|
have_mozjpeg = Py_False;
|
||||||
|
#endif
|
||||||
|
Py_INCREF(have_mozjpeg);
|
||||||
|
PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg);
|
||||||
|
|
||||||
PyObject *have_libimagequant;
|
PyObject *have_libimagequant;
|
||||||
#ifdef HAVE_LIBIMAGEQUANT
|
#ifdef HAVE_LIBIMAGEQUANT
|
||||||
have_libimagequant = Py_True;
|
have_libimagequant = Py_True;
|
||||||
|
|
|
@ -339,29 +339,23 @@ text_layout_raqm(
|
||||||
len = PySequence_Fast_GET_SIZE(seq);
|
len = PySequence_Fast_GET_SIZE(seq);
|
||||||
for (j = 0; j < len; j++) {
|
for (j = 0; j < len; j++) {
|
||||||
PyObject *item = PySequence_Fast_GET_ITEM(seq, j);
|
PyObject *item = PySequence_Fast_GET_ITEM(seq, j);
|
||||||
char *feature = NULL;
|
|
||||||
Py_ssize_t size = 0;
|
|
||||||
PyObject *bytes;
|
|
||||||
|
|
||||||
if (!PyUnicode_Check(item)) {
|
if (!PyUnicode_Check(item)) {
|
||||||
Py_DECREF(seq);
|
Py_DECREF(seq);
|
||||||
PyErr_SetString(PyExc_TypeError, "expected a string");
|
PyErr_SetString(PyExc_TypeError, "expected a string");
|
||||||
goto failed;
|
goto failed;
|
||||||
}
|
}
|
||||||
bytes = PyUnicode_AsUTF8String(item);
|
|
||||||
if (bytes == NULL) {
|
Py_ssize_t size;
|
||||||
|
const char *feature = PyUnicode_AsUTF8AndSize(item, &size);
|
||||||
|
if (feature == NULL) {
|
||||||
Py_DECREF(seq);
|
Py_DECREF(seq);
|
||||||
goto failed;
|
goto failed;
|
||||||
}
|
}
|
||||||
feature = PyBytes_AS_STRING(bytes);
|
|
||||||
size = PyBytes_GET_SIZE(bytes);
|
|
||||||
if (!raqm_add_font_feature(rq, feature, size)) {
|
if (!raqm_add_font_feature(rq, feature, size)) {
|
||||||
Py_DECREF(seq);
|
Py_DECREF(seq);
|
||||||
Py_DECREF(bytes);
|
|
||||||
PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed");
|
PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed");
|
||||||
goto failed;
|
goto failed;
|
||||||
}
|
}
|
||||||
Py_DECREF(bytes);
|
|
||||||
}
|
}
|
||||||
Py_DECREF(seq);
|
Py_DECREF(seq);
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,6 @@
|
||||||
defines their own types with the same names, so we need to be able to undef
|
defines their own types with the same names, so we need to be able to undef
|
||||||
ours before including the JPEG code. */
|
ours before including the JPEG code. */
|
||||||
|
|
||||||
#if __STDC_VERSION__ >= 199901L /* C99+ */
|
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#define INT8 int8_t
|
#define INT8 int8_t
|
||||||
|
@ -55,34 +53,6 @@
|
||||||
#define INT32 int32_t
|
#define INT32 int32_t
|
||||||
#define UINT32 uint32_t
|
#define UINT32 uint32_t
|
||||||
|
|
||||||
#else /* < C99 */
|
|
||||||
|
|
||||||
#define INT8 signed char
|
|
||||||
|
|
||||||
#if SIZEOF_SHORT == 2
|
|
||||||
#define INT16 short
|
|
||||||
#elif SIZEOF_INT == 2
|
|
||||||
#define INT16 int
|
|
||||||
#else
|
|
||||||
#error Cannot find required 16-bit integer type
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if SIZEOF_SHORT == 4
|
|
||||||
#define INT32 short
|
|
||||||
#elif SIZEOF_INT == 4
|
|
||||||
#define INT32 int
|
|
||||||
#elif SIZEOF_LONG == 4
|
|
||||||
#define INT32 long
|
|
||||||
#else
|
|
||||||
#error Cannot find required 32-bit integer type
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define UINT8 unsigned char
|
|
||||||
#define UINT16 unsigned INT16
|
|
||||||
#define UINT32 unsigned INT32
|
|
||||||
|
|
||||||
#endif /* < C99 */
|
|
||||||
|
|
||||||
#endif /* not WIN */
|
#endif /* not WIN */
|
||||||
|
|
||||||
/* assume IEEE; tweak if necessary (patches are welcome) */
|
/* assume IEEE; tweak if necessary (patches are welcome) */
|
||||||
|
|
|
@ -330,6 +330,13 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
|
||||||
components = 4;
|
components = 4;
|
||||||
color_space = OPJ_CLRSPC_SRGB;
|
color_space = OPJ_CLRSPC_SRGB;
|
||||||
pack = j2k_pack_rgba;
|
pack = j2k_pack_rgba;
|
||||||
|
#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \
|
||||||
|
(OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2)
|
||||||
|
} else if (strcmp(im->mode, "CMYK") == 0) {
|
||||||
|
components = 4;
|
||||||
|
color_space = OPJ_CLRSPC_CMYK;
|
||||||
|
pack = j2k_pack_rgba;
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
state->errcode = IMAGING_CODEC_BROKEN;
|
state->errcode = IMAGING_CODEC_BROKEN;
|
||||||
state->state = J2K_STATE_FAILED;
|
state->state = J2K_STATE_FAILED;
|
||||||
|
|
|
@ -134,7 +134,16 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compressor configuration */
|
/* Compressor configuration */
|
||||||
|
#ifdef JPEG_C_PARAM_SUPPORTED
|
||||||
|
/* MozJPEG */
|
||||||
|
if (!context->progressive) {
|
||||||
|
/* Do not use MozJPEG progressive default */
|
||||||
|
jpeg_c_set_int_param(
|
||||||
|
&context->cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
jpeg_set_defaults(&context->cinfo);
|
jpeg_set_defaults(&context->cinfo);
|
||||||
|
|
||||||
/* Prevent RGB -> YCbCr conversion */
|
/* Prevent RGB -> YCbCr conversion */
|
||||||
|
|
|
@ -1664,6 +1664,7 @@ static struct {
|
||||||
{"RGBA", "RGBaXX", 48, unpackRGBaskip2},
|
{"RGBA", "RGBaXX", 48, unpackRGBaskip2},
|
||||||
{"RGBA", "RGBa;16L", 64, unpackRGBa16L},
|
{"RGBA", "RGBa;16L", 64, unpackRGBa16L},
|
||||||
{"RGBA", "RGBa;16B", 64, unpackRGBa16B},
|
{"RGBA", "RGBa;16B", 64, unpackRGBa16B},
|
||||||
|
{"RGBA", "BGR", 24, ImagingUnpackBGR},
|
||||||
{"RGBA", "BGRa", 32, unpackBGRa},
|
{"RGBA", "BGRa", 32, unpackBGRa},
|
||||||
{"RGBA", "RGBA;I", 32, unpackRGBAI},
|
{"RGBA", "RGBA;I", 32, unpackRGBAI},
|
||||||
{"RGBA", "RGBA;L", 32, unpackRGBAL},
|
{"RGBA", "RGBA;L", 32, unpackRGBAL},
|
||||||
|
@ -1695,6 +1696,7 @@ static struct {
|
||||||
|
|
||||||
#ifdef WORDS_BIGENDIAN
|
#ifdef WORDS_BIGENDIAN
|
||||||
{"RGB", "RGB;16N", 48, unpackRGB16B},
|
{"RGB", "RGB;16N", 48, unpackRGB16B},
|
||||||
|
{"RGB", "RGBX;16N", 64, unpackRGBA16B},
|
||||||
{"RGBA", "RGBa;16N", 64, unpackRGBa16B},
|
{"RGBA", "RGBa;16N", 64, unpackRGBa16B},
|
||||||
{"RGBA", "RGBA;16N", 64, unpackRGBA16B},
|
{"RGBA", "RGBA;16N", 64, unpackRGBA16B},
|
||||||
{"RGBX", "RGBX;16N", 64, unpackRGBA16B},
|
{"RGBX", "RGBX;16N", 64, unpackRGBA16B},
|
||||||
|
@ -1708,6 +1710,7 @@ static struct {
|
||||||
{"RGBA", "A;16N", 16, band316B},
|
{"RGBA", "A;16N", 16, band316B},
|
||||||
#else
|
#else
|
||||||
{"RGB", "RGB;16N", 48, unpackRGB16L},
|
{"RGB", "RGB;16N", 48, unpackRGB16L},
|
||||||
|
{"RGB", "RGBX;16N", 64, unpackRGBA16L},
|
||||||
{"RGBA", "RGBa;16N", 64, unpackRGBa16L},
|
{"RGBA", "RGBa;16N", 64, unpackRGBa16L},
|
||||||
{"RGBA", "RGBA;16N", 64, unpackRGBA16L},
|
{"RGBA", "RGBA;16N", 64, unpackRGBA16L},
|
||||||
{"RGBX", "RGBX;16N", 64, unpackRGBA16L},
|
{"RGBX", "RGBX;16N", 64, unpackRGBA16L},
|
||||||
|
|
|
@ -116,14 +116,13 @@ V = {
|
||||||
"HARFBUZZ": "10.1.0",
|
"HARFBUZZ": "10.1.0",
|
||||||
"JPEGTURBO": "3.1.0",
|
"JPEGTURBO": "3.1.0",
|
||||||
"LCMS2": "2.16",
|
"LCMS2": "2.16",
|
||||||
"LIBPNG": "1.6.44",
|
"LIBPNG": "1.6.45",
|
||||||
"LIBWEBP": "1.5.0",
|
"LIBWEBP": "1.5.0",
|
||||||
"OPENJPEG": "2.5.3",
|
"OPENJPEG": "2.5.3",
|
||||||
"TIFF": "4.6.0",
|
"TIFF": "4.6.0",
|
||||||
"XZ": "5.6.3",
|
"XZ": "5.6.3",
|
||||||
"ZLIBNG": "2.2.2",
|
"ZLIBNG": "2.2.3",
|
||||||
}
|
}
|
||||||
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
|
|
||||||
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
|
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
|
||||||
|
|
||||||
|
|
||||||
|
@ -241,8 +240,8 @@ DEPS: dict[str, dict[str, Any]] = {
|
||||||
},
|
},
|
||||||
"libpng": {
|
"libpng": {
|
||||||
"url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/"
|
"url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/"
|
||||||
f"lpng{V['LIBPNG_DOTLESS']}.zip/download",
|
f"FILENAME/download",
|
||||||
"filename": f"lpng{V['LIBPNG_DOTLESS']}.zip",
|
"filename": f"libpng-{V['LIBPNG']}.tar.gz",
|
||||||
"license": "LICENSE",
|
"license": "LICENSE",
|
||||||
"build": [
|
"build": [
|
||||||
*cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"),
|
*cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user