From f7e152d049f760fcd3ede7e3f9b1761b9800df0d Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Thu, 23 Feb 2023 22:37:08 -0500 Subject: [PATCH] LibGfx: Add scaffolding for a webp decoder At the moment, this processes the RIFF chunk structure and extracts the ICCP chunk, so that `icc` can now print ICC profiles embedded in webp files. (And are image files really more than containers of icc profiles?) It doesn't even decode image dimensions yet. The lossy format is a VP8 video frame. Once we get to that, we might want to move all the image decoders into a new LibImageDecoders that depends on both LibGfx and LibVideo. (Other newer image formats like heic and av1f also use video frames for image data.) --- AK/Debug.h.in | 4 + Meta/CMake/all_the_debug_macros.cmake | 1 + Userland/Libraries/LibCore/MimeData.cpp | 3 + Userland/Libraries/LibGfx/CMakeLists.txt | 1 + Userland/Libraries/LibGfx/ImageDecoder.cpp | 2 + Userland/Libraries/LibGfx/WebPLoader.cpp | 310 +++++++++++++++++++++ Userland/Libraries/LibGfx/WebPLoader.h | 38 +++ 7 files changed, 359 insertions(+) create mode 100644 Userland/Libraries/LibGfx/WebPLoader.cpp create mode 100644 Userland/Libraries/LibGfx/WebPLoader.h diff --git a/AK/Debug.h.in b/AK/Debug.h.in index eb04cba8db..7c31107ef6 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -522,6 +522,10 @@ #cmakedefine01 WEB_WORKER_DEBUG #endif +#ifndef WEBP_DEBUG +#cmakedefine01 WEBP_DEBUG +#endif + #ifndef WINDOWMANAGER_DEBUG #cmakedefine01 WINDOWMANAGER_DEBUG #endif diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index 8a5db2c19a..9905999fa7 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -217,6 +217,7 @@ set(WEBGL_CONTEXT_DEBUG ON) set(WEBSERVER_DEBUG ON) set(WEB_FETCH_DEBUG ON) set(WEB_WORKER_DEBUG ON) +set(WEBP_DEBUG ON) set(WINDOWMANAGER_DEBUG ON) set(WSMESSAGELOOP_DEBUG ON) set(WSSCREEN_DEBUG ON) diff --git a/Userland/Libraries/LibCore/MimeData.cpp b/Userland/Libraries/LibCore/MimeData.cpp index d679ea53b3..7e08ebea1b 100644 --- a/Userland/Libraries/LibCore/MimeData.cpp +++ b/Userland/Libraries/LibCore/MimeData.cpp @@ -73,6 +73,8 @@ StringView guess_mime_type_based_on_filename(StringView path) return "image/svg+xml"sv; if (path.ends_with(".tga"sv, CaseSensitivity::CaseInsensitive)) return "image/x-targa"sv; + if (path.ends_with(".webp"sv, CaseSensitivity::CaseInsensitive)) + return "image/webp"sv; if (path.ends_with(".md"sv, CaseSensitivity::CaseInsensitive)) return "text/markdown"sv; if (path.ends_with(".html"sv, CaseSensitivity::CaseInsensitive) || path.ends_with(".htm"sv, CaseSensitivity::CaseInsensitive)) @@ -152,6 +154,7 @@ StringView guess_mime_type_based_on_filename(StringView path) __ENUMERATE_MIME_TYPE_HEADER(tiff_bigendian, "image/tiff", 0, 4, 'M', 'M', 0x00, '*') \ __ENUMERATE_MIME_TYPE_HEADER(wasm, "application/wasm", 0, 4, 0x00, 'a', 's', 'm') \ __ENUMERATE_MIME_TYPE_HEADER(wav, "audio/wave", 8, 4, 'W', 'A', 'V', 'E') \ + __ENUMERATE_MIME_TYPE_HEADER(webp, "image/webp", 8, 4, 'W', 'E', 'B', 'P') \ __ENUMERATE_MIME_TYPE_HEADER(win_31x_archive, "extra/win-31x-compressed", 0, 4, 'K', 'W', 'A', 'J') \ __ENUMERATE_MIME_TYPE_HEADER(win_95_archive, "extra/win-95-compressed", 0, 4, 'S', 'Z', 'D', 'D') \ __ENUMERATE_MIME_TYPE_HEADER(zlib_0, "extra/raw-zlib", 0, 2, 0x78, 0x01) \ diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index 9e5ac10092..d0199b82c9 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -55,6 +55,7 @@ set(SOURCES TextLayout.cpp TGALoader.cpp Triangle.cpp + WebPLoader.cpp WindowTheme.cpp ) diff --git a/Userland/Libraries/LibGfx/ImageDecoder.cpp b/Userland/Libraries/LibGfx/ImageDecoder.cpp index 3395a55bd9..3a404d44d3 100644 --- a/Userland/Libraries/LibGfx/ImageDecoder.cpp +++ b/Userland/Libraries/LibGfx/ImageDecoder.cpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace Gfx { @@ -36,6 +37,7 @@ static constexpr ImagePluginInitializer s_initializers[] = { { JPEGImageDecoderPlugin::sniff, JPEGImageDecoderPlugin::create }, { DDSImageDecoderPlugin::sniff, DDSImageDecoderPlugin::create }, { QOIImageDecoderPlugin::sniff, QOIImageDecoderPlugin::create }, + { WebPImageDecoderPlugin::sniff, WebPImageDecoderPlugin::create }, }; struct ImagePluginWithMIMETypeInitializer { diff --git a/Userland/Libraries/LibGfx/WebPLoader.cpp b/Userland/Libraries/LibGfx/WebPLoader.cpp new file mode 100644 index 0000000000..19755e6003 --- /dev/null +++ b/Userland/Libraries/LibGfx/WebPLoader.cpp @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2023, Nico Weber + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +// Container: https://developers.google.com/speed/webp/docs/riff_container +// Lossless format: https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification +// Lossy format: https://datatracker.ietf.org/doc/html/rfc6386 + +namespace Gfx { + +namespace { + +struct FourCC { + constexpr FourCC(char const* name) + { + cc[0] = name[0]; + cc[1] = name[1]; + cc[2] = name[2]; + cc[3] = name[3]; + } + + bool operator==(FourCC const&) const = default; + bool operator!=(FourCC const&) const = default; + + char cc[4]; +}; + +// https://developers.google.com/speed/webp/docs/riff_container#webp_file_header +struct WebPFileHeader { + FourCC riff; + LittleEndian file_size; + FourCC webp; +}; +static_assert(AssertSize()); + +struct ChunkHeader { + FourCC chunk_type; + LittleEndian chunk_size; +}; +static_assert(AssertSize()); + +struct Chunk { + FourCC type; + ReadonlyBytes data; +}; + +} + +struct WebPLoadingContext { + enum State { + NotDecoded = 0, + Error, + HeaderDecoded, + SizeDecoded, + ChunksDecoded, + BitmapDecoded, + }; + State state { State::NotDecoded }; + ReadonlyBytes data; + + RefPtr bitmap; + + Optional icc_data; +}; + +// https://developers.google.com/speed/webp/docs/riff_container#webp_file_header +static ErrorOr decode_webp_header(WebPLoadingContext& context) +{ + if (context.state >= WebPLoadingContext::HeaderDecoded) + return {}; + + if (context.data.size() < sizeof(WebPFileHeader)) { + context.state = WebPLoadingContext::State::Error; + return Error::from_string_literal("Missing WebP header"); + } + + auto& header = *bit_cast(context.data.data()); + if (header.riff != FourCC("RIFF") || header.webp != FourCC("WEBP")) { + context.state = WebPLoadingContext::State::Error; + return Error::from_string_literal("Invalid WebP header"); + } + + // "File Size: [...] The size of the file in bytes starting at offset 8. The maximum value of this field is 2^32 minus 10 bytes." + u32 const maximum_webp_file_size = 0xffff'ffff - 9; + if (header.file_size > maximum_webp_file_size) { + context.state = WebPLoadingContext::State::Error; + return Error::from_string_literal("WebP header file size over maximum"); + } + + // "The file size in the header is the total size of the chunks that follow plus 4 bytes for the 'WEBP' FourCC. + // The file SHOULD NOT contain any data after the data specified by File Size. + // Readers MAY parse such files, ignoring the trailing data." + if (context.data.size() - 8 < header.file_size) { + context.state = WebPLoadingContext::State::Error; + return Error::from_string_literal("WebP data too small for size in header"); + } + if (context.data.size() - 8 > header.file_size) { + dbgln_if(WEBP_DEBUG, "WebP has {} bytes of data, but header needs only {}. Trimming.", context.data.size(), header.file_size + 8); + context.data = context.data.trim(header.file_size + 8); + } + + context.state = WebPLoadingContext::HeaderDecoded; + return {}; +} + +static ErrorOr decode_webp_chunk_header(WebPLoadingContext& context, ReadonlyBytes chunks) +{ + if (chunks.size() < sizeof(ChunkHeader)) { + context.state = WebPLoadingContext::State::Error; + return Error::from_string_literal("Not enough data for WebP chunk header"); + } + + auto const& header = *bit_cast(chunks.data()); + dbgln_if(WEBP_DEBUG, "chunk {} size {}", header.chunk_type, header.chunk_size); + + if (chunks.size() < sizeof(ChunkHeader) + header.chunk_size) { + context.state = WebPLoadingContext::State::Error; + return Error::from_string_literal("Not enough data for WebP chunk"); + } + + return Chunk { header.chunk_type, { chunks.data() + sizeof(ChunkHeader), header.chunk_size } }; +} + +static ErrorOr decode_webp_advance_chunk(WebPLoadingContext& context, ReadonlyBytes& chunks) +{ + auto chunk = TRY(decode_webp_chunk_header(context, chunks)); + chunks = chunks.slice(sizeof(ChunkHeader) + chunk.data.size()); + return chunk; +} + +// https://developers.google.com/speed/webp/docs/riff_container#simple_file_format_lossy +static ErrorOr decode_webp_simple_lossy(WebPLoadingContext& context, Chunk const& vp8_chunk) +{ + // FIXME + (void)context; + (void)vp8_chunk; + return {}; +} + +// https://developers.google.com/speed/webp/docs/riff_container#simple_file_format_lossless +static ErrorOr decode_webp_simple_lossless(WebPLoadingContext& context, Chunk const& vp8l_chunk) +{ + // FIXME + (void)context; + (void)vp8l_chunk; + return {}; +} + +// https://developers.google.com/speed/webp/docs/riff_container#extended_file_format +static ErrorOr decode_webp_extended(WebPLoadingContext& context, Chunk const& vp8x_chunk, ReadonlyBytes chunks) +{ + + // FIXME: Do something with this. + (void)vp8x_chunk; + + // FIXME: This isn't quite to spec, which says + // "All chunks SHOULD be placed in the same order as listed above. + // If a chunk appears in the wrong place, the file is invalid, but readers MAY parse the file, ignoring the chunks that are out of order." + while (!chunks.is_empty()) { + auto chunk = TRY(decode_webp_advance_chunk(context, chunks)); + + if (chunk.type == FourCC("ICCP")) + context.icc_data = chunk.data; + + // FIXME: Probably want to make this and decode_webp_simple_lossy/lossless call the same function + // instead of calling the _simple functions from the _extended function. + if (chunk.type == FourCC("VP8 ")) + TRY(decode_webp_simple_lossy(context, chunk)); + if (chunk.type == FourCC("VP8X")) + TRY(decode_webp_simple_lossless(context, chunk)); + } + + context.state = WebPLoadingContext::State::ChunksDecoded; + return {}; +} + +static ErrorOr decode_webp_chunks(WebPLoadingContext& context) +{ + if (context.state >= WebPLoadingContext::State::ChunksDecoded) + return {}; + + if (context.state < WebPLoadingContext::HeaderDecoded) + TRY(decode_webp_header(context)); + + ReadonlyBytes chunks = context.data.slice(sizeof(WebPFileHeader)); + auto first_chunk = TRY(decode_webp_advance_chunk(context, chunks)); + + if (first_chunk.type == FourCC("VP8 ")) { + context.state = WebPLoadingContext::State::ChunksDecoded; + return decode_webp_simple_lossy(context, first_chunk); + } + + if (first_chunk.type == FourCC("VP8L")) { + context.state = WebPLoadingContext::State::ChunksDecoded; + return decode_webp_simple_lossless(context, first_chunk); + } + + if (first_chunk.type == FourCC("VP8X")) + return decode_webp_extended(context, first_chunk, chunks); + + return Error::from_string_literal("WebPImageDecoderPlugin: Invalid first chunk type"); +} + +WebPImageDecoderPlugin::WebPImageDecoderPlugin(ReadonlyBytes data, OwnPtr context) + : m_context(move(context)) +{ + m_context->data = data; +} + +WebPImageDecoderPlugin::~WebPImageDecoderPlugin() = default; + +IntSize WebPImageDecoderPlugin::size() +{ + if (m_context->state == WebPLoadingContext::State::Error) + return {}; + + if (m_context->state < WebPLoadingContext::State::SizeDecoded) { + // FIXME + } + + // FIXME + return { 0, 0 }; +} + +void WebPImageDecoderPlugin::set_volatile() +{ + if (m_context->bitmap) + m_context->bitmap->set_volatile(); +} + +bool WebPImageDecoderPlugin::set_nonvolatile(bool& was_purged) +{ + if (!m_context->bitmap) + return false; + return m_context->bitmap->set_nonvolatile(was_purged); +} + +bool WebPImageDecoderPlugin::initialize() +{ + return !decode_webp_header(*m_context).is_error(); +} + +ErrorOr WebPImageDecoderPlugin::sniff(ReadonlyBytes data) +{ + WebPLoadingContext context; + context.data = data; + TRY(decode_webp_header(context)); + return true; +} + +ErrorOr> WebPImageDecoderPlugin::create(ReadonlyBytes data) +{ + auto context = TRY(try_make()); + return adopt_nonnull_own_or_enomem(new (nothrow) WebPImageDecoderPlugin(data, move(context))); +} + +bool WebPImageDecoderPlugin::is_animated() +{ + // FIXME + return false; +} + +size_t WebPImageDecoderPlugin::loop_count() +{ + // FIXME + return 0; +} + +size_t WebPImageDecoderPlugin::frame_count() +{ + // FIXME + return 1; +} + +ErrorOr WebPImageDecoderPlugin::frame(size_t index) +{ + if (index >= frame_count()) + return Error::from_string_literal("WebPImageDecoderPlugin: Invalid frame index"); + + return Error::from_string_literal("WebPImageDecoderPlugin: decoding not yet implemented"); +} + +ErrorOr> WebPImageDecoderPlugin::icc_data() +{ + TRY(decode_webp_chunks(*m_context)); + return m_context->icc_data; +} + +} + +template<> +struct AK::Formatter : StandardFormatter { + ErrorOr format(FormatBuilder& builder, Gfx::FourCC const& four_cc) + { + TRY(builder.put_padding('\'', 1)); + TRY(builder.put_padding(four_cc.cc[0], 1)); + TRY(builder.put_padding(four_cc.cc[1], 1)); + TRY(builder.put_padding(four_cc.cc[2], 1)); + TRY(builder.put_padding(four_cc.cc[3], 1)); + TRY(builder.put_padding('\'', 1)); + return {}; + } +}; diff --git a/Userland/Libraries/LibGfx/WebPLoader.h b/Userland/Libraries/LibGfx/WebPLoader.h new file mode 100644 index 0000000000..8352093f1a --- /dev/null +++ b/Userland/Libraries/LibGfx/WebPLoader.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, Nico Weber + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Gfx { + +struct WebPLoadingContext; + +class WebPImageDecoderPlugin final : public ImageDecoderPlugin { +public: + static ErrorOr sniff(ReadonlyBytes); + static ErrorOr> create(ReadonlyBytes); + + virtual ~WebPImageDecoderPlugin() override; + + virtual IntSize size() override; + virtual void set_volatile() override; + [[nodiscard]] virtual bool set_nonvolatile(bool& was_purged) override; + virtual bool initialize() override; + virtual bool is_animated() override; + virtual size_t loop_count() override; + virtual size_t frame_count() override; + virtual ErrorOr frame(size_t index) override; + virtual ErrorOr> icc_data() override; + +private: + WebPImageDecoderPlugin(ReadonlyBytes, OwnPtr); + + OwnPtr m_context; +}; + +}