mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-05-18 17:12:54 +00:00
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.)
310 lines
9.4 KiB
C++
310 lines
9.4 KiB
C++
/*
|
|
* 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 {};
|
|
}
|
|
};
|