imagine.canvas
import imagine.canvas
imaginelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledimagine.canvas.*needsimport 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 * 4bytes 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_ROUNDorCAP_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 aslinear_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