package pdf import "core:math" // Path_Verb tags one element of a path. The point count each verb consumes is // fixed: Move and Line take one, Curve takes three (two controls and an end // point), Close takes none. Path_Verb :: enum u8 { Move, Line, Curve, Close, } // Path is a sequence of subpaths in one coordinate space. // // Verbs and points are kept in parallel arrays rather than as a tagged union // per element: a page of text is tens of thousands of elements, and the // rasteriser walks them once per fill, so the layout that keeps points // contiguous is worth the small awkwardness of the cursor arithmetic. Path :: struct { verbs: [dynamic]Path_Verb, points: [dynamic]Point, // Start of the current subpath, for Close. start: Point, // Current point. Meaningless before the first Move. cur: Point, open: bool, } path_make :: proc(allocator := context.allocator, cap := 32) -> Path { return Path{ verbs = make([dynamic]Path_Verb, 0, cap, allocator), points = make([dynamic]Point, 0, cap, allocator), } } path_destroy :: proc(p: ^Path) { if p == nil { return } delete(p.verbs) delete(p.points) p^ = {} } path_clear :: proc(p: ^Path) { clear(&p.verbs) clear(&p.points) p.open = false } path_is_empty :: proc(p: ^Path) -> bool { return p == nil || len(p.verbs) == 0 } // --------------------------------------------------------------------------- // Construction // // Every entry point drops a non-finite coordinate rather than storing it. A // single NaN reaching the rasteriser turns a fill into an unbounded scanline // walk, and content streams contain NaNs -- usually from a division by a zero // text size -- often enough that filtering here is cheaper than checking in // the inner loop. // --------------------------------------------------------------------------- path_move_to :: proc(p: ^Path, x, y: f64) { if !is_finite(x) || !is_finite(y) { return } append(&p.verbs, Path_Verb.Move) append(&p.points, Point{x, y}) p.start = Point{x, y} p.cur = Point{x, y} p.open = true } path_line_to :: proc(p: ^Path, x, y: f64) { if !is_finite(x) || !is_finite(y) { return } // A line with no current point starts a subpath instead of being dropped: // content streams do emit "l" before "m", and viewers treat it as a move. if !p.open { path_move_to(p, x, y) return } append(&p.verbs, Path_Verb.Line) append(&p.points, Point{x, y}) p.cur = Point{x, y} } path_curve_to :: proc(p: ^Path, x1, y1, x2, y2, x3, y3: f64) { if !is_finite(x1) || !is_finite(y1) || !is_finite(x2) || !is_finite(y2) || !is_finite(x3) || !is_finite(y3) { return } if !p.open { path_move_to(p, x1, y1) } append(&p.verbs, Path_Verb.Curve) append(&p.points, Point{x1, y1}, Point{x2, y2}, Point{x3, y3}) p.cur = Point{x3, y3} } // path_curve_to_v is the "v" operator: the first control point is the current // point. path_curve_to_v :: proc(p: ^Path, x2, y2, x3, y3: f64) { path_curve_to(p, p.cur.x, p.cur.y, x2, y2, x3, y3) } // path_curve_to_y is the "y" operator: the second control point is the end // point. path_curve_to_y :: proc(p: ^Path, x1, y1, x3, y3: f64) { path_curve_to(p, x1, y1, x3, y3, x3, y3) } path_close :: proc(p: ^Path) { if !p.open || len(p.verbs) == 0 { return } if p.verbs[len(p.verbs) - 1] == .Close { return } append(&p.verbs, Path_Verb.Close) p.cur = p.start } // path_rect appends a closed rectangle, in the winding direction the "re" // operator specifies (counter-clockwise in user space). path_rect :: proc(p: ^Path, x, y, w, h: f64) { if !is_finite(x) || !is_finite(y) || !is_finite(w) || !is_finite(h) { return } path_move_to(p, x, y) path_line_to(p, x + w, y) path_line_to(p, x + w, y + h) path_line_to(p, x, y + h) path_close(p) } // path_ellipse appends a closed ellipse inscribed in the rectangle, built from // four cubics. The magic constant is the standard circle-to-cubic ratio. path_ellipse :: proc(p: ^Path, r: Rect) { K :: 0.5522847498307933 cx := (r.x0 + r.x1) * 0.5 cy := (r.y0 + r.y1) * 0.5 rx := (r.x1 - r.x0) * 0.5 ry := (r.y1 - r.y0) * 0.5 ox := rx * K oy := ry * K path_move_to(p, cx + rx, cy) path_curve_to(p, cx + rx, cy + oy, cx + ox, cy + ry, cx, cy + ry) path_curve_to(p, cx - ox, cy + ry, cx - rx, cy + oy, cx - rx, cy) path_curve_to(p, cx - rx, cy - oy, cx - ox, cy - ry, cx, cy - ry) path_curve_to(p, cx + ox, cy - ry, cx + rx, cy - oy, cx + rx, cy) path_close(p) } // path_append copies every element of `src` onto `dst`. path_append :: proc(dst: ^Path, src: ^Path) { if src == nil { return } append(&dst.verbs, ..src.verbs[:]) append(&dst.points, ..src.points[:]) dst.cur = src.cur dst.start = src.start dst.open = src.open || dst.open } // path_append_transformed copies `src` onto `dst` with `m` applied. path_append_transformed :: proc(dst: ^Path, src: ^Path, m: Matrix) { if src == nil { return } for pt in src.points { append(&dst.points, transform_point(m, pt)) } append(&dst.verbs, ..src.verbs[:]) dst.cur = transform_point(m, src.cur) dst.start = transform_point(m, src.start) dst.open = src.open || dst.open } path_transform :: proc(p: ^Path, m: Matrix) { for &pt in p.points { pt = transform_point(m, pt) } p.cur = transform_point(m, p.cur) p.start = transform_point(m, p.start) } path_clone :: proc(p: ^Path, allocator := context.allocator) -> Path { out := path_make(allocator, max(len(p.verbs), 8)) append(&out.verbs, ..p.verbs[:]) append(&out.points, ..p.points[:]) out.cur = p.cur out.start = p.start out.open = p.open return out } // --------------------------------------------------------------------------- // Bounds // --------------------------------------------------------------------------- // path_bounds is the control-point hull, which is a conservative // over-estimate for curves: a cubic never leaves the convex hull of its four // points. Good enough for culling; `path_tight_bounds` is the exact one. path_bounds :: proc(p: ^Path, m := MATRIX_IDENTITY) -> Rect { if p == nil || len(p.points) == 0 { return RECT_EMPTY } identity := matrix_is_identity(m) first := identity ? p.points[0] : transform_point(m, p.points[0]) r := Rect{first.x, first.y, first.x, first.y} for i in 1 ..< len(p.points) { pt := identity ? p.points[i] : transform_point(m, p.points[i]) r.x0 = min(r.x0, pt.x) r.y0 = min(r.y0, pt.y) r.x1 = max(r.x1, pt.x) r.y1 = max(r.y1, pt.y) } return r } // path_tight_bounds solves for the cubic's extrema instead of using the hull. // Worth it for the clip stack, where a loose box costs whole scanlines. path_tight_bounds :: proc(p: ^Path, m := MATRIX_IDENTITY) -> Rect { if p == nil || len(p.verbs) == 0 { return RECT_EMPTY } r := RECT_EMPTY have := false include :: proc(r: ^Rect, have: ^bool, pt: Point) { if !have^ { r^ = Rect{pt.x, pt.y, pt.x, pt.y} have^ = true return } r.x0 = min(r.x0, pt.x) r.y0 = min(r.y0, pt.y) r.x1 = max(r.x1, pt.x) r.y1 = max(r.y1, pt.y) } cur := Point{} i := 0 for verb in p.verbs { switch verb { case .Move, .Line: if i >= len(p.points) { break } pt := transform_point(m, p.points[i]) include(&r, &have, pt) cur = pt i += 1 case .Curve: if i + 2 >= len(p.points) { break } c1 := transform_point(m, p.points[i]) c2 := transform_point(m, p.points[i + 1]) e := transform_point(m, p.points[i + 2]) include(&r, &have, e) cubic_extrema_include(&r, &have, cur, c1, c2, e) cur = e i += 3 case .Close: // no points } } return have ? r : RECT_EMPTY } // cubic_extrema_include folds the interior extrema of one cubic into the box. // The derivative of a cubic Bezier is a quadratic; its roots in (0,1) are the // only places the curve can turn. @(private) cubic_extrema_include :: proc(r: ^Rect, have: ^bool, p0, p1, p2, p3: Point) { for axis in 0 ..< 2 { v0 := p0[axis] v1 := p1[axis] v2 := p2[axis] v3 := p3[axis] a := -v0 + 3 * v1 - 3 * v2 + v3 b := 2 * (v0 - 2 * v1 + v2) c := v1 - v0 fold :: proc(r: ^Rect, have: ^bool, axis: int, value: f64) { if !is_finite(value) { return } if !have^ { r^ = Rect{value, value, value, value} have^ = true return } if axis == 0 { r.x0 = min(r.x0, value) r.x1 = max(r.x1, value) } else { r.y0 = min(r.y0, value) r.y1 = max(r.y1, value) } } eval :: proc(v0, v1, v2, v3, t: f64) -> f64 { mt := 1 - t return mt * mt * mt * v0 + 3 * mt * mt * t * v1 + 3 * mt * t * t * v2 + t * t * t * v3 } // Degenerate to a linear derivative: solve b*t + c == 0. if abs(a) < 1e-12 { if abs(b) > 1e-12 { t := -c / b if t > 0 && t < 1 { fold(r, have, axis, eval(v0, v1, v2, v3, t)) } } continue } // 3*a*t^2 + 2*b'*t + 3*c == 0, with the constant factors cancelled. disc := b * b - 4 * a * c if disc < 0 { continue } sq := math.sqrt(disc) for t in ([2]f64{(-b + sq) / (2 * a), (-b - sq) / (2 * a)}) { if t > 0 && t < 1 { fold(r, have, axis, eval(v0, v1, v2, v3, t)) } } } } // --------------------------------------------------------------------------- // Flattening // --------------------------------------------------------------------------- // Flat_Subpath describes one polyline inside a Flat_Path. Flat_Subpath :: struct { start: int, // index into Flat_Path.points count: int, closed: bool, } // Flat_Path is a path reduced to polylines, in device space. The rasteriser // and the stroker both consume this rather than curves. // // Subpaths are described explicitly rather than by a start-index array with a // sentinel, so that several paths can be flattened into one Flat_Path -- which // is exactly what a clip stack and a tiling pattern need to do. Flat_Path :: struct { points: [dynamic]Point, subpaths: [dynamic]Flat_Subpath, } flat_path_make :: proc(allocator := context.allocator) -> Flat_Path { return Flat_Path{ points = make([dynamic]Point, 0, 64, allocator), subpaths = make([dynamic]Flat_Subpath, 0, 8, allocator), } } flat_path_destroy :: proc(fp: ^Flat_Path) { if fp == nil { return } delete(fp.points) delete(fp.subpaths) fp^ = {} } flat_path_clear :: proc(fp: ^Flat_Path) { clear(&fp.points) clear(&fp.subpaths) } flat_path_count :: proc(fp: ^Flat_Path) -> int { return len(fp.subpaths) } flat_path_slice :: proc(fp: ^Flat_Path, i: int) -> []Point { if i < 0 || i >= len(fp.subpaths) { return nil } sp := fp.subpaths[i] if sp.start < 0 || sp.start + sp.count > len(fp.points) { return nil } return fp.points[sp.start:sp.start + sp.count] } flat_path_bounds :: proc(fp: ^Flat_Path) -> Rect { if fp == nil || len(fp.points) == 0 { return RECT_EMPTY } r := Rect{fp.points[0].x, fp.points[0].y, fp.points[0].x, fp.points[0].y} for pt in fp.points { r.x0 = min(r.x0, pt.x) r.y0 = min(r.y0, pt.y) r.x1 = max(r.x1, pt.x) r.y1 = max(r.y1, pt.y) } return r } // FLATNESS is the maximum distance, in device pixels, between the true curve // and the polyline that replaces it. A quarter of a pixel is below what // anti-aliased output can show and keeps segment counts sane on text. FLATNESS :: 0.25 // path_flatten transforms `p` by `m` and subdivides every curve into line // segments, appending the result to `out`. Existing contents of `out` are kept. // // Subdivision is adaptive and depth-limited: a curve with a cusp or a very // large control polygon would otherwise subdivide until the stack ran out. // // `cull`, when given, is the region -- in the same space `m` maps into -- that // the result will actually be drawn or clipped through. A curve that cannot // reach it is replaced by its chord instead of being subdivided, which is what // keeps a path whose control points are a billion units off the page from // costing a million points per painting operator. See `flatten_reaches`: the // substitution is exact, not an approximation. path_flatten :: proc(out: ^Flat_Path, p: ^Path, m: Matrix, flatness := FLATNESS, cull: Maybe(Rect) = nil) { if out == nil || p == nil || len(p.verbs) == 0 { return } tol := max(flatness, 0.01) cull_box, culling := cull.? cur := Point{} i := 0 // Index into out.subpaths of the subpath being built, or -1. active := -1 for verb in p.verbs { if len(out.points) >= MAX_FLATTEN_POINTS { break } switch verb { case .Move: if i >= len(p.points) { break } cur = transform_point(m, p.points[i]) i += 1 append(&out.subpaths, Flat_Subpath{start = len(out.points), count = 1}) append(&out.points, cur) active = len(out.subpaths) - 1 case .Line: if i >= len(p.points) { break } pt := transform_point(m, p.points[i]) i += 1 if active < 0 { // A segment after a close begins a new subpath AT the current // point, which `h` left at the start of the subpath it closed // (ISO 32000-2, 8.5.2.1). Seeding it is what keeps the // connecting segment: without `cur` the new subpath is a // single stray point, which a fill drops and a stroke turns // into a dot in the wrong place. The .Curve arm below has // always done this; the two must agree. append(&out.subpaths, Flat_Subpath{start = len(out.points), count = 1}) append(&out.points, cur) active = len(out.subpaths) - 1 } append(&out.points, pt) out.subpaths[active].count += 1 cur = pt case .Curve: if i + 2 >= len(p.points) { break } c1 := transform_point(m, p.points[i]) c2 := transform_point(m, p.points[i + 1]) e := transform_point(m, p.points[i + 2]) i += 3 if active < 0 { append(&out.subpaths, Flat_Subpath{start = len(out.points), count = 1}) append(&out.points, cur) active = len(out.subpaths) - 1 } before := len(out.points) flatten_cubic(&out.points, cur, c1, c2, e, tol, 0, cull_box, culling) append(&out.points, e) out.subpaths[active].count += len(out.points) - before cur = e case .Close: if active >= 0 { out.subpaths[active].closed = true sp := out.subpaths[active] if sp.count > 0 { cur = out.points[sp.start] } } // A subpath after a close starts fresh, but at the same point: // "m 0 0 l 10 0 h l 20 0" continues from the start point. active = -1 } } } // MAX_FLATTEN_DEPTH bounds recursion at 2^16 segments per curve, which no // legitimate curve approaches and a degenerate one would exceed forever. MAX_FLATTEN_DEPTH :: 16 // MAX_FLATTEN_POINTS bounds the whole of one flattening. MAX_FLATTEN_DEPTH caps // a SINGLE curve, and nothing capped the number of curves: a content stream may // append hundreds of thousands of `c` segments to one path before the painting // operator that clears it, and a cubic whose control points are a billion units // off the chord subdivides to the depth limit every time. One `f` could // therefore ask for gigabytes of polyline -- `rd_fill_path` materialises the // whole flattened path before `raster_fill` ever sees the clip that would have // discarded it. A million points is two orders of magnitude past the densest // real path and costs 16 MB; past it the geometry is truncated, which draws // part of a shape no viewer could have drawn all of. MAX_FLATTEN_POINTS :: 1 << 20 // flatten_reaches reports whether any part of the cubic can lie inside `box`. // // Every point of a Bezier lies inside the convex hull of its control points, // so a hull that misses the box means the whole curve does. It is used to stop // subdividing there, which leaves the chord between the curve's ends in its // place -- and that chord is inside the same hull. Deforming one to the other // therefore never crosses a point of `box`, so the winding number, and with it // both fill rules, is unchanged everywhere the box covers. The picture inside // the clip is exactly the picture the full subdivision would have drawn. // // A non-finite coordinate makes every comparison false and the curve is kept, // which is the same thing the un-culled path does with it. @(private) flatten_reaches :: proc(p0, p1, p2, p3: Point, box: Rect) -> bool { if max(p0.x, p1.x, p2.x, p3.x) < box.x0 { return false } if min(p0.x, p1.x, p2.x, p3.x) > box.x1 { return false } if max(p0.y, p1.y, p2.y, p3.y) < box.y0 { return false } if min(p0.y, p1.y, p2.y, p3.y) > box.y1 { return false } return true } @(private) flatten_cubic :: proc(out: ^[dynamic]Point, p0, p1, p2, p3: Point, tol: f64, depth: int, cull: Rect, culling: bool) { if depth >= MAX_FLATTEN_DEPTH || len(out) >= MAX_FLATTEN_POINTS { return } // Tested at every level, not only the first: the halves of a curve that // sweeps across the page and back are each smaller than the whole, so a // curve nothing culls at the top collapses to a handful of segments a few // levels down. if culling && !flatten_reaches(p0, p1, p2, p3, cull) { return } // Flatness test: how far the control points sit off the chord. d1 and d2 // are perpendicular distances scaled by the chord length, so comparing // (d1+d2)^2 against tol^2 * chord^2 is the same as requiring the two // perpendicular distances to sum to less than `tol` -- and the curve's own // deviation from the chord is at most three quarters of that. dx := p3.x - p0.x dy := p3.y - p0.y chord2 := dx * dx + dy * dy if chord2 < 1e-12 { // A curve that comes back to its own start point has no chord to // measure against, and the cross-product test would call it flat -- // collapsing an entire loop into a zero-length segment. Measure the // control points against the start point instead. c1 := (p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y) c2 := (p2.x - p0.x) * (p2.x - p0.x) + (p2.y - p0.y) * (p2.y - p0.y) if max(c1, c2) <= tol * tol { return } } else { d1 := abs((p1.x - p3.x) * dy - (p1.y - p3.y) * dx) d2 := abs((p2.x - p3.x) * dy - (p2.y - p3.y) * dx) dd := d1 + d2 if dd * dd <= tol * tol * chord2 { return } } // de Casteljau split at t = 0.5. p01 := Point{(p0.x + p1.x) * 0.5, (p0.y + p1.y) * 0.5} p12 := Point{(p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5} p23 := Point{(p2.x + p3.x) * 0.5, (p2.y + p3.y) * 0.5} p012 := Point{(p01.x + p12.x) * 0.5, (p01.y + p12.y) * 0.5} p123 := Point{(p12.x + p23.x) * 0.5, (p12.y + p23.y) * 0.5} mid := Point{(p012.x + p123.x) * 0.5, (p012.y + p123.y) * 0.5} flatten_cubic(out, p0, p01, p012, mid, tol, depth + 1, cull, culling) append(out, mid) flatten_cubic(out, mid, p123, p23, p3, tol, depth + 1, cull, culling) }