Simplified code

This commit is contained in:
Andrew Murray 2025-05-09 20:41:20 +10:00
parent 29c1e4c56f
commit c8409e0969
4 changed files with 73 additions and 81 deletions

View File

@ -73,4 +73,4 @@ class TestFileJpegXl:
""" """
with pytest.raises(TypeError): with pytest.raises(TypeError):
_jpegxl.PILJpegXlDecoder() _jpegxl.JpegXlDecoder()

View File

@ -93,12 +93,12 @@ def test_getxmp() -> None:
def test_4_byte_exif(monkeypatch: pytest.MonkeyPatch) -> None: def test_4_byte_exif(monkeypatch: pytest.MonkeyPatch) -> None:
class _mock_jpegxl: class _mock_jpegxl:
class PILJpegXlDecoder: class JpegXlDecoder:
def __init__(self, b: bytes) -> None: def __init__(self, b: bytes) -> None:
pass pass
def get_info(self) -> tuple[int, int, str, int, int, int, int, int]: def get_info(self) -> tuple[tuple[int, int], str, int, int, int, int, int]:
return (1, 1, "L", 0, 0, 0, 0, 0) return ((1, 1), "L", 0, 0, 0, 0, 0)
def get_icc(self) -> None: def get_icc(self) -> None:
pass pass

View File

@ -39,20 +39,17 @@ class JpegXlImageFile(ImageFile.ImageFile):
__logical_frame = 0 __logical_frame = 0
def _open(self) -> None: def _open(self) -> None:
self._decoder = _jpegxl.PILJpegXlDecoder(self.fp.read()) self._decoder = _jpegxl.JpegXlDecoder(self.fp.read())
( (
width, self._size,
height,
self._mode, self._mode,
self.is_animated, self.is_animated,
tps_num, tps_num,
tps_denom, tps_denom,
n_loops, self.info["loop"],
n_frames, n_frames,
) = self._decoder.get_info() ) = self._decoder.get_info()
self._size = width, height
self.info["loop"] = n_loops
self._tps_dur_secs = 1 self._tps_dur_secs = 1
self.n_frames: int | None = 1 self.n_frames: int | None = 1
@ -108,16 +105,6 @@ class JpegXlImageFile(ImageFile.ImageFile):
self.__loaded = -1 self.__loaded = -1
self.__timestamp = 0 self.__timestamp = 0
def _seek_check(self, frame: int) -> bool:
# 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: int) -> None: def _seek(self, frame: int) -> None:
if frame == self.__physical_frame: if frame == self.__physical_frame:
return # Nothing to do return # Nothing to do

View File

@ -8,14 +8,14 @@
#include <jxl/types.h> #include <jxl/types.h>
#include <jxl/thread_parallel_runner.h> #include <jxl/thread_parallel_runner.h>
#define _PIL_JXL_CHECK(call_name) \ #define _JXL_CHECK(call_name) \
if (decp->status != JXL_DEC_SUCCESS) { \ if (decp->status != JXL_DEC_SUCCESS) { \
jxl_call_name = call_name; \ jxl_call_name = call_name; \
goto end; \ goto end; \
} }
void void
_pil_jxl_get_pixel_format(JxlPixelFormat *pf, const JxlBasicInfo *bi) { _jxl_get_pixel_format(JxlPixelFormat *pf, const JxlBasicInfo *bi) {
pf->num_channels = bi->num_color_channels + bi->num_extra_channels; pf->num_channels = bi->num_color_channels + bi->num_extra_channels;
if (bi->exponent_bits_per_sample > 0 || bi->alpha_exponent_bits > 0) { if (bi->exponent_bits_per_sample > 0 || bi->alpha_exponent_bits > 0) {
@ -34,7 +34,7 @@ _pil_jxl_get_pixel_format(JxlPixelFormat *pf, const JxlBasicInfo *bi) {
// TODO: floating point mode // TODO: floating point mode
char * char *
_pil_jxl_get_mode(const JxlBasicInfo *bi) { _jxl_get_mode(const JxlBasicInfo *bi) {
// 16-bit single channel images are supported // 16-bit single channel images are supported
if (bi->bits_per_sample == 16 && bi->num_color_channels == 1 && if (bi->bits_per_sample == 16 && bi->num_color_channels == 1 &&
bi->alpha_bits == 0 && !bi->alpha_premultiplied) { bi->alpha_bits == 0 && !bi->alpha_premultiplied) {
@ -45,7 +45,7 @@ _pil_jxl_get_mode(const JxlBasicInfo *bi) {
// it will throw an exception but that's for your own good // it will throw an exception but that's for your own good
// you wouldn't want to see distorted image // you wouldn't want to see distorted image
if (bi->bits_per_sample != 8) { if (bi->bits_per_sample != 8) {
return "uns"; return NULL;
} }
// image has transparency // image has transparency
@ -101,13 +101,13 @@ typedef struct {
Py_ssize_t n_frames; Py_ssize_t n_frames;
char *mode; char *mode;
} PILJpegXlDecoderObject; } JpegXlDecoderObject;
static PyTypeObject PILJpegXlDecoder_Type; static PyTypeObject JpegXlDecoder_Type;
void void
_jxl_decoder_dealloc(PyObject *self) { _jxl_decoder_dealloc(PyObject *self) {
PILJpegXlDecoderObject *decp = (PILJpegXlDecoderObject *)self; JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
if (decp->jxl_data) { if (decp->jxl_data) {
free(decp->jxl_data); free(decp->jxl_data);
@ -150,7 +150,7 @@ _jxl_decoder_dealloc(PyObject *self) {
// has to be called after every rewind // has to be called after every rewind
void void
_jxl_decoder_set_input(PyObject *self) { _jxl_decoder_set_input(PyObject *self) {
PILJpegXlDecoderObject *decp = (PILJpegXlDecoderObject *)self; JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
decp->status = decp->status =
JxlDecoderSetInput(decp->decoder, decp->jxl_data, decp->jxl_data_len); JxlDecoderSetInput(decp->decoder, decp->jxl_data, decp->jxl_data_len);
@ -161,14 +161,14 @@ _jxl_decoder_set_input(PyObject *self) {
PyObject * PyObject *
_jxl_decoder_rewind(PyObject *self) { _jxl_decoder_rewind(PyObject *self) {
PILJpegXlDecoderObject *decp = (PILJpegXlDecoderObject *)self; JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
JxlDecoderRewind(decp->decoder); JxlDecoderRewind(decp->decoder);
Py_RETURN_NONE; Py_RETURN_NONE;
} }
bool bool
_jxl_decoder_count_frames(PyObject *self) { _jxl_decoder_count_frames(PyObject *self) {
PILJpegXlDecoderObject *decp = (PILJpegXlDecoderObject *)self; JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
decp->n_frames = 0; decp->n_frames = 0;
@ -193,8 +193,8 @@ PyObject *
_jxl_decoder_new(PyObject *self, PyObject *args) { _jxl_decoder_new(PyObject *self, PyObject *args) {
PyBytesObject *jxl_string; PyBytesObject *jxl_string;
PILJpegXlDecoderObject *decp = NULL; JpegXlDecoderObject *decp = NULL;
decp = PyObject_New(PILJpegXlDecoderObject, &PILJpegXlDecoder_Type); decp = PyObject_New(JpegXlDecoderObject, &JpegXlDecoder_Type);
decp->mode = NULL; decp->mode = NULL;
decp->jxl_data = NULL; decp->jxl_data = NULL;
decp->jxl_data_len = 0; decp->jxl_data_len = 0;
@ -216,7 +216,7 @@ _jxl_decoder_new(PyObject *self, PyObject *args) {
return NULL; return NULL;
} }
// this data needs to be copied to PILJpegXlDecoderObject // this data needs to be copied to JpegXlDecoderObject
// so that input bitstream is preserved across calls // so that input bitstream is preserved across calls
const uint8_t *_tmp_jxl_data; const uint8_t *_tmp_jxl_data;
Py_ssize_t _tmp_jxl_data_len; Py_ssize_t _tmp_jxl_data_len;
@ -238,21 +238,21 @@ _jxl_decoder_new(PyObject *self, PyObject *args) {
decp->status = JxlDecoderSetParallelRunner( decp->status = JxlDecoderSetParallelRunner(
decp->decoder, JxlThreadParallelRunner, decp->runner decp->decoder, JxlThreadParallelRunner, decp->runner
); );
_PIL_JXL_CHECK("JxlDecoderSetParallelRunner") _JXL_CHECK("JxlDecoderSetParallelRunner")
decp->status = JxlDecoderSubscribeEvents( decp->status = JxlDecoderSubscribeEvents(
decp->decoder, decp->decoder,
JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_BOX | JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_BOX |
JXL_DEC_FULL_IMAGE JXL_DEC_FULL_IMAGE
); );
_PIL_JXL_CHECK("JxlDecoderSubscribeEvents") _JXL_CHECK("JxlDecoderSubscribeEvents")
// tell libjxl to decompress boxes (for example Exif is usually compressed) // tell libjxl to decompress boxes (for example Exif is usually compressed)
decp->status = JxlDecoderSetDecompressBoxes(decp->decoder, JXL_TRUE); decp->status = JxlDecoderSetDecompressBoxes(decp->decoder, JXL_TRUE);
_PIL_JXL_CHECK("JxlDecoderSetDecompressBoxes") _JXL_CHECK("JxlDecoderSetDecompressBoxes")
_jxl_decoder_set_input((PyObject *)decp); _jxl_decoder_set_input((PyObject *)decp);
_PIL_JXL_CHECK("JxlDecoderSetInput") _JXL_CHECK("JxlDecoderSetInput")
// decode everything up to the first frame // decode everything up to the first frame
do { do {
@ -269,9 +269,9 @@ decoder_loop_skip_process:
// got basic info // got basic info
if (decp->status == JXL_DEC_BASIC_INFO) { if (decp->status == JXL_DEC_BASIC_INFO) {
decp->status = JxlDecoderGetBasicInfo(decp->decoder, &decp->basic_info); decp->status = JxlDecoderGetBasicInfo(decp->decoder, &decp->basic_info);
_PIL_JXL_CHECK("JxlDecoderGetBasicInfo"); _JXL_CHECK("JxlDecoderGetBasicInfo");
_pil_jxl_get_pixel_format(&decp->pixel_format, &decp->basic_info); _jxl_get_pixel_format(&decp->pixel_format, &decp->basic_info);
if (decp->pixel_format.data_type != JXL_TYPE_UINT8 && if (decp->pixel_format.data_type != JXL_TYPE_UINT8 &&
decp->pixel_format.data_type != JXL_TYPE_UINT16) { decp->pixel_format.data_type != JXL_TYPE_UINT16) {
// only 8 bit integer value images are supported for now // only 8 bit integer value images are supported for now
@ -280,7 +280,7 @@ decoder_loop_skip_process:
); );
goto end_with_custom_error; goto end_with_custom_error;
} }
decp->mode = _pil_jxl_get_mode(&decp->basic_info); decp->mode = _jxl_get_mode(&decp->basic_info);
continue; continue;
} }
@ -290,7 +290,7 @@ decoder_loop_skip_process:
decp->status = JxlDecoderGetICCProfileSize( decp->status = JxlDecoderGetICCProfileSize(
decp->decoder, JXL_COLOR_PROFILE_TARGET_DATA, &decp->jxl_icc_len decp->decoder, JXL_COLOR_PROFILE_TARGET_DATA, &decp->jxl_icc_len
); );
_PIL_JXL_CHECK("JxlDecoderGetICCProfileSize"); _JXL_CHECK("JxlDecoderGetICCProfileSize");
decp->jxl_icc = malloc(decp->jxl_icc_len); decp->jxl_icc = malloc(decp->jxl_icc_len);
if (!decp->jxl_icc) { if (!decp->jxl_icc) {
@ -304,7 +304,7 @@ decoder_loop_skip_process:
decp->jxl_icc, decp->jxl_icc,
decp->jxl_icc_len decp->jxl_icc_len
); );
_PIL_JXL_CHECK("JxlDecoderGetColorAsICCProfile"); _JXL_CHECK("JxlDecoderGetColorAsICCProfile");
continue; continue;
} }
@ -312,11 +312,10 @@ decoder_loop_skip_process:
if (decp->status == JXL_DEC_BOX) { if (decp->status == JXL_DEC_BOX) {
char btype[4]; char btype[4];
decp->status = JxlDecoderGetBoxType(decp->decoder, btype, JXL_TRUE); decp->status = JxlDecoderGetBoxType(decp->decoder, btype, JXL_TRUE);
_PIL_JXL_CHECK("JxlDecoderGetBoxType"); _JXL_CHECK("JxlDecoderGetBoxType");
bool is_box_exif, is_box_xmp; bool is_box_exif = !memcmp(btype, "Exif", 4);
is_box_exif = !memcmp(btype, "Exif", 4); bool is_box_xmp = !memcmp(btype, "xml ", 4);
is_box_xmp = !memcmp(btype, "xml ", 4);
if (!is_box_exif && !is_box_xmp) { if (!is_box_exif && !is_box_xmp) {
// not exif/xmp box so continue // not exif/xmp box so continue
continue; continue;
@ -324,7 +323,7 @@ decoder_loop_skip_process:
size_t cur_compr_box_size; size_t cur_compr_box_size;
decp->status = JxlDecoderGetBoxSizeRaw(decp->decoder, &cur_compr_box_size); decp->status = JxlDecoderGetBoxSizeRaw(decp->decoder, &cur_compr_box_size);
_PIL_JXL_CHECK("JxlDecoderGetBoxSizeRaw"); _JXL_CHECK("JxlDecoderGetBoxSizeRaw");
uint8_t *final_jxl_buf = NULL; uint8_t *final_jxl_buf = NULL;
Py_ssize_t final_jxl_buf_len = 0; Py_ssize_t final_jxl_buf_len = 0;
@ -343,7 +342,7 @@ decoder_loop_skip_process:
decp->status = JxlDecoderSetBoxBuffer( decp->status = JxlDecoderSetBoxBuffer(
decp->decoder, final_jxl_buf + final_jxl_buf_len, cur_compr_box_size decp->decoder, final_jxl_buf + final_jxl_buf_len, cur_compr_box_size
); );
_PIL_JXL_CHECK("JxlDecoderSetBoxBuffer"); _JXL_CHECK("JxlDecoderSetBoxBuffer");
decp->status = JxlDecoderProcessInput(decp->decoder); decp->status = JxlDecoderProcessInput(decp->decoder);
@ -367,7 +366,7 @@ decoder_loop_skip_process:
} while (decp->status != JXL_DEC_FRAME); } while (decp->status != JXL_DEC_FRAME);
// couldn't determine Image mode or it is unsupported // couldn't determine Image mode or it is unsupported
if (!strcmp(decp->mode, "uns") || !decp->mode) { if (!decp->mode) {
PyErr_SetString(PyExc_NotImplementedError, "only 8-bit images are supported"); PyErr_SetString(PyExc_NotImplementedError, "only 8-bit images are supported");
goto end_with_custom_error; goto end_with_custom_error;
} }
@ -408,10 +407,10 @@ end_with_custom_error:
PyObject * PyObject *
_jxl_decoder_get_info(PyObject *self) { _jxl_decoder_get_info(PyObject *self) {
PILJpegXlDecoderObject *decp = (PILJpegXlDecoderObject *)self; JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
return Py_BuildValue( return Py_BuildValue(
"IIsiIIII", "(II)siIIII",
decp->basic_info.xsize, decp->basic_info.xsize,
decp->basic_info.ysize, decp->basic_info.ysize,
decp->mode, decp->mode,
@ -425,7 +424,7 @@ _jxl_decoder_get_info(PyObject *self) {
PyObject * PyObject *
_jxl_decoder_get_next(PyObject *self) { _jxl_decoder_get_next(PyObject *self) {
PILJpegXlDecoderObject *decp = (PILJpegXlDecoderObject *)self; JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
PyObject *bytes; PyObject *bytes;
PyObject *ret; PyObject *ret;
JxlFrameHeader fhdr = {}; JxlFrameHeader fhdr = {};
@ -444,14 +443,14 @@ _jxl_decoder_get_next(PyObject *self) {
// this should only occur after rewind // this should only occur after rewind
if (decp->status == JXL_DEC_NEED_MORE_INPUT) { if (decp->status == JXL_DEC_NEED_MORE_INPUT) {
_jxl_decoder_set_input((PyObject *)decp); _jxl_decoder_set_input((PyObject *)decp);
_PIL_JXL_CHECK("JxlDecoderSetInput") _JXL_CHECK("JxlDecoderSetInput")
continue; continue;
} }
if (decp->status == JXL_DEC_FRAME) { if (decp->status == JXL_DEC_FRAME) {
// decode frame header // decode frame header
decp->status = JxlDecoderGetFrameHeader(decp->decoder, &fhdr); decp->status = JxlDecoderGetFrameHeader(decp->decoder, &fhdr);
_PIL_JXL_CHECK("JxlDecoderGetFrameHeader"); _JXL_CHECK("JxlDecoderGetFrameHeader");
continue; continue;
} }
} }
@ -460,7 +459,7 @@ _jxl_decoder_get_next(PyObject *self) {
decp->status = JxlDecoderImageOutBufferSize( decp->status = JxlDecoderImageOutBufferSize(
decp->decoder, &decp->pixel_format, &new_outbuf_len decp->decoder, &decp->pixel_format, &new_outbuf_len
); );
_PIL_JXL_CHECK("JxlDecoderImageOutBufferSize"); _JXL_CHECK("JxlDecoderImageOutBufferSize");
// only allocate memory when current buffer is too small // only allocate memory when current buffer is too small
if (decp->outbuf_len < new_outbuf_len) { if (decp->outbuf_len < new_outbuf_len) {
@ -476,7 +475,7 @@ _jxl_decoder_get_next(PyObject *self) {
decp->status = JxlDecoderSetImageOutBuffer( decp->status = JxlDecoderSetImageOutBuffer(
decp->decoder, &decp->pixel_format, decp->outbuf, decp->outbuf_len decp->decoder, &decp->pixel_format, decp->outbuf, decp->outbuf_len
); );
_PIL_JXL_CHECK("JxlDecoderSetImageOutBuffer"); _JXL_CHECK("JxlDecoderSetImageOutBuffer");
// decode image into output_buffer // decode image into output_buffer
decp->status = JxlDecoderProcessInput(decp->decoder); decp->status = JxlDecoderProcessInput(decp->decoder);
@ -518,7 +517,7 @@ end_with_custom_error:
PyObject * PyObject *
_jxl_decoder_get_icc(PyObject *self) { _jxl_decoder_get_icc(PyObject *self) {
PILJpegXlDecoderObject *decp = (PILJpegXlDecoderObject *)self; JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
if (!decp->jxl_icc) { if (!decp->jxl_icc) {
Py_RETURN_NONE; Py_RETURN_NONE;
@ -529,7 +528,7 @@ _jxl_decoder_get_icc(PyObject *self) {
PyObject * PyObject *
_jxl_decoder_get_exif(PyObject *self) { _jxl_decoder_get_exif(PyObject *self) {
PILJpegXlDecoderObject *decp = (PILJpegXlDecoderObject *)self; JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
if (!decp->jxl_exif) { if (!decp->jxl_exif) {
Py_RETURN_NONE; Py_RETURN_NONE;
@ -540,7 +539,7 @@ _jxl_decoder_get_exif(PyObject *self) {
PyObject * PyObject *
_jxl_decoder_get_xmp(PyObject *self) { _jxl_decoder_get_xmp(PyObject *self) {
PILJpegXlDecoderObject *decp = (PILJpegXlDecoderObject *)self; JpegXlDecoderObject *decp = (JpegXlDecoderObject *)self;
if (!decp->jxl_xmp) { if (!decp->jxl_xmp) {
Py_RETURN_NONE; Py_RETURN_NONE;
@ -549,7 +548,25 @@ _jxl_decoder_get_xmp(PyObject *self) {
return PyBytes_FromStringAndSize((const char *)decp->jxl_xmp, decp->jxl_xmp_len); return PyBytes_FromStringAndSize((const char *)decp->jxl_xmp, decp->jxl_xmp_len);
} }
// PILJpegXlDecoder methods // 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[] = { static struct PyMethodDef _jpegxl_decoder_methods[] = {
{"get_info", (PyCFunction)_jxl_decoder_get_info, METH_NOARGS, "get_info"}, {"get_info", (PyCFunction)_jxl_decoder_get_info, METH_NOARGS, "get_info"},
{"get_next", (PyCFunction)_jxl_decoder_get_next, METH_NOARGS, "get_next"}, {"get_next", (PyCFunction)_jxl_decoder_get_next, METH_NOARGS, "get_next"},
@ -560,37 +577,25 @@ static struct PyMethodDef _jpegxl_decoder_methods[] = {
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
}; };
// PILJpegXlDecoder type definition // JpegXlDecoder type definition
static PyTypeObject PILJpegXlDecoder_Type = { static PyTypeObject JpegXlDecoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PILJpegXlDecoder", PyVarObject_HEAD_INIT(NULL, 0).tp_name = "JpegXlDecoder",
.tp_basicsize = sizeof(PILJpegXlDecoderObject), .tp_basicsize = sizeof(JpegXlDecoderObject),
.tp_dealloc = (destructor)_jxl_decoder_dealloc, .tp_dealloc = (destructor)_jxl_decoder_dealloc,
.tp_methods = _jpegxl_decoder_methods, .tp_methods = _jpegxl_decoder_methods,
}; };
// Version as string /* -------------------------------------------------------------------- */
const char * /* Module Setup */
JpegXlDecoderVersion_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 jpegxlMethods[] = { static PyMethodDef jpegxlMethods[] = {
{"PILJpegXlDecoder", _jxl_decoder_new, METH_VARARGS, "PILJpegXlDecoder"}, {"JpegXlDecoder", _jxl_decoder_new, METH_VARARGS, "JpegXlDecoder"}, {NULL, NULL}
{NULL, NULL}
}; };
static int static int
setup_module(PyObject *m) { setup_module(PyObject *m) {
if (PyType_Ready(&PILJpegXlDecoder_Type) < 0) { if (PyType_Ready(&JpegXlDecoder_Type) < 0) {
return -1; return -1;
} }