Skip to content

Commit

Permalink
feat: Use new libavif quality and qualityAlpha encoder options
Browse files Browse the repository at this point in the history
These replace the (now deprecated) qmin and qmax options
  • Loading branch information
fdintino committed Sep 11, 2023
1 parent ae0a969 commit abee2a4
Show file tree
Hide file tree
Showing 3 changed files with 18 additions and 50 deletions.
27 changes: 8 additions & 19 deletions src/pillow_avif/AvifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,23 +123,12 @@ def _save(im, fp, filename, save_all=False):

is_single_frame = total == 1

qmin = info.get("qmin")
qmax = info.get("qmax")

if qmin is None and qmax is None:
# The min and max quantizer settings in libavif range from 0 (best quality)
# to 63 (worst quality). If neither are explicitly specified, we use a 0-100
# quality scale (default 75) and calculate the qmin and qmax from that.
#
# - qmin is 0 for quality >= 64. Below that, qmin has an inverse linear
# relation to quality (i.e., quality 63 = qmin 1, quality 0 => qmin 63)
# - qmax is 0 for quality=100, then qmax increases linearly relative to
# quality decreasing, until it flattens out at quality=37.
quality = info.get("quality", 75)
if not isinstance(quality, int):
raise ValueError("Invalid quality setting")
qmin = max(0, min(64 - quality, 63))
qmax = max(0, min(100 - quality, 63))
quality = info.get("quality", 75)
if not isinstance(quality, int) or quality < 0 or quality > 100:
raise ValueError("Invalid quality setting")
quality_alpha = info.get("quality_alpha", 100)
if not isinstance(quality_alpha, int) or quality_alpha < 0 or quality_alpha > 100:
raise ValueError("Invalid quality_alpha setting")

duration = info.get("duration", 0)
subsampling = info.get("subsampling", "4:2:0")
Expand Down Expand Up @@ -184,8 +173,8 @@ def _save(im, fp, filename, save_all=False):
im.size[0],
im.size[1],
subsampling,
qmin,
qmax,
quality,
quality_alpha,
speed,
codec,
range_,
Expand Down
20 changes: 10 additions & 10 deletions src/pillow_avif/_avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

typedef struct {
avifPixelFormat subsampling;
int qmin;
int qmax;
int quality;
int quality_alpha;
int speed;
avifCodecChoice codec;
avifRange range;
Expand Down Expand Up @@ -203,8 +203,8 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
avifEncoder *encoder = NULL;

char *subsampling = "4:2:0";
int qmin = AVIF_QUANTIZER_BEST_QUALITY; // =0
int qmax = 10; // "High Quality", but not lossless
int quality = 75;
int quality_alpha = 100;
int speed = 8;
PyObject *icc_bytes;
PyObject *exif_bytes;
Expand All @@ -225,8 +225,8 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
&width,
&height,
&subsampling,
&qmin,
&qmax,
&quality,
&quality_alpha,
&speed,
&codec,
&range,
Expand Down Expand Up @@ -254,8 +254,8 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
return NULL;
}

enc_options.qmin = normalize_quantize_value(qmin);
enc_options.qmax = normalize_quantize_value(qmax);
enc_options.quality = quality;
enc_options.quality_alpha = quality_alpha;

if (speed < AVIF_SPEED_SLOWEST) {
speed = AVIF_SPEED_SLOWEST;
Expand Down Expand Up @@ -321,8 +321,8 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
}

encoder->maxThreads = max_threads;
encoder->minQuantizer = enc_options.qmin;
encoder->maxQuantizer = enc_options.qmax;
encoder->quality = enc_options.quality;
encoder->qualityAlpha = enc_options.quality_alpha;
encoder->codecChoice = enc_options.codec;
encoder->speed = enc_options.speed;
encoder->timescale = (uint64_t)1000;
Expand Down
21 changes: 0 additions & 21 deletions tests/test_file_avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,27 +494,6 @@ def test_encoder_codec_available_cannot_decode(self):
def test_encoder_codec_available_invalid(self):
assert _avif.encoder_codec_available("foo") is False

@pytest.mark.parametrize(
"quality,expected_qminmax",
[
[0, (63, 63)],
[100, (0, 0)],
[90, (0, 10)],
[None, (0, 25)], # default
[50, (14, 50)],
],
)
def test_encoder_quality_qmin_qmax_map(self, tmp_path, quality, expected_qminmax):
MockEncoder = mock.Mock(wraps=_avif.AvifEncoder)
with mock.patch.object(_avif, "AvifEncoder", new=MockEncoder) as mock_encoder:
with Image.open("tests/images/hopper.avif") as im:
test_file = str(tmp_path / "temp.avif")
if quality is None:
im.save(test_file)
else:
im.save(test_file, quality=quality)
assert mock_encoder.call_args[0][3:5] == expected_qminmax

def test_encoder_quality_valueerror(self, tmp_path):
with Image.open("tests/images/hopper.avif") as im:
test_file = str(tmp_path / "temp.avif")
Expand Down

0 comments on commit abee2a4

Please sign in to comment.