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 }