bounds
Mojo module 🡭
bounds
A DrawTarget that draws nothing and remembers where the ink would have gone: BoundsTarget.
A caller generic over the trait renders its scene into one of these,
reads the box back, and then renders for real into a target sized to
that box – the bbox_inches="tight" of a figure export, on every
backend. It exists because nothing else can answer the question. A
raster Canvas can be scanned for non-background pixels, but
SvgCanvas and PdfCanvas hold markup and operators with nothing to
scan, and the extents that matter are per-primitive knowledge this
package holds and a consumer would otherwise reproduce by hand: a
stroke’s box is its outline with the width, caps and joins
(Path.stroke_bounds), an arc’s needs the flattening the fill uses,
text’s needs the font metrics and the layout draw_text performs
(measure_text_block) (#460).
It is the same idea measure_text is – a layout you can ask about
without drawing – and it lives beside the three drawing backends as a
fourth conformer rather than as a running box inside each of them, so
no existing backend gains an invariant to keep, only callers that ask
pay, and a primitive added to the trait later fails to compile here
rather than silently contributing no extent.
What the box is
Two readings of one union, both in the target’s own coordinates after the transform in force at each call:
ink_bounds()is geometric: the smallest axis-aligned box, inFloat64, around every shape’s outline as the fills flatten it.ink_pixels()is that box as whole pixels: every pixel whose square[k - 0.5, k + 0.5]the geometry enters, which is the set an anti-aliased edge can give coverage to. The rule is stated once, in_PIXEL_HALF, andtests/test_bounds.mojopins it against a raster scan of the same scene: the scan is a subset of this box, and this box is at most one pixel wider on any side. Erring outward is the right direction for a crop, since a box that excludes a barely-inked pixel shaves the edge it was cropping to.
has_ink() tells a scene with nothing drawn from one with a box at
the origin. Both readings are clipped to the page – BoundsTarget(w, h) inks nothing outside [0, w) x [0, h) – and to any push_clip
in force, so a shape drawn wholly outside a clip contributes nothing.
What is exact and what is generous
Under no transform, a translation, or an axis-aligned scale every
primitive’s box is the box the raster backend inks: rectangles snap
the way fill_rect snaps, disks and wedges flatten the way the fills
flatten, strokes are measured on their own outline, text on its
laid-out block. Under a rotation or a skew three things are measured
as the bounding box of a mapped bounding box, which is never smaller
than the ink and can be larger: a stroke under a skew, a text block
under any rotation of the canvas (the block’s own rotation is
exact), and a rectangular clip. A chart’s tight crop is an unrotated
page, so this is the case that matters least.
Blend mode and color space are carried for save/restore and
otherwise ignored: CLEAR or DESTINATION_OUT still count as ink,
since the box answers where a primitive drew, not what it left.
Measuring a scene that has a background
A full-page background fill is ink like any other, so a scene that paints one measures as the whole page and a crop to that box is a no-op. A caller measuring for a tight crop should draw the scene without its background and paint the background into the cropped target afterwards. The same applies to any mark placed to cover the page rather than to be seen, such as a border drawn at the edges.
Structs
BoundsTarget: ADrawTargetthat keeps the union of what it was asked to draw and draws nothing. See this module’s docstring for what the box means and where it is exact.