This commit is contained in:
Aleksander Błażelonis 2026-01-31 15:21:20 -06:00 committed by GitHub
commit 945a946689
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1207 additions and 20 deletions

View File

@ -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

View File

@ -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 \

View File

@ -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'

View File

@ -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 {

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
Tests/images/jxl/flower.jxl Normal file

Binary file not shown.

Binary file not shown.

BIN
Tests/images/jxl/hopper.jxl Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Tests/images/jxl/iss634.jxl Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

64
Tests/test_file_jxl.py Normal file
View 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()

View 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

View 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() == {}

View File

@ -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
View 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

View File

@ -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
^^^^^^

View File

@ -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.

View File

@ -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

View File

@ -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
------------------------------------

View File

@ -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"]),

View 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")

View File

@ -48,6 +48,7 @@ _plugins = [
"IptcImagePlugin",
"JpegImagePlugin",
"Jpeg2KImagePlugin",
"JpegXlImagePlugin",
"McIdasImagePlugin",
"MicImagePlugin",
"MpegImagePlugin",

3
src/PIL/_jpegxl.pyi Normal file
View File

@ -0,0 +1,3 @@
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -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
View 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);
}

View File

@ -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},

View 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.

View File

@ -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}"')