ladybird/Libraries/LibGfx/Bitmap.cpp
InvalidUsernameException 7c315ef67f Everywhere: Unify naming of RGBA-like colors
The `Bitmap` type was referring to to its internal pixel format by a
name that represents the order of the color components as they are layed
out in memory. Contrary, the `Color` type was using a naming that where
the name represents the order of the components from most to least
significant byte when viewed as a unsigned 32bit integer. This is
confusing as you have to keep remembering which mental model to use
depending on which code you work with.

To unify the two, the naming of RGBA-like colors in the `Color` type has
been adjusted to match the one from the Bitmap type. This seems to be
generally in line with how web APIs think about these types:
* `ImageData.pixelFormat` can be `rgba-8unorm` backed by a
  `Uint8ClamedArray`, but there is no pixel format backed by a 32bit
  unsigned type.
* WebGL can use format `RGBA` with type `UNSIGNED_BYTE`, but there is no
  such format with type `UNSIGNED_INT`.

Additionally, it appears that other browsers and browser-adjacent
libraries also think similarly about these types:
* Firefox:
  https://github.com/mozilla-firefox/firefox/blob/main/gfx/2d/Types.h
* WebKit:
  https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/graphics/PixelFormat.h
* Skia:
  https://chromium.googlesource.com/skia/+/refs/heads/main/include/core/SkColorType.h

This has the not so nice side effect that APIs that interact with these
types through 32bit unsigned integers now have the component order
inverted due to little-endian byte order. E.g. specifying a color as hex
constant needs to be done as `0xAABBGGRR` if it is to be treated as
RGBA8888.

We could alleviate this by providing endian-independent APIs to callers.
But I suspect long-term we might want to think differently about bitmap
data anyway, e.g. to better support HDR in the future. However, such
changes would be more involved than just unifying the naming as done
here. So I considered that out of scope for now.
2025-11-28 18:32:48 +01:00

377 lines
14 KiB
C++

/*
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022, Timothy Slater <tslater2006@gmail.com>
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Bitmap.h>
#include <AK/Checked.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibGfx/SkiaUtils.h>
#include <core/SkBitmap.h>
#include <core/SkColorSpace.h>
#include <core/SkImage.h>
#include <core/SkImageInfo.h>
#include <core/SkPixmap.h>
#include <errno.h>
#ifdef AK_OS_MACOS
# include <Accelerate/Accelerate.h>
#endif
namespace Gfx {
struct BackingStore {
void* data { nullptr };
size_t pitch { 0 };
size_t size_in_bytes { 0 };
};
size_t Bitmap::minimum_pitch(size_t width, BitmapFormat format)
{
size_t element_size;
switch (format) {
case BitmapFormat::BGRx8888:
case BitmapFormat::BGRA8888:
case BitmapFormat::RGBx8888:
case BitmapFormat::RGBA8888:
element_size = 4;
break;
default:
VERIFY_NOT_REACHED();
}
return width * element_size;
}
static bool size_would_overflow(BitmapFormat format, IntSize size)
{
if (size.width() < 0 || size.height() < 0)
return true;
// This check is a bit arbitrary, but should protect us from most shenanigans:
if (size.width() >= INT16_MAX || size.height() >= INT16_MAX)
return true;
// In contrast, this check is absolutely necessary:
size_t pitch = Bitmap::minimum_pitch(size.width(), format);
return Checked<size_t>::multiplication_would_overflow(pitch, size.height());
}
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create(BitmapFormat format, IntSize size)
{
// For backwards compatibility, premultiplied alpha is assumed
return create(format, AlphaType::Premultiplied, size);
}
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create(BitmapFormat format, AlphaType alpha_type, IntSize size)
{
auto backing_store = TRY(Bitmap::allocate_backing_store(format, size));
return AK::adopt_nonnull_ref_or_enomem(new (nothrow) Bitmap(format, alpha_type, size, backing_store));
}
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_shareable(BitmapFormat format, AlphaType alpha_type, IntSize size)
{
if (size_would_overflow(format, size))
return Error::from_string_literal("Gfx::Bitmap::create_shareable size overflow");
auto const pitch = minimum_pitch(size.width(), format);
auto const data_size = size_in_bytes(pitch, size.height());
auto buffer = TRY(Core::AnonymousBuffer::create_with_size(round_up_to_power_of_two(data_size, PAGE_SIZE)));
auto bitmap = TRY(Bitmap::create_with_anonymous_buffer(format, alpha_type, buffer, size));
return bitmap;
}
Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, IntSize size, BackingStore const& backing_store)
: m_size(size)
, m_data(backing_store.data)
, m_pitch(backing_store.pitch)
, m_format(format)
, m_alpha_type(alpha_type)
{
VERIFY(!m_size.is_empty());
VERIFY(!size_would_overflow(format, size));
VERIFY(m_data);
VERIFY(backing_store.size_in_bytes == size_in_bytes());
m_destruction_callback = [data = m_data, size_in_bytes = this->size_in_bytes()] {
kfree_sized(data, size_in_bytes);
};
}
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_wrapper(BitmapFormat format, AlphaType alpha_type, IntSize size, size_t pitch, void* data, Function<void()>&& destruction_callback)
{
if (size_would_overflow(format, size))
return Error::from_string_literal("Gfx::Bitmap::create_wrapper size overflow");
return adopt_ref(*new Bitmap(format, alpha_type, size, pitch, data, move(destruction_callback)));
}
Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, IntSize size, size_t pitch, void* data, Function<void()>&& destruction_callback)
: m_size(size)
, m_data(data)
, m_pitch(pitch)
, m_format(format)
, m_alpha_type(alpha_type)
, m_destruction_callback(move(destruction_callback))
{
VERIFY(pitch >= minimum_pitch(size.width(), format));
VERIFY(!size_would_overflow(format, size));
// FIXME: assert that `data` is actually long enough!
}
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_with_anonymous_buffer(BitmapFormat format, AlphaType alpha_type, Core::AnonymousBuffer buffer, IntSize size)
{
if (size_would_overflow(format, size))
return Error::from_string_literal("Gfx::Bitmap::create_with_anonymous_buffer size overflow");
return adopt_nonnull_ref_or_enomem(new (nothrow) Bitmap(format, alpha_type, move(buffer), size));
}
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_with_raw_data(BitmapFormat format, AlphaType alpha_type, ReadonlyBytes raw_data, IntSize size)
{
if (size_would_overflow(format, size))
return Error::from_string_literal("Gfx::Bitmap::create_with_raw_data size overflow");
auto backing_store = TRY(Bitmap::allocate_backing_store(format, size, InitializeBackingStore::No));
raw_data.copy_to(Bytes { backing_store.data, backing_store.size_in_bytes });
return AK::adopt_nonnull_ref_or_enomem(new (nothrow) Bitmap(format, alpha_type, size, backing_store));
}
Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, Core::AnonymousBuffer buffer, IntSize size)
: m_size(size)
, m_data(buffer.data<void>())
, m_pitch(minimum_pitch(size.width(), format))
, m_format(format)
, m_alpha_type(alpha_type)
, m_buffer(move(buffer))
{
VERIFY(!size_would_overflow(format, size));
}
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> Bitmap::clone() const
{
auto new_bitmap = TRY(Bitmap::create(format(), alpha_type(), size()));
VERIFY(size_in_bytes() == new_bitmap->size_in_bytes());
memcpy(new_bitmap->scanline(0), scanline(0), size_in_bytes());
return new_bitmap;
}
void Bitmap::apply_mask(Gfx::Bitmap const& mask, MaskKind mask_kind)
{
VERIFY(size() == mask.size());
for (int y = 0; y < height(); y++) {
for (int x = 0; x < width(); x++) {
auto color = get_pixel(x, y);
auto mask_color = mask.get_pixel(x, y);
if (mask_kind == MaskKind::Luminance) {
color = color.with_alpha(color.alpha() * mask_color.alpha() * mask_color.luminosity() / (255 * 255));
} else {
VERIFY(mask_kind == MaskKind::Alpha);
color = color.with_alpha(color.alpha() * mask_color.alpha() / 255);
}
set_pixel(x, y, color);
}
}
}
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> Bitmap::cropped(Gfx::IntRect crop, Gfx::Color outside_color) const
{
// OPTIMIZATION: Skip slow manual copying for NO-OP crops
if (crop == rect())
return clone();
auto new_bitmap = TRY(Gfx::Bitmap::create(format(), alpha_type(), { crop.width(), crop.height() }));
for (int y = 0; y < crop.height(); ++y) {
for (int x = 0; x < crop.width(); ++x) {
int global_x = x + crop.left();
int global_y = y + crop.top();
if (global_x >= width() || global_y >= height() || global_x < 0 || global_y < 0) {
new_bitmap->set_pixel(x, y, outside_color);
} else {
new_bitmap->set_pixel(x, y, get_pixel(global_x, global_y));
}
}
}
return new_bitmap;
}
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::scaled(int const width, int const height, ScalingMode const scaling_mode) const
{
auto const source_info = SkImageInfo::Make(this->width(), this->height(), to_skia_color_type(format()), to_skia_alpha_type(format(), alpha_type()), nullptr);
SkPixmap const source_sk_pixmap(source_info, begin(), pitch());
SkBitmap source_sk_bitmap;
source_sk_bitmap.installPixels(source_sk_pixmap);
source_sk_bitmap.setImmutable();
auto scaled_bitmap = TRY(Gfx::Bitmap::create(format(), alpha_type(), { width, height }));
auto const scaled_info = SkImageInfo::Make(scaled_bitmap->width(), scaled_bitmap->height(), to_skia_color_type(scaled_bitmap->format()), to_skia_alpha_type(scaled_bitmap->format(), scaled_bitmap->alpha_type()), nullptr);
SkPixmap const scaled_sk_pixmap(scaled_info, scaled_bitmap->begin(), scaled_bitmap->pitch());
sk_sp<SkImage> source_sk_image = source_sk_bitmap.asImage();
if (!source_sk_image->scalePixels(scaled_sk_pixmap, to_skia_sampling_options(scaling_mode)))
return Error::from_string_literal("Unable to scale pixels for bitmap");
return scaled_bitmap;
}
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::to_bitmap_backed_by_anonymous_buffer() const
{
if (m_buffer.is_valid()) {
// FIXME: The const_cast here is awkward.
return NonnullRefPtr { const_cast<Bitmap&>(*this) };
}
auto buffer = TRY(Core::AnonymousBuffer::create_with_size(round_up_to_power_of_two(size_in_bytes(), PAGE_SIZE)));
auto bitmap = TRY(Bitmap::create_with_anonymous_buffer(format(), alpha_type(), move(buffer), size()));
memcpy(bitmap->scanline(0), scanline(0), size_in_bytes());
return bitmap;
}
Bitmap::~Bitmap()
{
if (m_destruction_callback)
m_destruction_callback();
m_data = nullptr;
}
void Bitmap::strip_alpha_channel()
{
VERIFY(m_format == BitmapFormat::BGRA8888 || m_format == BitmapFormat::BGRx8888);
for (BGRA8888& pixel : *this)
pixel = 0xff000000 | (pixel & 0xffffff);
m_format = BitmapFormat::BGRx8888;
}
Gfx::ShareableBitmap Bitmap::to_shareable_bitmap() const
{
auto bitmap_or_error = to_bitmap_backed_by_anonymous_buffer();
if (bitmap_or_error.is_error())
return {};
return Gfx::ShareableBitmap { bitmap_or_error.release_value_but_fixme_should_propagate_errors(), Gfx::ShareableBitmap::ConstructWithKnownGoodBitmap };
}
ErrorOr<BackingStore> Bitmap::allocate_backing_store(BitmapFormat format, IntSize size, InitializeBackingStore initialize_backing_store)
{
if (size.is_empty())
return Error::from_string_literal("Gfx::Bitmap backing store size is empty");
if (size_would_overflow(format, size))
return Error::from_string_literal("Gfx::Bitmap backing store size overflow");
auto const pitch = minimum_pitch(size.width(), format);
auto const data_size_in_bytes = size_in_bytes(pitch, size.height());
void* data;
if (initialize_backing_store == InitializeBackingStore::Yes)
data = kcalloc(1, data_size_in_bytes);
else
data = kmalloc(data_size_in_bytes);
if (data == nullptr)
return Error::from_errno(errno);
return BackingStore { data, pitch, data_size_in_bytes };
}
Bitmap::DiffResult Bitmap::diff(Bitmap const& other) const
{
auto own_width = width();
auto own_height = height();
VERIFY(own_width == other.width() && own_height == other.height());
DiffResult result;
for (auto y = 0; y < own_height; ++y) {
for (auto x = 0; x < own_width; ++x) {
auto own_pixel = get_pixel(x, y);
auto other_pixel = other.get_pixel(x, y);
if (own_pixel == other_pixel)
continue;
++result.pixel_error_count;
u8 red_error = abs(static_cast<int>(own_pixel.red()) - other_pixel.red());
u8 green_error = abs(static_cast<int>(own_pixel.green()) - other_pixel.green());
u8 blue_error = abs(static_cast<int>(own_pixel.blue()) - other_pixel.blue());
u8 alpha_error = abs(static_cast<int>(own_pixel.alpha()) - other_pixel.alpha());
result.total_red_error += red_error;
result.total_green_error += green_error;
result.total_blue_error += blue_error;
result.total_alpha_error += alpha_error;
result.maximum_red_error = max(result.maximum_red_error, red_error);
result.maximum_green_error = max(result.maximum_green_error, green_error);
result.maximum_blue_error = max(result.maximum_blue_error, blue_error);
result.maximum_alpha_error = max(result.maximum_alpha_error, alpha_error);
}
}
result.identical = result.pixel_error_count == 0;
result.total_error = result.total_red_error + result.total_green_error + result.total_blue_error + result.total_alpha_error;
u8 maximum_red_green_error = max(result.maximum_red_error, result.maximum_green_error);
u8 maximum_blue_alpha_error = max(result.maximum_blue_error, result.maximum_alpha_error);
result.maximum_error = max(maximum_red_green_error, maximum_blue_alpha_error);
return result;
}
void Bitmap::set_alpha_type_destructive(AlphaType alpha_type)
{
if (alpha_type == m_alpha_type)
return;
if (m_format == BitmapFormat::BGRx8888 || m_format == BitmapFormat::RGBx8888) {
m_alpha_type = alpha_type;
return;
}
#ifdef AK_OS_MACOS
vImage_Buffer buf { .data = m_data, .height = vImagePixelCount(height()), .width = vImagePixelCount(width()), .rowBytes = pitch() };
vImage_Error err;
if (m_alpha_type == AlphaType::Unpremultiplied) {
switch (m_format) {
case BitmapFormat::BGRA8888:
err = vImagePremultiplyData_BGRA8888(&buf, &buf, kvImageNoFlags);
break;
case BitmapFormat::RGBA8888:
err = vImagePremultiplyData_RGBA8888(&buf, &buf, kvImageNoFlags);
break;
default:
VERIFY_NOT_REACHED();
}
} else {
switch (m_format) {
case BitmapFormat::BGRA8888:
err = vImageUnpremultiplyData_BGRA8888(&buf, &buf, kvImageNoFlags);
break;
case BitmapFormat::RGBA8888:
err = vImageUnpremultiplyData_RGBA8888(&buf, &buf, kvImageNoFlags);
break;
default:
VERIFY_NOT_REACHED();
}
}
VERIFY(err == kvImageNoError);
#else
auto color_type = to_skia_color_type(m_format);
auto source_alpha = to_skia_alpha_type(m_format, m_alpha_type);
auto destination_alpha = to_skia_alpha_type(m_format, alpha_type);
auto color_space = SkColorSpace::MakeSRGB();
auto source_info = SkImageInfo::Make(width(), height(), color_type, source_alpha, color_space);
auto destination_info = SkImageInfo::Make(width(), height(), color_type, destination_alpha, color_space);
SkPixmap src_pixmap(source_info, m_data, pitch());
SkPixmap dst_pixmap(destination_info, m_data, pitch());
bool ok = src_pixmap.readPixels(dst_pixmap);
VERIFY(ok);
#endif
m_alpha_type = alpha_type;
}
}