/* * Copyright (c) 2024, Lucas Chollet * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include namespace Gfx { namespace { ErrorOr write_header(Stream& stream) { // 17. Header TRY(stream.write_until_depleted("GIF89a"sv)); return {}; } ErrorOr write_logical_descriptor(BigEndianOutputBitStream& stream, IntSize size) { // 18. Logical Screen Descriptor if (size.width() > NumericLimits::max() || size.height() > NumericLimits::max()) return Error::from_string_literal("Bitmap size is too big for a GIF"); TRY(stream.write_value(size.width())); TRY(stream.write_value(size.height())); // Global Color Table Flag TRY(stream.write_bits(false, 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(0u, 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_color_table(Stream& stream, ColorPalette const& palette) { // 19. Global Color Table or 21. Local Color Table. for (u16 i = 0; i < 256; ++i) { auto const color = i < palette.palette().size() ? palette.palette()[i] : Color::NamedColor::White; TRY(stream.write_value(color.red())); TRY(stream.write_value(color.green())); TRY(stream.write_value(color.blue())); } return {}; } ErrorOr write_image_data(Stream& stream, Bitmap const& bitmap, ColorPalette const& palette) { // 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)); indexes[i] = palette.index_of_closest_color(color); } 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, IntPoint at = {}) { // 20. Image Descriptor // Image Separator TRY(stream.write_value(0x2c)); // Image Left Position TRY(stream.write_value(at.x())); // Image Top Position TRY(stream.write_value(at.y())); // Image Width TRY(stream.write_value(bitmap.width())); // Image Height TRY(stream.write_value(bitmap.height())); // Local Color Table Flag TRY(stream.write_bits(true, 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(7u, 3)); return {}; } ErrorOr write_graphic_control_extension(BigEndianOutputBitStream& stream, int duration_ms) { // 23. Graphic Control Extension // Extension Introducer TRY(stream.write_value(0x21)); // Graphic Control Label TRY(stream.write_value(0xF9)); // Block Size TRY(stream.write_value(4)); // Packed Field // Reserved TRY(stream.write_bits(0u, 3)); // Disposal Method TRY(stream.write_bits(0u, 3)); // User Input Flag TRY(stream.write_bits(false, 1)); // Transparency Flag TRY(stream.write_bits(false, 1)); // Delay Time TRY(stream.write_value(duration_ms / 10)); // Transparent Color Index TRY(stream.write_value(0)); // Block Terminator TRY(stream.write_value(0)); return {}; } ErrorOr write_trailer(Stream& stream) { TRY(stream.write_value(0x3B)); return {}; } class GIFAnimationWriter : public AnimationWriter { public: GIFAnimationWriter(SeekableStream& stream) : m_stream(stream) { } virtual ErrorOr add_frame(Bitmap&, int, IntPoint) override; private: SeekableStream& m_stream; bool m_is_first_frame { true }; }; ErrorOr GIFAnimationWriter::add_frame(Bitmap& bitmap, int duration_ms, IntPoint at = {}) { // Let's get rid of the previously written trailer if (!m_is_first_frame) TRY(m_stream.seek(-1, SeekMode::FromCurrentPosition)); m_is_first_frame = false; // Write a Table-Based Image BigEndianOutputBitStream bit_stream { MaybeOwned { m_stream } }; TRY(write_graphic_control_extension(bit_stream, duration_ms)); TRY(write_image_descriptor(bit_stream, bitmap, at)); auto const palette = TRY(median_cut(bitmap, 256)); TRY(write_color_table(m_stream, palette)); TRY(write_image_data(m_stream, bitmap, palette)); // We always write a trailer to ensure that the file is valid. TRY(write_trailer(m_stream)); return {}; } ErrorOr write_netscape_extension(BigEndianOutputBitStream& stream, u16 loop_count) { // This is a vendor extension, its sole usage is to provide the loop count. // I used this link as a source: https://web.archive.org/web/19990418091037/http://www6.uniovi.es/gifanim/gifabout.htm // Extension Introducer TRY(stream.write_value(0x21)); // Application Extension Label TRY(stream.write_value(0xFF)); // Block Size constexpr auto netscape_signature = "NETSCAPE2.0"sv; TRY(stream.write_value(netscape_signature.length())); TRY(stream.write_until_depleted(netscape_signature)); // Length of Data Sub-Block TRY(stream.write_value(3)); // Undocumented TRY(stream.write_value(1)); // Number of loops, 0 means infinite TRY(stream.write_value(loop_count)); // Block Terminator TRY(stream.write_value(0)); return {}; } } ErrorOr GIFWriter::encode(Stream& stream, Bitmap const& bitmap) { auto const palette = TRY(median_cut(bitmap, 256)); TRY(write_header(stream)); BigEndianOutputBitStream bit_stream { MaybeOwned { stream } }; TRY(write_logical_descriptor(bit_stream, bitmap.size())); // Write a Table-Based Image TRY(write_image_descriptor(bit_stream, bitmap)); TRY(write_color_table(bit_stream, palette)); TRY(write_image_data(stream, bitmap, palette)); TRY(write_trailer(bit_stream)); return {}; } ErrorOr> GIFWriter::start_encoding_animation(SeekableStream& stream, IntSize dimensions, u16 loop_count) { TRY(write_header(stream)); BigEndianOutputBitStream bit_stream { MaybeOwned { stream } }; TRY(write_logical_descriptor(bit_stream, dimensions)); // Vendor extension to support looping TRY(write_netscape_extension(bit_stream, loop_count)); return make(stream); } }