Files

739 lines
27 KiB
Odin
Raw Permalink Normal View History

2026-09-25 23:32:29 +02:00
package pdf
import "core:math"
// Blend modes and compositing (ISO 32000-2, 11.3.5 and 11.3.6).
//
// ---------------------------------------------------------------------------
// Which representation each procedure expects
//
// Mixing premultiplied and straight alpha is the classic source of dark
// fringes, so the rule here is stated once and never bent:
//
// * Every `Bitmap` this file reads or writes -- `dst` and `src` in
// `blend_span`, `blend_span_rgba8` and `blend_bitmap` -- is PREMULTIPLIED.
// That is the internal representation the whole renderer composites in.
// `bitmap_unpremultiply` is called once, at the very end, on the bitmap
// that leaves the library.
// * Every colour passed as an `f64` -- `rgba` in `blend_span`, the arguments
// to `blend_separable` and `blend_non_separable`, `backdrop` in
// `mask_from_bitmap` -- is STRAIGHT, in 0..1, with alpha separate. That is
// what a `Paint` carries (`paint.rgb`, `paint.alpha`) and what the spec's
// blend formulas are written in terms of.
// * `blend_span_rgba8`'s `src` bytes are STRAIGHT RGBA8, because that is what
// `image_decode` produces.
// * Coverage and mask bytes are plain 0..255 fractions; they multiply into
// the source alpha and are never premultiplied by anything.
//
// A freshly zeroed bitmap is simultaneously valid as straight and as
// premultiplied data, which is why the raster device can start from
// `bitmap_make` without a conversion. Compositing into a bitmap sets its
// `premultiplied` flag, so a device that forgets to set it cannot end up
// handing straight-labelled premultiplied pixels to the caller. A bitmap that
// already holds STRAIGHT content must be run through `bitmap_premultiply`
// before anything here touches it.
//
// The inner loops are f64 throughout. There is deliberately no separate
// integer fast path for Normal: two paths that round differently would
// disagree by a least-significant bit depending on which one a given span took,
// and a seam down the middle of a fill is far worse than the cycles saved.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// The separable blend functions (11.3.5.2)
//
// All arguments and results are in 0..1. `cb` is the backdrop colour, `cs` the
// source colour, one component at a time.
// ---------------------------------------------------------------------------
// blend_separable evaluates B(cb, cs) for one component.
//
// The four non-separable modes cannot be evaluated one component at a time;
// passing one here returns `cs`, i.e. Normal. Callers dispatch on
// `blend_mode_is_separable` rather than relying on that, but a mode that
// arrives here by accident must degrade to the identity rather than to noise.
@(private)
blend_separable :: proc(mode: Blend_Mode, cb, cs: f64) -> f64 {
switch mode {
case .Normal:
return cs
case .Multiply:
return cb * cs
case .Screen:
return cb + cs - cb * cs
case .Overlay:
// Defined as HardLight with the operands exchanged.
return blend_hard_light(cs, cb)
case .Darken:
return min(cb, cs)
case .Lighten:
return max(cb, cs)
case .Color_Dodge:
// ISO 32000-2 rewrote this case split; ISO 32000-1 said only
// "cs < 1 ? min(1, cb/(1-cs)) : 1", which lights a black backdrop up
// to white when the source is white. Testing cb == 0 first is what
// stops that, and it is what every current implementation does.
if cb <= 0 { return 0 }
if cs >= 1 { return 1 }
return min(1, cb / (1 - cs))
case .Color_Burn:
// Mirror image of ColorDodge: cb == 1 is tested before cs == 0, so a
// white backdrop stays white instead of being burned to black.
if cb >= 1 { return 1 }
if cs <= 0 { return 0 }
return 1 - min(1, (1 - cb) / cs)
case .Hard_Light:
return blend_hard_light(cb, cs)
case .Soft_Light:
return blend_soft_light(cb, cs)
case .Difference:
return abs(cb - cs)
case .Exclusion:
return cb + cs - 2 * cb * cs
case .Hue, .Saturation, .Color, .Luminosity:
return cs
}
return cs
}
@(private)
blend_hard_light :: proc(cb, cs: f64) -> f64 {
if cs <= 0.5 {
return cb * (2 * cs) // Multiply(cb, 2*cs)
}
d := 2 * cs - 1
return cb + d - cb * d // Screen(cb, 2*cs - 1)
}
// blend_soft_light is the mode implementations most often get wrong: D(x) is
// piecewise, and the low branch is a cubic, not sqrt(x). The two agree at
// x = 0.25 (both give 0.5); they are 0.05 apart at x = 0.05, which is dark
// shadow detail and plainly visible.
@(private)
blend_soft_light :: proc(cb, cs: f64) -> f64 {
if cs <= 0.5 {
return cb - (1 - 2 * cs) * cb * (1 - cb)
}
return cb + (2 * cs - 1) * (blend_soft_light_d(cb) - cb)
}
@(private)
blend_soft_light_d :: proc(x: f64) -> f64 {
if x <= 0.25 {
return ((16 * x - 12) * x + 4) * x
}
if x <= 0 { return 0 }
return math.sqrt(x)
}
// ---------------------------------------------------------------------------
// The non-separable blend functions (11.3.5.3)
//
// These act on all three components at once, through the spec's Lum, ClipColor,
// SetLum, Sat and SetSat helpers. They are defined for RGB; a greyscale
// destination is handled by replicating its single component into all three,
// which is exactly what the formulas reduce to anyway.
// ---------------------------------------------------------------------------
@(private)
blend_lum :: proc(c: [3]f64) -> f64 {
return 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
}
// blend_clip_color pulls a colour back inside the unit cube without changing
// its luminosity, by scaling it towards the grey axis.
//
// L, n and x are all taken from the ORIGINAL colour: the second test uses the
// pre-clip maximum, not the maximum after the first branch has run. Recomputing
// them between the branches is a real and easy mistake, and it changes the
// result whenever both branches fire.
@(private)
blend_clip_color :: proc(c: [3]f64) -> [3]f64 {
r := c
l := blend_lum(c)
n := min(c[0], c[1], c[2])
x := max(c[0], c[1], c[2])
if n < 0 {
d := l - n
// d == 0 needs all three components equal to l, and then n < 0 means
// the colour is a uniform negative grey. Nothing can be scaled towards
// the axis it is already on, so clamp instead of dividing by zero.
if d <= 0 { return [3]f64{0, 0, 0} }
for i in 0 ..< 3 { r[i] = l + ((r[i] - l) * l) / d }
}
if x > 1 {
d := x - l
if d <= 0 { return [3]f64{1, 1, 1} }
for i in 0 ..< 3 { r[i] = l + ((r[i] - l) * (1 - l)) / d }
}
return r
}
@(private)
blend_set_lum :: proc(c: [3]f64, l: f64) -> [3]f64 {
d := l - blend_lum(c)
return blend_clip_color([3]f64{c[0] + d, c[1] + d, c[2] + d})
}
@(private)
blend_sat :: proc(c: [3]f64) -> f64 {
return max(c[0], c[1], c[2]) - min(c[0], c[1], c[2])
}
// blend_set_sat rescales a colour to the given saturation, keeping the relative
// ordering of its components and pinning the smallest to zero. The luminosity
// it destroys is put back by the SetLum that always follows it.
@(private)
blend_set_sat :: proc(c: [3]f64, s: f64) -> [3]f64 {
// Sort the three component indices, so the result can be written back to
// the component it came from.
lo, mid, hi := 0, 1, 2
if c[lo] > c[mid] { lo, mid = mid, lo }
if c[mid] > c[hi] { mid, hi = hi, mid }
if c[lo] > c[mid] { lo, mid = mid, lo }
r: [3]f64
if c[hi] > c[lo] {
r[mid] = ((c[mid] - c[lo]) * s) / (c[hi] - c[lo])
r[hi] = s
} else {
r[mid] = 0
r[hi] = 0
}
r[lo] = 0
return r
}
// blend_non_separable evaluates B(Cb, Cs) for Hue, Saturation, Color and
// Luminosity. A separable mode passed here is applied component by component,
// so this is a complete B() for any mode.
@(private)
blend_non_separable :: proc(mode: Blend_Mode, cb, cs: [3]f64) -> [3]f64 {
#partial switch mode {
case .Hue:
return blend_set_lum(blend_set_sat(cs, blend_sat(cb)), blend_lum(cb))
case .Saturation:
return blend_set_lum(blend_set_sat(cb, blend_sat(cs)), blend_lum(cb))
case .Color:
return blend_set_lum(cs, blend_lum(cb))
case .Luminosity:
return blend_set_lum(cb, blend_lum(cs))
}
return [3]f64{
blend_separable(mode, cb[0], cs[0]),
blend_separable(mode, cb[1], cs[1]),
blend_separable(mode, cb[2], cs[2]),
}
}
// ---------------------------------------------------------------------------
// The compositing formula (11.3.6)
// ---------------------------------------------------------------------------
// blend_over composites one source colour onto one backdrop colour.
//
// Both inputs are STRAIGHT: `cb`/`ab` is the backdrop, `cs`/`as_` the source,
// all in 0..1. The result is PREMULTIPLIED colour plus its alpha, because that
// is the form the destination bitmap stores and it is the form in which the
// union formula
//
// co = as*(1-ab)*Cs + as*ab*B(Cb,Cs) + (1-as)*ab*Cb
// ao = as + ab*(1-as)
//
// is actually written. This is the single reference implementation: every span
// and bitmap loop below goes through it, so there is no second rounding path to
// drift away from.
@(private)
blend_over :: proc(cb: [3]f64, ab: f64, cs: [3]f64, as_: f64, mode: Blend_Mode) -> (co: [3]f64, ao: f64) {
b := cs
// With no backdrop there is nothing to blend with: the B() term is
// multiplied by ab, so skipping it is exact, not an approximation.
if mode != .Normal && ab > 0 {
if blend_mode_is_separable(mode) {
b[0] = blend_separable(mode, cb[0], cs[0])
b[1] = blend_separable(mode, cb[1], cs[1])
b[2] = blend_separable(mode, cb[2], cs[2])
} else {
b = blend_non_separable(mode, cb, cs)
}
}
for i in 0 ..< 3 {
co[i] = as_ * ((1 - ab) * cs[i] + ab * b[i]) + (1 - as_) * ab * cb[i]
}
ao = as_ + ab * (1 - as_)
return
}
// ---------------------------------------------------------------------------
// Pixel access
// ---------------------------------------------------------------------------
// blend_quantize turns a 0..1 value into a byte, rounding half up. NaN maps to
// 0: `!(v > 0)` is true for NaN, which is the point of writing the test that
// way round.
@(private)
blend_quantize :: proc(v: f64) -> u8 {
if !(v > 0) { return 0 }
if v >= 1 { return 255 }
return u8(v * 255 + 0.5)
}
// blend_clamp01 forces a colour or alpha coming from a content stream into
// 0..1. NaN maps to 0 and the infinities to the end they point at, which is
// what clamping means for a value that merely overshot; `!(v > 0)` is the test
// that catches NaN, since every comparison with a NaN is false.
@(private)
blend_clamp01 :: proc(v: f64) -> f64 {
if !(v > 0) { return 0 }
if !(v < 1) { return 1 }
return v
}
// blend_has_alpha reports whether a component count carries an alpha channel:
// 2 is grey+alpha and 4 is RGBA, while 1 and 3 are opaque.
@(private)
blend_has_alpha :: proc(n: int) -> bool {
return n == 2 || n == 4
}
// blend_read_straight unpacks one PREMULTIPLIED pixel into a straight RGB
// triple and its alpha. A grey pixel is replicated across all three
// components; a pixel with no alpha channel reads as fully opaque.
@(private)
blend_read_straight :: proc(row: []u8, off, n: int) -> (c: [3]f64, a: f64) {
if blend_has_alpha(n) {
a8 := row[off + n - 1]
a = f64(a8) / 255
if a8 == 0 {
// The colour of a fully transparent pixel is not recoverable, and
// it is multiplied by ab = 0 everywhere it is used.
return [3]f64{0, 0, 0}, 0
}
inv := 1 / f64(a8)
if n == 2 {
g := f64(row[off]) * inv
return [3]f64{g, g, g}, a
}
return [3]f64{f64(row[off]) * inv, f64(row[off + 1]) * inv, f64(row[off + 2]) * inv}, a
}
if n == 1 {
g := f64(row[off]) / 255
return [3]f64{g, g, g}, 1
}
return [3]f64{f64(row[off]) / 255, f64(row[off + 1]) / 255, f64(row[off + 2]) / 255}, 1
}
// blend_write_premul stores a PREMULTIPLIED result. For a greyscale
// destination the three components are equal by construction -- the source was
// reduced to grey before blending -- so component 0 is the whole answer.
@(private)
blend_write_premul :: proc(row: []u8, off, n: int, co: [3]f64, ao: f64) {
if n <= 2 {
row[off] = blend_quantize(co[0])
} else {
row[off] = blend_quantize(co[0])
row[off + 1] = blend_quantize(co[1])
row[off + 2] = blend_quantize(co[2])
}
if blend_has_alpha(n) {
row[off + n - 1] = blend_quantize(ao)
}
}
// blend_source_for reduces a straight RGB source to the destination's colour
// model. A greyscale destination gets the luminosity, which is the same
// weighting the non-separable modes use, so grey output stays consistent with
// what those modes would have produced.
@(private)
blend_source_for :: proc(n: int, rgb: [3]f64) -> [3]f64 {
if n <= 2 {
g := blend_lum(rgb)
return [3]f64{g, g, g}
}
return rgb
}
// ---------------------------------------------------------------------------
// Spans
// ---------------------------------------------------------------------------
// blend_span composites one horizontal run of coverage in a single colour onto
// `dst`, which must be PREMULTIPLIED.
//
// y, x0 the first pixel of the run, in `dst` pixel coordinates
// coverage anti-aliasing coverage, 0..255, one byte per pixel from x0
// rgba the source colour: STRAIGHT RGB in 0..1 plus its alpha, which is
// the constant alpha of the paint operation
// mask an optional soft-mask row, 0..255, aligned with `coverage` so
// that mask[i] modulates the same pixel coverage[i] does. nil means
// no mask. Bytes past the end of a short mask read as 255 rather
// than erasing content that has no mask data.
//
// The effective source alpha of a pixel is rgba.a * coverage/255 * mask/255.
// Runs that fall partly or wholly outside `dst` are clipped, not rejected: the
// rasteriser is allowed to hand over spans that start left of zero.
@(private)
blend_span :: proc(dst: ^Bitmap, y, x0: int, coverage: []u8, rgba: [4]f64,
mode: Blend_Mode, mask: []u8 = nil) {
if dst == nil || len(coverage) == 0 { return }
sa := blend_clamp01(rgba[3])
if sa <= 0 { return }
row := bitmap_row(dst, y)
if row == nil { return }
// Clip the run to the bitmap. `skip` is how many coverage bytes fall off
// the left edge, so the mask stays in step with the coverage.
skip := 0
x := x0
if x < 0 {
skip = -x
x = 0
}
if skip >= len(coverage) { return }
count := min(len(coverage) - skip, dst.width - x)
if count <= 0 { return }
n := dst.n
cs := blend_source_for(n, [3]f64{
blend_clamp01(rgba[0]), blend_clamp01(rgba[1]), blend_clamp01(rgba[2]),
})
for i in 0 ..< count {
cov := coverage[skip + i]
if cov == 0 { continue }
m := u8(255)
if mask != nil {
mi := skip + i
m = mi < len(mask) ? mask[mi] : 255
}
if m == 0 { continue }
as_ := sa * (f64(cov) / 255) * (f64(m) / 255)
if as_ <= 0 { continue }
off := (x + i) * n
if off + n > len(row) { break }
cb, ab := blend_read_straight(row, off, n)
co, ao := blend_over(cb, ab, cs, as_, mode)
blend_write_premul(row, off, n, co, ao)
}
if blend_has_alpha(n) { dst.premultiplied = true }
}
// blend_span_rgba8 is blend_span for a run whose colour varies per pixel: what
// an image or a shading paints.
//
// `src` is STRAIGHT RGBA8, four bytes per pixel, starting at destination pixel
// x0; that is the form `image_decode` hands back. `alpha` is the constant alpha
// of the paint operation, multiplied into every pixel's own alpha. `coverage`
// may be nil, meaning fully covered; when present it must be one byte per
// source pixel. The run length is len(src)/4.
@(private)
blend_span_rgba8 :: proc(dst: ^Bitmap, y, x0: int, src: []u8, alpha: f64,
mode: Blend_Mode, coverage: []u8 = nil, mask: []u8 = nil) {
if dst == nil || len(src) < 4 { return }
ga := blend_clamp01(alpha)
if ga <= 0 { return }
row := bitmap_row(dst, y)
if row == nil { return }
skip := 0
x := x0
if x < 0 {
skip = -x
x = 0
}
total := len(src) / 4
if skip >= total { return }
count := min(total - skip, dst.width - x)
if count <= 0 { return }
n := dst.n
for i in 0 ..< count {
si := (skip + i) * 4
sa8 := src[si + 3]
if sa8 == 0 { continue }
cov := u8(255)
if coverage != nil {
ci := skip + i
cov = ci < len(coverage) ? coverage[ci] : 255
}
if cov == 0 { continue }
m := u8(255)
if mask != nil {
mi := skip + i
m = mi < len(mask) ? mask[mi] : 255
}
if m == 0 { continue }
as_ := ga * (f64(sa8) / 255) * (f64(cov) / 255) * (f64(m) / 255)
if as_ <= 0 { continue }
cs := blend_source_for(n, [3]f64{
f64(src[si]) / 255, f64(src[si + 1]) / 255, f64(src[si + 2]) / 255,
})
off := (x + i) * n
if off + n > len(row) { break }
cb, ab := blend_read_straight(row, off, n)
co, ao := blend_over(cb, ab, cs, as_, mode)
blend_write_premul(row, off, n, co, ao)
}
if blend_has_alpha(n) { dst.premultiplied = true }
}
// ---------------------------------------------------------------------------
// Whole-bitmap compositing: transparency groups
// ---------------------------------------------------------------------------
// blend_bitmap_restore puts `src` BACK onto `dst`, replacing rather than
// compositing, and interpolating towards what `dst` already holds wherever the
// mask is not opaque.
//
// This is what finishes a NON-ISOLATED transparency group (ISO 32000-1,
// 11.4.6). Such a group is handed a copy of its backdrop as its initial
// contents, so its finished bitmap already IS "contents over backdrop" -- and
// compositing that over the backdrop a second time blends the backdrop with
// itself. `C over C` under Normal gives ao = a + a(1-a) = 2a - a^2, so a
// backdrop pixel at alpha 0.5 comes back at 0.75, which is wrong everywhere the
// backdrop is not already opaque: any group nested inside another group, and
// every page rendered with a transparent background. Replacing is exact for
// both, and reduces to the old composite wherever the backdrop was opaque.
//
// `mask` is optional and is aligned with `src`, exactly as `blend_bitmap`'s is.
// Both bitmaps are PREMULTIPLIED.
@(private)
blend_bitmap_restore :: proc(dst, src: ^Bitmap, x, y: int, mask: ^Bitmap) {
if dst == nil || src == nil { return }
dn := dst.n
sn := src.n
sx0 := max(0, -x)
sy0 := max(0, -y)
sx1 := min(src.width, dst.width - x)
sy1 := min(src.height, dst.height - y)
if sx1 <= sx0 || sy1 <= sy0 { return }
for sy in sy0 ..< sy1 {
srow := bitmap_row(src, sy)
drow := bitmap_row(dst, y + sy)
if srow == nil || drow == nil { continue }
mrow: []u8
if mask != nil { mrow = bitmap_row(mask, sy) }
for sx in sx0 ..< sx1 {
soff := sx * sn
if soff + sn > len(srow) { break }
doff := (x + sx) * dn
if doff + dn > len(drow) { break }
m := u8(255)
if mask != nil {
moff := sx * mask.n
if mrow != nil && moff < len(mrow) { m = mrow[moff] } else { m = 255 }
}
if m == 0 { continue }
cs_raw, sa := blend_read_straight(srow, soff, sn)
cs := blend_source_for(dn, cs_raw)
if m == 255 {
blend_write_premul(drow, doff, dn, [3]f64{cs[0] * sa, cs[1] * sa, cs[2] * sa}, sa)
continue
}
// Interpolate in PREMULTIPLIED space: a transparent pixel has no
// recoverable colour, and lerping straight colour towards it would
// drag a black fringe in.
t := f64(m) / 255
cb, ab := blend_read_straight(drow, doff, dn)
ao := ab + (sa - ab) * t
co: [3]f64
for i in 0 ..< 3 {
co[i] = cb[i] * ab + (cs[i] * sa - cb[i] * ab) * t
}
blend_write_premul(drow, doff, dn, co, ao)
}
}
if blend_has_alpha(dn) { dst.premultiplied = true }
}
// blend_bitmap composites `src` onto `dst` with its top-left corner at
// (x, y) in `dst`. This is what `end_group` does with a rendered transparency
// group (11.4.6): the group was rendered into its own bitmap, and its constant
// alpha, blend mode and soft mask are applied once, here, to the group as a
// whole rather than to each operation inside it.
//
// Both bitmaps are PREMULTIPLIED. `alpha` is the group's constant alpha.
//
// `mask` is optional and is aligned with `src`, not with `dst`: mask pixel
// (i, j) modulates src pixel (i, j). A device holding a device-space soft mask
// passes a borrowed window onto it, which `Bitmap`'s independent `stride`
// exists to allow. Only component 0 of the mask is read, which is what
// `mask_from_bitmap` produces for both mask types. Pixels outside the mask read
// as 255, so a mask that is accidentally too small shows content rather than
// erasing it.
@(private)
blend_bitmap :: proc(dst, src: ^Bitmap, x, y: int, alpha: f64, mode: Blend_Mode, mask: ^Bitmap) {
if dst == nil || src == nil { return }
ga := blend_clamp01(alpha)
if ga <= 0 { return }
dn := dst.n
sn := src.n
// Clip the placement to the destination once, rather than testing every
// pixel: a group whose bitmap hangs off the page is the normal case.
sx0 := max(0, -x)
sy0 := max(0, -y)
sx1 := min(src.width, dst.width - x)
sy1 := min(src.height, dst.height - y)
if sx1 <= sx0 || sy1 <= sy0 { return }
for sy in sy0 ..< sy1 {
srow := bitmap_row(src, sy)
drow := bitmap_row(dst, y + sy)
if srow == nil || drow == nil { continue }
mrow: []u8
if mask != nil { mrow = bitmap_row(mask, sy) }
for sx in sx0 ..< sx1 {
soff := sx * sn
if soff + sn > len(srow) { break }
cs_raw, sa := blend_read_straight(srow, soff, sn)
if sa <= 0 { continue }
m := u8(255)
if mask != nil {
moff := sx * mask.n
if mrow != nil && moff < len(mrow) { m = mrow[moff] } else { m = 255 }
}
if m == 0 { continue }
as_ := sa * ga * (f64(m) / 255)
if as_ <= 0 { continue }
doff := (x + sx) * dn
if doff + dn > len(drow) { break }
cs := blend_source_for(dn, cs_raw)
cb, ab := blend_read_straight(drow, doff, dn)
co, ao := blend_over(cb, ab, cs, as_, mode)
blend_write_premul(drow, doff, dn, co, ao)
}
}
if blend_has_alpha(dn) { dst.premultiplied = true }
}
// ---------------------------------------------------------------------------
// Soft masks
// ---------------------------------------------------------------------------
// mask_from_bitmap turns a rendered group bitmap into a one-component mask
// (ISO 32000-2, 11.6.5).
//
// luminosity false selects an alpha mask -- the mask value is the group's
// alpha and its colour is ignored. true selects a luminosity
// mask: the group is composited onto an opaque backdrop and the
// mask value is the luminosity of the result.
// backdrop the /BC colour, already converted to RGB, used only by a
// luminosity mask. Black is the default the format specifies,
// and it is what makes the area a group does not cover mask
// everything out.
// transfer the /TR function, or nil. It is evaluated once per possible
// byte value into a 256-entry table rather than once per pixel:
// a type 4 PostScript transfer over a full page would otherwise
// run the calculator several million times.
//
// `src` may be premultiplied or straight; its `premultiplied` flag decides.
// The result is a fresh `n = 1` Bitmap the same size as `src`, allocated with
// `allocator` and OWNED BY THE CALLER -- free it with `bitmap_destroy`.
@(private)
mask_from_bitmap :: proc(src: ^Bitmap, luminosity: bool, backdrop: [3]f64,
transfer: ^Function, allocator := context.allocator) -> (out: Bitmap, err: Error) {
if src == nil || src.width <= 0 || src.height <= 0 { return {}, .Invalid_Argument }
out = bitmap_make(src.width, src.height, 1, allocator) or_return
lut := blend_transfer_table(transfer)
bd := [3]f64{blend_clamp01(backdrop[0]), blend_clamp01(backdrop[1]), blend_clamp01(backdrop[2])}
bd_lum_v := blend_quantize(blend_lum(bd))
sn := src.n
has_a := blend_has_alpha(sn)
for j in 0 ..< src.height {
srow := bitmap_row(src, j)
drow := bitmap_row(&out, j)
if srow == nil || drow == nil { continue }
for i in 0 ..< src.width {
soff := i * sn
if soff + sn > len(srow) { break }
v: u8
if !luminosity {
v = has_a ? srow[soff + sn - 1] : 255
} else {
a := has_a ? f64(srow[soff + sn - 1]) / 255 : 1.0
if a <= 0 {
// Nothing was painted here, so the mask is the backdrop.
v = bd_lum_v
} else {
c: [3]f64
if sn <= 2 {
g := f64(srow[soff]) / 255
c = {g, g, g}
} else {
c = {f64(srow[soff]) / 255, f64(srow[soff + 1]) / 255, f64(srow[soff + 2]) / 255}
}
// Composite over the opaque backdrop. A premultiplied
// source already carries the a*C factor; a straight one
// does not.
if !src.premultiplied {
c = {c[0] * a, c[1] * a, c[2] * a}
}
inv := 1 - a
v = blend_quantize(blend_lum([3]f64{
c[0] + inv * bd[0], c[1] + inv * bd[1], c[2] + inv * bd[2],
}))
}
}
drow[i] = lut[v]
}
}
return out, .None
}
// blend_transfer_table precomputes fn over the 256 byte values. An absent,
// malformed or wrongly-shaped function yields the identity, because a soft
// mask with an unusable /TR must still mask (11.6.5.2) rather than vanish.
@(private)
blend_transfer_table :: proc(fn: ^Function) -> (lut: [256]u8) {
for i in 0 ..< 256 { lut[i] = u8(i) }
if fn == nil || fn.n_in != 1 || fn.n_out < 1 { return }
in_buf: [1]f64
// Sized for the widest function the library will build, so a function that
// reports one output but writes n_out cannot run off the end.
out_buf: [MAX_FUNCTION_OUTPUTS]f64
for i in 0 ..< 256 {
in_buf[0] = f64(i) / 255
out_buf = {}
function_eval(fn, in_buf[:], out_buf[:])
lut[i] = blend_quantize(out_buf[0])
}
return
}