diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index 8b5333c605..ddbae4fd68 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -428,6 +428,26 @@ TEST_CASE(test_webp_lossy_4) EXPECT_EQ(frame.image->get_pixel(780, 570), Gfx::Color(0x72, 0xc8, 0xf6, 255)); } +TEST_CASE(test_webp_lossy_4_with_partitions) +{ + // Same input file as in the previous test, but re-encoded to use 8 secondary partitions. + auto file = MUST(Core::MappedFile::map(TEST_INPUT("4-with-8-partitions.webp"sv))); + EXPECT(Gfx::WebPImageDecoderPlugin::sniff(file->bytes())); + auto plugin_decoder = MUST(Gfx::WebPImageDecoderPlugin::create(file->bytes())); + MUST(plugin_decoder->initialize()); + + EXPECT_EQ(plugin_decoder->frame_count(), 1u); + EXPECT(!plugin_decoder->is_animated()); + EXPECT(!plugin_decoder->loop_count()); + + EXPECT_EQ(plugin_decoder->size(), Gfx::IntSize(1024, 772)); + + auto frame = MUST(plugin_decoder->frame(0)); + EXPECT_EQ(frame.image->size(), Gfx::IntSize(1024, 772)); + + EXPECT_EQ(frame.image->get_pixel(780, 570), Gfx::Color(0x73, 0xc9, 0xf9, 255)); +} + TEST_CASE(test_webp_extended_lossless) { auto file = MUST(Core::MappedFile::map(TEST_INPUT("extended-lossless.webp"sv))); diff --git a/Tests/LibGfx/test-inputs/4-with-8-partitions.webp b/Tests/LibGfx/test-inputs/4-with-8-partitions.webp new file mode 100644 index 0000000000..aea328a8ff Binary files /dev/null and b/Tests/LibGfx/test-inputs/4-with-8-partitions.webp differ diff --git a/Userland/Libraries/LibGfx/ImageFormats/WebPLoaderLossy.cpp b/Userland/Libraries/LibGfx/ImageFormats/WebPLoaderLossy.cpp index dded6bfff4..a64b347adc 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/WebPLoaderLossy.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/WebPLoaderLossy.cpp @@ -1142,11 +1142,16 @@ void convert_yuv_to_rgb(Bitmap& bitmap, int mb_x, int mb_y, ReadonlySpan y_ } } -ErrorOr decode_VP8_image_data(Gfx::Bitmap& bitmap, FrameHeader const& header, ReadonlyBytes data, int macroblock_width, int macroblock_height, Vector const& macroblock_metadata) +ErrorOr decode_VP8_image_data(Gfx::Bitmap& bitmap, FrameHeader const& header, Vector data_partitions, int macroblock_width, int macroblock_height, Vector const& macroblock_metadata) { - FixedMemoryStream memory_stream { data }; - BigEndianInputBitStream bit_stream { MaybeOwned(memory_stream) }; - auto decoder = TRY(BooleanDecoder::initialize(MaybeOwned { bit_stream }, data.size() * 8)); + + Vector streams; + for (auto data : data_partitions) { + auto memory_stream = make(data); + auto bit_stream = make(move(memory_stream)); + auto decoder = TRY(BooleanDecoder::initialize(move(bit_stream), data.size() * 8)); + TRY(streams.try_append(move(decoder))); + } CoefficientReadingContext coefficient_reading_context; TRY(coefficient_reading_context.initialize(macroblock_width)); @@ -1167,6 +1172,8 @@ ErrorOr decode_VP8_image_data(Gfx::Bitmap& bitmap, FrameHeader const& head predicted_v_above[i] = 127; for (int mb_y = 0, macroblock_index = 0; mb_y < macroblock_height; ++mb_y) { + BooleanDecoder& decoder = streams[mb_y % streams.size()]; + coefficient_reading_context.start_new_row(); i16 predicted_y_left[16] { 129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129 }; @@ -1223,6 +1230,40 @@ ErrorOr decode_VP8_image_data(Gfx::Bitmap& bitmap, FrameHeader const& head return {}; } +static ErrorOr> split_data_partitions(ReadonlyBytes second_partition, u8 number_of_dct_partitions) +{ + Vector data_partitions; + // https://datatracker.ietf.org/doc/html/rfc6386#section-9.5 "Token Partition and Partition Data Offsets" + // "If the number of data partitions is + // greater than 1, the size of each partition (except the last) is + // written in 3 bytes (24 bits). The size of the last partition is the + // remainder of the data not used by any of the previous partitions. + // The partitioned data are consecutive in the bitstream, so the size + // can also be used to calculate the offset of each partition." + // In practice, virtually all lossy webp files have a single data partition. + VERIFY(number_of_dct_partitions >= 1); + VERIFY(number_of_dct_partitions <= 8); + + size_t sizes_size = (number_of_dct_partitions - 1) * 3; + if (second_partition.size() < sizes_size) + return Error::from_string_literal("WebPImageDecoderPlugin: not enough data for partition sizes"); + + ReadonlyBytes sizes = second_partition.slice(0, sizes_size); + ReadonlyBytes data = second_partition.slice(sizes_size); + + for (int i = 0; i < number_of_dct_partitions - 1; ++i) { + u32 partition_size = sizes[0] | (sizes[1] << 8) | (sizes[2] << 16); + dbgln_if(WEBP_DEBUG, "partition_size {}", partition_size); + sizes = sizes.slice(3); + if (partition_size > data.size()) + return Error::from_string_literal("WebPImageDecoderPlugin: not enough data for partition data"); + TRY(data_partitions.try_append(data.slice(0, partition_size))); + data = data.slice(partition_size); + } + TRY(data_partitions.try_append(data)); + return data_partitions; +} + } ErrorOr> decode_webp_chunk_VP8_contents(VP8Header const& vp8_header, bool include_alpha_channel) @@ -1245,13 +1286,11 @@ ErrorOr> decode_webp_chunk_VP8_contents(VP8Header const& v // Done with the first partition! - 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; auto bitmap = TRY(Bitmap::create(bitmap_format, { macroblock_width * 16, macroblock_height * 16 })); - TRY(decode_VP8_image_data(*bitmap, header, vp8_header.second_partition, macroblock_width, macroblock_height, macroblock_metadata)); + auto data_partitions = TRY(split_data_partitions(vp8_header.second_partition, header.number_of_dct_partitions)); + TRY(decode_VP8_image_data(*bitmap, header, move(data_partitions), macroblock_width, macroblock_height, macroblock_metadata)); auto width = static_cast(vp8_header.width); auto height = static_cast(vp8_header.height);