diff options
author | Nico Weber <thakis@chromium.org> | 2023-02-23 22:37:08 -0500 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2023-02-24 19:44:20 +0100 |
commit | f7e152d049f760fcd3ede7e3f9b1761b9800df0d (patch) | |
tree | edd97d4a34a61214ac51f591bd9fa3782b9bfa0e /Userland | |
parent | 4bf639b63598791fe746e3cde17b89319a19b59f (diff) | |
download | serenity-f7e152d049f760fcd3ede7e3f9b1761b9800df0d.zip |
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.)
Diffstat (limited to 'Userland')
-rw-r--r-- | Userland/Libraries/LibCore/MimeData.cpp | 3 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/ImageDecoder.cpp | 2 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/WebPLoader.cpp | 310 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/WebPLoader.h | 38 |
5 files changed, 354 insertions, 0 deletions
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 <LibGfx/PPMLoader.h> #include <LibGfx/QOILoader.h> #include <LibGfx/TGALoader.h> +#include <LibGfx/WebPLoader.h> 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 <thakis@chromium.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <AK/Debug.h> +#include <AK/Endian.h> +#include <AK/Format.h> +#include <LibGfx/WebPLoader.h> + +// 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<u32> file_size; + FourCC webp; +}; +static_assert(AssertSize<WebPFileHeader, 12>()); + +struct ChunkHeader { + FourCC chunk_type; + LittleEndian<u32> chunk_size; +}; +static_assert(AssertSize<ChunkHeader, 8>()); + +struct Chunk { + FourCC type; + ReadonlyBytes data; +}; + +} + +struct WebPLoadingContext { + enum State { + NotDecoded = 0, + Error, + HeaderDecoded, + SizeDecoded, + ChunksDecoded, + BitmapDecoded, + }; + State state { State::NotDecoded }; + ReadonlyBytes data; + + RefPtr<Gfx::Bitmap> bitmap; + + Optional<ReadonlyBytes> icc_data; +}; + +// https://developers.google.com/speed/webp/docs/riff_container#webp_file_header +static ErrorOr<void> 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<WebPFileHeader const*>(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<Chunk> 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<ChunkHeader const*>(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<Chunk> 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<void> 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<void> 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<void> 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<void> 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<WebPLoadingContext> 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<bool> WebPImageDecoderPlugin::sniff(ReadonlyBytes data) +{ + WebPLoadingContext context; + context.data = data; + TRY(decode_webp_header(context)); + return true; +} + +ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> WebPImageDecoderPlugin::create(ReadonlyBytes data) +{ + auto context = TRY(try_make<WebPLoadingContext>()); + 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<ImageFrameDescriptor> 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<Optional<ReadonlyBytes>> WebPImageDecoderPlugin::icc_data() +{ + TRY(decode_webp_chunks(*m_context)); + return m_context->icc_data; +} + +} + +template<> +struct AK::Formatter<Gfx::FourCC> : StandardFormatter { + ErrorOr<void> 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 <thakis@chromium.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibGfx/ImageDecoder.h> + +namespace Gfx { + +struct WebPLoadingContext; + +class WebPImageDecoderPlugin final : public ImageDecoderPlugin { +public: + static ErrorOr<bool> sniff(ReadonlyBytes); + static ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> 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<ImageFrameDescriptor> frame(size_t index) override; + virtual ErrorOr<Optional<ReadonlyBytes>> icc_data() override; + +private: + WebPImageDecoderPlugin(ReadonlyBytes, OwnPtr<WebPLoadingContext>); + + OwnPtr<WebPLoadingContext> m_context; +}; + +} |