mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-12-05 01:10:24 +00:00
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).
This commit is contained in:
committed by
Jelle Raaijmakers
parent
fa181c2be8
commit
88c4814de6
Notes:
github-actions[bot]
2025-11-28 17:34:40 +00:00
Author: https://github.com/InvalidUsernameException Commit: https://github.com/LadybirdBrowser/ladybird/commit/88c4814de6a Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/6911 Reviewed-by: https://github.com/Hendiadyoin1 Reviewed-by: https://github.com/Psychpsyo Reviewed-by: https://github.com/gmta
19
Libraries/LibGfx/BitmapExportResult.h
Normal file
19
Libraries/LibGfx/BitmapExportResult.h
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Ladybird contributors
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteBuffer.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
struct BitmapExportResult {
|
||||
ByteBuffer buffer;
|
||||
int width { 0 };
|
||||
int height { 0 };
|
||||
};
|
||||
|
||||
}
|
||||
@@ -9,8 +9,10 @@
|
||||
#include <LibGfx/SkiaUtils.h>
|
||||
|
||||
#include <core/SkBitmap.h>
|
||||
#include <core/SkCanvas.h>
|
||||
#include <core/SkColorSpace.h>
|
||||
#include <core/SkImage.h>
|
||||
#include <core/SkSurface.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
@@ -53,6 +55,91 @@ SkImage const* ImmutableBitmap::sk_image() const
|
||||
return m_impl->sk_image.get();
|
||||
}
|
||||
|
||||
static int bytes_per_pixel_for_export_format(ExportFormat format)
|
||||
{
|
||||
switch (format) {
|
||||
case ExportFormat::Gray8:
|
||||
case ExportFormat::Alpha8:
|
||||
return 1;
|
||||
case ExportFormat::RGB565:
|
||||
case ExportFormat::RGBA5551:
|
||||
case ExportFormat::RGBA4444:
|
||||
return 2;
|
||||
case ExportFormat::RGB888:
|
||||
return 3;
|
||||
case ExportFormat::RGBA8888:
|
||||
return 4;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
static SkColorType export_format_to_skia_color_type(ExportFormat format)
|
||||
{
|
||||
switch (format) {
|
||||
case ExportFormat::Gray8:
|
||||
return SkColorType::kGray_8_SkColorType;
|
||||
case ExportFormat::Alpha8:
|
||||
return SkColorType::kAlpha_8_SkColorType;
|
||||
case ExportFormat::RGB565:
|
||||
return SkColorType::kRGB_565_SkColorType;
|
||||
case ExportFormat::RGBA5551:
|
||||
dbgln("FIXME: Support conversion to RGBA5551.");
|
||||
return SkColorType::kUnknown_SkColorType;
|
||||
case ExportFormat::RGBA4444:
|
||||
return SkColorType::kARGB_4444_SkColorType;
|
||||
case ExportFormat::RGB888:
|
||||
return SkColorType::kRGB_888x_SkColorType;
|
||||
case ExportFormat::RGBA8888:
|
||||
return SkColorType::kRGBA_8888_SkColorType;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
ErrorOr<BitmapExportResult> ImmutableBitmap::export_to_byte_buffer(ExportFormat format, int flags, Optional<int> target_width, Optional<int> target_height) const
|
||||
{
|
||||
int width = target_width.value_or(this->width());
|
||||
int height = target_height.value_or(this->height());
|
||||
|
||||
Checked<size_t> buffer_pitch = width;
|
||||
int number_of_bytes = bytes_per_pixel_for_export_format(format);
|
||||
buffer_pitch *= number_of_bytes;
|
||||
if (buffer_pitch.has_overflow())
|
||||
return Error::from_string_literal("Gfx::ImmutableBitmap::export_to_byte_buffer size overflow");
|
||||
|
||||
if (Checked<size_t>::multiplication_would_overflow(buffer_pitch.value(), height))
|
||||
return Error::from_string_literal("Gfx::ImmutableBitmap::export_to_byte_buffer size overflow");
|
||||
|
||||
auto buffer = MUST(ByteBuffer::create_zeroed(buffer_pitch.value() * height));
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
auto skia_format = export_format_to_skia_color_type(format);
|
||||
auto color_space = SkColorSpace::MakeSRGB();
|
||||
|
||||
auto image_info = SkImageInfo::Make(width, height, skia_format, flags & ExportFlags::PremultiplyAlpha ? SkAlphaType::kPremul_SkAlphaType : SkAlphaType::kUnpremul_SkAlphaType, color_space);
|
||||
auto surface = SkSurfaces::WrapPixels(image_info, buffer.data(), buffer_pitch.value());
|
||||
VERIFY(surface);
|
||||
auto* surface_canvas = surface->getCanvas();
|
||||
auto dst_rect = Gfx::to_skia_rect(Gfx::Rect { 0, 0, width, height });
|
||||
|
||||
if (flags & ExportFlags::FlipY) {
|
||||
surface_canvas->translate(0, dst_rect.height());
|
||||
surface_canvas->scale(1, -1);
|
||||
}
|
||||
|
||||
surface_canvas->drawImageRect(sk_image(), dst_rect, Gfx::to_skia_sampling_options(Gfx::ScalingMode::NearestNeighbor));
|
||||
} else {
|
||||
VERIFY(buffer.is_empty());
|
||||
}
|
||||
|
||||
return BitmapExportResult {
|
||||
.buffer = move(buffer),
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
}
|
||||
|
||||
RefPtr<Gfx::Bitmap const> ImmutableBitmap::bitmap() const
|
||||
{
|
||||
// FIXME: Implement for PaintingSurface
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <AK/AtomicRefCounted.h>
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <LibGfx/BitmapExportResult.h>
|
||||
#include <LibGfx/Color.h>
|
||||
#include <LibGfx/ColorSpace.h>
|
||||
#include <LibGfx/Forward.h>
|
||||
@@ -20,6 +21,27 @@ namespace Gfx {
|
||||
|
||||
struct ImmutableBitmapImpl;
|
||||
|
||||
enum class ExportFormat : u8 {
|
||||
// 8 bit
|
||||
Gray8,
|
||||
Alpha8,
|
||||
// 16 bit
|
||||
RGB565,
|
||||
RGBA5551,
|
||||
RGBA4444,
|
||||
// 24 bit
|
||||
RGB888,
|
||||
// 32 bit
|
||||
RGBA8888,
|
||||
};
|
||||
|
||||
struct ExportFlags {
|
||||
enum : u8 {
|
||||
PremultiplyAlpha = 1 << 0,
|
||||
FlipY = 1 << 1,
|
||||
};
|
||||
};
|
||||
|
||||
class ImmutableBitmap final : public AtomicRefCounted<ImmutableBitmap> {
|
||||
public:
|
||||
static NonnullRefPtr<ImmutableBitmap> create(NonnullRefPtr<Bitmap> bitmap, ColorSpace color_space = {});
|
||||
@@ -36,6 +58,7 @@ public:
|
||||
AlphaType alpha_type() const;
|
||||
|
||||
SkImage const* sk_image() const;
|
||||
[[nodiscard]] ErrorOr<BitmapExportResult> export_to_byte_buffer(ExportFormat format, int flags, Optional<int> target_width, Optional<int> target_height) const;
|
||||
|
||||
Color get_pixel(int x, int y) const;
|
||||
|
||||
|
||||
@@ -31,44 +31,15 @@ extern "C" {
|
||||
|
||||
namespace Web::WebGL {
|
||||
|
||||
static constexpr Optional<int> opengl_format_and_type_number_of_bytes(WebIDL::UnsignedLong format, WebIDL::UnsignedLong type)
|
||||
{
|
||||
switch (format) {
|
||||
case GL_LUMINANCE:
|
||||
case GL_ALPHA:
|
||||
if (type != GL_UNSIGNED_BYTE)
|
||||
return OptionalNone {};
|
||||
|
||||
return 1;
|
||||
case GL_LUMINANCE_ALPHA:
|
||||
if (type != GL_UNSIGNED_BYTE)
|
||||
return OptionalNone {};
|
||||
|
||||
return 2;
|
||||
case GL_RGB:
|
||||
if (type != GL_UNSIGNED_BYTE && type != GL_UNSIGNED_SHORT_5_6_5)
|
||||
return OptionalNone {};
|
||||
|
||||
return type == GL_UNSIGNED_BYTE ? 3 : 2;
|
||||
case GL_RGBA:
|
||||
if (type != GL_UNSIGNED_BYTE && type != GL_UNSIGNED_SHORT_4_4_4_4 && type != GL_UNSIGNED_SHORT_5_5_5_1)
|
||||
return OptionalNone {};
|
||||
|
||||
return type == GL_UNSIGNED_BYTE ? 4 : 2;
|
||||
default:
|
||||
return OptionalNone {};
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr SkColorType opengl_format_and_type_to_skia_color_type(WebIDL::UnsignedLong format, WebIDL::UnsignedLong type)
|
||||
static constexpr Optional<Gfx::ExportFormat> determine_export_format(WebIDL::UnsignedLong format, WebIDL::UnsignedLong type)
|
||||
{
|
||||
switch (format) {
|
||||
case GL_RGB:
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
return SkColorType::kRGB_888x_SkColorType;
|
||||
return Gfx::ExportFormat::RGB888;
|
||||
case GL_UNSIGNED_SHORT_5_6_5:
|
||||
return SkColorType::kRGB_565_SkColorType;
|
||||
return Gfx::ExportFormat::RGB565;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -76,12 +47,12 @@ static constexpr SkColorType opengl_format_and_type_to_skia_color_type(WebIDL::U
|
||||
case GL_RGBA:
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
return SkColorType::kRGBA_8888_SkColorType;
|
||||
return Gfx::ExportFormat::RGBA8888;
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4:
|
||||
// FIXME: This is not exactly the same as RGBA.
|
||||
return SkColorType::kARGB_4444_SkColorType;
|
||||
return Gfx::ExportFormat::RGBA4444;
|
||||
case GL_UNSIGNED_SHORT_5_5_5_1:
|
||||
dbgln("WebGL FIXME: Support conversion to RGBA5551.");
|
||||
return Gfx::ExportFormat::RGBA5551;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -90,7 +61,7 @@ static constexpr SkColorType opengl_format_and_type_to_skia_color_type(WebIDL::U
|
||||
case GL_ALPHA:
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
return SkColorType::kAlpha_8_SkColorType;
|
||||
return Gfx::ExportFormat::Alpha8;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -98,7 +69,7 @@ static constexpr SkColorType opengl_format_and_type_to_skia_color_type(WebIDL::U
|
||||
case GL_LUMINANCE:
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
return SkColorType::kGray_8_SkColorType;
|
||||
return Gfx::ExportFormat::Gray8;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -108,10 +79,10 @@ static constexpr SkColorType opengl_format_and_type_to_skia_color_type(WebIDL::U
|
||||
}
|
||||
|
||||
dbgln("WebGL: Unsupported format and type combination. format: 0x{:04x}, type: 0x{:04x}", format, type);
|
||||
return SkColorType::kUnknown_SkColorType;
|
||||
return {};
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
{
|
||||
// FIXME: If this function is called with an ImageData whose data attribute has been neutered,
|
||||
// an INVALID_VALUE error is generated.
|
||||
@@ -147,53 +118,27 @@ Optional<WebGLRenderingContextBase::ConvertedTexture> WebGLRenderingContextBase:
|
||||
if (!bitmap)
|
||||
return OptionalNone {};
|
||||
|
||||
int width = destination_width.value_or(bitmap->width());
|
||||
int height = destination_height.value_or(bitmap->height());
|
||||
|
||||
Checked<size_t> buffer_pitch = width;
|
||||
|
||||
auto number_of_bytes = opengl_format_and_type_number_of_bytes(format, type);
|
||||
if (!number_of_bytes.has_value())
|
||||
auto export_format = determine_export_format(format, type);
|
||||
if (!export_format.has_value())
|
||||
return OptionalNone {};
|
||||
|
||||
buffer_pitch *= number_of_bytes.value();
|
||||
|
||||
if (buffer_pitch.has_overflow())
|
||||
return OptionalNone {};
|
||||
|
||||
if (Checked<size_t>::multiplication_would_overflow(buffer_pitch.value(), height))
|
||||
return OptionalNone {};
|
||||
|
||||
auto buffer = MUST(ByteBuffer::create_zeroed(buffer_pitch.value() * height));
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
// FIXME: Respect unpackColorSpace
|
||||
auto skia_format = opengl_format_and_type_to_skia_color_type(format, type);
|
||||
auto color_space = SkColorSpace::MakeSRGB();
|
||||
auto image_info = SkImageInfo::Make(width, height, skia_format, m_unpack_premultiply_alpha ? SkAlphaType::kPremul_SkAlphaType : SkAlphaType::kUnpremul_SkAlphaType, color_space);
|
||||
auto surface = SkSurfaces::WrapPixels(image_info, buffer.data(), buffer_pitch.value());
|
||||
VERIFY(surface);
|
||||
auto surface_canvas = surface->getCanvas();
|
||||
auto dst_rect = Gfx::to_skia_rect(Gfx::Rect { 0, 0, width, height });
|
||||
|
||||
// FIXME: Respect unpackColorSpace
|
||||
auto export_flags = 0;
|
||||
if (m_unpack_flip_y && !source.has<GC::Root<HTML::ImageBitmap>>())
|
||||
// The first pixel transferred from the source to the WebGL implementation corresponds to the upper left corner of
|
||||
// the source. This behavior is modified by the UNPACK_FLIP_Y_WEBGL pixel storage parameter, except for ImageBitmap
|
||||
// arguments, as described in the abovementioned section.
|
||||
if (m_unpack_flip_y && !source.has<GC::Root<HTML::ImageBitmap>>()) {
|
||||
surface_canvas->translate(0, dst_rect.height());
|
||||
surface_canvas->scale(1, -1);
|
||||
}
|
||||
export_flags |= Gfx::ExportFlags::FlipY;
|
||||
if (m_unpack_premultiply_alpha)
|
||||
export_flags |= Gfx::ExportFlags::PremultiplyAlpha;
|
||||
|
||||
surface_canvas->drawImageRect(bitmap->sk_image(), dst_rect, Gfx::to_skia_sampling_options(Gfx::ScalingMode::NearestNeighbor));
|
||||
} else {
|
||||
VERIFY(buffer.is_empty());
|
||||
auto result = bitmap->export_to_byte_buffer(export_format.value(), export_flags, destination_width, destination_height);
|
||||
if (result.is_error()) {
|
||||
dbgln("Could not export bitmap: {}", result.release_error());
|
||||
return OptionalNone {};
|
||||
}
|
||||
|
||||
return ConvertedTexture {
|
||||
.buffer = move(buffer),
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
return result.release_value();
|
||||
}
|
||||
|
||||
// TODO: The glGetError spec allows for queueing errors which is something we should probably do, for now
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGfx/BitmapExportResult.h>
|
||||
#include <LibJS/Runtime/DataView.h>
|
||||
#include <LibJS/Runtime/TypedArray.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
@@ -122,12 +123,7 @@ public:
|
||||
return get_offset_span(buffer->data(), src_offset, src_length_override);
|
||||
}
|
||||
|
||||
struct ConvertedTexture {
|
||||
ByteBuffer buffer;
|
||||
int width { 0 };
|
||||
int height { 0 };
|
||||
};
|
||||
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 {});
|
||||
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 {});
|
||||
|
||||
protected:
|
||||
static Vector<GLchar> null_terminated_string(StringView string)
|
||||
|
||||
Reference in New Issue
Block a user