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:
parent
9f7f622362
commit
00181dcf50
10 changed files with 235 additions and 182 deletions
156
Libraries/LibGfx/BitmapExport.cpp
Normal file
156
Libraries/LibGfx/BitmapExport.cpp
Normal 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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
55
Libraries/LibGfx/BitmapExport.h
Normal file
55
Libraries/LibGfx/BitmapExport.h
Normal 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);
|
||||
|
||||
}
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ include(vulkan)
|
|||
set(SOURCES
|
||||
AffineTransform.cpp
|
||||
Bitmap.cpp
|
||||
BitmapExport.cpp
|
||||
BitmapSequence.cpp
|
||||
CMYKBitmap.cpp
|
||||
Color.cpp
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
Loading…
Reference in a new issue