Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
8e0c5db
add tests
olokelo Mar 1, 2024
a57ebea
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 2, 2024
23fb57d
minor fixes, linting corrections
olokelo Mar 2, 2024
2eb5987
Added type hints
radarhere Mar 6, 2024
37b58f3
Removed feature
radarhere Mar 6, 2024
eeaecb4
Merge pull request #1 from radarhere/jxl-support2
olokelo Mar 11, 2024
24b63ad
fix goto labels for clang
olokelo Mar 11, 2024
0b50410
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 11, 2024
f403672
Merge branch 'main' into jxl-support2
hugovk Mar 19, 2024
f6086d4
modify leak test
olokelo Mar 19, 2024
5320450
fix _jxl_decoder_count_frames
olokelo Mar 19, 2024
1b049ab
minor plugin code tweaks
olokelo Mar 19, 2024
6048520
add type hints
olokelo Mar 19, 2024
8fa280f
rename jxl -> jpegxl
olokelo Mar 19, 2024
58c37bf
add test case for seeking to the same frame
olokelo Mar 19, 2024
48bbc2e
flip cases in metadata test
olokelo Mar 19, 2024
443a352
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 19, 2024
62c58c2
fix some type hinting mistakes
olokelo Mar 19, 2024
8cab1c1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 19, 2024
e5003ff
change Optional to python 3.10+ syntax
olokelo Mar 19, 2024
fa5bfac
add more metadata test cases
olokelo Mar 20, 2024
0b71605
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 20, 2024
1f00fb8
add 16-bits grayscale support for jpeg xl images
olokelo May 18, 2024
08270a7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 18, 2024
9313587
Merge branch 'main' into jxl-support2
radarhere May 22, 2024
4256b2a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 22, 2024
8a1c03e
Merge branch 'main' into jxl-support2
radarhere Aug 14, 2024
13944d5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 14, 2024
bb06057
Merge branch 'main' into jxl-support2
radarhere Sep 11, 2024
bc4a794
Merge branch 'main' into jxl-support2
radarhere Apr 26, 2025
ff269ab
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 26, 2025
661c0d8
Lint fixes
radarhere Apr 26, 2025
ade1db0
Replace slice and comparison with startswith
radarhere May 7, 2025
ceec3f9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 7, 2025
5e0457a
Removed getxmp()
radarhere May 7, 2025
9266318
Do not set info["exif"] to None
radarhere May 7, 2025
29e4b55
Do not add _getexif to new plugin
radarhere May 7, 2025
3cd3848
Added type hint
radarhere May 7, 2025
aa6510f
Removed self.rawmode
radarhere May 7, 2025
36640de
tile is already empty list
radarhere May 7, 2025
7125fe4
Fixed type hints
radarhere May 7, 2025
05aee33
Use monkeypatch
radarhere May 7, 2025
6c3f0b5
Use member names to initialize module
radarhere May 9, 2025
80e9963
Use member names to initialize PyTypeObject
radarhere May 9, 2025
29c1e4c
Removed C method unused by Python
radarhere May 9, 2025
c8409e0
Simplified code
radarhere May 9, 2025
61ce5c2
is_animated should be a bool
radarhere May 10, 2025
bf0cdb2
Test on Linux
radarhere May 12, 2025
79f941d
Added argument that was removed in 0.9.0
radarhere May 12, 2025
b5d64e8
Removed specific leak check
radarhere Jun 15, 2025
ccca015
Use multi-phase initialization
radarhere Jun 25, 2025
ece4065
Merge branch 'main' into jxl-support2
radarhere Jun 25, 2025
e99989f
Merge branch 'main' into jxl-support2
radarhere Aug 4, 2025
099048b
Raise warning if image file identification fails due to lack of support
radarhere Nov 19, 2025
ff0d3b4
Fixed typo
radarhere Nov 19, 2025
358150b
libjxl < 0.9.0 is not supported. See https://github.com/libjxl/libjxl…
radarhere Nov 22, 2025
fe612ea
Revert "Added argument that was removed in 0.9.0"
radarhere Nov 22, 2025
66e5838
Test on MinGW
radarhere Nov 22, 2025
6ab48ab
Merge branch 'main' into jxl-support2
radarhere Nov 22, 2025
9096240
Build on macOS and Linux wheels, except for manylinux2014
radarhere Nov 26, 2025
762235c
Simplified code
radarhere Dec 6, 2025
5e3dc40
Do not count frames on image open
radarhere Dec 6, 2025
612de5a
Populate duration before load
radarhere Dec 13, 2025
e2105f3
Improve wheel build speed
radarhere Dec 13, 2025
0b70b34
Fix build on manylinux_2_28 x86_64
radarhere Dec 15, 2025
52912c3
Check for presence of jxl_threads
radarhere Dec 16, 2025
7db30e0
Removed TODO until sample image can be provided
radarhere Dec 17, 2025
ffad6af
Merge branch 'main' into jxl-support2
radarhere Dec 20, 2025
daaf3b0
Added to list of read-only formats
radarhere Dec 20, 2025
6e3f32c
Merge branch 'main' into jxl-support2
radarhere Dec 20, 2025
96f0db3
Added license and patents
radarhere Dec 21, 2025
ffa84e5
Corrected ICC profile test
radarhere Dec 23, 2025
e24b3eb
Added support for 1 mode images
radarhere Dec 23, 2025
05ef7b4
Added JPEGXL_ROOT
radarhere Dec 24, 2025
7906f57
Merge branch 'main' into jxl-support2
radarhere Dec 24, 2025
193ca32
Test on Windows
radarhere Dec 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .ci/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions .github/workflows/test-mingw.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
45 changes: 32 additions & 13 deletions .github/workflows/wheels-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ HARFBUZZ_VERSION=12.2.0
LIBPNG_VERSION=1.6.53
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Binary file added Tests/images/jxl/16bit_subcutaneous.cropped.jxl
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/jxl/flower.jxl
Binary file not shown.
Binary file added Tests/images/jxl/flower2.jxl
Binary file not shown.
Binary file added Tests/images/jxl/hopper.jxl
Binary file not shown.
Binary file added Tests/images/jxl/hopper_bw_500.jxl
Binary file not shown.
Binary file added Tests/images/jxl/hopper_gray.jxl
Binary file not shown.
Binary file added Tests/images/jxl/iss634.jxl
Binary file not shown.
Binary file added Tests/images/jxl/traffic_light.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/jxl/traffic_light.jxl
Binary file not shown.
Binary file added Tests/images/jxl/transparent.jxl
Binary file not shown.
Binary file added Tests/images/jxl/unknown_mode.jxl
Binary file not shown.
64 changes: 64 additions & 0 deletions Tests/test_file_jxl.py
Original file line number Diff line number Diff line change
@@ -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()
76 changes: 76 additions & 0 deletions Tests/test_file_jxl_animated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from __future__ import annotations

import pytest

from PIL import Image

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 im.n_frames == 1
assert not im.is_animated

with Image.open("Tests/images/jxl/iss634.jxl") as im:
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 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 changes: 114 additions & 0 deletions Tests/test_file_jxl_metadata.py
Original file line number Diff line number Diff line change
@@ -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() == {}
Loading
Loading