1091 lines
38 KiB
Odin
1091 lines
38 KiB
Odin
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
|
|
}
|