Documentation
¶
Overview ¶
Package stilus is a sparse CPU rasterizer for 2D vector graphics.
Its cost per path is the length of the path's edges in pixels plus the covered spans, never the area of the bounding box or the width of the canvas. It knows nothing about PDF, fonts or color spaces and has no dependencies, so it can serve as the CPU backend of a PDF renderer, a UI toolkit or another 2D library (for example as the CPU filler behind gogpu/gg).
Layers ¶
Each layer can be used on its own:
- Rasterizer turns device-space paths (Fill, or AddLine/AddPath and Rasterize) into coverage spans delivered to a Blitter: BlitRun for constant interior runs, BlitCoverage for antialiased edge pixels. Implement Blitter to composite into any pixel format.
- Stroker turns a stroke (width, caps, joins, miter limit, dashes; all in user space, exact under anisotropic transforms) into fill geometry for a LineSink, such as a Rasterizer or a PathSink.
- Canvas combines both with a clip stack and compositing onto a caller-owned *image.RGBA. Its methods map one to one onto a display-list device: Fill, Stroke, ClipPath, ClipRect, PopClip.
Performance model ¶
Edges are accumulated as signed area and cover into a band of cells (32 rows × clip width) in 24.8 fixed point. Per row, the touched cell range is integrated directly when it is narrow; wide rows are swept with a dirty bitset that skips untouched cells, and the constant stretches between edges are emitted as runs that blitters fill without a per-pixel loop. Nothing is cleared as a whole: the sweep zeroes what it reads.
Strokes are filled analytically between their corners: rows bounded only by a segment's two parallel sides get each pixel's exact area in closed form; only the rows around joins and caps go through the accumulator. Strokes thinner than one device pixel are drawn one pixel wide. Rectangle clips cost nothing per pixel; other clips are rasterized once into a mask over their bounds.
All buffers grow on demand and are retained: after warm-up, drawing performs no allocations. Rasterizer, Stroker and Canvas are not safe for concurrent use; use one per worker and give each worker its own band or tile of the destination (Canvas.Reset takes a region).
Robustness ¶
NaN and infinite coordinates are rejected, huge ones are clipped analytically, curve and arc subdivision and dash counts are bounded, and the edge count per path is budgeted (Rasterizer.MaxEdges). Canvas never panics; it reports problems through Err and keeps drawing.
Example ¶
package main
import (
"fmt"
"image"
"image/color"
"github.com/timzifer/stilus"
)
func main() {
img := image.NewRGBA(image.Rect(0, 0, 200, 100))
c := stilus.NewCanvas(img)
// Page space in points, drawn at 144 dpi.
m := stilus.Scale(2, 2)
c.ClipRect(stilus.Rect{X0: 5, Y0: 5, X1: 95, Y1: 45}, m)
var p stilus.Path
p.MoveTo(10, 40)
p.CubicTo(30, 0, 70, 80, 90, 10)
c.Stroke(&p, m, &stilus.StrokeStyle{Width: 1.5, Cap: stilus.RoundCap}, &stilus.Paint{Color: color.RGBA{0, 0, 0, 255}})
var r stilus.Path
r.Ellipse(50, 25, 20, 12)
c.Fill(&r, m, stilus.NonZero, &stilus.Paint{Color: color.RGBA{0, 0, 128, 128}}) // premultiplied
c.PopClip()
fmt.Println(c.Err())
}
Output: <nil>
Index ¶
- Constants
- Variables
- func IsHairline(m Matrix, st *StrokeStyle) bool
- func PackRGBA(c color.RGBA) uint32
- func SIMD() bool
- func UnpackRGBA(v uint32) color.RGBA
- type Blitter
- type Canvas
- func (c *Canvas) Clip() image.Rectangle
- func (c *Canvas) ClipDepth() int
- func (c *Canvas) ClipPath(p *Path, m Matrix, rule FillRule)
- func (c *Canvas) ClipRect(r Rect, m Matrix)
- func (c *Canvas) Err() error
- func (c *Canvas) Fill(p *Path, m Matrix, rule FillRule, paint *Paint)
- func (c *Canvas) PopClip()
- func (c *Canvas) Reset(dst *image.RGBA, region image.Rectangle)
- func (c *Canvas) Stroke(p *Path, m Matrix, st *StrokeStyle, paint *Paint)
- type Cap
- type FillRule
- type Join
- type LineSink
- type MaskBlitter
- type Matrix
- type Paint
- type Path
- func (p *Path) Bounds() Rect
- func (p *Path) Close()
- func (p *Path) CubicTo(c1x, c1y, c2x, c2y, x, y float32)
- func (p *Path) Ellipse(cx, cy, rx, ry float32)
- func (p *Path) Empty() bool
- func (p *Path) LineTo(x, y float32)
- func (p *Path) MoveTo(x, y float32)
- func (p *Path) QuadTo(cx, cy, x, y float32)
- func (p *Path) Rect(x, y, w, h float32)
- func (p *Path) Reset()
- type PathSink
- type Point
- type Rasterizer
- func (r *Rasterizer) AddLine(x0, y0, x1, y1 float64)
- func (r *Rasterizer) AddPath(p *Path, m Matrix)
- func (r *Rasterizer) Clip() image.Rectangle
- func (r *Rasterizer) Fill(p *Path, m Matrix, rule FillRule, b Blitter)
- func (r *Rasterizer) Rasterize(rule FillRule, b Blitter)
- func (r *Rasterizer) Reset()
- func (r *Rasterizer) SetAntialias(aa bool)
- func (r *Rasterizer) SetClip(clip image.Rectangle)
- func (r *Rasterizer) Truncated() bool
- type Rect
- type Shader
- type ShaderBlitter
- type SolidBlitter
- type StrokeStyle
- type Stroker
- type Verb
Examples ¶
Constants ¶
const DefaultMaxClipDepth = 256
DefaultMaxClipDepth bounds the clip stack of a Canvas.
const DefaultMaxDashes = 1 << 20
DefaultMaxDashes bounds dash transitions per subpath. Subpaths estimated to exceed the budget are skipped; the subdivision loop also stops at the budget. Stroker.Truncated reports either condition.
const (
// DefaultMaxEdges is the default edge budget per path.
DefaultMaxEdges = 1 << 22
)
Variables ¶
var ( ErrEdgeBudget = errors.New("stilus: edge budget exceeded, path truncated") ErrDashBudget = errors.New("stilus: dash budget exceeded, stroke truncated") ErrClipDepth = errors.New("stilus: clip stack too deep") ErrInternal = errors.New("stilus: internal error") )
Errors reported by Canvas.Err.
var Identity = Matrix{1, 0, 0, 1, 0, 0}
Identity is the identity transform.
Functions ¶
func IsHairline ¶
func IsHairline(m Matrix, st *StrokeStyle) bool
IsHairline reports whether st under m is thinner than one device pixel in every direction. Such strokes are drawn one pixel wide in device space, like PDFium.
func PackRGBA ¶
PackRGBA packs a premultiplied color into the native pixel layout used by Shader.ShadeSpan.
Types ¶
type Blitter ¶
type Blitter interface {
// BlitRun paints the constant coverage alpha over [x0, x1) on row y.
// Interior pixels of a shape arrive here, never through BlitCoverage.
BlitRun(y, x0, x1 int, alpha uint8)
// BlitCoverage paints per-pixel coverage for [x, x+len(cov)) on row y.
BlitCoverage(y, x int, cov []uint8)
}
Blitter receives the output of the rasterizer, one span at a time, in increasing y order and, within a row, in increasing x order. Spans never overlap and always lie inside the rasterizer's clip.
Implementations must not retain cov after the call returns.
type Canvas ¶
type Canvas struct {
// MaxClipDepth overrides DefaultMaxClipDepth when non-zero.
MaxClipDepth int
// contains filtered or unexported fields
}
Canvas draws paths onto a caller-owned *image.RGBA (premultiplied, as image.RGBA always is). It combines the rasterizer, the stroker and a clip stack, and maps one to one onto a display-list device: Fill/Stroke/ClipPath/ClipRect/PopClip.
Device space is the pixel space of the destination image, so a tile or band is rendered by passing an image whose Rect is that tile, or by restricting Reset's region. A Canvas is not safe for concurrent use; use one per worker. After warm-up a Canvas does not allocate.
func (*Canvas) ClipPath ¶
ClipPath intersects the clip with p under m. Rectangles ("re W n") take the rectangle fast path; other shapes are rasterized once into a mask over their bounds.
func (*Canvas) ClipRect ¶
ClipRect intersects the clip with rectangle r in user space. An axis-aligned result costs nothing per pixel; a rotated one becomes a mask.
func (*Canvas) Err ¶
Err returns the first error since Reset, if any. Drawing continues after an error; the error only reports that output may be incomplete.
type LineSink ¶
type LineSink interface {
AddLine(x0, y0, x1, y1 float64)
}
LineSink receives device-space line segments. Segments emitted by the Stroker form closed polygons that must be filled with the NonZero rule. *Rasterizer implements LineSink.
type MaskBlitter ¶
MaskBlitter accumulates coverage into an 8-bit alpha image (for clip masks, soft masks and tests). Coverage is combined with "max", which is exact for the non-overlapping spans of a single rasterization.
func (*MaskBlitter) BlitCoverage ¶
func (b *MaskBlitter) BlitCoverage(y, x int, cov []uint8)
func (*MaskBlitter) BlitRun ¶
func (b *MaskBlitter) BlitRun(y, x0, x1 int, alpha uint8)
type Matrix ¶
type Matrix [6]float64
Matrix is an affine transform in PDF order [a b c d e f]:
x' = a·x + c·y + e y' = b·x + d·y + f
type Paint ¶
Paint describes what a fill or stroke paints: a solid premultiplied color, or a Shader when Shader is non-nil.
type Path ¶
Path is a sequence of subpaths. The zero value is an empty path ready to use. Reset keeps the backing storage, so a Path can be rebuilt many times without allocating.
type Point ¶
type Point struct{ X, Y float32 }
Point is a 2D point. Path coordinates are stored as float32 to keep paths (and display lists built on top of them) compact.
type Rasterizer ¶
type Rasterizer struct {
// MaxEdges bounds the number of edges per path. Edges beyond it are
// dropped and Truncated reports true. Zero means DefaultMaxEdges.
MaxEdges int
// contains filtered or unexported fields
}
Rasterizer converts paths into coverage spans. Its cost per path is the length of the path's edges in pixels plus the number of covered spans; it never touches the area of the bounding box.
A Rasterizer is not safe for concurrent use; use one per worker. All buffers grow on demand and are retained, so steady-state use performs no allocations.
Example ¶
package main
import (
"fmt"
"image"
"github.com/timzifer/stilus"
)
// areaBlitter shows the span interface used to plug the rasterizer into
// another compositor: it just integrates the coverage.
type areaBlitter struct{ area int }
func (b *areaBlitter) BlitRun(y, x0, x1 int, alpha uint8) { b.area += (x1 - x0) * int(alpha) }
func (b *areaBlitter) BlitCoverage(y, x int, cov []uint8) {
for _, a := range cov {
b.area += int(a)
}
}
func main() {
r := stilus.NewRasterizer(image.Rect(0, 0, 100, 100))
var p stilus.Path
p.Rect(10.5, 10.5, 50, 30)
p.Rect(20, 60, 400, 10) // wide: interior rows arrive as runs
var b areaBlitter
r.Fill(&p, stilus.Identity, stilus.NonZero, &b)
fmt.Printf("%.0f px²\n", float64(b.area)/255)
}
Output: 2300 px²
func NewRasterizer ¶
func NewRasterizer(clip image.Rectangle) *Rasterizer
NewRasterizer returns a rasterizer clipped to clip.
func (*Rasterizer) AddLine ¶
func (r *Rasterizer) AddLine(x0, y0, x1, y1 float64)
AddLine adds a device-space line segment. Segments are clipped analytically: parts above or below the clip are dropped, parts right of it are dropped, and parts left of it become vertical edges on the clip's left border, which preserves the winding of everything inside.
func (*Rasterizer) AddPath ¶
func (r *Rasterizer) AddPath(p *Path, m Matrix)
AddPath adds the edges of p transformed by m. Open subpaths are closed implicitly. Curves are flattened in device space with a tolerance of a quarter pixel. Paths containing NaN or infinite values are ignored.
func (*Rasterizer) Clip ¶
func (r *Rasterizer) Clip() image.Rectangle
Clip returns the current clip rectangle.
func (*Rasterizer) Fill ¶
func (r *Rasterizer) Fill(p *Path, m Matrix, rule FillRule, b Blitter)
Fill rasterizes p transformed by m with the given fill rule. It is equivalent to Reset, AddPath and Rasterize.
func (*Rasterizer) Rasterize ¶
func (r *Rasterizer) Rasterize(rule FillRule, b Blitter)
Rasterize converts all added edges into spans and resets the rasterizer.
func (*Rasterizer) Reset ¶
func (r *Rasterizer) Reset()
Reset discards all accumulated edges and the Truncated state.
func (*Rasterizer) SetAntialias ¶
func (r *Rasterizer) SetAntialias(aa bool)
SetAntialias enables (default) or disables antialiasing. Without antialiasing a pixel is covered when at least half of it is inside.
func (*Rasterizer) SetClip ¶
func (r *Rasterizer) SetClip(clip image.Rectangle)
SetClip sets the device-space clip rectangle. Every span produced lies inside it. Buffers are resized only when the clip grows.
func (*Rasterizer) Truncated ¶
func (r *Rasterizer) Truncated() bool
Truncated reports whether the last path exceeded the edge budget.
type Rect ¶
type Rect struct{ X0, Y0, X1, Y1 float64 }
Rect is an axis-aligned rectangle [X0,X1)×[Y0,Y1).
type Shader ¶
Shader produces premultiplied colors (in PackRGBA layout) for the pixels [x, x+len(dst)) on row y. It is called once per span, never per pixel.
type ShaderBlitter ¶
type ShaderBlitter struct {
// contains filtered or unexported fields
}
ShaderBlitter composites the output of a Shader onto an image.RGBA.
func NewShaderBlitter ¶
func NewShaderBlitter(dst *image.RGBA, s Shader) *ShaderBlitter
NewShaderBlitter returns a blitter painting s onto dst.
func (*ShaderBlitter) BlitCoverage ¶
func (b *ShaderBlitter) BlitCoverage(y, x int, cov []uint8)
func (*ShaderBlitter) BlitRun ¶
func (b *ShaderBlitter) BlitRun(y, x0, x1 int, alpha uint8)
type SolidBlitter ¶
type SolidBlitter struct {
// contains filtered or unexported fields
}
SolidBlitter composites a single premultiplied color onto an image.RGBA.
func NewSolidBlitter ¶
func NewSolidBlitter(dst *image.RGBA, c color.RGBA) *SolidBlitter
NewSolidBlitter returns a blitter painting c (premultiplied) onto dst.
func (*SolidBlitter) BlitCoverage ¶
func (b *SolidBlitter) BlitCoverage(y, x int, cov []uint8)
func (*SolidBlitter) BlitRun ¶
func (b *SolidBlitter) BlitRun(y, x0, x1 int, alpha uint8)
func (*SolidBlitter) Reset ¶
func (b *SolidBlitter) Reset(dst *image.RGBA, c color.RGBA)
Reset retargets the blitter without allocating.
func (*SolidBlitter) SetColor ¶
func (b *SolidBlitter) SetColor(c color.RGBA)
SetColor changes the paint color (premultiplied).
type StrokeStyle ¶
type StrokeStyle struct {
Width float64 // 0 means the thinnest line the device can render
Cap Cap
Join Join
MiterLimit float64 // PDF default 10; values < 1 are treated as 1
Dash []float64 // on/off lengths; empty means solid
DashPhase float64
}
StrokeStyle describes a stroke in user space (the space the path is defined in), like the PDF graphics state.
type Stroker ¶
type Stroker struct {
// MaxDashes overrides DefaultMaxDashes when non-zero.
MaxDashes int
// contains filtered or unexported fields
}
Stroker converts strokes into fill geometry (closed outlines to be filled with NonZero). Offsets are computed in user space and transformed afterwards, which is exact for anisotropic transforms.
A Stroker is not safe for concurrent use. Buffers are retained.
func (*Stroker) Coverage ¶ added in v0.1.1
Coverage reports the factor to apply to the coverage of the last stroke's outline: the mean on-fraction of a dash pattern emitted as a solid stroke, else 1.
func (*Stroker) Stroke ¶
func (s *Stroker) Stroke(sink LineSink, p *Path, m Matrix, st *StrokeStyle)
Stroke emits the outline of p stroked with st under m into sink.
A dash pattern too dense to resolve in device space is emitted as a solid stroke; Coverage then reports the fraction the caller must scale the resulting coverage by.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
stilus-scenes
command
Command stilus-scenes renders the benchmark scenes, prints timings and allocations and optionally writes PNGs for visual inspection.
|
Command stilus-scenes renders the benchmark scenes, prints timings and allocations and optionally writes PNGs for visual inspection. |
|
internal
|
|
|
scenes
Package scenes provides deterministic benchmark scenes modelled on technical drawings: an A3 landscape page in PDF points, drawn under a scale to the requested resolution with a rectangular page clip.
|
Package scenes provides deterministic benchmark scenes modelled on technical drawings: an A3 landscape page in PDF points, drawn under a scale to the requested resolution with a rectangular page clip. |