package pdf import "core:math" import "core:mem/virtual" import "core:testing" // --------------------------------------------------------------------------- // Matrices // // matrix_mul's operand order is the single thing every `cm` operator, form // XObject and pattern matrix depends on. Getting it backwards transposes the // whole page, so it is pinned here against worked examples rather than against // the implementation's own arithmetic. // --------------------------------------------------------------------------- @(test) test_matrix_mul_order :: proc(t: ^testing.T) { // m applied first, then n. m := matrix_mul(scale_matrix(2, 3), translate_matrix(5, 7)) testing.expect_value(t, m, Matrix{2, 0, 0, 3, 5, 7}) testing.expect_value(t, transform_point(m, Point{1, 1}), Point{7, 10}) // The reverse order is a different matrix -- if these were equal the test // would pass with the operands transposed. n := matrix_mul(translate_matrix(5, 7), scale_matrix(2, 3)) testing.expect_value(t, n, Matrix{2, 0, 0, 3, 10, 21}) testing.expect(t, m != n, "multiplication is not commutative") testing.expect_value(t, matrix_mul(MATRIX_IDENTITY, m), m) testing.expect_value(t, matrix_mul(m, MATRIX_IDENTITY), m) } // The row-vector convention the format uses: x' = a*x + c*y + e. @(test) test_transform_point_convention :: proc(t: ^testing.T) { m := Matrix{2, 3, 5, 7, 11, 13} testing.expect_value(t, transform_point(m, Point{1, 0}), Point{2 + 11, 3 + 13}) testing.expect_value(t, transform_point(m, Point{0, 1}), Point{5 + 11, 7 + 13}) testing.expect_value(t, transform_point(m, Point{0, 0}), Point{11, 13}) // A vector has direction and magnitude but no position, so the translation // must not apply. testing.expect_value(t, transform_vector(m, Point{1, 0}), Point{2, 3}) testing.expect_value(t, transform_vector(m, Point{0, 0}), Point{0, 0}) } @(test) test_matrix_rotate_and_skew :: proc(t: ^testing.T) { r := rotate_matrix(90) p := transform_point(r, Point{1, 0}) testing.expect(t, abs(p.x) < 1e-12, "90 degrees sends x to 0") testing.expect(t, abs(p.y - 1) < 1e-12, "90 degrees sends x to +y") // Four quarter turns is the identity, to floating-point tolerance. full := matrix_mul(matrix_mul(r, r), matrix_mul(r, r)) testing.expect(t, abs(full.a - 1) < 1e-9 && abs(full.d - 1) < 1e-9, "four quarter turns") s := skew_matrix(45, 0) testing.expect(t, abs(s.c - 1) < 1e-12, "a 45 degree x-skew has c == 1") } @(test) test_matrix_invert :: proc(t: ^testing.T) { m := Matrix{2, 1, 1, 3, 10, 20} inv, ok := matrix_invert(m) testing.expect(t, ok, "a non-singular matrix inverts") round := matrix_mul(m, inv) testing.expect(t, abs(round.a - 1) < 1e-12 && abs(round.d - 1) < 1e-12, "m * m^-1 is identity") testing.expect(t, abs(round.b) < 1e-12 && abs(round.c) < 1e-12, "") testing.expect(t, abs(round.e) < 1e-12 && abs(round.f) < 1e-12, "translation cancels too") // Round-tripping a point is the property callers actually rely on. p := Point{3, -4} testing.expect(t, abs(transform_point(inv, transform_point(m, p)).x - p.x) < 1e-12, "") testing.expect(t, abs(transform_point(inv, transform_point(m, p)).y - p.y) < 1e-12, "") } // A degenerate CTM is legal content -- a zero-width scale is how some producers // hide artwork -- so callers must be told rather than handed nonsense. @(test) test_matrix_invert_singular :: proc(t: ^testing.T) { _, ok := matrix_invert(scale_matrix(0, 1)) testing.expect(t, !ok, "a zero scale is singular") _, ok2 := matrix_invert(Matrix{1, 2, 2, 4, 0, 0}) testing.expect(t, !ok2, "collinear rows are singular") _, ok3 := matrix_invert(Matrix{math.nan_f64(), 0, 0, 1, 0, 0}) testing.expect(t, !ok3, "a NaN determinant is not invertible") inv, _ := matrix_invert(scale_matrix(0, 0)) testing.expect_value(t, inv, MATRIX_IDENTITY) } @(test) test_matrix_expansion :: proc(t: ^testing.T) { testing.expect_value(t, matrix_expansion(scale_matrix(2, 8)), 4) testing.expect_value(t, matrix_expansion(MATRIX_IDENTITY), 1) // Reflection scales lengths as much as rotation does; the sign is not it. testing.expect_value(t, matrix_expansion(scale_matrix(-3, 3)), 3) // The maximum stretch of any single direction, which is what flattening // tolerance must use: the average would flatten the stretched axis coarsely. testing.expect_value(t, matrix_max_expansion(scale_matrix(2, 8)), 8) testing.expect_value(t, matrix_max_expansion(scale_matrix(8, 2)), 8) } @(test) test_matrix_predicates :: proc(t: ^testing.T) { testing.expect(t, matrix_is_identity(MATRIX_IDENTITY), "") testing.expect(t, !matrix_is_identity(translate_matrix(1, 0)), "") testing.expect(t, matrix_is_rectilinear(scale_matrix(2, 3)), "a scale keeps axes") testing.expect(t, matrix_is_rectilinear(rotate_matrix(90)), "a quarter turn swaps axes") testing.expect(t, !matrix_is_rectilinear(rotate_matrix(45)), "45 degrees does not") testing.expect(t, !matrix_is_rectilinear(skew_matrix(10, 0)), "a skew does not") testing.expect(t, matrix_is_finite(MATRIX_IDENTITY), "") testing.expect(t, !matrix_is_finite(Matrix{1, 0, 0, 1, math.nan_f64(), 0}), "") testing.expect(t, !matrix_is_finite(Matrix{math.INF_F64, 0, 0, 1, 0, 0}), "") } // --------------------------------------------------------------------------- // Rectangles // --------------------------------------------------------------------------- @(test) test_transform_rect_bounds_the_shape :: proc(t: ^testing.T) { r := Rect{0, 0, 10, 10} testing.expect_value(t, transform_rect(scale_matrix(2, 3), r), Rect{0, 0, 20, 30}) // A rotation gives the bounding box of the rotated shape, which is larger // than the shape itself. rot := transform_rect(rotate_matrix(45), r) d := 10 * math.sqrt_f64(2) testing.expect(t, abs(rect_width(rot) - d) < 1e-9, "a 45-degree box is sqrt(2) wider") // Clipping against the infinite rect must stay a no-op after transforming. testing.expect_value(t, transform_rect(scale_matrix(2, 2), RECT_INFINITE), RECT_INFINITE) } @(test) test_rect_ops :: proc(t: ^testing.T) { a := Rect{0, 0, 10, 10} b := Rect{5, 5, 20, 20} testing.expect_value(t, rect_intersect(a, b), Rect{5, 5, 10, 10}) testing.expect_value(t, rect_union(a, b), Rect{0, 0, 20, 20}) testing.expect_value(t, rect_intersect(a, Rect{100, 100, 110, 110}), RECT_EMPTY) // The infinite rect is the identity for intersection, which is what an // unset clip path needs to be. testing.expect_value(t, rect_intersect(a, RECT_INFINITE), a) testing.expect_value(t, rect_intersect(RECT_INFINITE, a), a) // The empty rect is the identity for union. testing.expect_value(t, rect_union(a, RECT_EMPTY), a) testing.expect_value(t, rect_union(RECT_EMPTY, a), a) testing.expect_value(t, rect_width(a), 10) testing.expect_value(t, rect_height(a), 10) testing.expect(t, rect_is_empty(Rect{5, 5, 5, 10}), "zero width is empty") testing.expect(t, rect_is_empty(Rect{10, 0, 5, 10}), "inverted is empty") testing.expect(t, !rect_is_empty(a), "") testing.expect(t, rect_contains(a, Point{5, 5}), "") testing.expect(t, rect_contains(a, Point{0, 0}), "the boundary counts") testing.expect(t, !rect_contains(a, Point{11, 5}), "") testing.expect_value(t, rect_expand(a, 2), Rect{-2, -2, 12, 12}) testing.expect_value(t, rect_include(a, Point{-5, 20}), Rect{-5, 0, 10, 20}) testing.expect_value(t, rect_include(RECT_EMPTY, Point{3, 4}), Rect{3, 4, 3, 4}) } // A /MediaBox is explicitly allowed to give its corners in either order. @(test) test_rect_normalize :: proc(t: ^testing.T) { testing.expect_value(t, rect_normalize(Rect{612, 792, 0, 0}), Rect{0, 0, 612, 792}) testing.expect_value(t, rect_normalize(Rect{0, 0, 612, 792}), Rect{0, 0, 612, 792}) } // --------------------------------------------------------------------------- // Reading geometry out of objects // --------------------------------------------------------------------------- @(test) test_object_to_rect_and_matrix :: proc(t: ^testing.T) { arena: virtual.Arena _ = virtual.arena_init_growing(&arena) defer virtual.arena_destroy(&arena) a := virtual.arena_allocator(&arena) box := new_array(a) array_push(box, i64(612)) array_push(box, i64(792)) array_push(box, i64(0)) array_push(box, f64(0)) r, ok := object_to_rect(nil, box) testing.expect(t, ok, "a four-number array is a rectangle") testing.expect_value(t, r, Rect{0, 0, 612, 792}) short := new_array(a) array_push(short, i64(1)) _, ok2 := object_to_rect(nil, short) testing.expect(t, !ok2, "three numbers is not a rectangle") _, ok3 := object_to_rect(nil, nil) testing.expect(t, !ok3, "null is not a rectangle") _, ok4 := object_to_rect(nil, i64(7)) testing.expect(t, !ok4, "an integer is not a rectangle") // A non-finite entry must be refused, not passed on to the rasteriser. bad := new_array(a) for _ in 0 ..< 3 { array_push(bad, f64(0)) } array_push(bad, math.inf_f64(1)) _, ok5 := object_to_rect(nil, bad) testing.expect(t, !ok5, "an infinite corner is not a rectangle") mtx := new_array(a) for v in ([]f64{1, 2, 3, 4, 5, 6}) { array_push(mtx, v) } m, ok6 := object_to_matrix(nil, mtx) testing.expect(t, ok6, "a six-number array is a matrix") testing.expect_value(t, m, Matrix{1, 2, 3, 4, 5, 6}) _, ok7 := object_to_matrix(nil, box) testing.expect(t, !ok7, "four numbers is not a matrix") } @(test) test_dict_rect_and_matrix_defaults :: proc(t: ^testing.T) { arena: virtual.Arena _ = virtual.arena_init_growing(&arena) defer virtual.arena_destroy(&arena) a := virtual.arena_allocator(&arena) d := new_dict(a) testing.expect_value(t, dict_rect(nil, d, "MediaBox", DEFAULT_PAGE_RECT), DEFAULT_PAGE_RECT) testing.expect_value(t, dict_matrix(nil, d, "Matrix"), MATRIX_IDENTITY) box := new_array(a) for v in ([]i64{0, 0, 200, 100}) { array_push(box, v) } dict_set(d, "MediaBox", box) testing.expect_value(t, dict_rect(nil, d, "MediaBox", DEFAULT_PAGE_RECT), Rect{0, 0, 200, 100}) // A nil dictionary must fall back rather than crash. testing.expect_value(t, dict_rect(nil, nil, "MediaBox", DEFAULT_PAGE_RECT), DEFAULT_PAGE_RECT) } @(test) test_finite_guards :: proc(t: ^testing.T) { testing.expect(t, is_finite(0), "") testing.expect(t, is_finite(-1e300), "") testing.expect(t, !is_finite(math.nan_f64()), "") testing.expect(t, !is_finite(math.INF_F64), "") testing.expect(t, !is_finite(math.NEG_INF_F64), "") testing.expect_value(t, clamp_finite(5, 99), 5) testing.expect_value(t, clamp_finite(math.nan_f64(), 99), 99) testing.expect_value(t, clamp_finite(math.INF_F64, 99), 99) testing.expect(t, rect_is_finite(Rect{0, 0, 1, 1}), "") testing.expect(t, !rect_is_finite(Rect{0, 0, math.nan_f64(), 1}), "") } // rotate_matrix builds its entries from sin and cos, so a quarter turn carries // a 6e-17 where an exact zero would be. Every /Rotate 90 page in existence goes // through this path. @(test) test_matrix_is_rectilinear_tolerates_rotation_error :: proc(t: ^testing.T) { for deg in ([]f64{0, 90, 180, 270, -90, 360}) { testing.expect(t, matrix_is_rectilinear(rotate_matrix(deg)), "a quarter turn maps axis-aligned rectangles to axis-aligned rectangles") } for deg in ([]f64{1, 30, 45, 89, 100}) { testing.expect(t, !matrix_is_rectilinear(rotate_matrix(deg)), "an off-axis rotation does not") } // Scale must not change the answer: the tolerance is relative. tiny := matrix_mul(rotate_matrix(90), scale_matrix(1e-6, 1e-6)) testing.expect(t, matrix_is_rectilinear(tiny), "a tiny rotated matrix is still rectilinear") huge := matrix_mul(rotate_matrix(45), scale_matrix(1e9, 1e9)) testing.expect(t, !matrix_is_rectilinear(huge), "a huge skewed matrix is still not") }