Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ gc.FillGlyph(c, fontID, glyphID, outline, m, paint)
`Canvas.ClipStroke` clips to the area a stroke paints, for strokes and
stroked text painted with a shader.

**CMYK.** `Canvas.ResetPair(cmy, k, region)` draws into two images at
once, rasterizing once: (C, M, Y, A) and (K, K, K, A), premultiplied ink
amounts, make a CMYK destination with alpha; `Paint.Second` is what goes
into the second. Compositing treats channels alike and shaders interpolate
each on its own, so one shader per image paints CMYK. `LayerShader` blends
such layers with `Subtractive` (on the complements of the inks, PDF 2.0,
11.3.5) and, for the K image, `Black` (K of the backdrop for Hue,
Saturation and Color, of the source for Luminosity).

## How it is fast

- **Band accumulation**: edges are accumulated as area/cover into a
Expand Down
77 changes: 68 additions & 9 deletions blend.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,34 @@ type LayerShader struct {
// and Alone the same group drawn onto a transparent image, of which
// only the alpha is read. Both cover Src's rectangle.
Initial, Alone *image.RGBA

// Subtractive says that Src and Dst hold amounts of ink, 0 for none,
// as the images of a CMYK canvas do (Canvas.ResetPair): blend modes
// other than Normal blend the complements, 1 − c, and the result is
// complemented again (PDF 2.0, 11.3.5). In premultiplied colours the
// complement of c is α − c. The non-separable modes then blend
// (C, M, Y) as they blend (R, G, B).
Subtractive bool
// Black says that the layer is the black image of such a pair,
// (K, K, K, A): the non-separable modes take K from the backdrop
// (Hue, Saturation, Color) or from the source (Luminosity), as PDF
// 2.0, 11.3.5.3 has it; the separable ones blend K on its own.
Black bool
}

// blendBackdrop is the blend function B(cb, cs) = cb, what the black
// image of a CMYK pair blends with in Hue, Saturation and Color.
const blendBackdrop BlendMode = 255

// mode returns the blend mode the shader blends with.
func (c *LayerShader) mode() BlendMode {
if c.Black && c.Blend >= BlendHue {
if c.Blend == BlendLuminosity {
return BlendNormal
}
return blendBackdrop
}
return c.Blend
}

// ShadeSpan implements Shader.
Expand All @@ -97,7 +125,8 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) {
c.removeBackdrop(y, x, sp, out)
sp = unsafe.Slice((*byte)(unsafe.Pointer(&out[0])), 4*n)
}
if c.Blend == BlendNormal && c.Mask == nil {
bm := c.mode()
if bm == BlendNormal && c.Mask == nil {
// The layer's pixels are in the layout of out already.
src := unsafe.Slice((*uint32)(unsafe.Pointer(&sp[0])), n)
if c.Alpha == 255 {
Expand All @@ -111,7 +140,7 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) {
return
}
var dp []byte
if c.Blend != BlendNormal {
if bm != BlendNormal {
dp = c.Dst.Pix[c.Dst.PixOffset(x, y):][: 4*n : 4*n]
}
var mrow []uint8
Expand All @@ -123,7 +152,7 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) {
}
mx = mr.Min.X
}
if c.Blend == BlendNormal {
if bm == BlendNormal {
// Masked Normal: scale the packed pixels by the combined alpha.
src := unsafe.Slice((*uint32)(unsafe.Pointer(&sp[0])), n)
k0 := uint32(c.Alpha)
Expand All @@ -140,15 +169,15 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) {
}
return
}
if c.Blend == BlendMultiply || c.Blend == BlendScreen {
if (bm == BlendMultiply || bm == BlendScreen) && !c.Subtractive {
c.shadeSeparable(x, out, sp, dp, mrow, mx)
return
}
// The separable modes look opaque pixels up in a table, taken once
// per span.
var tab *[1 << 16]uint8
if c.Blend < BlendHue {
tab = blendTable(c.Blend)
if bm < BlendHue {
tab = blendTable(bm)
}
for i := range out {
p := sp[4*i : 4*i+4 : 4*i+4]
Expand All @@ -170,10 +199,24 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) {
}
if dp != nil && dp[4*i+3] != 0 {
q := dp[4*i : 4*i+4 : 4*i+4]
if a == 255 && q[3] == 255 && tab != nil {
r, g, b = tab[int(q[0])<<8|int(r)], tab[int(q[1])<<8|int(g)], tab[int(q[2])<<8|int(b)]
qr, qg, qb, qa := q[0], q[1], q[2], q[3]
switch {
case bm == blendBackdrop:
// s·(1−αb) + αs·αb·cb = s·(1−αb) + αs·d.
r, g, b = backdropInt(r, a, qr, qa), backdropInt(g, a, qg, qa), backdropInt(b, a, qb, qa)
out[i] = pack(r, g, b, a)
continue
case c.Subtractive:
r, g, b = sub8(a, r), sub8(a, g), sub8(a, b)
qr, qg, qb = sub8(qa, qr), sub8(qa, qg), sub8(qa, qb)
}
if a == 255 && qa == 255 && tab != nil {
r, g, b = tab[int(qr)<<8|int(r)], tab[int(qg)<<8|int(g)], tab[int(qb)<<8|int(b)]
} else {
r, g, b = blendPixel(c.Blend, r, g, b, a, q[0], q[1], q[2], q[3])
r, g, b = blendPixel(bm, r, g, b, a, qr, qg, qb, qa)
}
if c.Subtractive {
r, g, b = sub8(a, r), sub8(a, g), sub8(a, b)
}
}
out[i] = pack(r, g, b, a)
Expand Down Expand Up @@ -287,6 +330,22 @@ func (c *LayerShader) shadeSeparable(x int, out []uint32, sp, dp, mrow []uint8,
}
}

// sub8 returns a − c, the complement of a premultiplied colour channel c
// of alpha a, 0 where c exceeds a.
func sub8(a, c uint8) uint8 {
if c > a {
return 0
}
return a - c
}

// backdropInt is one channel of s·(1−αb) + αs·d, the colour a source s
// of alpha sa takes the backdrop d of alpha da with.
func backdropInt(s, sa, d, da uint8) uint8 {
v := div255(uint32(s)*(255-uint32(da)) + uint32(sa)*uint32(d))
return uint8(min(v, uint32(sa)))
}

// overAlpha is negative if a colour channel exceeds alpha. It does not
// branch: on varied colours, the branches of max(r, g, b) > a mispredict.
func overAlpha(r, g, b, a uint8) int32 {
Expand Down
94 changes: 85 additions & 9 deletions canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,26 @@ import (
type Paint struct {
Color color.RGBA // premultiplied
Shader Shader
// Second is what is painted into the second image of a canvas that
// draws into two (Canvas.ResetPair); nil paints Color or Shader there
// too.
Second *Paint
}

// second returns what p paints into a second image.
func (p *Paint) second() *Paint {
if p.Second != nil {
return p.Second
}
return p
}

// invisible reports that p paints nothing.
func (p *Paint) invisible() bool { return p.Shader == nil && p.Color.A == 0 }

// opaque reports that p is an opaque solid colour.
func (p *Paint) opaque() bool { return p.Shader == nil && p.Color.A == 255 }

// Errors reported by Canvas.Err.
var (
ErrEdgeBudget = errors.New("stilus: edge budget exceeded, path truncated")
Expand All @@ -35,6 +53,13 @@ const DefaultMaxClipDepth = 256
// 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.
//
// A canvas reset with ResetPair draws into two images at once: what it
// paints is rasterized once and composited into both, each with its paint
// (Paint.Second). Two images of premultiplied ink amounts, (C, M, Y, A) and
// (K, K, K, A), make a CMYK destination with alpha: compositing treats
// the channels alike, and gradients and images interpolate each on its
// own, so that a shader per image paints a CMYK one.
type Canvas struct {
// MaxClipDepth overrides DefaultMaxClipDepth when non-zero.
MaxClipDepth int
Expand All @@ -46,6 +71,14 @@ type Canvas struct {
s Stroker
solid SolidBlitter
shader ShaderBlitter

// second is the second image of ResetPair, nil for none, and what
// paints into it.
second *image.RGBA
solid2 SolidBlitter
shader2 ShaderBlitter
pair pairBlitter

mread maskBlitter
frac fracBlitter
scale scaleBlitter
Expand Down Expand Up @@ -73,14 +106,47 @@ func NewCanvas(dst *image.RGBA) *Canvas {
// Reset retargets the canvas to dst, limited to region (in dst's
// coordinates), clears the clip stack and the error.
func (c *Canvas) Reset(dst *image.RGBA, region image.Rectangle) {
c.dst = dst
c.dst, c.second = dst, nil
c.solid.t.set(dst)
c.shader.t.set(dst)
c.stack = append(c.stack[:0], newClipState(region.Intersect(dst.Bounds())))
c.overflow = 0
c.err = nil
}

// ResetPair is Reset with a second image that everything is drawn into
// as well, with the second paint of each Paint; region is limited to both
// images.
func (c *Canvas) ResetPair(dst, second *image.RGBA, region image.Rectangle) {
c.Reset(dst, region.Intersect(second.Bounds()))
c.second = second
c.solid2.t.set(second)
c.shader2.t.set(second)
}

// pairBlitter blits the same coverage through two blitters.
type pairBlitter struct{ a, b Blitter }

func (p *pairBlitter) BlitRun(y, x0, x1 int, alpha uint8) {
p.a.BlitRun(y, x0, x1, alpha)
p.b.BlitRun(y, x0, x1, alpha)
}

func (p *pairBlitter) BlitCoverage(y, x int, cov []uint8) {
p.a.BlitCoverage(y, x, cov)
p.b.BlitCoverage(y, x, cov)
}

// invisible reports that p paints nothing into the canvas's images.
func (c *Canvas) invisible(p *Paint) bool {
return p.invisible() && (c.second == nil || p.second().invisible())
}

// opaque reports that p is an opaque solid colour in every image.
func (c *Canvas) opaque(p *Paint) bool {
return p.opaque() && (c.second == nil || p.second().opaque())
}

// Err returns the first error since Reset, if any. Drawing continues after
// an error; the error only reports that output may be incomplete.
func (c *Canvas) Err() error { return c.err }
Expand Down Expand Up @@ -136,12 +202,22 @@ func (c *Canvas) chain(st *clipState, b Blitter) Blitter {
}

func (c *Canvas) paint(p *Paint) Blitter {
b := paintWith(p, &c.solid, &c.shader)
if c.second == nil {
return b
}
c.pair.a, c.pair.b = b, paintWith(p.second(), &c.solid2, &c.shader2)
return &c.pair
}

// paintWith returns the blitter of p, of the two given.
func paintWith(p *Paint, solid *SolidBlitter, shader *ShaderBlitter) Blitter {
if p.Shader != nil {
c.shader.setShader(p.Shader)
return &c.shader
shader.setShader(p.Shader)
return shader
}
c.solid.SetColor(p.Color)
return &c.solid
solid.SetColor(p.Color)
return solid
}

func (c *Canvas) setClip(b image.Rectangle) {
Expand All @@ -160,7 +236,7 @@ func (c *Canvas) checkBudget() {
func (c *Canvas) Fill(p *Path, m Matrix, rule FillRule, paint *Paint) {
defer c.guard()
st := c.top()
if st.bounds.Empty() || (paint.Shader == nil && paint.Color.A == 0) {
if st.bounds.Empty() || c.invisible(paint) {
return
}
c.setClip(st.bounds)
Expand Down Expand Up @@ -201,7 +277,7 @@ func (c *Canvas) fillRect(p *Path, m Matrix, st *clipState, paint *Paint) bool {
func (c *Canvas) Stroke(p *Path, m Matrix, st *StrokeStyle, paint *Paint) {
defer c.guard()
cs := c.top()
if cs.bounds.Empty() || (paint.Shader == nil && paint.Color.A == 0) || len(p.Points) == 0 {
if cs.bounds.Empty() || c.invisible(paint) || len(p.Points) == 0 {
return
}
pr := prepStroke(m, st)
Expand Down Expand Up @@ -266,7 +342,7 @@ func (c *Canvas) strokePaint(cs *clipState, paint *Paint, pr strokePrep) (Blitte
// sets c.seg up for it: b is the stroke's blitter chain, overlap and single
// are mayOverlap and singleSegment of its path and style.
func (c *Canvas) strokeFast(cs *clipState, b Blitter, paint *Paint, pr strokePrep, overlap, single bool) bool {
opaque := paint.Shader == nil && paint.Color.A == 255
opaque := c.opaque(paint)
if pr.dense || !(opaque && (cs.mask == nil || !overlap) || single) {
return false
}
Expand All @@ -282,7 +358,7 @@ func (c *Canvas) strokeFast(cs *clipState, b Blitter, paint *Paint, pr strokePre
c.seg.r, c.seg.b = &c.r, b
c.seg.solid = nil
c.seg.frac = cs.frac
if cs.mask == nil && opaque {
if cs.mask == nil && opaque && c.second == nil {
c.seg.solid = &c.solid
}
c.seg.setBorder(cs, overlap)
Expand Down
Loading
Loading