gamut 2.0.5

image library


To use this package, run the following command in your project's root directory:

Manual usage
Put the following dependency into your project's dependences section:

Gamut

Gamut (DUB package: gamut) is an image decoding/encoding library for D.

It's design is inspired by the FreeImage design, where the Image concept is monomorphic and can do it all.

Gamut tries to have the fastest and most memory-conscious image decoders available in pure D code. It is nothrow @nogc @safe for usage in -betterC and in disabled-runtime D.

Decoding

  • PNG: 8-bit and 16-bit, L/LA/RGB/RGBA
  • JPEG: 8-bit, L/RGB/RGBA, baseline and progressive
  • QOI: 8-bit, RGB/RGBA
  • QOIX: 8-bit, 10-bit, L/LA/RGB/RGBA. This is still an evolving format, specific to Gamut, that embeds some developments in the QOI family of formats.

Encoding

  • PNG. 8-bit, RGB/RGBA
  • JPEG: 8-bit, greyscale/RGB, baseline
  • QOI: 8-bit, RGB/RGBA
  • QOIX: 8-bit, 10-bit, L/LA/RGB/RGBA
  • DDS: BC7 encoded, 8-bit, RGB/RGBA

Changelog

  • v2.x.y QOIX bitstream changed. Ways to disown and deallocate image allocation pointer.
  • v1.x.y Initial release.

Why QOIX?

Our benchmark results for 8-bit color images:

Codecdecode mppsencode mppsbit-per-pixel
PNG (stb)89.7314.3410.29693
QOI201.9150.810.35162
QOIX179.0125.07.93607
  • QOIX and QOI generally outperforms PNG in decoding speed and encoding speed.
  • QOIX outperforms QOI in compression efficiency at the cost of speed:
  • because it's based upon better intra predictors
  • because it is followed by LZ4, which removes some of the QOI worst cases.
  • QOIX adds support for 8-bit greyscale and greyscale + alpha images, with a "QOI-plane" custom codec.
  • QOIX adds support for 10-bit images, with a "QOI-10b" custom codec. It drops the last 6 bits of precision (lossy) to outperform PNG 16-bit in every way for some use cases.

 

 

Gamut API documentation

1. Image basics

Key concept: The Image struct is where most of the public API resides.

1.1 Get the dimensions of an image:

  Image image = Image(800, 600);
  int w = image.width();
  int h = image.height();
  assert(w == 800 && h == 600);

1.2 Get the pixel format of an image:

  Image image = Image(800, 600);
  PixelType type = image.type();
  assert(type == PixelType.rgba8); // rgba8 is default if not provided

Key concept: PixelType completely describes the pixel format, for example PixelType.rgb8 is a 24-bit format with one byte for red, green and blue components each (in that order). Nothing is specified about the color space though.

Here are the possible PixelType:

  enum PixelType
  {
      l8,
      l16,
      lf32,
      la8,
      la16,
      laf32,
      rgb8, 
      rgb16,
      rgbf32,
      rgba8,
      rgba16,
      rgbaf32
  }

For now, all pixels format have one to four components:

  • 1 component is implicitely Greyscale
  • 2 components is implicitely Greyscale + alpha
  • 3 components is implicitely Red + Green + Blue
  • 4 components is implicitely Red + Green + Blue + Alpha

**Bit-depth:** Each of these components can be represented in 8-bit, 16-bit, or 32-bit floating-point (0.0f to 1.0f range).

1.3 Create an image:

Different ways to create an Image:

  • create() or regular constructor this() creates a new owned image filled with zeros.
  • createNoInit() or setSize() creates a new owned uninitialized image.
  • createViewFromData() creates a view into existing data.
  • createNoData() creates a new image with no data pointed to (still has a type, size...).
  // Create with zero initialization.
  Image image = Image(640, 480, PixelType.rgba8); 
  image.create(640, 480, PixelType.rgba8);

  // Create with no initialization.
  image.setSize(640, 480, PixelType.rgba8);
  image.createNoInit(640, 480, PixelType.rgba8);

  // Create view into existing data.
  image.createViewFromData(data.ptr, w, h, PixelType.rgb8, pitchbytes);
  • At creation time, the Image forgets about its former life, and leaves any isError() state or former data/type
  • Image.init is in isError() state
  • isValid() can be used instead of !isError()
  • Being valid == not being error == having a PixelType

 

 

2. Loading and saving an image

2.1 Load an `Image` from a file:

Another way to create an Image is to load an encoded image.

  Image image;
  image.loadFromFile("logo.png");
  if (image.isError)
      throw new Exception(image.errorMessage);

You can then read width(), height(), type(), etc...

There is no exceptions in Gamut. Instead the Image itself has an error API:

  • bool errored() return true if the Image is in an error state. In an error state, the image can't be used anymore until recreated (for example, loading another file).
  • const(char)[] errorMessage() is then available, and is guaranteed to be zero-terminated.

2.2 Load an image from memory:

  auto pngBytes = cast(const(ubyte)[]) import("logo.png"); 
  Image image;
  image.loadFromMemory(pngBytes);
  if (!image.isValid) 
      throw new Exception(image.errorMessage());

Key concept: You can force the loaded image to be a certain type using LoadFlags.

Here are the possible LoadFlags:

  LOAD_NORMAL      // Default: preserve type from original.
  
  LOAD_ALPHA       // Force one alpha channel.
  LOAD_NO_ALPHA    // Force zero alpha channel.
  
  LOAD_GREYSCALE   // Force greyscale.
  LOAD_RGB         // Force RGB values.
  
  LOAD_8BIT        // Force 8-bit `ubyte` per component.
  LOAD_16BIT       // Force 16-bit `ushort` per component.
  LOAD_FP32        // Force 32-bit `float` per component.

Example:

  Image image;  
  image.loadFromMemory(pngBytes, LOAD_RGB | LOAD_ALPHA | LOAD_8BIT);  // force PixelType.rgba8 

Not all load flags are compatible, for example LOAD_8BIT and LOAD_16BIT.

2.3 Save an image to a file:

  Image image;
  if (!image.saveToFile("output.png"))
      throw new Exception("Writing output.png failed");

Key concept: ImageFormat is simply the codecs/containers files Gamut encode and decodes to.

  enum ImageFormat
  {
      unknown,
      JPEG,
      PNG,
      QOI,
      QOIX,
      DDS
  }

This can be used to avoid inferring the output format from the filename:

  Image image;
  if (!image.saveToFile(ImageFormat.PNG, "output.png"))
      throw new Exception("Writing output.png failed");

2.4 Save an image to memory:

  Image image;
  ubyte[] qoixEncoded = image.saveToMemory(ImageFormat.QOIX);
  scope(exit) freeEncodedImage(qoix_encoded);

The returned slice must be freed up with freeEncodedImage.

 

 

3. Accessing image pixels

3.1 Get the row pitch, in bytes:

  int pitch = image.pitchInBytes();

Key concept: The image pitch is the distance between the start of two consecutive scanlines, in bytes.

This pitch can be negative.

3.2 Access a row of pixels:

  void* scan = image.scanptr(y);  // get pointer to start of pixel row
  void[] row = image.scanline(y); // get slice of pixel row

Key concept: The scanline is void* because the type it points to depends upon the PixelType. In a given scanline, the bytes scan[0..abs(pitchInBytes())] are all accessible, even if they may be outside of the image (trailing pixels, gap bytes for alignment, border pixels).

3.3 Iterate on pixels:

  assert(image.type == PixelType.rgba16);
  assert(image.hasData());
  for (int y = 0; y < image.height(); ++y)
  {
      ushort* scan = cast(ushort*) image.scanptr(y);
      for (int x = 0; x < image.width(); ++x)
      {
          ushort r = scanline[4*x + 0];
          ushort g = scanline[4*x + 1];
          ushort b = scanline[4*x + 2];
          ushort a = scanline[4*x + 3];
      }
  }

Key concept: The default is that you do not access pixels in a contiguous manner. See 4. for layout constraints that allow you to get all pixels at once.

 

 

4. Layout constraints

One of the most interesting feature of Gamut! Images in Gamut can follow given constraints over the data layout.

Key concept: LayoutConstraint are carried by images all their /

life.

Example:

  // Do nothing in particular.
  LayoutConstraint constraints = LAYOUT_DEFAULT; // 0 = default

  // Layout can be given directly at image creation or afterwards.
  Image image;  
  image.loadFromMemory(pngBytes, constraints); 

  // Now the image has a 1 pixel border (at least).
  image.changeLayout(LAYOUT_BORDER_1);
  
  // Those layout constraints are preserved 
  // (but: not the excess bytes content, if reallocated)
  image.convertToGreyscale();
  assert(image.layoutConstraints() == LAYOUT_BORDER_1);   

Important: Layout constraints are about the minimum guarantee you want. Your image may be more constrained than that in practice, but you can't rely on that.

  • If you don't specify LAYOUT_VERT_STRAIGHT, you should expect your image to be possibly stored upside-down, and account for that possibility.
  • If you don't specify LAYOUT_SCANLINE_ALIGNED_16, you should not expect your scanlines to be aligned on 16-byte boundaries, even though that can happen accidentally.

Beware not to accidentally reset constraints when resizing:

// If you do not provide layout constraints, 
// the one choosen is 0, the most permissive.
image.setSize(640, 480, PixelType.rgba8, LAYOUT_TRAILING_3);

4.1 Scanline alignment

Scanline alignment guarantees minimum alignment of each scanline.

LAYOUT_SCANLINE_ALIGNED_1 = 0
LAYOUT_SCANLINE_ALIGNED_2
LAYOUT_SCANLINE_ALIGNED_4
LAYOUT_SCANLINE_ALIGNED_8
LAYOUT_SCANLINE_ALIGNED_16
LAYOUT_SCANLINE_ALIGNED_32
LAYOUT_SCANLINE_ALIGNED_64
LAYOUT_SCANLINE_ALIGNED_128

4.2 Layout multiplicity

Multiplicity guarantees access to pixels 1, 2, 4 or 8 at a time. It does this with excess pixels at the end of the scanline, but they need not exist if the scanline has the right width.

LAYOUT_MULTIPLICITY_1 = 0
LAYOUT_MULTIPLICITY_2
LAYOUT_MULTIPLICITY_4
LAYOUT_MULTIPLICITY_8

Together with scanline alignment, this allow processing a scanline using aligned SIMD without processing the last few pixels differently.

4.3 Trailing pixels

Trailing pixels gives you up to 7 excess pixels after each scanline.

LAYOUT_TRAILING_0 = 0
LAYOUT_TRAILING_1
LAYOUT_TRAILING_3
LAYOUT_TRAILING_7

Allows unaligned SIMD access by itself.

4.4 Pixel border

Border gives you up to 3 excess pixels around an image, eg. for filtering.

LAYOUT_BORDER_0 = 0
LAYOUT_BORDER_1
LAYOUT_BORDER_2
LAYOUT_BORDER_3

4.5 Forcing pixels to be upside down or straight

Vertical constraint force the image to be store in a certain vertical direction (by default: any).

LAYOUT_VERT_FLIPPED
LAYOUT_VERT_STRAIGHT

4.6 Gapless pixel access

The Gapless constraint force the image to have contiguous scanlines without excess bytes.

LAYOUT_GAPLESS

If you have both LAYOUT_GAPLESS and LAYOUT_VERT_STRAIGHT, then you can access a slice of all pixels at once, with the ubyte[] allPixelsAtOnce() method.

  image.setSize(640, 480, PixelType.rgba8, LAYOUT_GAPLESS | LAYOUT_VERT_STRAIGHT);
  ubyte[] allpixels = image.allPixelsAtOnce(y);

 

 

5. Geometric transforms

Gamut provides a few geometric transforms.

Image image;
image.flipHorizontally(); // Flip image pixels horizontally.
image.flipVertically();   // Flip image vertically (pixels or logically)
Dependencies:
intel-intrinsics
Versions:
2.5.0 2024-Jan-28
2.4.0 2024-Jan-21
2.3.3 2023-Dec-28
2.3.2 2023-Dec-28
2.3.1 2023-Dec-25
Show all 38 versions
Download Stats:
  • 12 downloads today

  • 87 downloads this week

  • 414 downloads this month

  • 7410 downloads total

Score:
2.0
Short URL:
gamut.dub.pm