Files
pdf-odin-llm/device_list.odin
T

1091 lines
38 KiB
Odin
Raw Normal View History

2026-09-25 23:32:29 +02:00
package pdf
import "core:mem"
import "core:strings"
// The display-list Device: it records the drawing calls the interpreter makes
// instead of painting them, so a caller can replay them onto its own backend
// -- a GPU renderer, a PostScript writer, a hit-tester -- or replay them twice
// without re-running the content stream.
//
// SELF-CONTAINMENT IS THE WHOLE POINT
//
// `device.odin` tells devices they must not retain the Path and the Text they
// are handed: the interpreter reuses both for the next operator. A device that
// paints immediately does not care. A device that records has to copy, and the
// classic display-list bug is exactly this -- the list keeps the pointer, the
// interpreter clears the path for the next `re`, and every recorded fill
// silently becomes the last shape on the page. So every Path, every Text, every
// /OC name and every Soft_Mask is copied into the list's own allocator when it
// is recorded, and the list stays valid after the interpreter, its scratch
// arena and the original path are all gone.
//
// What is NOT copied is what the Document owns and therefore outlives the
// list: an ^Image, an ^Shading, an ^Font, an ^Colorspace, an ^Pattern and the
// ^Function on a soft mask all live in the Document arena until `close`.
// Copying a decoded 40 MB image per fill to gain nothing would be absurd.
// A Soft_Mask is the exception among the pointers: it is built by the
// interpreter, in interpreter scratch, so it is copied by value.
//
// THE LIST IS ALWAYS BALANCED
//
// Every `clip_*` pushes and `pop_clip` pops; every `begin_*` has its `end_*`.
// The interpreter guarantees that, but a device that trusts a content stream
// is a device waiting for a crash, so the recorder tracks its own depth: a
// push that is refused (because a cap was hit) also refuses its matching pop,
// and `close` unwinds whatever is still open. A recorded list can therefore be
// replayed into a real device without unbalancing that device's clip stack,
// however malformed the page was.
//
// CAPS
//
// A content stream is attacker-controlled and can emit fills forever, so the
// recorder is bounded twice: by command count and by the bytes it has copied.
// Past either cap it stops recording new drawing commands and sets `overflow`;
// what it has already recorded stays valid and replayable. Both caps are
// fields on the list rather than constants so a caller -- and the tests --
// can tighten them.
// MAX_DISPLAY_CMDS bounds the command count. Two million is far past the
// busiest real page (a dense map is tens of thousands) and still a bounded
// amount of memory for a generated one.
MAX_DISPLAY_CMDS :: 1 << 21
// MAX_DISPLAY_BYTES bounds the copied geometry -- path points, glyph runs,
// layer names -- plus the command array itself. 256 MiB of *copies* is already
// pathological; whichever of the two caps bites first wins, and in practice
// this one does, since a command is a few hundred bytes.
MAX_DISPLAY_BYTES :: 1 << 28
// Depth caps for the two nesting stacks the recorder keeps. Groups nest a
// handful deep in real files; clips follow q/Q, which `gstate.odin` caps at 64.
MAX_DISPLAY_SCOPES :: 256
MAX_DISPLAY_CLIPS :: 4096
// ---------------------------------------------------------------------------
// The recorded commands
//
// One variant per Device hook that draws, clips or nests. `close` is not a
// command: it marks the list finished rather than describing any ink.
// ---------------------------------------------------------------------------
Display_Fill_Path :: struct {
path: ^Path,
even_odd: bool,
ctm: Matrix,
paint: Paint,
}
Display_Stroke_Path :: struct {
path: ^Path,
// By value: a Stroke_State is a plain value -- the dash array is a fixed
// array, not a slice -- so there is nothing here to alias.
stroke: Stroke_State,
ctm: Matrix,
paint: Paint,
}
Display_Clip_Path :: struct {
path: ^Path,
even_odd: bool,
ctm: Matrix,
}
Display_Clip_Stroke_Path :: struct {
path: ^Path,
stroke: Stroke_State,
ctm: Matrix,
}
Display_Clip_Text :: struct {
text: ^Text,
ctm: Matrix,
}
Display_Clip_Image_Mask :: struct {
img: ^Image,
ctm: Matrix,
}
Display_Pop_Clip :: struct {}
Display_Fill_Text :: struct {
text: ^Text,
ctm: Matrix,
paint: Paint,
}
Display_Stroke_Text :: struct {
text: ^Text,
stroke: Stroke_State,
ctm: Matrix,
paint: Paint,
}
Display_Ignore_Text :: struct {
text: ^Text,
ctm: Matrix,
}
Display_Fill_Image :: struct {
img: ^Image,
ctm: Matrix,
paint: Paint,
}
Display_Fill_Image_Mask :: struct {
img: ^Image,
ctm: Matrix,
paint: Paint,
}
Display_Fill_Shade :: struct {
shade: ^Shading,
ctm: Matrix,
paint: Paint,
}
Display_Begin_Mask :: struct {
area: Rect,
luminosity: bool,
backdrop: Paint,
}
Display_End_Mask :: struct {
// The Soft_Mask this plane belongs to, copied into the list like every
// other one: it is what a replay device matches against the `soft_mask` on
// the paints that follow.
mask: ^Soft_Mask,
transfer: ^Function,
}
Display_Begin_Group :: struct {
area: Rect,
isolated: bool,
knockout: bool,
blend: Blend_Mode,
alpha: f64,
// The mask the group's RESULT composites through (11.6.6).
soft_mask: ^Soft_Mask,
}
Display_End_Group :: struct {}
Display_Begin_Tile :: struct {
area: Rect,
view: Rect,
xstep: f64,
ystep: f64,
ctm: Matrix,
}
Display_End_Tile :: struct {}
Display_Begin_Layer :: struct {
// Copied into the list: an /OC name can come from interpreter scratch.
name: string,
}
Display_End_Layer :: struct {}
Display_Cmd :: union {
Display_Fill_Path,
Display_Stroke_Path,
Display_Clip_Path,
Display_Clip_Stroke_Path,
Display_Clip_Text,
Display_Clip_Image_Mask,
Display_Pop_Clip,
Display_Fill_Text,
Display_Stroke_Text,
Display_Ignore_Text,
Display_Fill_Image,
Display_Fill_Image_Mask,
Display_Fill_Shade,
Display_Begin_Mask,
Display_End_Mask,
Display_Begin_Group,
Display_End_Group,
Display_Begin_Tile,
Display_End_Tile,
Display_Begin_Layer,
Display_End_Layer,
}
// ---------------------------------------------------------------------------
// The list
// ---------------------------------------------------------------------------
// Display_List is a page's drawing calls, in order, owning everything they
// point at except what the Document owns. It is produced by a List_Device and
// dies with it.
Display_List :: struct {
cmds: [dynamic]Display_Cmd,
// Where the copies live, so `display_list_destroy` can give them back.
allocator: mem.Allocator,
// Set when a cap stopped a command being recorded: the list is a valid
// prefix of the page, not the whole page.
overflow: bool,
// Set when something recorded has no bound `display_list_bounds` can
// compute -- an unbounded shading, an infinite group area. The bounds it
// returns then covers everything else, but not that.
unbounded: bool,
// Set by `close`, once the page is finished.
closed: bool,
// Bytes copied into the list so far -- paths, glyph runs, layer names,
// soft masks. The command array itself is counted against `max_bytes`
// separately, because one command is a few hundred bytes (most of it
// Paint's 32-component Color) and a cap that ignored it would let a
// generated page cost a gigabyte of commands carrying no geometry at all.
bytes: int,
max_cmds: int,
max_bytes: int,
// Soft masks copied out of interpreter scratch, kept for freeing. A run of
// commands under one mask shares one copy.
masks: [dynamic]^Soft_Mask,
}
// Display_Scope tags one open begin_* while recording, so the matching end_*
// knows whether its begin made it into the list.
@(private)
Display_Scope :: enum u8 {
Dropped = 0,
Mask,
Group,
Tile,
Layer,
}
// List_Device is the Device that fills a Display_List.
List_Device :: struct {
dev: Device,
list: Display_List,
// Recording bookkeeping, not part of the result. `clips` holds one entry
// per live clip -- true when it was recorded -- and `clip_dropped` counts
// the ones too deep to track. Same for `scopes`.
// The allocator the device itself came from. The list carries the same
// one, but `display_list_destroy` zeroes the list, and freeing the device
// afterwards must still work.
allocator: mem.Allocator,
clips: [dynamic]bool,
clip_dropped: int,
scopes: [dynamic]Display_Scope,
scope_dropped: int,
}
// list_device_make builds a recording device. The device, its list and every
// copy the list makes come from `allocator`; `list_device_destroy` frees them.
list_device_make :: proc(allocator := context.allocator) -> (^List_Device, Error) {
ld, err := new(List_Device, allocator)
if err != nil || ld == nil {
return nil, .Out_Of_Memory
}
ld.list = Display_List{
cmds = make([dynamic]Display_Cmd, 0, 64, allocator),
masks = make([dynamic]^Soft_Mask, 0, 2, allocator),
allocator = allocator,
max_cmds = MAX_DISPLAY_CMDS,
max_bytes = MAX_DISPLAY_BYTES,
}
ld.allocator = allocator
ld.clips = make([dynamic]bool, 0, 8, allocator)
ld.scopes = make([dynamic]Display_Scope, 0, 8, allocator)
ld.dev = Device{
user = ld,
fill_path = list_fill_path,
stroke_path = list_stroke_path,
clip_path = list_clip_path,
clip_stroke_path = list_clip_stroke_path,
clip_text = list_clip_text,
clip_image_mask = list_clip_image_mask,
pop_clip = list_pop_clip,
fill_text = list_fill_text,
stroke_text = list_stroke_text,
ignore_text = list_ignore_text,
fill_image = list_fill_image,
fill_image_mask = list_fill_image_mask,
fill_shade = list_fill_shade,
begin_mask = list_begin_mask,
end_mask = list_end_mask,
begin_group = list_begin_group,
end_group = list_end_group,
begin_tile = list_begin_tile,
end_tile = list_end_tile,
begin_layer = list_begin_layer,
end_layer = list_end_layer,
close = list_close,
}
return ld, .None
}
list_device_destroy :: proc(ld: ^List_Device) {
if ld == nil { return }
display_list_destroy(&ld.list)
delete(ld.clips)
delete(ld.scopes)
free(ld, ld.allocator)
}
// list_device_as_device returns the vtable to hand the interpreter.
list_device_as_device :: proc(ld: ^List_Device) -> ^Device {
if ld == nil { return nil }
return &ld.dev
}
// list_device_result returns the recorded list. It stays valid, and keeps
// growing, until `list_device_destroy`.
list_device_result :: proc(ld: ^List_Device) -> ^Display_List {
if ld == nil { return nil }
return &ld.list
}
// display_list_destroy releases every copy the list made. The Images, Fonts,
// Shadings and Colorspaces it points at belong to the Document and are not
// touched.
display_list_destroy :: proc(dl: ^Display_List) {
if dl == nil { return }
allocator := dl.allocator
for cmd in dl.cmds {
switch v in cmd {
case Display_Fill_Path: list_free_path(v.path, allocator)
case Display_Stroke_Path: list_free_path(v.path, allocator)
case Display_Clip_Path: list_free_path(v.path, allocator)
case Display_Clip_Stroke_Path: list_free_path(v.path, allocator)
case Display_Clip_Text: list_free_text(v.text, allocator)
case Display_Fill_Text: list_free_text(v.text, allocator)
case Display_Stroke_Text: list_free_text(v.text, allocator)
case Display_Ignore_Text: list_free_text(v.text, allocator)
case Display_Begin_Layer: delete(v.name, allocator)
case Display_Clip_Image_Mask, Display_Pop_Clip, Display_Fill_Image,
Display_Fill_Image_Mask, Display_Fill_Shade, Display_Begin_Mask,
Display_End_Mask, Display_Begin_Group, Display_End_Group,
Display_Begin_Tile, Display_End_Tile, Display_End_Layer:
// Nothing owned: every pointer in these belongs to the Document.
}
}
for m in dl.masks {
free(m, allocator)
}
delete(dl.masks)
delete(dl.cmds)
dl^ = {}
}
// ---------------------------------------------------------------------------
// Copying into the list
// ---------------------------------------------------------------------------
@(private)
list_free_path :: proc(p: ^Path, allocator: mem.Allocator) {
if p == nil { return }
path_destroy(p)
free(p, allocator)
}
@(private)
list_free_text :: proc(t: ^Text, allocator: mem.Allocator) {
if t == nil { return }
text_destroy(t)
free(t, allocator)
}
// list_full reports whether either cap has been reached. A zero or negative
// cap means the default, so a caller that zeroes the struct still gets a
// bounded recorder rather than an unbounded one.
@(private)
list_full :: proc(dl: ^Display_List) -> bool {
max_cmds := dl.max_cmds > 0 ? dl.max_cmds : MAX_DISPLAY_CMDS
max_bytes := dl.max_bytes > 0 ? dl.max_bytes : MAX_DISPLAY_BYTES
total := dl.bytes + len(dl.cmds) * size_of(Display_Cmd)
return len(dl.cmds) >= max_cmds || total >= max_bytes
}
@(private)
list_copy_path :: proc(dl: ^Display_List, p: ^Path) -> ^Path {
if p == nil { return nil }
out, err := new(Path, dl.allocator)
if err != nil || out == nil { return nil }
out^ = path_clone(p, dl.allocator)
dl.bytes += size_of(Path) +
len(p.verbs) * size_of(Path_Verb) +
len(p.points) * size_of(Point)
return out
}
@(private)
list_copy_text :: proc(dl: ^Display_List, t: ^Text) -> ^Text {
if t == nil { return nil }
out, err := new(Text, dl.allocator)
if err != nil || out == nil { return nil }
out.font = t.font
out.wmode = t.wmode
out.render_mode = t.render_mode
n := len(t.items)
out.items = make([dynamic]Text_Item, n, max(n, 1), dl.allocator)
if n > 0 {
copy(out.items[:], t.items[:])
}
dl.bytes += size_of(Text) + n * size_of(Text_Item)
return out
}
// list_copy_paint copies the one thing in a Paint that the Document does not
// own. Everything else in it is a value or a Document-owned pointer.
@(private)
list_copy_paint :: proc(dl: ^Display_List, p: Paint) -> Paint {
out := p
out.soft_mask = list_copy_mask(dl, p.soft_mask)
return out
}
// list_copy_mask copies a Soft_Mask by value, because the interpreter builds
// it in scratch that does not outlive the page.
//
// A run of hundreds of fills under one /SMask shares one copy, but the test is
// on the CONTENTS of the last copy rather than on the pointer: interpreter
// scratch is reused, so the same address can carry a different mask later in
// the page, and deduplicating on the address alone would record the first
// mask's backdrop for the second mask's fills.
@(private)
list_copy_mask :: proc(dl: ^Display_List, m: ^Soft_Mask) -> ^Soft_Mask {
if m == nil { return nil }
if n := len(dl.masks); n > 0 {
last := dl.masks[n - 1]
if last != nil && last^ == m^ { return last }
}
out, err := new(Soft_Mask, dl.allocator)
if err != nil || out == nil { return nil }
out^ = m^
append(&dl.masks, out)
dl.bytes += size_of(Soft_Mask)
return out
}
@(private)
list_from_dev :: proc(dev: ^Device) -> ^List_Device {
if dev == nil || dev.user == nil { return nil }
return cast(^List_Device)dev.user
}
// ---------------------------------------------------------------------------
// Nesting bookkeeping
// ---------------------------------------------------------------------------
// list_clip_note remembers whether a clip push made it into the list. The
// matching pop is recorded only if the push was, which is what keeps the list
// balanced when a cap refuses a push in the middle of a page.
@(private)
list_clip_note :: proc(ld: ^List_Device, recorded: bool) {
if len(ld.clips) >= MAX_DISPLAY_CLIPS {
ld.clip_dropped += 1
return
}
append(&ld.clips, recorded)
}
@(private)
list_clip_can_record :: proc(ld: ^List_Device) -> bool {
if len(ld.clips) >= MAX_DISPLAY_CLIPS {
ld.list.overflow = true
return false
}
if list_full(&ld.list) {
ld.list.overflow = true
return false
}
return true
}
// list_scope_open pushes one begin_* and reports whether to record it.
@(private)
list_scope_open :: proc(ld: ^List_Device, kind: Display_Scope) -> bool {
if len(ld.scopes) >= MAX_DISPLAY_SCOPES {
ld.scope_dropped += 1
ld.list.overflow = true
return false
}
if list_full(&ld.list) {
ld.list.overflow = true
append(&ld.scopes, Display_Scope.Dropped)
return false
}
append(&ld.scopes, kind)
return true
}
// list_scope_close pops one begin_* and reports the kind whose end_* should be
// recorded, if any. The kind comes from the stack rather than from the caller
// so that the list is balanced even if the interpreter's begins and ends were
// not; the alternative is a recorded begin whose end never appears, which
// unbalances every device the list is ever replayed into.
@(private)
list_scope_close :: proc(ld: ^List_Device) -> (Display_Scope, bool) {
if ld.scope_dropped > 0 {
ld.scope_dropped -= 1
return .Dropped, false
}
if len(ld.scopes) == 0 { return .Dropped, false }
kind := pop(&ld.scopes)
return kind, kind != .Dropped
}
@(private)
list_emit_end :: proc(dl: ^Display_List, kind: Display_Scope) {
switch kind {
case .Dropped:
case .Mask: append(&dl.cmds, Display_End_Mask{})
case .Group: append(&dl.cmds, Display_End_Group{})
case .Tile: append(&dl.cmds, Display_End_Tile{})
case .Layer: append(&dl.cmds, Display_End_Layer{})
}
}
// ---------------------------------------------------------------------------
// The hooks
// ---------------------------------------------------------------------------
@(private)
list_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) {
ld := list_from_dev(dev)
if ld == nil || path == nil { return }
dl := &ld.list
if list_full(dl) { dl.overflow = true; return }
p := list_copy_path(dl, path)
if p == nil { return }
append(&dl.cmds, Display_Fill_Path{
path = p, even_odd = even_odd, ctm = ctm, paint = list_copy_paint(dl, paint),
})
}
@(private)
list_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) {
ld := list_from_dev(dev)
if ld == nil || path == nil { return }
dl := &ld.list
if list_full(dl) { dl.overflow = true; return }
p := list_copy_path(dl, path)
if p == nil { return }
append(&dl.cmds, Display_Stroke_Path{
path = p,
stroke = stroke != nil ? stroke^ : STROKE_STATE_DEFAULT,
ctm = ctm,
paint = list_copy_paint(dl, paint),
})
}
@(private)
list_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) {
ld := list_from_dev(dev)
if ld == nil { return }
dl := &ld.list
recorded := false
if path != nil && list_clip_can_record(ld) {
if p := list_copy_path(dl, path); p != nil {
append(&dl.cmds, Display_Clip_Path{path = p, even_odd = even_odd, ctm = ctm})
recorded = true
}
}
list_clip_note(ld, recorded)
}
@(private)
list_clip_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) {
ld := list_from_dev(dev)
if ld == nil { return }
dl := &ld.list
recorded := false
if path != nil && list_clip_can_record(ld) {
if p := list_copy_path(dl, path); p != nil {
append(&dl.cmds, Display_Clip_Stroke_Path{
path = p,
stroke = stroke != nil ? stroke^ : STROKE_STATE_DEFAULT,
ctm = ctm,
})
recorded = true
}
}
list_clip_note(ld, recorded)
}
@(private)
list_clip_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) {
ld := list_from_dev(dev)
if ld == nil { return }
dl := &ld.list
recorded := false
if text != nil && list_clip_can_record(ld) {
if t := list_copy_text(dl, text); t != nil {
append(&dl.cmds, Display_Clip_Text{text = t, ctm = ctm})
recorded = true
}
}
list_clip_note(ld, recorded)
}
@(private)
list_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) {
ld := list_from_dev(dev)
if ld == nil { return }
dl := &ld.list
recorded := false
if img != nil && list_clip_can_record(ld) {
append(&dl.cmds, Display_Clip_Image_Mask{img = img, ctm = ctm})
recorded = true
}
list_clip_note(ld, recorded)
}
@(private)
list_pop_clip :: proc(dev: ^Device) {
ld := list_from_dev(dev)
if ld == nil { return }
if ld.clip_dropped > 0 {
ld.clip_dropped -= 1
return
}
// A pop with nothing pushed is a broken content stream, or an interpreter
// unwinding an error path twice. Dropping it is what keeps a replay from
// popping a clip the target device never pushed.
if len(ld.clips) == 0 { return }
if pop(&ld.clips) {
append(&ld.list.cmds, Display_Pop_Clip{})
}
}
@(private)
list_fill_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) {
ld := list_from_dev(dev)
if ld == nil || text == nil { return }
dl := &ld.list
if list_full(dl) { dl.overflow = true; return }
t := list_copy_text(dl, text)
if t == nil { return }
append(&dl.cmds, Display_Fill_Text{text = t, ctm = ctm, paint = list_copy_paint(dl, paint)})
}
@(private)
list_stroke_text :: proc(dev: ^Device, text: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) {
ld := list_from_dev(dev)
if ld == nil || text == nil { return }
dl := &ld.list
if list_full(dl) { dl.overflow = true; return }
t := list_copy_text(dl, text)
if t == nil { return }
append(&dl.cmds, Display_Stroke_Text{
text = t,
stroke = stroke != nil ? stroke^ : STROKE_STATE_DEFAULT,
ctm = ctm,
paint = list_copy_paint(dl, paint),
})
}
@(private)
list_ignore_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) {
ld := list_from_dev(dev)
if ld == nil || text == nil { return }
dl := &ld.list
if list_full(dl) { dl.overflow = true; return }
t := list_copy_text(dl, text)
if t == nil { return }
append(&dl.cmds, Display_Ignore_Text{text = t, ctm = ctm})
}
@(private)
list_fill_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) {
ld := list_from_dev(dev)
if ld == nil || img == nil { return }
dl := &ld.list
if list_full(dl) { dl.overflow = true; return }
append(&dl.cmds, Display_Fill_Image{img = img, ctm = ctm, paint = list_copy_paint(dl, paint)})
}
@(private)
list_fill_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) {
ld := list_from_dev(dev)
if ld == nil || img == nil { return }
dl := &ld.list
if list_full(dl) { dl.overflow = true; return }
append(&dl.cmds, Display_Fill_Image_Mask{img = img, ctm = ctm, paint = list_copy_paint(dl, paint)})
}
@(private)
list_fill_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) {
ld := list_from_dev(dev)
if ld == nil || shade == nil { return }
dl := &ld.list
if list_full(dl) { dl.overflow = true; return }
// A shading with no /BBox paints the whole clip: there is no box to fold
// into the list's bounds, so say so rather than under-report.
if !shade.has_bbox { dl.unbounded = true }
append(&dl.cmds, Display_Fill_Shade{shade = shade, ctm = ctm, paint = list_copy_paint(dl, paint)})
}
@(private)
list_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) {
ld := list_from_dev(dev)
if ld == nil { return }
if !list_scope_open(ld, .Mask) { return }
dl := &ld.list
append(&dl.cmds, Display_Begin_Mask{
area = list_safe_area(dl, area),
luminosity = luminosity,
backdrop = list_copy_paint(dl, backdrop),
})
}
@(private)
list_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) {
ld := list_from_dev(dev)
if ld == nil { return }
kind, ok := list_scope_close(ld)
if !ok { return }
if kind == .Mask {
append(&ld.list.cmds, Display_End_Mask{
mask = list_copy_mask(&ld.list, mask),
transfer = transfer,
})
return
}
list_emit_end(&ld.list, kind)
}
@(private)
list_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) {
ld := list_from_dev(dev)
if ld == nil { return }
if !list_scope_open(ld, .Group) { return }
dl := &ld.list
append(&dl.cmds, Display_Begin_Group{
area = list_safe_area(dl, area),
isolated = isolated,
knockout = knockout,
blend = blend,
alpha = clamp_finite(alpha, 1),
soft_mask = list_copy_mask(dl, soft_mask),
})
}
@(private)
list_end_group :: proc(dev: ^Device) {
ld := list_from_dev(dev)
if ld == nil { return }
kind, ok := list_scope_close(ld)
if ok { list_emit_end(&ld.list, kind) }
}
// list_begin_tile returns 0: the recorder wants the tile's content stream run
// once, into the tile, and records that one cell together with the lattice
// (`area`, `view`, the two steps and the CTM) that repeats it. A consumer of
// the list places the copies -- exactly as a GPU backend would rather do it,
// with one draw call and an instance count, instead of receiving the same cell
// a thousand times.
@(private)
list_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int {
ld := list_from_dev(dev)
if ld == nil { return 0 }
if !list_scope_open(ld, .Tile) { return 0 }
dl := &ld.list
append(&dl.cmds, Display_Begin_Tile{
area = list_safe_area(dl, area),
view = list_safe_area(dl, view),
xstep = clamp_finite(xstep, 0),
ystep = clamp_finite(ystep, 0),
ctm = ctm,
})
return 0
}
@(private)
list_end_tile :: proc(dev: ^Device) {
ld := list_from_dev(dev)
if ld == nil { return }
kind, ok := list_scope_close(ld)
if ok { list_emit_end(&ld.list, kind) }
}
@(private)
list_begin_layer :: proc(dev: ^Device, name: string) {
ld := list_from_dev(dev)
if ld == nil { return }
if !list_scope_open(ld, .Layer) { return }
dl := &ld.list
// The name is copied: a /OC entry's name may be built in scratch, and a
// list that outlives the page must not point into it.
copied := strings.clone(name, dl.allocator)
dl.bytes += len(copied)
append(&dl.cmds, Display_Begin_Layer{name = copied})
}
@(private)
list_end_layer :: proc(dev: ^Device) {
ld := list_from_dev(dev)
if ld == nil { return }
kind, ok := list_scope_close(ld)
if ok { list_emit_end(&ld.list, kind) }
}
// list_close unwinds anything the page left open, so that a list recorded off
// a content stream that ended inside three groups and a clip is still a list
// that replays into a balanced device.
//
// Groups, masks, tiles and layers unwind before the clips do. The two stacks
// carry no ordering between them, and a page that ended inside both was
// already broken; what matters is that both reach zero, so that the device the
// list is replayed into ends where it started.
@(private)
list_close :: proc(dev: ^Device) {
ld := list_from_dev(dev)
if ld == nil { return }
dl := &ld.list
for len(ld.scopes) > 0 {
list_emit_end(dl, pop(&ld.scopes))
}
ld.scope_dropped = 0
for len(ld.clips) > 0 {
if pop(&ld.clips) {
append(&dl.cmds, Display_Pop_Clip{})
}
}
ld.clip_dropped = 0
dl.closed = true
}
// list_safe_area flags an area that carries no usable bound, and keeps a
// non-finite one out of the recorded geometry: RECT_INFINITE is what "no
// bound" already means to everything below the Device line, whereas a NaN
// corner reaching a consumer's scanline loop is how a renderer hangs.
@(private)
list_safe_area :: proc(dl: ^Display_List, area: Rect) -> Rect {
if !rect_is_finite(area) {
dl.unbounded = true
return RECT_INFINITE
}
if area == RECT_INFINITE {
dl.unbounded = true
}
return area
}
// ---------------------------------------------------------------------------
// Replay
// ---------------------------------------------------------------------------
// display_list_run replays every recorded command into `dev`, in order. This
// is the identity that makes a recording worth trusting: recording a page and
// replaying it into the rasteriser must produce the same bitmap as rasterising
// it directly.
//
// It goes through the same `dev_*` wrappers the interpreter uses, so a target
// device may leave any hook nil, and it honours the target's hints -- a text
// device replaying a list has no more use for a decoded image than it had the
// first time.
//
// `close` is NOT replayed: closing is the owner's business, and a list may be
// replayed into the same device several times.
//
// One deviation from a live interpreter run: a tile was recorded as a single
// cell plus its lattice, so `begin_tile`'s return value is ignored here. A
// target device that asks for `n` placements gets the cell once and must place
// the copies itself from the `Display_Begin_Tile` it was handed.
display_list_run :: proc(dl: ^Display_List, dev: ^Device) -> Error {
if dl == nil { return .Invalid_Argument }
if dev == nil { return .None }
want_images := dev_wants_images(dev)
want_shades := dev_wants_shades(dev)
// The command count is snapshotted, and the walk is by index. Replaying a
// list into the device that is still recording it is a thing a caller can
// do by accident; iterating the array while appending to it would never
// terminate, and would keep allocating while it did not.
n := len(dl.cmds)
for i in 0 ..< n {
cmd := dl.cmds[i]
switch v in cmd {
case Display_Fill_Path:
dev_fill_path(dev, v.path, v.even_odd, v.ctm, v.paint)
case Display_Stroke_Path:
s := v.stroke
dev_stroke_path(dev, v.path, &s, v.ctm, v.paint)
case Display_Clip_Path:
dev_clip_path(dev, v.path, v.even_odd, v.ctm)
case Display_Clip_Stroke_Path:
s := v.stroke
dev_clip_stroke_path(dev, v.path, &s, v.ctm)
case Display_Clip_Text:
dev_clip_text(dev, v.text, v.ctm)
case Display_Clip_Image_Mask:
dev_clip_image_mask(dev, v.img, v.ctm)
case Display_Pop_Clip:
dev_pop_clip(dev)
case Display_Fill_Text:
dev_fill_text(dev, v.text, v.ctm, v.paint)
case Display_Stroke_Text:
s := v.stroke
dev_stroke_text(dev, v.text, &s, v.ctm, v.paint)
case Display_Ignore_Text:
dev_ignore_text(dev, v.text, v.ctm)
case Display_Fill_Image:
if want_images { dev_fill_image(dev, v.img, v.ctm, v.paint) }
case Display_Fill_Image_Mask:
if want_images { dev_fill_image_mask(dev, v.img, v.ctm, v.paint) }
case Display_Fill_Shade:
if want_shades { dev_fill_shade(dev, v.shade, v.ctm, v.paint) }
case Display_Begin_Mask:
dev_begin_mask(dev, v.area, v.luminosity, v.backdrop)
case Display_End_Mask:
dev_end_mask(dev, v.mask, v.transfer)
case Display_Begin_Group:
dev_begin_group(dev, v.area, v.isolated, v.knockout, v.blend, v.alpha, v.soft_mask)
case Display_End_Group:
dev_end_group(dev)
case Display_Begin_Tile:
_ = dev_begin_tile(dev, v.area, v.view, v.xstep, v.ystep, v.ctm)
case Display_End_Tile:
dev_end_tile(dev)
case Display_Begin_Layer:
dev_begin_layer(dev, v.name)
case Display_End_Layer:
dev_end_layer(dev)
}
}
return .None
}
// ---------------------------------------------------------------------------
// Bounds
// ---------------------------------------------------------------------------
// display_list_bounds is the union, in device space, of everything recorded --
// what a caller needs to size a surface to replay onto.
//
// It is deliberately conservative in three ways. Clips are unioned in rather
// than intersected: a clip cannot add ink, but a consumer still has to be able
// to address the region it covers, and tracking the intersection would mean
// re-implementing the clip stack here. A stroke's bound is its path's bound
// grown by the transformed line width and the miter allowance rather than by
// stroking the path. And a glyph's bound is its em box under its own text
// rendering matrix rather than its outline, which costs no glyph loading and
// is wrong only for a font whose glyphs leave the em square.
//
// Invisible text (render mode 3, the OCR layer under a scan) is the one thing
// left out: it paints nothing, so it cannot enlarge the surface anything is
// drawn on.
//
// `Display_List.unbounded` says whether anything recorded had no bound at all
// -- an unbounded shading, an infinite group area -- in which case what comes
// back covers everything else but not that.
display_list_bounds :: proc(dl: ^Display_List) -> Rect {
if dl == nil { return RECT_EMPTY }
r := RECT_EMPTY
for cmd in dl.cmds {
switch v in cmd {
case Display_Fill_Path:
r = list_bounds_add(r, path_tight_bounds(v.path, v.ctm))
case Display_Clip_Path:
r = list_bounds_add(r, path_tight_bounds(v.path, v.ctm))
case Display_Stroke_Path:
r = list_bounds_add(r, list_stroke_bounds(v.path, v.stroke, v.ctm))
case Display_Clip_Stroke_Path:
r = list_bounds_add(r, list_stroke_bounds(v.path, v.stroke, v.ctm))
case Display_Clip_Text:
r = list_bounds_add(r, list_text_bounds(v.text))
case Display_Fill_Text:
r = list_bounds_add(r, list_text_bounds(v.text))
case Display_Stroke_Text:
r = list_bounds_add(r, list_text_bounds(v.text))
case Display_Ignore_Text:
// Invisible text paints nothing, so it cannot enlarge the surface.
case Display_Clip_Image_Mask:
r = list_bounds_add(r, transform_rect(v.ctm, DISPLAY_UNIT_RECT))
case Display_Fill_Image:
r = list_bounds_add(r, transform_rect(v.ctm, DISPLAY_UNIT_RECT))
case Display_Fill_Image_Mask:
r = list_bounds_add(r, transform_rect(v.ctm, DISPLAY_UNIT_RECT))
case Display_Fill_Shade:
if v.shade != nil && v.shade.has_bbox {
r = list_bounds_add(r, transform_rect(v.ctm, v.shade.bbox))
}
case Display_Begin_Mask:
r = list_bounds_add(r, v.area)
case Display_Begin_Group:
r = list_bounds_add(r, v.area)
case Display_Begin_Tile:
r = list_bounds_add(r, v.area)
case Display_Pop_Clip, Display_End_Mask, Display_End_Group,
Display_End_Tile, Display_Begin_Layer, Display_End_Layer:
// No geometry of their own.
}
}
return r
}
// The unit square an image occupies in user space (ISO 32000-2, 8.9.5.2).
@(private)
DISPLAY_UNIT_RECT :: Rect{0, 0, 1, 1}
@(private)
list_bounds_add :: proc(r: Rect, b: Rect) -> Rect {
if !rect_is_finite(b) || b == RECT_INFINITE { return r }
if rect_is_empty(b) { return r }
return rect_union(r, b)
}
// list_stroke_bounds grows the path's own box by half the device-space line
// width, times the miter limit when the joins can spike. Cheaper than stroking
// and never smaller than the ink.
@(private)
list_stroke_bounds :: proc(p: ^Path, s: Stroke_State, ctm: Matrix) -> Rect {
b := path_tight_bounds(p, ctm)
if !rect_is_finite(b) { return RECT_EMPTY }
if p == nil || len(p.verbs) == 0 { return RECT_EMPTY }
width := max(clamp_finite(s.line_width, 1), 0)
pad := 0.5 * width * matrix_max_expansion(ctm)
// A zero-width line is a one-pixel hairline (8.4.3.2), so it still has
// half a pixel of ink on each side.
pad = max(pad, 0.5)
if s.line_join == .Miter {
limit := clamp_finite(s.miter_limit, 10)
pad *= clamp(limit, 1, 10)
}
if !is_finite(pad) { return b }
return rect_expand(b, pad)
}
// list_text_bounds boxes each glyph by its em square under its own text
// rendering matrix. The em box is taken as one em up and a third of an em
// down, which covers the ascender and descender of any text face; a glyph that
// draws outside its em square -- a swash, a big initial -- can escape it, and
// that is the price of not loading an outline to size a surface.
@(private)
list_text_bounds :: proc(t: ^Text) -> Rect {
if t == nil || len(t.items) == 0 { return RECT_EMPTY }
// Glyph space per em: 1000 for Type1 and CFF, unitsPerEm for TrueType.
// The trm maps glyph space, so the box has to be in glyph units too.
upem := 1000.0
m := font_matrix(t.font)
if is_finite(m.a) && abs(m.a) > 1e-12 {
upem = abs(1 / m.a)
}
if !is_finite(upem) || upem <= 0 || upem > 1e6 { upem = 1000 }
r := RECT_EMPTY
for it in t.items {
adv := abs(clamp_finite(it.advance, 1)) * upem
box: Rect
if t.wmode == 1 {
box = Rect{-0.6 * upem, -adv - 0.3 * upem, 0.6 * upem, 0.3 * upem}
} else {
box = Rect{0, -0.35 * upem, max(adv, 0.1 * upem), upem}
}
r = list_bounds_add(r, transform_rect(it.trm, box))
}
return r
}