mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-07-10 16:22:22 +03:00
add tests
This commit is contained in:
parent
334c26d80b
commit
8e0c5db1e0
BIN
Tests/images/flower.jxl
Normal file
BIN
Tests/images/flower.jxl
Normal file
Binary file not shown.
BIN
Tests/images/flower2.jxl
Normal file
BIN
Tests/images/flower2.jxl
Normal file
Binary file not shown.
BIN
Tests/images/hopper.jxl
Normal file
BIN
Tests/images/hopper.jxl
Normal file
Binary file not shown.
BIN
Tests/images/hopper_jxl_bits.ppm
Normal file
BIN
Tests/images/hopper_jxl_bits.ppm
Normal file
Binary file not shown.
BIN
Tests/images/iss634.jxl
Normal file
BIN
Tests/images/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/transparent.jxl
Normal file
BIN
Tests/images/transparent.jxl
Normal file
Binary file not shown.
72
Tests/test_file_jxl.py
Normal file
72
Tests/test_file_jxl.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
import re
|
||||
import pytest
|
||||
|
||||
from PIL import Image, JxlImagePlugin, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
assert_image_similar,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
||||
try:
|
||||
from PIL import _jxl
|
||||
|
||||
HAVE_JXL = True
|
||||
except ImportError:
|
||||
HAVE_JXL = False
|
||||
|
||||
# cjxl v0.9.2 41b8cdab
|
||||
# hopper.jxl: cjxl hopper.png hopper.jxl -q 75 -e 8
|
||||
|
||||
class TestUnsupportedJxl:
|
||||
def test_unsupported(self) -> None:
|
||||
if HAVE_JXL:
|
||||
JxlImagePlugin.SUPPORTED = False
|
||||
|
||||
file_path = "Tests/images/hopper.jxl"
|
||||
with pytest.warns(UserWarning):
|
||||
with pytest.raises(OSError):
|
||||
with Image.open(file_path):
|
||||
pass
|
||||
|
||||
if HAVE_JXL:
|
||||
JxlImagePlugin.SUPPORTED = True
|
||||
|
||||
@skip_unless_feature("jxl")
|
||||
class TestFileJxl:
|
||||
def setup_method(self) -> None:
|
||||
self.rgb_mode = "RGB"
|
||||
|
||||
def test_version(self) -> None:
|
||||
_jxl.JxlDecoderVersion()
|
||||
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("jxl"))
|
||||
|
||||
def test_read_rgb(self) -> None:
|
||||
"""
|
||||
Can we read a RGB mode Jpeg XL file without error?
|
||||
Does it have the bits we expect?
|
||||
"""
|
||||
|
||||
with Image.open("Tests/images/hopper.jxl") as image:
|
||||
assert image.mode == self.rgb_mode
|
||||
assert image.size == (128, 128)
|
||||
assert image.format == "JPEG XL"
|
||||
image.load()
|
||||
image.getdata()
|
||||
|
||||
# generated with:
|
||||
# djxl hopper.jxl hopper_jxl_bits.ppm
|
||||
assert_image_similar_tofile(image, "Tests/images/hopper_jxl_bits.ppm", 1.0)
|
||||
|
||||
def test_JxlDecode_with_invalid_args(self) -> None:
|
||||
"""
|
||||
Calling decoder functions with no arguments should result in an error.
|
||||
"""
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
_jxl.PILJxlDecoder()
|
||||
|
||||
|
32
Tests/test_file_jxl_alpha.py
Normal file
32
Tests/test_file_jxl_alpha.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import (
|
||||
assert_image_similar_tofile
|
||||
)
|
||||
|
||||
_webp = pytest.importorskip("PIL._jxl", reason="JXL support not installed")
|
||||
|
||||
|
||||
def test_read_rgba() -> None:
|
||||
"""
|
||||
Can we read an RGBA mode file without error?
|
||||
Does it have the bits we expect?
|
||||
"""
|
||||
|
||||
# Generated with `cjxl transparent.png transparent.jxl -q 100 -e 8`
|
||||
file_path = "Tests/images/transparent.jxl"
|
||||
with Image.open(file_path) as image:
|
||||
assert image.mode == "RGBA"
|
||||
assert image.size == (200, 150)
|
||||
assert image.format == "JPEG XL"
|
||||
image.load()
|
||||
image.getdata()
|
||||
|
||||
image.tobytes()
|
||||
|
||||
assert_image_similar_tofile(image, "Tests/images/transparent.png", 1.0)
|
||||
|
70
Tests/test_file_jxl_animated.py
Normal file
70
Tests/test_file_jxl_animated.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
||||
pytestmark = [
|
||||
skip_unless_feature("jxl"),
|
||||
]
|
||||
|
||||
|
||||
def test_n_frames() -> None:
|
||||
"""Ensure that jxl format sets n_frames and is_animated attributes correctly."""
|
||||
|
||||
with Image.open("Tests/images/hopper.jxl") as im:
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open("Tests/images/iss634.jxl") as im:
|
||||
assert im.n_frames == 41
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_float_duration() -> None:
|
||||
|
||||
with Image.open("Tests/images/iss634.jxl") as im:
|
||||
im.load()
|
||||
assert im.info["duration"] == 70
|
||||
|
||||
|
||||
def test_seeking() -> 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_dur = 0
|
||||
for frame in reversed(range(im1.n_frames)):
|
||||
im1.seek(frame)
|
||||
im1.load()
|
||||
im2.seek(frame)
|
||||
im2.load()
|
||||
|
||||
assert_image_equal(im1.convert('RGB'), im2.convert('RGB'))
|
||||
|
||||
total_dur += im1.info["duration"]
|
||||
assert im1.info["duration"] == im2.info["duration"]
|
||||
assert im1.info["timestamp"] == im1.info["timestamp"]
|
||||
assert total_dur == 8000
|
||||
|
||||
|
||||
def test_seek_errors() -> None:
|
||||
with Image.open("Tests/images/iss634.jxl") as im:
|
||||
with pytest.raises(EOFError):
|
||||
im.seek(-1)
|
||||
|
||||
with pytest.raises(EOFError):
|
||||
im.seek(47)
|
88
Tests/test_file_jxl_metadata.py
Normal file
88
Tests/test_file_jxl_metadata.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import mark_if_feature_version, skip_unless_feature
|
||||
|
||||
pytestmark = [
|
||||
skip_unless_feature("jxl"),
|
||||
]
|
||||
|
||||
ElementTree: ModuleType | None
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
ElementTree = None
|
||||
|
||||
|
||||
# cjxl flower.jpg flower.jxl --lossless_jpeg=0 -q 75 -e 8
|
||||
|
||||
# python -c "from PIL import Image; im=Image.open('Tests/images/flower2.webp'); f=open('/tmp/xmp.xml', 'wb'); f.write(im.info['xmp']); f.close()"
|
||||
# cjxl flower2.jpg flower2.jxl --lossless_jpeg=0 -q 75 -e 8 -x xmp=/tmp/xmp.xml
|
||||
|
||||
def test_read_exif_metadata() -> None:
|
||||
file_path = "Tests/images/flower.jxl"
|
||||
with Image.open(file_path) as image:
|
||||
assert image.format == "JPEG XL"
|
||||
exif_data = image.info.get("exif", None)
|
||||
assert exif_data
|
||||
|
||||
exif = image._getexif()
|
||||
|
||||
# Camera make
|
||||
assert exif[271] == "Canon"
|
||||
|
||||
with Image.open("Tests/images/flower.jpg") as jpeg_image:
|
||||
expected_exif = jpeg_image.info["exif"]
|
||||
|
||||
# jpeg xl always returns exif without 'Exif\0\0' prefix
|
||||
assert exif_data == expected_exif[6:]
|
||||
|
||||
|
||||
def test_read_exif_metadata_without_prefix() -> None:
|
||||
with Image.open("Tests/images/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:
|
||||
file_path = "Tests/images/flower2.jxl"
|
||||
with Image.open(file_path) as image:
|
||||
assert image.format == "JPEG XL"
|
||||
assert image.info.get("icc_profile", None)
|
||||
|
||||
icc = image.info["icc_profile"]
|
||||
|
||||
with Image.open("Tests/images/flower2.jxl") as jpeg_image:
|
||||
expected_icc = jpeg_image.info["icc_profile"]
|
||||
|
||||
assert icc == expected_icc
|
||||
|
||||
|
||||
def test_getxmp() -> None:
|
||||
with Image.open("Tests/images/flower.jxl") as im:
|
||||
assert "xmp" not in im.info
|
||||
assert im.getxmp() == {}
|
||||
|
||||
with Image.open("Tests/images/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 (
|
||||
im.getxmp()["xmpmeta"]["xmptk"]
|
||||
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
|
||||
)
|
||||
|
26
Tests/test_jxl_leaks.py
Normal file
26
Tests/test_jxl_leaks.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import PillowLeakTestCase, skip_unless_feature
|
||||
|
||||
test_file = "Tests/images/hopper.jxl"
|
||||
|
||||
|
||||
@skip_unless_feature("jxl")
|
||||
class TestJxlLeaks(PillowLeakTestCase):
|
||||
# TODO: lower the limit, I'm not sure what is correct limit since I have libjxl debug system-wide
|
||||
mem_limit = 16 * 1024 # kb
|
||||
iterations = 100
|
||||
|
||||
def test_leak_load(self) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
im_data = f.read()
|
||||
|
||||
def core() -> None:
|
||||
with Image.open(BytesIO(im_data)) as im:
|
||||
im.load()
|
||||
|
||||
self._test_leak(core)
|
20
setup.py
20
setup.py
|
@ -286,6 +286,7 @@ class pil_build_ext(build_ext):
|
|||
features = [
|
||||
"zlib",
|
||||
"jpeg",
|
||||
"jxl",
|
||||
"tiff",
|
||||
"freetype",
|
||||
"raqm",
|
||||
|
@ -691,6 +692,14 @@ class pil_build_ext(build_ext):
|
|||
feature.jpeg2000 = "openjp2"
|
||||
feature.openjpeg_version = ".".join(str(x) for x in best_version)
|
||||
|
||||
if feature.want("jxl"):
|
||||
_dbg("Looking for jxl")
|
||||
if _find_include_file(self, "jxl/encode.h") and _find_include_file(
|
||||
self, "jxl/decode.h"
|
||||
):
|
||||
if _find_library_file(self, "jxl"):
|
||||
feature.jxl = "jxl jxl_threads"
|
||||
|
||||
if feature.want("imagequant"):
|
||||
_dbg("Looking for imagequant")
|
||||
if _find_include_file(self, "libimagequant.h"):
|
||||
|
@ -774,6 +783,15 @@ class pil_build_ext(build_ext):
|
|||
# alternate Windows name.
|
||||
feature.lcms = "lcms2_static"
|
||||
|
||||
if feature.jxl:
|
||||
# jxl and jxl_threads are required
|
||||
libs = feature.jxl.split()
|
||||
defs = []
|
||||
|
||||
self._update_extension("PIL._jxl", libs, defs)
|
||||
else:
|
||||
self._remove_extension("PIL._jxl")
|
||||
|
||||
if feature.want("webp"):
|
||||
_dbg("Looking for webp")
|
||||
if _find_include_file(self, "webp/encode.h") and _find_include_file(
|
||||
|
@ -934,6 +952,7 @@ class pil_build_ext(build_ext):
|
|||
(feature.freetype, "FREETYPE2"),
|
||||
(feature.raqm, "RAQM (Text shaping)", raqm_extra_info),
|
||||
(feature.lcms, "LITTLECMS2"),
|
||||
(feature.jxl, "JXL"),
|
||||
(feature.webp, "WEBP"),
|
||||
(feature.webpmux, "WEBPMUX"),
|
||||
(feature.xcb, "XCB (X protocol)"),
|
||||
|
@ -978,6 +997,7 @@ ext_modules = [
|
|||
Extension("PIL._imaging", files),
|
||||
Extension("PIL._imagingft", ["src/_imagingft.c"]),
|
||||
Extension("PIL._imagingcms", ["src/_imagingcms.c"]),
|
||||
Extension("PIL._jxl", ["src/_jxl.c"]),
|
||||
Extension("PIL._webp", ["src/_webp.c"]),
|
||||
Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]),
|
||||
Extension("PIL._imagingmath", ["src/_imagingmath.c"]),
|
||||
|
|
172
src/PIL/JxlImagePlugin.py
Normal file
172
src/PIL/JxlImagePlugin.py
Normal file
|
@ -0,0 +1,172 @@
|
|||
from io import BytesIO
|
||||
from . import Image, ImageFile
|
||||
import struct
|
||||
|
||||
try:
|
||||
from . import _jxl
|
||||
|
||||
SUPPORTED = True
|
||||
except ImportError:
|
||||
SUPPORTED = False
|
||||
|
||||
|
||||
## Future idea:
|
||||
## it's not known how many frames does animated image have
|
||||
## by default, _jxl_decoder_new will iterate over all frames without decoding them
|
||||
## then libjxl decoder is rewinded and we're ready to decode frame by frame
|
||||
## if OPEN_COUNTS_FRAMES is False, n_frames will be None until the last frame is decoded
|
||||
## it only applies to animated jpeg xl images
|
||||
#OPEN_COUNTS_FRAMES = True
|
||||
|
||||
def _accept(prefix):
|
||||
is_jxl = prefix[:2] == b'\xff\x0a' \
|
||||
or prefix[:12] == 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 JxlImageFile(ImageFile.ImageFile):
|
||||
format = "JPEG XL"
|
||||
format_description = "JPEG XL image"
|
||||
__loaded = 0
|
||||
__logical_frame = 0
|
||||
|
||||
def _open(self):
|
||||
self._decoder = _jxl.PILJxlDecoder(self.fp.read())
|
||||
|
||||
width, height, mode, has_anim, tps_num, tps_denom, n_loops, n_frames = self._decoder.get_info()
|
||||
self._size = width, height
|
||||
self.info["loop"] = n_loops
|
||||
self.is_animated = has_anim
|
||||
|
||||
self.n_frames = None
|
||||
self._tps_dur_secs = 1
|
||||
if not self.is_animated:
|
||||
self.n_frames = 1
|
||||
elif n_frames > 0:
|
||||
self.n_frames = n_frames
|
||||
self._tps_dur_secs = tps_num/tps_denom
|
||||
# TODO: handle libjxl timecods
|
||||
self.__timestamp = 0
|
||||
|
||||
self._mode = mode
|
||||
self.rawmode = mode
|
||||
self.tile = []
|
||||
|
||||
icc = self._decoder.get_icc()
|
||||
exif = self._decoder.get_exif()
|
||||
xmp = self._decoder.get_xmp()
|
||||
if icc: self.info["icc_profile"] = icc
|
||||
import traceback
|
||||
try:
|
||||
if exif: self.info["exif"] = self._fix_exif(exif)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
if xmp: self.info["xmp"] = xmp
|
||||
|
||||
self._rewind()
|
||||
|
||||
def _fix_exif(self, 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:
|
||||
return None
|
||||
exif_start_offset = struct.unpack(">I", exif[:4])[0]
|
||||
return exif[exif_start_offset+4:]
|
||||
|
||||
def _getexif(self):
|
||||
if "exif" not in self.info:
|
||||
return None
|
||||
return self.getexif()._get_merged_dict()
|
||||
|
||||
def getxmp(self):
|
||||
return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {}
|
||||
|
||||
def _get_next(self):
|
||||
|
||||
# Get next frame
|
||||
next_frame = self._decoder.get_next()
|
||||
self.__physical_frame += 1
|
||||
|
||||
# this actually means EOF, errors are raised in _jxl
|
||||
if next_frame is None:
|
||||
msg = "failed to decode next frame in JXL file"
|
||||
raise EOFError(msg)
|
||||
|
||||
data, tps_duration, is_last = next_frame
|
||||
if is_last and self.n_frames is None:
|
||||
# libjxl said this frame is the last one
|
||||
self.n_frames = self.__physical_frame
|
||||
|
||||
# duration in miliseconds
|
||||
duration = 1000 * tps_duration * (1/self._tps_dur_secs)
|
||||
timestamp = self.__timestamp
|
||||
self.__timestamp += duration
|
||||
|
||||
return data, timestamp, duration, is_last
|
||||
|
||||
def _rewind(self, hard=False):
|
||||
if hard:
|
||||
self._decoder.rewind()
|
||||
self.__physical_frame = 0
|
||||
self.__loaded = -1
|
||||
self.__timestamp = 0
|
||||
|
||||
def _seek_check(self, frame):
|
||||
# if image is not animated then only the 0th frame is available
|
||||
if (not self.is_animated and frame != 0) or \
|
||||
(self.n_frames is not None and (frame >= self.n_frames or frame < 0)):
|
||||
msg = "attempt to seek outside sequence"
|
||||
raise EOFError(msg)
|
||||
|
||||
return self.tell() != frame
|
||||
|
||||
def _seek(self, frame):
|
||||
#print("_seek: phy: {}, fr: {}".format(self.__physical_frame, frame))
|
||||
if frame == self.__physical_frame:
|
||||
return # Nothing to do
|
||||
if frame < self.__physical_frame:
|
||||
# also rewind libjxl decoder instance
|
||||
self._rewind(hard=True)
|
||||
|
||||
while self.__physical_frame < frame:
|
||||
self._get_next() # Advance to the requested frame
|
||||
|
||||
def seek(self, frame):
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
|
||||
# Set logical frame to requested position
|
||||
self.__logical_frame = frame
|
||||
|
||||
def load(self):
|
||||
|
||||
if self.__loaded != self.__logical_frame:
|
||||
self._seek(self.__logical_frame)
|
||||
|
||||
data, timestamp, duration, is_last = self._get_next()
|
||||
self.info["timestamp"] = timestamp
|
||||
self.info["duration"] = duration
|
||||
self.__loaded = self.__logical_frame
|
||||
|
||||
# Set tile
|
||||
if self.fp and self._exclusive_fp:
|
||||
self.fp.close()
|
||||
# this is horribly memory inefficient
|
||||
# you need probably 2*(raw image plane) bytes of memory
|
||||
self.fp = BytesIO(data)
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
|
||||
|
||||
return super().load()
|
||||
|
||||
def load_seek(self, pos):
|
||||
pass
|
||||
|
||||
def tell(self):
|
||||
return self.__logical_frame
|
||||
|
||||
|
||||
Image.register_open(JxlImageFile.format, JxlImageFile, _accept)
|
||||
Image.register_extension(JxlImageFile.format, ".jxl")
|
||||
Image.register_mime(JxlImageFile.format, "image/jxl")
|
|
@ -47,6 +47,7 @@ _plugins = [
|
|||
"IptcImagePlugin",
|
||||
"JpegImagePlugin",
|
||||
"Jpeg2KImagePlugin",
|
||||
"JxlImagePlugin",
|
||||
"McIdasImagePlugin",
|
||||
"MicImagePlugin",
|
||||
"MpegImagePlugin",
|
||||
|
|
|
@ -14,6 +14,7 @@ modules = {
|
|||
"tkinter": ("PIL._tkinter_finder", "tk_version"),
|
||||
"freetype2": ("PIL._imagingft", "freetype2_version"),
|
||||
"littlecms2": ("PIL._imagingcms", "littlecms_version"),
|
||||
"jxl": ("PIL._jxl", "libjxl_version"),
|
||||
"webp": ("PIL._webp", "webpdecoder_version"),
|
||||
}
|
||||
|
||||
|
@ -124,6 +125,7 @@ features = {
|
|||
"webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None),
|
||||
"webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None),
|
||||
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None),
|
||||
"jxl": ("PIL._jxl", "HAVE_JXL", None),
|
||||
"raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"),
|
||||
"fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
|
||||
"harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"),
|
||||
|
@ -268,6 +270,7 @@ def pilinfo(out=None, supported_formats=True):
|
|||
("transp_webp", "WEBP Transparency"),
|
||||
("webp_mux", "WEBPMUX"),
|
||||
("webp_anim", "WEBP Animation"),
|
||||
("jxl", "JPEG XL")
|
||||
("jpg", "JPEG"),
|
||||
("jpg_2000", "OPENJPEG (JPEG2000)"),
|
||||
("zlib", "ZLIB (PNG/ZIP)"),
|
||||
|
|
634
src/_jxl.c
Normal file
634
src/_jxl.c
Normal file
|
@ -0,0 +1,634 @@
|
|||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
#include <stdbool.h>
|
||||
#include "libImaging/Imaging.h"
|
||||
|
||||
#include <jxl/codestream_header.h>
|
||||
#include <jxl/decode.h>
|
||||
#include <jxl/types.h>
|
||||
#include <jxl/thread_parallel_runner.h>
|
||||
|
||||
#define _PIL_JXL_CHECK(call_name) if (decp->status != JXL_DEC_SUCCESS) { jxl_call_name = call_name; goto end; }
|
||||
|
||||
|
||||
void _pil_jxl_get_pixel_format(JxlPixelFormat *pf, const JxlBasicInfo *bi) {
|
||||
|
||||
pf->num_channels = bi->num_color_channels + bi->num_extra_channels;
|
||||
|
||||
if (bi->exponent_bits_per_sample > 0 || bi->alpha_exponent_bits > 0) {
|
||||
pf->data_type = JXL_TYPE_FLOAT; // not yet supported
|
||||
} else if (bi->bits_per_sample > 8) {
|
||||
pf->data_type = JXL_TYPE_UINT16; // not yet supported
|
||||
} else {
|
||||
pf->data_type = JXL_TYPE_UINT8;
|
||||
}
|
||||
|
||||
// this *might* cause some issues on Big-Endian systems
|
||||
// would be great to test it
|
||||
pf->endianness = JXL_NATIVE_ENDIAN;
|
||||
pf->align = 0;
|
||||
|
||||
}
|
||||
|
||||
// TODO: floating point mode
|
||||
char* _pil_jxl_get_mode(const JxlBasicInfo *bi) {
|
||||
|
||||
// PIL doesn't support high bit depth images
|
||||
// it will throw an exception but that's for your own good
|
||||
// you wouldn't want to see distorted image
|
||||
if (bi->bits_per_sample != 8) return "uns";
|
||||
|
||||
// image has transparency
|
||||
if (bi->alpha_bits > 0) {
|
||||
if (bi->num_color_channels == 3) {
|
||||
if (bi->alpha_premultiplied) return "RGBa";
|
||||
return "RGBA";
|
||||
} if (bi->num_color_channels == 1) {
|
||||
if (bi->alpha_premultiplied) return "La";
|
||||
return "LA";
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *outbuf;
|
||||
Py_ssize_t outbuf_len;
|
||||
|
||||
uint8_t *jxl_icc;
|
||||
Py_ssize_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;
|
||||
|
||||
Py_ssize_t n_frames;
|
||||
|
||||
char *mode;
|
||||
} PILJxlDecoderObject;
|
||||
|
||||
static PyTypeObject PILJxlDecoder_Type;
|
||||
|
||||
void
|
||||
_jxl_decoder_dealloc(PyObject *self) {
|
||||
PILJxlDecoderObject *decp = (PILJxlDecoderObject *)self;
|
||||
|
||||
if (decp->jxl_data) {
|
||||
free(decp->jxl_data);
|
||||
decp->jxl_data = NULL;
|
||||
decp->jxl_data_len = 0;
|
||||
}
|
||||
if (decp->outbuf) {
|
||||
free(decp->outbuf);
|
||||
decp->outbuf = NULL;
|
||||
decp->outbuf_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
|
||||
// has to be called after every rewind
|
||||
void _jxl_decoder_set_input(PyObject *self) {
|
||||
PILJxlDecoderObject *decp = (PILJxlDecoderObject *)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) {
|
||||
|
||||
PILJxlDecoderObject *decp = (PILJxlDecoderObject *)self;
|
||||
JxlDecoderRewind(decp->decoder);
|
||||
Py_RETURN_NONE;
|
||||
|
||||
}
|
||||
|
||||
void *
|
||||
_jxl_decoder_count_frames(PyObject *self) {
|
||||
PILJxlDecoderObject *decp = (PILJxlDecoderObject *)self;
|
||||
|
||||
decp->n_frames = 0;
|
||||
|
||||
// count all JXL_DEC_NEED_IMAGE_OUT_BUFFER events
|
||||
while (decp->status != JXL_DEC_SUCCESS) {
|
||||
//printf("fetch_frame_count status: %u\n", decp->status);
|
||||
decp->status = JxlDecoderProcessInput(decp->decoder);
|
||||
|
||||
if (decp->status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
|
||||
if (JxlDecoderSkipCurrentFrame(decp->decoder) != JXL_DEC_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
decp->n_frames++;
|
||||
}
|
||||
}
|
||||
|
||||
_jxl_decoder_rewind((PyObject *)decp);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_new(PyObject *self, PyObject *args) {
|
||||
PyBytesObject *jxl_string;
|
||||
|
||||
PILJxlDecoderObject *decp = NULL;
|
||||
decp = PyObject_New(PILJxlDecoderObject, &PILJxlDecoder_Type);
|
||||
decp->mode = NULL;
|
||||
decp->jxl_data = NULL;
|
||||
decp->jxl_data_len = 0;
|
||||
decp->outbuf = NULL;
|
||||
decp->outbuf_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->n_frames = 0;
|
||||
|
||||
// used for printing more detailed error messages
|
||||
char* jxl_call_name;
|
||||
|
||||
// parse one argument which is a string with jxl data
|
||||
if (!PyArg_ParseTuple(args, "S", &jxl_string)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// this data needs to be copied to PILJxlDecoderObject
|
||||
// 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);
|
||||
|
||||
// here occurs this copying (inefficiency)
|
||||
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;
|
||||
|
||||
//printf("%zu\n", decp->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);
|
||||
_PIL_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
|
||||
);
|
||||
_PIL_JXL_CHECK("JxlDecoderSubscribeEvents")
|
||||
|
||||
// tell libjxl to decompress boxes (for example Exif is usually compressed)
|
||||
decp->status = JxlDecoderSetDecompressBoxes(decp->decoder, JXL_TRUE);
|
||||
_PIL_JXL_CHECK("JxlDecoderSetDecompressBoxes")
|
||||
|
||||
_jxl_decoder_set_input((PyObject *)decp);
|
||||
_PIL_JXL_CHECK("JxlDecoderSetInput")
|
||||
|
||||
// decode everything up to the first frame
|
||||
do {
|
||||
|
||||
decp->status = JxlDecoderProcessInput(decp->decoder);
|
||||
//printf("Status: %d\n", decp->status);
|
||||
|
||||
decoder_loop_skip_process:
|
||||
|
||||
// there was an error at JxlDecoderProcessInput stage
|
||||
if (decp->status == JXL_DEC_ERROR) {
|
||||
jxl_call_name = "JxlDecoderProcessInput";
|
||||
goto end;
|
||||
}
|
||||
|
||||
// got basic info
|
||||
if (decp->status == JXL_DEC_BASIC_INFO) {
|
||||
|
||||
decp->status = JxlDecoderGetBasicInfo(decp->decoder,
|
||||
&decp->basic_info);
|
||||
_PIL_JXL_CHECK("JxlDecoderGetBasicInfo");
|
||||
|
||||
_pil_jxl_get_pixel_format(&decp->pixel_format, &decp->basic_info);
|
||||
if (decp->pixel_format.data_type != JXL_TYPE_UINT8) {
|
||||
// only 8 bit integer value images are supported for now
|
||||
PyErr_SetString(PyExc_NotImplementedError,
|
||||
"unsupported pixel data type");
|
||||
goto end_with_custom_error;
|
||||
}
|
||||
decp->mode = _pil_jxl_get_mode(&decp->basic_info);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// got color encoding
|
||||
if (decp->status == JXL_DEC_COLOR_ENCODING) {
|
||||
|
||||
decp->status = JxlDecoderGetICCProfileSize(decp->decoder,
|
||||
JXL_COLOR_PROFILE_TARGET_DATA, &decp->jxl_icc_len);
|
||||
_PIL_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);
|
||||
_PIL_JXL_CHECK("JxlDecoderGetColorAsICCProfile");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (decp->status == JXL_DEC_BOX) {
|
||||
|
||||
char btype[4];
|
||||
decp->status = JxlDecoderGetBoxType(decp->decoder, btype, JXL_TRUE);
|
||||
_PIL_JXL_CHECK("JxlDecoderGetBoxType");
|
||||
|
||||
//printf("found box type: %c%c%c%c\n", btype[0], btype[1], btype[2], btype[3]);
|
||||
|
||||
bool is_box_exif, is_box_xmp;
|
||||
is_box_exif = !memcmp(btype, "Exif", 4);
|
||||
is_box_xmp = !memcmp(btype, "xml ", 4);
|
||||
if (!is_box_exif && !is_box_xmp) {
|
||||
// not exif/xmp box so continue
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t cur_compr_box_size;
|
||||
decp->status = JxlDecoderGetBoxSizeRaw(decp->decoder, &cur_compr_box_size);
|
||||
_PIL_JXL_CHECK("JxlDecoderGetBoxSizeRaw");
|
||||
//printf("Exif/xmp box size: %zu\n", cur_compr_box_size);
|
||||
|
||||
uint8_t* final_jxl_buf = NULL;
|
||||
Py_ssize_t final_jxl_buf_len = 0;
|
||||
|
||||
// cur_box_size is actually compressed box size
|
||||
// it will also serve as our chunk size
|
||||
do {
|
||||
uint8_t* _new_jxl_buf = realloc(final_jxl_buf, final_jxl_buf_len + cur_compr_box_size);
|
||||
if (!_new_jxl_buf) {
|
||||
PyErr_SetString(PyExc_OSError, "failed to allocate final_jxl_buf");
|
||||
goto end;
|
||||
}
|
||||
final_jxl_buf = _new_jxl_buf;
|
||||
|
||||
decp->status = JxlDecoderSetBoxBuffer(decp->decoder, final_jxl_buf + final_jxl_buf_len, cur_compr_box_size);
|
||||
_PIL_JXL_CHECK("JxlDecoderSetBoxBuffer");
|
||||
|
||||
decp->status = JxlDecoderProcessInput(decp->decoder);
|
||||
|
||||
size_t remaining = JxlDecoderReleaseBoxBuffer(decp->decoder);
|
||||
//printf("boxes status: %d, remaining: %zu\n", decp->status, remaining);
|
||||
final_jxl_buf_len += (cur_compr_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);
|
||||
|
||||
// couldn't determine Image mode or it is unsupported
|
||||
if (!strcmp(decp->mode, "uns") || !decp->mode) {
|
||||
PyErr_SetString(PyExc_NotImplementedError, "only 8-bit images are supported");
|
||||
goto end_with_custom_error;
|
||||
}
|
||||
|
||||
if (decp->basic_info.have_animation) {
|
||||
// get frame count by iterating over image out events
|
||||
if (!_jxl_decoder_count_frames((PyObject *)decp)) {
|
||||
PyErr_SetString(PyExc_OSError, "something went wrong when counting frames");
|
||||
goto end_with_custom_error;
|
||||
}
|
||||
}
|
||||
|
||||
return (PyObject *)decp;
|
||||
//Py_RETURN_NONE;
|
||||
|
||||
// on success we should never reach here
|
||||
|
||||
end:
|
||||
// set error message
|
||||
char err_msg[128];
|
||||
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) {
|
||||
PILJxlDecoderObject *decp = (PILJxlDecoderObject *)self;
|
||||
|
||||
return Py_BuildValue(
|
||||
"IIsiIIII",
|
||||
decp->basic_info.xsize,
|
||||
decp->basic_info.ysize,
|
||||
decp->mode,
|
||||
decp->basic_info.have_animation,
|
||||
decp->basic_info.animation.tps_numerator,
|
||||
decp->basic_info.animation.tps_denominator,
|
||||
decp->basic_info.animation.num_loops,
|
||||
decp->n_frames
|
||||
);
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_get_next(PyObject *self) {
|
||||
|
||||
PILJxlDecoderObject *decp = (PILJxlDecoderObject *)self;
|
||||
PyObject *bytes;
|
||||
PyObject *ret;
|
||||
JxlFrameHeader fhdr = {};
|
||||
|
||||
char* jxl_call_name;
|
||||
|
||||
// process events until next frame output is ready
|
||||
while (decp->status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
|
||||
decp->status = JxlDecoderProcessInput(decp->decoder);
|
||||
|
||||
// every frame was decoded successfully
|
||||
if (decp->status == JXL_DEC_SUCCESS) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// this should only occur after rewind
|
||||
if (decp->status == JXL_DEC_NEED_MORE_INPUT) {
|
||||
_jxl_decoder_set_input((PyObject *)decp);
|
||||
_PIL_JXL_CHECK("JxlDecoderSetInput")
|
||||
continue;
|
||||
}
|
||||
|
||||
if (decp->status == JXL_DEC_FRAME) {
|
||||
// decode frame header
|
||||
decp->status = JxlDecoderGetFrameHeader(decp->decoder, &fhdr);
|
||||
_PIL_JXL_CHECK("JxlDecoderGetFrameHeader");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
size_t new_outbuf_len;
|
||||
decp->status = JxlDecoderImageOutBufferSize(decp->decoder, &decp->pixel_format, &new_outbuf_len);
|
||||
_PIL_JXL_CHECK("JxlDecoderImageOutBufferSize");
|
||||
|
||||
// only allocate memory when current buffer is too small
|
||||
if (decp->outbuf_len < new_outbuf_len) {
|
||||
|
||||
decp->outbuf_len = new_outbuf_len;
|
||||
uint8_t* _new_outbuf = realloc(decp->outbuf, decp->outbuf_len);
|
||||
if (!_new_outbuf) {
|
||||
PyErr_SetString(PyExc_OSError, "failed to allocate outbuf");
|
||||
goto end_with_custom_error;
|
||||
}
|
||||
decp->outbuf = _new_outbuf;
|
||||
|
||||
}
|
||||
|
||||
decp->status = JxlDecoderSetImageOutBuffer(decp->decoder, &decp->pixel_format, decp->outbuf, decp->outbuf_len);
|
||||
_PIL_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");
|
||||
goto end_with_custom_error;
|
||||
}
|
||||
|
||||
bytes = PyBytes_FromStringAndSize(
|
||||
(char *)(decp->outbuf), decp->outbuf_len);
|
||||
|
||||
ret = Py_BuildValue("SIi", bytes, fhdr.duration, fhdr.is_last);
|
||||
|
||||
Py_DECREF(bytes);
|
||||
return ret;
|
||||
|
||||
end:
|
||||
// we also shouldn't reach here if frame read was ok
|
||||
|
||||
// set error message
|
||||
char err_msg[128];
|
||||
snprintf(err_msg, 128,
|
||||
"could not read frame. libjxl call: %s returned: %d",
|
||||
jxl_call_name, decp->status);
|
||||
PyErr_SetString(PyExc_OSError, err_msg);
|
||||
|
||||
end_with_custom_error:
|
||||
|
||||
// no need to deallocate anything here
|
||||
// user can just igonre error
|
||||
|
||||
return NULL;
|
||||
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_jxl_decoder_get_icc(PyObject *self) {
|
||||
PILJxlDecoderObject *decp = (PILJxlDecoderObject *)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) {
|
||||
PILJxlDecoderObject *decp = (PILJxlDecoderObject *)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) {
|
||||
PILJxlDecoderObject *decp = (PILJxlDecoderObject *)self;
|
||||
|
||||
if (!decp->jxl_xmp) Py_RETURN_NONE;
|
||||
|
||||
return PyBytes_FromStringAndSize((const char *)decp->jxl_xmp, decp->jxl_xmp_len);
|
||||
}
|
||||
|
||||
// PILJxlDecoder methods
|
||||
static struct PyMethodDef _jxl_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"},
|
||||
{"rewind", (PyCFunction)_jxl_decoder_rewind, METH_NOARGS, "rewind"},
|
||||
{NULL, NULL} /* sentinel */
|
||||
};
|
||||
|
||||
// PILJxlDecoder type definition
|
||||
static PyTypeObject PILJxlDecoder_Type = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0) "PILJxlDecoder", /*tp_name */
|
||||
sizeof(PILJxlDecoderObject), /*tp_basicsize */
|
||||
0, /*tp_itemsize */
|
||||
/* methods */
|
||||
(destructor)_jxl_decoder_dealloc, /*tp_dealloc*/
|
||||
0, /*tp_vectorcall_offset*/
|
||||
0, /*tp_getattr*/
|
||||
0, /*tp_setattr*/
|
||||
0, /*tp_as_async*/
|
||||
0, /*tp_repr*/
|
||||
0, /*tp_as_number*/
|
||||
0, /*tp_as_sequence*/
|
||||
0, /*tp_as_mapping*/
|
||||
0, /*tp_hash*/
|
||||
0, /*tp_call*/
|
||||
0, /*tp_str*/
|
||||
0, /*tp_getattro*/
|
||||
0, /*tp_setattro*/
|
||||
0, /*tp_as_buffer*/
|
||||
Py_TPFLAGS_DEFAULT, /*tp_flags*/
|
||||
0, /*tp_doc*/
|
||||
0, /*tp_traverse*/
|
||||
0, /*tp_clear*/
|
||||
0, /*tp_richcompare*/
|
||||
0, /*tp_weaklistoffset*/
|
||||
0, /*tp_iter*/
|
||||
0, /*tp_iternext*/
|
||||
_jxl_decoder_methods, /*tp_methods*/
|
||||
0, /*tp_members*/
|
||||
0, /*tp_getset*/
|
||||
};
|
||||
|
||||
// Return libjxl decoder version available as integer:
|
||||
// MAJ*1_000_000 + MIN*1_000 + PATCH
|
||||
PyObject *
|
||||
JxlDecoderVersion_wrapper() {
|
||||
return Py_BuildValue("i", JxlDecoderVersion());
|
||||
}
|
||||
|
||||
// Version as string
|
||||
const char *
|
||||
JxlDecoderVersion_str(void) {
|
||||
static char version[20];
|
||||
int version_number = JxlDecoderVersion();
|
||||
sprintf(
|
||||
version,
|
||||
"%d.%d.%d",
|
||||
version_number / 1000000,
|
||||
(version_number % 1000000) / 1000,
|
||||
(version_number % 1000)
|
||||
);
|
||||
return version;
|
||||
}
|
||||
|
||||
static PyMethodDef jxlMethods[] = {
|
||||
{"JxlDecoderVersion", JxlDecoderVersion_wrapper, METH_NOARGS, "JxlVersion"},
|
||||
{"PILJxlDecoder", _jxl_decoder_new, METH_VARARGS, "PILJxlDecoder"},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
static int
|
||||
setup_module(PyObject *m) {
|
||||
if (PyType_Ready(&PILJxlDecoder_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// TODO(oloke) ready object types?
|
||||
PyObject *d = PyModule_GetDict(m);
|
||||
|
||||
PyObject *v = PyUnicode_FromString(JxlDecoderVersion_str());
|
||||
PyDict_SetItemString(d, "libjxl_version", v ? v : Py_None);
|
||||
Py_XDECREF(v);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyMODINIT_FUNC
|
||||
PyInit__jxl(void) {
|
||||
PyObject *m;
|
||||
|
||||
static PyModuleDef module_def = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
"_jxl", /* m_name */
|
||||
NULL, /* m_doc */
|
||||
-1, /* m_size */
|
||||
jxlMethods, /* m_methods */
|
||||
};
|
||||
|
||||
m = PyModule_Create(&module_def);
|
||||
if (setup_module(m) < 0) {
|
||||
Py_DECREF(m);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
Loading…
Reference in New Issue
Block a user