-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Add JPEG XL Open/Read support via libjxl #7848
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
olokelo
wants to merge
76
commits into
python-pillow:main
Choose a base branch
from
olokelo:jxl-support2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
76 commits
Select commit
Hold shift + click to select a range
8e0c5db
add tests
olokelo a57ebea
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 23fb57d
minor fixes, linting corrections
olokelo 2eb5987
Added type hints
radarhere 37b58f3
Removed feature
radarhere eeaecb4
Merge pull request #1 from radarhere/jxl-support2
olokelo 24b63ad
fix goto labels for clang
olokelo 0b50410
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] f403672
Merge branch 'main' into jxl-support2
hugovk f6086d4
modify leak test
olokelo 5320450
fix _jxl_decoder_count_frames
olokelo 1b049ab
minor plugin code tweaks
olokelo 6048520
add type hints
olokelo 8fa280f
rename jxl -> jpegxl
olokelo 58c37bf
add test case for seeking to the same frame
olokelo 48bbc2e
flip cases in metadata test
olokelo 443a352
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 62c58c2
fix some type hinting mistakes
olokelo 8cab1c1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] e5003ff
change Optional to python 3.10+ syntax
olokelo fa5bfac
add more metadata test cases
olokelo 0b71605
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 1f00fb8
add 16-bits grayscale support for jpeg xl images
olokelo 08270a7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 9313587
Merge branch 'main' into jxl-support2
radarhere 4256b2a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 8a1c03e
Merge branch 'main' into jxl-support2
radarhere 13944d5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] bb06057
Merge branch 'main' into jxl-support2
radarhere bc4a794
Merge branch 'main' into jxl-support2
radarhere ff269ab
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 661c0d8
Lint fixes
radarhere ade1db0
Replace slice and comparison with startswith
radarhere ceec3f9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 5e0457a
Removed getxmp()
radarhere 9266318
Do not set info["exif"] to None
radarhere 29e4b55
Do not add _getexif to new plugin
radarhere 3cd3848
Added type hint
radarhere aa6510f
Removed self.rawmode
radarhere 36640de
tile is already empty list
radarhere 7125fe4
Fixed type hints
radarhere 05aee33
Use monkeypatch
radarhere 6c3f0b5
Use member names to initialize module
radarhere 80e9963
Use member names to initialize PyTypeObject
radarhere 29c1e4c
Removed C method unused by Python
radarhere c8409e0
Simplified code
radarhere 61ce5c2
is_animated should be a bool
radarhere bf0cdb2
Test on Linux
radarhere 79f941d
Added argument that was removed in 0.9.0
radarhere b5d64e8
Removed specific leak check
radarhere ccca015
Use multi-phase initialization
radarhere ece4065
Merge branch 'main' into jxl-support2
radarhere e99989f
Merge branch 'main' into jxl-support2
radarhere 099048b
Raise warning if image file identification fails due to lack of support
radarhere ff0d3b4
Fixed typo
radarhere 358150b
libjxl < 0.9.0 is not supported. See https://github.com/libjxl/libjxl…
radarhere fe612ea
Revert "Added argument that was removed in 0.9.0"
radarhere 66e5838
Test on MinGW
radarhere 6ab48ab
Merge branch 'main' into jxl-support2
radarhere 9096240
Build on macOS and Linux wheels, except for manylinux2014
radarhere 762235c
Simplified code
radarhere 5e3dc40
Do not count frames on image open
radarhere 612de5a
Populate duration before load
radarhere e2105f3
Improve wheel build speed
radarhere 0b70b34
Fix build on manylinux_2_28 x86_64
radarhere 52912c3
Check for presence of jxl_threads
radarhere 7db30e0
Removed TODO until sample image can be provided
radarhere ffad6af
Merge branch 'main' into jxl-support2
radarhere daaf3b0
Added to list of read-only formats
radarhere 6e3f32c
Merge branch 'main' into jxl-support2
radarhere 96f0db3
Added license and patents
radarhere ffa84e5
Corrected ICC profile test
radarhere e24b3eb
Added support for 1 mode images
radarhere 05ef7b4
Added JPEGXL_ROOT
radarhere 7906f57
Merge branch 'main' into jxl-support2
radarhere 193ca32
Test on Windows
radarhere File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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 not shown.
Binary file not shown.
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() == {} | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.