std.image

Decode, transform and encode PNG and JPEG. This is also what backs image input to ai.

import std.image

img = image.load("photo.jpg")
print(image.width(img))
print(image.height(img))
print(image.dimensions(img))

small = image.resize(img, 512, 512)
part  = image.crop(img, 10, 10, 100, 100)

image.save(small, "thumb.png")
data = image.encode(small, "png")
img2 = image.decode(bytes_from_somewhere)

An image is a value

An image is a map: { format, width, height, bytes }, where bytes is the image encoded as format ("png" or "jpeg"). img.width reads the same number image.width(img) returns. Like any value, an image is released when nothing refers to it, and it can be sent over a channel or handed to another task.

Every operation returns a new image and leaves its input alone. load and decode keep the bytes they were given. A transform - resize, crop - stores its result as PNG, which is lossless, so a chain of steps on a JPEG loses nothing along the way; encode(img, "jpeg") converts once, at the end. encode in the image's own format hands back its bytes unchanged.

Until 0.59 an image was an integer handle that stayed in memory until image.free. free did nothing from 0.59 and was removed in 0.61; delete the call.

Feeding ai

ai "what does this chart show?" on image.load("chart.png")

A transform pipeline flows straight in, and resizing first is usually the right move:

ai "read the label" on image.resize(photo, 1024, 1024)

Images are tokens, often a great many. A 4000-pixel photograph rarely answers a question better than a 1000-pixel one and costs several times more. See Multimodal.

Formats

PNG and JPEG, in and out. PNG is lossless and right for screenshots, charts and anything with text or sharp edges; JPEG is lossy and right for photographs.

Untrusted images

Decoding attacker-supplied data means running a decoder over hostile input, and a small file can claim an enormous canvas. Ecko refuses to decode an image whose pixels would need more memory than ECKO_LIMIT_ALLOC (256 MiB by default), and refuses a resize to an output that large - both are catchable errors rather than an attempted allocation. Within that, bound what you accept:

img = image.decode(upload)
if img.width * img.height > 40000000 {
    reject("too large")
}