package pdf import "core:math" // Point is a coordinate pair. PDF user space is y-up with the origin at the // bottom-left of the page; device space here is y-down with the origin at the // top-left of the bitmap, and the flip lives in the base matrix that // `render_page` builds, not in the geometry code. Point :: [2]f64 // Matrix is a PDF transformation matrix, stored in the order the format writes // it: `[a b c d e f]` standing for // // | a b 0 | // | c d 0 | // | e f 1 | // // with row vectors, so a point maps as x' = a*x + c*y + e, y' = b*x + d*y + f. Matrix :: struct { a, b, c, d, e, f: f64, } MATRIX_IDENTITY :: Matrix{1, 0, 0, 1, 0, 0} // Rect is an axis-aligned rectangle. x0/y0 is not required to be the lower // corner on construction -- `rect_normalize` sorts them -- because PDF // rectangle arrays are explicitly allowed to give their corners in either // order. Rect :: struct { x0, y0, x1, y1: f64, } RECT_EMPTY :: Rect{0, 0, 0, 0} // RECT_INFINITE is the identity for intersection: clipping against it is a // no-op, which is what an unset clip path needs to be. RECT_INFINITE :: Rect{-1e30, -1e30, 1e30, 1e30} // A4 in points, the fallback page size for a page with no usable /MediaBox. DEFAULT_PAGE_RECT :: Rect{0, 0, 595.276, 841.89} // --------------------------------------------------------------------------- // Matrices // --------------------------------------------------------------------------- // matrix_mul returns m applied first, then n. This is the order the `cm` // operator wants: `CTM = cm_operand * CTM`. matrix_mul :: proc "contextless" (m, n: Matrix) -> Matrix { return Matrix{ a = m.a * n.a + m.b * n.c, b = m.a * n.b + m.b * n.d, c = m.c * n.a + m.d * n.c, d = m.c * n.b + m.d * n.d, e = m.e * n.a + m.f * n.c + n.e, f = m.e * n.b + m.f * n.d + n.f, } } translate_matrix :: proc "contextless" (tx, ty: f64) -> Matrix { return Matrix{1, 0, 0, 1, tx, ty} } scale_matrix :: proc "contextless" (sx, sy: f64) -> Matrix { return Matrix{sx, 0, 0, sy, 0, 0} } // rotate_matrix takes degrees, matching the /Rotate page entry and the way // callers think about page rotation. rotate_matrix :: proc "contextless" (degrees: f64) -> Matrix { r := degrees * math.PI / 180 s := math.sin(r) c := math.cos(r) return Matrix{c, s, -s, c, 0, 0} } skew_matrix :: proc "contextless" (ax, ay: f64) -> Matrix { return Matrix{1, math.tan(ay * math.PI / 180), math.tan(ax * math.PI / 180), 1, 0, 0} } matrix_determinant :: proc "contextless" (m: Matrix) -> f64 { return m.a * m.d - m.b * m.c } // matrix_invert reports ok = false for a singular matrix. Degenerate CTMs are // legal in content streams (a zero-width scale is how some producers hide // content) so callers must not assume success. matrix_invert :: proc "contextless" (m: Matrix) -> (inv: Matrix, ok: bool) { det := matrix_determinant(m) if det == 0 || !is_finite(det) { return MATRIX_IDENTITY, false } id := 1 / det inv.a = m.d * id inv.b = -m.b * id inv.c = -m.c * id inv.d = m.a * id inv.e = (m.c * m.f - m.d * m.e) * id inv.f = (m.b * m.e - m.a * m.f) * id return inv, true } transform_point :: proc "contextless" (m: Matrix, p: Point) -> Point { return Point{ m.a * p.x + m.c * p.y + m.e, m.b * p.x + m.d * p.y + m.f, } } // transform_vector applies only the linear part, for quantities that have a // direction and a magnitude but no position -- a dash phase, a line width. transform_vector :: proc "contextless" (m: Matrix, p: Point) -> Point { return Point{m.a * p.x + m.c * p.y, m.b * p.x + m.d * p.y} } // transform_rect returns the bounding box of the transformed rectangle, which // is larger than the transformed shape whenever the matrix rotates or skews. transform_rect :: proc "contextless" (m: Matrix, r: Rect) -> Rect { if r == RECT_INFINITE { return r } p0 := transform_point(m, Point{r.x0, r.y0}) p1 := transform_point(m, Point{r.x1, r.y0}) p2 := transform_point(m, Point{r.x0, r.y1}) p3 := transform_point(m, Point{r.x1, r.y1}) return Rect{ x0 = min(p0.x, p1.x, p2.x, p3.x), y0 = min(p0.y, p1.y, p2.y, p3.y), x1 = max(p0.x, p1.x, p2.x, p3.x), y1 = max(p0.y, p1.y, p2.y, p3.y), } } // matrix_expansion is the square root of |determinant|: the average factor by // which the matrix scales lengths. Used to turn a user-space line width or // flatness tolerance into a device-space one. matrix_expansion :: proc "contextless" (m: Matrix) -> f64 { return math.sqrt(abs(matrix_determinant(m))) } // matrix_max_expansion is the largest factor by which the matrix can stretch // any single direction. Flattening tolerance has to use this rather than the // average, or a curve under an anisotropic scale flattens too coarsely along // its stretched axis. matrix_max_expansion :: proc "contextless" (m: Matrix) -> f64 { return max(math.sqrt(m.a * m.a + m.b * m.b), math.sqrt(m.c * m.c + m.d * m.d)) } matrix_is_identity :: proc "contextless" (m: Matrix) -> bool { return m == MATRIX_IDENTITY } // matrix_is_rectilinear reports whether the matrix maps axis-aligned // rectangles to axis-aligned rectangles. Axis-aligned clips can be applied as // a scissor rect instead of a mask, which is much cheaper and covers most // real content. // // The comparison is against a relative tolerance rather than against zero: // `rotate_matrix(90)` computes cos(pi/2) as 6.1e-17, not 0, and a page with // `/Rotate 90` is precisely the case this predicate exists to catch. Testing // for exact zeros would answer "no" for every rotated page in existence. matrix_is_rectilinear :: proc "contextless" (m: Matrix) -> bool { scale := max(abs(m.a), abs(m.b), abs(m.c), abs(m.d)) if scale == 0 { // A matrix that collapses everything to a point maps a rectangle to a // (degenerate) rectangle, so the cheap path is still correct. return true } eps := scale * 1e-9 return (abs(m.b) <= eps && abs(m.c) <= eps) || (abs(m.a) <= eps && abs(m.d) <= eps) } // --------------------------------------------------------------------------- // Rectangles // --------------------------------------------------------------------------- rect_normalize :: proc "contextless" (r: Rect) -> Rect { return Rect{min(r.x0, r.x1), min(r.y0, r.y1), max(r.x0, r.x1), max(r.y0, r.y1)} } rect_width :: proc "contextless" (r: Rect) -> f64 { return r.x1 - r.x0 } rect_height :: proc "contextless" (r: Rect) -> f64 { return r.y1 - r.y0 } rect_is_empty :: proc "contextless" (r: Rect) -> bool { return r.x1 <= r.x0 || r.y1 <= r.y0 } rect_contains :: proc "contextless" (r: Rect, p: Point) -> bool { return p.x >= r.x0 && p.x <= r.x1 && p.y >= r.y0 && p.y <= r.y1 } rect_intersect :: proc "contextless" (a, b: Rect) -> Rect { if a == RECT_INFINITE { return b } if b == RECT_INFINITE { return a } r := Rect{max(a.x0, b.x0), max(a.y0, b.y0), min(a.x1, b.x1), min(a.y1, b.y1)} if rect_is_empty(r) { return RECT_EMPTY } return r } rect_union :: proc "contextless" (a, b: Rect) -> Rect { if rect_is_empty(a) { return b } if rect_is_empty(b) { return a } return Rect{min(a.x0, b.x0), min(a.y0, b.y0), max(a.x1, b.x1), max(a.y1, b.y1)} } rect_include :: proc "contextless" (r: Rect, p: Point) -> Rect { if rect_is_empty(r) { return Rect{p.x, p.y, p.x, p.y} } return Rect{min(r.x0, p.x), min(r.y0, p.y), max(r.x1, p.x), max(r.y1, p.y)} } rect_expand :: proc "contextless" (r: Rect, by: f64) -> Rect { return Rect{r.x0 - by, r.y0 - by, r.x1 + by, r.y1 + by} } // --------------------------------------------------------------------------- // Reading geometry out of the file // --------------------------------------------------------------------------- // object_to_rect reads a 4-number array. The corners are normalised, so a // /MediaBox written as [612 792 0 0] -- which is legal, and does occur -- comes // back the right way round. object_to_rect :: proc(doc: ^Document, o: Object) -> (r: Rect, ok: bool) { a := to_array(o) or_return if array_len(a) < 4 { return RECT_EMPTY, false } r = Rect{ array_num(doc, a, 0), array_num(doc, a, 1), array_num(doc, a, 2), array_num(doc, a, 3), } if !rect_is_finite(r) { return RECT_EMPTY, false } return rect_normalize(r), true } // object_to_matrix reads a 6-number array, as written for /Matrix on form // XObjects, patterns and Type3 fonts. object_to_matrix :: proc(doc: ^Document, o: Object) -> (m: Matrix, ok: bool) { a := to_array(o) or_return if array_len(a) < 6 { return MATRIX_IDENTITY, false } m = Matrix{ array_num(doc, a, 0), array_num(doc, a, 1), array_num(doc, a, 2), array_num(doc, a, 3), array_num(doc, a, 4), array_num(doc, a, 5), } if !matrix_is_finite(m) { return MATRIX_IDENTITY, false } return m, true } dict_rect :: proc(doc: ^Document, d: ^Dict, key: string, default := RECT_EMPTY) -> Rect { r, ok := object_to_rect(doc, dict_get(doc, d, key)) return ok ? r : default } dict_matrix :: proc(doc: ^Document, d: ^Dict, key: string, default := MATRIX_IDENTITY) -> Matrix { m, ok := object_to_matrix(doc, dict_get(doc, d, key)) return ok ? m : default } // --------------------------------------------------------------------------- // Finite-value guards // // A NaN or infinity reaching the rasteriser turns into an unbounded scanline // loop or a silently blank page, so every number that comes out of a file is // checked at the boundary rather than trusted. // --------------------------------------------------------------------------- is_finite :: proc "contextless" (x: f64) -> bool { return x == x && x != math.INF_F64 && x != math.NEG_INF_F64 } rect_is_finite :: proc "contextless" (r: Rect) -> bool { return is_finite(r.x0) && is_finite(r.y0) && is_finite(r.x1) && is_finite(r.y1) } matrix_is_finite :: proc "contextless" (m: Matrix) -> bool { return is_finite(m.a) && is_finite(m.b) && is_finite(m.c) && is_finite(m.d) && is_finite(m.e) && is_finite(m.f) } // clamp_finite replaces a non-finite value with `fallback`. clamp_finite :: proc "contextless" (x, fallback: f64) -> f64 { return is_finite(x) ? x : fallback }