Skip to content
Closed
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: 0 additions & 3 deletions .github/workflows/macos-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ if [[ "$ImageOS" == "macos13" ]]; then
fi
brew install \
aom \
dav1d \
freetype \
ghostscript \
jpeg-turbo \
Expand All @@ -16,8 +15,6 @@ brew install \
libtiff \
little-cms2 \
openjpeg \
rav1e \
svt-av1 \
webp
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"

Expand Down
14 changes: 1 addition & 13 deletions .github/workflows/wheels-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -142,11 +132,9 @@ 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 \
-DCMAKE_BUILD_TYPE=MinSizeRel \
. \
&& make install)
touch libavif-stamp
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
125 changes: 15 additions & 110 deletions Tests/test_file_avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,6 @@
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")
Expand Down Expand Up @@ -99,15 +85,12 @@
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

Check warning on line 93 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L93

Added line #L93 was not covered by tests

def test_read(self) -> None:
"""
Expand Down Expand Up @@ -146,14 +129,14 @@

# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.14
)

# This test asserts that the images are similar. If the average pixel
# difference between the two images is less than the epsilon value,
# then we're going to accept that it's a reasonable lossy version of
# the image.
assert_image_similar(reloaded, im, 8.62)
assert_image_similar(reloaded, im, 8.65)

def test_AvifEncoder_with_invalid_args(self) -> None:
"""
Expand Down Expand Up @@ -181,6 +164,10 @@
"""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
Expand Down Expand Up @@ -434,26 +421,6 @@
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",
[
Expand All @@ -470,86 +437,30 @@
) -> 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
) -> None:
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:
test_file = tmp_path / "temp.avif"
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)
Expand All @@ -570,16 +481,10 @@
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")
Expand Down
20 changes: 0 additions & 20 deletions depends/install_libavif.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 1 addition & 7 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 3 additions & 12 deletions docs/installation/building-from-source.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should only consider removing dav1d as a last resort. Although the decoder in libaom is the reference implementation and we test it with open-source and commercial AV1 test vectors and fuzzing regularly at Google, dav1d is likely to have better software security because it is being used in far more applications, including some that perform additional fuzzing of dav1d.


.. tab:: Linux

Expand Down Expand Up @@ -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.
Expand Down
16 changes: 4 additions & 12 deletions src/PIL/AvifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@
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

Expand Down Expand Up @@ -73,14 +70,11 @@
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"

Check warning on line 74 in src/PIL/AvifImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/AvifImagePlugin.py#L74

Added line #L74 was not covered by tests
raise ValueError(msg)
self._decoder = _avif.AvifDecoder(
self.fp.read(),
DECODE_CODEC_CHOICE,
_get_default_max_threads(),
)

Expand Down Expand Up @@ -165,9 +159,8 @@
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"

Check warning on line 163 in src/PIL/AvifImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/AvifImagePlugin.py#L163

Added line #L163 was not covered by tests
raise ValueError(msg)
range_ = info.get("range", "full")
tile_rows_log2 = info.get("tile_rows", 0)
Expand Down Expand Up @@ -218,7 +211,6 @@
quality,
speed,
max_threads,
codec,
range_,
tile_rows_log2,
tile_cols_log2,
Expand Down
Loading
Loading