package pdf import "core:math" import "core:mem" // Anti-aliased scanline fill. // // WHICH ALGORITHM, AND WHY // // This is a signed-area cell accumulator in the style of FreeType's `smooth` // renderer, not a sub-scanline sampler. Every line segment is chopped at the // scanline boundaries it crosses and then at the pixel boundaries it crosses // within a scanline, and each resulting piece deposits two numbers into the // cell it lies in: // // cover[i] += dy the signed height the edge spans // area [i] += dy * (fx1 + fx2) twice the trapezoid between the edge // and the LEFT wall of the cell // // A left-to-right prefix sum over `cover` then gives the winding-weighted // coverage of every cell exactly: // // coverage(i) = sum(cover[j] for j < i) + cover[i] - area[i]/2 // // which is the integral of the winding number over the pixel -- exact in both // axes for a polygon, with no sampling error at all. Sixteen sub-scanlines // would quantise vertical coverage to 1/16 and cost sixteen passes over every // edge; this costs one, and a near-horizontal glyph stem comes out smooth // instead of banded. The one place it is an approximation rather than an // identity is a pixel that a path enters with winding +1 and leaves with // winding -1: the integral cancels where the true nonzero rule would fill. // That is a sub-pixel artefact at a self-intersection, it is what FreeType, // stb_truetype and every other accumulator-based rasteriser do, and it is // invisible next to the cost of the alternative. // // The prefix sum is kept in floating point rather than 26.6 fixed point // because the input is already f64 device coordinates: rounding to a fixed // grid first would throw away precision that the analytic area then pretends // to have. // Span_Proc receives the coverage of one horizontal run on scanline `y`, // beginning at device pixel `x0`. Values are 0..255. The slice is scratch // owned by the rasteriser and is only valid for the duration of the call. // // Runs arrive in increasing `y`, and in increasing `x` within a row, and never // overlap, so a consumer can write straight into a row without sorting. @(private) Span_Proc :: #type proc(user: rawptr, y: int, x0: int, coverage: []u8) // Raster_Clip is the device-pixel box outside which nothing is drawn, given as // a half-open range: x0 <= x < x1, y0 <= y < y1. @(private) Raster_Clip :: struct { x0, y0, x1, y1: int, } // MAX_RASTER_EDGES caps the edge table. A page-sized fill needs a handful of // edges and the most complex real content -- a vector map -- a few hundred // thousand; a million is past anything legitimate and stops a hostile content // stream from turning a 200-byte path into gigabytes of edges. Past the cap // the remaining segments are dropped, which distorts the shape but keeps the // page rendering, per the degrade-don't-fail rule. @(private) MAX_RASTER_EDGES :: 1 << 20 // RASTER_COORD_MAX is where coordinates stop being geometry and start being // noise. Everything is clipped to the clip box anyway, so the only job of this // clamp is to keep infinities out of the arithmetic that computes the clip // intersection. @(private) RASTER_COORD_MAX :: 1e15 // Raster_Edge is one non-horizontal segment, already clipped to the raster // box in both axes, oriented downwards (y0 < y1) with the original direction // kept in `dir` -- the winding number needs the direction, the sweep needs the // order, and carrying both separately is cheaper than re-deriving either. @(private) Raster_Edge :: struct { x0, y0: f64, x1, y1: f64, // 1/(y1-y0), for interpolating x at a scanline boundary. inv_dy: f64, // +1 when the segment ran down the page, -1 when it ran up it. dir: f64, // Next edge starting on the same scanline, or -1. next: i32, } @(private) Raster_State :: struct { // The raster box: the clip intersected with the path's bounds. Cells run // rx0 ..= rx1 inclusive -- one past the right edge is a guard cell that // collects the winding of everything clipped off to the right and is never // emitted. rx0, rx1, ry0, ry1: int, fx0, fx1: f64, edges: [dynamic]Raster_Edge, // Head of the list of edges that start on each scanline, indexed from ry0. buckets: []i32, active: []i32, n_active: int, cover: []f64, area: []f64, covbuf: []u8, // Cells touched on the row being built, so the next row only has to clear // what was written. A page of text is thousands of narrow rows; clearing // the full clip width for each would dominate the whole fill. min_x, max_x: int, } // raster_fill scan-converts `fp` and reports its coverage through `span`. // // Subpaths are closed implicitly whether or not `Flat_Subpath.closed` is set: // that is what the fill operators mean (ISO 32000-2, 8.5.3.3.1). Horizontal // segments are dropped, since they carry no winding. // // Degenerate input -- no path, no points, a single point, a subpath that never // leaves one horizontal line, a path wholly outside `clip` -- produces no // spans and no error. `allocator` is used for scratch only; everything it // allocates is released before this returns. @(private) raster_fill :: proc(fp: ^Flat_Path, even_odd: bool, clip: Raster_Clip, span: Span_Proc, user: rawptr, allocator := context.allocator) -> Error { if span == nil { return .Invalid_Argument } if fp == nil || len(fp.points) == 0 || len(fp.subpaths) == 0 { return .None } if clip.x1 <= clip.x0 || clip.y1 <= clip.y0 { return .None } // Narrowing the raster box to the path is not just an optimisation: it is // what keeps a glyph in the corner of a 300 dpi page from allocating and // clearing a 2550-cell row, and what bounds a path whose bounds are 1e9 // wide to the size of the clip. b := flat_path_bounds(fp) r := Raster_State{ rx0 = raster_floor_clamped(b.x0, clip.x0, clip.x1), rx1 = raster_ceil_clamped (b.x1, clip.x0, clip.x1), ry0 = raster_floor_clamped(b.y0, clip.y0, clip.y1), ry1 = raster_ceil_clamped (b.y1, clip.y0, clip.y1), } if r.rx1 <= r.rx0 || r.ry1 <= r.ry0 { return .None } r.fx0 = f64(r.rx0) r.fx1 = f64(r.rx1) width := r.rx1 - r.rx0 height := r.ry1 - r.ry0 // One allocation each, up front. Nothing below this line allocates, which // is what makes the per-scanline cost independent of the path. segments := 0 for sp in fp.subpaths { if sp.count >= 2 { segments += sp.count } } if segments <= 0 { return .None } aerr: mem.Allocator_Error r.edges, aerr = make([dynamic]Raster_Edge, 0, min(segments, 1024), allocator) if aerr != nil { return .Out_Of_Memory } defer delete(r.edges) r.buckets, aerr = make([]i32, height, allocator) if aerr != nil { return .Out_Of_Memory } defer delete(r.buckets, allocator) for &h in r.buckets { h = -1 } r.cover, aerr = make([]f64, width + 1, allocator) if aerr != nil { return .Out_Of_Memory } defer delete(r.cover, allocator) r.area, aerr = make([]f64, width + 1, allocator) if aerr != nil { return .Out_Of_Memory } defer delete(r.area, allocator) r.covbuf, aerr = make([]u8, width, allocator) if aerr != nil { return .Out_Of_Memory } defer delete(r.covbuf, allocator) for i in 0 ..< len(fp.subpaths) { pts := flat_path_slice(fp, i) if len(pts) < 2 { continue } for j in 0 ..< len(pts) - 1 { raster_add_edge(&r, pts[j], pts[j + 1]) } // The closing segment. A subpath whose last point already coincides // with its first would only add a zero-length edge, which is dropped // by the horizontal test anyway, but skipping it keeps the edge table // honest for the common case of an explicitly closed rectangle. if pts[len(pts) - 1] != pts[0] { raster_add_edge(&r, pts[len(pts) - 1], pts[0]) } } if len(r.edges) == 0 { return .None } r.active, aerr = make([]i32, len(r.edges), allocator) if aerr != nil { return .Out_Of_Memory } defer delete(r.active, allocator) for y in r.ry0 ..< r.ry1 { for e := r.buckets[y - r.ry0]; e >= 0; { next := r.edges[e].next if r.n_active < len(r.active) { r.active[r.n_active] = e r.n_active += 1 } e = next } if r.n_active == 0 { continue } row_top := f64(y) row_bot := f64(y + 1) r.min_x = r.rx1 + 1 r.max_x = r.rx0 - 1 for k in 0 ..< r.n_active { e := &r.edges[r.active[k]] ya := max(row_top, e.y0) yb := min(row_bot, e.y1) if yb <= ya { continue } xa := ya <= e.y0 ? e.x0 : raster_edge_x(&r, e, ya) xb := yb >= e.y1 ? e.x1 : raster_edge_x(&r, e, yb) raster_accumulate(&r, xa, ya, xb, yb, e.dir) } raster_sweep(&r, y, even_odd, span, user) if r.min_x <= r.max_x { for i in r.min_x ..= r.max_x { r.cover[i - r.rx0] = 0 r.area [i - r.rx0] = 0 } } w := 0 for k in 0 ..< r.n_active { if r.edges[r.active[k]].y1 > row_bot { r.active[w] = r.active[k] w += 1 } } r.n_active = w } return .None } // --------------------------------------------------------------------------- // Building the edge table // --------------------------------------------------------------------------- // raster_add_edge clips one segment to the raster box and appends the one to // three edges that survive. // // Clipping in y is a plain truncation: coverage on a scanline depends only on // the parts of edges inside that scanline, so anything above or below the box // cannot affect it. Clipping in x is NOT a truncation -- a segment to the left // of the box still winds every pixel to its right -- so the parts outside are // replaced by vertical edges standing on the box's walls, which wind // identically. That substitution is what makes a path 10^9 units wide cost the // same as one the width of the clip. @(private) raster_add_edge :: proc(r: ^Raster_State, p0, p1: Point) { if len(r.edges) >= MAX_RASTER_EDGES { return } x0 := raster_sane(p0.x) y0 := raster_sane(p0.y) x1 := raster_sane(p1.x) y1 := raster_sane(p1.y) if y0 == y1 { return } dir := 1.0 if y0 > y1 { x0, x1 = x1, x0 y0, y1 = y1, y0 dir = -1 } top := f64(r.ry0) bot := f64(r.ry1) cy0 := max(y0, top) cy1 := min(y1, bot) if cy1 <= cy0 { return } cx0 := cy0 == y0 ? x0 : raster_lerp_x(x0, y0, x1, y1, cy0) cx1 := cy1 == y1 ? x1 : raster_lerp_x(x0, y0, x1, y1, cy1) // Split where the segment crosses the left and right walls, so that each // piece is wholly inside or wholly outside and clamping its endpoints is // exact rather than a bend. ys: [4]f64 n := 0 ys[n] = cy0; n += 1 dx := cx1 - cx0 if dx != 0 { for wall in ([2]f64{r.fx0, r.fx1}) { if (cx0 < wall) == (cx1 < wall) { continue } t := (wall - cx0) / dx if !(t > 0 && t < 1) { continue } yy := cy0 + (cy1 - cy0) * t if yy > cy0 && yy < cy1 { ys[n] = yy; n += 1 } } } ys[n] = cy1; n += 1 if n == 4 && ys[1] > ys[2] { ys[1], ys[2] = ys[2], ys[1] } for i in 0 ..< n - 1 { ya := ys[i] yb := ys[i + 1] if yb <= ya { continue } xa := clamp(ya == cy0 ? cx0 : raster_lerp_x(cx0, cy0, cx1, cy1, ya), r.fx0, r.fx1) xb := clamp(yb == cy1 ? cx1 : raster_lerp_x(cx0, cy0, cx1, cy1, yb), r.fx0, r.fx1) raster_push_edge(r, xa, ya, xb, yb, dir) } } @(private) raster_push_edge :: proc(r: ^Raster_State, x0, y0, x1, y1, dir: f64) { if len(r.edges) >= MAX_RASTER_EDGES { return } dy := y1 - y0 if !(dy > 0) { return } row := int(math.floor(y0)) row = clamp(row, r.ry0, r.ry1 - 1) idx := i32(len(r.edges)) append(&r.edges, Raster_Edge{ x0 = x0, y0 = y0, x1 = x1, y1 = y1, inv_dy = 1 / dy, dir = dir, next = r.buckets[row - r.ry0], }) r.buckets[row - r.ry0] = idx } // raster_edge_x interpolates the edge's x at a scanline boundary. The guards // are not decoration: an edge whose y extent is denormally small has an // infinite 1/dy, and 0 * infinity is a NaN that would otherwise reach the cell // walk and index an array with it. @(private) raster_edge_x :: proc(r: ^Raster_State, e: ^Raster_Edge, y: f64) -> f64 { t := (y - e.y0) * e.inv_dy if !is_finite(t) { t = 0 } t = clamp(t, 0, 1) x := e.x0 + (e.x1 - e.x0) * t if !is_finite(x) { x = e.x0 } return clamp(x, r.fx0, r.fx1) } @(private) raster_lerp_x :: proc "contextless" (x0, y0, x1, y1, y: f64) -> f64 { dy := y1 - y0 t := dy != 0 ? (y - y0) / dy : 0 if !is_finite(t) { t = 0 } t = clamp(t, 0, 1) x := x0 + (x1 - x0) * t return is_finite(x) ? x : x0 } @(private) raster_sane :: proc "contextless" (v: f64) -> f64 { if v != v { return 0 } return clamp(v, -RASTER_COORD_MAX, RASTER_COORD_MAX) } // raster_floor_clamped and raster_ceil_clamped convert a bound to a pixel // index without ever letting a huge or non-finite f64 reach the integer // conversion, which is undefined when the value does not fit. A NaN bound // widens to the clip rather than narrowing to nothing: dropping content is a // worse failure than drawing too much. @(private) raster_floor_clamped :: proc "contextless" (v: f64, lo, hi: int) -> int { if v != v || v <= f64(lo) { return lo } if v >= f64(hi) { return hi } return int(math.floor(v)) } @(private) raster_ceil_clamped :: proc "contextless" (v: f64, lo, hi: int) -> int { if v != v || v >= f64(hi) { return hi } if v <= f64(lo) { return lo } return int(math.ceil(v)) } // --------------------------------------------------------------------------- // Accumulation // --------------------------------------------------------------------------- @(private) raster_cell :: proc "contextless" (r: ^Raster_State, x: f64) -> int { if !(x > r.fx0) { return r.rx0 } if x >= r.fx1 { return r.rx1 } return int(math.floor(x)) } // raster_accumulate deposits one piece of one edge -- already confined to a // single scanline -- into the cells it crosses. @(private) raster_accumulate :: proc(r: ^Raster_State, xa, ya, xb, yb, dir: f64) { if !(yb > ya) { return } ex1 := raster_cell(r, xa) ex2 := raster_cell(r, xb) if ex1 == ex2 { raster_cell_add(r, ex1, (yb - ya) * dir, xa, xb) return } // dy per unit x. Both endpoints sit inside the raster box, so the number // of cells crossed is bounded by its width. dydx := (yb - ya) / (xb - xa) step := xb > xa ? 1 : -1 ex := ex1 xc := xa yc := ya for ex != ex2 { xn := f64(step > 0 ? ex + 1 : ex) yn := ya + (xn - xa) * dydx if !(yn > yc) { yn = yc } if yn > yb { yn = yb } raster_cell_add(r, ex, (yn - yc) * dir, xc, xn) xc = xn yc = yn ex += step } raster_cell_add(r, ex, (yb - yc) * dir, xc, xb) } // raster_cell_add records `dy` of signed height crossing cell `ex` between the // x offsets `x_from` and `x_to`. `area` holds twice the trapezoid on the left // of the edge, which the sweep subtracts from the running cover. @(private) raster_cell_add :: proc "contextless" (r: ^Raster_State, ex: int, dy, x_from, x_to: f64) { if dy == 0 { return } i := ex - r.rx0 if i < 0 || i >= len(r.cover) { return } fx := f64(ex) r.cover[i] += dy r.area [i] += dy * ((x_from - fx) + (x_to - fx)) r.min_x = min(r.min_x, ex) r.max_x = max(r.max_x, ex) } // --------------------------------------------------------------------------- // Sweep // --------------------------------------------------------------------------- // raster_coverage folds a winding-weighted coverage into a 0..255 alpha. // // Nonzero clamps the magnitude; even-odd folds it, which is the floating-point // spelling of the usual integer trick of masking the accumulator to nine bits // and mirroring everything above the halfway point. Even-odd is not a corner // case: it is what `f*` and `B*` mean, and Type1 glyph counters depend on it. @(private) raster_coverage :: proc "contextless" (cov: f64, even_odd: bool) -> u8 { a := abs(cov) if even_odd { a -= 2 * math.floor(a * 0.5) if a > 1 { a = 2 - a } } else if a > 1 { a = 1 } // Written as a positive test so a NaN that survived every other guard // becomes transparent rather than an out-of-range conversion. if !(a > 0) { return 0 } v := int(a * 255 + 0.5) return u8(clamp(v, 0, 255)) } @(private) raster_sweep :: proc(r: ^Raster_State, y: int, even_odd: bool, span: Span_Proc, user: rawptr) { if r.min_x > r.max_x { return } lo := r.min_x // The sweep stops at the last cell that any edge touched, and never needs // a tail: a closed path's windings sum to zero at the far right, and the // ones that would have closed beyond the clip were moved onto the guard // cell rather than dropped, so `running` is back at zero by the time the // touched range ends. The guard cell itself is one past the last emittable // pixel, which is why `hi` is capped rather than clamped away. hi := min(r.max_x, r.rx1 - 1) if lo > hi { return } end := hi + 1 running := 0.0 for i in lo ..= hi { j := i - r.rx0 c := running + r.cover[j] - r.area[j] * 0.5 running += r.cover[j] r.covbuf[j] = raster_coverage(c, even_odd) } i := lo for i < end { if r.covbuf[i - r.rx0] == 0 { i += 1 continue } j := i for j < end && r.covbuf[j - r.rx0] != 0 { j += 1 } span(user, y, i, r.covbuf[i - r.rx0 : j - r.rx0]) i = j } }