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; +}; + +}