initial commit

This commit is contained in:
2026-09-25 23:32:29 +02:00
commit fff8b677f8
114 changed files with 84233 additions and 0 deletions
+513
View File
@@ -0,0 +1,513 @@
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
}
}