mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-13 11:39:43 +00:00
LibGfx: Support APNG more completely
This fixes 3/4 of the remaining test failures in wpt/png/apng. Also, about half of the APNG files on https://commons.wikimedia.org/wiki/Category:Animated_PNG_files used to be broken, and they all seem to work properly now :^) I also refactored the code to be (at least to me) simpler and more similar to what the spec describes. As a nice side effect, there are now fewer lines of code than before. Lastly, I replaced DeprecatedPainter with Painter.
This commit is contained in:
parent
c31d44ee18
commit
6d9dab5a29
Notes:
github-actions[bot]
2024-11-05 09:38:52 +00:00
Author: https://github.com/justus2510
Commit: 6d9dab5a29
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2155
1 changed files with 54 additions and 85 deletions
|
@ -6,53 +6,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <AK/Vector.h>
|
#include <AK/Vector.h>
|
||||||
#include <LibGfx/DeprecatedPainter.h>
|
|
||||||
#include <LibGfx/ImageFormats/ExifOrientedBitmap.h>
|
#include <LibGfx/ImageFormats/ExifOrientedBitmap.h>
|
||||||
#include <LibGfx/ImageFormats/PNGLoader.h>
|
#include <LibGfx/ImageFormats/PNGLoader.h>
|
||||||
#include <LibGfx/ImageFormats/TIFFLoader.h>
|
#include <LibGfx/ImageFormats/TIFFLoader.h>
|
||||||
#include <LibGfx/ImageFormats/TIFFMetadata.h>
|
#include <LibGfx/ImageFormats/TIFFMetadata.h>
|
||||||
|
#include <LibGfx/Painter.h>
|
||||||
#include <png.h>
|
#include <png.h>
|
||||||
|
|
||||||
namespace Gfx {
|
namespace Gfx {
|
||||||
|
|
||||||
struct AnimationFrame {
|
|
||||||
RefPtr<Bitmap> bitmap;
|
|
||||||
int x_offset { 0 };
|
|
||||||
int y_offset { 0 };
|
|
||||||
int width { 0 };
|
|
||||||
int height { 0 };
|
|
||||||
int delay_den { 0 };
|
|
||||||
int delay_num { 0 };
|
|
||||||
u8 blend_op { 0 };
|
|
||||||
u8 dispose_op { 0 };
|
|
||||||
|
|
||||||
AnimationFrame(RefPtr<Bitmap> bitmap, int x_offset, int y_offset, int width, int height, int delay_den, int delay_num, u8 blend_op, u8 dispose_op)
|
|
||||||
: bitmap(move(bitmap))
|
|
||||||
, x_offset(x_offset)
|
|
||||||
, y_offset(y_offset)
|
|
||||||
, width(width)
|
|
||||||
, height(height)
|
|
||||||
, delay_den(delay_den)
|
|
||||||
, delay_num(delay_num)
|
|
||||||
, blend_op(blend_op)
|
|
||||||
, dispose_op(dispose_op)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] int duration_ms() const
|
|
||||||
{
|
|
||||||
if (delay_num == 0)
|
|
||||||
return 1;
|
|
||||||
u32 const denominator = delay_den != 0 ? static_cast<u32>(delay_den) : 100u;
|
|
||||||
auto unsigned_duration_ms = (delay_num * 1000) / denominator;
|
|
||||||
if (unsigned_duration_ms > INT_MAX)
|
|
||||||
return INT_MAX;
|
|
||||||
return static_cast<int>(unsigned_duration_ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] IntRect rect() const { return { x_offset, y_offset, width, height }; }
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PNGLoadingContext {
|
struct PNGLoadingContext {
|
||||||
ReadonlyBytes data;
|
ReadonlyBytes data;
|
||||||
IntSize size;
|
IntSize size;
|
||||||
|
@ -62,11 +24,6 @@ struct PNGLoadingContext {
|
||||||
Optional<ByteBuffer> icc_profile;
|
Optional<ByteBuffer> icc_profile;
|
||||||
OwnPtr<ExifMetadata> exif_metadata;
|
OwnPtr<ExifMetadata> exif_metadata;
|
||||||
|
|
||||||
Vector<AnimationFrame> animation_frames;
|
|
||||||
Vector<u8*> row_pointers;
|
|
||||||
Vector<u8> image_data;
|
|
||||||
RefPtr<Gfx::Bitmap> decoded_frame_bitmap;
|
|
||||||
|
|
||||||
ErrorOr<size_t> read_frames(png_structp, png_infop);
|
ErrorOr<size_t> read_frames(png_structp, png_infop);
|
||||||
ErrorOr<void> apply_exif_orientation();
|
ErrorOr<void> apply_exif_orientation();
|
||||||
};
|
};
|
||||||
|
@ -226,35 +183,6 @@ ErrorOr<void> PNGLoadingContext::apply_exif_orientation()
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
static ErrorOr<NonnullRefPtr<Bitmap>> render_animation_frame(AnimationFrame const& prev_animation_frame, AnimationFrame const& animation_frame, Bitmap const& decoded_frame_bitmap)
|
|
||||||
{
|
|
||||||
auto rendered_bitmap = TRY(prev_animation_frame.bitmap->clone());
|
|
||||||
DeprecatedPainter painter(rendered_bitmap);
|
|
||||||
|
|
||||||
auto frame_rect = animation_frame.rect();
|
|
||||||
switch (prev_animation_frame.dispose_op) {
|
|
||||||
case PNG_DISPOSE_OP_BACKGROUND:
|
|
||||||
painter.clear_rect(rendered_bitmap->rect(), Color::NamedColor::Transparent);
|
|
||||||
break;
|
|
||||||
case PNG_DISPOSE_OP_PREVIOUS:
|
|
||||||
painter.blit(frame_rect.location(), decoded_frame_bitmap, frame_rect, 1.0f, false);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (animation_frame.blend_op) {
|
|
||||||
case PNG_BLEND_OP_SOURCE:
|
|
||||||
painter.blit(frame_rect.location(), decoded_frame_bitmap, decoded_frame_bitmap.rect(), 1.0f, false);
|
|
||||||
break;
|
|
||||||
case PNG_BLEND_OP_OVER:
|
|
||||||
painter.blit(frame_rect.location(), decoded_frame_bitmap, decoded_frame_bitmap.rect(), 1.0f, true);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return rendered_bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop info_ptr)
|
ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop info_ptr)
|
||||||
{
|
{
|
||||||
if (png_get_acTL(png_ptr, info_ptr, &frame_count, &loop_count)) {
|
if (png_get_acTL(png_ptr, info_ptr, &frame_count, &loop_count)) {
|
||||||
|
@ -262,6 +190,11 @@ ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop in
|
||||||
|
|
||||||
png_set_acTL(png_ptr, info_ptr, frame_count, loop_count);
|
png_set_acTL(png_ptr, info_ptr, frame_count, loop_count);
|
||||||
|
|
||||||
|
// Conceptually, at the beginning of each play the output buffer must be completely initialized to a fully transparent black rectangle, with width and height dimensions from the `IHDR` chunk.
|
||||||
|
auto output_buffer = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, size));
|
||||||
|
auto painter = Painter::create(output_buffer);
|
||||||
|
Vector<u8*> row_pointers;
|
||||||
|
|
||||||
for (size_t frame_index = 0; frame_index < frame_count; ++frame_index) {
|
for (size_t frame_index = 0; frame_index < frame_count; ++frame_index) {
|
||||||
png_read_frame_head(png_ptr, info_ptr);
|
png_read_frame_head(png_ptr, info_ptr);
|
||||||
u32 width = 0;
|
u32 width = 0;
|
||||||
|
@ -273,32 +206,67 @@ ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop in
|
||||||
u8 dispose_op = PNG_DISPOSE_OP_NONE;
|
u8 dispose_op = PNG_DISPOSE_OP_NONE;
|
||||||
u8 blend_op = PNG_BLEND_OP_SOURCE;
|
u8 blend_op = PNG_BLEND_OP_SOURCE;
|
||||||
|
|
||||||
|
auto duration_ms = [&]() -> int {
|
||||||
|
if (delay_num == 0)
|
||||||
|
return 1;
|
||||||
|
u32 const denominator = delay_den != 0 ? static_cast<u32>(delay_den) : 100u;
|
||||||
|
auto unsigned_duration_ms = (delay_num * 1000) / denominator;
|
||||||
|
if (unsigned_duration_ms > INT_MAX)
|
||||||
|
return INT_MAX;
|
||||||
|
return static_cast<int>(unsigned_duration_ms);
|
||||||
|
};
|
||||||
|
|
||||||
if (png_get_valid(png_ptr, info_ptr, PNG_INFO_fcTL)) {
|
if (png_get_valid(png_ptr, info_ptr, PNG_INFO_fcTL)) {
|
||||||
png_get_next_frame_fcTL(png_ptr, info_ptr, &width, &height, &x, &y, &delay_num, &delay_den, &dispose_op, &blend_op);
|
png_get_next_frame_fcTL(png_ptr, info_ptr, &width, &height, &x, &y, &delay_num, &delay_den, &dispose_op, &blend_op);
|
||||||
} else {
|
} else {
|
||||||
width = png_get_image_width(png_ptr, info_ptr);
|
width = png_get_image_width(png_ptr, info_ptr);
|
||||||
height = png_get_image_height(png_ptr, info_ptr);
|
height = png_get_image_height(png_ptr, info_ptr);
|
||||||
}
|
}
|
||||||
|
auto frame_rect = FloatRect { x, y, width, height };
|
||||||
|
|
||||||
decoded_frame_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, IntSize { static_cast<int>(width), static_cast<int>(height) }));
|
auto decoded_frame_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, IntSize { static_cast<int>(width), static_cast<int>(height) }));
|
||||||
|
|
||||||
row_pointers.resize(height);
|
row_pointers.resize(height);
|
||||||
for (u32 i = 0; i < height; ++i) {
|
for (u32 i = 0; i < height; ++i) {
|
||||||
row_pointers[i] = decoded_frame_bitmap->scanline_u8(i);
|
row_pointers[i] = decoded_frame_bitmap->scanline_u8(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
png_read_image(png_ptr, row_pointers.data());
|
png_read_image(png_ptr, row_pointers.data());
|
||||||
|
|
||||||
auto animation_frame = AnimationFrame(nullptr, x, y, width, height, delay_den, delay_num, blend_op, dispose_op);
|
RefPtr<Bitmap> prev_output_buffer;
|
||||||
|
if (dispose_op == PNG_DISPOSE_OP_PREVIOUS) // Only actually clone if it's necessary
|
||||||
|
prev_output_buffer = TRY(output_buffer->clone());
|
||||||
|
|
||||||
if (frame_index == 0) {
|
switch (blend_op) {
|
||||||
animation_frame.bitmap = decoded_frame_bitmap;
|
case PNG_BLEND_OP_SOURCE:
|
||||||
frame_descriptors.append({ decoded_frame_bitmap, animation_frame.duration_ms() });
|
// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
|
||||||
} else {
|
painter->clear_rect(frame_rect, Gfx::Color::Transparent);
|
||||||
animation_frame.bitmap = TRY(render_animation_frame(animation_frames.last(), animation_frame, *decoded_frame_bitmap));
|
painter->draw_bitmap(frame_rect, *decoded_frame_bitmap, decoded_frame_bitmap->rect(), Gfx::ScalingMode::NearestNeighbor, 1.0f);
|
||||||
frame_descriptors.append({ animation_frame.bitmap, animation_frame.duration_ms() });
|
break;
|
||||||
|
case PNG_BLEND_OP_OVER:
|
||||||
|
// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification.
|
||||||
|
painter->draw_bitmap(frame_rect, *decoded_frame_bitmap, decoded_frame_bitmap->rect(), ScalingMode::NearestNeighbor, 1.0f);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
VERIFY_NOT_REACHED();
|
||||||
|
}
|
||||||
|
|
||||||
|
frame_descriptors.append({ TRY(output_buffer->clone()), duration_ms() });
|
||||||
|
|
||||||
|
switch (dispose_op) {
|
||||||
|
case PNG_DISPOSE_OP_NONE:
|
||||||
|
// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
|
||||||
|
break;
|
||||||
|
case PNG_DISPOSE_OP_BACKGROUND:
|
||||||
|
// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
|
||||||
|
painter->clear_rect(frame_rect, Gfx::Color::Transparent);
|
||||||
|
break;
|
||||||
|
case PNG_DISPOSE_OP_PREVIOUS:
|
||||||
|
// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
|
||||||
|
painter->clear_rect(frame_rect, Gfx::Color::Transparent);
|
||||||
|
painter->draw_bitmap(frame_rect, *prev_output_buffer, IntRect { x, y, width, height }, Gfx::ScalingMode::NearestNeighbor, 1.0f);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
VERIFY_NOT_REACHED();
|
||||||
}
|
}
|
||||||
animation_frames.append(move(animation_frame));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This is a single-frame PNG.
|
// This is a single-frame PNG.
|
||||||
|
@ -306,7 +274,8 @@ ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop in
|
||||||
frame_count = 1;
|
frame_count = 1;
|
||||||
loop_count = 0;
|
loop_count = 0;
|
||||||
|
|
||||||
decoded_frame_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, size));
|
auto decoded_frame_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, size));
|
||||||
|
Vector<u8*> row_pointers;
|
||||||
row_pointers.resize(size.height());
|
row_pointers.resize(size.height());
|
||||||
for (int i = 0; i < size.height(); ++i)
|
for (int i = 0; i < size.height(); ++i)
|
||||||
row_pointers[i] = decoded_frame_bitmap->scanline_u8(i);
|
row_pointers[i] = decoded_frame_bitmap->scanline_u8(i);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue