Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

imagine.canvas

import imagine.canvas

imagine lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled imagine.canvas.* needs import imagine.canvas.

Canvas: the drawing surface. Every shape the module can draw is a method on it.

A canvas wraps an Image’s pixel buffer rather than copying it, so drawing goes straight into the image being built. The shape methods share three primitives underneath (span fill, blend, and stroke), which is why a new shape rarely needs new native code.

Classes

Canvas

class imagine.Canvas

A mutable RGBA pixel surface and everything that draws onto one.

Canvas is the lower half of Image: it owns the pixel buffer and knows how to put colour into it, but nothing about files, formats or resizing. It is separate mostly so the drawing code can be read on its own, and it is rarely worth using directly; Image inherits all of this.

How drawing works here

Every filled shape becomes a polygon and goes through one scanline rasterizer, and every stroked shape becomes the polygon outlining its stroke and goes through the same one. That means anti-aliasing, clipping and the fill rule are implemented once rather than once per primitive, and a shape this module does not offer can be drawn by handing fill_polygon() the points for it.

Coordinates

The origin is the top-left corner. Integer coordinates fall on pixel corners, not centres, so a rectangle from (0, 0) to (10, 10) covers exactly the first ten pixels in each direction with no half-covered edge. Fractional coordinates are meaningful and are what anti-aliasing acts on.

Drawing outside the surface is not an error. Anything that falls outside is clipped away, which is what makes it safe to draw a shape that only partly overlaps the image.

Constructor

imagine.Canvas(pixels: bytes, width: number, height: number)

Wraps an existing pixel buffer.

The buffer is taken as-is and not copied, so the caller must not keep writing to it independently. Image is the supported way to get a Canvas; this constructor exists for code that already has pixels from somewhere else.

Parameters

  • pixels (bytes) — width * height * 4 bytes of RGBA.
  • width (number)
  • height (number)

Canvas.width()

imagine.Canvas.width() -> number

The width in pixels.

Returns number

Canvas.height()

imagine.Canvas.height() -> number

The height in pixels.

Returns number

Canvas.pixels()

imagine.Canvas.pixels() -> bytes

The raw RGBA pixel buffer.

This is the live buffer, not a copy: writing to it changes the image. Pixel (x, y) starts at byte (y * width + x) * 4 and runs red, green, blue, alpha, with alpha straight rather than premultiplied.

Handing this out is deliberate. Reading and writing bytes directly is far faster than a method call per pixel, so an operation this module does not provide can still be written efficiently in Zuri.

Returns bytes

Canvas.set_pixels()

imagine.Canvas.set_pixels(pixels: bytes)

Replaces the surface’s pixels with another buffer.

The buffer must hold exactly width * height * 4 bytes, and is adopted rather than copied, so whatever produced it must not keep writing to it afterwards.

The pair to pixels(): that one hands the buffer out so an operation this module does not provide can be written directly, and this one takes a buffer back. Saving a copy before a destructive filter and restoring it afterwards is the usual reason to want it.

Parameters

  • pixels (bytes)

Returns — self

Raises ImageError When the buffer is the wrong size.

Canvas.clip()

imagine.Canvas.clip(x, y, width, height)

Restricts every subsequent drawing operation to a rectangle.

The rectangle is intersected with the surface, so a clip larger than the image is the same as no clip. Passing no arguments, or calling clear_clip(), removes it.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)

Returns — self

Canvas.clear_clip()

imagine.Canvas.clear_clip()

Removes any clipping rectangle.

Returns — self

Canvas.get_clip()

imagine.Canvas.get_clip() -> ?dict

The current clipping rectangle as {x, y, width, height}, or nil when there isn’t one.

Returns ?dict

Canvas.antialias()

imagine.Canvas.antialias(enabled)

Turns anti-aliasing on or off for subsequent drawing.

On by default. Turning it off makes every edge hard, which is faster and is what you want for pixel-exact output such as barcodes, or when drawing into a mask that will be thresholded anyway.

Parameters

  • enabled (bool)

Returns — self

Canvas.thickness()

imagine.Canvas.thickness(thickness: number)

Sets the stroke width in pixels for lines and outlines.

Strokes are centred on the path, so a thickness of 4 puts 2 pixels on each side. The default is 1.

Parameters

  • thickness (number)

Returns — self

Canvas.cap()

imagine.Canvas.cap(style)

Sets how the two ends of an open stroke are finished.

CAP_ROUND (the default) ends in a half-disc, CAP_SQUARE in a square, and both reach half the stroke’s width past the endpoint. CAP_BUTT stops exactly at it.

Reach for CAP_BUTT when the coordinates have to mean exactly what they say: segments meeting end to end, a scale bar of a known length, the pieces of a dashed line. The joins between a path’s segments are always round and are not affected by this.

chart.cap(CAP_BUTT).thickness(6)
chart.line(40, 200, 40, 40, '#334155')

Parameters

  • style (string) — CAP_BUTT, CAP_ROUND or CAP_SQUARE.

Returns — self

Canvas.get_cap()

imagine.Canvas.get_cap() -> string

The line cap in force.

Returns string

Canvas.get_pixel()

imagine.Canvas.get_pixel(x, y) -> ?Color

Returns the colour at a pixel, or nil when the coordinates fall outside the image.

Clipping does not apply; a clip restricts what is written, not what can be read.

Parameters

  • x (number)
  • y (number)

Returns ?Color

Canvas.set_pixel()

imagine.Canvas.set_pixel(x, y, color)

Replaces the colour at a pixel, ignoring whatever was there.

This overwrites rather than blends, so drawing a half-transparent colour leaves a half-transparent pixel instead of compositing it over the old one. Use blend_pixel() for the compositing version, which is what every other drawing method uses.

Coordinates outside the image or outside the clip are ignored.

Parameters

  • x (number)
  • y (number)
  • color (Color|string|number)

Returns — self

Canvas.pixel()

imagine.Canvas.pixel(x, y, color)

Draws one pixel, compositing it over whatever is already there.

The single-pixel member of the drawing family, alongside line() and rect(). It is the same operation as blend_pixel(), under the name that reads correctly in a chain of drawing calls.

Coordinates outside the image or outside the clip are ignored.

Parameters

  • x (number)
  • y (number)
  • color (Color|string|number)

Returns — self

Canvas.blend_pixel()

imagine.Canvas.blend_pixel(x, y, color)

Composites a colour over the pixel already there, the way every drawing operation does.

Coordinates outside the image or outside the clip are ignored.

Parameters

  • x (number)
  • y (number)
  • color (Color|string|number)

Returns — self

Canvas.fill_polygon()

imagine.Canvas.fill_polygon(points, color, options)

Fills a polygon.

The points are a flat list of alternating x and y values, or a list of [x, y] pairs; both are accepted because both read naturally depending on where the points came from. The outline is closed automatically, so the last point does not need to repeat the first.

Self-intersecting outlines are filled by the non-zero winding rule by default, which is what fills a five-pointed star solid. Pass { even_odd: true } for the other convention, which leaves the middle of that star empty.

Parameters

  • points (list)
  • color (Color|string|number)
  • options (?dict) — even_odd (bool).

Returns — self

Canvas.polygon()

imagine.Canvas.polygon(points, color, options)

Draws a polygon’s outline, closing it back to the first point.

Parameters

  • points (list)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.polyline()

imagine.Canvas.polyline(points, color, options)

Draws a connected run of line segments without closing it.

Parameters

  • points (list)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.line()

imagine.Canvas.line(x1, y1, x2, y2, color, options)

Draws a straight line between two points.

The line is thickness() pixels wide, centred on the path, and anti-aliased unless that has been turned off.

Parameters

  • x1 (number)
  • y1 (number)
  • x2 (number)
  • y2 (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.rect()

imagine.Canvas.rect(x, y, width, height, color, options)

Draws the outline of a rectangle.

The stroke is centred on the rectangle’s edge, so half of a thick outline falls inside it and half outside.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill_rect()

imagine.Canvas.fill_rect(x, y, width, height, color)

Fills a rectangle.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)
  • color (Color|string|number)

Returns — self

Canvas.rounded_rect()

imagine.Canvas.rounded_rect(x, y, width, height, radius, color, options)

Draws the outline of a rectangle with rounded corners.

A radius larger than half the shorter side is reduced to fit, so a very large radius gives a stadium shape rather than a broken one.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)
  • radius (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill_rounded_rect()

imagine.Canvas.fill_rounded_rect(x, y, width, height, radius, color)

Fills a rectangle with rounded corners.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)
  • radius (number)
  • color (Color|string|number)

Returns — self

Canvas.circle()

imagine.Canvas.circle(cx, cy, radius, color, options)

Draws the outline of a circle.

Parameters

  • cx (number) — The centre’s x coordinate.
  • cy (number) — The centre’s y coordinate.
  • radius (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill_circle()

imagine.Canvas.fill_circle(cx, cy, radius, color)

Fills a circle.

Parameters

  • cx (number)
  • cy (number)
  • radius (number)
  • color (Color|string|number)

Returns — self

Canvas.ellipse()

imagine.Canvas.ellipse(cx, cy, rx, ry, color, options)

Draws the outline of an ellipse.

Parameters

  • cx (number) — The centre’s x coordinate.
  • cy (number) — The centre’s y coordinate.
  • rx (number) — The horizontal radius.
  • ry (number) — The vertical radius.
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill_ellipse()

imagine.Canvas.fill_ellipse(cx, cy, rx, ry, color)

Fills an ellipse.

Parameters

  • cx (number)
  • cy (number)
  • rx (number)
  • ry (number)
  • color (Color|string|number)

Returns — self

Canvas.arc()

imagine.Canvas.arc(cx, cy, rx, ry, start, end, color, options)

Draws an elliptical arc.

Angles are in degrees, measured clockwise from three o’clock, matching the direction the y axis runs. An end angle below the start angle sweeps the long way round.

Parameters

  • cx (number)
  • cy (number)
  • rx (number)
  • ry (number)
  • start (number) — The starting angle in degrees.
  • end (number) — The ending angle in degrees.
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.pie()

imagine.Canvas.pie(cx, cy, rx, ry, start, end, color)

Fills a pie slice: an arc closed back through the centre.

Parameters

  • cx (number)
  • cy (number)
  • rx (number)
  • ry (number)
  • start (number) — The starting angle in degrees.
  • end (number) — The ending angle in degrees.
  • color (Color|string|number)

Returns — self

Canvas.chord()

imagine.Canvas.chord(cx, cy, rx, ry, start, end, color)

Fills the region between an arc and its chord.

Parameters

  • cx (number)
  • cy (number)
  • rx (number)
  • ry (number)
  • start (number)
  • end (number)
  • color (Color|string|number)

Returns — self

Canvas.bezier()

imagine.Canvas.bezier(x1, y1, cx1, cy1, cx2, cy2, x2, y2, color, options)

Draws a cubic Bezier curve through four control points.

The curve starts at the first point, ends at the fourth, and is pulled toward the two in between without passing through them.

Parameters

  • x1 (number)
  • y1 (number)
  • cx1 (number) — The first control point’s x coordinate.
  • cy1 (number)
  • cx2 (number) — The second control point’s x coordinate.
  • cy2 (number)
  • x2 (number)
  • y2 (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill()

imagine.Canvas.fill(color)

Paints every pixel of the surface, ignoring the clip and whatever was there before.

This is a replace, not a blend: filling with a transparent colour empties the image rather than leaving it unchanged.

Parameters

  • color (Color|string|number)

Returns — self

Canvas.clear()

imagine.Canvas.clear()

Clears the surface to fully transparent.

Returns — self

Canvas.flood_fill()

imagine.Canvas.flood_fill(x, y, color, tolerance)

Flood fills the connected region of similar colour containing (x, y).

Similarity is measured against the colour that was at the starting point, as the largest difference across the four channels. tolerance is that difference in 0-255 units, so 0 spreads only through exactly equal pixels and 255 fills everything.

Filling is four-connected: it spreads up, down, left and right, but not diagonally, so a one-pixel diagonal line holds it back. The fill is never anti-aliased.

Parameters

  • x (number)
  • y (number)
  • color (Color|string|number)
  • tolerance (?number) — Default: 0.

Returns — self

Canvas.linear_gradient()

imagine.Canvas.linear_gradient(x1, y1, x2, y2, stops, options)

Fills a rectangle with a linear gradient running between two points.

The gradient’s direction and length are the vector from (x1, y1) to (x2, y2). Everything before the first point takes the first stop’s colour and everything past the second takes the last stop’s, so a short vector across a large area gives a hard transition with flat bands on either side.

# top to bottom, dark to light
image.linear_gradient(0, 0, 0, image.height(), ['#0f172a', '#334155'])

Parameters

  • x1 (number)
  • y1 (number)
  • x2 (number)
  • y2 (number)
  • stops (list) — Colours, or [offset, colour] pairs with the offset from 0 to 1.
  • options (?dict) — rect (a {x, y, width, height} to confine the fill to, default the whole surface), blend (bool, default false: composite over what is there rather than replacing it).

Returns — self

Canvas.radial_gradient()

imagine.Canvas.radial_gradient(cx, cy, radius, stops, options)

Fills a rectangle with a radial gradient spreading from a centre.

# a soft vignette
image.radial_gradient(cx, cy, radius, [
  [0, Color(0, 0, 0, 0)],
  [1, Color(0, 0, 0, 180)],
], { blend: true })

Parameters

  • cx (number) — The centre’s x coordinate.
  • cy (number) — The centre’s y coordinate.
  • radius (number) — Where the last stop lands.
  • stops (list) — Colours, or [offset, colour] pairs.
  • options (?dict) — rect, blend. The same as linear_gradient().

Returns — self

Canvas.draw_mask()

imagine.Canvas.draw_mask(mask: bytes, mask_width, mask_height, x, y, color)

Paints a colour through an 8-bit coverage mask.

The mask is one byte per pixel, 0 for nothing and 255 for full coverage, laid out row-major with no padding. Coverage scales the colour’s alpha, so this is how anti-aliased text and any externally rasterized shape reach the surface.

Parameters

  • mask (bytes)
  • mask_width (number)
  • mask_height (number)
  • x (number) — Where the mask’s top-left corner lands.
  • y (number)
  • color (Color|string|number)

Returns — self


2026, Richard Ore and Zuri contributors