Skip to content

CMYK destinations: draw into a pair of images, blend subtractively - #40

Merged
timzifer merged 1 commit into
mainfrom
feat/cmyk-planes
Oct 14, 2026
Merged

timzifer merged 1 commit into
mainfrom
feat/cmyk-planes

Conversation

@timzifer

Copy link
Copy Markdown
Owner

Closes #39. For timzifer/cera#209.

API (additions only)

type Paint struct {
	Color  color.RGBA
	Shader Shader
	Second *Paint // what goes into the second image; nil: the same
}

func (c *Canvas) ResetPair(dst, second *image.RGBA, region image.Rectangle)

type LayerShader struct {
	// …
	Subtractive bool // Src and Dst hold ink amounts: blend on complements
	Black       bool // the K image of a pair: K of backdrop (Hue, Sat, Color) or source (Luminosity)
}

Why a pair of RGBA images

A CMYK destination with alpha is two images of the same rectangle, (C, M, Y, A) and (K, K, K, A), premultiplied ink amounts (0 = no ink, so a transparent backdrop is paper). Everything stilus does per pixel treats the four channels alike — over, coverage, clips and masks, gradients, meshes, image sampling — so it is exact on ink amounts, and the existing SWAR and SIMD paths stay as they are. Only blending needs to know about ink (Subtractive), and only the non-separable modes need K apart (Black, PDF 2.0 11.3.5.3). The K image carries K in all three channels, so separable modes give the same K in each.

Canvas

  • paint returns a pairBlitter (BlitRun/BlitCoverage forwarded to both) when a second image is set; rasterization, clips, the stroker, shapes and unions are untouched.
  • Direct paths: the glyph cache's mask blit writes both images (with the second paint's colour), and its mask-shader path gets a second mask shader; the analytic rows of opaque strokes (seg.solid) step aside for a pair and go through the blitter. Opaque/invisible tests look at both paints.
  • Reset drops the second image.

LayerShader

mode() maps the blend mode for the K image (Luminosity → Normal, Hue/Saturation/Color → blendBackdrop, B = cb, an internal value). With Subtractive the generic path complements source and backdrop (α − c), blends, and complements back; Multiply/Screen's integer fast path is for additive layers only. Normal is unchanged.

Tests (cmyk_test.go)

  • TestCanvasPair: fills of curves, opaque (analytic) and translucent strokes, a dashed stroke, pixel-aligned and offset rectangles, a shader fill, cached glyphs blitted directly and through the mask shader, under a fractional clip rectangle and a clip path and without clips: the pair gives byte for byte what each image gives drawn alone; with no second paint both get the first.
  • TestLayerShaderSubtractive: 400 random premultiplied pixel pairs, all 16 modes, subtractive and (for the non-separable) as the K image, against s·(1−αb) + αs·αb·(1 − B(1 − cb, 1 − cs)) in floating point, within one level.
  • go test ./... green. Benchmarks of the single-image paths (glyph cache, border stroke, layer shader, rectangle fill) show only this machine's spread.

Luminosity soft masks of CMYK groups and the conversion of a pair to image.CMYK are left to the caller (cera).

Canvas.ResetPair draws into two images at once: everything is rasterized
once and composited into both, each image with its paint (Paint.Second,
nil for the same). 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, meshes and images interpolate each
channel on its own, so one shader per image paints CMYK. The fast paths
that write the destination directly take the second image along (the
glyph mask blit) or step aside for a pair (the analytic rows of opaque
strokes); a paint is invisible or opaque only when it is in both images.

LayerShader.Subtractive blends layers of ink amounts on their
complements (PDF 2.0, 11.3.5), α − c in premultiplied colours; the
non-separable modes then blend (C, M, Y) as (R, G, B). LayerShader.Black
makes the shader that of the K image: Hue, Saturation and Color take K
from the backdrop, Luminosity from the source.

Tests: a scene of fills, strokes (analytic rows among them), dashes,
rectangles, shaders, clips and cached glyphs drawn as a pair gives the
bytes of each image drawn alone; every blend mode, subtractive and for
the K image, within a level of the formula in floating point.

For timzifer/cera#209. Closes #39.
@timzifer
timzifer merged commit 0a73e6d into main Oct 14, 2026
13 checks passed
@timzifer
timzifer deleted the feat/cmyk-planes branch October 14, 2026 10:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CMYK destinations: draw into a pair of planes, blend subtractively

1 participant