Coverage Report

Created: 2025-03-04 07:22

/src/serenity/Userland/Libraries/LibGfx/ImageFormats/AnimationWriter.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2024, Nico Weber <thakis@chromium.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <LibGfx/Bitmap.h>
8
#include <LibGfx/ImageFormats/AnimationWriter.h>
9
#include <LibGfx/Rect.h>
10
11
namespace Gfx {
12
13
0
AnimationWriter::~AnimationWriter() = default;
14
15
static bool are_scanlines_equal(Bitmap const& a, Bitmap const& b, int y)
16
0
{
17
0
    for (int x = 0; x < a.width(); ++x) {
18
0
        if (a.get_pixel(x, y) != b.get_pixel(x, y))
19
0
            return false;
20
0
    }
21
0
    return true;
22
0
}
23
24
static bool are_columns_equal(Bitmap const& a, Bitmap const& b, int x, int y1, int y2)
25
0
{
26
0
    for (int y = y1; y < y2; ++y) {
27
0
        if (a.get_pixel(x, y) != b.get_pixel(x, y))
28
0
            return false;
29
0
    }
30
0
    return true;
31
0
}
32
33
static Gfx::IntRect rect_where_pixels_are_different(Bitmap const& a, Bitmap const& b)
34
0
{
35
0
    VERIFY(a.size() == b.size());
36
37
    // FIXME: This works on physical pixels.
38
0
    VERIFY(a.scale() == 1);
39
0
    VERIFY(b.scale() == 1);
40
41
0
    int number_of_equal_pixels_at_top = 0;
42
0
    while (number_of_equal_pixels_at_top < a.height() && are_scanlines_equal(a, b, number_of_equal_pixels_at_top))
43
0
        ++number_of_equal_pixels_at_top;
44
45
0
    int number_of_equal_pixels_at_bottom = 0;
46
0
    while (number_of_equal_pixels_at_bottom < a.height() - number_of_equal_pixels_at_top && are_scanlines_equal(a, b, a.height() - number_of_equal_pixels_at_bottom - 1))
47
0
        ++number_of_equal_pixels_at_bottom;
48
49
0
    int const y1 = number_of_equal_pixels_at_top;
50
0
    int const y2 = a.height() - number_of_equal_pixels_at_bottom;
51
52
0
    int number_of_equal_pixels_at_left = 0;
53
0
    while (number_of_equal_pixels_at_left < a.width() && are_columns_equal(a, b, number_of_equal_pixels_at_left, y1, y2))
54
0
        ++number_of_equal_pixels_at_left;
55
56
0
    int number_of_equal_pixels_at_right = 0;
57
0
    while (number_of_equal_pixels_at_right < a.width() - number_of_equal_pixels_at_left && are_columns_equal(a, b, a.width() - number_of_equal_pixels_at_right - 1, y1, y2))
58
0
        ++number_of_equal_pixels_at_right;
59
60
    // WebP can only encode even-sized animation frame positions.
61
    // FIXME: Change API shape in some way so that the AnimationWriter base class doesn't have to know about this detail of a subclass.
62
0
    if (number_of_equal_pixels_at_left % 2 != 0)
63
0
        --number_of_equal_pixels_at_left;
64
0
    if (number_of_equal_pixels_at_top % 2 != 0)
65
0
        --number_of_equal_pixels_at_top;
66
67
0
    Gfx::IntRect rect;
68
0
    rect.set_x(number_of_equal_pixels_at_left);
69
0
    rect.set_y(number_of_equal_pixels_at_top);
70
0
    rect.set_width(a.width() - number_of_equal_pixels_at_left - number_of_equal_pixels_at_right);
71
0
    rect.set_height(a.height() - number_of_equal_pixels_at_top - number_of_equal_pixels_at_bottom);
72
73
0
    return rect;
74
0
}
75
76
bool AnimationWriter::can_zero_out_unchanging_pixels(Bitmap& new_frame, Gfx::IntRect new_frame_rect, Bitmap& last_frame, AllowInterFrameCompression allow_inter_frame_compression) const
77
0
{
78
0
    if (!can_blend_frames() || allow_inter_frame_compression == AllowInterFrameCompression::No)
79
0
        return false;
80
81
0
    VERIFY(new_frame.width() == new_frame_rect.width());
82
0
    VERIFY(new_frame.height() == new_frame_rect.height());
83
0
    for (int y = 0; y < new_frame.height(); ++y) {
84
0
        for (int x = 0; x < new_frame.width(); ++x) {
85
0
            if (new_frame.get_pixel(x, y).alpha() != 255 && new_frame.get_pixel(x, y) != last_frame.get_pixel(x + new_frame_rect.x(), y + new_frame_rect.y()))
86
0
                return false;
87
0
        }
88
0
    }
89
0
    return true;
90
0
}
91
92
ErrorOr<void> AnimationWriter::add_frame_relative_to_last_frame(Bitmap& frame, int duration_ms, RefPtr<Bitmap> last_frame, AllowInterFrameCompression allow_inter_frame_compression)
93
0
{
94
0
    if (!last_frame)
95
0
        return add_frame(frame, duration_ms);
96
97
0
    auto rect = rect_where_pixels_are_different(*last_frame, frame);
98
99
0
    if (rect.is_empty()) {
100
        // The frame is identical to the last frame. Don't store an empty bitmap.
101
        // FIXME: We could delay writing the last frame until we know that the next frame is different,
102
        //        and just keep increasing that frame's duration instead.
103
0
        rect = { 0, 0, 1, 1 };
104
0
    }
105
106
    // FIXME: It would be nice to have a way to crop a bitmap without copying the data.
107
0
    auto differences = TRY(frame.cropped(rect));
108
109
0
    BlendMode blend_mode = BlendMode::Replace;
110
111
    // If all frames of the animation have no alpha, set color values of pixels that are in the changed rect that are
112
    // equal to the last frame to transparent black and set the frame to be blended. This is almost smaller after compression.
113
0
    if (can_zero_out_unchanging_pixels(*differences, rect, *last_frame, allow_inter_frame_compression)) {
114
0
        for (int y = 0; y < differences->height(); ++y) {
115
0
            for (int x = 0; x < differences->width(); ++x) {
116
0
                if (differences->get_pixel(x, y) == last_frame->get_pixel(x + rect.x(), y + rect.y()) || differences->get_pixel(x, y).alpha() == 0)
117
0
                    differences->set_pixel(x, y, Color(0, 0, 0, 0));
118
0
            }
119
0
        }
120
0
        blend_mode = BlendMode::Blend;
121
0
    }
122
123
    // This assumes a replacement disposal method.
124
0
    return add_frame(differences, duration_ms, rect.location(), blend_mode);
125
0
}
126
127
}