Skip to content

Commit

Permalink
Add encoder advanced codec-specific options
Browse files Browse the repository at this point in the history
  • Loading branch information
fdintino committed Jul 18, 2021
1 parent 20b0edd commit 14b34ba
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 3 deletions.
19 changes: 19 additions & 0 deletions src/pillow_avif/AvifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,24 @@ def _save(im, fp, filename, save_all=False):
if isinstance(xmp, text_type):
xmp = xmp.encode('utf-8')

advanced = info.get("advanced")
if isinstance(advanced, dict):
advanced = tuple([k, v] for (k, v) in advanced.items())
if advanced is not None:
try:
advanced = tuple(advanced)
except TypeError:
invalid = True
else:
invalid = all(isinstance(v, tuple) and len(v) == 2 for v in advanced)
if invalid:
raise ValueError(
"advanced codec options must be a dict of key-value string "
"pairs or a series of key-value two-tuples")
advanced = tuple([
(str(k).encode("utf-8"), str(v).encode("utf-8"))
for k, v in advanced])

# Setup the AVIF encoder
enc = _avif.AvifEncoder(
im.size[0],
Expand All @@ -162,6 +180,7 @@ def _save(im, fp, filename, save_all=False):
icc_profile or b"",
exif or b"",
xmp or b"",
advanced
)

# Add each frame
Expand Down
40 changes: 37 additions & 3 deletions src/pillow_avif/_avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,12 @@ normalize_tiles_log2(int value) {
}
}


static PyObject *
exc_type_for_avif_result(avifResult result) {
switch (result) {
case AVIF_RESULT_INVALID_FTYP:
case AVIF_RESULT_INVALID_EXIF_PAYLOAD:
case AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION:
return PyExc_ValueError;
case AVIF_RESULT_BMFF_PARSE_FAILED:
case AVIF_RESULT_TRUNCATED_DATA:
Expand Down Expand Up @@ -167,6 +167,32 @@ _encoder_codec_available(PyObject *self, PyObject *args) {
return PyBool_FromLong(is_available);
}

static void
_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) {
Py_ssize_t i, size;
PyObject *keyval, *py_key, *py_val;
char *key, *val;
if (!PyTuple_Check(opts)) {
return;
}
size = PyTuple_GET_SIZE(opts);

for (i = 0; i < size; i++) {
keyval = PyTuple_GetItem(opts, i);
if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) {
return;
}
py_key = PyTuple_GetItem(keyval, 0);
py_val = PyTuple_GetItem(keyval, 1);
if (!PyBytes_Check(py_key) || !PyBytes_Check(py_val)) {
return;
}
key = PyBytes_AsString(py_key);
val = PyBytes_AsString(py_val);
avifEncoderSetCodecSpecificOption(encoder, key, val);
}
}

// Encoder functions
PyObject *
AvifEncoderNew(PyObject *self_, PyObject *args) {
Expand All @@ -189,9 +215,11 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
char *codec = "auto";
char *range = "full";

PyObject *advanced;

if (!PyArg_ParseTuple(
args,
"IIsiiissiiOSSS",
"IIsiiissiiOSSSO",
&width,
&height,
&subsampling,
Expand All @@ -205,7 +233,8 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
&alpha_premultiplied,
&icc_bytes,
&exif_bytes,
&xmp_bytes)) {
&xmp_bytes,
&advanced)) {
return NULL;
}

Expand Down Expand Up @@ -294,6 +323,11 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
encoder->timescale = (uint64_t)1000;
encoder->tileRowsLog2 = enc_options.tile_rows_log2;
encoder->tileColsLog2 = enc_options.tile_cols_log2;

#if AVIF_VERSION >= 80200
_add_codec_specific_options(encoder, advanced);
#endif

self->encoder = encoder;

avifImage *image = avifImageCreateEmpty();
Expand Down
22 changes: 22 additions & 0 deletions tests/test_file_avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,28 @@ def test_encoder_codec_cannot_encode(self, tmp_path):
with pytest.raises(ValueError):
im.save(test_file, codec="dav1d")

@skip_unless_avif_encoder("aom")
@skip_unless_avif_version_gte((0, 8, 2))
def test_encoder_advanced_codec_options(self):
with Image.open(TEST_AVIF_FILE) as im:
ctrl_buf = BytesIO()
im.save(ctrl_buf, "AVIF", codec="aom")
test_buf = BytesIO()
im.save(test_buf, "AVIF", codec="aom", advanced={
"aq-mode": "1",
"enable-chroma-deltaq": "1",
})
assert ctrl_buf.getvalue() != test_buf.getvalue()

@skip_unless_avif_encoder("aom")
@skip_unless_avif_version_gte((0, 8, 2))
@pytest.mark.parametrize("val", [{"foo": "bar"}, 1234])
def test_encoder_advanced_codec_options_invalid(self, tmp_path, val):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(ValueError):
im.save(test_file, codec="aom", advanced=val)

@skip_unless_avif_decoder("aom")
def test_decoder_codec_param(self):
AvifImagePlugin.DECODE_CODEC_CHOICE = "aom"
Expand Down

0 comments on commit 14b34ba

Please sign in to comment.