Image

PNG support is not built into loft — it lives in the imaging library, one of the installable packages in the loft registry. Add it to your project once with

loft install imaging

and import it at the top of any file with use imaging;. The library gives you three things: a Pixel struct (red / green / blue, each 0–255), an Image struct (name, width, height, and a flat vector<Pixel>), and the functions to move between an Image in memory and a .png file on disk.

file(path).png()        decode a PNG file into an Image (null if it cannot be read)
image.save_png(path)    encode an Image back to a PNG file (true on success)
pixel.value()           pack r,g,b into a single 0xRRGGBB integer

Because the library builds a small native (Rust) helper the first time you use it, the very first run compiles that helper — later runs are instant.

#cwd opts this program into cwd-relative paths so the bundled example file below resolves against the working directory (see 13-file). It must appear before the use line, which itself must come before any definition.

#cwd
use imaging;
fn main() {

Loading a PNG

file(path).png() reads and decodes the whole file in one call. Afterwards the width, height, file name, and every pixel are already in memory.

  img = file("tests/example/map.png").png();
  assert(img.width == 256, "width={img.width}");
  assert(img.height == 256, "height={img.height}");

img.name is just the file name, without the directory part.

  assert(img.name == "map.png", "name={img.name}");

img.data is a flat vector<Pixel> laid out row by row, so its length is always width * height.

  assert(len(img.data) == img.width * img.height, "pixel count {len(img.data)}");

The Pixel Struct

Each element of img.data is a Pixel with three channels — r, g, and b — each an integer from 0 (no intensity) to 255 (full intensity). pixel.value() packs the three channels into one 0xRRGGBB integer, handy for comparison or storage.

  first = img.data[0];
  assert(first.r >= 0 && first.r <= 255, "red channel in range: {first.r}");
  assert(first.value() == first.r * 0x10000 + first.g * 0x100 + first.b,
    "packed colour = {first.value()}");

Reading a Pixel by Coordinate

The pixel at column x, row y lives at index y * width + x. Here we read the centre pixel of the 256x256 image.

  centre = img.data[128 * img.width + 128];
  assert(centre.r >= 0 && centre.g >= 0 && centre.b >= 0, "centre pixel is valid");

Scanning Every Pixel

A for loop over img.data visits every pixel in row order. Count how many pixels are "bright" using a simple average-of-channels brightness.

  bright = 0;
  for px in img.data {
    brightness = (px.r + px.g + px.b) / 3;
    if brightness > 200 {
      bright += 1
    }
  }
  assert(bright >= 0 && bright <= len(img.data), "bright pixel count: {bright}");

Building and Saving an Image

You can also create an Image from scratch: fill a vector<Pixel>, wrap it in an Image with a matching width and height, and call save_png. Here we make a 2x2 swatch — red, green, blue, and grey — and write it to a scratch file.

  swatch = imaging::Image {
    name: "swatch",
    width: 2,
    height: 2,
    data: [
      imaging::Pixel { r: 255, g: 0,   b: 0 },
      imaging::Pixel { r: 0,   g: 255, b: 0 },
      imaging::Pixel { r: 0,   g: 0,   b: 255 },
      imaging::Pixel { r: 128, g: 128, b: 128 }
    ]
  };

Write to a scratch file in the working directory, then delete it below so the example leaves nothing behind. A cwd-relative path (with #cwd) works on every OS — a hard-coded /tmp/… would not exist on Windows.

  scratch = "loft-doc-swatch.png";
  saved = swatch.save_png(scratch);
  assert(saved, "save_png reported success");

Round-Tripping

Load the file we just wrote and confirm the pixels survived the encode/decode cycle unchanged.

  reloaded = file(scratch).png();
  assert(reloaded.width == 2 && reloaded.height == 2, "reloaded dimensions");
  assert(reloaded.data[0].r == 255 && reloaded.data[0].g == 0 && reloaded.data[0].b == 0,
    "reloaded red pixel");
  assert(reloaded.data[3].r == 128 && reloaded.data[3].g == 128 && reloaded.data[3].b == 128,
    "reloaded grey pixel");

Clean up the scratch file.

  delete(scratch);

Error Handling

png() returns null when the file does not exist or is not a decodable PNG. Always check the result with !img before you touch its fields.

  missing = file("no_such_image.png").png();
  assert(!missing, "png() of a missing file is null");
}