ECMAScript Embedded Bitmap Encoding

ECMAScript Embedded Bitmap Encoding

I propose the following bitmap format, suitable for embedding small images in TypeScript or JavaScript source code:

// ECMAScript Embedded Bitmap Encoding (EEBE)
// Required fields:
export const lines = [
  0b0001000,
  0b0111000,
  0b1101000,
  0b1001000,
  0b0001000,
  0b0001110,
  0b0001111,
  0b0001111,
  0b0000110,
]
export const width = 7
export const bpp = 1
// Optional fields:
export const palette = [
  0x000,
  0xfff,
]

Let's call it ECMAScript Embedded Bitmap Encoding (EEBE). EEBE is a format used to store bitmap images within ECMAScript code in an efficient manner. It is designed to be compact and easy to parse, suitable for scenarios with hard size constraints such as js13kGames, and isn't at all intended to be a general purpose image format.

An EEBE file is an ECMAScript module that exports the following fields:

  • lines: an array of integers, each representing a scanline of the image. Each integer is a bitfield of size width * bpp, with the least significant bit(s) representing the leftmost pixel of the scanline.

  • width: the width of the image in pixels.

  • bpp: the number of bits per pixel.

  • palette: an optional array of integers, each representing a color. The number of colors in the palette should equal 2 ** bpp. Nullish values are treated as transparent. If this field is omitted, the rendering is implementation-defined.

  • Any other fields, depending on the implementation.

Optimized for size, the above example is 116 bytes long (99 bytes gzipped) and produces the following image:

EEBE Playground

// This is the same example as above, written in a compact form.
export const lines=[8,56,104,72,8,14,15,15,6]
export const width=7
export const bpp=1
export const palette=[0,4095]

The footprint can be further reduced during build:

  • Using a module bundler such as Rollup will eliminate the export statements.
  • Running a minifier (e.g. Terser) will shorten the field names and inline some of the constants.

It's also possible to save bytes by using 12-bit color (#9d5 instead of #99dd55) and sharing the palette between multiple images.

Decoding the image

An EEBE image can be decoded as follows:

function readBitmap(lines, width, bpp, readFunction) {
  const shift = 1 << bpp
  for (let y = 0; y < lines.length; ++y) {
    for (let x = 0; x < width; ++x) {
      const value = lines[y] / shift ** x & shift - 1
      readFunction(x, y, value)
    }
  }
}

(This function is available in natlib.)

For each pixel in the image, the readFunction(x, y, value) is invoked. The pixel's value is an integer in the range [0, 2 ** bpp - 1] that can be used to retrieve the corresponding color from the palette.

You might be wondering why the readBitmap function uses the division operator instead of a << bit shift. It's because the bitwise operations in JS truncate their operands to 32 bits, whereas the Number type is a 64-bit floating point, allowing for integer values of up to 53 bits. So the tradeoff here is that we can either

  • Enjoy the full 53-bit stride for images, but use the (slower) division operator, or
  • Use the fast << bit shift operator, but only get 32 bits of stride.

Real-world usage

I've used this format in my js13kGames entries, including The Neatness, with good results.

It's clear that variations of this — storing bitmaps as int[] — have been in use since at least ZX Spectrum days. The motivation for writing this short spec and giving EEBE its name is to promote interoperability, not to claim originality.

Questions

Absolutely none of these questions have been asked.

What about extensibility?

You can include extra fields. For example, in The Neatness levels contain hotspots (entry and exit points) in addition to the level geometry represented by the bitmap.

Where's the tooling?

It's in the planning phase.

How to pronounce "EEBE"?

Try [jebi].

Why did the chicken cross the road?

To gather intelligence. It was a Chinese surveillance chicken.