/* * Copyright (c) 2023, Nico Weber * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include // Overview: https://developers.google.com/speed/webp/docs/compression // 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; template [[nodiscard]] class Error error(char const (&string_literal)[N]) { state = WebPLoadingContext::State::Error; return Error::from_string_literal(string_literal); } }; // 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)) return context.error("Missing WebP header"); auto& header = *bit_cast(context.data.data()); if (header.riff != FourCC("RIFF") || header.webp != FourCC("WEBP")) return context.error("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) return context.error("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) return context.error("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 {}; } // https://developers.google.com/speed/webp/docs/riff_container#riff_file_format static ErrorOr decode_webp_chunk_header(WebPLoadingContext& context, ReadonlyBytes chunks) { if (chunks.size() < sizeof(ChunkHeader)) return context.error("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) return context.error("Not enough data for WebP chunk"); return Chunk { header.chunk_type, { chunks.data() + sizeof(ChunkHeader), header.chunk_size } }; } // https://developers.google.com/speed/webp/docs/riff_container#riff_file_format static ErrorOr decode_webp_advance_chunk(WebPLoadingContext& context, ReadonlyBytes& chunks) { auto chunk = TRY(decode_webp_chunk_header(context, chunks)); // "Chunk Size: 32 bits (uint32) // The size of the chunk in bytes, not including this field, the chunk identifier or padding. // Chunk Payload: Chunk Size bytes // The data payload. If Chunk Size is odd, a single padding byte -- that MUST be 0 to conform with RIFF -- is added." chunks = chunks.slice(sizeof(ChunkHeader) + chunk.data.size()); if (chunk.data.size() % 2 != 0) { if (chunks.is_empty()) return context.error("Missing data for padding byte"); if (*chunks.data() != 0) return context.error("Padding byte is not 0"); chunks = chunks.slice(1); } return chunk; } // https://developers.google.com/speed/webp/docs/riff_container#simple_file_format_lossy // https://datatracker.ietf.org/doc/html/rfc6386#section-19 "Annex A: Bitstream Syntax" static ErrorOr decode_webp_simple_lossy(WebPLoadingContext& context, Chunk const& vp8_chunk) { VERIFY(vp8_chunk.type == FourCC("VP8 ")); if (vp8_chunk.data.size() < 10) return context.error("WebPImageDecoderPlugin: 'VP8 ' chunk too small"); // FIXME: Eventually, this should probably call into LibVideo/VP8, // and image decoders should move into LibImageDecoders which depends on both LibGfx and LibVideo. // (LibVideo depends on LibGfx, so LibGfx can't depend on LibVideo itself.) // https://datatracker.ietf.org/doc/html/rfc6386#section-4 "Overview of Compressed Data Format" // "The decoder is simply presented with a sequence of compressed frames [...] // The first frame presented to the decompressor is [...] a key frame. [...] // [E]very compressed frame has three or more pieces. It begins with an uncompressed data chunk comprising 10 bytes in the case of key frames u8 const* data = vp8_chunk.data.data(); // https://datatracker.ietf.org/doc/html/rfc6386#section-9.1 "Uncompressed Data Chunk" u32 frame_tag = data[0] | (data[1] << 8) | (data[2] << 16); bool is_key_frame = (frame_tag & 1) == 0; // https://www.rfc-editor.org/errata/eid5534 u8 version = (frame_tag & 0xe) >> 1; bool show_frame = (frame_tag & 0x10) != 0; u32 size_of_first_partition = frame_tag >> 5; if (!is_key_frame) return context.error("WebPImageDecoderPlugin: 'VP8 ' chunk not a key frame"); // FIXME: !show_frame does not make sense in a webp file either, probably? u32 start_code = data[3] | (data[4] << 8) | (data[5] << 16); if (start_code != 0x2a019d) // https://www.rfc-editor.org/errata/eid7370 return context.error("WebPImageDecoderPlugin: 'VP8 ' chunk invalid start_code"); // "The scaling specifications for each dimension are encoded as follows. // 0 | No upscaling (the most common case). // 1 | Upscale by 5/4. // 2 | Upscale by 5/3. // 3 | Upscale by 2." // This is a display-time operation and doesn't affect decoding. u16 width_and_horizontal_scale = data[6] | (data[7] << 8); u16 width = width_and_horizontal_scale & 0x3fff; u8 horizontal_scale = width_and_horizontal_scale >> 14; u16 heigth_and_vertical_scale = data[8] | (data[9] << 8); u16 height = heigth_and_vertical_scale & 0x3fff; u8 vertical_scale = heigth_and_vertical_scale >> 14; dbgln_if(WEBP_DEBUG, "version {}, show_frame {}, size_of_first_partition {}, width {}, horizontal_scale {}, height {}, vertical_scale {}", version, show_frame, size_of_first_partition, width, horizontal_scale, height, vertical_scale); 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 {}; } static ErrorOr decode_webp_chunk_VP8X(WebPLoadingContext& context, Chunk const& vp8x_chunk) { VERIFY(vp8x_chunk.type == FourCC("VP8X")); // The VP8X chunk is documented at "Extended WebP file header:" at the end of // https://developers.google.com/speed/webp/docs/riff_container#extended_file_format if (vp8x_chunk.data.size() < 10) return context.error("WebPImageDecoderPlugin: VP8X chunk too small"); u8 const* data = vp8x_chunk.data.data(); // 1 byte flags // "Reserved (Rsv): 2 bits MUST be 0. Readers MUST ignore this field. // ICC profile (I): 1 bit Set if the file contains an ICC profile. // Alpha (L): 1 bit Set if any of the frames of the image contain transparency information ("alpha"). // Exif metadata (E): 1 bit Set if the file contains Exif metadata. // XMP metadata (X): 1 bit Set if the file contains XMP metadata. // Animation (A): 1 bit Set if this is an animated image. Data in 'ANIM' and 'ANMF' chunks should be used to control the animation. // Reserved (R): 1 bit MUST be 0. Readers MUST ignore this field." u8 flags = data[0]; bool has_icc = flags & 0x20; bool has_alpha = flags & 0x10; bool has_exif = flags & 0x8; bool has_xmp = flags & 0x4; bool has_animation = flags & 0x2; // 3 byte reserved // 3 byte width minus one u32 width = (data[4] | (data[5] << 8) | (data[6] << 16)) + 1; // 3 byte height minus one u32 height = (data[7] | (data[8] << 8) | (data[9] << 16)) + 1; dbgln_if(WEBP_DEBUG, "flags 0x{:x} --{}{}{}{}{}{}, width {}, height {}", flags, has_icc ? " icc" : "", has_alpha ? " alpha" : "", has_exif ? " exif" : "", has_xmp ? " xmp" : "", has_animation ? " anim" : "", (flags & 0x3e) == 0 ? " none" : "", width, height); 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) { TRY(decode_webp_chunk_VP8X(context, 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 context.error("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)); // FIXME: "If this chunk is not present, sRGB SHOULD be assumed." 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 {}; } };