mirror of
https://github.com/python-pillow/Pillow.git
synced 2026-02-04 14:35:54 +03:00
Merge 90ba70d748 into fc4dbc3810
This commit is contained in:
commit
945a946689
|
|
@ -55,5 +55,8 @@ pushd depends && sudo ./install_raqm.sh && popd
|
|||
# libavif
|
||||
pushd depends && sudo ./install_libavif.sh && popd
|
||||
|
||||
# libjxl
|
||||
pushd depends && sudo ./install_libjxl.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
|
|||
1
.github/workflows/test-mingw.yml
vendored
1
.github/workflows/test-mingw.yml
vendored
|
|
@ -63,6 +63,7 @@ jobs:
|
|||
mingw-w64-x86_64-libavif \
|
||||
mingw-w64-x86_64-libimagequant \
|
||||
mingw-w64-x86_64-libjpeg-turbo \
|
||||
mingw-w64-x86_64-libjxl \
|
||||
mingw-w64-x86_64-libraqm \
|
||||
mingw-w64-x86_64-libtiff \
|
||||
mingw-w64-x86_64-libwebp \
|
||||
|
|
|
|||
8
.github/workflows/test-windows.yml
vendored
8
.github/workflows/test-windows.yml
vendored
|
|
@ -175,6 +175,14 @@ jobs:
|
|||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
run: "& winbuild\\build\\build_dep_libimagequant.cmd"
|
||||
|
||||
- name: Build dependencies / highway
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
run: "& winbuild\\build\\build_dep_highway.cmd"
|
||||
|
||||
- name: Build dependencies / libjxl
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
run: "& winbuild\\build\\build_dep_libjxl.cmd"
|
||||
|
||||
# Raqm dependencies
|
||||
- name: Build dependencies / HarfBuzz
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
|
|
|
|||
45
.github/workflows/wheels-dependencies.sh
vendored
45
.github/workflows/wheels-dependencies.sh
vendored
|
|
@ -99,6 +99,7 @@ HARFBUZZ_VERSION=12.3.0
|
|||
LIBPNG_VERSION=1.6.54
|
||||
JPEGTURBO_VERSION=3.1.3
|
||||
OPENJPEG_VERSION=2.5.4
|
||||
JPEGXL_VERSION=0.11.1
|
||||
XZ_VERSION=5.8.2
|
||||
ZSTD_VERSION=1.5.7
|
||||
TIFF_VERSION=4.7.1
|
||||
|
|
@ -161,6 +162,21 @@ function build_brotli {
|
|||
touch brotli-stamp
|
||||
}
|
||||
|
||||
function build_jpegxl {
|
||||
if [ -e jpegxl-stamp ]; then return; fi
|
||||
|
||||
local out_dir=$(fetch_unpack https://github.com/google/highway/archive/1.3.0.tar.gz)
|
||||
(cd $out_dir \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
|
||||
&& make -j4 install)
|
||||
|
||||
local out_dir=$(fetch_unpack https://github.com/libjxl/libjxl/archive/v$JPEGXL_VERSION.tar.gz)
|
||||
(cd $out_dir \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib -DJPEGXL_ENABLE_SJPEG=OFF -DJPEGXL_ENABLE_SKCMS=OFF -DBUILD_TESTING=OFF $HOST_CMAKE_FLAGS . \
|
||||
&& make -j4 install)
|
||||
touch jpegxl-stamp
|
||||
}
|
||||
|
||||
function build_harfbuzz {
|
||||
if [ -e harfbuzz-stamp ]; then return; fi
|
||||
python3 -m pip install meson ninja
|
||||
|
|
@ -293,19 +309,6 @@ function build {
|
|||
build_libpng
|
||||
build_lcms2
|
||||
build_openjpeg
|
||||
|
||||
webp_cflags="-O3 -DNDEBUG"
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
|
||||
fi
|
||||
webp_ldflags=""
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
webp_ldflags="$webp_ldflags -llzma -lz"
|
||||
fi
|
||||
CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \
|
||||
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
|
||||
--enable-libwebpmux --enable-libwebpdemux
|
||||
|
||||
build_brotli
|
||||
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
|
|
@ -323,7 +326,23 @@ function build {
|
|||
# On iOS, there's no vendor-provided raqm, and we can't ship it due to
|
||||
# licensing, so there's no point building harfbuzz.
|
||||
build_harfbuzz
|
||||
|
||||
if [[ "$MB_ML_VER" != 2014 ]]; then
|
||||
build_jpegxl
|
||||
fi
|
||||
fi
|
||||
|
||||
webp_cflags="-O3 -DNDEBUG"
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
|
||||
fi
|
||||
webp_ldflags=""
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
webp_ldflags="$webp_ldflags -llzma -lz"
|
||||
fi
|
||||
CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \
|
||||
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
|
||||
--enable-libwebpmux --enable-libwebpdemux
|
||||
}
|
||||
|
||||
function create_meson_cross_config {
|
||||
|
|
|
|||
BIN
Tests/images/jxl/16bit_subcutaneous.cropped.jxl
Normal file
BIN
Tests/images/jxl/16bit_subcutaneous.cropped.jxl
Normal file
Binary file not shown.
BIN
Tests/images/jxl/16bit_subcutaneous.cropped.png
Normal file
BIN
Tests/images/jxl/16bit_subcutaneous.cropped.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
Tests/images/jxl/flower.jxl
Normal file
BIN
Tests/images/jxl/flower.jxl
Normal file
Binary file not shown.
BIN
Tests/images/jxl/flower2.jxl
Normal file
BIN
Tests/images/jxl/flower2.jxl
Normal file
Binary file not shown.
BIN
Tests/images/jxl/hopper.jxl
Normal file
BIN
Tests/images/jxl/hopper.jxl
Normal file
Binary file not shown.
BIN
Tests/images/jxl/hopper_bw_500.jxl
Normal file
BIN
Tests/images/jxl/hopper_bw_500.jxl
Normal file
Binary file not shown.
BIN
Tests/images/jxl/hopper_gray.jxl
Normal file
BIN
Tests/images/jxl/hopper_gray.jxl
Normal file
Binary file not shown.
BIN
Tests/images/jxl/iss634.jxl
Normal file
BIN
Tests/images/jxl/iss634.jxl
Normal file
Binary file not shown.
BIN
Tests/images/jxl/traffic_light.gif
Normal file
BIN
Tests/images/jxl/traffic_light.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Tests/images/jxl/traffic_light.jxl
Normal file
BIN
Tests/images/jxl/traffic_light.jxl
Normal file
Binary file not shown.
BIN
Tests/images/jxl/transparent.jxl
Normal file
BIN
Tests/images/jxl/transparent.jxl
Normal file
Binary file not shown.
BIN
Tests/images/jxl/unknown_mode.jxl
Normal file
BIN
Tests/images/jxl/unknown_mode.jxl
Normal file
Binary file not shown.
64
Tests/test_file_jxl.py
Normal file
64
Tests/test_file_jxl.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, JpegXlImagePlugin, UnidentifiedImageError, features
|
||||
|
||||
from .helper import assert_image_similar_tofile, skip_unless_feature
|
||||
|
||||
try:
|
||||
from PIL import _jpegxl
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class TestUnsupportedJpegXl:
|
||||
def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(JpegXlImagePlugin, "SUPPORTED", False)
|
||||
|
||||
with pytest.raises(OSError):
|
||||
with pytest.warns(UserWarning, match="JXL support not installed"):
|
||||
Image.open("Tests/images/jxl/hopper.jxl")
|
||||
|
||||
|
||||
@skip_unless_feature("jpegxl")
|
||||
class TestFileJpegXl:
|
||||
def test_version(self) -> None:
|
||||
version = features.version_module("jpegxl")
|
||||
assert version is not None
|
||||
assert re.search(r"\d+\.\d+\.\d+$", version)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, test_file",
|
||||
(
|
||||
("1", "hopper_bw_500.png"),
|
||||
("L", "hopper_gray.jpg"),
|
||||
("I;16", "jxl/16bit_subcutaneous.cropped.png"),
|
||||
("RGB", "hopper.jpg"),
|
||||
("RGBA", "transparent.png"),
|
||||
),
|
||||
)
|
||||
def test_read(self, mode: str, test_file: str) -> None:
|
||||
with Image.open(
|
||||
"Tests/images/jxl/"
|
||||
+ os.path.splitext(os.path.basename(test_file))[0]
|
||||
+ ".jxl"
|
||||
) as im:
|
||||
assert im.format == "JPEG XL"
|
||||
assert im.mode == mode
|
||||
|
||||
assert_image_similar_tofile(im, "Tests/images/" + test_file, 1.9)
|
||||
|
||||
def test_unknown_mode(self) -> None:
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
Image.open("Tests/images/jxl/unknown_mode.jxl")
|
||||
|
||||
def test_JpegXlDecode_with_invalid_args(self) -> None:
|
||||
"""
|
||||
Calling decoder functions with no arguments should result in an error.
|
||||
"""
|
||||
with pytest.raises(TypeError):
|
||||
_jpegxl.JpegXlDecoder()
|
||||
80
Tests/test_file_jxl_animated.py
Normal file
80
Tests/test_file_jxl_animated.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import GifImagePlugin, Image, JpegXlImagePlugin
|
||||
|
||||
from .helper import assert_image_equal, skip_unless_feature
|
||||
|
||||
pytestmark = skip_unless_feature("jpegxl")
|
||||
|
||||
|
||||
def test_n_frames() -> None:
|
||||
"""Ensure that jxl format sets n_frames and is_animated attributes correctly."""
|
||||
|
||||
with Image.open("Tests/images/jxl/hopper.jxl") as im:
|
||||
assert isinstance(im, JpegXlImagePlugin.JpegXlImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open("Tests/images/jxl/iss634.jxl") as im:
|
||||
assert isinstance(im, JpegXlImagePlugin.JpegXlImageFile)
|
||||
assert im.n_frames == 41
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_duration() -> None:
|
||||
with Image.open("Tests/images/jxl/iss634.jxl") as im:
|
||||
assert im.info["duration"] == 70
|
||||
assert im.info["timestamp"] == 0
|
||||
|
||||
im.seek(2)
|
||||
assert im.info["duration"] == 60
|
||||
assert im.info["timestamp"] == 140
|
||||
|
||||
|
||||
def test_seek() -> None:
|
||||
"""
|
||||
Open an animated jxl file, and then try seeking through frames in reverse-order,
|
||||
verifying the durations are correct.
|
||||
"""
|
||||
|
||||
with Image.open("Tests/images/jxl/traffic_light.jxl") as im1:
|
||||
with Image.open("Tests/images/jxl/traffic_light.gif") as im2:
|
||||
assert isinstance(im1, JpegXlImagePlugin.JpegXlImageFile)
|
||||
assert isinstance(im2, GifImagePlugin.GifImageFile)
|
||||
assert im1.n_frames == im2.n_frames
|
||||
assert im1.is_animated
|
||||
|
||||
# Traverse frames in reverse, checking timestamps and durations
|
||||
total_duration = 0
|
||||
for frame in reversed(range(im1.n_frames)):
|
||||
im1.seek(frame)
|
||||
im2.seek(frame)
|
||||
|
||||
assert_image_equal(im1.convert("RGB"), im2.convert("RGB"))
|
||||
|
||||
total_duration += im1.info["duration"]
|
||||
assert im1.info["duration"] == im2.info["duration"]
|
||||
assert im1.info["timestamp"] == im1.info["timestamp"]
|
||||
assert total_duration == 8000
|
||||
|
||||
assert im1.tell() == 0
|
||||
assert im2.tell() == 0
|
||||
|
||||
im1.seek(0)
|
||||
im1.load()
|
||||
im2.seek(0)
|
||||
im2.load()
|
||||
|
||||
|
||||
def test_seek_errors() -> None:
|
||||
with Image.open("Tests/images/jxl/iss634.jxl") as im:
|
||||
with pytest.raises(EOFError, match="attempt to seek outside sequence"):
|
||||
im.seek(-1)
|
||||
|
||||
im.seek(1)
|
||||
with pytest.raises(EOFError, match="no more images in JPEG XL file"):
|
||||
im.seek(47)
|
||||
|
||||
assert im.tell() == 1
|
||||
114
Tests/test_file_jxl_metadata.py
Normal file
114
Tests/test_file_jxl_metadata.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, JpegXlImagePlugin
|
||||
|
||||
from .helper import skip_unless_feature
|
||||
|
||||
pytestmark = skip_unless_feature("jpegxl")
|
||||
|
||||
ElementTree: ModuleType | None
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
ElementTree = None
|
||||
|
||||
|
||||
# cjxl flower.jpg flower.jxl --lossless_jpeg=0 -q 75 -e 8
|
||||
|
||||
# >>> from PIL import Image
|
||||
# >>> with Image.open('Tests/images/flower2.webp') as im:
|
||||
# >>> with open('/tmp/xmp.xml', 'wb') as f:
|
||||
# >>> f.write(im.info['xmp'])
|
||||
# cjxl flower2.jpg flower2.jxl --lossless_jpeg=0 -q 75 -e 8 -x xmp=/tmp/xmp.xml
|
||||
|
||||
|
||||
def test_read_exif_metadata() -> None:
|
||||
with Image.open("Tests/images/jxl/flower.jxl") as im:
|
||||
assert im.format == "JPEG XL"
|
||||
exif_data = im.info["exif"]
|
||||
|
||||
exif = im.getexif()
|
||||
|
||||
# Camera make
|
||||
assert exif[271] == "Canon"
|
||||
|
||||
with Image.open("Tests/images/flower.jpg") as im_jpeg:
|
||||
expected_exif = im_jpeg.info["exif"]
|
||||
|
||||
# JPEG XL always returns exif without "Exif\x00\x00" prefix
|
||||
assert exif_data == expected_exif[6:]
|
||||
|
||||
|
||||
def test_read_exif_metadata_without_prefix() -> None:
|
||||
with Image.open("Tests/images/jxl/flower2.jxl") as im:
|
||||
# Assert prefix is not present
|
||||
assert im.info["exif"][:6] != b"Exif\x00\x00"
|
||||
|
||||
exif = im.getexif()
|
||||
assert exif[305] == "Adobe Photoshop CS6 (Macintosh)"
|
||||
|
||||
|
||||
def test_read_icc_profile() -> None:
|
||||
with Image.open("Tests/images/jxl/flower.jxl") as im:
|
||||
assert "icc_profile" in im.info
|
||||
|
||||
|
||||
def test_getxmp() -> None:
|
||||
with Image.open("Tests/images/jxl/flower.jxl") as im:
|
||||
assert "xmp" not in im.info
|
||||
if ElementTree is None:
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="XMP data cannot be read without defusedxml dependency",
|
||||
):
|
||||
xmp = im.getxmp()
|
||||
else:
|
||||
xmp = im.getxmp()
|
||||
assert xmp == {}
|
||||
|
||||
with Image.open("Tests/images/jxl/flower2.jxl") as im:
|
||||
if ElementTree is None:
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="XMP data cannot be read without defusedxml dependency",
|
||||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
assert (
|
||||
im.getxmp()["xmpmeta"]["xmptk"]
|
||||
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
|
||||
)
|
||||
|
||||
|
||||
def test_4_byte_exif(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class _mock_jpegxl:
|
||||
class JpegXlDecoder:
|
||||
def __init__(self, b: bytes) -> None:
|
||||
pass
|
||||
|
||||
def get_info(self) -> tuple[tuple[int, int], str, int, int, int, int, int]:
|
||||
return ((1, 1), "L", 0, 0, 0, 0, 0)
|
||||
|
||||
def get_icc(self) -> None:
|
||||
pass
|
||||
|
||||
def get_exif(self) -> bytes:
|
||||
return b"\0\0\0\0"
|
||||
|
||||
def get_xmp(self) -> None:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(JpegXlImagePlugin, "_jpegxl", _mock_jpegxl)
|
||||
|
||||
with Image.open("Tests/images/jxl/hopper.jxl") as im:
|
||||
assert "exif" not in im.info
|
||||
|
||||
|
||||
def test_read_exif_metadata_empty() -> None:
|
||||
with Image.open("Tests/images/jxl/hopper.jxl") as im:
|
||||
assert im.getexif() == {}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
|
|
@ -7,7 +8,15 @@ from PIL import features
|
|||
|
||||
|
||||
def test_wheel_modules() -> None:
|
||||
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"}
|
||||
expected_modules = {
|
||||
"pil",
|
||||
"tkinter",
|
||||
"freetype2",
|
||||
"littlecms2",
|
||||
"webp",
|
||||
"avif",
|
||||
"jpegxl",
|
||||
}
|
||||
|
||||
if sys.platform == "win32":
|
||||
# tkinter is not available in cibuildwheel installed CPython on Windows
|
||||
|
|
@ -18,13 +27,18 @@ def test_wheel_modules() -> None:
|
|||
except ImportError:
|
||||
expected_modules.remove("tkinter")
|
||||
|
||||
if hasattr(sys, "pypy_translation_info"):
|
||||
expected_modules.remove("jpegxl")
|
||||
|
||||
# libavif is not available on Windows for ARM64 architectures
|
||||
if platform.machine() == "ARM64":
|
||||
expected_modules.remove("avif")
|
||||
|
||||
elif sys.platform == "ios":
|
||||
# tkinter is not available on iOS
|
||||
expected_modules.remove("tkinter")
|
||||
expected_modules -= {"tkinter", "jpegxl"}
|
||||
elif os.environ.get("AUDITWHEEL_POLICY") == "manylinux2014":
|
||||
expected_modules.remove("jpegxl")
|
||||
|
||||
assert set(features.get_supported_modules()) == expected_modules
|
||||
|
||||
|
|
|
|||
17
depends/install_libjxl.sh
Executable file
17
depends/install_libjxl.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
|
||||
version=0.11.1
|
||||
|
||||
./download-and-extract.sh highway-1.3.0 https://github.com/google/highway/archive/1.3.0.tar.gz
|
||||
|
||||
pushd highway-1.3.0
|
||||
cmake .
|
||||
make -j4 install
|
||||
popd
|
||||
|
||||
./download-and-extract.sh libjxl-$version https://github.com/libjxl/libjxl/archive/v$version.tar.gz
|
||||
|
||||
pushd libjxl-$version
|
||||
cmake -DCMAKE_INSTALL_PREFIX=/usr -DJPEGXL_ENABLE_SJPEG=OFF -DJPEGXL_ENABLE_SKCMS=OFF -DBUILD_TESTING=OFF .
|
||||
make -j4 install
|
||||
popd
|
||||
|
|
@ -1568,6 +1568,31 @@ IPTC/NAA
|
|||
|
||||
Pillow provides limited read support for IPTC/NAA newsphoto files.
|
||||
|
||||
JPEG XL
|
||||
^^^^^^^
|
||||
|
||||
Pillow identifies and reads JPEG XL files. Requires libjxl version **0.9.0** or
|
||||
greater.
|
||||
|
||||
The :py:meth:`~PIL.Image.open` method sets the following
|
||||
:py:attr:`~PIL.Image.Image.info` properties:
|
||||
|
||||
**duration**
|
||||
The delay (in milliseconds) between each frame.
|
||||
|
||||
**exif**
|
||||
Raw EXIF data from the image.
|
||||
|
||||
**icc_profile**
|
||||
The ICC color profile for the image.
|
||||
|
||||
**timestamp**
|
||||
The time of the current frame. This is the sum of the duration of all previous
|
||||
frames.
|
||||
|
||||
**xmp**
|
||||
Raw XMP data from the image.
|
||||
|
||||
MCIDAS
|
||||
^^^^^^
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,10 @@ Many of Pillow's features require external libraries:
|
|||
and decoder, such as rav1e and dav1d, or libaom, which both encodes and
|
||||
decodes AVIF images.
|
||||
|
||||
* **libjxl** provides support for the JPEG XL format.
|
||||
|
||||
* Pillow requires libjxl version **0.9.0** or greater.
|
||||
|
||||
.. tab:: Linux
|
||||
|
||||
If you didn't build Python from source, make sure you have Python's
|
||||
|
|
@ -125,6 +129,8 @@ Many of Pillow's features require external libraries:
|
|||
To install libraqm, ``sudo apt-get install meson`` and then see
|
||||
``depends/install_raqm.sh``.
|
||||
|
||||
To install libjxl, see ``depends/install_libjxl.sh``.
|
||||
|
||||
Build prerequisites for libavif on Ubuntu are installed with::
|
||||
|
||||
sudo apt-get install cmake ninja-build nasm
|
||||
|
|
@ -162,7 +168,7 @@ Many of Pillow's features require external libraries:
|
|||
The easiest way to install external libraries is via `Homebrew
|
||||
<https://brew.sh/>`_. After you install Homebrew, run::
|
||||
|
||||
brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp
|
||||
brew install jpeg-xl libavif libjpeg libraqm libtiff little-cms2 openjpeg webp
|
||||
|
||||
If you would like to use libavif with more codecs than just aom, then
|
||||
instead of installing libavif through Homebrew directly, you can use
|
||||
|
|
@ -202,6 +208,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
pacman -S \
|
||||
mingw-w64-x86_64-libjpeg-turbo \
|
||||
mingw-w64-x86_64-libjxl \
|
||||
mingw-w64-x86_64-zlib \
|
||||
mingw-w64-x86_64-libtiff \
|
||||
mingw-w64-x86_64-freetype \
|
||||
|
|
@ -284,7 +291,7 @@ Build options
|
|||
``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
|
||||
``-C lcms=disable``, ``-C webp=disable``,
|
||||
``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``,
|
||||
``-C avif=disable``.
|
||||
``-C avif=disable``, ``-C jpegxl=disable``.
|
||||
Disable building the corresponding feature even if the development
|
||||
libraries are present on the building machine.
|
||||
|
||||
|
|
@ -292,7 +299,7 @@ Build options
|
|||
``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``,
|
||||
``-C lcms=enable``, ``-C webp=enable``,
|
||||
``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``,
|
||||
``-C avif=enable``.
|
||||
``-C avif=enable``, ``-C jpegxl=enable``.
|
||||
Require that the corresponding feature is built. The build will raise
|
||||
an exception if the libraries are not found. Tcl and Tk must be used
|
||||
together.
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ Support for the following modules can be checked:
|
|||
* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`.
|
||||
* ``webp``: WebP image support.
|
||||
* ``avif``: AVIF image support.
|
||||
* ``jpegxl``: JPEG XL image support.
|
||||
|
||||
.. autofunction:: PIL.features.check_module
|
||||
.. autofunction:: PIL.features.version_module
|
||||
|
|
|
|||
|
|
@ -169,6 +169,14 @@ Plugin reference
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`~PIL.JpegXlImagePlugin` module
|
||||
------------------------------------
|
||||
|
||||
.. automodule:: PIL.JpegXlImagePlugin
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`~PIL.McIdasImagePlugin` module
|
||||
------------------------------------
|
||||
|
||||
|
|
|
|||
31
setup.py
31
setup.py
|
|
@ -48,8 +48,9 @@ FREETYPE_ROOT = None
|
|||
HARFBUZZ_ROOT = None
|
||||
FRIBIDI_ROOT = None
|
||||
IMAGEQUANT_ROOT = None
|
||||
JPEG2K_ROOT = None
|
||||
JPEG_ROOT = None
|
||||
JPEG2K_ROOT = None
|
||||
JPEGXL_ROOT = None
|
||||
LCMS_ROOT = None
|
||||
RAQM_ROOT = None
|
||||
TIFF_ROOT = None
|
||||
|
|
@ -312,6 +313,7 @@ class pil_build_ext(build_ext):
|
|||
features = [
|
||||
"zlib",
|
||||
"jpeg",
|
||||
"jpegxl",
|
||||
"tiff",
|
||||
"freetype",
|
||||
"raqm",
|
||||
|
|
@ -441,6 +443,7 @@ class pil_build_ext(build_ext):
|
|||
libraries: list[str] | list[str | bool | None],
|
||||
define_macros: list[tuple[str, str | None]] | None = None,
|
||||
sources: list[str] | None = None,
|
||||
args: list[str] | None = None,
|
||||
) -> None:
|
||||
for extension in self.extensions:
|
||||
if extension.name == name:
|
||||
|
|
@ -449,6 +452,8 @@ class pil_build_ext(build_ext):
|
|||
extension.define_macros += define_macros
|
||||
if sources is not None:
|
||||
extension.sources += sources
|
||||
if args is not None:
|
||||
extension.extra_compile_args += args
|
||||
if FUZZING_BUILD:
|
||||
extension.language = "c++"
|
||||
extension.extra_link_args = ["--stdlib=libc++"]
|
||||
|
|
@ -508,6 +513,7 @@ class pil_build_ext(build_ext):
|
|||
"AVIF_ROOT": "avif",
|
||||
"JPEG_ROOT": "libjpeg",
|
||||
"JPEG2K_ROOT": "libopenjp2",
|
||||
"JPEGXL_ROOT": "jxl",
|
||||
"TIFF_ROOT": ("libtiff-5", "libtiff-4"),
|
||||
"ZLIB_ROOT": "zlib",
|
||||
"FREETYPE_ROOT": "freetype2",
|
||||
|
|
@ -775,6 +781,16 @@ class pil_build_ext(build_ext):
|
|||
feature.set("jpeg2000", "openjp2")
|
||||
feature.set("openjpeg_version", ".".join(str(x) for x in best_version))
|
||||
|
||||
if feature.want("jpegxl"):
|
||||
_dbg("Looking for jpegxl")
|
||||
if _find_include_file(self, "jxl/decode.h") and _find_include_file(
|
||||
self, "jxl/thread_parallel_runner.h"
|
||||
):
|
||||
if _find_library_file(self, "jxl") and _find_library_file(
|
||||
self, "jxl_threads"
|
||||
):
|
||||
feature.set("jpegxl", "jxl")
|
||||
|
||||
if feature.want("imagequant"):
|
||||
_dbg("Looking for imagequant")
|
||||
if _find_include_file(self, "libimagequant.h"):
|
||||
|
|
@ -998,6 +1014,17 @@ class pil_build_ext(build_ext):
|
|||
else:
|
||||
self._remove_extension("PIL._avif")
|
||||
|
||||
jpegxl = feature.get("jpegxl")
|
||||
if isinstance(jpegxl, str):
|
||||
libs = [jpegxl, jpegxl + "_threads"]
|
||||
args: list[str] | None = None
|
||||
if sys.platform == "win32":
|
||||
libs.extend(["brotlicommon", "brotlidec", "brotlienc", "hwy"])
|
||||
args = ["-DJXL_STATIC_DEFINE"]
|
||||
self._update_extension("PIL._jpegxl", libs, args=args)
|
||||
else:
|
||||
self._remove_extension("PIL._jpegxl")
|
||||
|
||||
tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else []
|
||||
self._update_extension("PIL._imagingtk", tk_libs)
|
||||
|
||||
|
|
@ -1038,6 +1065,7 @@ class pil_build_ext(build_ext):
|
|||
(feature.get("freetype"), "FREETYPE2"),
|
||||
(feature.get("raqm"), "RAQM (Text shaping)", raqm_extra_info),
|
||||
(feature.get("lcms"), "LITTLECMS2"),
|
||||
(feature.get("jpegxl"), "JPEG XL"),
|
||||
(feature.get("webp"), "WEBP"),
|
||||
(feature.get("xcb"), "XCB (X protocol)"),
|
||||
(feature.get("avif"), "LIBAVIF"),
|
||||
|
|
@ -1086,6 +1114,7 @@ ext_modules = [
|
|||
Extension("PIL._imaging", files),
|
||||
Extension("PIL._imagingft", ["src/_imagingft.c"]),
|
||||
Extension("PIL._imagingcms", ["src/_imagingcms.c"]),
|
||||
Extension("PIL._jpegxl", ["src/_jpegxl.c"]),
|
||||
Extension("PIL._webp", ["src/_webp.c"]),
|
||||
Extension("PIL._avif", ["src/_avif.c"]),
|
||||
Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]),
|
||||
|
|
|
|||
122
src/PIL/JpegXlImagePlugin.py
Normal file
122
src/PIL/JpegXlImagePlugin.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
try:
|
||||
from . import _jpegxl
|
||||
|
||||
SUPPORTED = True
|
||||
except ImportError:
|
||||
SUPPORTED = False
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool | str:
|
||||
is_jxl = prefix.startswith(
|
||||
(b"\xff\x0a", b"\x00\x00\x00\x0c\x4a\x58\x4c\x20\x0d\x0a\x87\x0a")
|
||||
)
|
||||
if is_jxl and not SUPPORTED:
|
||||
return "image file could not be identified because JXL support not installed"
|
||||
return is_jxl
|
||||
|
||||
|
||||
class JpegXlImageFile(ImageFile.ImageFile):
|
||||
format = "JPEG XL"
|
||||
format_description = "JPEG XL image"
|
||||
__frame = 0
|
||||
|
||||
def _open(self) -> None:
|
||||
assert self.fp is not None
|
||||
self._decoder = _jpegxl.JpegXlDecoder(self.fp.read())
|
||||
|
||||
(
|
||||
self._size,
|
||||
self._mode,
|
||||
self.is_animated,
|
||||
tps_num,
|
||||
tps_denom,
|
||||
self.info["loop"],
|
||||
tps_duration,
|
||||
) = self._decoder.get_info()
|
||||
|
||||
self._n_frames = None if self.is_animated else 1
|
||||
self._tps_dur_secs = tps_num / tps_denom if tps_denom != 0 else 1
|
||||
self.info["duration"] = 1000 * tps_duration * (1 / self._tps_dur_secs)
|
||||
self.info["timestamp"] = 0
|
||||
|
||||
if icc := self._decoder.get_icc():
|
||||
self.info["icc_profile"] = icc
|
||||
if exif := self._decoder.get_exif():
|
||||
# JPEG XL does some weird shenanigans when storing exif
|
||||
# it omits first 6 bytes of tiff header but adds 4 byte offset instead
|
||||
if len(exif) > 4:
|
||||
exif_start_offset = struct.unpack(">I", exif[:4])[0]
|
||||
self.info["exif"] = exif[exif_start_offset + 4 :]
|
||||
if xmp := self._decoder.get_xmp():
|
||||
self.info["xmp"] = xmp
|
||||
rawmode = "L" if self.mode == "1" else self.mode
|
||||
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, rawmode)]
|
||||
|
||||
@property
|
||||
def n_frames(self) -> int:
|
||||
if self._n_frames is None:
|
||||
current = self.tell()
|
||||
self._n_frames = current + self._decoder.get_frames_left()
|
||||
self.seek(current)
|
||||
|
||||
return self._n_frames
|
||||
|
||||
def _get_next(self) -> bytes:
|
||||
data, tps_duration, is_last = self._decoder.get_next()
|
||||
|
||||
if is_last and self._n_frames is None:
|
||||
self._n_frames = self.__frame
|
||||
|
||||
# duration in milliseconds
|
||||
self.info["timestamp"] += self.info["duration"]
|
||||
self.info["duration"] = 1000 * tps_duration * (1 / self._tps_dur_secs)
|
||||
|
||||
return data
|
||||
|
||||
def seek(self, frame: int) -> None:
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
|
||||
if frame < self.__frame:
|
||||
self.__frame = 0
|
||||
self._decoder.rewind()
|
||||
self.info["timestamp"] = 0
|
||||
|
||||
last_frame = self.__frame
|
||||
while self.__frame < frame:
|
||||
self._get_next()
|
||||
self.__frame += 1
|
||||
if self._n_frames is not None and self._n_frames < frame:
|
||||
self.seek(last_frame)
|
||||
msg = "no more images in JPEG XL file"
|
||||
raise EOFError(msg)
|
||||
|
||||
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
|
||||
|
||||
def load(self) -> Image.core.PixelAccess | None:
|
||||
if self.tile:
|
||||
data = self._get_next()
|
||||
|
||||
if self.fp and self._exclusive_fp:
|
||||
self.fp.close()
|
||||
self.fp = BytesIO(data)
|
||||
|
||||
return super().load()
|
||||
|
||||
def load_seek(self, pos: int) -> None:
|
||||
pass
|
||||
|
||||
def tell(self) -> int:
|
||||
return self.__frame
|
||||
|
||||
|
||||
Image.register_open(JpegXlImageFile.format, JpegXlImageFile, _accept)
|
||||
Image.register_extension(JpegXlImageFile.format, ".jxl")
|
||||
Image.register_mime(JpegXlImageFile.format, "image/jxl")
|
||||
|
|
@ -48,6 +48,7 @@ _plugins = [
|
|||
"IptcImagePlugin",
|
||||
"JpegImagePlugin",
|
||||
"Jpeg2KImagePlugin",
|
||||
"JpegXlImagePlugin",
|
||||
"McIdasImagePlugin",
|
||||
"MicImagePlugin",
|
||||
"MpegImagePlugin",
|
||||
|
|
|
|||
3
src/PIL/_jpegxl.pyi
Normal file
3
src/PIL/_jpegxl.pyi
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from typing import Any
|
||||
|
||||
def __getattr__(name: str) -> Any: ...
|
||||
|
|
@ -17,6 +17,7 @@ modules = {
|
|||
"littlecms2": ("PIL._imagingcms", "littlecms_version"),
|
||||
"webp": ("PIL._webp", "webpdecoder_version"),
|
||||
"avif": ("PIL._avif", "libavif_version"),
|
||||
"jpegxl": ("PIL._jpegxl", "libjxl_version"),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -272,6 +273,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
|
|||
("littlecms2", "LITTLECMS2"),
|
||||
("webp", "WEBP"),
|
||||
("avif", "AVIF"),
|
||||
("jpegxl", "JPEG XL"),
|
||||
("jpg", "JPEG"),
|
||||
("jpg_2000", "OPENJPEG (JPEG2000)"),
|
||||
("zlib", "ZLIB (PNG/ZIP)"),
|
||||
|
|
|
|||
563
src/_jpegxl.c
Normal file
563
src/_jpegxl.c
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
#include "libImaging/Imaging.h"
|
||||
|
||||
#include <jxl/decode.h>
|
||||
#include <jxl/thread_parallel_runner.h>
|
||||
|
||||
#define _JXL_CHECK(call_name) \
|
||||
if (decp->status != JXL_DEC_SUCCESS) { \
|
||||
jxl_call_name = call_name; \
|
||||
goto end; \
|
||||
}
|
||||
|
||||
void
|
||||
_jxl_get_pixel_format(JxlPixelFormat *pf, const JxlBasicInfo *bi) {
|
||||
pf->num_channels = bi->num_color_channels + bi->num_extra_channels;
|
||||
pf->data_type = bi->bits_per_sample > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8;
|
||||
pf->align = 0;
|
||||
}
|
||||
|
||||
char *
|
||||
_jxl_get_mode(const JxlBasicInfo *bi) {
|
||||
if (bi->num_color_channels == 1 && !bi->alpha_bits) {
|
||||
if (bi->bits_per_sample == 1) {
|
||||
return "1";
|
||||
}
|
||||
if (bi->bits_per_sample == 16) {
|
||||
return "I;16";
|
||||
}
|
||||
}
|
||||
|
||||
if (bi->bits_per_sample == 8) {
|
||||
if (bi->alpha_bits) {
|
||||
// image has transparency
|
||||
if (bi->num_color_channels == 3) {
|
||||
return bi->alpha_premultiplied ? "RGBa" : "RGBA";
|
||||
}
|
||||
} else {
|
||||
// image has no transparency
|
||||
if (bi->num_color_channels == 3) {
|
||||
return "RGB";
|
||||
}
|
||||
if (bi->num_color_channels == 1) {
|
||||
return "L";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// could not recognize mode
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Decoder type
|
||||
typedef struct {
|
||||
PyObject_HEAD JxlDecoder *decoder;
|
||||
void *runner;
|
||||
|
||||
uint8_t *jxl_data; // input jxl bitstream
|
||||
Py_ssize_t jxl_data_len; // length of input jxl bitstream
|
||||
|
||||
uint8_t *output_buffer;
|
||||
size_t output_buffer_len;
|
||||
|
||||
uint8_t *jxl_icc;
|
||||
size_t jxl_icc_len;
|
||||
uint8_t *jxl_exif;
|
||||
Py_ssize_t jxl_exif_len;
|
||||
uint8_t *jxl_xmp;
|
||||
Py_ssize_t jxl_xmp_len;
|
||||
|
||||
JxlDecoderStatus status;
|
||||
JxlBasicInfo basic_info;
|
||||
JxlPixelFormat pixel_format;
|
||||
|
||||
char *mode;
|
||||
} JpegXlDecoderObject;
|
||||
|
||||
static PyTypeObject JpegXlDecoder_Type;
|
||||
|
||||
void
|
||||
_jxl_decoder_dealloc(PyObject *self) {
|
||||
JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
|
||||
|
||||
if (decp->jxl_data) {
|
||||
free(decp->jxl_data);
|
||||
decp->jxl_data = NULL;
|
||||
decp->jxl_data_len = 0;
|
||||
}
|
||||
if (decp->output_buffer) {
|
||||
free(decp->output_buffer);
|
||||
decp->output_buffer = NULL;
|
||||
decp->output_buffer_len = 0;
|
||||
}
|
||||
if (decp->jxl_icc) {
|
||||
free(decp->jxl_icc);
|
||||
decp->jxl_icc = NULL;
|
||||
decp->jxl_icc_len = 0;
|
||||
}
|
||||
if (decp->jxl_exif) {
|
||||
free(decp->jxl_exif);
|
||||
decp->jxl_exif = NULL;
|
||||
decp->jxl_exif_len = 0;
|
||||
}
|
||||
if (decp->jxl_xmp) {
|
||||
free(decp->jxl_xmp);
|
||||
decp->jxl_xmp = NULL;
|
||||
decp->jxl_xmp_len = 0;
|
||||
}
|
||||
|
||||
if (decp->decoder) {
|
||||
JxlDecoderDestroy(decp->decoder);
|
||||
decp->decoder = NULL;
|
||||
}
|
||||
|
||||
if (decp->runner) {
|
||||
JxlThreadParallelRunnerDestroy(decp->runner);
|
||||
decp->runner = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// sets input jxl bitstream loaded into jxl_data
|
||||
void
|
||||
_jxl_decoder_set_input(PyObject *self) {
|
||||
JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
|
||||
|
||||
decp->status =
|
||||
JxlDecoderSetInput(decp->decoder, decp->jxl_data, decp->jxl_data_len);
|
||||
|
||||
// the input contains the whole jxl bitstream so it can be closed
|
||||
JxlDecoderCloseInput(decp->decoder);
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_rewind(PyObject *self) {
|
||||
JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
|
||||
JxlDecoderRewind(decp->decoder);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_get_frames_left(PyObject *self) {
|
||||
int frames_left = 0;
|
||||
|
||||
// count all JXL_DEC_NEED_IMAGE_OUT_BUFFER events
|
||||
JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
|
||||
while (decp->status != JXL_DEC_SUCCESS) {
|
||||
decp->status = JxlDecoderProcessInput(decp->decoder);
|
||||
|
||||
if (decp->status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
|
||||
if (JxlDecoderSkipCurrentFrame(decp->decoder) != JXL_DEC_SUCCESS) {
|
||||
PyErr_SetString(PyExc_OSError, "Error when counting frames");
|
||||
break;
|
||||
}
|
||||
frames_left++;
|
||||
}
|
||||
}
|
||||
JxlDecoderRewind(decp->decoder);
|
||||
|
||||
return Py_BuildValue("i", frames_left);
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_new(PyObject *self, PyObject *args) {
|
||||
PyBytesObject *jxl_string;
|
||||
|
||||
// parse one argument which is a string with jxl data
|
||||
if (!PyArg_ParseTuple(args, "O", &jxl_string)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JpegXlDecoderObject *decp = NULL;
|
||||
decp = PyObject_New(JpegXlDecoderObject, &JpegXlDecoder_Type);
|
||||
decp->jxl_data = NULL;
|
||||
decp->jxl_data_len = 0;
|
||||
decp->output_buffer = NULL;
|
||||
decp->output_buffer_len = 0;
|
||||
decp->jxl_icc = NULL;
|
||||
decp->jxl_icc_len = 0;
|
||||
decp->jxl_exif = NULL;
|
||||
decp->jxl_exif_len = 0;
|
||||
decp->jxl_xmp = NULL;
|
||||
decp->jxl_xmp_len = 0;
|
||||
decp->mode = NULL;
|
||||
|
||||
// used for printing more detailed error messages
|
||||
char *jxl_call_name;
|
||||
|
||||
// this data needs to be copied to JpegXlDecoderObject
|
||||
// so that input bitstream is preserved across calls
|
||||
const uint8_t *_tmp_jxl_data;
|
||||
Py_ssize_t _tmp_jxl_data_len;
|
||||
|
||||
// convert jxl data string to C uint8_t pointer
|
||||
PyBytes_AsStringAndSize(
|
||||
(PyObject *)jxl_string, (char **)&_tmp_jxl_data, &_tmp_jxl_data_len
|
||||
);
|
||||
|
||||
decp->jxl_data = malloc(_tmp_jxl_data_len);
|
||||
memcpy(decp->jxl_data, _tmp_jxl_data, _tmp_jxl_data_len);
|
||||
decp->jxl_data_len = _tmp_jxl_data_len;
|
||||
|
||||
size_t suggested_num_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads();
|
||||
decp->runner = JxlThreadParallelRunnerCreate(NULL, suggested_num_threads);
|
||||
decp->decoder = JxlDecoderCreate(NULL);
|
||||
|
||||
decp->status = JxlDecoderSetParallelRunner(
|
||||
decp->decoder, JxlThreadParallelRunner, decp->runner
|
||||
);
|
||||
_JXL_CHECK("JxlDecoderSetParallelRunner")
|
||||
|
||||
decp->status = JxlDecoderSubscribeEvents(
|
||||
decp->decoder,
|
||||
JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_BOX |
|
||||
JXL_DEC_FULL_IMAGE
|
||||
);
|
||||
_JXL_CHECK("JxlDecoderSubscribeEvents")
|
||||
|
||||
// tell libjxl to decompress boxes (for example Exif is usually compressed)
|
||||
decp->status = JxlDecoderSetDecompressBoxes(decp->decoder, JXL_TRUE);
|
||||
_JXL_CHECK("JxlDecoderSetDecompressBoxes")
|
||||
|
||||
_jxl_decoder_set_input((PyObject *)decp);
|
||||
_JXL_CHECK("JxlDecoderSetInput")
|
||||
|
||||
// decode everything up to the first frame
|
||||
do {
|
||||
decp->status = JxlDecoderProcessInput(decp->decoder);
|
||||
|
||||
decoder_loop_skip_process:
|
||||
|
||||
if (decp->status == JXL_DEC_ERROR) {
|
||||
jxl_call_name = "JxlDecoderProcessInput";
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (decp->status == JXL_DEC_BASIC_INFO) {
|
||||
decp->status = JxlDecoderGetBasicInfo(decp->decoder, &decp->basic_info);
|
||||
_JXL_CHECK("JxlDecoderGetBasicInfo");
|
||||
|
||||
_jxl_get_pixel_format(&decp->pixel_format, &decp->basic_info);
|
||||
decp->mode = _jxl_get_mode(&decp->basic_info);
|
||||
} else if (decp->status == JXL_DEC_COLOR_ENCODING) {
|
||||
decp->status = JxlDecoderGetICCProfileSize(
|
||||
decp->decoder, JXL_COLOR_PROFILE_TARGET_DATA, &decp->jxl_icc_len
|
||||
);
|
||||
_JXL_CHECK("JxlDecoderGetICCProfileSize");
|
||||
|
||||
decp->jxl_icc = malloc(decp->jxl_icc_len);
|
||||
if (!decp->jxl_icc) {
|
||||
PyErr_SetString(PyExc_OSError, "jxl_icc malloc failed");
|
||||
goto end_with_custom_error;
|
||||
}
|
||||
|
||||
decp->status = JxlDecoderGetColorAsICCProfile(
|
||||
decp->decoder,
|
||||
JXL_COLOR_PROFILE_TARGET_DATA,
|
||||
decp->jxl_icc,
|
||||
decp->jxl_icc_len
|
||||
);
|
||||
_JXL_CHECK("JxlDecoderGetColorAsICCProfile");
|
||||
} else if (decp->status == JXL_DEC_BOX) {
|
||||
char box_type[4];
|
||||
decp->status = JxlDecoderGetBoxType(decp->decoder, box_type, JXL_TRUE);
|
||||
_JXL_CHECK("JxlDecoderGetBoxType");
|
||||
|
||||
int is_box_exif = !memcmp(box_type, "Exif", 4);
|
||||
int is_box_xmp = is_box_exif ? 0 : !memcmp(box_type, "xml ", 4);
|
||||
if (!is_box_exif && !is_box_xmp) {
|
||||
// not exif/xmp box so continue
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t compressed_box_size;
|
||||
decp->status = JxlDecoderGetBoxSizeRaw(decp->decoder, &compressed_box_size);
|
||||
_JXL_CHECK("JxlDecoderGetBoxSizeRaw");
|
||||
|
||||
uint8_t *final_jxl_buf = NULL;
|
||||
Py_ssize_t final_jxl_buf_len = 0;
|
||||
|
||||
do {
|
||||
uint8_t *_new_jxl_buf =
|
||||
realloc(final_jxl_buf, final_jxl_buf_len + compressed_box_size);
|
||||
if (!_new_jxl_buf) {
|
||||
PyErr_SetString(PyExc_OSError, "failed to allocate final_jxl_buf");
|
||||
goto end_with_custom_error;
|
||||
}
|
||||
final_jxl_buf = _new_jxl_buf;
|
||||
|
||||
decp->status = JxlDecoderSetBoxBuffer(
|
||||
decp->decoder,
|
||||
final_jxl_buf + final_jxl_buf_len,
|
||||
compressed_box_size
|
||||
);
|
||||
_JXL_CHECK("JxlDecoderSetBoxBuffer");
|
||||
|
||||
decp->status = JxlDecoderProcessInput(decp->decoder);
|
||||
|
||||
size_t remaining = JxlDecoderReleaseBoxBuffer(decp->decoder);
|
||||
final_jxl_buf_len += compressed_box_size - remaining;
|
||||
} while (decp->status == JXL_DEC_BOX_NEED_MORE_OUTPUT);
|
||||
|
||||
if (is_box_exif) {
|
||||
decp->jxl_exif = final_jxl_buf;
|
||||
decp->jxl_exif_len = final_jxl_buf_len;
|
||||
} else {
|
||||
decp->jxl_xmp = final_jxl_buf;
|
||||
decp->jxl_xmp_len = final_jxl_buf_len;
|
||||
}
|
||||
|
||||
// dirty hack: skip first step of decoding loop since
|
||||
// we already did it in do...while above
|
||||
goto decoder_loop_skip_process;
|
||||
}
|
||||
|
||||
} while (decp->status != JXL_DEC_FRAME);
|
||||
|
||||
return (PyObject *)decp;
|
||||
|
||||
// on success we should never reach here
|
||||
|
||||
// set error message
|
||||
char err_msg[128];
|
||||
|
||||
end:
|
||||
snprintf(
|
||||
err_msg,
|
||||
128,
|
||||
"could not create decoder object. libjxl call: %s returned: %d",
|
||||
jxl_call_name,
|
||||
decp->status
|
||||
);
|
||||
PyErr_SetString(PyExc_OSError, err_msg);
|
||||
|
||||
end_with_custom_error:
|
||||
|
||||
// deallocate
|
||||
_jxl_decoder_dealloc((PyObject *)decp);
|
||||
PyObject_Del(decp);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_get_info(PyObject *self) {
|
||||
JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
|
||||
JxlFrameHeader fhdr = {};
|
||||
if (JxlDecoderGetFrameHeader(decp->decoder, &fhdr) != JXL_DEC_SUCCESS) {
|
||||
PyErr_SetString(PyExc_OSError, "Error determining duration");
|
||||
return NULL;
|
||||
}
|
||||
return Py_BuildValue(
|
||||
"(II)sOIIII",
|
||||
decp->basic_info.xsize,
|
||||
decp->basic_info.ysize,
|
||||
decp->mode,
|
||||
decp->basic_info.have_animation ? Py_True : Py_False,
|
||||
decp->basic_info.animation.tps_numerator,
|
||||
decp->basic_info.animation.tps_denominator,
|
||||
decp->basic_info.animation.num_loops,
|
||||
fhdr.duration
|
||||
);
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_get_next(PyObject *self) {
|
||||
JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
|
||||
PyObject *bytes;
|
||||
PyObject *ret;
|
||||
JxlFrameHeader fhdr = {};
|
||||
|
||||
char *jxl_call_name;
|
||||
|
||||
// process events until next frame output is ready
|
||||
if (decp->status == JXL_DEC_FRAME) {
|
||||
decp->status = JxlDecoderGetFrameHeader(decp->decoder, &fhdr);
|
||||
_JXL_CHECK("JxlDecoderGetFrameHeader");
|
||||
}
|
||||
while (decp->status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
|
||||
decp->status = JxlDecoderProcessInput(decp->decoder);
|
||||
|
||||
if (decp->status == JXL_DEC_NEED_MORE_INPUT) {
|
||||
// this should only occur after rewind
|
||||
_jxl_decoder_set_input((PyObject *)decp);
|
||||
_JXL_CHECK("JxlDecoderSetInput")
|
||||
} else if (decp->status == JXL_DEC_FRAME) {
|
||||
// decode frame header
|
||||
decp->status = JxlDecoderGetFrameHeader(decp->decoder, &fhdr);
|
||||
_JXL_CHECK("JxlDecoderGetFrameHeader");
|
||||
}
|
||||
}
|
||||
|
||||
size_t new_output_buffer_len;
|
||||
decp->status = JxlDecoderImageOutBufferSize(
|
||||
decp->decoder, &decp->pixel_format, &new_output_buffer_len
|
||||
);
|
||||
_JXL_CHECK("JxlDecoderImageOutBufferSize");
|
||||
|
||||
// only allocate memory when current buffer is too small
|
||||
if (decp->output_buffer_len < new_output_buffer_len) {
|
||||
decp->output_buffer_len = new_output_buffer_len;
|
||||
uint8_t *new_output_buffer =
|
||||
realloc(decp->output_buffer, decp->output_buffer_len);
|
||||
if (!new_output_buffer) {
|
||||
PyErr_SetString(PyExc_OSError, "failed to allocate buffer");
|
||||
return NULL;
|
||||
}
|
||||
decp->output_buffer = new_output_buffer;
|
||||
}
|
||||
|
||||
decp->status = JxlDecoderSetImageOutBuffer(
|
||||
decp->decoder, &decp->pixel_format, decp->output_buffer, decp->output_buffer_len
|
||||
);
|
||||
_JXL_CHECK("JxlDecoderSetImageOutBuffer");
|
||||
|
||||
// decode image into output buffer
|
||||
decp->status = JxlDecoderProcessInput(decp->decoder);
|
||||
|
||||
if (decp->status != JXL_DEC_FULL_IMAGE) {
|
||||
PyErr_SetString(PyExc_OSError, "failed to read next frame");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bytes = PyBytes_FromStringAndSize(
|
||||
(char *)(decp->output_buffer), decp->output_buffer_len
|
||||
);
|
||||
|
||||
ret = Py_BuildValue("SIi", bytes, fhdr.duration, fhdr.is_last);
|
||||
|
||||
Py_DECREF(bytes);
|
||||
return ret;
|
||||
|
||||
// we also shouldn't reach here if frame read was ok
|
||||
|
||||
// set error message
|
||||
char err_msg[128];
|
||||
|
||||
end:
|
||||
snprintf(
|
||||
err_msg,
|
||||
128,
|
||||
"could not read frame. libjxl call: %s returned: %d",
|
||||
jxl_call_name,
|
||||
decp->status
|
||||
);
|
||||
PyErr_SetString(PyExc_OSError, err_msg);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_get_icc(PyObject *self) {
|
||||
JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
|
||||
|
||||
if (!decp->jxl_icc) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
return PyBytes_FromStringAndSize((const char *)decp->jxl_icc, decp->jxl_icc_len);
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_get_exif(PyObject *self) {
|
||||
JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
|
||||
|
||||
if (!decp->jxl_exif) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
return PyBytes_FromStringAndSize((const char *)decp->jxl_exif, decp->jxl_exif_len);
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_get_xmp(PyObject *self) {
|
||||
JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
|
||||
|
||||
if (!decp->jxl_xmp) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
return PyBytes_FromStringAndSize((const char *)decp->jxl_xmp, decp->jxl_xmp_len);
|
||||
}
|
||||
|
||||
// Version as string
|
||||
const char *
|
||||
JpegXlDecoderVersion_str(void) {
|
||||
static char version[20];
|
||||
sprintf(
|
||||
version,
|
||||
"%d.%d.%d",
|
||||
JPEGXL_MAJOR_VERSION,
|
||||
JPEGXL_MINOR_VERSION,
|
||||
JPEGXL_PATCH_VERSION
|
||||
);
|
||||
return version;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/* Type Definitions */
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
// JpegXlDecoder methods
|
||||
static struct PyMethodDef _jpegxl_decoder_methods[] = {
|
||||
{"get_info", (PyCFunction)_jxl_decoder_get_info, METH_NOARGS, "get_info"},
|
||||
{"get_next", (PyCFunction)_jxl_decoder_get_next, METH_NOARGS, "get_next"},
|
||||
{"get_icc", (PyCFunction)_jxl_decoder_get_icc, METH_NOARGS, "get_icc"},
|
||||
{"get_exif", (PyCFunction)_jxl_decoder_get_exif, METH_NOARGS, "get_exif"},
|
||||
{"get_xmp", (PyCFunction)_jxl_decoder_get_xmp, METH_NOARGS, "get_xmp"},
|
||||
{"get_frames_left",
|
||||
(PyCFunction)_jxl_decoder_get_frames_left,
|
||||
METH_NOARGS,
|
||||
"get_frames_left"},
|
||||
{"rewind", (PyCFunction)_jxl_decoder_rewind, METH_NOARGS, "rewind"},
|
||||
{NULL, NULL} /* sentinel */
|
||||
};
|
||||
|
||||
// JpegXlDecoder type definition
|
||||
static PyTypeObject JpegXlDecoder_Type = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0).tp_name = "JpegXlDecoder",
|
||||
.tp_basicsize = sizeof(JpegXlDecoderObject),
|
||||
.tp_dealloc = (destructor)_jxl_decoder_dealloc,
|
||||
.tp_methods = _jpegxl_decoder_methods,
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/* Module Setup */
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
static PyMethodDef jpegxlMethods[] = {
|
||||
{"JpegXlDecoder", _jxl_decoder_new, METH_VARARGS, "JpegXlDecoder"}, {NULL, NULL}
|
||||
};
|
||||
|
||||
static int
|
||||
setup_module(PyObject *m) {
|
||||
if (PyType_Ready(&JpegXlDecoder_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyObject *d = PyModule_GetDict(m);
|
||||
PyObject *v = PyUnicode_FromString(JpegXlDecoderVersion_str());
|
||||
PyDict_SetItemString(d, "libjxl_version", v ? v : Py_None);
|
||||
Py_XDECREF(v);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static PyModuleDef_Slot slots[] = {
|
||||
{Py_mod_exec, setup_module},
|
||||
#ifdef Py_GIL_DISABLED
|
||||
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
|
||||
#endif
|
||||
{0, NULL}
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC
|
||||
PyInit__jpegxl(void) {
|
||||
static PyModuleDef module_def = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
.m_name = "_jpegxl",
|
||||
.m_methods = jpegxlMethods,
|
||||
.m_slots = slots
|
||||
};
|
||||
|
||||
return PyModuleDef_Init(&module_def);
|
||||
}
|
||||
|
|
@ -256,6 +256,14 @@ unpack18(UINT8 *out, const UINT8 *in, int pixels) {
|
|||
}
|
||||
}
|
||||
|
||||
static void
|
||||
unpack1L(UINT8 *out, const UINT8 *in, int pixels) {
|
||||
int i;
|
||||
for (i = 0; i < pixels; i++) {
|
||||
out[i] = in[i] > 128 ? 255 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Unpack to "L" image */
|
||||
|
||||
static void
|
||||
|
|
@ -1564,6 +1572,7 @@ static struct {
|
|||
{IMAGING_MODE_1, IMAGING_RAWMODE_1_R, 1, unpack1R},
|
||||
{IMAGING_MODE_1, IMAGING_RAWMODE_1_IR, 1, unpack1IR},
|
||||
{IMAGING_MODE_1, IMAGING_RAWMODE_1_8, 8, unpack18},
|
||||
{IMAGING_MODE_1, IMAGING_RAWMODE_L, 8, unpack1L},
|
||||
|
||||
/* grayscale */
|
||||
{IMAGING_MODE_L, IMAGING_RAWMODE_L_2, 2, unpackL2},
|
||||
|
|
|
|||
50
wheels/dependency_licenses/LIBJXL.txt
Normal file
50
wheels/dependency_licenses/LIBJXL.txt
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
Copyright (c) the JPEG XL Project Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"This implementation" means the copyrightable works distributed by
|
||||
Google as part of the JPEG XL project.
|
||||
|
||||
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||
patent license to make, have made, use, offer to sell, sell, import,
|
||||
transfer and otherwise run, modify and propagate the contents of this
|
||||
implementation of JPEG XL, where such license applies only to those patent
|
||||
claims, both currently owned or controlled by Google and acquired in
|
||||
the future, licensable by Google that are necessarily infringed by this
|
||||
implementation of JPEG XL. This grant does not include claims that would be
|
||||
infringed only as a consequence of further modification of this
|
||||
implementation. If you or your agent or exclusive licensee institute or
|
||||
order or agree to the institution of patent litigation against any
|
||||
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that this implementation of JPEG XL or any code incorporated within this
|
||||
implementation of JPEG XL constitutes direct or contributory patent
|
||||
infringement, or inducement of patent infringement, then any patent
|
||||
rights granted to you under this License for this implementation of JPEG XL
|
||||
shall terminate as of the date such litigation is filed.
|
||||
|
|
@ -117,7 +117,9 @@ V = {
|
|||
"FREETYPE": "2.14.1",
|
||||
"FRIBIDI": "1.0.16",
|
||||
"HARFBUZZ": "12.3.0",
|
||||
"HIGHWAY": "1.3.0",
|
||||
"JPEGTURBO": "3.1.3",
|
||||
"JPEGXL": "0.11.1",
|
||||
"LCMS2": "2.17",
|
||||
"LIBAVIF": "1.3.0",
|
||||
"LIBIMAGEQUANT": "4.4.1",
|
||||
|
|
@ -255,7 +257,10 @@ DEPS: dict[str, dict[str, Any]] = {
|
|||
"filename": f"brotli-{V['BROTLI']}.tar.gz",
|
||||
"license": "LICENSE",
|
||||
"build": [
|
||||
*cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"),
|
||||
*cmds_cmake(
|
||||
("brotlicommon", "brotlidec", "brotlienc"),
|
||||
"-DBUILD_SHARED_LIBS:BOOL=OFF",
|
||||
),
|
||||
cmd_xcopy(r"c\include", "{inc_dir}"),
|
||||
],
|
||||
"libs": ["*.lib"],
|
||||
|
|
@ -332,6 +337,46 @@ DEPS: dict[str, dict[str, Any]] = {
|
|||
],
|
||||
"libs": [r"bin\*.lib"],
|
||||
},
|
||||
"highway": {
|
||||
"url": f"https://github.com/google/highway/archive/{V['HIGHWAY']}.tar.gz",
|
||||
"filename": f"highway-{V['HIGHWAY']}.tar.gz",
|
||||
"license": "LICENSE",
|
||||
"patch": {
|
||||
r"CMakeLists.txt": {
|
||||
"cmake_minimum_required(VERSION 3.10)": "cmake_minimum_required(VERSION 3.15)", # noqa: E501
|
||||
}
|
||||
},
|
||||
"build": [
|
||||
*cmds_cmake(
|
||||
"hwy",
|
||||
'-DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$<CONFIG:Debug>:Debug>"',
|
||||
)
|
||||
],
|
||||
"libs": ["hwy.lib"],
|
||||
},
|
||||
"libjxl": {
|
||||
"url": f"https://github.com/libjxl/libjxl/archive/v{V['JPEGXL']}.tar.gz",
|
||||
"filename": f"libjxl-{V['JPEGXL']}.tar.gz",
|
||||
"license": "LICENSE",
|
||||
"build": [
|
||||
*cmds_cmake(
|
||||
"jxl",
|
||||
rf"-DHWY_INCLUDE_DIR=..\highway-{V['HIGHWAY']}",
|
||||
r"-DLCMS2_LIBRARY=..\..\lib\lcms2_static",
|
||||
r"-DLCMS2_INCLUDE_DIR=..\..\inc",
|
||||
"-DJPEGXL_ENABLE_SJPEG:BOOL=OFF",
|
||||
"-DJPEGXL_ENABLE_SKCMS:BOOL=OFF",
|
||||
"-DJPEGXL_STATIC:BOOL=ON",
|
||||
"-DBUILD_TESTING:BOOL=OFF",
|
||||
"-DBUILD_SHARED_LIBS:BOOL=OFF",
|
||||
),
|
||||
cmd_copy(r"lib\jxl.lib", "{lib_dir}"),
|
||||
*cmds_cmake("jxl_threads"),
|
||||
cmd_copy(r"lib\jxl_threads.lib", "{lib_dir}"),
|
||||
cmd_mkdir(r"{inc_dir}\jxl"),
|
||||
cmd_copy(r"lib\include\jxl\*.h", r"{inc_dir}\jxl"),
|
||||
],
|
||||
},
|
||||
"libimagequant": {
|
||||
"url": "https://github.com/ImageOptim/libimagequant/archive/{V['LIBIMAGEQUANT']}.tar.gz",
|
||||
"filename": f"libimagequant-{V['LIBIMAGEQUANT']}.tar.gz",
|
||||
|
|
@ -630,6 +675,8 @@ def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) ->
|
|||
print(f"Skipping disabled dependency {dep_name}")
|
||||
continue
|
||||
script = build_dep(dep_name, prefs, verbose)
|
||||
if dep_name in ("highway", "libjxl") and hasattr(sys, "pypy_translation_info"):
|
||||
continue
|
||||
if gha_groups:
|
||||
lines.append(f"@echo ::group::Running {script}")
|
||||
lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"')
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user