docs

Working with Bitmaps

Create pixel buffers, access raw pixels, and copy, resize, or export bitmaps to PNG.

On this page

Every pixel buffer across the library is represented as a Bitmap. You use bitmaps to read CPU-rendered views, pass images to the page, supply textures to a GPU driver, and save screenshots.

Bitmaps are reference-counted, so you hold them in ref-pointers.

Creating a Bitmap

To allocate a new bitmap, call Bitmap::Create() with the width, height, and pixel format.

C++
#include <Ultralight/Ultralight.h>

using namespace ultralight;

RefPtr<Bitmap> bitmap;

void CreateBitmap() {
  ///
  /// Allocate a 256 by 256 BGRA bitmap. The pixels are allocated but not
  /// initialized, so fill them before you read them.
  ///
  bitmap = Bitmap::Create(256, 256, BitmapFormat::BGRA8_UNORM_SRGB);
}

The allocated pixels are uninitialized— call Bitmap::Erase() to clear them to zero before reading or drawing.

Neither dimension can exceed Bitmap::MaxDimension (16,384 pixels). If either dimension exceeds this limit, Bitmap::Create() returns nullptr.

Wrapping Existing Memory

To wrap an existing pixel buffer without copying, pass the pointer, row pitch, byte size, and false for should_copy to Bitmap::Create().

C++
RefPtr<Bitmap> WrapEngineBuffer(void* pixels, uint32_t width, uint32_t height,
                                uint32_t row_bytes) {
  ///
  /// Use the engine's buffer in place (no copy). We still own the memory.
  ///
  return Bitmap::Create(width, height, BitmapFormat::BGRA8_UNORM_SRGB,
                        row_bytes, pixels, row_bytes * height, false);
}

By default, Bitmap::Create() copies pixel data into new storage. When should_copy is false, the bitmap uses the buffer in place.

Alternatively, you can provide a DestroyBitmapCallback and user data pointer so the library calls back when the bitmap is destroyed.

đźš§ Lifetime of Wrapped Buffers

When wrapping memory with should_copy = false, the buffer must remain valid for the entire lifetime of the Bitmap. You retain ownership and must free the memory after the bitmap is destroyed.

Choosing a Pixel Format

You select a pixel format from the BitmapFormat enum when creating or wrapping a bitmap.

Format Details
BitmapFormat::BGRA8_UNORM_SRGB 32-bit color with 8 bits per channel in sRGB space and premultiplied linear alpha. This is the primary format used across the library— every Surface and screenshot uses it.
BitmapFormat::A8_UNORM 8-bit alpha channel with no color data. Used for mask textures received by the GPU driver.
BitmapFormat::BC1_UNORM, BitmapFormat::BC2_UNORM, BitmapFormat::BC3_UNORM, BitmapFormat::BC7_UNORM Block-compressed GPU texture formats, available in the Pro edition or higher. These formats store pixels in 4x4 blocks and do not support per-pixel operations. See Compressed Textures.

Reading and Writing Pixels

To inspect or modify raw pixel data, lock the buffer using Bitmap::LockPixelsSafe().

C++
void FillWithPurple() {
  ///
  /// Lock the pixels. The guard unlocks them when it goes out of scope.
  ///
  auto pixels = bitmap->LockPixelsSafe();
  if (!pixels || !pixels.data())
    return;

  ///
  /// Walk the rows. Rows are row_bytes() apart, which may be more than
  /// width() * bpp(), so never assume they are packed.
  ///
  uint8_t* row = static_cast<uint8_t*>(pixels.data());
  for (uint32_t y = 0; y < bitmap->height(); ++y) {
    uint8_t* pixel = row;
    for (uint32_t x = 0; x < bitmap->width(); ++x) {
      pixel[0] = 128; // blue
      pixel[1] = 0;   // green
      pixel[2] = 128; // red
      pixel[3] = 255; // alpha
      pixel += bitmap->bpp();
    }
    row += bitmap->row_bytes();
  }
}

Rows may contain padding due to memory alignment rules. Always advance across rows using Bitmap::row_bytes() and step pixels using Bitmap::bpp() rather than assuming rows are tightly packed.

If a lock must outlive the current scope, call Bitmap::LockPixels() and Bitmap::UnlockPixels() manually.

đźš§ Thread Safety with Wrapped Memory

The library takes an internal lock only when the Bitmap owns its pixel buffer. If the bitmap wraps external memory through should_copy = false or a destruction callback, it performs no locking— you must synchronize access across threads.

Copying, Resizing, and Converting

The library provides operations to blit rectangles between bitmaps, scale image dimensions, and convert color channels or alpha formats.

Copying Between Bitmaps

To copy a rectangular region from one bitmap into another, call Bitmap::DrawBitmap().

C++
void CopyIntoAtlas(RefPtr<Bitmap> atlas, RefPtr<Bitmap> sprite) {
  ///
  /// Draw the whole sprite into the top-left corner of the atlas.
  ///
  IntRect dest = { 0, 0, static_cast<int>(sprite->width()),
                   static_cast<int>(sprite->height()) };
  atlas->DrawBitmap(sprite->bounds(), dest, sprite, false);
}

The source and destination formats do not need to match— Bitmap::DrawBitmap() converts between formats automatically.

Resizing a Bitmap

To resize a bitmap, call Bitmap::Resample() with a destination bitmap allocated at the target size.

C++
RefPtr<Bitmap> MakeThumbnail(RefPtr<Bitmap> source) {
  ///
  /// Resample() fills a destination we allocate at the target size.
  ///
  RefPtr<Bitmap> thumbnail =
      Bitmap::Create(64, 64, BitmapFormat::BGRA8_UNORM_SRGB);
  if (!source->Resample(thumbnail, true))
    return nullptr;

  return thumbnail;
}

Pass true for high_quality to use smooth filtering, or false for fast nearest-neighbor sampling.

Both bitmaps must use BitmapFormat::BGRA8_UNORM_SRGB— Bitmap::Resample() returns false if either bitmap uses another format.

Converting Channel Order and Alpha

These operations modify pixels in place and require BitmapFormat::BGRA8_UNORM_SRGB.

Saving to PNG

To write a bitmap to disk or encode it as a memory buffer, call Bitmap::WritePNG() or Bitmap::EncodePNG().

C++
void SavePurple() {
  ///
  /// Write to disk. The defaults convert BGRA premultiplied to RGBA straight.
  ///
  bitmap->WritePNG("purple.png");

  ///
  /// Or keep the encoded bytes in memory instead.
  ///
  RefPtr<Buffer> png = bitmap->EncodePNG();
  if (png) {
    // png->data() holds png->size() bytes of PNG.
  }
}

The PNG standard expects straight-alpha RGBA pixels. By default, both methods automatically convert from premultiplied BGRA.

Both functions accept optional flags to toggle channel swapping and alpha conversion. Keep the default true values unless the bitmap already contains straight-alpha RGBA data.