Skip to content

Commit

Permalink
Merge pull request #10621 from rouault/avif
Browse files Browse the repository at this point in the history
Add read/write AVIF raster driver
  • Loading branch information
rouault committed Sep 10, 2024
2 parents feb0e99 + 272efa8 commit 54bec3d
Show file tree
Hide file tree
Showing 35 changed files with 1,980 additions and 12 deletions.
1 change: 1 addition & 0 deletions .github/workflows/alpine/Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ RUN apk add \
kealib-dev \
libaec-dev \
libarchive-dev \
libavif-dev \
libdeflate-dev \
libgeotiff-dev \
libheif-dev \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/alpine_32bit/Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ RUN apk add \
kealib-dev \
libaec-dev \
libarchive-dev \
libavif-dev \
libdeflate-dev \
libgeotiff-dev \
libheif-dev \
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/cmake_builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ jobs:
mingw-w64-x86_64-geos mingw-w64-x86_64-libspatialite mingw-w64-x86_64-proj
mingw-w64-x86_64-cgal mingw-w64-x86_64-libfreexl mingw-w64-x86_64-hdf5 mingw-w64-x86_64-netcdf mingw-w64-x86_64-poppler mingw-w64-x86_64-podofo mingw-w64-x86_64-postgresql
mingw-w64-x86_64-libgeotiff mingw-w64-x86_64-libpng mingw-w64-x86_64-libtiff mingw-w64-x86_64-openjpeg2
mingw-w64-x86_64-python-pip mingw-w64-x86_64-python-numpy mingw-w64-x86_64-python-pytest mingw-w64-x86_64-python-setuptools mingw-w64-x86_64-python-lxml mingw-w64-x86_64-swig mingw-w64-x86_64-python-psutil mingw-w64-x86_64-blosc
mingw-w64-x86_64-python-pip mingw-w64-x86_64-python-numpy mingw-w64-x86_64-python-pytest mingw-w64-x86_64-python-setuptools mingw-w64-x86_64-python-lxml mingw-w64-x86_64-swig mingw-w64-x86_64-python-psutil mingw-w64-x86_64-blosc mingw-w64-x86_64-libavif
- name: Setup cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache
Expand Down Expand Up @@ -429,7 +429,7 @@ jobs:
cfitsio freexl geotiff libjpeg-turbo libpq libspatialite libwebp-base pcre pcre2 postgresql \
sqlite tiledb zstd cryptopp cgal doxygen librttopo libkml openssl xz \
openjdk ant qhull armadillo blas blas-devel libblas libcblas liblapack liblapacke blosc libarchive \
arrow-cpp pyarrow libaec cmake
arrow-cpp pyarrow libaec libavif cmake
- name: Check CMake version
shell: bash -l {0}
run: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/fedora_rawhide/Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ RUN dnf install -y clang make diffutils ccache cmake \
armadillo-devel qhull-devel \
hdf-devel hdf5-devel netcdf-devel \
libpq-devel \
libavif-devel \
python3-setuptools python3-pip python3-devel python3-lxml swig \
glibc-gconv-extra

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ubuntu_22.04/Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ RUN apt-get update && \
g++ \
git \
gpsbabel \
libavif-dev \
libblosc-dev \
libboost-dev \
libcairo2-dev \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ubuntu_24.04/Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ RUN apt-get update && \
g++ \
git \
gpsbabel \
libavif-dev \
libblosc-dev \
libboost-dev \
libcairo2-dev \
Expand Down
273 changes: 273 additions & 0 deletions autotest/gdrivers/avif.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
#!/usr/bin/env pytest
# -*- coding: utf-8 -*-
###############################################################################
#
# Project: GDAL/OGR Test Suite
# Purpose: Test AVIF driver
# Author: Even Rouault, <even dot rouault at spatialys.com>
#
###############################################################################
# Copyright (c) 2024, Even Rouault <even dot rouault at spatialys.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
###############################################################################

import base64
import shutil

import gdaltest
import pytest

from osgeo import gdal

pytestmark = pytest.mark.require_driver("AVIF")


def has_avif_encoder():
drv = gdal.GetDriverByName("AVIF")
return drv is not None and drv.GetMetadataItem("DMD_CREATIONOPTIONLIST") is not None


def test_avif_subdatasets(tmp_path):

filename = str(tmp_path / "out.avif")
shutil.copy("data/avif/colors-animated-8bpc-alpha-exif-xmp.avif", filename)

ds = gdal.Open(filename)
assert ds
assert len(ds.GetSubDatasets()) == 5
subds1_name = ds.GetSubDatasets()[0][0]
subds2_name = ds.GetSubDatasets()[1][0]

ds = gdal.Open(subds1_name)
assert ds
assert ds.RasterXSize == 150
assert ds.GetRasterBand(1).GetMetadataItem("STATISTICS_MINIMUM") is None
assert ds.GetRasterBand(1).ComputeStatistics(False)
assert ds.GetRasterBand(1).GetMetadataItem("STATISTICS_MINIMUM") is not None
ds.Close()

ds = gdal.Open(subds1_name)
assert ds.GetRasterBand(1).GetMetadataItem("STATISTICS_MINIMUM") is not None

ds = gdal.Open(subds2_name)
assert ds
assert ds.RasterXSize == 150
assert ds.GetRasterBand(1).GetMetadataItem("STATISTICS_MINIMUM") is None

with pytest.raises(Exception):
gdal.Open(f"AVIF:0:{filename}")
with pytest.raises(Exception):
gdal.Open(f"AVIF:6:{filename}")
with pytest.raises(Exception):
gdal.Open("AVIF:1:non_existing.heic")
with pytest.raises(Exception):
gdal.Open("AVIF:")
with pytest.raises(Exception):
gdal.Open("AVIF:1")
with pytest.raises(Exception):
gdal.Open("AVIF:1:")


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
def test_avif_single_band():
tst = gdaltest.GDALTest(
"AVIF",
"byte.tif",
1,
4672,
)
tst.testCreateCopy(vsimem=1, check_checksum_not_null=True, check_minmax=False)


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
@pytest.mark.require_driver("PNG")
def test_avif_gray_alpha():
tst = gdaltest.GDALTest(
"AVIF",
"wms/gray+alpha.png",
1,
39910,
)
tst.testCreateCopy(vsimem=1, check_checksum_not_null=True, check_minmax=False)


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
def test_avif_rgb():
tst = gdaltest.GDALTest(
"AVIF",
"rgbsmall.tif",
1,
21212,
options=["QUALITY=100", "NUM_THREADS=1"],
)
tst.testCreateCopy(vsimem=1)


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
def test_avif_rgba():
tst = gdaltest.GDALTest(
"AVIF",
"../../gcore/data/stefan_full_rgba.tif",
1,
12603,
options=["QUALITY=100"],
)
tst.testCreateCopy(vsimem=1)


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
def test_avif_uint16():
tst = gdaltest.GDALTest(
"AVIF", "../../gcore/data/uint16.tif", 1, 4672, options=["NBITS=10"]
)
tst.testCreateCopy(vsimem=1, check_checksum_not_null=True, check_minmax=False)


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
@pytest.mark.parametrize("yuv_subsampling", ["444", "422", "420"])
def test_avif_yuv_subsampling(tmp_vsimem, yuv_subsampling):

src_ds = gdal.Open("data/rgbsmall.tif")
out_filename = str(tmp_vsimem / "out.avif")
gdal.GetDriverByName("AVIF").CreateCopy(
out_filename, src_ds, options=["YUV_SUBSAMPLING=" + yuv_subsampling]
)
ds = gdal.Open(out_filename)
assert ds.GetMetadataItem("YUV_SUBSAMPLING", "IMAGE_STRUCTURE") == yuv_subsampling


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
def test_avif_nbits_from_src_ds(tmp_vsimem):

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1, gdal.GDT_UInt16)
src_ds.GetRasterBand(1).SetMetadataItem("NBITS", "12", "IMAGE_STRUCTURE")
out_filename = str(tmp_vsimem / "out.avif")
gdal.GetDriverByName("AVIF").CreateCopy(out_filename, src_ds)
ds = gdal.Open(out_filename)
assert ds.GetRasterBand(1).GetMetadataItem("NBITS", "IMAGE_STRUCTURE") == "12"


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
def test_avif_exif_xmp(tmp_vsimem):

src_ds = gdal.Open("data/avif/colors-animated-8bpc-alpha-exif-xmp.avif")
out_filename = str(tmp_vsimem / "out.avif")
gdal.GetDriverByName("AVIF").CreateCopy(out_filename, src_ds)
if gdal.VSIStatL(out_filename + ".aux.xml"):
gdal.Unlink(out_filename + ".aux.xml")
ds = gdal.Open(out_filename)
exif_mdd = ds.GetMetadata("EXIF")
assert exif_mdd
assert exif_mdd["EXIF_LensMake"] == "Google"
xmp = ds.GetMetadata("xml:XMP")
assert xmp
assert xmp[0].startswith("<?xpacket")


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
def test_avif_icc_profile(tmp_vsimem):

if "SOURCE_ICC_PROFILE" not in gdal.GetDriverByName("AVIF").GetMetadataItem(
"DMD_CREATIONOPTIONLIST"
):
pytest.skip("ICC profile setting requires libavif >= 1.0")

f = open("data/sRGB.icc", "rb")
data = f.read()
icc = base64.b64encode(data).decode("ascii")
f.close()

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 3)
src_ds.GetRasterBand(1).SetColorInterpretation(gdal.GCI_RedBand)
src_ds.GetRasterBand(2).SetColorInterpretation(gdal.GCI_GreenBand)
src_ds.GetRasterBand(3).SetColorInterpretation(gdal.GCI_BlueBand)
src_ds.SetMetadataItem("SOURCE_ICC_PROFILE", icc, "COLOR_PROFILE")

out_filename = str(tmp_vsimem / "out.avif")
gdal.GetDriverByName("AVIF").CreateCopy(out_filename, src_ds)
ds = gdal.Open(out_filename)
assert ds.GetMetadataItem("SOURCE_ICC_PROFILE", "COLOR_PROFILE") == icc


@pytest.mark.skipif(not has_avif_encoder(), reason="libavif encoder missing")
def test_avif_creation_errors(tmp_vsimem):

out_filename = str(tmp_vsimem / "out.avif")

src_ds = gdal.GetDriverByName("MEM").Create("", 65537, 1)
with pytest.raises(
Exception,
match="Too big source dataset. Maximum AVIF image dimension is 65,536 x 65,536 pixels",
):
gdal.GetDriverByName("AVIF").CreateCopy(out_filename, src_ds)

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 65537)
with pytest.raises(
Exception,
match="Too big source dataset. Maximum AVIF image dimension is 65,536 x 65,536 pixels",
):
gdal.GetDriverByName("AVIF").CreateCopy(out_filename, src_ds)

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 5)
with pytest.raises(Exception, match="Unsupported number of bands"):
gdal.GetDriverByName("AVIF").CreateCopy(out_filename, src_ds)

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1, gdal.GDT_Float32)
with pytest.raises(
Exception,
match="Unsupported data type: only Byte or UInt16 bands are supported",
):
gdal.GetDriverByName("AVIF").CreateCopy(out_filename, src_ds)

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1)
with pytest.raises(
Exception, match="Invalid/inconsistent bit depth w.r.t data type"
):
gdal.GetDriverByName("AVIF").CreateCopy(
out_filename, src_ds, options=["NBITS=10"]
)

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1, gdal.GDT_UInt16)
with pytest.raises(
Exception, match="Invalid/inconsistent bit depth w.r.t data type"
):
gdal.GetDriverByName("AVIF").CreateCopy(
out_filename, src_ds, options=["NBITS=8"]
)

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1)
src_ds.GetRasterBand(1).SetColorTable(gdal.ColorTable())
with pytest.raises(
Exception,
match="Source dataset with color table unsupported. Use gdal_translate -expand rgb|rgba first",
):
gdal.GetDriverByName("AVIF").CreateCopy(out_filename, src_ds)

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 3)
with pytest.raises(
Exception, match="Only YUV_SUBSAMPLING=444 is supported for lossless encoding"
):
gdal.GetDriverByName("AVIF").CreateCopy(
out_filename, src_ds, options=["QUALITY=100", "YUV_SUBSAMPLING=422"]
)

src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1)
with pytest.raises(Exception, match="Cannot create file /i_do/not/exist.avif"):
gdal.GetDriverByName("AVIF").CreateCopy("/i_do/not/exist.avif", src_ds)
63 changes: 63 additions & 0 deletions autotest/gdrivers/avif_heif.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env pytest
# -*- coding: utf-8 -*-
###############################################################################
# Project: GDAL/OGR Test Suite
# Purpose: Test read functionality for AVIF_HEIF driver.
# Author: Even Rouault <even dot rouault at spatialys.com>
#
###############################################################################
# Copyright (c) 2024, Even Rouault <even dot rouault at spatialys.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
###############################################################################

import os
import subprocess
import sys

import pytest


def test_avif_heif():

from osgeo import gdal

drv = gdal.GetDriverByName("HEIF")
if drv is None:
pytest.skip("HEIF driver must be available")
if drv.GetMetadataItem("SUPPORTS_AVIF", "HEIF") is None:
pytest.skip("libheif has no AVIF support")

subprocess.check_call(
[
sys.executable,
"avif_heif.py",
"subprocess",
]
)


if __name__ == "__main__":

os.environ["GDAL_SKIP"] = "AVIF"
from osgeo import gdal

gdal.UseExceptions()
ds = gdal.Open("data/avif/byte.avif")
assert ds.GetRasterBand(1).Checksum() == 4672
Binary file added autotest/gdrivers/data/avif/byte.avif
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit 54bec3d

Please sign in to comment.