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 }