LibGfx: Add a GIF writer
This version is really barebone as it does not support colors (only black and white) or animated images.
This commit is contained in:
parent
54f33b43c6
commit
2513a1b83f
Notes:
sideshowbarker
2024-07-17 05:23:40 +09:00
Author: https://github.com/LucasChollet Commit: https://github.com/SerenityOS/serenity/commit/2513a1b83f Pull-request: https://github.com/SerenityOS/serenity/pull/24191 Reviewed-by: https://github.com/nico ✅ Reviewed-by: https://github.com/timschumi
4 changed files with 196 additions and 9 deletions
|
@ -12,6 +12,8 @@
|
|||
#include <LibGfx/ImageFormats/AnimationWriter.h>
|
||||
#include <LibGfx/ImageFormats/BMPLoader.h>
|
||||
#include <LibGfx/ImageFormats/BMPWriter.h>
|
||||
#include <LibGfx/ImageFormats/GIFLoader.h>
|
||||
#include <LibGfx/ImageFormats/GIFWriter.h>
|
||||
#include <LibGfx/ImageFormats/JPEGLoader.h>
|
||||
#include <LibGfx/ImageFormats/JPEGWriter.h>
|
||||
#include <LibGfx/ImageFormats/PNGLoader.h>
|
||||
|
@ -22,21 +24,22 @@
|
|||
#include <LibGfx/ImageFormats/WebPWriter.h>
|
||||
#include <LibTest/TestCase.h>
|
||||
|
||||
static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> expect_single_frame(Gfx::ImageDecoderPlugin& plugin_decoder)
|
||||
static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> 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<NonnullRefPtr<Gfx::Bitmap>> expect_single_frame_of_size(Gfx::ImageDecoderPlugin& plugin_decoder, Gfx::IntSize size)
|
||||
static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> 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<ByteBuffer> encode_bitmap(Gfx::Bitmap const& bitmap, ExtraArgs...
|
|||
}
|
||||
|
||||
template<class Writer, class Loader>
|
||||
static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> get_roundtrip_bitmap(Gfx::Bitmap const& bitmap)
|
||||
static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> get_roundtrip_bitmap(Gfx::Bitmap const& bitmap, bool is_gif = false)
|
||||
{
|
||||
auto encoded_data = TRY(encode_bitmap<Writer>(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<class Writer, class Loader>
|
||||
static ErrorOr<void> test_roundtrip(Gfx::Bitmap const& bitmap)
|
||||
static ErrorOr<void> test_roundtrip(Gfx::Bitmap const& bitmap, bool is_gif = false)
|
||||
{
|
||||
auto decoded = TRY((get_roundtrip_bitmap<Writer, Loader>(bitmap)));
|
||||
auto decoded = TRY((get_roundtrip_bitmap<Writer, Loader>(bitmap, is_gif)));
|
||||
expect_bitmaps_equal(*decoded, bitmap);
|
||||
return {};
|
||||
}
|
||||
|
@ -108,6 +111,18 @@ TEST_CASE(test_bmp)
|
|||
TRY_OR_FAIL((test_roundtrip<Gfx::BMPWriter, Gfx::BMPImageDecoderPlugin>(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<Gfx::GIFWriter, Gfx::GIFImageDecoderPlugin>(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.
|
||||
|
|
|
@ -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
|
||||
|
|
150
Userland/Libraries/LibGfx/ImageFormats/GIFWriter.cpp
Normal file
150
Userland/Libraries/LibGfx/ImageFormats/GIFWriter.cpp
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Lucas Chollet <lucas.chollet@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/BitStream.h>
|
||||
#include <LibCompress/Lzw.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/ImageFormats/GIFWriter.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
namespace {
|
||||
|
||||
ErrorOr<void> write_header(Stream& stream)
|
||||
{
|
||||
// 17. Header
|
||||
TRY(stream.write_until_depleted("GIF87a"sv));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> write_logical_descriptor(BigEndianOutputBitStream& stream, Bitmap const& bitmap)
|
||||
{
|
||||
// 18. Logical Screen Descriptor
|
||||
|
||||
if (bitmap.width() > NumericLimits<u16>::max() || bitmap.height() > NumericLimits<u16>::max())
|
||||
return Error::from_string_literal("Bitmap size is too big for a GIF");
|
||||
|
||||
TRY(stream.write_value<u16>(bitmap.width()));
|
||||
TRY(stream.write_value<u16>(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<u8>(0));
|
||||
|
||||
// Pixel Aspect Ratio
|
||||
// NOTE: We can write a zero as most decoders discard the value.
|
||||
TRY(stream.write_value<u8>(0));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> 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<u8>(i));
|
||||
TRY(stream.write_value<u8>(i));
|
||||
TRY(stream.write_value<u8>(i));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> write_image_data(Stream& stream, Bitmap const& bitmap)
|
||||
{
|
||||
// 22. Table Based Image Data
|
||||
auto const pixel_number = static_cast<u32>(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<u8>(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<u8>(to_write));
|
||||
TRY(stream.write_until_depleted(encoded.bytes().slice(offset, to_write)));
|
||||
}
|
||||
|
||||
// Block terminator
|
||||
TRY(stream.write_value<u8>(0));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> write_image_descriptor(BigEndianOutputBitStream& stream, Bitmap const& bitmap)
|
||||
{
|
||||
// 20. Image Descriptor
|
||||
|
||||
// Image Separator
|
||||
TRY(stream.write_value<u8>(0x2c));
|
||||
// Image Left Position
|
||||
TRY(stream.write_value<u16>(0));
|
||||
// Image Top Position
|
||||
TRY(stream.write_value<u16>(0));
|
||||
// Image Width
|
||||
TRY(stream.write_value<u16>(bitmap.width()));
|
||||
// Image Height
|
||||
TRY(stream.write_value<u16>(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<void> write_trailer(Stream& stream)
|
||||
{
|
||||
TRY(stream.write_value<u8>(0x3B));
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ErrorOr<void> GIFWriter::encode(Stream& stream, Bitmap const& bitmap)
|
||||
{
|
||||
TRY(write_header(stream));
|
||||
|
||||
BigEndianOutputBitStream bit_stream { MaybeOwned<Stream> { 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 {};
|
||||
}
|
||||
|
||||
}
|
21
Userland/Libraries/LibGfx/ImageFormats/GIFWriter.h
Normal file
21
Userland/Libraries/LibGfx/ImageFormats/GIFWriter.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Lucas Chollet <lucas.chollet@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <LibGfx/Forward.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
// Specified at: https://www.w3.org/Graphics/GIF/spec-gif89a.txt
|
||||
|
||||
class GIFWriter {
|
||||
public:
|
||||
static ErrorOr<void> encode(Stream&, Bitmap const&);
|
||||
};
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue