Skip to content

Commit 88c4814

Browse files
InvalidUsernameExceptiongmta
authored andcommitted
LibGfx+LibWeb: Extract bitmap-to-buffer conversion into LibGfx
This factors the conversion logic to be independent from WebGL code, allowing us to write unit tests for it that can run in CI (since WebGL can't run in CI).
1 parent fa181c2 commit 88c4814

File tree

5 files changed

+154
-84
lines changed

5 files changed

+154
-84
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) 2025, Ladybird contributors
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#pragma once
8+
9+
#include <AK/ByteBuffer.h>
10+
11+
namespace Gfx {
12+
13+
struct BitmapExportResult {
14+
ByteBuffer buffer;
15+
int width { 0 };
16+
int height { 0 };
17+
};
18+
19+
}

Libraries/LibGfx/ImmutableBitmap.cpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
#include <LibGfx/SkiaUtils.h>
1010

1111
#include <core/SkBitmap.h>
12+
#include <core/SkCanvas.h>
1213
#include <core/SkColorSpace.h>
1314
#include <core/SkImage.h>
15+
#include <core/SkSurface.h>
1416

1517
namespace Gfx {
1618

@@ -53,6 +55,91 @@ SkImage const* ImmutableBitmap::sk_image() const
5355
return m_impl->sk_image.get();
5456
}
5557

58+
static int bytes_per_pixel_for_export_format(ExportFormat format)
59+
{
60+
switch (format) {
61+
case ExportFormat::Gray8:
62+
case ExportFormat::Alpha8:
63+
return 1;
64+
case ExportFormat::RGB565:
65+
case ExportFormat::RGBA5551:
66+
case ExportFormat::RGBA4444:
67+
return 2;
68+
case ExportFormat::RGB888:
69+
return 3;
70+
case ExportFormat::RGBA8888:
71+
return 4;
72+
default:
73+
VERIFY_NOT_REACHED();
74+
}
75+
}
76+
77+
static SkColorType export_format_to_skia_color_type(ExportFormat format)
78+
{
79+
switch (format) {
80+
case ExportFormat::Gray8:
81+
return SkColorType::kGray_8_SkColorType;
82+
case ExportFormat::Alpha8:
83+
return SkColorType::kAlpha_8_SkColorType;
84+
case ExportFormat::RGB565:
85+
return SkColorType::kRGB_565_SkColorType;
86+
case ExportFormat::RGBA5551:
87+
dbgln("FIXME: Support conversion to RGBA5551.");
88+
return SkColorType::kUnknown_SkColorType;
89+
case ExportFormat::RGBA4444:
90+
return SkColorType::kARGB_4444_SkColorType;
91+
case ExportFormat::RGB888:
92+
return SkColorType::kRGB_888x_SkColorType;
93+
case ExportFormat::RGBA8888:
94+
return SkColorType::kRGBA_8888_SkColorType;
95+
default:
96+
VERIFY_NOT_REACHED();
97+
}
98+
}
99+
100+
ErrorOr<BitmapExportResult> ImmutableBitmap::export_to_byte_buffer(ExportFormat format, int flags, Optional<int> target_width, Optional<int> target_height) const
101+
{
102+
int width = target_width.value_or(this->width());
103+
int height = target_height.value_or(this->height());
104+
105+
Checked<size_t> buffer_pitch = width;
106+
int number_of_bytes = bytes_per_pixel_for_export_format(format);
107+
buffer_pitch *= number_of_bytes;
108+
if (buffer_pitch.has_overflow())
109+
return Error::from_string_literal("Gfx::ImmutableBitmap::export_to_byte_buffer size overflow");
110+
111+
if (Checked<size_t>::multiplication_would_overflow(buffer_pitch.value(), height))
112+
return Error::from_string_literal("Gfx::ImmutableBitmap::export_to_byte_buffer size overflow");
113+
114+
auto buffer = MUST(ByteBuffer::create_zeroed(buffer_pitch.value() * height));
115+
116+
if (width > 0 && height > 0) {
117+
auto skia_format = export_format_to_skia_color_type(format);
118+
auto color_space = SkColorSpace::MakeSRGB();
119+
120+
auto image_info = SkImageInfo::Make(width, height, skia_format, flags & ExportFlags::PremultiplyAlpha ? SkAlphaType::kPremul_SkAlphaType : SkAlphaType::kUnpremul_SkAlphaType, color_space);
121+
auto surface = SkSurfaces::WrapPixels(image_info, buffer.data(), buffer_pitch.value());
122+
VERIFY(surface);
123+
auto* surface_canvas = surface->getCanvas();
124+
auto dst_rect = Gfx::to_skia_rect(Gfx::Rect { 0, 0, width, height });
125+
126+
if (flags & ExportFlags::FlipY) {
127+
surface_canvas->translate(0, dst_rect.height());
128+
surface_canvas->scale(1, -1);
129+
}
130+
131+
surface_canvas->drawImageRect(sk_image(), dst_rect, Gfx::to_skia_sampling_options(Gfx::ScalingMode::NearestNeighbor));
132+
} else {
133+
VERIFY(buffer.is_empty());
134+
}
135+
136+
return BitmapExportResult {
137+
.buffer = move(buffer),
138+
.width = width,
139+
.height = height,
140+
};
141+
}
142+
56143
RefPtr<Gfx::Bitmap const> ImmutableBitmap::bitmap() const
57144
{
58145
// FIXME: Implement for PaintingSurface

Libraries/LibGfx/ImmutableBitmap.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <AK/AtomicRefCounted.h>
1010
#include <AK/Forward.h>
1111
#include <AK/NonnullOwnPtr.h>
12+
#include <LibGfx/BitmapExportResult.h>
1213
#include <LibGfx/Color.h>
1314
#include <LibGfx/ColorSpace.h>
1415
#include <LibGfx/Forward.h>
@@ -20,6 +21,27 @@ namespace Gfx {
2021

2122
struct ImmutableBitmapImpl;
2223

24+
enum class ExportFormat : u8 {
25+
// 8 bit
26+
Gray8,
27+
Alpha8,
28+
// 16 bit
29+
RGB565,
30+
RGBA5551,
31+
RGBA4444,
32+
// 24 bit
33+
RGB888,
34+
// 32 bit
35+
RGBA8888,
36+
};
37+
38+
struct ExportFlags {
39+
enum : u8 {
40+
PremultiplyAlpha = 1 << 0,
41+
FlipY = 1 << 1,
42+
};
43+
};
44+
2345
class ImmutableBitmap final : public AtomicRefCounted<ImmutableBitmap> {
2446
public:
2547
static NonnullRefPtr<ImmutableBitmap> create(NonnullRefPtr<Bitmap> bitmap, ColorSpace color_space = {});
@@ -36,6 +58,7 @@ class ImmutableBitmap final : public AtomicRefCounted<ImmutableBitmap> {
3658
AlphaType alpha_type() const;
3759

3860
SkImage const* sk_image() const;
61+
[[nodiscard]] ErrorOr<BitmapExportResult> export_to_byte_buffer(ExportFormat format, int flags, Optional<int> target_width, Optional<int> target_height) const;
3962

4063
Color get_pixel(int x, int y) const;
4164

Libraries/LibWeb/WebGL/WebGLRenderingContextBase.cpp

Lines changed: 23 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -31,57 +31,28 @@ extern "C" {
3131

3232
namespace Web::WebGL {
3333

34-
static constexpr Optional<int> opengl_format_and_type_number_of_bytes(WebIDL::UnsignedLong format, WebIDL::UnsignedLong type)
35-
{
36-
switch (format) {
37-
case GL_LUMINANCE:
38-
case GL_ALPHA:
39-
if (type != GL_UNSIGNED_BYTE)
40-
return OptionalNone {};
41-
42-
return 1;
43-
case GL_LUMINANCE_ALPHA:
44-
if (type != GL_UNSIGNED_BYTE)
45-
return OptionalNone {};
46-
47-
return 2;
48-
case GL_RGB:
49-
if (type != GL_UNSIGNED_BYTE && type != GL_UNSIGNED_SHORT_5_6_5)
50-
return OptionalNone {};
51-
52-
return type == GL_UNSIGNED_BYTE ? 3 : 2;
53-
case GL_RGBA:
54-
if (type != GL_UNSIGNED_BYTE && type != GL_UNSIGNED_SHORT_4_4_4_4 && type != GL_UNSIGNED_SHORT_5_5_5_1)
55-
return OptionalNone {};
56-
57-
return type == GL_UNSIGNED_BYTE ? 4 : 2;
58-
default:
59-
return OptionalNone {};
60-
}
61-
}
62-
63-
static constexpr SkColorType opengl_format_and_type_to_skia_color_type(WebIDL::UnsignedLong format, WebIDL::UnsignedLong type)
34+
static constexpr Optional<Gfx::ExportFormat> determine_export_format(WebIDL::UnsignedLong format, WebIDL::UnsignedLong type)
6435
{
6536
switch (format) {
6637
case GL_RGB:
6738
switch (type) {
6839
case GL_UNSIGNED_BYTE:
69-
return SkColorType::kRGB_888x_SkColorType;
40+
return Gfx::ExportFormat::RGB888;
7041
case GL_UNSIGNED_SHORT_5_6_5:
71-
return SkColorType::kRGB_565_SkColorType;
42+
return Gfx::ExportFormat::RGB565;
7243
default:
7344
break;
7445
}
7546
break;
7647
case GL_RGBA:
7748
switch (type) {
7849
case GL_UNSIGNED_BYTE:
79-
return SkColorType::kRGBA_8888_SkColorType;
50+
return Gfx::ExportFormat::RGBA8888;
8051
case GL_UNSIGNED_SHORT_4_4_4_4:
8152
// FIXME: This is not exactly the same as RGBA.
82-
return SkColorType::kARGB_4444_SkColorType;
53+
return Gfx::ExportFormat::RGBA4444;
8354
case GL_UNSIGNED_SHORT_5_5_5_1:
84-
dbgln("WebGL FIXME: Support conversion to RGBA5551.");
55+
return Gfx::ExportFormat::RGBA5551;
8556
break;
8657
default:
8758
break;
@@ -90,15 +61,15 @@ static constexpr SkColorType opengl_format_and_type_to_skia_color_type(WebIDL::U
9061
case GL_ALPHA:
9162
switch (type) {
9263
case GL_UNSIGNED_BYTE:
93-
return SkColorType::kAlpha_8_SkColorType;
64+
return Gfx::ExportFormat::Alpha8;
9465
default:
9566
break;
9667
}
9768
break;
9869
case GL_LUMINANCE:
9970
switch (type) {
10071
case GL_UNSIGNED_BYTE:
101-
return SkColorType::kGray_8_SkColorType;
72+
return Gfx::ExportFormat::Gray8;
10273
default:
10374
break;
10475
}
@@ -108,10 +79,10 @@ static constexpr SkColorType opengl_format_and_type_to_skia_color_type(WebIDL::U
10879
}
10980

11081
dbgln("WebGL: Unsupported format and type combination. format: 0x{:04x}, type: 0x{:04x}", format, type);
111-
return SkColorType::kUnknown_SkColorType;
82+
return {};
11283
}
11384

114-
Optional<WebGLRenderingContextBase::ConvertedTexture> WebGLRenderingContextBase::read_and_pixel_convert_texture_image_source(TexImageSource const& source, WebIDL::UnsignedLong format, WebIDL::UnsignedLong type, Optional<int> destination_width, Optional<int> destination_height)
85+
Optional<Gfx::BitmapExportResult> WebGLRenderingContextBase::read_and_pixel_convert_texture_image_source(TexImageSource const& source, WebIDL::UnsignedLong format, WebIDL::UnsignedLong type, Optional<int> destination_width, Optional<int> destination_height)
11586
{
11687
// FIXME: If this function is called with an ImageData whose data attribute has been neutered,
11788
// an INVALID_VALUE error is generated.
@@ -147,53 +118,27 @@ Optional<WebGLRenderingContextBase::ConvertedTexture> WebGLRenderingContextBase:
147118
if (!bitmap)
148119
return OptionalNone {};
149120

150-
int width = destination_width.value_or(bitmap->width());
151-
int height = destination_height.value_or(bitmap->height());
152-
153-
Checked<size_t> buffer_pitch = width;
154-
155-
auto number_of_bytes = opengl_format_and_type_number_of_bytes(format, type);
156-
if (!number_of_bytes.has_value())
157-
return OptionalNone {};
158-
159-
buffer_pitch *= number_of_bytes.value();
160-
161-
if (buffer_pitch.has_overflow())
121+
auto export_format = determine_export_format(format, type);
122+
if (!export_format.has_value())
162123
return OptionalNone {};
163124

164-
if (Checked<size_t>::multiplication_would_overflow(buffer_pitch.value(), height))
165-
return OptionalNone {};
166-
167-
auto buffer = MUST(ByteBuffer::create_zeroed(buffer_pitch.value() * height));
168-
169-
if (width > 0 && height > 0) {
170-
// FIXME: Respect unpackColorSpace
171-
auto skia_format = opengl_format_and_type_to_skia_color_type(format, type);
172-
auto color_space = SkColorSpace::MakeSRGB();
173-
auto image_info = SkImageInfo::Make(width, height, skia_format, m_unpack_premultiply_alpha ? SkAlphaType::kPremul_SkAlphaType : SkAlphaType::kUnpremul_SkAlphaType, color_space);
174-
auto surface = SkSurfaces::WrapPixels(image_info, buffer.data(), buffer_pitch.value());
175-
VERIFY(surface);
176-
auto surface_canvas = surface->getCanvas();
177-
auto dst_rect = Gfx::to_skia_rect(Gfx::Rect { 0, 0, width, height });
178-
125+
// FIXME: Respect unpackColorSpace
126+
auto export_flags = 0;
127+
if (m_unpack_flip_y && !source.has<GC::Root<HTML::ImageBitmap>>())
179128
// The first pixel transferred from the source to the WebGL implementation corresponds to the upper left corner of
180129
// the source. This behavior is modified by the UNPACK_FLIP_Y_WEBGL pixel storage parameter, except for ImageBitmap
181130
// arguments, as described in the abovementioned section.
182-
if (m_unpack_flip_y && !source.has<GC::Root<HTML::ImageBitmap>>()) {
183-
surface_canvas->translate(0, dst_rect.height());
184-
surface_canvas->scale(1, -1);
185-
}
131+
export_flags |= Gfx::ExportFlags::FlipY;
132+
if (m_unpack_premultiply_alpha)
133+
export_flags |= Gfx::ExportFlags::PremultiplyAlpha;
186134

187-
surface_canvas->drawImageRect(bitmap->sk_image(), dst_rect, Gfx::to_skia_sampling_options(Gfx::ScalingMode::NearestNeighbor));
188-
} else {
189-
VERIFY(buffer.is_empty());
135+
auto result = bitmap->export_to_byte_buffer(export_format.value(), export_flags, destination_width, destination_height);
136+
if (result.is_error()) {
137+
dbgln("Could not export bitmap: {}", result.release_error());
138+
return OptionalNone {};
190139
}
191140

192-
return ConvertedTexture {
193-
.buffer = move(buffer),
194-
.width = width,
195-
.height = height,
196-
};
141+
return result.release_value();
197142
}
198143

199144
// TODO: The glGetError spec allows for queueing errors which is something we should probably do, for now

Libraries/LibWeb/WebGL/WebGLRenderingContextBase.h

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#pragma once
88

9+
#include <LibGfx/BitmapExportResult.h>
910
#include <LibJS/Runtime/DataView.h>
1011
#include <LibJS/Runtime/TypedArray.h>
1112
#include <LibWeb/Forward.h>
@@ -122,12 +123,7 @@ class WebGLRenderingContextBase {
122123
return get_offset_span(buffer->data(), src_offset, src_length_override);
123124
}
124125

125-
struct ConvertedTexture {
126-
ByteBuffer buffer;
127-
int width { 0 };
128-
int height { 0 };
129-
};
130-
Optional<ConvertedTexture> read_and_pixel_convert_texture_image_source(TexImageSource const& source, WebIDL::UnsignedLong format, WebIDL::UnsignedLong type, Optional<int> destination_width = OptionalNone {}, Optional<int> destination_height = OptionalNone {});
126+
Optional<Gfx::BitmapExportResult> read_and_pixel_convert_texture_image_source(TexImageSource const& source, WebIDL::UnsignedLong format, WebIDL::UnsignedLong type, Optional<int> destination_width = OptionalNone {}, Optional<int> destination_height = OptionalNone {});
131127

132128
protected:
133129
static Vector<GLchar> null_terminated_string(StringView string)

0 commit comments

Comments
 (0)