283 lines
12 KiB
Odin
283 lines
12 KiB
Odin
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")
|
|
}
|