diff --git a/Libraries/LibGfx/CMakeLists.txt b/Libraries/LibGfx/CMakeLists.txt index ff66fb25481..7868393080d 100644 --- a/Libraries/LibGfx/CMakeLists.txt +++ b/Libraries/LibGfx/CMakeLists.txt @@ -21,7 +21,7 @@ set(SOURCES Font/WOFF/Loader.cpp Font/WOFF2/Loader.cpp GradientPainting.cpp - ImageFormats/AnimationWriter.cpp + ImageFormats/AVIFLoader.cpp ImageFormats/BMPLoader.cpp ImageFormats/BMPWriter.cpp ImageFormats/BooleanDecoder.cpp @@ -40,7 +40,6 @@ set(SOURCES ImageFormats/WebPSharedLossless.cpp ImageFormats/WebPWriter.cpp ImageFormats/WebPWriterLossless.cpp - ImageFormats/AVIFLoader.cpp ImmutableBitmap.cpp SkiaUtils.cpp PaintingSurface.cpp diff --git a/Libraries/LibGfx/ImageFormats/AnimationWriter.cpp b/Libraries/LibGfx/ImageFormats/AnimationWriter.cpp deleted file mode 100644 index 640b44a5d29..00000000000 --- a/Libraries/LibGfx/ImageFormats/AnimationWriter.cpp +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2024, Nico Weber - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include -#include -#include - -namespace Gfx { - -AnimationWriter::~AnimationWriter() = default; - -static bool are_scanlines_equal(Bitmap const& a, Bitmap const& b, int y) -{ - for (int x = 0; x < a.width(); ++x) { - if (a.get_pixel(x, y) != b.get_pixel(x, y)) - return false; - } - return true; -} - -static bool are_columns_equal(Bitmap const& a, Bitmap const& b, int x, int y1, int y2) -{ - for (int y = y1; y < y2; ++y) { - if (a.get_pixel(x, y) != b.get_pixel(x, y)) - return false; - } - return true; -} - -static Gfx::IntRect rect_where_pixels_are_different(Bitmap const& a, Bitmap const& b) -{ - VERIFY(a.size() == b.size()); - - int number_of_equal_pixels_at_top = 0; - while (number_of_equal_pixels_at_top < a.height() && are_scanlines_equal(a, b, number_of_equal_pixels_at_top)) - ++number_of_equal_pixels_at_top; - - int number_of_equal_pixels_at_bottom = 0; - while (number_of_equal_pixels_at_bottom < a.height() - number_of_equal_pixels_at_top && are_scanlines_equal(a, b, a.height() - number_of_equal_pixels_at_bottom - 1)) - ++number_of_equal_pixels_at_bottom; - - int const y1 = number_of_equal_pixels_at_top; - int const y2 = a.height() - number_of_equal_pixels_at_bottom; - - int number_of_equal_pixels_at_left = 0; - while (number_of_equal_pixels_at_left < a.width() && are_columns_equal(a, b, number_of_equal_pixels_at_left, y1, y2)) - ++number_of_equal_pixels_at_left; - - int number_of_equal_pixels_at_right = 0; - while (number_of_equal_pixels_at_right < a.width() - number_of_equal_pixels_at_left && are_columns_equal(a, b, a.width() - number_of_equal_pixels_at_right - 1, y1, y2)) - ++number_of_equal_pixels_at_right; - - // WebP can only encode even-sized animation frame positions. - // FIXME: Change API shape in some way so that the AnimationWriter base class doesn't have to know about this detail of a subclass. - if (number_of_equal_pixels_at_left % 2 != 0) - --number_of_equal_pixels_at_left; - if (number_of_equal_pixels_at_top % 2 != 0) - --number_of_equal_pixels_at_top; - - Gfx::IntRect rect; - rect.set_x(number_of_equal_pixels_at_left); - rect.set_y(number_of_equal_pixels_at_top); - rect.set_width(a.width() - number_of_equal_pixels_at_left - number_of_equal_pixels_at_right); - rect.set_height(a.height() - number_of_equal_pixels_at_top - number_of_equal_pixels_at_bottom); - - return rect; -} - -ErrorOr AnimationWriter::add_frame_relative_to_last_frame(Bitmap& frame, int duration_ms, RefPtr last_frame) -{ - if (!last_frame) - return add_frame(frame, duration_ms); - - auto rect = rect_where_pixels_are_different(*last_frame, frame); - - if (rect.is_empty()) { - // The frame is identical to the last frame. Don't store an empty bitmap. - // FIXME: We could delay writing the last frame until we know that the next frame is different, - // and just keep increasing that frame's duration instead. - rect = { 0, 0, 1, 1 }; - } - - // FIXME: It would be nice to have a way to crop a bitmap without copying the data. - auto differences = TRY(frame.cropped(rect)); - - // FIXME: Another idea: If all frames of the animation have no alpha, - // this could set color values of pixels that are in the changed rect that are - // equal to the last frame to transparent black and set the frame to be blended. - // That might take less space after compression. - - // This assumes a replacement disposal method. - return add_frame(differences, duration_ms, rect.location()); -} - -} diff --git a/Libraries/LibGfx/ImageFormats/AnimationWriter.h b/Libraries/LibGfx/ImageFormats/AnimationWriter.h deleted file mode 100644 index 9048f088b9f..00000000000 --- a/Libraries/LibGfx/ImageFormats/AnimationWriter.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2024, Nico Weber - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include - -namespace Gfx { - -class AnimationWriter { -public: - virtual ~AnimationWriter(); - - // Flushes the frame to disk. - // IntRect { at, at + bitmap.size() } must fit in the dimensions - // passed to `start_writing_animation()`. - // FIXME: Consider passing in disposal method and blend mode. - virtual ErrorOr add_frame(Bitmap&, int duration_ms, IntPoint at = {}) = 0; - - ErrorOr add_frame_relative_to_last_frame(Bitmap&, int duration_ms, RefPtr last_frame); -}; - -} diff --git a/Libraries/LibGfx/ImageFormats/WebPWriter.cpp b/Libraries/LibGfx/ImageFormats/WebPWriter.cpp index b1019242250..072c5cefc0e 100644 --- a/Libraries/LibGfx/ImageFormats/WebPWriter.cpp +++ b/Libraries/LibGfx/ImageFormats/WebPWriter.cpp @@ -11,9 +11,9 @@ #include #include #include -#include #include #include +#include namespace Gfx { @@ -211,184 +211,4 @@ ErrorOr WebPWriter::encode(Stream& stream, Bitmap const& bitmap, Options c return {}; } -class WebPAnimationWriter : public AnimationWriter { -public: - WebPAnimationWriter(SeekableStream& stream, IntSize dimensions, u8 original_vp8x_flags, VP8LEncoderOptions vp8l_options) - : m_stream(stream) - , m_dimensions(dimensions) - , m_vp8x_flags(original_vp8x_flags) - , m_vp8l_options(vp8l_options) - { - } - - virtual ErrorOr add_frame(Bitmap&, int, IntPoint) override; - - ErrorOr update_size_in_header(); - ErrorOr set_alpha_bit_in_header(); - -private: - SeekableStream& m_stream; - IntSize m_dimensions; - u8 m_vp8x_flags { 0 }; - VP8LEncoderOptions m_vp8l_options; -}; - -static ErrorOr align_to_two(SeekableStream& stream) -{ - return align_to_two(stream, TRY(stream.tell())); -} - -static ErrorOr write_ANMF_chunk_header(Stream& stream, ANMFChunkHeader const& chunk, size_t payload_size) -{ - if (chunk.frame_width > (1 << 24) || chunk.frame_height > (1 << 24)) - return Error::from_string_literal("WebP dimensions too large for ANMF chunk"); - - if (chunk.frame_width == 0 || chunk.frame_height == 0) - return Error::from_string_literal("WebP lossless animation frames must be at least one pixel wide and tall"); - - if (chunk.frame_x % 2 != 0 || chunk.frame_y % 2 != 0) - return Error::from_string_literal("WebP lossless animation frames must be at at even coordinates"); - - dbgln_if(WEBP_DEBUG, "writing ANMF frame_x {} frame_y {} frame_width {} frame_height {} frame_duration {} blending_method {} disposal_method {}", - chunk.frame_x, chunk.frame_y, chunk.frame_width, chunk.frame_height, chunk.frame_duration_in_milliseconds, (int)chunk.blending_method, (int)chunk.disposal_method); - - TRY(write_chunk_header(stream, "ANMF"sv, 16 + payload_size)); - - LittleEndianOutputBitStream bit_stream { MaybeOwned(stream) }; - - // "Frame X: 24 bits (uint24) - // The X coordinate of the upper left corner of the frame is Frame X * 2." - TRY(bit_stream.write_bits(chunk.frame_x / 2, 24u)); - - // "Frame Y: 24 bits (uint24) - // The Y coordinate of the upper left corner of the frame is Frame Y * 2." - TRY(bit_stream.write_bits(chunk.frame_y / 2, 24u)); - - // "Frame Width: 24 bits (uint24) - // The 1-based width of the frame. The frame width is 1 + Frame Width Minus One." - TRY(bit_stream.write_bits(chunk.frame_width - 1, 24u)); - - // "Frame Height: 24 bits (uint24) - // The 1-based height of the frame. The frame height is 1 + Frame Height Minus One." - TRY(bit_stream.write_bits(chunk.frame_height - 1, 24u)); - - // "Frame Duration: 24 bits (uint24)" - TRY(bit_stream.write_bits(chunk.frame_duration_in_milliseconds, 24u)); - - // Don't use bit_stream.write_bits() to write individual flags here: - // The spec describes bit flags in MSB to LSB order, but write_bits() writes LSB to MSB. - u8 flags = 0; - // "Reserved: 6 bits - // MUST be 0. Readers MUST ignore this field." - - // "Blending method (B): 1 bit" - if (chunk.blending_method == ANMFChunkHeader::BlendingMethod::DoNotBlend) - flags |= 0x2; - - // "Disposal method (D): 1 bit" - if (chunk.disposal_method == ANMFChunkHeader::DisposalMethod::DisposeToBackgroundColor) - flags |= 0x1; - - TRY(bit_stream.write_bits(flags, 8u)); - - // FIXME: Make ~LittleEndianOutputBitStream do this, or make it VERIFY() that it has happened at least. - TRY(bit_stream.flush_buffer_to_stream()); - - return {}; -} - -ErrorOr WebPAnimationWriter::add_frame(Bitmap& bitmap, int duration_ms, IntPoint at) -{ - if (at.x() < 0 || at.y() < 0 || at.x() + bitmap.width() > m_dimensions.width() || at.y() + bitmap.height() > m_dimensions.height()) - return Error::from_string_literal("Frame does not fit in animation dimensions"); - - // Since we have a SeekableStream, we could write both the VP8L chunk header and the ANMF chunk header with a placeholder size, - // compress the frame data directly to the stream, and then go back and update the two sizes. - // That's pretty messy though, and the compressed image data is smaller than the uncompressed bitmap passed in. So we'll buffer it. - bool is_fully_opaque; - auto vp8l_data_bytes = TRY(compress_VP8L_image_data(bitmap, m_vp8l_options, is_fully_opaque)); - - ANMFChunkHeader chunk; - chunk.frame_x = static_cast(at.x()); - chunk.frame_y = static_cast(at.y()); - chunk.frame_width = static_cast(bitmap.width()); - chunk.frame_height = static_cast(bitmap.height()); - chunk.frame_duration_in_milliseconds = static_cast(duration_ms); - chunk.blending_method = ANMFChunkHeader::BlendingMethod::DoNotBlend; - chunk.disposal_method = ANMFChunkHeader::DisposalMethod::DoNotDispose; - - TRY(write_ANMF_chunk_header(m_stream, chunk, compute_VP8L_chunk_size(vp8l_data_bytes))); - bool alpha_is_used_hint = !is_fully_opaque; - TRY(write_VP8L_chunk(m_stream, bitmap.width(), bitmap.height(), alpha_is_used_hint, vp8l_data_bytes)); - - TRY(update_size_in_header()); - - if (!(m_vp8x_flags & 0x10) && !is_fully_opaque) - TRY(set_alpha_bit_in_header()); - - return {}; -} - -ErrorOr WebPAnimationWriter::update_size_in_header() -{ - auto current_offset = TRY(m_stream.tell()); - TRY(m_stream.seek(4, SeekMode::SetPosition)); - VERIFY(current_offset > 8); - TRY(m_stream.write_value>(current_offset - 8)); - TRY(m_stream.seek(current_offset, SeekMode::SetPosition)); - return {}; -} - -ErrorOr WebPAnimationWriter::set_alpha_bit_in_header() -{ - m_vp8x_flags |= 0x10; - - auto current_offset = TRY(m_stream.tell()); - // 4 bytes for "RIFF", - // 4 bytes RIFF chunk size (i.e. file size - 8), - // 4 bytes for "WEBP", - // 4 bytes for "VP8X", - // 4 bytes for VP8X chunk size, - // followed by VP8X flags in the first byte of the VP8X chunk data. - TRY(m_stream.seek(20, SeekMode::SetPosition)); - TRY(m_stream.write_value(m_vp8x_flags)); - TRY(m_stream.seek(current_offset, SeekMode::SetPosition)); - return {}; -} - -static ErrorOr write_ANIM_chunk(Stream& stream, ANIMChunk const& chunk) -{ - TRY(write_chunk_header(stream, "ANIM"sv, 6)); // Size of the ANIM chunk. - TRY(stream.write_value>(chunk.background_color)); - TRY(stream.write_value>(chunk.loop_count)); - return {}; -} - -ErrorOr> WebPWriter::start_encoding_animation(SeekableStream& stream, IntSize dimensions, int loop_count, Color background_color, Options const& options) -{ - // We'll update the stream with the actual size later. - TRY(write_webp_header(stream, 0)); - - VP8XHeader vp8x_header; - vp8x_header.has_icc = options.icc_data.has_value(); - vp8x_header.width = dimensions.width(); - vp8x_header.height = dimensions.height(); - vp8x_header.has_animation = true; - TRY(write_VP8X_chunk(stream, vp8x_header)); - VERIFY(TRY(stream.tell()) % 2 == 0); - - ByteBuffer iccp_chunk_bytes; - if (options.icc_data.has_value()) { - TRY(write_chunk_header(stream, "ICCP"sv, options.icc_data.value().size())); - TRY(stream.write_until_depleted(options.icc_data.value())); - TRY(align_to_two(stream)); - } - - TRY(write_ANIM_chunk(stream, { .background_color = background_color.value(), .loop_count = static_cast(loop_count) })); - - auto writer = make(stream, dimensions, vp8x_flags_from_header(vp8x_header), options.vp8l_options); - TRY(writer->update_size_in_header()); - return writer; -} - } diff --git a/Libraries/LibGfx/ImageFormats/WebPWriter.h b/Libraries/LibGfx/ImageFormats/WebPWriter.h index 7f1b2c5f8d9..a984f30165b 100644 --- a/Libraries/LibGfx/ImageFormats/WebPWriter.h +++ b/Libraries/LibGfx/ImageFormats/WebPWriter.h @@ -7,14 +7,11 @@ #pragma once #include -#include #include #include namespace Gfx { -class AnimationWriter; - struct WebPEncoderOptions { VP8LEncoderOptions vp8l_options; Optional icc_data; @@ -27,9 +24,6 @@ public: // Always lossless at the moment. static ErrorOr encode(Stream&, Bitmap const&, Options const& = {}); - // Always lossless at the moment. - static ErrorOr> start_encoding_animation(SeekableStream&, IntSize dimensions, int loop_count = 0, Color background_color = Color::Black, Options const& = {}); - private: WebPWriter() = delete; }; diff --git a/Tests/LibGfx/TestImageWriter.cpp b/Tests/LibGfx/TestImageWriter.cpp index 64198333cab..596804b8a05 100644 --- a/Tests/LibGfx/TestImageWriter.cpp +++ b/Tests/LibGfx/TestImageWriter.cpp @@ -6,10 +6,8 @@ #include #include -#include #include #include -#include #include #include #include @@ -184,100 +182,3 @@ TEST_CASE(test_webp_color_indexing_transform_single_channel) expect_bitmaps_equal(*decoded_bitmap_without_color_indexing, *decoded_bitmap); } } - -TEST_CASE(test_webp_animation) -{ - auto rgb_bitmap = TRY_OR_FAIL(create_test_rgb_bitmap()); - auto rgba_bitmap = TRY_OR_FAIL(create_test_rgba_bitmap()); - - // 20 kiB is enough for two 47x33 frames. - auto stream_buffer = TRY_OR_FAIL(ByteBuffer::create_uninitialized(20 * 1024)); - FixedMemoryStream stream { Bytes { stream_buffer } }; - - auto animation_writer = TRY_OR_FAIL(Gfx::WebPWriter::start_encoding_animation(stream, rgb_bitmap->size())); - - TRY_OR_FAIL(animation_writer->add_frame(*rgb_bitmap, 100)); - TRY_OR_FAIL(animation_writer->add_frame(*rgba_bitmap, 200)); - - auto encoded_animation = ReadonlyBytes { stream_buffer.data(), stream.offset() }; - - auto decoded_animation_plugin = TRY_OR_FAIL(Gfx::WebPImageDecoderPlugin::create(encoded_animation)); - EXPECT(decoded_animation_plugin->is_animated()); - EXPECT_EQ(decoded_animation_plugin->frame_count(), 2u); - EXPECT_EQ(decoded_animation_plugin->loop_count(), 0u); - EXPECT_EQ(decoded_animation_plugin->size(), rgb_bitmap->size()); - - auto frame0 = TRY_OR_FAIL(decoded_animation_plugin->frame(0)); - EXPECT_EQ(frame0.duration, 100); - expect_bitmaps_equal(*frame0.image, *rgb_bitmap); - - auto frame1 = TRY_OR_FAIL(decoded_animation_plugin->frame(1)); - EXPECT_EQ(frame1.duration, 200); - expect_bitmaps_equal(*frame1.image, *rgba_bitmap); -} - -TEST_CASE(test_webp_incremental_animation) -{ - auto rgb_bitmap_1 = TRY_OR_FAIL(create_test_rgb_bitmap()); - - auto rgb_bitmap_2 = TRY_OR_FAIL(create_test_rgb_bitmap()); - - // WebP frames can't be at odd coordinates. Make a pixel at an odd coordinate different to make sure we handle this. - rgb_bitmap_2->scanline(3)[3] = Gfx::Color(Color::Red).value(); - - // 20 kiB is enough for two 47x33 frames. - auto stream_buffer = TRY_OR_FAIL(ByteBuffer::create_uninitialized(20 * 1024)); - FixedMemoryStream stream { Bytes { stream_buffer } }; - - auto animation_writer = TRY_OR_FAIL(Gfx::WebPWriter::start_encoding_animation(stream, rgb_bitmap_1->size())); - - TRY_OR_FAIL(animation_writer->add_frame(*rgb_bitmap_1, 100)); - TRY_OR_FAIL(animation_writer->add_frame_relative_to_last_frame(*rgb_bitmap_2, 200, *rgb_bitmap_1)); - - auto encoded_animation = ReadonlyBytes { stream_buffer.data(), stream.offset() }; - - auto decoded_animation_plugin = TRY_OR_FAIL(Gfx::WebPImageDecoderPlugin::create(encoded_animation)); - EXPECT(decoded_animation_plugin->is_animated()); - EXPECT_EQ(decoded_animation_plugin->frame_count(), 2u); - EXPECT_EQ(decoded_animation_plugin->loop_count(), 0u); - EXPECT_EQ(decoded_animation_plugin->size(), rgb_bitmap_1->size()); - - auto frame0 = TRY_OR_FAIL(decoded_animation_plugin->frame(0)); - EXPECT_EQ(frame0.duration, 100); - expect_bitmaps_equal(*frame0.image, *rgb_bitmap_1); - - auto frame1 = TRY_OR_FAIL(decoded_animation_plugin->frame(1)); - EXPECT_EQ(frame1.duration, 200); - expect_bitmaps_equal(*frame1.image, *rgb_bitmap_2); -} - -TEST_CASE(test_webp_incremental_animation_two_identical_frames) -{ - auto rgb_bitmap = TRY_OR_FAIL(create_test_rgba_bitmap()); - rgb_bitmap = TRY_OR_FAIL(rgb_bitmap->cropped({ 0, 0, 40, 20 })); // Even-sized. - - // 20 kiB is enough for two 47x33 frames. - auto stream_buffer = TRY_OR_FAIL(ByteBuffer::create_uninitialized(20 * 1024)); - FixedMemoryStream stream { Bytes { stream_buffer } }; - - auto animation_writer = TRY_OR_FAIL(Gfx::WebPWriter::start_encoding_animation(stream, rgb_bitmap->size())); - - TRY_OR_FAIL(animation_writer->add_frame(*rgb_bitmap, 100)); - TRY_OR_FAIL(animation_writer->add_frame_relative_to_last_frame(*rgb_bitmap, 200, *rgb_bitmap)); - - auto encoded_animation = ReadonlyBytes { stream_buffer.data(), stream.offset() }; - - auto decoded_animation_plugin = TRY_OR_FAIL(Gfx::WebPImageDecoderPlugin::create(encoded_animation)); - EXPECT(decoded_animation_plugin->is_animated()); - EXPECT_EQ(decoded_animation_plugin->frame_count(), 2u); - EXPECT_EQ(decoded_animation_plugin->loop_count(), 0u); - EXPECT_EQ(decoded_animation_plugin->size(), rgb_bitmap->size()); - - auto frame0 = TRY_OR_FAIL(decoded_animation_plugin->frame(0)); - EXPECT_EQ(frame0.duration, 100); - expect_bitmaps_equal(*frame0.image, *rgb_bitmap); - - auto frame1 = TRY_OR_FAIL(decoded_animation_plugin->frame(1)); - EXPECT_EQ(frame1.duration, 200); - expect_bitmaps_equal(*frame1.image, *rgb_bitmap); -} diff --git a/Utilities/CMakeLists.txt b/Utilities/CMakeLists.txt index 4a267257515..b527cc24415 100644 --- a/Utilities/CMakeLists.txt +++ b/Utilities/CMakeLists.txt @@ -13,7 +13,6 @@ lagom_utility(abench SOURCES abench.cpp LIBS LibMain LibFileSystem LibMedia) lagom_utility(dns SOURCES dns.cpp LIBS LibDNS LibMain LibTLS LibCrypto) if (ENABLE_GUI_TARGETS) - lagom_utility(animation SOURCES animation.cpp LIBS LibGfx LibMain) lagom_utility(image SOURCES image.cpp LIBS LibGfx LibMain) endif() diff --git a/Utilities/animation.cpp b/Utilities/animation.cpp deleted file mode 100644 index a339d509e87..00000000000 --- a/Utilities/animation.cpp +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2024, Nico Weber - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include -#include -#include -#include -#include -#include - -struct Options { - StringView in_path; - StringView out_path; - bool write_full_frames { false }; -}; - -static ErrorOr parse_options(Main::Arguments arguments) -{ - Options options; - Core::ArgsParser args_parser; - args_parser.add_positional_argument(options.in_path, "Path to input image file", "FILE"); - args_parser.add_option(options.out_path, "Path to output image file", "output", 'o', "FILE"); - args_parser.add_option(options.write_full_frames, "Do not store incremental frames. Produces larger files.", "write-full-frames"); - args_parser.parse(arguments); - - if (options.out_path.is_empty()) - return Error::from_string_literal("-o is required "); - - return options; -} - -ErrorOr ladybird_main(Main::Arguments arguments) -{ - Options options = TRY(parse_options(arguments)); - - // FIXME: Allow multiple single frames as input too, and allow manually setting their duration. - - auto file = TRY(Core::MappedFile::map(options.in_path)); - auto decoder = TRY(Gfx::ImageDecoder::try_create_for_raw_bytes(file->bytes())); - if (!decoder) - return Error::from_string_literal("Could not find decoder for input file"); - - auto output_file = TRY(Core::File::open(options.out_path, Core::File::OpenMode::Write)); - auto output_stream = TRY(Core::OutputBufferedFile::create(move(output_file))); - - auto animation_writer = TRY([&]() -> ErrorOr> { - if (options.out_path.ends_with(".webp"sv)) - return Gfx::WebPWriter::start_encoding_animation(*output_stream, decoder->size(), decoder->loop_count()); - return Error::from_string_literal("Unable to find a encoder for the requested extension."); - }()); - - RefPtr last_frame; - for (size_t i = 0; i < decoder->frame_count(); ++i) { - auto frame = TRY(decoder->frame(i)); - if (options.write_full_frames) { - TRY(animation_writer->add_frame(*frame.image, frame.duration)); - } else { - TRY(animation_writer->add_frame_relative_to_last_frame(*frame.image, frame.duration, last_frame)); - last_frame = frame.image; - } - } - - return 0; -}