334 lines
12 KiB
Odin
334 lines
12 KiB
Odin
package pdf
|
|
|
|
import "core:mem/virtual"
|
|
import "core:testing"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Coercions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(test)
|
|
test_to_int_accepts_reals :: proc(t: ^testing.T) {
|
|
// Files write "1.0" where an integer is required often enough that
|
|
// refusing it would reject working documents.
|
|
v, ok := to_int(f64(1.0))
|
|
testing.expect(t, ok, "a real coerces to an integer")
|
|
testing.expect_value(t, v, i64(1))
|
|
|
|
v2, ok2 := to_int(i64(-7))
|
|
testing.expect(t, ok2, "")
|
|
testing.expect_value(t, v2, i64(-7))
|
|
|
|
// Truncation, not rounding -- an index of 2.9 is element 2.
|
|
v3, _ := to_int(f64(2.9))
|
|
testing.expect_value(t, v3, i64(2))
|
|
|
|
// A real too large for i64 must be refused, not wrapped.
|
|
_, ok4 := to_int(f64(1e300))
|
|
testing.expect(t, !ok4, "a real beyond i64 range is not an integer")
|
|
_, ok5 := to_int(f64(-1e300))
|
|
testing.expect(t, !ok5, "")
|
|
|
|
_, ok6 := to_int(nil)
|
|
testing.expect(t, !ok6, "null is not an integer")
|
|
_, ok7 := to_int(true)
|
|
testing.expect(t, !ok7, "a boolean is not an integer")
|
|
}
|
|
|
|
@(test)
|
|
test_to_number_and_real :: proc(t: ^testing.T) {
|
|
v, ok := to_number(i64(3))
|
|
testing.expect(t, ok, "an integer is a number")
|
|
testing.expect_value(t, v, 3.0)
|
|
|
|
v2, ok2 := to_number(f64(2.5))
|
|
testing.expect(t, ok2, "")
|
|
testing.expect_value(t, v2, 2.5)
|
|
|
|
_, ok3 := to_number(nil)
|
|
testing.expect(t, !ok3, "")
|
|
|
|
// to_real is strict: an integer object is not a real object.
|
|
_, ok4 := to_real(i64(3))
|
|
testing.expect(t, !ok4, "to_real does not coerce")
|
|
}
|
|
|
|
@(test)
|
|
test_to_name_bytes_and_ref :: proc(t: ^testing.T) {
|
|
n, ok := to_name(Name("Page"))
|
|
testing.expect(t, ok, "")
|
|
testing.expect_value(t, n, "Page")
|
|
|
|
b, ok2 := to_bytes(String(transmute([]byte)string("hi")))
|
|
testing.expect(t, ok2, "")
|
|
testing.expect_value(t, string(b), "hi")
|
|
|
|
r, ok3 := to_ref(Ref{12, 0})
|
|
testing.expect(t, ok3, "")
|
|
testing.expect_value(t, r, Ref{12, 0})
|
|
|
|
testing.expect(t, name_is(Name("Catalog"), "Catalog"), "")
|
|
testing.expect(t, !name_is(Name("Catalog"), "Page"), "")
|
|
testing.expect(t, !name_is(String(transmute([]byte)string("Catalog")), "Catalog"),
|
|
"a string is not a name, even with the same bytes")
|
|
testing.expect(t, !name_is(nil, ""), "null is not the empty name")
|
|
}
|
|
|
|
@(test)
|
|
test_object_kind :: proc(t: ^testing.T) {
|
|
testing.expect_value(t, object_kind(nil), "null")
|
|
testing.expect_value(t, object_kind(true), "boolean")
|
|
testing.expect_value(t, object_kind(i64(1)), "integer")
|
|
testing.expect_value(t, object_kind(f64(1)), "real")
|
|
testing.expect_value(t, object_kind(Name("x")), "name")
|
|
testing.expect_value(t, object_kind(Ref{1, 0}), "reference")
|
|
testing.expect(t, is_null(nil), "")
|
|
testing.expect(t, !is_null(i64(0)), "the integer zero is not null")
|
|
}
|
|
|
|
// A stream where a dictionary is wanted hands back the stream's dictionary:
|
|
// /Resources, /Group and annotation appearances are all reached through entries
|
|
// that are sometimes one and sometimes the other.
|
|
@(test)
|
|
test_to_dict_accepts_a_stream :: 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)
|
|
dict_set(d, "Type", Name("XObject"))
|
|
s := new_stream(d, nil, a)
|
|
|
|
got, ok := to_dict(s)
|
|
testing.expect(t, ok, "a stream is usable as a dictionary")
|
|
testing.expect_value(t, got, d)
|
|
|
|
_, ok2 := to_stream(d)
|
|
testing.expect(t, !ok2, "but a dictionary is not a stream")
|
|
|
|
// A stream with no dictionary must not report success with a nil result.
|
|
bare := new_stream(nil, nil, a)
|
|
_, ok3 := to_dict(bare)
|
|
testing.expect(t, !ok3, "a stream without a dictionary is not a dictionary")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dictionary access
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(test)
|
|
test_dict_accessors_and_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)
|
|
dict_set(d, "Count", i64(5))
|
|
dict_set(d, "Size", f64(2.5))
|
|
dict_set(d, "Type", Name("Pages"))
|
|
dict_set(d, "Flag", true)
|
|
dict_set(d, "Ref", Ref{9, 0})
|
|
|
|
testing.expect_value(t, dict_int(nil, d, "Count"), i64(5))
|
|
testing.expect_value(t, dict_num(nil, d, "Size"), 2.5)
|
|
testing.expect_value(t, dict_num(nil, d, "Count"), 5.0)
|
|
testing.expect_value(t, dict_name(nil, d, "Type"), "Pages")
|
|
testing.expect_value(t, dict_type(nil, d), "Pages")
|
|
testing.expect(t, dict_bool(nil, d, "Flag"), "")
|
|
|
|
// A missing key takes the default, and so does a key of the wrong type --
|
|
// a viewer substitutes rather than failing.
|
|
testing.expect_value(t, dict_int(nil, d, "Absent", 42), i64(42))
|
|
testing.expect_value(t, dict_int(nil, d, "Type", 42), i64(42))
|
|
testing.expect_value(t, dict_name(nil, d, "Count", "fallback"), "fallback")
|
|
testing.expect_value(t, dict_bool(nil, d, "Absent", true), true)
|
|
|
|
testing.expect(t, dict_has(d, "Count"), "")
|
|
testing.expect(t, !dict_has(d, "Nope"), "")
|
|
testing.expect(t, !dict_has(nil, "Count"), "a nil dictionary has nothing")
|
|
|
|
// dict_ref must not resolve: /Parent is followed for identity, and
|
|
// resolving it is exactly what makes a cyclic walk.
|
|
r, ok := dict_ref(d, "Ref")
|
|
testing.expect(t, ok, "")
|
|
testing.expect_value(t, r, Ref{9, 0})
|
|
_, ok2 := dict_ref(d, "Count")
|
|
testing.expect(t, !ok2, "a direct value is not a reference")
|
|
|
|
// Every accessor must survive a nil dictionary.
|
|
testing.expect_value(t, dict_int(nil, nil, "Count", 1), i64(1))
|
|
testing.expect(t, dict_get(nil, nil, "Count") == nil, "")
|
|
testing.expect(t, dict_dict(nil, nil, "x") == nil, "")
|
|
testing.expect(t, dict_array(nil, nil, "x") == nil, "")
|
|
testing.expect(t, dict_stream(nil, nil, "x") == nil, "")
|
|
dict_set(nil, "x", i64(1)) // must not crash
|
|
}
|
|
|
|
// Inline images abbreviate their keys, and both spellings are legal.
|
|
@(test)
|
|
test_dict_get_alt :: 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)
|
|
dict_set(d, "W", i64(16))
|
|
v, ok := to_int(dict_get_alt(nil, d, "Width", "W"))
|
|
testing.expect(t, ok, "the abbreviation is found when the full name is absent")
|
|
testing.expect_value(t, v, i64(16))
|
|
|
|
dict_set(d, "Width", i64(32))
|
|
v2, _ := to_int(dict_get_alt(nil, d, "Width", "W"))
|
|
testing.expect_value(t, v2, i64(32), )
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Arrays
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(test)
|
|
test_array_accessors :: proc(t: ^testing.T) {
|
|
arena: virtual.Arena
|
|
_ = virtual.arena_init_growing(&arena)
|
|
defer virtual.arena_destroy(&arena)
|
|
a := virtual.arena_allocator(&arena)
|
|
|
|
arr := new_array(a)
|
|
array_push(arr, i64(10))
|
|
array_push(arr, f64(2.5))
|
|
array_push(arr, Name("Foo"))
|
|
|
|
testing.expect_value(t, array_len(arr), 3)
|
|
testing.expect_value(t, array_int(nil, arr, 0), i64(10))
|
|
testing.expect_value(t, array_num(nil, arr, 1), 2.5)
|
|
testing.expect_value(t, array_name(nil, arr, 2), "Foo")
|
|
|
|
// Out of range in either direction reads as the default, never a panic.
|
|
testing.expect_value(t, array_int(nil, arr, 99, -1), i64(-1))
|
|
testing.expect_value(t, array_int(nil, arr, -1, -1), i64(-1))
|
|
testing.expect(t, array_get_raw(arr, 99) == nil, "")
|
|
testing.expect_value(t, array_len(nil), 0)
|
|
testing.expect(t, array_get_raw(nil, 0) == nil, "")
|
|
array_push(nil, i64(1)) // must not crash
|
|
}
|
|
|
|
// A stray null in a /Widths or /Decode array reads as 0, which is what a
|
|
// viewer substitutes -- dropping the entry would shift every later index.
|
|
@(test)
|
|
test_array_numbers :: proc(t: ^testing.T) {
|
|
arena: virtual.Arena
|
|
_ = virtual.arena_init_growing(&arena)
|
|
defer virtual.arena_destroy(&arena)
|
|
a := virtual.arena_allocator(&arena)
|
|
|
|
arr := new_array(a)
|
|
array_push(arr, i64(1))
|
|
array_push(arr, nil)
|
|
array_push(arr, f64(3.5))
|
|
array_push(arr, Name("junk"))
|
|
|
|
nums := array_numbers(nil, arr, a)
|
|
testing.expect_value(t, len(nums), 4)
|
|
testing.expect_value(t, nums[0], 1.0)
|
|
testing.expect_value(t, nums[1], 0.0)
|
|
testing.expect_value(t, nums[2], 3.5)
|
|
testing.expect_value(t, nums[3], 0.0)
|
|
|
|
testing.expect_value(t, len(array_numbers(nil, nil, a)), 0)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Names and numbers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(test)
|
|
test_decode_name_escapes :: proc(t: ^testing.T) {
|
|
// The common case allocates nothing and returns the input unchanged.
|
|
plain := decode_name_escapes("Plain")
|
|
testing.expect_value(t, plain, "Plain")
|
|
|
|
check :: proc(t: ^testing.T, input, want: string) {
|
|
got := decode_name_escapes(input)
|
|
testing.expect_value(t, got, want)
|
|
if raw_data(got) != raw_data(input) {
|
|
delete(got)
|
|
}
|
|
}
|
|
check(t, "Adobe#20Green", "Adobe Green")
|
|
check(t, "#41#42", "AB")
|
|
check(t, "a#2Fb", "a/b")
|
|
check(t, "lower#7ecase", "lower~case")
|
|
// A '#' that is not followed by two hex digits is a broken name, not a
|
|
// reason to drop bytes.
|
|
check(t, "bad#zz", "bad#zz")
|
|
check(t, "trailing#", "trailing#")
|
|
check(t, "short#4", "short#4")
|
|
check(t, "", "")
|
|
}
|
|
|
|
@(test)
|
|
test_parse_number :: proc(t: ^testing.T) {
|
|
expect_int :: proc(t: ^testing.T, s: string, want: i64) {
|
|
o, ok := parse_number(s)
|
|
testing.expect(t, ok, "should parse")
|
|
v, is_int := o.(i64)
|
|
testing.expect(t, is_int, "should be an integer object")
|
|
testing.expect_value(t, v, want)
|
|
}
|
|
expect_real :: proc(t: ^testing.T, s: string, want: f64) {
|
|
o, ok := parse_number(s)
|
|
testing.expect(t, ok, "should parse")
|
|
v, is_real := o.(f64)
|
|
testing.expect(t, is_real, "should be a real object")
|
|
testing.expect_value(t, v, want)
|
|
}
|
|
|
|
expect_int(t, "0", 0)
|
|
expect_int(t, "42", 42)
|
|
expect_int(t, "-17", -17)
|
|
expect_int(t, "+6", 6)
|
|
|
|
expect_real(t, "3.5", 3.5)
|
|
expect_real(t, "-2.25", -2.25)
|
|
// ".5" and "4." are both legal PDF and both rejected by strict parsers.
|
|
expect_real(t, ".5", 0.5)
|
|
expect_real(t, "4.", 4.0)
|
|
expect_real(t, "-.5", -0.5)
|
|
|
|
// Garbage yields 0, the value a viewer substitutes, and reports failure so
|
|
// a caller that cares can tell.
|
|
_, ok := parse_number("")
|
|
testing.expect(t, !ok, "the empty string is not a number")
|
|
_, ok2 := parse_number("--5")
|
|
testing.expect(t, !ok2, "a doubled sign is not a number")
|
|
o, _ := parse_number("junk")
|
|
v, _ := to_number(o)
|
|
testing.expect_value(t, v, 0.0)
|
|
}
|
|
|
|
@(test)
|
|
test_new_object_constructors :: 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(t, d != nil, "")
|
|
testing.expect_value(t, len(d^), 0)
|
|
|
|
arr := new_array(a)
|
|
testing.expect(t, arr != nil, "")
|
|
testing.expect_value(t, array_len(arr), 0)
|
|
|
|
payload := []byte{1, 2, 3}
|
|
s := new_stream(d, payload, a)
|
|
testing.expect(t, s != nil, "")
|
|
testing.expect_value(t, s.state, Stream_Decode_State.Pending)
|
|
testing.expect_value(t, len(s.raw), 3)
|
|
testing.expect_value(t, s.dict, d)
|
|
}
|