Skip to content
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

Preferred-encoder global option. #192

Merged
merged 1 commit into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
All notable changes to this project will be documented in this file.

## [0.15.0 - 2024-01-xx]
## [0.15.0 - 2024-0x-xx]

### Added

- `libheif_info` function: added `encoders` and `decoders` keys to the result, for future libheif plugins support. #189
- `options.PREFERRED_ENCODER` - to use `encoder` different from the default one. #192

### Changed

Expand Down
1 change: 1 addition & 0 deletions docs/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Options
.. autodata:: pillow_heif.options.SAVE_HDR_TO_12_BIT
.. autodata:: pillow_heif.options.ALLOW_INCORRECT_HEADERS
.. autodata:: pillow_heif.options.SAVE_NCLX_PROFILE
.. autodata:: pillow_heif.options.PREFERRED_ENCODER

Example of use
""""""""""""""
Expand Down
18 changes: 15 additions & 3 deletions pillow_heif/_pillow_heif.c
Original file line number Diff line number Diff line change
Expand Up @@ -1161,16 +1161,28 @@ static struct PyGetSetDef _CtxImage_getseters[] = {
/* =========== Functions ======== */

static PyObject* _CtxWrite(PyObject* self, PyObject* args) {
/* compression_format: int, quality: int */
/* compression_format: int, quality: int, encoder_id: str */
struct heif_encoder* encoder;
struct heif_error error;
int compression_format, quality;
const char *encoder_id;
const struct heif_encoder_descriptor* encoders[1];

if (!PyArg_ParseTuple(args, "ii", &compression_format, &quality))
if (!PyArg_ParseTuple(args, "iis", &compression_format, &quality, &encoder_id))
return NULL;

struct heif_context* ctx = heif_context_alloc();
error = heif_context_get_encoder_for_format(ctx, compression_format, &encoder);
if (strlen(encoder_id) > 0) {
if (heif_get_encoder_descriptors(heif_compression_undefined, encoder_id, encoders, 1) != 1) {
PyErr_SetString(PyExc_RuntimeError, "could not find encoder with provided ID");
return NULL;
}
error = heif_context_get_encoder(ctx, encoders[0], &encoder);
}
else {
error = heif_context_get_encoder_for_format(ctx, compression_format, &encoder);
}

if (check_error(error)) {
heif_context_free(ctx);
return NULL;
Expand Down
16 changes: 10 additions & 6 deletions pillow_heif/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,23 @@ class HeifCompressionFormat(IntEnum):
UNDEFINED = 0
"""The compression format is not defined."""
HEVC = 1
"""The compression format is HEVC."""
"""Equivalent to H.265."""
AVC = 2
"""The compression format is AVC."""
"""Equivalent to H.264. Defined in ISO/IEC 14496-10."""
JPEG = 3
"""The compression format is JPEG."""
"""JPEG compression. Defined in ISO/IEC 10918-1."""
AV1 = 4
"""The compression format is AV1."""
"""AV1 compression, used for AVIF images."""
VVC = 5
"""The compression format is VVC."""
"""Equivalent to H.266. Defined in ISO/IEC 23090-3."""
EVC = 6
"""The compression format is EVC."""
"""Equivalent to H.266. Defined in ISO/IEC 23094-1."""
JPEG2000 = 7
"""The compression format is JPEG200 ISO/IEC 15444-16:2021"""
UNCOMPRESSED = 8
"""Defined in ISO/IEC 23001-17:2023 (Final Draft International Standard)."""
MASK = 9
"""Mask image encoding. See ISO/IEC 23008-12:2022 Section 6.10.2"""


class HeifColorPrimaries(IntEnum):
Expand Down
6 changes: 5 additions & 1 deletion pillow_heif/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,11 @@ class CtxEncode:

def __init__(self, compression_format: HeifCompressionFormat, **kwargs):
quality = kwargs.get("quality", options.QUALITY)
self.ctx_write = _pillow_heif.CtxWrite(compression_format, -2 if quality is None else quality)
self.ctx_write = _pillow_heif.CtxWrite(
compression_format,
-2 if quality is None else quality,
options.PREFERRED_ENCODER.get("HEIF" if compression_format == HeifCompressionFormat.HEVC else "AVIF", ""),
)
enc_params = kwargs.get("enc_params", {})
chroma = kwargs.get("chroma", None)
if chroma is None and "subsampling" in kwargs:
Expand Down
7 changes: 7 additions & 0 deletions pillow_heif/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,10 @@
.. note:: `save_nclx_profile` specified during calling ``save`` has higher priority than this.

When use pillow_heif as a plugin you can unset it with: `register_*_opener(save_nclx_profile=False)`"""


PREFERRED_ENCODER = {
"AVIF": "",
"HEIF": "",
}
"""Use the specified encoder for format. You can get the available encoders IDs using ``libheif_info()`` function."""
51 changes: 51 additions & 0 deletions tests/write_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,3 +581,54 @@ def test_lossless_encoding_rgba(save_format):
buf = BytesIO()
im_rgb.save(buf, format=save_format, quality=-1, chroma=444, matrix_coefficients=0)
helpers.assert_image_equal(im_rgb, Image.open(buf))


def test_invalid_encoder():
im_rgb = helpers.gradient_rgba()
buf = BytesIO()
try:
pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "invalid_id"
pillow_heif.options.PREFERRED_ENCODER["HEIF"] = "invalid_id"
with pytest.raises(RuntimeError):
im_rgb.save(buf, format="AVIF")
with pytest.raises(RuntimeError):
im_rgb.save(buf, format="HEIF")
finally:
pillow_heif.options.PREFERRED_ENCODER["AVIF"] = ""
pillow_heif.options.PREFERRED_ENCODER["HEIF"] = ""


@pytest.mark.skipif("svt" not in pillow_heif.libheif_info()["encoders"], reason="Requires SVT AVIF encoder.")
@pytest.mark.skipif("aom" not in pillow_heif.libheif_info()["encoders"], reason="Requires AOM AVIF encoder.")
def test_svt_encoder():
im_rgb = helpers.gradient_rgb()
buf_aom = BytesIO()
im_rgb.save(buf_aom, format="AVIF")
buf_svt = BytesIO()
try:
pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "svt"
im_rgb.save(buf_svt, format="AVIF")
finally:
pillow_heif.options.PREFERRED_ENCODER["AVIF"] = ""
aom_img_data = Image.open(buf_aom).tobytes()
svt_image_data = Image.open(buf_svt).tobytes()
# print(f"AOM size: {len(aom_img_data)} , SVT size: {len(svt_image_data)}", )
assert aom_img_data != svt_image_data # Suppose that: different decoders by default will have different results


@pytest.mark.skipif("rav1e" not in pillow_heif.libheif_info()["encoders"], reason="Requires RAV1E AVIF encoder.")
@pytest.mark.skipif("aom" not in pillow_heif.libheif_info()["encoders"], reason="Requires AOM AVIF encoder.")
def test_rav1e_encoder():
im_rgb = helpers.gradient_rgb()
buf_aom = BytesIO()
im_rgb.save(buf_aom, format="AVIF")
buf_rav1e = BytesIO()
try:
pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "rav1e"
im_rgb.save(buf_rav1e, format="AVIF")
finally:
pillow_heif.options.PREFERRED_ENCODER["AVIF"] = ""
aom_img_data = Image.open(buf_aom).tobytes()
rav1e_image_data = Image.open(buf_rav1e).tobytes()
# print(f"AOM size: {len(aom_img_data)} , RAV1E size: {len(rav1e_image_data)}", )
assert aom_img_data != rav1e_image_data # Suppose that: different decoders by default will have different results