diff --git a/Userland/Libraries/LibGfx/ImageFormats/WebPLoaderLossy.cpp b/Userland/Libraries/LibGfx/ImageFormats/WebPLoaderLossy.cpp index dbb42960d1..e17511dab0 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/WebPLoaderLossy.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/WebPLoaderLossy.cpp @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include // Lossy format: https://datatracker.ietf.org/doc/html/rfc6386 @@ -99,8 +101,306 @@ ErrorOr decode_webp_chunk_VP8_header(ReadonlyBytes vp8_data) return VP8Header { version, show_frame, size_of_first_partition, width, horizontal_scale, height, vertical_scale, vp8_data.slice(10) }; } +namespace { + +// Reads n bits followed by a sign bit (0: positive, 1: negative). +ErrorOr read_signed_literal(BooleanDecoder& decoder, u8 n) +{ + VERIFY(n <= 7); + i8 i = TRY(decoder.read_literal(n)); + if (TRY(decoder.read_literal(1))) + i = -i; + return i; +} + +// https://datatracker.ietf.org/doc/html/rfc6386#section-19 "Annex A: Bitstream Syntax" +#define L(n) decoder.read_literal(n) +#define B(prob) decoder.read_bool(prob) +#define L_signed(n) read_signed_literal(decoder, n) + +// https://datatracker.ietf.org/doc/html/rfc6386#section-9.2 "Color Space and Pixel Type (Key Frames Only)" +enum class ColorSpaceAndPixelType { + YUV = 0, + ReservedForFutureUse = 1, +}; +enum class ClampingSpecification { + DecoderMustClampTo0To255 = 0, + NoClampingNecessary = 1, +}; + +// https://datatracker.ietf.org/doc/html/rfc6386#section-9.3 Segment-Based Adjustments" +// https://datatracker.ietf.org/doc/html/rfc6386#section-19.2 "Frame Header" +enum class SegmentFeatureMode { + // Spec 19.2 says 0 is delta, 1 absolute; spec 9.3 has it the other way round. 19.2 is correct. + // https://www.rfc-editor.org/errata/eid7519 + DeltaValueMode = 0, + AbsoluteValueMode = 1, + +}; +struct Segmentation { + bool update_metablock_segmentation_map { false }; + SegmentFeatureMode segment_feature_mode { SegmentFeatureMode::DeltaValueMode }; + + i8 quantizer_update_value[4] {}; + i8 loop_filter_update_value[4] {}; + + u8 metablock_segment_tree_probabilities[3] = { 255, 255, 255 }; +}; +ErrorOr decode_VP8_frame_header_segmentation(BooleanDecoder&); + +// Also https://datatracker.ietf.org/doc/html/rfc6386#section-9.6 "Dequantization Indices" +struct QuantizationIndices { + u8 y_ac { 0 }; + i8 y_dc_delta { 0 }; + + i8 y2_dc_delta { 0 }; + i8 y2_ac_delta { 0 }; + + i8 uv_dc_delta { 0 }; + i8 uv_ac_delta { 0 }; +}; +ErrorOr decode_VP8_frame_header_quantization_indices(BooleanDecoder&); + +struct LoopFilterAdjustment { + bool enable_loop_filter_adjustment { false }; + i8 ref_frame_delta[4] {}; + i8 mb_mode_delta[4] {}; +}; +ErrorOr decode_VP8_frame_header_loop_filter_adjustment(BooleanDecoder&); + +using CoefficientProbabilities = Prob[4][8][3][num_dct_tokens - 1]; +ErrorOr decode_VP8_frame_header_coefficient_probabilities(BooleanDecoder&, CoefficientProbabilities); + +// https://datatracker.ietf.org/doc/html/rfc6386#section-15 "Loop Filter" +// "The first is a flag (filter_type) selecting the type of filter (normal or simple)" +enum class FilterType { + Normal = 0, + Simple = 1, +}; + +// https://datatracker.ietf.org/doc/html/rfc6386#section-19.2 "Frame Header" +struct FrameHeader { + ColorSpaceAndPixelType color_space {}; + ClampingSpecification clamping_type {}; + + bool is_segmentation_enabled {}; + Segmentation segmentation {}; + + FilterType filter_type {}; + u8 loop_filter_level {}; + u8 sharpness_level {}; + LoopFilterAdjustment loop_filter_adjustment {}; + + u8 number_of_dct_partitions {}; + + QuantizationIndices quantization_indices {}; + + CoefficientProbabilities coefficient_probabilities; + + bool enable_skipping_of_metablocks_containing_only_zero_coefficients {}; + u8 probability_skip_false; +}; + +ErrorOr decode_VP8_frame_header(BooleanDecoder& decoder) +{ + // https://datatracker.ietf.org/doc/html/rfc6386#section-19.2 "Frame Header" + FrameHeader header; + + // In the VP8 spec, this is in an `if (key_frames)`, but webp files only have key frames. + header.color_space = ColorSpaceAndPixelType { TRY(L(1)) }; + header.clamping_type = ClampingSpecification { TRY(L(1)) }; + dbgln_if(WEBP_DEBUG, "color_space {} clamping_type {}", (int)header.color_space, (int)header.clamping_type); + + // https://datatracker.ietf.org/doc/html/rfc6386#section-9.3 "Segment-Based Adjustments" + header.is_segmentation_enabled = TRY(L(1)); + dbgln_if(WEBP_DEBUG, "segmentation_enabled {}", header.is_segmentation_enabled); + + if (header.is_segmentation_enabled) + header.segmentation = TRY(decode_VP8_frame_header_segmentation(decoder)); + + header.filter_type = FilterType { TRY(L(1)) }; + header.loop_filter_level = TRY(L(6)); + header.sharpness_level = TRY(L(3)); + dbgln_if(WEBP_DEBUG, "filter_type {} loop_filter_level {} sharpness_level {}", (int)header.filter_type, header.loop_filter_level, header.sharpness_level); + + header.loop_filter_adjustment = TRY(decode_VP8_frame_header_loop_filter_adjustment(decoder)); + + u8 log2_nbr_of_dct_partitions = TRY(L(2)); + dbgln_if(WEBP_DEBUG, "log2_nbr_of_dct_partitions {}", log2_nbr_of_dct_partitions); + header.number_of_dct_partitions = 1 << log2_nbr_of_dct_partitions; + + header.quantization_indices = TRY(decode_VP8_frame_header_quantization_indices(decoder)); + + // In the VP8 spec, this is in an `if (key_frames)` followed by a lengthy `else`, but webp files only have key frames. + u8 refresh_entropy_probs = TRY(L(1)); // Has no effect in webp files. + dbgln_if(WEBP_DEBUG, "refresh_entropy_probs {}", refresh_entropy_probs); + + memcpy(header.coefficient_probabilities, default_coeff_probs, sizeof(header.coefficient_probabilities)); + TRY(decode_VP8_frame_header_coefficient_probabilities(decoder, header.coefficient_probabilities)); + + // https://datatracker.ietf.org/doc/html/rfc6386#section-9.11 "Remaining Frame Header Data (Key Frame)" + header.enable_skipping_of_metablocks_containing_only_zero_coefficients = TRY(L(1)); + dbgln_if(WEBP_DEBUG, "mb_no_skip_coeff {}", header.enable_skipping_of_metablocks_containing_only_zero_coefficients); + if (header.enable_skipping_of_metablocks_containing_only_zero_coefficients) { + header.probability_skip_false = TRY(L(8)); + dbgln_if(WEBP_DEBUG, "prob_skip_false {}", header.probability_skip_false); + } + + // In the VP8 spec, there is a length `if (!key_frames)` here, but webp files only have key frames. + + return header; +} + +ErrorOr decode_VP8_frame_header_segmentation(BooleanDecoder& decoder) +{ + // Corresponds to "update_segmentation()" in section 19.2 of the spec. + Segmentation segmentation; + + segmentation.update_metablock_segmentation_map = TRY(L(1)); + u8 update_segment_feature_data = TRY(L(1)); + + dbgln_if(WEBP_DEBUG, "update_mb_segmentation_map {} update_segment_feature_data {}", + segmentation.update_metablock_segmentation_map, update_segment_feature_data); + + if (update_segment_feature_data) { + segmentation.segment_feature_mode = static_cast(TRY(L(1))); + dbgln_if(WEBP_DEBUG, "segment_feature_mode {}", (int)segmentation.segment_feature_mode); + + for (int i = 0; i < 4; ++i) { + u8 quantizer_update = TRY(L(1)); + dbgln_if(WEBP_DEBUG, "quantizer_update {}", quantizer_update); + if (quantizer_update) { + i8 quantizer_update_value = TRY(L_signed(7)); + dbgln_if(WEBP_DEBUG, "quantizer_update_value {}", quantizer_update_value); + segmentation.quantizer_update_value[i] = quantizer_update_value; + } + } + for (int i = 0; i < 4; ++i) { + u8 loop_filter_update = TRY(L(1)); + dbgln_if(WEBP_DEBUG, "loop_filter_update {}", loop_filter_update); + if (loop_filter_update) { + i8 loop_filter_update_value = TRY(L_signed(6)); + dbgln_if(WEBP_DEBUG, "loop_filter_update_value {}", loop_filter_update_value); + segmentation.loop_filter_update_value[i] = loop_filter_update_value; + } + } + } + + if (segmentation.update_metablock_segmentation_map) { + // This reads mb_segment_tree_probs for https://datatracker.ietf.org/doc/html/rfc6386#section-10. + for (int i = 0; i < 3; ++i) { + u8 segment_prob_update = TRY(L(1)); + dbgln_if(WEBP_DEBUG, "segment_prob_update {}", segment_prob_update); + if (segment_prob_update) { + u8 segment_prob = TRY(L(8)); + dbgln_if(WEBP_DEBUG, "segment_prob {}", segment_prob); + segmentation.metablock_segment_tree_probabilities[i] = segment_prob; + } + } + } + + return segmentation; +} + +ErrorOr decode_VP8_frame_header_quantization_indices(BooleanDecoder& decoder) +{ + // Corresponds to "quant_indices()" in section 19.2 of the spec. + QuantizationIndices quantization_indices; + + // "The first 7-bit index gives the dequantization table index for + // Y-plane AC coefficients, called yac_qi. It is always coded and acts + // as a baseline for the other 5 quantization indices, each of which is + // represented by a delta from this baseline index." + quantization_indices.y_ac = TRY(L(7)); + dbgln_if(WEBP_DEBUG, "y_ac_qi {}", quantization_indices.y_ac); + + auto read_delta = [&decoder](StringView name, i8* destination) -> ErrorOr { + u8 is_present = TRY(L(1)); + dbgln_if(WEBP_DEBUG, "{}_present {}", name, is_present); + if (is_present) { + i8 delta = TRY(L_signed(4)); + dbgln_if(WEBP_DEBUG, "{} {}", name, delta); + *destination = delta; + } + return {}; + }; + TRY(read_delta("y_dc_delta"sv, &quantization_indices.y_dc_delta)); + TRY(read_delta("y2_dc_delta"sv, &quantization_indices.y2_dc_delta)); + TRY(read_delta("y2_ac_delta"sv, &quantization_indices.y2_ac_delta)); + TRY(read_delta("uv_dc_delta"sv, &quantization_indices.uv_dc_delta)); + TRY(read_delta("uv_ac_delta"sv, &quantization_indices.uv_ac_delta)); + + return quantization_indices; +} + +ErrorOr decode_VP8_frame_header_loop_filter_adjustment(BooleanDecoder& decoder) +{ + // Corresponds to "mb_lf_adjustments()" in section 19.2 of the spec. + LoopFilterAdjustment adjustment; + + adjustment.enable_loop_filter_adjustment = TRY(L(1)); + if (adjustment.enable_loop_filter_adjustment) { + u8 mode_ref_lf_delta_update = TRY(L(1)); + dbgln_if(WEBP_DEBUG, "mode_ref_lf_delta_update {}", mode_ref_lf_delta_update); + if (mode_ref_lf_delta_update) { + for (int i = 0; i < 4; ++i) { + u8 ref_frame_delta_update_flag = TRY(L(1)); + dbgln_if(WEBP_DEBUG, "ref_frame_delta_update_flag {}", ref_frame_delta_update_flag); + if (ref_frame_delta_update_flag) { + i8 delta = TRY(L_signed(6)); + dbgln_if(WEBP_DEBUG, "delta {}", delta); + adjustment.ref_frame_delta[i] = delta; + } + } + for (int i = 0; i < 4; ++i) { + u8 mb_mode_delta_update_flag = TRY(L(1)); + dbgln_if(WEBP_DEBUG, "mb_mode_delta_update_flag {}", mb_mode_delta_update_flag); + if (mb_mode_delta_update_flag) { + i8 delta = TRY(L_signed(6)); + dbgln_if(WEBP_DEBUG, "delta {}", delta); + adjustment.mb_mode_delta[i] = delta; + } + } + } + } + + return adjustment; +} + +ErrorOr decode_VP8_frame_header_coefficient_probabilities(BooleanDecoder& decoder, CoefficientProbabilities coefficient_probabilities) +{ + // Corresponds to "token_prob_update()" in section 19.2 of the spec. + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 8; j++) { + for (int k = 0; k < 3; k++) { + for (int l = 0; l < 11; l++) { + // token_prob_update() says L(1) and L(8), but it's actually B(p) and L(8). + // https://datatracker.ietf.org/doc/html/rfc6386#section-13.4 "Token Probability Updates" describes it correctly. + if (TRY(B(coeff_update_probs[i][j][k][l]))) + coefficient_probabilities[i][j][k][l] = TRY(L(8)); + } + } + } + } + + return {}; +} + +} + ErrorOr> decode_webp_chunk_VP8_contents(VP8Header const& vp8_header, bool include_alpha_channel) { + // The first partition stores header, per-segment state, and macroblock metadata. + + FixedMemoryStream memory_stream { vp8_header.lossy_data }; + BigEndianInputBitStream bit_stream { MaybeOwned(memory_stream) }; + auto decoder = TRY(BooleanDecoder::initialize(MaybeOwned { bit_stream }, vp8_header.lossy_data.size() * 8)); + + auto header = TRY(decode_VP8_frame_header(decoder)); + + if (header.number_of_dct_partitions > 1) + return Error::from_string_literal("WebPImageDecoderPlugin: decoding lossy webps with more than one dct partition not yet implemented"); + auto bitmap_format = include_alpha_channel ? BitmapFormat::BGRA8888 : BitmapFormat::BGRx8888; // Uncomment this to test ALPH decoding for WebP-lossy-with-alpha images while lossy decoding isn't implemented yet. @@ -108,7 +408,6 @@ ErrorOr> decode_webp_chunk_VP8_contents(VP8Header const& v return Bitmap::create(bitmap_format, { vp8_header.width, vp8_header.height }); #else // FIXME: Implement webp lossy decoding. - (void)vp8_header; (void)bitmap_format; return Error::from_string_literal("WebPImageDecoderPlugin: decoding lossy webps not yet implemented"); #endif