This commit is contained in:
Andrew Murray 2025-04-04 18:36:19 +03:00 committed by GitHub
commit 50d565dfb5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 26 additions and 289 deletions

View File

@ -7,7 +7,6 @@ if [[ "$ImageOS" == "macos13" ]]; then
fi
brew install \
aom \
dav1d \
freetype \
ghostscript \
jpeg-turbo \
@ -16,8 +15,6 @@ brew install \
libtiff \
little-cms2 \
openjpeg \
rav1e \
svt-av1 \
webp
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"

View File

@ -122,16 +122,6 @@ function build_libavif {
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
fi
# For rav1e
curl https://sh.rustup.rs -sSf | sh -s -- -y
. "$HOME/.cargo/env"
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
yum install -y perl
if [[ "$MB_ML_VER" == 2014 ]]; then
yum install -y perl-IPC-Cmd
fi
fi
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
(cd $out_dir \
&& CMAKE_POLICY_VERSION_MINIMUM=3.5 cmake \
@ -142,9 +132,6 @@ function build_libavif {
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_AOM=LOCAL \
-DAVIF_CODEC_DAV1D=LOCAL \
-DAVIF_CODEC_RAV1E=LOCAL \
-DAVIF_CODEC_SVT=LOCAL \
-DENABLE_NASM=ON \
-DCMAKE_MODULE_PATH=/tmp/cmake/Modules \
. \

View File

@ -160,11 +160,6 @@ jobs:
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
shell: pwsh
- name: Update rust
if: matrix.cibw_arch == 'AMD64'
run: |
rustup update
- name: Build wheels
run: |
setlocal EnableDelayedExpansion

View File

@ -51,20 +51,6 @@ def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile:
return Image.open(out)
def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator:
reason = f"{codec_name} decode not available"
return pytest.mark.skipif(
not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason
)
def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator:
reason = f"{codec_name} encode not available"
return pytest.mark.skipif(
not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason
)
def is_docker_qemu() -> bool:
try:
init_proc_exe = os.readlink("/proc/1/exe")
@ -99,15 +85,12 @@ class TestFileAvif:
def test_codec_version(self) -> None:
assert AvifImagePlugin.get_codec_version("unknown") is None
for codec_name in ("aom", "dav1d", "rav1e", "svt"):
codec_version = AvifImagePlugin.get_codec_version(codec_name)
if _avif.decoder_codec_available(
codec_name
) or _avif.encoder_codec_available(codec_name):
assert codec_version is not None
assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version)
else:
assert codec_version is None
codec_version = AvifImagePlugin.get_codec_version("aom")
if _avif.decoder_codec_available() or _avif.encoder_codec_available():
assert codec_version is not None
assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version)
else:
assert codec_version is None
def test_read(self) -> None:
"""
@ -181,6 +164,10 @@ class TestFileAvif:
"""Save should raise an OSError if AvifEncoder.finish returns None"""
class _mock_avif:
@classmethod
def encoder_codec_available(cls) -> bool:
return True
class AvifEncoder:
def __init__(self, *args: Any) -> None:
pass
@ -434,26 +421,6 @@ class TestFileAvif:
with pytest.raises(ValueError):
im.save(test_file, range="foo")
@skip_unless_avif_encoder("aom")
def test_encoder_codec_param(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, codec="aom")
def test_encoder_codec_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, codec="foo")
@skip_unless_avif_decoder("dav1d")
def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, codec="dav1d")
@skip_unless_avif_encoder("aom")
@pytest.mark.parametrize(
"advanced",
[
@ -470,17 +437,15 @@ class TestFileAvif:
) -> None:
with Image.open(TEST_AVIF_FILE) as im:
ctrl_buf = BytesIO()
im.save(ctrl_buf, "AVIF", codec="aom")
im.save(ctrl_buf, "AVIF")
test_buf = BytesIO()
im.save(
test_buf,
"AVIF",
codec="aom",
advanced=advanced,
)
assert ctrl_buf.getvalue() != test_buf.getvalue()
@skip_unless_avif_encoder("aom")
@pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234])
def test_encoder_advanced_codec_options_invalid(
self, tmp_path: Path, advanced: dict[str, str] | int
@ -488,46 +453,7 @@ class TestFileAvif:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, codec="aom", advanced=advanced)
@skip_unless_avif_decoder("aom")
def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom")
with Image.open(TEST_AVIF_FILE) as im:
assert im.size == (128, 128)
@skip_unless_avif_encoder("rav1e")
def test_encoder_codec_cannot_decode(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e")
with pytest.raises(ValueError):
with Image.open(TEST_AVIF_FILE):
pass
def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo")
with pytest.raises(ValueError):
with Image.open(TEST_AVIF_FILE):
pass
@skip_unless_avif_encoder("aom")
def test_encoder_codec_available(self) -> None:
assert _avif.encoder_codec_available("aom") is True
def test_encoder_codec_available_bad_params(self) -> None:
with pytest.raises(TypeError):
_avif.encoder_codec_available()
@skip_unless_avif_decoder("dav1d")
def test_encoder_codec_available_cannot_decode(self) -> None:
assert _avif.encoder_codec_available("dav1d") is False
def test_encoder_codec_available_invalid(self) -> None:
assert _avif.encoder_codec_available("foo") is False
im.save(test_file, advanced=advanced)
def test_encoder_quality_valueerror(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
@ -535,21 +461,6 @@ class TestFileAvif:
with pytest.raises(ValueError):
im.save(test_file, quality="invalid")
@skip_unless_avif_decoder("aom")
def test_decoder_codec_available(self) -> None:
assert _avif.decoder_codec_available("aom") is True
def test_decoder_codec_available_bad_params(self) -> None:
with pytest.raises(TypeError):
_avif.decoder_codec_available()
@skip_unless_avif_encoder("rav1e")
def test_decoder_codec_available_cannot_decode(self) -> None:
assert _avif.decoder_codec_available("rav1e") is False
def test_decoder_codec_available_invalid(self) -> None:
assert _avif.decoder_codec_available("foo") is False
def test_p_mode_transparency(self, tmp_path: Path) -> None:
im = Image.new("P", size=(64, 64))
draw = ImageDraw.Draw(im)
@ -570,16 +481,10 @@ class TestFileAvif:
with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im:
assert im.size == (128, 128)
@skip_unless_avif_encoder("aom")
@pytest.mark.parametrize("speed", [-1, 1, 11])
def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None:
test_file = tmp_path / "temp.avif"
hopper().save(test_file, codec="aom", speed=speed)
@skip_unless_avif_encoder("svt")
def test_svt_optimizations(self, tmp_path: Path) -> None:
test_file = tmp_path / "temp.avif"
hopper().save(test_file, codec="svt", speed=1)
hopper().save(test_file, speed=speed)
@skip_unless_feature("avif")

View File

@ -25,26 +25,6 @@ if $PKGCONFIG --exists aom; then
HAS_DECODER=1
fi
if $PKGCONFIG --exists dav1d; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists rav1e; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
HAS_ENCODER=1
fi
if $PKGCONFIG --exists SvtAv1Enc; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
HAS_ENCODER=1
fi
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
fi

View File

@ -50,17 +50,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
Quality/speed trade-off (0=slower/better, 10=fastest). Defaults to 6.
**max_threads**
Limit the number of active threads used. By default, there is no limit. If the aom
codec is used, there is a maximum of 64.
Limit the number of active threads used. There is a maximum of 64.
**range**
YUV range, either "full" or "limited". Defaults to "full".
**codec**
AV1 codec to use for encoding. Specific values are "aom", "rav1e", and
"svt", presuming the chosen codec is available. Defaults to "auto", which
will choose the first available codec in the order of the preceding list.
**tile_rows** / **tile_cols**
For tile encoding, the (log 2) number of tile rows and columns to use.
Valid values are 0-6, default 0. Ignored if "autotiling" is set to true.

View File

@ -92,10 +92,9 @@ Many of Pillow's features require external libraries:
* **libavif** provides support for the AVIF format.
* Pillow requires libavif version **1.0.0** or greater.
* libavif is merely an API that wraps AVIF codecs. If you are compiling
libavif from source, you will also need to install both an AVIF encoder
and decoder, such as rav1e and dav1d, or libaom, which both encodes and
decodes AVIF images.
* libavif is merely an API that wraps AVIF codecs. If you are compiling libavif from
source, you will also need to install libaom, which both encodes and decodes AVIF
images.
.. tab:: Linux
@ -164,14 +163,6 @@ Many of Pillow's features require external libraries:
brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp
If you would like to use libavif with more codecs than just aom, then
instead of installing libavif through Homebrew directly, you can use
Homebrew to install libavif's build dependencies::
brew install aom dav1d rav1e svt-av1
Then see ``depends/install_libavif.sh`` to install libavif.
.. tab:: Windows
We recommend you use prebuilt wheels from PyPI.

View File

@ -13,9 +13,6 @@ try:
except ImportError:
SUPPORTED = False
# Decoder options as module globals, until there is a way to pass parameters
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
DECODE_CODEC_CHOICE = "auto"
# Decoding is only affected by this for libavif **0.8.4** or greater.
DEFAULT_MAX_THREADS = 0
@ -73,14 +70,11 @@ class AvifImageFile(ImageFile.ImageFile):
msg = "image file could not be opened because AVIF support not installed"
raise SyntaxError(msg)
if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available(
DECODE_CODEC_CHOICE
):
msg = "Invalid opening codec"
if not _avif.decoder_codec_available():
msg = "Codec not available"
raise ValueError(msg)
self._decoder = _avif.AvifDecoder(
self.fp.read(),
DECODE_CODEC_CHOICE,
_get_default_max_threads(),
)
@ -165,9 +159,8 @@ def _save(
subsampling = info.get("subsampling", "4:2:0")
speed = info.get("speed", 6)
max_threads = info.get("max_threads", _get_default_max_threads())
codec = info.get("codec", "auto")
if codec != "auto" and not _avif.encoder_codec_available(codec):
msg = "Invalid saving codec"
if not _avif.encoder_codec_available():
msg = "Codec not available"
raise ValueError(msg)
range_ = info.get("range", "full")
tile_rows_log2 = info.get("tile_rows", 0)
@ -218,7 +211,6 @@ def _save(
quality,
speed,
max_threads,
codec,
range_,
tile_rows_log2,
tile_cols_log2,

View File

@ -142,21 +142,13 @@ _codec_available(const char *name, avifCodecFlags flags) {
PyObject *
_decoder_codec_available(PyObject *self, PyObject *args) {
char *codec_name;
if (!PyArg_ParseTuple(args, "s", &codec_name)) {
return NULL;
}
int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_DECODE);
int is_available = _codec_available("aom", AVIF_CODEC_FLAG_CAN_DECODE);
return PyBool_FromLong(is_available);
}
PyObject *
_encoder_codec_available(PyObject *self, PyObject *args) {
char *codec_name;
if (!PyArg_ParseTuple(args, "s", &codec_name)) {
return NULL;
}
int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_ENCODE);
int is_available = _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE);
return PyBool_FromLong(is_available);
}
@ -229,7 +221,6 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
int tile_rows_log2;
int tile_cols_log2;
char *codec;
char *range;
PyObject *advanced;
@ -237,14 +228,13 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
if (!PyArg_ParseTuple(
args,
"(II)siiissiippy*y*iy*O",
"(II)siiisiippy*y*iy*O",
&width,
&height,
&subsampling,
&quality,
&speed,
&max_threads,
&codec,
&range,
&tile_rows_log2,
&tile_cols_log2,
@ -310,18 +300,10 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
goto end;
}
int is_aom_encode = strcmp(codec, "aom") == 0 ||
(strcmp(codec, "auto") == 0 &&
_codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE));
encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads;
encoder->maxThreads = max_threads > 64 ? 64 : max_threads;
encoder->quality = quality;
if (strcmp(codec, "auto") == 0) {
encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO;
} else {
encoder->codecChoice = avifCodecChoiceFromName(codec);
}
if (speed < AVIF_SPEED_SLOWEST) {
speed = AVIF_SPEED_SLOWEST;
} else if (speed > AVIF_SPEED_FASTEST) {
@ -616,22 +598,14 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
AvifDecoderObject *self = NULL;
avifDecoder *decoder;
char *codec_str;
avifCodecChoice codec;
int max_threads;
avifResult result;
if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) {
if (!PyArg_ParseTuple(args, "y*i", &buffer, &max_threads)) {
return NULL;
}
if (strcmp(codec_str, "auto") == 0) {
codec = AVIF_CODEC_CHOICE_AUTO;
} else {
codec = avifCodecChoiceFromName(codec_str);
}
self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type);
if (!self) {
PyErr_SetString(PyExc_RuntimeError, "could not create decoder object");
@ -653,7 +627,6 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
// items. libheif v1.11.0 and older does not add the 'pixi' item property to
// AV1 image items.
decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED;
decoder->codecChoice = codec;
result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len);
if (result != AVIF_RESULT_OK) {

View File

@ -1,23 +0,0 @@
Copyright © 2018-2019, VideoLAN and dav1d authors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,25 +0,0 @@
BSD 2-Clause License
Copyright (c) 2017-2023, the rav1e contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,26 +0,0 @@
Copyright (c) 2019, Alliance for Open Media. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@ -392,9 +392,6 @@ DEPS: dict[str, dict[str, Any]] = {
"-DAVIF_LIBSHARPYUV=LOCAL",
"-DAVIF_LIBYUV=LOCAL",
"-DAVIF_CODEC_AOM=LOCAL",
"-DAVIF_CODEC_DAV1D=LOCAL",
"-DAVIF_CODEC_RAV1E=LOCAL",
"-DAVIF_CODEC_SVT=LOCAL",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
),
cmd_xcopy("include", "{inc_dir}"),