From 528865b14acb583114014b5c67552f90f94044f9 Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Sun, 5 May 2024 20:12:57 -0400 Subject: [PATCH] make-webp --- Meta/Lagom/CMakeLists.txt | 1 + Userland/Utilities/CMakeLists.txt | 1 + Userland/Utilities/make-webp.cpp | 188 ++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 Userland/Utilities/make-webp.cpp diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index 98cbe73627c5d2..51de6df1cb905e 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -566,6 +566,7 @@ if (BUILD_LAGOM) lagom_utility(icc SOURCES ../../Userland/Utilities/icc.cpp LIBS LibGfx LibMain LibURL) lagom_utility(image SOURCES ../../Userland/Utilities/image.cpp LIBS LibGfx LibMain) + lagom_utility(make-webp SOURCES ../../Userland/Utilities/make-webp.cpp LIBS LibGfx LibMain) lagom_utility(isobmff SOURCES ../../Userland/Utilities/isobmff.cpp LIBS LibGfx LibMain) lagom_utility(ttfdisasm SOURCES ../../Userland/Utilities/ttfdisasm.cpp LIBS LibGfx LibMain) lagom_utility(js SOURCES ../../Userland/Utilities/js.cpp LIBS LibCrypto LibJS LibLine LibLocale LibMain LibTextCodec Threads::Threads) diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index 12b8ef0ec9e355..bd1225f3bfb2ab 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -103,6 +103,7 @@ target_link_libraries(gzip PRIVATE LibCompress) target_link_libraries(headless-browser PRIVATE LibCrypto LibFileSystem LibGemini LibGfx LibHTTP LibImageDecoderClient LibTLS LibWeb LibWebView LibWebSocket LibIPC LibJS LibDiff LibURL) target_link_libraries(icc PRIVATE LibGfx LibVideo LibURL) target_link_libraries(image PRIVATE LibGfx) +target_link_libraries(make-webp PRIVATE LibGfx) target_link_libraries(image2bin PRIVATE LibGfx) target_link_libraries(ini PRIVATE LibFileSystem) target_link_libraries(install-bin PRIVATE LibFileSystem) diff --git a/Userland/Utilities/make-webp.cpp b/Userland/Utilities/make-webp.cpp new file mode 100644 index 00000000000000..3a949d13490092 --- /dev/null +++ b/Userland/Utilities/make-webp.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2024, Nico Weber + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +// Container: https://developers.google.com/speed/webp/docs/riff_container +// Lossless format: https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification + +#include +#include +#include +#include +#include +#include +#include + +struct Options { + StringView out_path; + int width { 512 }; + int height { 512 }; +}; + +// https://developers.google.com/speed/webp/docs/riff_container#webp_file_header +static ErrorOr write_webp_header(Stream& stream, unsigned data_size) +{ + TRY(stream.write_until_depleted("RIFF"sv)); + TRY(stream.write_value>(4 + data_size)); // Including size of "WEBP" and the data size itself. + TRY(stream.write_until_depleted("WEBP"sv)); + return {}; +} + +static ErrorOr write_chunk_header(Stream& stream, StringView chunk_fourcc, unsigned vp8l_data_size) +{ + TRY(stream.write_until_depleted(chunk_fourcc)); + TRY(stream.write_value>(vp8l_data_size)); + return {}; +} + +// https://developers.google.com/speed/webp/docs/riff_container#simple_file_format_lossless +// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#7_overall_structure_of_the_format +static ErrorOr write_VP8L_header(Stream& stream, unsigned width, unsigned height, bool alpha_hint) +{ + // "The 14-bit precision for image width and height limits the maximum size of a WebP lossless image to 16384✕16384 pixels." + if (width > 16384 || height > 16384) + return Error::from_string_literal("WebP lossless images can't be larger than 16384x16384 pixels"); + + if (width == 0 || height == 0) + return Error::from_string_literal("WebP lossless images must be at least one pixel wide and tall"); + + LittleEndianOutputBitStream bit_stream { MaybeOwned(stream) }; + + // Signature byte. + TRY(bit_stream.write_bits(0x2fu, 8u)); // Signature byte + + // 14 bits width-1, 14 bits height-1, 1 bit alpha hint, 3 bit version_number. + TRY(bit_stream.write_bits(width - 1, 14u)); + TRY(bit_stream.write_bits(height - 1, 14u)); + + // "The alpha_is_used bit is a hint only, and should not impact decoding. + // It should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise." + TRY(bit_stream.write_bits(alpha_hint, 1u)); + + // "The version_number is a 3 bit code that must be set to 0." + TRY(bit_stream.write_bits(0u, 3u)); + + // FIXME: Make ~LittleEndianOutputBitStream do this, or make it VERIFY() that it has happened at least. + TRY(bit_stream.flush_buffer_to_stream()); + + return {}; +} + +static ErrorOr write_VP8L_image_data(Stream& stream) +{ + LittleEndianOutputBitStream bit_stream { MaybeOwned(stream) }; + + // optional-transform = (%b1 transform optional-transform) / %b0 + TRY(bit_stream.write_bits(0u, 1u)); // No transform for now. + + // https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#5_image_data + // spatially-coded-image = color-cache-info meta-prefix data + + // color-cache-info = %b0 + // color-cache-info =/ (%b1 4BIT) ; 1 followed by color cache size + TRY(bit_stream.write_bits(0u, 1u)); // No color cache for now. + + // meta-prefix = %b0 / (%b1 entropy-image) + TRY(bit_stream.write_bits(0u, 1u)); // No meta prefix for now. + + u8 r = 255; + u8 g = 0; + u8 b = 0; + u8 a = 255; + + // G + TRY(bit_stream.write_bits(1u, 1u)); // Simple code length code. + TRY(bit_stream.write_bits(0u, 1u)); // num_symbols - 1 + TRY(bit_stream.write_bits(1u, 1u)); // is_first_8bits + TRY(bit_stream.write_bits(g, 8u)); // symbol0 + + // R + TRY(bit_stream.write_bits(1u, 1u)); // Simple code length code. + TRY(bit_stream.write_bits(0u, 1u)); // num_symbols - 1 + TRY(bit_stream.write_bits(1u, 1u)); // is_first_8bits + TRY(bit_stream.write_bits(r, 8u)); // symbol0 + + // B + TRY(bit_stream.write_bits(1u, 1u)); // Simple code length code. + TRY(bit_stream.write_bits(0u, 1u)); // num_symbols - 1 + TRY(bit_stream.write_bits(1u, 1u)); // is_first_8bits + TRY(bit_stream.write_bits(b, 8u)); // symbol0 + + // A + TRY(bit_stream.write_bits(1u, 1u)); // Simple code length code. + TRY(bit_stream.write_bits(0u, 1u)); // num_symbols - 1 + TRY(bit_stream.write_bits(1u, 1u)); // is_first_8bits + TRY(bit_stream.write_bits(a, 8u)); // symbol0 + + // Distance codes (unused). + TRY(bit_stream.write_bits(1u, 1u)); // Simple code length code. + TRY(bit_stream.write_bits(0u, 1u)); // num_symbols - 1 + TRY(bit_stream.write_bits(0u, 1u)); // is_first_8bits + TRY(bit_stream.write_bits(0u, 1u)); // symbol0 + + // FIXME: Make ~LittleEndianOutputBitStream do this, or make it VERIFY() that it has happened at least. + TRY(bit_stream.align_to_byte_boundary()); + TRY(bit_stream.flush_buffer_to_stream()); + + return {}; +} + +// FIXME: Consider using LibRIFF for RIFF writing details. (It currently has no writing support.) +static ErrorOr align_to_two(AllocatingMemoryStream& stream) +{ + // https://developers.google.com/speed/webp/docs/riff_container + // "If Chunk Size is odd, a single padding byte -- which MUST be 0 to conform with RIFF -- is added." + if (stream.used_buffer_size() % 2 != 0) + TRY(stream.write_value(0)); + return {}; +} + +ErrorOr write_webp(Stream& stream, Options const& options) +{ + // The chunk headers need to know their size, so we either need a SeekableStream or need to buffer the data. We're doing the latter. + // FIXME: The whole writing-and-reading-into-buffer over-and-over is awkward and inefficient. + AllocatingMemoryStream vp8l_header_stream; + TRY(write_VP8L_header(vp8l_header_stream, options.width, options.height, true)); + auto vp8l_header_bytes = TRY(vp8l_header_stream.read_until_eof()); + + AllocatingMemoryStream vp8l_data_stream; + TRY(write_VP8L_image_data(vp8l_data_stream)); + auto vp8l_data_bytes = TRY(vp8l_data_stream.read_until_eof()); + + AllocatingMemoryStream vp8l_chunk_stream; + TRY(write_chunk_header(vp8l_chunk_stream, "VP8L"sv, vp8l_header_bytes.size() + vp8l_data_bytes.size())); + TRY(vp8l_chunk_stream.write_until_depleted(vp8l_header_bytes)); + TRY(vp8l_chunk_stream.write_until_depleted(vp8l_data_bytes)); + TRY(align_to_two(vp8l_chunk_stream)); + auto vp8l_chunk_bytes = TRY(vp8l_chunk_stream.read_until_eof()); + + TRY(write_webp_header(stream, vp8l_chunk_bytes.size())); + TRY(stream.write_until_depleted(vp8l_chunk_bytes)); + return {}; +} + +static ErrorOr parse_options(Main::Arguments arguments) +{ + Options options; + Core::ArgsParser args_parser; + args_parser.add_option(options.out_path, "Path to output image file", "output", 'o', "FILE"); + args_parser.parse(arguments); + + if (options.out_path.is_empty()) + return Error::from_string_view("-o is required"sv); + + return options; +} + +ErrorOr serenity_main(Main::Arguments arguments) +{ + Options options = TRY(parse_options(arguments)); + + auto output_stream = TRY(Core::File::open(options.out_path, Core::File::OpenMode::Write)); + + TRY(write_webp(*TRY(Core::OutputBufferedFile::create(move(output_stream))), options)); + + return 0; +}