LibGfx: Move bitmap export out of ImmutableBitmap

Bitmap export is pixel conversion, not an ImmutableBitmap-specific
operation. It needs a source bitmap and color space, and callers should
provide those explicitly instead of routing through the immutable
snapshot abstraction.

Move the export formats, flags, result type, and conversion
implementation into a new BitmapExport helper. Keep
BitmapExportResult.h as a forwarding header for existing includes
while making BitmapExport.h the new home for the public API.

Update WebGL and the LibGfx export test to use the standalone helper
directly.
This commit is contained in:
Aliaksandr Kalenik 2026-05-05 12:26:10 +02:00 committed by Gregory Bertilson
parent 9f7f622362
commit 00181dcf50
10 changed files with 235 additions and 182 deletions

View file

@ -0,0 +1,156 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Bitmap.h>
#include <LibGfx/BitmapExport.h>
#include <LibGfx/ColorSpace.h>
#include <LibGfx/SkiaUtils.h>
#include <core/SkCanvas.h>
#include <core/SkColorSpace.h>
#include <core/SkImage.h>
#include <core/SkSurface.h>
namespace Gfx {
StringView export_format_name(ExportFormat format)
{
switch (format) {
#define ENUMERATE_EXPORT_FORMAT(format) \
case Gfx::ExportFormat::format: \
return #format##sv;
ENUMERATE_EXPORT_FORMATS(ENUMERATE_EXPORT_FORMAT)
#undef ENUMERATE_EXPORT_FORMAT
}
VERIFY_NOT_REACHED();
}
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:
// This one needs to be converted manually because Skia has no valid 24-bit color type.
VERIFY_NOT_REACHED();
case ExportFormat::RGBA8888:
return SkColorType::kRGBA_8888_SkColorType;
default:
VERIFY_NOT_REACHED();
}
}
ErrorOr<BitmapExportResult> export_bitmap_to_byte_buffer(
Bitmap const& bitmap,
ColorSpace const& color_space,
ExportFormat format,
int flags,
Optional<int> target_width,
Optional<int> target_height)
{
int width = target_width.value_or(bitmap.width());
int height = target_height.value_or(bitmap.height());
if (format == ExportFormat::RGB888 && (width != bitmap.width() || height != bitmap.height())) {
dbgln("FIXME: Ignoring target width and height because scaling is not implemented for this export format.");
width = bitmap.width();
height = bitmap.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::export_bitmap_to_byte_buffer size overflow");
if (Checked<size_t>::multiplication_would_overflow(buffer_pitch.value(), height))
return Error::from_string_literal("Gfx::export_bitmap_to_byte_buffer size overflow");
auto buffer = MUST(ByteBuffer::create_zeroed(buffer_pitch.value() * height));
if (width > 0 && height > 0) {
if (format == ExportFormat::RGB888) {
// 24 bit RGB is not supported by Skia, so we need to handle this format ourselves.
auto* raw_buffer = buffer.data();
for (auto y = 0; y < height; y++) {
auto target_y = flags & ExportFlags::FlipY ? height - y - 1 : y;
for (auto x = 0; x < width; x++) {
auto pixel = bitmap.get_pixel(x, y);
auto buffer_offset = (target_y * buffer_pitch.value()) + (x * 3ull);
raw_buffer[buffer_offset + 0] = pixel.red();
raw_buffer[buffer_offset + 1] = pixel.green();
raw_buffer[buffer_offset + 2] = pixel.blue();
}
}
} else {
auto image = sk_image_from_bitmap(bitmap, color_space);
if (!image)
return Error::from_string_literal("Failed to create a Skia image for this Bitmap");
auto skia_format = export_format_to_skia_color_type(format);
auto skia_color_space = SkColorSpace::MakeSRGB();
auto image_info = SkImageInfo::Make(
width,
height,
skia_format,
flags & ExportFlags::PremultiplyAlpha ? SkAlphaType::kPremul_SkAlphaType : SkAlphaType::kUnpremul_SkAlphaType,
skia_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(
image.get(),
dst_rect,
Gfx::to_skia_sampling_options(Gfx::ScalingMode::NearestNeighbor));
}
} else {
VERIFY(buffer.is_empty());
}
return BitmapExportResult {
.buffer = move(buffer),
.width = width,
.height = height,
};
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/Error.h>
#include <AK/Forward.h>
#include <LibGfx/ColorSpace.h>
#include <LibGfx/Forward.h>
namespace Gfx {
#define ENUMERATE_EXPORT_FORMATS(X) \
X(Gray8) \
X(Alpha8) \
X(RGB565) \
X(RGBA5551) \
X(RGBA4444) \
X(RGB888) \
X(RGBA8888)
enum class ExportFormat : u8 {
#define ENUMERATE_EXPORT_FORMAT(format) format,
ENUMERATE_EXPORT_FORMATS(ENUMERATE_EXPORT_FORMAT)
#undef ENUMERATE_EXPORT_FORMAT
};
[[nodiscard]] StringView export_format_name(ExportFormat);
struct ExportFlags {
enum : u8 {
PremultiplyAlpha = 1 << 0,
FlipY = 1 << 1,
};
};
struct BitmapExportResult {
ByteBuffer buffer;
int width { 0 };
int height { 0 };
};
[[nodiscard]] ErrorOr<BitmapExportResult> export_bitmap_to_byte_buffer(
Bitmap const&,
ColorSpace const&,
ExportFormat,
int flags,
Optional<int> target_width,
Optional<int> target_height);
}

View file

@ -1,19 +0,0 @@
/*
* 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 };
};
}

View file

@ -4,6 +4,7 @@ include(vulkan)
set(SOURCES
AffineTransform.cpp
Bitmap.cpp
BitmapExport.cpp
BitmapSequence.cpp
CMYKBitmap.cpp
Color.cpp

View file

@ -8,27 +8,9 @@
#include <LibGfx/Bitmap.h>
#include <LibGfx/ImmutableBitmap.h>
#include <LibGfx/PaintingSurface.h>
#include <LibGfx/SkiaUtils.h>
#include <core/SkCanvas.h>
#include <core/SkColorSpace.h>
#include <core/SkImage.h>
#include <core/SkSurface.h>
namespace Gfx {
StringView export_format_name(ExportFormat format)
{
switch (format) {
#define ENUMERATE_EXPORT_FORMAT(format) \
case Gfx::ExportFormat::format: \
return #format##sv;
ENUMERATE_EXPORT_FORMATS(ENUMERATE_EXPORT_FORMAT)
#undef ENUMERATE_EXPORT_FORMAT
}
VERIFY_NOT_REACHED();
}
int ImmutableBitmap::width() const
{
return m_bitmap->width();
@ -59,117 +41,6 @@ ColorSpace const& ImmutableBitmap::color_space() const
return m_color_space;
}
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:
// This one needs to be converted manually because Skia has no valid 24-bit color type.
VERIFY_NOT_REACHED();
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());
if (format == ExportFormat::RGB888 && (width != this->width() || height != this->height())) {
dbgln("FIXME: Ignoring target width and height because scaling is not implemented for this export format.");
width = this->width();
height = 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) {
if (format == ExportFormat::RGB888) {
// 24 bit RGB is not supported by Skia, so we need to handle this format ourselves.
auto* raw_buffer = buffer.data();
for (auto y = 0; y < height; y++) {
auto target_y = flags & ExportFlags::FlipY ? height - y - 1 : y;
for (auto x = 0; x < width; x++) {
auto pixel = get_pixel(x, y);
auto buffer_offset = (target_y * buffer_pitch.value()) + (x * 3ull);
raw_buffer[buffer_offset + 0] = pixel.red();
raw_buffer[buffer_offset + 1] = pixel.green();
raw_buffer[buffer_offset + 2] = pixel.blue();
}
}
} else {
auto image = sk_image_from_bitmap(*m_bitmap, m_color_space);
if (!image)
return Error::from_string_literal("Failed to create a Skia image for this ImmutableBitmap");
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(image.get(), 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
{
return m_bitmap;

View file

@ -10,7 +10,6 @@
#include <AK/AtomicRefCounted.h>
#include <AK/Forward.h>
#include <AK/NonnullRefPtr.h>
#include <LibGfx/BitmapExportResult.h>
#include <LibGfx/Color.h>
#include <LibGfx/ColorSpace.h>
#include <LibGfx/Forward.h>
@ -18,30 +17,6 @@
namespace Gfx {
#define ENUMERATE_EXPORT_FORMATS(X) \
X(Gray8) \
X(Alpha8) \
X(RGB565) \
X(RGBA5551) \
X(RGBA4444) \
X(RGB888) \
X(RGBA8888)
enum class ExportFormat : u8 {
#define ENUMERATE_EXPORT_FORMAT(format) format,
ENUMERATE_EXPORT_FORMATS(ENUMERATE_EXPORT_FORMAT)
#undef ENUMERATE_EXPORT_FORMAT
};
[[nodiscard]] StringView export_format_name(ExportFormat);
struct ExportFlags {
enum : u8 {
PremultiplyAlpha = 1 << 0,
FlipY = 1 << 1,
};
};
class ImmutableBitmap final : public AtomicRefCounted<ImmutableBitmap> {
public:
static NonnullRefPtr<ImmutableBitmap> create(NonnullRefPtr<Bitmap const> const& bitmap, ColorSpace color_space = {});
@ -58,8 +33,6 @@ public:
AlphaType alpha_type() const;
ColorSpace const& color_space() 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;
RefPtr<Bitmap const> bitmap() const;

View file

@ -12,6 +12,7 @@ extern "C" {
#include <GLES2/gl2ext_angle.h>
}
#include <LibGfx/BitmapExport.h>
#include <LibGfx/ImmutableBitmap.h>
#include <LibGfx/SkiaUtils.h>
#include <LibWeb/HTML/EventLoop/Task.h>
@ -302,7 +303,17 @@ Optional<Gfx::BitmapExportResult> WebGLRenderingContextBase::read_and_pixel_conv
if (m_unpack_premultiply_alpha)
export_flags |= Gfx::ExportFlags::PremultiplyAlpha;
auto result = bitmap->export_to_byte_buffer(export_format.value(), export_flags, destination_width, destination_height);
auto source_bitmap = bitmap->bitmap();
if (!source_bitmap)
return OptionalNone {};
auto result = Gfx::export_bitmap_to_byte_buffer(
*source_bitmap,
bitmap->color_space(),
export_format.value(),
export_flags,
destination_width,
destination_height);
if (result.is_error()) {
dbgln("Could not export bitmap: {}", result.release_error());
return OptionalNone {};

View file

@ -6,7 +6,7 @@
#pragma once
#include <LibGfx/BitmapExportResult.h>
#include <LibGfx/BitmapExport.h>
#include <LibJS/Runtime/DataView.h>
#include <LibJS/Runtime/TypedArray.h>
#include <LibWeb/Bindings/PlatformObject.h>

View file

@ -2,10 +2,10 @@ include(skia)
set(TEST_SOURCES
BenchmarkJPEGLoader.cpp
TestBitmapExport.cpp
TestColor.cpp
TestImageDecoder.cpp
TestImageWriter.cpp
TestImmutableBitmap.cpp
TestQuad.cpp
TestRect.cpp
TestWOFF.cpp

View file

@ -6,11 +6,11 @@
#include <AK/Try.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/BitmapExport.h>
#include <LibGfx/Color.h>
#include <LibGfx/ImmutableBitmap.h>
#include <LibTest/TestCase.h>
TEST_CASE(export_to_byte_buffer)
TEST_CASE(export_bitmap_to_byte_buffer)
{
enum class Premultiplied : u8 {
Yes,
@ -229,8 +229,13 @@ TEST_CASE(export_to_byte_buffer)
bitmap->set_pixel(0, logical_y1, Color::from_bgra(subtest.source_pixels[2]));
bitmap->set_pixel(1, logical_y1, Color::from_bgra(subtest.source_pixels[3]));
auto immutable_bitmap = Gfx::ImmutableBitmap::create(bitmap);
auto result = MUST(immutable_bitmap->export_to_byte_buffer(subtest.export_format, export_flags, 2, 2));
auto result = MUST(Gfx::export_bitmap_to_byte_buffer(
*bitmap,
{},
subtest.export_format,
export_flags,
2,
2));
EXPECT_EQ(result.width, 2);
EXPECT_EQ(result.height, 2);