Added support for 1 mode images

This commit is contained in:
Andrew Murray 2025-12-23 23:28:35 +11:00
parent ffa84e5668
commit e24b3ebaab
16 changed files with 54 additions and 74 deletions

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.

Binary file not shown.

View File

@ -1,10 +1,11 @@
from __future__ import annotations
import os
import re
import pytest
from PIL import Image, JpegXlImagePlugin, features
from PIL import Image, JpegXlImagePlugin, UnidentifiedImageError, features
from .helper import assert_image_similar_tofile, skip_unless_feature
@ -13,10 +14,6 @@ try:
except ImportError:
pass
# cjxl v0.9.2 41b8cdab
# hopper.jxl: cjxl hopper.png hopper.jxl -q 75 -e 8
# 16_bit_binary.jxl: cjxl 16_bit_binary.pgm 16_bit_binary.jxl -q 100 -e 9
class TestUnsupportedJpegXl:
def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
@ -24,7 +21,7 @@ class TestUnsupportedJpegXl:
with pytest.raises(OSError):
with pytest.warns(UserWarning, match="JXL support not installed"):
Image.open("Tests/images/hopper.jxl")
Image.open("Tests/images/jxl/hopper.jxl")
@skip_unless_feature("jpegxl")
@ -34,53 +31,34 @@ class TestFileJpegXl:
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
def test_read_rgb(self) -> None:
"""
Can we read an RGB mode JPEG XL file without error?
Does it have the bits we expect?
"""
with Image.open("Tests/images/hopper.jxl") as im:
assert im.mode == "RGB"
assert im.size == (128, 128)
@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"
im.getdata()
assert im.mode == mode
# generated with:
# djxl hopper.jxl hopper_jxl_bits.ppm
assert_image_similar_tofile(im, "Tests/images/hopper_jxl_bits.ppm", 1)
assert_image_similar_tofile(im, "Tests/images/" + test_file, 1.9)
def test_read_rgba(self) -> None:
# Generated with `cjxl transparent.png transparent.jxl -q 100 -e 8`
with Image.open("Tests/images/transparent.jxl") as im:
assert im.mode == "RGBA"
assert im.size == (200, 150)
assert im.format == "JPEG XL"
im.getdata()
im.tobytes()
assert_image_similar_tofile(im, "Tests/images/transparent.png", 1)
def test_read_i16(self) -> None:
"""
Can we read 16-bit Grayscale JPEG XL image?
"""
with Image.open("Tests/images/jxl/16bit_subcutaneous.cropped.jxl") as im:
assert im.mode == "I;16"
assert im.size == (128, 64)
assert im.format == "JPEG XL"
im.getdata()
assert_image_similar_tofile(
im, "Tests/images/jxl/16bit_subcutaneous.cropped.png", 1
)
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

@ -12,17 +12,17 @@ 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/hopper.jxl") as im:
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/iss634.jxl") as im:
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/iss634.jxl") as im:
with Image.open("Tests/images/jxl/iss634.jxl") as im:
assert im.info["duration"] == 70
assert im.info["timestamp"] == 0
@ -43,17 +43,17 @@ def test_seek() -> None:
assert im1.is_animated
# Traverse frames in reverse, checking timestamps and durations
total_dur = 0
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_dur += im1.info["duration"]
total_duration += im1.info["duration"]
assert im1.info["duration"] == im2.info["duration"]
assert im1.info["timestamp"] == im1.info["timestamp"]
assert total_dur == 8000
assert total_duration == 8000
assert im1.tell() == 0
assert im2.tell() == 0
@ -65,7 +65,7 @@ def test_seek() -> None:
def test_seek_errors() -> None:
with Image.open("Tests/images/iss634.jxl") as im:
with Image.open("Tests/images/jxl/iss634.jxl") as im:
with pytest.raises(EOFError, match="attempt to seek outside sequence"):
im.seek(-1)

View File

@ -27,7 +27,7 @@ except ImportError:
def test_read_exif_metadata() -> None:
with Image.open("Tests/images/flower.jxl") as im:
with Image.open("Tests/images/jxl/flower.jxl") as im:
assert im.format == "JPEG XL"
exif_data = im.info["exif"]
@ -44,7 +44,7 @@ def test_read_exif_metadata() -> None:
def test_read_exif_metadata_without_prefix() -> None:
with Image.open("Tests/images/flower2.jxl") as im:
with Image.open("Tests/images/jxl/flower2.jxl") as im:
# Assert prefix is not present
assert im.info["exif"][:6] != b"Exif\x00\x00"
@ -53,12 +53,12 @@ def test_read_exif_metadata_without_prefix() -> None:
def test_read_icc_profile() -> None:
with Image.open("Tests/images/flower.jxl") as im:
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/flower.jxl") as im:
with Image.open("Tests/images/jxl/flower.jxl") as im:
assert "xmp" not in im.info
if ElementTree is None:
with pytest.warns(
@ -70,7 +70,7 @@ def test_getxmp() -> None:
xmp = im.getxmp()
assert xmp == {}
with Image.open("Tests/images/flower2.jxl") as im:
with Image.open("Tests/images/jxl/flower2.jxl") as im:
if ElementTree is None:
with pytest.warns(
UserWarning,
@ -105,10 +105,10 @@ def test_4_byte_exif(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(JpegXlImagePlugin, "_jpegxl", _mock_jpegxl)
with Image.open("Tests/images/hopper.jxl") as im:
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/hopper.jxl") as im:
with Image.open("Tests/images/jxl/hopper.jxl") as im:
assert im.getexif() == {}

View File

@ -55,7 +55,8 @@ class JpegXlImageFile(ImageFile.ImageFile):
self.info["exif"] = exif[exif_start_offset + 4 :]
if xmp := self._decoder.get_xmp():
self.info["xmp"] = xmp
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
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:

View File

@ -14,21 +14,16 @@
void
_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) {
pf->data_type = JXL_TYPE_FLOAT;
} else if (bi->bits_per_sample > 8) {
pf->data_type = JXL_TYPE_UINT16;
} else {
pf->data_type = JXL_TYPE_UINT8;
}
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";
}
@ -40,9 +35,6 @@ _jxl_get_mode(const JxlBasicInfo *bi) {
if (bi->num_color_channels == 3) {
return bi->alpha_premultiplied ? "RGBa" : "RGBA";
}
if (bi->num_color_channels == 1) {
return bi->alpha_premultiplied ? "La" : "LA";
}
} else {
// image has no transparency
if (bi->num_color_channels == 3) {
@ -290,7 +282,7 @@ decoder_loop_skip_process:
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;
goto end_with_custom_error;
}
final_jxl_buf = _new_jxl_buf;

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