Selaa lähdekoodia

LibGfx: Decode AVIF images

Use libavif to decode AVIF images in LibGfx.
doctortheemh 1 vuosi sitten
vanhempi
commit
4ef76f3198

+ 2 - 2
.github/actions/setup/action.yml

@@ -26,7 +26,7 @@ runs:
 
         sudo apt-get update
         sudo apt-get install autoconf autoconf-archive automake build-essential cmake libavcodec-dev fonts-liberation2 zip curl tar ccache clang-18 clang++-18 lld-18 gcc-13 g++-13 libstdc++-13-dev \
-            ninja-build unzip qt6-base-dev qt6-tools-dev-tools libqt6svg6-dev qt6-multimedia-dev libgl1-mesa-dev libpulse-dev libssl-dev libegl1-mesa-dev
+            ninja-build unzip qt6-base-dev qt6-tools-dev-tools libqt6svg6-dev qt6-multimedia-dev libgl1-mesa-dev libpulse-dev libssl-dev libegl1-mesa-dev nasm
 
         sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-18 100
         sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-18 100
@@ -52,7 +52,7 @@ runs:
         set -e
         sudo xcode-select --switch /Applications/Xcode_15.4.app
         brew update
-        brew install autoconf autoconf-archive automake coreutils bash ffmpeg ninja wabt ccache unzip qt llvm@18
+        brew install autoconf autoconf-archive automake coreutils bash ffmpeg ninja wabt ccache unzip qt llvm@18 nasm
 
     - name: 'Install vcpkg'
       shell: bash

+ 62 - 0
Tests/LibGfx/TestImageDecoder.cpp

@@ -7,6 +7,7 @@
 
 #include <AK/ByteString.h>
 #include <LibCore/MappedFile.h>
+#include <LibGfx/ImageFormats/AVIFLoader.h>
 #include <LibGfx/ImageFormats/BMPLoader.h>
 #include <LibGfx/ImageFormats/GIFLoader.h>
 #include <LibGfx/ImageFormats/ICOLoader.h>
@@ -1007,3 +1008,64 @@ TEST_CASE(test_jxl_modular_property_8)
         }
     }
 }
+
+TEST_CASE(test_avif_simple_lossy)
+{
+    auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("avif/simple-lossy.avif"sv)));
+    EXPECT(Gfx::AVIFImageDecoderPlugin::sniff(file->bytes()));
+    auto plugin_decoder = TRY_OR_FAIL(Gfx::AVIFImageDecoderPlugin::create(file->bytes()));
+
+    auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 240, 240 }));
+
+    // While AVIF YUV contents are defined bit-exact, the YUV->RGB conversion isn't.
+    // So pixels changing by 1 or so below is fine if you change code.
+    EXPECT_EQ(frame.image->get_pixel(120, 232), Gfx::Color(0xf1, 0xef, 0xf0, 255));
+    EXPECT_EQ(frame.image->get_pixel(198, 202), Gfx::Color(0x7b, 0xaa, 0xd6, 255));
+}
+
+TEST_CASE(test_avif_simple_lossless)
+{
+    auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("avif/simple-lossless.avif"sv)));
+    EXPECT(Gfx::AVIFImageDecoderPlugin::sniff(file->bytes()));
+    auto plugin_decoder = TRY_OR_FAIL(Gfx::AVIFImageDecoderPlugin::create(file->bytes()));
+
+    auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 386, 395 }));
+    EXPECT_EQ(frame.image->get_pixel(0, 0), Gfx::Color(0, 0, 0, 0));
+    EXPECT_EQ(frame.image->get_pixel(289, 332), Gfx::Color(0xf2, 0xee, 0xd3, 255));
+}
+
+TEST_CASE(test_avif_simple_lossy_bitdepth10)
+{
+    auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("avif/simple-bitdepth10.avif"sv)));
+    EXPECT(!Gfx::AVIFImageDecoderPlugin::sniff(file->bytes()));
+}
+
+TEST_CASE(test_avif_icc_profile)
+{
+    auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("avif/icc_profile.avif"sv)));
+    EXPECT(Gfx::AVIFImageDecoderPlugin::sniff(file->bytes()));
+    auto plugin_decoder = TRY_OR_FAIL(Gfx::AVIFImageDecoderPlugin::create(file->bytes()));
+
+    auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 240, 240 }));
+    EXPECT(TRY_OR_FAIL(plugin_decoder->icc_data()).has_value());
+}
+
+TEST_CASE(test_avif_no_icc_profile)
+{
+    auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("avif/simple-lossy.avif"sv)));
+    EXPECT(Gfx::AVIFImageDecoderPlugin::sniff(file->bytes()));
+    auto plugin_decoder = TRY_OR_FAIL(Gfx::AVIFImageDecoderPlugin::create(file->bytes()));
+
+    auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 240, 240 }));
+    EXPECT(!TRY_OR_FAIL(plugin_decoder->icc_data()).has_value());
+}
+
+TEST_CASE(test_avif_frame_out_of_bounds)
+{
+    auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("avif/simple-lossy.avif"sv)));
+    EXPECT(Gfx::AVIFImageDecoderPlugin::sniff(file->bytes()));
+    auto plugin_decoder = TRY_OR_FAIL(Gfx::AVIFImageDecoderPlugin::create(file->bytes()));
+
+    auto frame1 = TRY_OR_FAIL(plugin_decoder->frame(0));
+    EXPECT(plugin_decoder->frame(1).is_error());
+}

BIN
Tests/LibGfx/test-inputs/avif/icc_profile.avif


BIN
Tests/LibGfx/test-inputs/avif/simple-bitdepth10.avif


BIN
Tests/LibGfx/test-inputs/avif/simple-lossless.avif


BIN
Tests/LibGfx/test-inputs/avif/simple-lossy.avif


+ 3 - 1
Userland/Libraries/LibGfx/CMakeLists.txt

@@ -52,6 +52,7 @@ set(SOURCES
     ImageFormats/WebPSharedLossless.cpp
     ImageFormats/WebPWriter.cpp
     ImageFormats/WebPWriterLossless.cpp
+    ImageFormats/AVIFLoader.cpp
     ImmutableBitmap.cpp
     MedianCut.cpp
     Painter.cpp
@@ -96,5 +97,6 @@ find_package(PkgConfig)
 pkg_check_modules(WOFF2 REQUIRED IMPORTED_TARGET libwoff2dec)
 find_package(JPEG REQUIRED)
 find_package(PNG REQUIRED)
+find_package(LIBAVIF REQUIRED)
 
-target_link_libraries(LibGfx PRIVATE PkgConfig::WOFF2 JPEG::JPEG PNG::PNG)
+target_link_libraries(LibGfx PRIVATE PkgConfig::WOFF2 JPEG::JPEG PNG::PNG avif)

+ 196 - 0
Userland/Libraries/LibGfx/ImageFormats/AVIFLoader.cpp

@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2023, Nico Weber <thakis@chromium.org>
+ * Copyright (c) 2024, doctortheemh <doctortheemh@gmail.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/ByteBuffer.h>
+#include <AK/Error.h>
+#include <LibGfx/ImageFormats/AVIFLoader.h>
+
+#include <avif/avif.h>
+
+namespace Gfx {
+
+class AVIFLoadingContext {
+    AK_MAKE_NONMOVABLE(AVIFLoadingContext);
+    AK_MAKE_NONCOPYABLE(AVIFLoadingContext);
+
+public:
+    enum State {
+        NotDecoded = 0,
+        Error,
+        HeaderDecoded,
+        BitmapDecoded,
+    };
+
+    State state { State::NotDecoded };
+    ReadonlyBytes data;
+
+    avifDecoder* decoder { nullptr };
+
+    // image properties
+    Optional<IntSize> size;
+    bool has_alpha { false };
+    size_t image_count { 0 };
+    size_t repetition_count { 0 };
+    ByteBuffer icc_data;
+
+    Vector<ImageFrameDescriptor> frame_descriptors;
+
+    AVIFLoadingContext() = default;
+    ~AVIFLoadingContext()
+    {
+        avifDecoderDestroy(decoder);
+        decoder = nullptr;
+    }
+};
+
+AVIFImageDecoderPlugin::AVIFImageDecoderPlugin(ReadonlyBytes data, OwnPtr<AVIFLoadingContext> context)
+    : m_context(move(context))
+{
+    m_context->data = data;
+}
+
+AVIFImageDecoderPlugin::~AVIFImageDecoderPlugin()
+{
+}
+
+static ErrorOr<void> decode_avif_header(AVIFLoadingContext& context)
+{
+    if (context.state >= AVIFLoadingContext::HeaderDecoded)
+        return {};
+
+    if (context.decoder == nullptr) {
+        context.decoder = avifDecoderCreate();
+
+        if (context.decoder == nullptr) {
+            return Error::from_string_literal("failed to allocate AVIF decoder");
+        }
+    }
+
+    avifResult result = avifDecoderSetIOMemory(context.decoder, context.data.data(), context.data.size());
+    if (result != AVIF_RESULT_OK)
+        return Error::from_string_literal("Cannot set IO on avifDecoder");
+
+    result = avifDecoderParse(context.decoder);
+    if (result != AVIF_RESULT_OK)
+        return Error::from_string_literal("Failed to decode AVIF");
+
+    if (context.decoder->image->depth != 8)
+        return Error::from_string_literal("Unsupported bitdepth");
+
+    // Image header now decoded, save some results for fast access in other parts of the plugin.
+    context.size = IntSize { context.decoder->image->width, context.decoder->image->height };
+    context.has_alpha = context.decoder->alphaPresent == 1;
+    context.image_count = context.decoder->imageCount;
+    context.repetition_count = context.decoder->repetitionCount <= 0 ? 0 : context.decoder->repetitionCount;
+    context.state = AVIFLoadingContext::State::HeaderDecoded;
+
+    if (context.decoder->image->icc.size > 0) {
+        context.icc_data.resize(context.decoder->image->icc.size);
+        memcpy(context.icc_data.data(), context.decoder->image->icc.data, context.decoder->image->icc.size);
+    }
+
+    return {};
+}
+
+static ErrorOr<void> decode_avif_image(AVIFLoadingContext& context)
+{
+    VERIFY(context.state >= AVIFLoadingContext::State::HeaderDecoded);
+
+    avifRGBImage rgb;
+    while (avifDecoderNextImage(context.decoder) == AVIF_RESULT_OK) {
+        auto bitmap_format = context.has_alpha ? BitmapFormat::BGRA8888 : BitmapFormat::BGRx8888;
+        auto bitmap = TRY(Bitmap::create(bitmap_format, context.size.value()));
+
+        avifRGBImageSetDefaults(&rgb, context.decoder->image);
+        rgb.pixels = bitmap->scanline_u8(0);
+        rgb.rowBytes = bitmap->pitch();
+        rgb.format = avifRGBFormat::AVIF_RGB_FORMAT_BGRA;
+
+        avifResult result = avifImageYUVToRGB(context.decoder->image, &rgb);
+        if (result != AVIF_RESULT_OK)
+            return Error::from_string_literal("Conversion from YUV to RGB failed");
+
+        auto duration = context.decoder->imageCount == 1 ? 0 : static_cast<int>(context.decoder->imageTiming.duration * 1000);
+        context.frame_descriptors.append(ImageFrameDescriptor { bitmap, duration });
+
+        context.state = AVIFLoadingContext::BitmapDecoded;
+    }
+
+    return {};
+}
+
+IntSize AVIFImageDecoderPlugin::size()
+{
+    return m_context->size.value();
+}
+
+bool AVIFImageDecoderPlugin::sniff(ReadonlyBytes data)
+{
+    AVIFLoadingContext context;
+    context.data = data;
+    return !decode_avif_header(context).is_error();
+}
+
+ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> AVIFImageDecoderPlugin::create(ReadonlyBytes data)
+{
+    auto context = TRY(try_make<AVIFLoadingContext>());
+    auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) AVIFImageDecoderPlugin(data, move(context))));
+    TRY(decode_avif_header(*plugin->m_context));
+    return plugin;
+}
+
+bool AVIFImageDecoderPlugin::is_animated()
+{
+    return m_context->image_count > 1;
+}
+
+size_t AVIFImageDecoderPlugin::loop_count()
+{
+    return is_animated() ? m_context->repetition_count : 0;
+}
+
+size_t AVIFImageDecoderPlugin::frame_count()
+{
+    if (!is_animated())
+        return 1;
+    return m_context->image_count;
+}
+
+size_t AVIFImageDecoderPlugin::first_animated_frame_index()
+{
+    return 0;
+}
+
+ErrorOr<ImageFrameDescriptor> AVIFImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
+{
+    if (index >= frame_count())
+        return Error::from_string_literal("AVIFImageDecoderPlugin: Invalid frame index");
+
+    if (m_context->state == AVIFLoadingContext::State::Error)
+        return Error::from_string_literal("AVIFImageDecoderPlugin: Decoding failed");
+
+    if (m_context->state < AVIFLoadingContext::State::BitmapDecoded) {
+        TRY(decode_avif_image(*m_context));
+        m_context->state = AVIFLoadingContext::State::BitmapDecoded;
+    }
+
+    if (index >= m_context->frame_descriptors.size())
+        return Error::from_string_literal("AVIFImageDecoderPlugin: Invalid frame index");
+    return m_context->frame_descriptors[index];
+}
+
+ErrorOr<Optional<ReadonlyBytes>> AVIFImageDecoderPlugin::icc_data()
+{
+    if (m_context->state < AVIFLoadingContext::State::HeaderDecoded)
+        (void)frame(0);
+
+    if (!m_context->icc_data.is_empty())
+        return m_context->icc_data;
+    return OptionalNone {};
+}
+
+}

+ 38 - 0
Userland/Libraries/LibGfx/ImageFormats/AVIFLoader.h

@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023, Nico Weber <thakis@chromium.org>
+ * Copyright (c) 2024, doctortheemh <doctortheemh@gmail.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibGfx/ImageFormats/ImageDecoder.h>
+
+namespace Gfx {
+
+class AVIFLoadingContext;
+
+class AVIFImageDecoderPlugin final : public ImageDecoderPlugin {
+public:
+    static bool sniff(ReadonlyBytes);
+    static ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> create(ReadonlyBytes);
+
+    virtual ~AVIFImageDecoderPlugin() override;
+
+    virtual IntSize size() override;
+
+    virtual bool is_animated() override;
+    virtual size_t loop_count() override;
+    virtual size_t frame_count() override;
+    virtual size_t first_animated_frame_index() override;
+    virtual ErrorOr<ImageFrameDescriptor> frame(size_t index, Optional<IntSize> ideal_size = {}) override;
+    virtual ErrorOr<Optional<ReadonlyBytes>> icc_data() override;
+
+private:
+    AVIFImageDecoderPlugin(ReadonlyBytes, OwnPtr<AVIFLoadingContext>);
+
+    OwnPtr<AVIFLoadingContext> m_context;
+};
+
+}

+ 2 - 0
Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp

@@ -5,6 +5,7 @@
  */
 
 #include <AK/LexicalPath.h>
+#include <LibGfx/ImageFormats/AVIFLoader.h>
 #include <LibGfx/ImageFormats/BMPLoader.h>
 #include <LibGfx/ImageFormats/GIFLoader.h>
 #include <LibGfx/ImageFormats/ICOLoader.h>
@@ -35,6 +36,7 @@ static ErrorOr<OwnPtr<ImageDecoderPlugin>> probe_and_sniff_for_appropriate_plugi
         { TIFFImageDecoderPlugin::sniff, TIFFImageDecoderPlugin::create },
         { TinyVGImageDecoderPlugin::sniff, TinyVGImageDecoderPlugin::create },
         { WebPImageDecoderPlugin::sniff, WebPImageDecoderPlugin::create },
+        { AVIFImageDecoderPlugin::sniff, AVIFImageDecoderPlugin::create }
     };
 
     for (auto& plugin : s_initializers) {

+ 10 - 0
vcpkg.json

@@ -13,6 +13,12 @@
         "apng"
       ]
     },
+    {
+      "name": "libavif",
+      "features": [
+        "dav1d"
+      ]
+    },
     {
       "name": "skia",
       "platform": "osx",
@@ -59,6 +65,10 @@
       "name": "libpng",
       "version": "1.6.43#1"
     },
+    {
+      "name": "libavif",
+      "version": "1.0.4#1"
+    },
     {
       "name": "skia",
       "version": "124#0"