diff --git a/Tests/LibGfx/TestImageWriter.cpp b/Tests/LibGfx/TestImageWriter.cpp index dc2196345a4..81307a244e5 100644 --- a/Tests/LibGfx/TestImageWriter.cpp +++ b/Tests/LibGfx/TestImageWriter.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -22,21 +24,22 @@ #include #include -static ErrorOr> expect_single_frame(Gfx::ImageDecoderPlugin& plugin_decoder) +static ErrorOr> expect_single_frame(Gfx::ImageDecoderPlugin& plugin_decoder, bool is_gif) { EXPECT_EQ(plugin_decoder.frame_count(), 1u); EXPECT(!plugin_decoder.is_animated()); - EXPECT(!plugin_decoder.loop_count()); + EXPECT_EQ(plugin_decoder.loop_count(), is_gif ? 1u : 0u); auto frame_descriptor = TRY(plugin_decoder.frame(0)); - EXPECT_EQ(frame_descriptor.duration, 0); + if (!is_gif) + EXPECT_EQ(frame_descriptor.duration, 0); return *frame_descriptor.image; } -static ErrorOr> expect_single_frame_of_size(Gfx::ImageDecoderPlugin& plugin_decoder, Gfx::IntSize size) +static ErrorOr> expect_single_frame_of_size(Gfx::ImageDecoderPlugin& plugin_decoder, Gfx::IntSize size, bool is_gif = false) { EXPECT_EQ(plugin_decoder.size(), size); - auto frame = TRY(expect_single_frame(plugin_decoder)); + auto frame = TRY(expect_single_frame(plugin_decoder, is_gif)); EXPECT_EQ(frame->size(), size); return frame; } @@ -54,10 +57,10 @@ static ErrorOr encode_bitmap(Gfx::Bitmap const& bitmap, ExtraArgs... } template -static ErrorOr> get_roundtrip_bitmap(Gfx::Bitmap const& bitmap) +static ErrorOr> get_roundtrip_bitmap(Gfx::Bitmap const& bitmap, bool is_gif = false) { auto encoded_data = TRY(encode_bitmap(bitmap)); - return expect_single_frame_of_size(*TRY(Loader::create(encoded_data)), bitmap.size()); + return expect_single_frame_of_size(*TRY(Loader::create(encoded_data)), bitmap.size(), is_gif); } static void expect_bitmaps_equal(Gfx::Bitmap const& a, Gfx::Bitmap const& b) @@ -69,9 +72,9 @@ static void expect_bitmaps_equal(Gfx::Bitmap const& a, Gfx::Bitmap const& b) } template -static ErrorOr test_roundtrip(Gfx::Bitmap const& bitmap) +static ErrorOr test_roundtrip(Gfx::Bitmap const& bitmap, bool is_gif = false) { - auto decoded = TRY((get_roundtrip_bitmap(bitmap))); + auto decoded = TRY((get_roundtrip_bitmap(bitmap, is_gif))); expect_bitmaps_equal(*decoded, bitmap); return {}; } @@ -108,6 +111,18 @@ TEST_CASE(test_bmp) TRY_OR_FAIL((test_roundtrip(TRY_OR_FAIL(create_test_rgba_bitmap())))); } +TEST_CASE(test_gif) +{ + // We only support grayscale and non-animated images yet + auto bitmap = TRY_OR_FAIL(create_test_rgb_bitmap()); + + // Convert bitmap to grayscale + for (auto& argb : *bitmap) + argb = Color::from_argb(argb).to_grayscale().value(); + + TRY_OR_FAIL((test_roundtrip(bitmap, true))); +} + TEST_CASE(test_jpeg) { // JPEG is lossy, so the roundtripped bitmap won't match the original bitmap. But it should still have the same size. diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index e8c97884106..da6ad704ab6 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -43,6 +43,7 @@ set(SOURCES ImageFormats/CCITTDecoder.cpp ImageFormats/DDSLoader.cpp ImageFormats/GIFLoader.cpp + ImageFormats/GIFWriter.cpp ImageFormats/ICOLoader.cpp ImageFormats/ILBMLoader.cpp ImageFormats/ImageDecoder.cpp diff --git a/Userland/Libraries/LibGfx/ImageFormats/GIFWriter.cpp b/Userland/Libraries/LibGfx/ImageFormats/GIFWriter.cpp new file mode 100644 index 00000000000..f3f43e5538a --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/GIFWriter.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2024, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace Gfx { + +namespace { + +ErrorOr write_header(Stream& stream) +{ + // 17. Header + TRY(stream.write_until_depleted("GIF87a"sv)); + return {}; +} + +ErrorOr write_logical_descriptor(BigEndianOutputBitStream& stream, Bitmap const& bitmap) +{ + // 18. Logical Screen Descriptor + + if (bitmap.width() > NumericLimits::max() || bitmap.height() > NumericLimits::max()) + return Error::from_string_literal("Bitmap size is too big for a GIF"); + + TRY(stream.write_value(bitmap.width())); + TRY(stream.write_value(bitmap.height())); + + // Global Color Table Flag + TRY(stream.write_bits(true, 1)); + // Color Resolution + TRY(stream.write_bits(6u, 3)); + // Sort Flag + TRY(stream.write_bits(false, 1)); + // Size of Global Color Table + TRY(stream.write_bits(7u, 3)); + + // Background Color Index + TRY(stream.write_value(0)); + + // Pixel Aspect Ratio + // NOTE: We can write a zero as most decoders discard the value. + TRY(stream.write_value(0)); + + return {}; +} + +ErrorOr write_global_color_table(Stream& stream) +{ + // 19. Global Color Table + + // FIXME: The color table should include color specific to the image + for (u16 i = 0; i < 256; ++i) { + TRY(stream.write_value(i)); + TRY(stream.write_value(i)); + TRY(stream.write_value(i)); + } + return {}; +} + +ErrorOr write_image_data(Stream& stream, Bitmap const& bitmap) +{ + // 22. Table Based Image Data + auto const pixel_number = static_cast(bitmap.width() * bitmap.height()); + auto indexes = TRY(ByteBuffer::create_uninitialized(pixel_number)); + for (u32 i = 0; i < pixel_number; ++i) { + auto const color = Color::from_argb(*(bitmap.begin() + i)); + if (color.red() != color.green() || color.green() != color.blue()) + return Error::from_string_literal("Non grayscale images are unsupported."); + indexes[i] = Color::from_argb(*(bitmap.begin() + i)).red(); // Any channel is correct + } + + constexpr u8 lzw_minimum_code_size = 8; + auto const encoded = TRY(Compress::LzwCompressor::compress_all(move(indexes), lzw_minimum_code_size)); + + auto const number_of_subblocks = ceil_div(encoded.size(), 255ul); + + TRY(stream.write_value(lzw_minimum_code_size)); + + for (u32 i = 0; i < number_of_subblocks; ++i) { + auto const offset = i * 255; + auto const to_write = min(255, encoded.size() - offset); + TRY(stream.write_value(to_write)); + TRY(stream.write_until_depleted(encoded.bytes().slice(offset, to_write))); + } + + // Block terminator + TRY(stream.write_value(0)); + + return {}; +} + +ErrorOr write_image_descriptor(BigEndianOutputBitStream& stream, Bitmap const& bitmap) +{ + // 20. Image Descriptor + + // Image Separator + TRY(stream.write_value(0x2c)); + // Image Left Position + TRY(stream.write_value(0)); + // Image Top Position + TRY(stream.write_value(0)); + // Image Width + TRY(stream.write_value(bitmap.width())); + // Image Height + TRY(stream.write_value(bitmap.height())); + + // Local Color Table Flag + TRY(stream.write_bits(false, 1)); + // Interlace Flag + TRY(stream.write_bits(false, 1)); + // Sort Flag + TRY(stream.write_bits(false, 1)); + // Reserved + TRY(stream.write_bits(0u, 2)); + // Size of Local Color Table + TRY(stream.write_bits(0u, 3)); + return {}; +} + +ErrorOr write_trailer(Stream& stream) +{ + TRY(stream.write_value(0x3B)); + return {}; +} + +} + +ErrorOr GIFWriter::encode(Stream& stream, Bitmap const& bitmap) +{ + TRY(write_header(stream)); + + BigEndianOutputBitStream bit_stream { MaybeOwned { stream } }; + TRY(write_logical_descriptor(bit_stream, bitmap)); + TRY(write_global_color_table(bit_stream)); + + // Write a Table-Based Image + TRY(write_image_descriptor(bit_stream, bitmap)); + TRY(write_image_data(stream, bitmap)); + + TRY(write_trailer(bit_stream)); + + return {}; +} + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/GIFWriter.h b/Userland/Libraries/LibGfx/ImageFormats/GIFWriter.h new file mode 100644 index 00000000000..d31c95c8d02 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/GIFWriter.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Gfx { + +// Specified at: https://www.w3.org/Graphics/GIF/spec-gif89a.txt + +class GIFWriter { +public: + static ErrorOr encode(Stream&, Bitmap const&); +}; + +}