Skip to content

Commit

Permalink
added preferred-encoder global option. (#192)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Piskun <[email protected]>
  • Loading branch information
bigcat88 authored Jan 6, 2024
1 parent 999f7a2 commit 17e7bb2
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 11 deletions.
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

0 comments on commit 17e7bb2

Please sign in to comment.