1275 lines
50 KiB
Odin
1275 lines
50 KiB
Odin
package pdf
|
|
|
|
import "core:mem"
|
|
import "core:testing"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
//
|
|
// Comparisons are structural: a recorded command holds its OWN copy of the
|
|
// path and the glyph run, so the pointers deliberately differ and only the
|
|
// contents may be compared.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(private = "file")
|
|
dl_path_eq :: proc(a, b: ^Path) -> bool {
|
|
if a == nil || b == nil { return a == b }
|
|
if len(a.verbs) != len(b.verbs) || len(a.points) != len(b.points) { return false }
|
|
for v, i in a.verbs {
|
|
if v != b.verbs[i] { return false }
|
|
}
|
|
for p, i in a.points {
|
|
if p != b.points[i] { return false }
|
|
}
|
|
return true
|
|
}
|
|
|
|
@(private = "file")
|
|
dl_text_eq :: proc(a, b: ^Text) -> bool {
|
|
if a == nil || b == nil { return a == b }
|
|
if a.font != b.font || a.wmode != b.wmode || a.render_mode != b.render_mode { return false }
|
|
if len(a.items) != len(b.items) { return false }
|
|
for it, i in a.items {
|
|
if it != b.items[i] { return false }
|
|
}
|
|
return true
|
|
}
|
|
|
|
// dl_paint_eq compares two paints, taking the soft mask by value: the list
|
|
// copies the mask, so the two pointers must NOT be equal and the two masks
|
|
// must be.
|
|
@(private = "file")
|
|
dl_paint_eq :: proc(a, b: Paint) -> bool {
|
|
x, y := a, b
|
|
x.soft_mask, y.soft_mask = nil, nil
|
|
if x != y { return false }
|
|
if (a.soft_mask == nil) != (b.soft_mask == nil) { return false }
|
|
if a.soft_mask != nil && a.soft_mask^ != b.soft_mask^ { return false }
|
|
return true
|
|
}
|
|
|
|
// dl_mask_eq compares two recorded masks the same way, for the hooks that
|
|
// carry a `^Soft_Mask` of their own rather than one inside a Paint:
|
|
// `begin_group`, whose mask is the one the group's RESULT composites through,
|
|
// and `end_mask`, whose mask names the plane just drawn.
|
|
@(private = "file")
|
|
dl_mask_eq :: proc(a, b: ^Soft_Mask) -> bool {
|
|
if (a == nil) != (b == nil) { return false }
|
|
return a == nil || a^ == b^
|
|
}
|
|
|
|
// dl_find_group returns the first recorded `begin_group`, by pointer so that
|
|
// the mask it carries can be compared against the other list's copy.
|
|
@(private = "file")
|
|
dl_find_group :: proc(dl: ^Display_List) -> ^Display_Begin_Group {
|
|
for &cmd in dl.cmds {
|
|
if g, ok := &cmd.(Display_Begin_Group); ok { return g }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
@(private = "file")
|
|
dl_cmd_eq :: proc(a, b: Display_Cmd) -> bool {
|
|
switch av in a {
|
|
case Display_Fill_Path:
|
|
bv := b.(Display_Fill_Path) or_return
|
|
return dl_path_eq(av.path, bv.path) && av.even_odd == bv.even_odd &&
|
|
av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint)
|
|
case Display_Stroke_Path:
|
|
bv := b.(Display_Stroke_Path) or_return
|
|
return dl_path_eq(av.path, bv.path) && av.stroke == bv.stroke &&
|
|
av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint)
|
|
case Display_Clip_Path:
|
|
bv := b.(Display_Clip_Path) or_return
|
|
return dl_path_eq(av.path, bv.path) && av.even_odd == bv.even_odd && av.ctm == bv.ctm
|
|
case Display_Clip_Stroke_Path:
|
|
bv := b.(Display_Clip_Stroke_Path) or_return
|
|
return dl_path_eq(av.path, bv.path) && av.stroke == bv.stroke && av.ctm == bv.ctm
|
|
case Display_Clip_Text:
|
|
bv := b.(Display_Clip_Text) or_return
|
|
return dl_text_eq(av.text, bv.text) && av.ctm == bv.ctm
|
|
case Display_Clip_Image_Mask:
|
|
bv := b.(Display_Clip_Image_Mask) or_return
|
|
return av.img == bv.img && av.ctm == bv.ctm
|
|
case Display_Pop_Clip:
|
|
_ = b.(Display_Pop_Clip) or_return
|
|
return true
|
|
case Display_Fill_Text:
|
|
bv := b.(Display_Fill_Text) or_return
|
|
return dl_text_eq(av.text, bv.text) && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint)
|
|
case Display_Stroke_Text:
|
|
bv := b.(Display_Stroke_Text) or_return
|
|
return dl_text_eq(av.text, bv.text) && av.stroke == bv.stroke &&
|
|
av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint)
|
|
case Display_Ignore_Text:
|
|
bv := b.(Display_Ignore_Text) or_return
|
|
return dl_text_eq(av.text, bv.text) && av.ctm == bv.ctm
|
|
case Display_Fill_Image:
|
|
bv := b.(Display_Fill_Image) or_return
|
|
return av.img == bv.img && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint)
|
|
case Display_Fill_Image_Mask:
|
|
bv := b.(Display_Fill_Image_Mask) or_return
|
|
return av.img == bv.img && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint)
|
|
case Display_Fill_Shade:
|
|
bv := b.(Display_Fill_Shade) or_return
|
|
return av.shade == bv.shade && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint)
|
|
case Display_Begin_Mask:
|
|
bv := b.(Display_Begin_Mask) or_return
|
|
return av.area == bv.area && av.luminosity == bv.luminosity &&
|
|
dl_paint_eq(av.backdrop, bv.backdrop)
|
|
case Display_End_Mask:
|
|
bv := b.(Display_End_Mask) or_return
|
|
return av.transfer == bv.transfer && dl_mask_eq(av.mask, bv.mask)
|
|
case Display_Begin_Group:
|
|
bv := b.(Display_Begin_Group) or_return
|
|
return av.area == bv.area && av.isolated == bv.isolated &&
|
|
av.knockout == bv.knockout && av.blend == bv.blend &&
|
|
av.alpha == bv.alpha && dl_mask_eq(av.soft_mask, bv.soft_mask)
|
|
case Display_End_Group:
|
|
_ = b.(Display_End_Group) or_return
|
|
return true
|
|
case Display_Begin_Tile:
|
|
bv := b.(Display_Begin_Tile) or_return
|
|
return av == bv
|
|
case Display_End_Tile:
|
|
_ = b.(Display_End_Tile) or_return
|
|
return true
|
|
case Display_Begin_Layer:
|
|
bv := b.(Display_Begin_Layer) or_return
|
|
return av.name == bv.name
|
|
case Display_End_Layer:
|
|
_ = b.(Display_End_Layer) or_return
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// dl_kinds spells the recorded sequence as a list of tags, which is how the
|
|
// ordering tests read.
|
|
@(private = "file")
|
|
dl_kind :: proc(cmd: Display_Cmd) -> string {
|
|
switch _ in cmd {
|
|
case Display_Fill_Path: return "fill"
|
|
case Display_Stroke_Path: return "stroke"
|
|
case Display_Clip_Path: return "clip"
|
|
case Display_Clip_Stroke_Path: return "clip_stroke"
|
|
case Display_Clip_Text: return "clip_text"
|
|
case Display_Clip_Image_Mask: return "clip_image_mask"
|
|
case Display_Pop_Clip: return "pop"
|
|
case Display_Fill_Text: return "text"
|
|
case Display_Stroke_Text: return "stroke_text"
|
|
case Display_Ignore_Text: return "ignore_text"
|
|
case Display_Fill_Image: return "image"
|
|
case Display_Fill_Image_Mask: return "image_mask"
|
|
case Display_Fill_Shade: return "shade"
|
|
case Display_Begin_Mask: return "begin_mask"
|
|
case Display_End_Mask: return "end_mask"
|
|
case Display_Begin_Group: return "begin_group"
|
|
case Display_End_Group: return "end_group"
|
|
case Display_Begin_Tile: return "begin_tile"
|
|
case Display_End_Tile: return "end_tile"
|
|
case Display_Begin_Layer: return "begin_layer"
|
|
case Display_End_Layer: return "end_layer"
|
|
}
|
|
return "?"
|
|
}
|
|
|
|
@(private = "file")
|
|
dl_expect_kinds :: proc(t: ^testing.T, dl: ^Display_List, want: []string, loc := #caller_location) {
|
|
if !testing.expect_value(t, len(dl.cmds), len(want), loc = loc) {
|
|
for cmd, i in dl.cmds {
|
|
testing.expectf(t, false, " recorded [%d] %s", i, dl_kind(cmd), loc = loc)
|
|
}
|
|
return
|
|
}
|
|
for cmd, i in dl.cmds {
|
|
got := dl_kind(cmd)
|
|
testing.expectf(t, got == want[i], "command %d is %s, want %s", i, got, want[i], loc = loc)
|
|
}
|
|
}
|
|
|
|
@(private = "file")
|
|
dl_test_paint :: proc(cs: ^Colorspace) -> Paint {
|
|
return Paint{
|
|
cs = cs,
|
|
color = color_make(0.25, 0.5, 0.75),
|
|
rgb = {0.25, 0.5, 0.75},
|
|
alpha = 0.5,
|
|
blend = .Multiply,
|
|
}
|
|
}
|
|
|
|
@(private = "file")
|
|
dl_rect_near :: proc(t: ^testing.T, got, want: Rect, tol: f64, loc := #caller_location) {
|
|
ok := abs(got.x0 - want.x0) <= tol && abs(got.y0 - want.y0) <= tol &&
|
|
abs(got.x1 - want.x1) <= tol && abs(got.y1 - want.y1) <= tol
|
|
testing.expectf(t, ok, "bounds %v, want %v (tolerance %v)", got, want, tol, loc = loc)
|
|
}
|
|
|
|
// dl_contains reports whether `outer` covers `inner` -- what a surface sized
|
|
// from the bounds has to do for every command in the list.
|
|
@(private = "file")
|
|
dl_contains :: proc(outer, inner: Rect) -> bool {
|
|
return outer.x0 <= inner.x0 + 1e-9 && outer.y0 <= inner.y0 + 1e-9 &&
|
|
outer.x1 >= inner.x1 - 1e-9 && outer.y1 >= inner.y1 - 1e-9
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// A probe device: a foreign backend, to prove a replay reaches one with the
|
|
// values it was recorded with rather than only agreeing with another list.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(private = "file")
|
|
Dl_Event :: struct {
|
|
kind: string,
|
|
path: ^Path,
|
|
text: ^Text,
|
|
even_odd: bool,
|
|
ctm: Matrix,
|
|
paint: Paint,
|
|
stroke: Stroke_State,
|
|
}
|
|
|
|
@(private = "file")
|
|
Dl_Probe :: struct {
|
|
dev: Device,
|
|
events: [dynamic]Dl_Event,
|
|
}
|
|
|
|
@(private = "file")
|
|
dl_probe_init :: proc(p: ^Dl_Probe) {
|
|
p.events = make([dynamic]Dl_Event, 0, 8)
|
|
p.dev = Device{
|
|
user = p,
|
|
fill_path = proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) {
|
|
p := cast(^Dl_Probe)dev.user
|
|
append(&p.events, Dl_Event{
|
|
kind = "fill", path = path, even_odd = even_odd, ctm = ctm, paint = paint,
|
|
})
|
|
},
|
|
stroke_path = proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) {
|
|
p := cast(^Dl_Probe)dev.user
|
|
append(&p.events, Dl_Event{
|
|
kind = "stroke", path = path, ctm = ctm, paint = paint,
|
|
stroke = stroke != nil ? stroke^ : {},
|
|
})
|
|
},
|
|
fill_text = proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) {
|
|
p := cast(^Dl_Probe)dev.user
|
|
append(&p.events, Dl_Event{kind = "text", text = text, ctm = ctm, paint = paint})
|
|
},
|
|
pop_clip = proc(dev: ^Device) {
|
|
p := cast(^Dl_Probe)dev.user
|
|
append(&p.events, Dl_Event{kind = "pop"})
|
|
},
|
|
begin_group = proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) {
|
|
p := cast(^Dl_Probe)dev.user
|
|
append(&p.events, Dl_Event{kind = "begin_group"})
|
|
},
|
|
end_group = proc(dev: ^Device) {
|
|
p := cast(^Dl_Probe)dev.user
|
|
append(&p.events, Dl_Event{kind = "end_group"})
|
|
},
|
|
fill_image = proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) {
|
|
p := cast(^Dl_Probe)dev.user
|
|
append(&p.events, Dl_Event{kind = "image", ctm = ctm, paint = paint})
|
|
},
|
|
}
|
|
}
|
|
|
|
@(private = "file")
|
|
dl_probe_destroy :: proc(p: ^Dl_Probe) {
|
|
delete(p.events)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Recording
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(test)
|
|
test_display_list_records_a_fill_verbatim :: proc(t: ^testing.T) {
|
|
ld, err := list_device_make()
|
|
testing.expect_value(t, err, Error.None)
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 10, 20, 30, 40)
|
|
|
|
cs := Colorspace{kind = .Device_RGB, n = 3}
|
|
paint := dl_test_paint(&cs)
|
|
ctm := Matrix{2, 0, 0, 2, 5, 7}
|
|
|
|
dev_fill_path(dev, &p, true, ctm, paint)
|
|
|
|
dl := list_device_result(ld)
|
|
testing.expect_value(t, len(dl.cmds), 1)
|
|
cmd, ok := dl.cmds[0].(Display_Fill_Path)
|
|
testing.expect(t, ok, "a fill must record a Display_Fill_Path")
|
|
if !ok { return }
|
|
|
|
testing.expect(t, cmd.path != &p, "the list must not alias the caller's path")
|
|
testing.expect(t, dl_path_eq(cmd.path, &p), "the recorded path must equal the one given")
|
|
testing.expect_value(t, cmd.even_odd, true)
|
|
testing.expect_value(t, cmd.ctm, ctm)
|
|
testing.expect(t, dl_paint_eq(cmd.paint, paint), "the paint must be recorded unchanged")
|
|
testing.expect_value(t, cmd.paint.cs, &cs)
|
|
testing.expect_value(t, cmd.paint.alpha, 0.5)
|
|
testing.expect_value(t, cmd.paint.blend, Blend_Mode.Multiply)
|
|
testing.expect_value(t, cmd.paint.color.v[1], 0.5)
|
|
testing.expect(t, !dl.overflow, "one fill is not an overflow")
|
|
testing.expect(t, !dl.closed, "the list is not closed until close is called")
|
|
}
|
|
|
|
// THE headline: the interpreter reuses its path for the next operator, so a
|
|
// list that kept the pointer records whatever the path became later. Mutating
|
|
// the original after recording must not be visible in the list.
|
|
@(test)
|
|
test_display_list_copies_the_path :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 10, 10)
|
|
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
|
|
want := path_clone(&p)
|
|
defer path_destroy(&want)
|
|
|
|
// Everything the interpreter does to a path between two operators.
|
|
path_transform(&p, Matrix{3, 0, 0, 3, 100, 100})
|
|
path_line_to(&p, 999, 999)
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
path_clear(&p)
|
|
path_move_to(&p, -5, -5)
|
|
|
|
dl := list_device_result(ld)
|
|
testing.expect_value(t, len(dl.cmds), 2)
|
|
|
|
first, ok := dl.cmds[0].(Display_Fill_Path)
|
|
testing.expect(t, ok, "")
|
|
if !ok { return }
|
|
testing.expect(t, dl_path_eq(first.path, &want),
|
|
"the first recording changed when the caller's path was reused")
|
|
testing.expect_value(t, len(first.path.points), 4)
|
|
testing.expect_value(t, first.path.points[2], Point{10, 10})
|
|
|
|
// And the second recording is its own copy of the mutated path, not a
|
|
// second view of the same one.
|
|
second, ok2 := dl.cmds[1].(Display_Fill_Path)
|
|
testing.expect(t, ok2, "")
|
|
if !ok2 { return }
|
|
testing.expect(t, second.path != first.path, "each command owns its own copy")
|
|
testing.expect_value(t, len(second.path.points), 5)
|
|
testing.expect_value(t, second.path.points[0], Point{100, 100})
|
|
testing.expect_value(t, second.path.points[4], Point{999, 999})
|
|
|
|
// The list outlives the path it was recorded from: destroy it entirely and
|
|
// the recording is still readable.
|
|
path_destroy(&p)
|
|
testing.expect_value(t, len(first.path.points), 4)
|
|
testing.expect(t, !rect_is_empty(display_list_bounds(dl)), "bounds after the source path is gone")
|
|
}
|
|
|
|
@(test)
|
|
test_display_list_copies_the_text_run :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}}
|
|
txt := text_make()
|
|
defer text_destroy(&txt)
|
|
txt.font = &font
|
|
// A vertical CMap: the writing mode is part of the run and has to survive
|
|
// the copy, and leaving it at its default would hide a copy that dropped it.
|
|
txt.wmode = 1
|
|
txt.render_mode = .Fill_Stroke
|
|
append(&txt.items, Text_Item{gid = 36, code = 'A', cid = 36, trm = Matrix{0.01, 0, 0, 0.01, 100, 200}, advance = 0.6, unicode = 'A'})
|
|
append(&txt.items, Text_Item{gid = 37, code = 'B', cid = 37, trm = Matrix{0.01, 0, 0, 0.01, 106, 200}, advance = 0.6, unicode = 'B'})
|
|
append(&txt.items, Text_Item{gid = 3, code = 32, cid = 3, trm = Matrix{0.01, 0, 0, 0.01, 112, 200}, advance = 0.25, unicode = ' ', is_space = true})
|
|
|
|
dev_fill_text(dev, &txt, MATRIX_IDENTITY, Paint{alpha = 1})
|
|
|
|
// The interpreter reuses one Text for every show operator on the page.
|
|
clear(&txt.items)
|
|
append(&txt.items, Text_Item{gid = 99, unicode = 'Z'})
|
|
txt.render_mode = .Invisible
|
|
|
|
dl := list_device_result(ld)
|
|
testing.expect_value(t, len(dl.cmds), 1)
|
|
cmd, ok := dl.cmds[0].(Display_Fill_Text)
|
|
testing.expect(t, ok, "")
|
|
if !ok { return }
|
|
|
|
testing.expect(t, cmd.text != &txt, "the list must not alias the interpreter's Text")
|
|
testing.expect_value(t, cmd.text.font, &font)
|
|
testing.expect_value(t, cmd.text.render_mode, Text_Render_Mode.Fill_Stroke)
|
|
testing.expect_value(t, cmd.text.wmode, 1)
|
|
testing.expect_value(t, len(cmd.text.items), 3)
|
|
testing.expect_value(t, cmd.text.items[0].gid, 36)
|
|
testing.expect_value(t, cmd.text.items[1].unicode, 'B')
|
|
testing.expect_value(t, cmd.text.items[1].trm, Matrix{0.01, 0, 0, 0.01, 106, 200})
|
|
testing.expect_value(t, cmd.text.items[2].is_space, true)
|
|
testing.expect_value(t, cmd.text.items[2].advance, 0.25)
|
|
}
|
|
|
|
@(test)
|
|
test_display_list_copies_the_soft_mask :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 1, 1)
|
|
|
|
// Interpreter scratch: one Soft_Mask value, reused for a second mask later
|
|
// in the page, which is why the recorder compares contents and not the
|
|
// address.
|
|
mask := Soft_Mask{luminosity = true, backdrop = color_make(0.5), ctm = Matrix{1, 0, 0, 1, 3, 4}}
|
|
paint := Paint{alpha = 1, soft_mask = &mask}
|
|
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint)
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint)
|
|
mask.luminosity = false
|
|
mask.backdrop = color_make(0.25)
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint)
|
|
|
|
dl := list_device_result(ld)
|
|
testing.expect_value(t, len(dl.cmds), 3)
|
|
a := dl.cmds[0].(Display_Fill_Path).paint.soft_mask
|
|
b := dl.cmds[1].(Display_Fill_Path).paint.soft_mask
|
|
c := dl.cmds[2].(Display_Fill_Path).paint.soft_mask
|
|
testing.expect(t, a != nil && b != nil && c != nil, "")
|
|
if a == nil || b == nil || c == nil { return }
|
|
|
|
testing.expect(t, a != &mask, "the mask must be copied out of interpreter scratch")
|
|
testing.expect(t, a == b, "two fills under one unchanged mask share one copy")
|
|
testing.expect(t, c != a, "a changed mask at the same address is a different mask")
|
|
testing.expect_value(t, a.luminosity, true)
|
|
testing.expect_value(t, a.backdrop.v[0], 0.5)
|
|
testing.expect_value(t, a.ctm, Matrix{1, 0, 0, 1, 3, 4})
|
|
testing.expect_value(t, c.luminosity, false)
|
|
testing.expect_value(t, c.backdrop.v[0], 0.25)
|
|
}
|
|
|
|
@(test)
|
|
test_display_list_copies_the_layer_name :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
scratch := make([]byte, 5)
|
|
defer delete(scratch)
|
|
copy(scratch, "Layer")
|
|
|
|
dev_begin_layer(dev, string(scratch))
|
|
dev_end_layer(dev)
|
|
|
|
// The interpreter's buffer is reused for the next /OC name.
|
|
copy(scratch, "XXXXX")
|
|
|
|
dl := list_device_result(ld)
|
|
cmd, ok := dl.cmds[0].(Display_Begin_Layer)
|
|
testing.expect(t, ok, "")
|
|
if !ok { return }
|
|
testing.expect_value(t, cmd.name, "Layer")
|
|
testing.expect(t, raw_data(cmd.name) != raw_data(scratch), "the name must be copied")
|
|
|
|
// An /OC with no usable name still nests, and copying "" must not upset
|
|
// the teardown that gives every copied name back.
|
|
dev_begin_layer(dev, "")
|
|
dev_end_layer(dev)
|
|
empty, ok2 := dl.cmds[2].(Display_Begin_Layer)
|
|
testing.expect(t, ok2, "")
|
|
if ok2 {
|
|
testing.expect_value(t, empty.name, "")
|
|
}
|
|
dl_expect_kinds(t, dl, {"begin_layer", "end_layer", "begin_layer", "end_layer"})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Ordering and nesting
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(test)
|
|
test_display_list_clip_pushes_and_pops_in_order :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 10, 10)
|
|
stroke := STROKE_STATE_DEFAULT
|
|
|
|
dev_clip_path(dev, &p, false, MATRIX_IDENTITY)
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
dev_clip_stroke_path(dev, &p, &stroke, MATRIX_IDENTITY)
|
|
dev_fill_path(dev, &p, true, MATRIX_IDENTITY, Paint{})
|
|
dev_pop_clip(dev)
|
|
dev_pop_clip(dev)
|
|
|
|
dl := list_device_result(ld)
|
|
dl_expect_kinds(t, dl, {"clip", "fill", "clip_stroke", "fill", "pop", "pop"})
|
|
|
|
// The even-odd flag belongs to the fill it was given with, not to the
|
|
// clip that preceded it.
|
|
testing.expect_value(t, dl.cmds[1].(Display_Fill_Path).even_odd, false)
|
|
testing.expect_value(t, dl.cmds[3].(Display_Fill_Path).even_odd, true)
|
|
testing.expect_value(t, dl.cmds[0].(Display_Clip_Path).even_odd, false)
|
|
}
|
|
|
|
@(test)
|
|
test_display_list_nested_groups :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 10, 10)
|
|
|
|
dev_begin_group(dev, Rect{0, 0, 100, 100}, true, false, .Multiply, 0.5, nil)
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
dev_begin_layer(dev, "inner")
|
|
dev_begin_group(dev, Rect{10, 10, 20, 20}, false, true, .Screen, 0.25, nil)
|
|
dev_begin_mask(dev, Rect{0, 0, 5, 5}, true, Paint{})
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
dev_end_mask(dev, nil, nil)
|
|
dev_begin_tile(dev, Rect{0, 0, 8, 8}, Rect{0, 0, 8, 8}, 8, 8, MATRIX_IDENTITY)
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
dev_end_tile(dev)
|
|
dev_end_group(dev)
|
|
dev_end_layer(dev)
|
|
dev_end_group(dev)
|
|
|
|
dl := list_device_result(ld)
|
|
dl_expect_kinds(t, dl, {
|
|
"begin_group", "fill", "begin_layer", "begin_group",
|
|
"begin_mask", "fill", "end_mask",
|
|
"begin_tile", "fill", "end_tile",
|
|
"end_group", "end_layer", "end_group",
|
|
})
|
|
|
|
outer := dl.cmds[0].(Display_Begin_Group)
|
|
inner := dl.cmds[3].(Display_Begin_Group)
|
|
testing.expect_value(t, outer.area, Rect{0, 0, 100, 100})
|
|
testing.expect_value(t, outer.isolated, true)
|
|
testing.expect_value(t, outer.knockout, false)
|
|
testing.expect_value(t, outer.blend, Blend_Mode.Multiply)
|
|
testing.expect_value(t, outer.alpha, 0.5)
|
|
testing.expect_value(t, inner.area, Rect{10, 10, 20, 20})
|
|
testing.expect_value(t, inner.isolated, false)
|
|
testing.expect_value(t, inner.knockout, true)
|
|
testing.expect_value(t, inner.blend, Blend_Mode.Screen)
|
|
testing.expect_value(t, inner.alpha, 0.25)
|
|
|
|
tile := dl.cmds[7].(Display_Begin_Tile)
|
|
testing.expect_value(t, tile.xstep, 8)
|
|
testing.expect_value(t, tile.ystep, 8)
|
|
testing.expect_value(t, dl.cmds[4].(Display_Begin_Mask).luminosity, true)
|
|
}
|
|
|
|
// A recorder that wants the tile's content once, into the tile, must say 0 --
|
|
// a positive count would have the interpreter replay the cell for every
|
|
// placement and the list would hold a thousand copies of one cell.
|
|
@(test)
|
|
test_display_list_begin_tile_asks_for_one_pass :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
n := dev_begin_tile(dev, Rect{0, 0, 4, 4}, Rect{0, 0, 4, 4}, 4, 4, MATRIX_IDENTITY)
|
|
testing.expect_value(t, n, 0)
|
|
dev_end_tile(dev)
|
|
testing.expect_value(t, len(list_device_result(ld).cmds), 2)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Replay
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(test)
|
|
test_display_list_replay_is_faithful :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 3, 4, 20, 30)
|
|
path_move_to(&p, 1, 2)
|
|
path_curve_to(&p, 3, 4, 5, 6, 7, 8)
|
|
|
|
font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}}
|
|
txt := text_make()
|
|
defer text_destroy(&txt)
|
|
txt.font = &font
|
|
append(&txt.items, Text_Item{gid = 7, trm = Matrix{0.012, 0, 0, 0.012, 50, 60}, advance = 0.5, unicode = 'x'})
|
|
|
|
cs := Colorspace{kind = .Device_RGB, n = 3}
|
|
paint := dl_test_paint(&cs)
|
|
mask := Soft_Mask{luminosity = true, ctm = MATRIX_IDENTITY}
|
|
paint.soft_mask = &mask
|
|
|
|
img := Image{width = 4, height = 4, bpc = 8}
|
|
shade := Shading{kind = .Axial, has_bbox = true, bbox = Rect{0, 0, 1, 1}}
|
|
stroke := Stroke_State{line_width = 2.5, line_cap = .Round, line_join = .Bevel, miter_limit = 4}
|
|
stroke.dash[0] = 3
|
|
stroke.dash[1] = 1
|
|
stroke.dash_count = 2
|
|
|
|
dev_clip_path(dev, &p, true, Matrix{1, 0, 0, 1, 9, 9})
|
|
dev_fill_path(dev, &p, true, MATRIX_IDENTITY, paint)
|
|
dev_stroke_path(dev, &p, &stroke, Matrix{2, 0, 0, 2, 0, 0}, paint)
|
|
dev_clip_text(dev, &txt, MATRIX_IDENTITY)
|
|
dev_fill_text(dev, &txt, MATRIX_IDENTITY, paint)
|
|
dev_stroke_text(dev, &txt, &stroke, MATRIX_IDENTITY, paint)
|
|
dev_ignore_text(dev, &txt, MATRIX_IDENTITY)
|
|
dev_fill_image(dev, &img, Matrix{10, 0, 0, 10, 1, 1}, paint)
|
|
dev_fill_image_mask(dev, &img, MATRIX_IDENTITY, paint)
|
|
dev_clip_image_mask(dev, &img, MATRIX_IDENTITY)
|
|
dev_fill_shade(dev, &shade, MATRIX_IDENTITY, paint)
|
|
// The group's own mask is a different mask from the paints', so a replay
|
|
// that confused the two would be visible: 11.6.6 hands `begin_group` the
|
|
// mask the group's RESULT composites through, and it has to survive being
|
|
// recorded and played back like every other operand.
|
|
gmask := Soft_Mask{luminosity = true, ctm = Matrix{1, 0, 0, 1, 5, 6}}
|
|
dev_begin_group(dev, Rect{0, 0, 9, 9}, true, true, .Darken, 0.75, &gmask)
|
|
dev_begin_layer(dev, "L1")
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint)
|
|
dev_end_layer(dev)
|
|
dev_end_group(dev)
|
|
dev_begin_mask(dev, Rect{1, 1, 2, 2}, false, paint)
|
|
dev_end_mask(dev, &gmask, nil)
|
|
dev_pop_clip(dev)
|
|
dev_pop_clip(dev)
|
|
dev_pop_clip(dev)
|
|
|
|
first := list_device_result(ld)
|
|
n_first := len(first.cmds)
|
|
testing.expect(t, n_first > 0, "")
|
|
|
|
// Replay into a second recorder: the second device must see exactly what
|
|
// the first was given, command for command.
|
|
ld2, _ := list_device_make()
|
|
defer list_device_destroy(ld2)
|
|
testing.expect_value(t, display_list_run(first, list_device_as_device(ld2)), Error.None)
|
|
|
|
second := list_device_result(ld2)
|
|
testing.expect_value(t, len(second.cmds), n_first)
|
|
if len(second.cmds) != n_first { return }
|
|
for cmd, i in first.cmds {
|
|
testing.expectf(t, dl_cmd_eq(cmd, second.cmds[i]),
|
|
"command %d (%s) did not survive the replay", i, dl_kind(cmd))
|
|
}
|
|
|
|
// The second list is its own recording, not a view onto the first.
|
|
a := first.cmds[1].(Display_Fill_Path)
|
|
b := second.cmds[1].(Display_Fill_Path)
|
|
testing.expect(t, a.path != b.path, "a replay into a recorder copies again")
|
|
testing.expect(t, a.paint.soft_mask != b.paint.soft_mask, "and copies the mask again")
|
|
|
|
ga := dl_find_group(first)
|
|
gb := dl_find_group(second)
|
|
testing.expect(t, ga != nil && gb != nil, "both lists recorded the group")
|
|
if ga == nil || gb == nil { return }
|
|
testing.expect(t, ga.soft_mask != nil, "begin_group recorded its soft mask")
|
|
testing.expect(t, ga.soft_mask != &gmask,
|
|
"the group's mask must be copied out of interpreter scratch")
|
|
testing.expect(t, ga.soft_mask != gb.soft_mask, "and copied again on replay")
|
|
if ga.soft_mask != nil && gb.soft_mask != nil {
|
|
testing.expect_value(t, gb.soft_mask^, gmask)
|
|
}
|
|
|
|
// And a replay into a foreign device gets the same values.
|
|
probe: Dl_Probe
|
|
dl_probe_init(&probe)
|
|
defer dl_probe_destroy(&probe)
|
|
testing.expect_value(t, display_list_run(first, &probe.dev), Error.None)
|
|
|
|
// The probe implements seven hooks; the commands for the ones it leaves
|
|
// nil (clip_text, stroke_text, ignore_text, image masks, shade, mask,
|
|
// layer) pass through the dev_* wrappers and are simply not seen.
|
|
testing.expect_value(t, len(probe.events), 10)
|
|
if len(probe.events) != 10 { return }
|
|
testing.expect_value(t, probe.events[0].kind, "fill")
|
|
testing.expect_value(t, probe.events[0].even_odd, true)
|
|
testing.expect(t, dl_path_eq(probe.events[0].path, a.path), "the probe saw the recorded path")
|
|
testing.expect(t, dl_paint_eq(probe.events[0].paint, paint), "the probe saw the recorded paint")
|
|
testing.expect_value(t, probe.events[1].kind, "stroke")
|
|
testing.expect_value(t, probe.events[1].stroke, stroke)
|
|
testing.expect_value(t, probe.events[1].ctm, Matrix{2, 0, 0, 2, 0, 0})
|
|
testing.expect_value(t, probe.events[2].kind, "text")
|
|
testing.expect(t, dl_text_eq(probe.events[2].text, first.cmds[4].(Display_Fill_Text).text), "")
|
|
testing.expect_value(t, probe.events[3].kind, "image")
|
|
testing.expect_value(t, probe.events[3].ctm, Matrix{10, 0, 0, 10, 1, 1})
|
|
testing.expect_value(t, probe.events[4].kind, "begin_group")
|
|
testing.expect_value(t, probe.events[5].kind, "fill")
|
|
testing.expect_value(t, probe.events[6].kind, "end_group")
|
|
testing.expect_value(t, probe.events[7].kind, "pop")
|
|
testing.expect_value(t, probe.events[8].kind, "pop")
|
|
testing.expect_value(t, probe.events[9].kind, "pop")
|
|
}
|
|
|
|
// A replay honours the target's hints, exactly as the interpreter does: a text
|
|
// device has no more use for a decoded image the second time round.
|
|
@(test)
|
|
test_display_list_replay_honours_hints :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 1, 1)
|
|
img := Image{width = 2, height = 2}
|
|
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
dev_fill_image(dev, &img, MATRIX_IDENTITY, Paint{})
|
|
|
|
probe: Dl_Probe
|
|
dl_probe_init(&probe)
|
|
defer dl_probe_destroy(&probe)
|
|
|
|
probe.dev.hints = {.Ignore_Image}
|
|
testing.expect_value(t, display_list_run(list_device_result(ld), &probe.dev), Error.None)
|
|
testing.expect_value(t, len(probe.events), 1)
|
|
testing.expect_value(t, probe.events[0].kind, "fill")
|
|
|
|
clear(&probe.events)
|
|
probe.dev.hints = {}
|
|
_ = display_list_run(list_device_result(ld), &probe.dev)
|
|
testing.expect_value(t, len(probe.events), 2)
|
|
}
|
|
|
|
@(test)
|
|
test_display_list_replay_tolerates_nil :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 1, 1)
|
|
dev_fill_path(list_device_as_device(ld), &p, false, MATRIX_IDENTITY, Paint{})
|
|
|
|
testing.expect_value(t, display_list_run(nil, nil), Error.Invalid_Argument)
|
|
testing.expect_value(t, display_list_run(list_device_result(ld), nil), Error.None)
|
|
|
|
// A device with every hook nil is the interpreter's own tolerance case.
|
|
empty: Device
|
|
testing.expect_value(t, display_list_run(list_device_result(ld), &empty), Error.None)
|
|
|
|
// Destroying the list before the device that owns it is a misuse, and one
|
|
// that must not take the allocator with it: the list is zeroed, so the
|
|
// device has to remember where it came from.
|
|
ld2, _ := list_device_make()
|
|
display_list_destroy(list_device_result(ld2))
|
|
display_list_destroy(list_device_result(ld2))
|
|
list_device_destroy(ld2)
|
|
|
|
testing.expect_value(t, list_device_as_device(nil), (^Device)(nil))
|
|
testing.expect_value(t, list_device_result(nil), (^Display_List)(nil))
|
|
list_device_destroy(nil)
|
|
display_list_destroy(nil)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Bounds
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(test)
|
|
test_display_list_bounds_covers_every_command :: proc(t: ^testing.T) {
|
|
testing.expect_value(t, display_list_bounds(nil), RECT_EMPTY)
|
|
|
|
rect := path_make()
|
|
defer path_destroy(&rect)
|
|
path_rect(&rect, 10, 20, 20, 20) // 10,20 .. 30,40
|
|
|
|
line := path_make()
|
|
defer path_destroy(&line)
|
|
path_move_to(&line, 200, 100)
|
|
path_line_to(&line, 260, 100)
|
|
|
|
font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}}
|
|
txt := text_make()
|
|
defer text_destroy(&txt)
|
|
txt.font = &font
|
|
// Glyph space is 1000 units to the em, and the trm scales by 0.01, so the
|
|
// em is 10 device units and the 0.5-em advance is 5 of them.
|
|
append(&txt.items, Text_Item{gid = 1, trm = Matrix{0.01, 0, 0, 0.01, 400, 500}, advance = 0.5})
|
|
|
|
img := Image{width = 2, height = 2}
|
|
shade := Shading{kind = .Axial, has_bbox = true, bbox = Rect{0, 0, 2, 2}}
|
|
stroke := Stroke_State{line_width = 4, line_join = .Bevel, miter_limit = 10}
|
|
|
|
// Each command alone, so that a bounds case that stopped contributing
|
|
// cannot hide inside the union of the others.
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_fill_path(list_device_as_device(ld), &rect, false, MATRIX_IDENTITY, Paint{})
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{10, 20, 30, 40}, 1e-9)
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_fill_path(list_device_as_device(ld), &rect, false, Matrix{2, 0, 0, 2, 5, 5}, Paint{})
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{25, 45, 65, 85}, 1e-9)
|
|
}
|
|
{
|
|
// A clip is unioned in too: a consumer still has to address it.
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_clip_path(list_device_as_device(ld), &rect, false, MATRIX_IDENTITY)
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{10, 20, 30, 40}, 1e-9)
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_stroke_path(list_device_as_device(ld), &line, &stroke, MATRIX_IDENTITY, Paint{})
|
|
b := display_list_bounds(list_device_result(ld))
|
|
testing.expect(t, dl_contains(b, Rect{200, 98, 260, 102}), "the stroked line's ink")
|
|
testing.expect(t, dl_contains(Rect{170, 70, 290, 130}, b), "and not much more than that")
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_fill_text(list_device_as_device(ld), &txt, MATRIX_IDENTITY, Paint{})
|
|
b := display_list_bounds(list_device_result(ld))
|
|
testing.expect(t, dl_contains(b, Rect{400, 500, 405, 500}), "the glyph origin and its advance")
|
|
testing.expect(t, dl_contains(Rect{399, 485, 420, 515}, b), "roughly one em around the origin")
|
|
}
|
|
{
|
|
// Invisible text is the exception: it paints nothing, so it cannot
|
|
// enlarge the surface.
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_ignore_text(list_device_as_device(ld), &txt, MATRIX_IDENTITY)
|
|
testing.expect_value(t, len(list_device_result(ld).cmds), 1)
|
|
testing.expect_value(t, display_list_bounds(list_device_result(ld)), RECT_EMPTY)
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_fill_image(list_device_as_device(ld), &img, Matrix{50, 0, 0, 50, 600, 700}, Paint{})
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{600, 700, 650, 750}, 1e-9)
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_fill_image_mask(list_device_as_device(ld), &img, Matrix{4, 0, 0, 4, 0, 0}, Paint{})
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{0, 0, 4, 4}, 1e-9)
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_clip_image_mask(list_device_as_device(ld), &img, Matrix{0, 3, -3, 0, 10, 10})
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{7, 10, 10, 13}, 1e-9)
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_fill_shade(list_device_as_device(ld), &shade, Matrix{10, 0, 0, 10, 800, 900}, Paint{})
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{800, 900, 820, 920}, 1e-9)
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_begin_group(list_device_as_device(ld), Rect{1, 2, 3, 4}, false, false, .Normal, 1, nil)
|
|
dev_end_group(list_device_as_device(ld))
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{1, 2, 3, 4}, 1e-9)
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev_begin_mask(list_device_as_device(ld), Rect{5, 6, 7, 8}, true, Paint{})
|
|
dev_end_mask(list_device_as_device(ld), nil, nil)
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{5, 6, 7, 8}, 1e-9)
|
|
}
|
|
{
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
_ = dev_begin_tile(list_device_as_device(ld), Rect{2, 2, 6, 6}, Rect{2, 2, 6, 6}, 4, 4, MATRIX_IDENTITY)
|
|
dev_end_tile(list_device_as_device(ld))
|
|
dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{2, 2, 6, 6}, 1e-9)
|
|
}
|
|
|
|
// And all of them together: the union, from the fill's top-left corner to
|
|
// the shading's bottom-right.
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
testing.expect_value(t, display_list_bounds(list_device_result(ld)), RECT_EMPTY)
|
|
|
|
dev_fill_path(dev, &rect, false, MATRIX_IDENTITY, Paint{})
|
|
dev_stroke_path(dev, &line, &stroke, MATRIX_IDENTITY, Paint{})
|
|
dev_fill_text(dev, &txt, MATRIX_IDENTITY, Paint{})
|
|
dev_fill_image(dev, &img, Matrix{50, 0, 0, 50, 600, 700}, Paint{})
|
|
dev_fill_shade(dev, &shade, Matrix{10, 0, 0, 10, 800, 900}, Paint{})
|
|
|
|
dl := list_device_result(ld)
|
|
b := display_list_bounds(dl)
|
|
testing.expect(t, !dl.unbounded, "every command here has a bound")
|
|
testing.expect(t, dl_contains(b, Rect{10, 20, 30, 40}), "the fill")
|
|
testing.expect(t, dl_contains(b, Rect{200, 98, 260, 102}), "the stroke")
|
|
testing.expect(t, dl_contains(b, Rect{600, 700, 650, 750}), "the image")
|
|
testing.expect(t, dl_contains(b, Rect{800, 900, 820, 920}), "the shading")
|
|
dl_rect_near(t, b, Rect{10, 20, 820, 920}, 1)
|
|
}
|
|
|
|
@(test)
|
|
test_display_list_bounds_flags_the_unbounded :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 10, 10)
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
|
|
dl := list_device_result(ld)
|
|
testing.expect(t, !dl.unbounded, "")
|
|
|
|
// A shading with no /BBox paints the whole clip: there is no box for it.
|
|
shade := Shading{kind = .Axial}
|
|
dev_fill_shade(dev, &shade, MATRIX_IDENTITY, Paint{})
|
|
testing.expect(t, dl.unbounded, "an unbounded shading must be flagged")
|
|
dl_rect_near(t, display_list_bounds(dl), Rect{0, 0, 10, 10}, 1e-9)
|
|
|
|
// So does an infinite group area, which is what an XObject with no /BBox
|
|
// reaches the device as.
|
|
ld2, _ := list_device_make()
|
|
defer list_device_destroy(ld2)
|
|
dev_begin_group(list_device_as_device(ld2), RECT_INFINITE, false, false, .Normal, 1, nil)
|
|
dev_end_group(list_device_as_device(ld2))
|
|
dl2 := list_device_result(ld2)
|
|
testing.expect(t, dl2.unbounded, "an infinite area must be flagged")
|
|
testing.expect_value(t, display_list_bounds(dl2), RECT_EMPTY)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Robustness
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Past its cap the recorder must stop growing without unbalancing: a clip
|
|
// whose push was refused must not have its pop recorded, or every replay pops
|
|
// a clip the target never pushed.
|
|
@(test)
|
|
test_display_list_overflow_stays_balanced :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
dl := list_device_result(ld)
|
|
dl.max_cmds = 2
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 10, 10)
|
|
|
|
dev_clip_path(dev, &p, false, MATRIX_IDENTITY) // recorded
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) // recorded, now full
|
|
dev_clip_path(dev, &p, false, MATRIX_IDENTITY) // refused
|
|
dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) // refused
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) // refused
|
|
dev_end_group(dev) // must not be recorded
|
|
dev_pop_clip(dev) // matches the refused clip
|
|
dev_pop_clip(dev) // matches the recorded clip
|
|
|
|
testing.expect(t, dl.overflow, "the cap must be reported")
|
|
dl_expect_kinds(t, dl, {"clip", "fill", "pop"})
|
|
|
|
depth := 0
|
|
for cmd in dl.cmds {
|
|
#partial switch _ in cmd {
|
|
case Display_Clip_Path: depth += 1
|
|
case Display_Pop_Clip: depth -= 1
|
|
}
|
|
testing.expect(t, depth >= 0, "a replay must never pop more than it pushed")
|
|
}
|
|
testing.expect_value(t, depth, 0)
|
|
}
|
|
|
|
@(test)
|
|
test_display_list_byte_cap :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
dl := list_device_result(ld)
|
|
dl.max_bytes = 1
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 10, 10)
|
|
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
testing.expect_value(t, len(dl.cmds), 1)
|
|
testing.expect(t, dl.bytes > 1, "the copy must be counted")
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
testing.expect_value(t, len(dl.cmds), 1)
|
|
testing.expect(t, dl.overflow, "")
|
|
}
|
|
|
|
// close is the last thing the interpreter does, including on the error path,
|
|
// so it must leave a list that replays into a balanced device even if the page
|
|
// died inside three groups and a clip.
|
|
@(test)
|
|
test_display_list_close_unwinds_what_was_left_open :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 10, 10)
|
|
|
|
dev_clip_path(dev, &p, false, MATRIX_IDENTITY)
|
|
dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil)
|
|
dev_begin_layer(dev, "oc")
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
dev_close(dev)
|
|
|
|
dl := list_device_result(ld)
|
|
testing.expect(t, dl.closed, "")
|
|
dl_expect_kinds(t, dl, {"clip", "begin_group", "begin_layer", "fill",
|
|
"end_layer", "end_group", "pop"})
|
|
|
|
// Replaying it leaves a counting device back at depth zero.
|
|
depth := 0
|
|
for cmd in dl.cmds {
|
|
#partial switch _ in cmd {
|
|
case Display_Clip_Path: depth += 1
|
|
case Display_Begin_Group: depth += 1
|
|
case Display_Begin_Layer: depth += 1
|
|
case Display_Pop_Clip: depth -= 1
|
|
case Display_End_Group: depth -= 1
|
|
case Display_End_Layer: depth -= 1
|
|
}
|
|
}
|
|
testing.expect_value(t, depth, 0)
|
|
}
|
|
|
|
// A content stream with more Q than q reaches the device as a stray pop. The
|
|
// recorder must swallow it rather than record a pop with no push.
|
|
@(test)
|
|
test_display_list_stray_pops_are_dropped :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
dev_pop_clip(dev)
|
|
dev_pop_clip(dev)
|
|
dev_end_group(dev)
|
|
dev_end_mask(dev, nil, nil)
|
|
dev_end_tile(dev)
|
|
dev_end_layer(dev)
|
|
testing.expect_value(t, len(list_device_result(ld).cmds), 0)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 1, 1)
|
|
dev_clip_path(dev, &p, false, MATRIX_IDENTITY)
|
|
dev_pop_clip(dev)
|
|
dev_pop_clip(dev)
|
|
dl_expect_kinds(t, list_device_result(ld), {"clip", "pop"})
|
|
}
|
|
|
|
// Nothing the interpreter can hand a hook may crash it, including the nil an
|
|
// image that failed to load leaves behind.
|
|
@(test)
|
|
test_display_list_tolerates_nil_arguments :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
dev_fill_path(dev, nil, false, MATRIX_IDENTITY, Paint{})
|
|
dev_stroke_path(dev, nil, nil, MATRIX_IDENTITY, Paint{})
|
|
dev_clip_text(dev, nil, MATRIX_IDENTITY)
|
|
dev_clip_image_mask(dev, nil, MATRIX_IDENTITY)
|
|
dev_fill_text(dev, nil, MATRIX_IDENTITY, Paint{})
|
|
dev_fill_image(dev, nil, MATRIX_IDENTITY, Paint{})
|
|
dev_fill_image_mask(dev, nil, MATRIX_IDENTITY, Paint{})
|
|
dev_fill_shade(dev, nil, MATRIX_IDENTITY, Paint{})
|
|
dev_ignore_text(dev, nil, MATRIX_IDENTITY)
|
|
testing.expect_value(t, len(list_device_result(ld).cmds), 0)
|
|
|
|
// A clip that could not be recorded still has to swallow its pop.
|
|
dev_pop_clip(dev)
|
|
testing.expect_value(t, len(list_device_result(ld).cmds), 0)
|
|
|
|
// A stroke with a nil Stroke_State records the default, not zeros: a zero
|
|
// line width means a hairline, but a zero miter limit means nothing.
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 1, 1)
|
|
dev_stroke_path(dev, &p, nil, MATRIX_IDENTITY, Paint{})
|
|
cmd, ok := list_device_result(ld).cmds[0].(Display_Stroke_Path)
|
|
testing.expect(t, ok, "")
|
|
if ok {
|
|
testing.expect_value(t, cmd.stroke, STROKE_STATE_DEFAULT)
|
|
}
|
|
}
|
|
|
|
// A page can nest deeper than the recorder tracks. Past the cap it stops
|
|
// recording, and the pops still have to line up.
|
|
@(test)
|
|
test_display_list_deep_nesting_is_capped :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
n := MAX_DISPLAY_SCOPES + 10
|
|
for _ in 0 ..< n {
|
|
dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil)
|
|
}
|
|
for _ in 0 ..< n {
|
|
dev_end_group(dev)
|
|
}
|
|
dev_close(dev)
|
|
|
|
dl := list_device_result(ld)
|
|
testing.expect(t, dl.overflow, "the depth cap must be reported")
|
|
begins, ends := 0, 0
|
|
for cmd in dl.cmds {
|
|
#partial switch _ in cmd {
|
|
case Display_Begin_Group: begins += 1
|
|
case Display_End_Group: ends += 1
|
|
}
|
|
}
|
|
testing.expect_value(t, begins, MAX_DISPLAY_SCOPES)
|
|
testing.expect_value(t, ends, MAX_DISPLAY_SCOPES)
|
|
testing.expect_value(t, len(dl.cmds), 2 * MAX_DISPLAY_SCOPES)
|
|
}
|
|
|
|
// Non-finite geometry must never reach a consumer through the list.
|
|
@(test)
|
|
test_display_list_keeps_nan_out :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
inf := max(f64) * 10
|
|
dev_begin_group(dev, Rect{0, 0, inf, inf}, false, false, .Normal, inf, nil)
|
|
dev_begin_tile(dev, RECT_EMPTY, RECT_EMPTY, inf, inf, MATRIX_IDENTITY)
|
|
|
|
dl := list_device_result(ld)
|
|
g := dl.cmds[0].(Display_Begin_Group)
|
|
testing.expect(t, is_finite(g.alpha), "a non-finite alpha must be clamped")
|
|
testing.expect_value(t, g.alpha, 1)
|
|
testing.expect(t, dl.unbounded, "an area that is not finite has no bound")
|
|
testing.expect_value(t, g.area, RECT_INFINITE)
|
|
testing.expect(t, rect_is_finite(g.area) || g.area == RECT_INFINITE,
|
|
"a NaN corner must never reach a consumer")
|
|
|
|
tile := dl.cmds[1].(Display_Begin_Tile)
|
|
testing.expect(t, is_finite(tile.xstep) && is_finite(tile.ystep), "steps must be finite")
|
|
|
|
// path.odin filters NaN on construction, so the bounds of whatever does
|
|
// get recorded are finite too.
|
|
testing.expect(t, rect_is_finite(display_list_bounds(dl)), "bounds must be finite")
|
|
}
|
|
|
|
// Replaying a list into the device that is recording it is something a caller
|
|
// can do by accident. It has to terminate: the walk is over a snapshot of the
|
|
// command count, not over an array growing under it.
|
|
@(test)
|
|
test_display_list_self_replay_terminates :: proc(t: ^testing.T) {
|
|
ld, _ := list_device_make()
|
|
defer list_device_destroy(ld)
|
|
dev := list_device_as_device(ld)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 4, 4)
|
|
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{})
|
|
dev_clip_path(dev, &p, false, MATRIX_IDENTITY)
|
|
dev_pop_clip(dev)
|
|
|
|
dl := list_device_result(ld)
|
|
testing.expect_value(t, len(dl.cmds), 3)
|
|
testing.expect_value(t, display_list_run(dl, dev), Error.None)
|
|
testing.expect_value(t, len(dl.cmds), 6)
|
|
dl_expect_kinds(t, dl, {"fill", "clip", "pop", "fill", "clip", "pop"})
|
|
}
|
|
|
|
// Everything the list copied has to come back. The runner's own leak check
|
|
// reports a leak as a warning and still calls the suite successful, so the
|
|
// teardown gets an allocator it can be held to: one recording of every command
|
|
// that owns memory, destroyed, and nothing left behind.
|
|
@(test)
|
|
test_display_list_destroy_gives_everything_back :: proc(t: ^testing.T) {
|
|
track: mem.Tracking_Allocator
|
|
mem.tracking_allocator_init(&track, context.allocator)
|
|
defer mem.tracking_allocator_destroy(&track)
|
|
alloc := mem.tracking_allocator(&track)
|
|
|
|
p := path_make()
|
|
defer path_destroy(&p)
|
|
path_rect(&p, 0, 0, 10, 10)
|
|
|
|
font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}}
|
|
txt := text_make()
|
|
defer text_destroy(&txt)
|
|
txt.font = &font
|
|
append(&txt.items, Text_Item{gid = 1, trm = MATRIX_IDENTITY, advance = 0.5})
|
|
|
|
img := Image{width = 2, height = 2}
|
|
shade := Shading{kind = .Axial, has_bbox = true, bbox = Rect{0, 0, 1, 1}}
|
|
stroke := STROKE_STATE_DEFAULT
|
|
mask := Soft_Mask{luminosity = true}
|
|
paint := Paint{alpha = 1, soft_mask = &mask}
|
|
|
|
{
|
|
ld, err := list_device_make(alloc)
|
|
testing.expect_value(t, err, Error.None)
|
|
dev := list_device_as_device(ld)
|
|
|
|
dev_clip_path(dev, &p, false, MATRIX_IDENTITY)
|
|
dev_clip_stroke_path(dev, &p, &stroke, MATRIX_IDENTITY)
|
|
dev_clip_text(dev, &txt, MATRIX_IDENTITY)
|
|
dev_clip_image_mask(dev, &img, MATRIX_IDENTITY)
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint)
|
|
dev_stroke_path(dev, &p, &stroke, MATRIX_IDENTITY, paint)
|
|
dev_fill_text(dev, &txt, MATRIX_IDENTITY, paint)
|
|
dev_stroke_text(dev, &txt, &stroke, MATRIX_IDENTITY, paint)
|
|
dev_ignore_text(dev, &txt, MATRIX_IDENTITY)
|
|
dev_fill_image(dev, &img, MATRIX_IDENTITY, paint)
|
|
dev_fill_image_mask(dev, &img, MATRIX_IDENTITY, paint)
|
|
dev_fill_shade(dev, &shade, MATRIX_IDENTITY, paint)
|
|
dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil)
|
|
dev_begin_layer(dev, "a layer name long enough to be its own allocation")
|
|
mask.luminosity = false
|
|
dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint)
|
|
dev_end_layer(dev)
|
|
dev_end_group(dev)
|
|
dev_close(dev)
|
|
|
|
testing.expect(t, len(list_device_result(ld).cmds) > 15, "")
|
|
testing.expect(t, len(track.allocation_map) > 0, "the recording allocated")
|
|
|
|
list_device_destroy(ld)
|
|
}
|
|
testing.expectf(t, len(track.allocation_map) == 0,
|
|
"%d allocations were not given back", len(track.allocation_map))
|
|
testing.expect_value(t, len(track.bad_free_array), 0)
|
|
|
|
// And the same for a list destroyed before the device that owns it.
|
|
{
|
|
ld, _ := list_device_make(alloc)
|
|
dev_fill_path(list_device_as_device(ld), &p, false, MATRIX_IDENTITY, paint)
|
|
display_list_destroy(list_device_result(ld))
|
|
list_device_destroy(ld)
|
|
}
|
|
testing.expectf(t, len(track.allocation_map) == 0,
|
|
"%d allocations survived a list destroyed out of order", len(track.allocation_map))
|
|
testing.expect_value(t, len(track.bad_free_array), 0)
|
|
}
|