/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 | | } |