Files
2026-09-25 23:32:29 +02:00

2642 lines
91 KiB
Odin

package pdf
import "core:mem"
import "core:mem/virtual"
// The content-stream interpreter (ISO 32000-2, 8.2 and Annex A).
//
// A content stream is postfix: operands are pushed until an operator arrives,
// the operator consumes the last few of them, and the stack is emptied again.
// That is the whole of the syntax, and it is why an unrecognised operator is
// harmless -- it takes its operands with it and the next one starts clean.
//
// This file is where every other file in the library meets. It resolves names
// against /Resources, keeps the graphics state, turns strings into positioned
// glyphs, and hands finished drawing operations to a Device. It never
// rasterises anything and never reaches into a device: everything goes through
// the `dev_*` wrappers in device.odin, so the three devices and this
// interpreter could be, and were, written apart from one another.
//
// THE THINGS THAT ARE EASY TO GET WRONG
//
// * `W` and `W*` do not clip. They arm a clip that the NEXT painting
// operator installs, after it has painted, using the path it painted.
// `W n` is the common spelling and paints nothing at all.
// * `Q` must restore the device clip stack to the depth its `q` saw, or
// everything after it draws through the wrong clip. `Graphics_State`
// carries `clip_depth` for exactly this.
// * A `q` that overflows the depth cap must not be matched by a later `Q`,
// or the stack underflows and the rest of the page draws in the state of
// whatever enclosed it.
// * Word spacing applies to a single-byte code 32 and to nothing else --
// never to a two-byte 0x0020 in a composite font, which is an ordinary
// CID (9.3.3). `Font_Code.is_space` is the only test.
// * Pattern space is anchored to the default space of the stream the
// pattern is a resource of, not to the CTM of the fill that paints it
// (8.7.3.1). `base_ctm` is that space; `gs.ctm` is not.
// * Inline image data is binary and is not lexed. Finding its end wrongly
// desynchronises every operator after it.
//
// TWO THINGS A DEVICE AUTHOR HAS TO KNOW
//
// * The interpreter counts the clips IT pushes, not the ones a device chose
// to honour, so a device that implements `pop_clip` must implement every
// `clip_*` hook it can be handed. Leaving `clip_stroke_path` nil while
// implementing `pop_clip` unbalances the stack: a patterned stroke pushes
// through the one and pops through the other.
// * A Type 3 glyph run reaches the device through `ignore_text` whatever the
// render mode is, because the drawing is done by running the glyph
// descriptions -- which arrive as ordinary fills and strokes. A device
// that wants the text of a Type 3 font must implement `ignore_text`.
//
// EVERYTHING IS BOUNDED
//
// A content stream is the most attacker-controlled data in a PDF. One fuel
// counter, shared by the whole run, bounds every token the interpreter reads
// anywhere -- in the page, in a form it invokes, in a Type 3 glyph, in a
// tiling pattern's cell. Recursion is depth-capped and guarded by a visited
// set, the operand stack is fixed, and every operand that reaches the geometry
// is checked for finiteness. Nothing here can fail a page: a malformed
// operator records a `Repair` and the next one runs.
// ---------------------------------------------------------------------------
// Limits
// ---------------------------------------------------------------------------
// Tokens -- operands and operators together -- one run may consume, including
// everything it recurses into. A 5 MB content stream is about a million
// tokens, so this is several of the largest pages anyone has drawn; past it a
// stream is looping, not drawing.
@(private)
DEFAULT_CONTENT_FUEL :: 1 << 22
// Nested BMC/BDC sections. The format has no limit and neither do producers,
// but a page that nests marked content 256 deep is not marking anything up.
@(private)
MAX_MARKED_DEPTH :: 256
// Cells of one tiling pattern replayed for one fill. `pattern_tiles` caps the
// lattice itself much higher; this is the cap on running a content stream per
// cell, which is thousands of times more expensive than placing one.
@(private)
MAX_TILE_REPLAYS :: 8192
// Soft masks one PAGE may install, counted on the shared state rather than on
// one run: a page's content stream and every one of its annotations are
// separate `run_content` calls, and a per-run cap would let MAX_ANNOTS of them
// multiply it. `q /GS gs Q` repeated a million times must not be a million
// allocations.
@(private)
MAX_SOFT_MASKS :: 1024
// Inline images one PAGE may read, counted on the shared state rather than on
// one run for the same reason the soft-mask budget is. Each one keeps its
// dictionary, its payload and its Image on the Document, none of which the
// page's scratch reclaims and none of which a second render of the same page
// reuses, and `BI ID EI` costs two tokens -- so the token budget alone
// permitted two million of them from a 37 KB file, which measured 1.35 GB per
// render. Four thousand is far past what any producer emits: 8.9.7 means an
// inline image for the handful of small ones a page carries -- it recommends
// they stay under 4 KiB each -- and a page with thousands of them is using
// them as a bomb, not as pictures.
@(private)
MAX_INLINE_IMAGES :: 4096
// Bytes of inline image data one BI/ID/EI may carry. 8.9.7 recommends 4 KiB;
// producers exceed it, but a 16 MB inline image is a decompression bomb
// rather than a picture.
@(private)
MAX_INLINE_IMAGE_BYTES :: 1 << 24
// Key/value pairs read from an inline image dictionary before the scan for
// `ID` gives up. The format defines eleven.
@(private)
MAX_INLINE_KEYS :: 64
// Entries read from an /OCProperties list.
@(private)
MAX_OC_GROUPS :: 4096
// Glyphs accumulated for one text-clipping object.
@(private)
MAX_TEXT_CLIP_ITEMS :: 1 << 16
// ---------------------------------------------------------------------------
// Options
// ---------------------------------------------------------------------------
// Content_Options tunes one run of the interpreter.
Content_Options :: struct {
// Scratch memory for everything the run allocates: the operand stack's
// arrays, the path under construction, glyph runs, tile lattices, and the
// string bodies the lexer materialises.
//
// It MUST be an arena or another bulk-freeing allocator: the run frees
// nothing individually, by design -- a page of text is tens of thousands
// of tiny allocations and a per-page arena reclaims them in one call.
// Leave it zero and the run makes, and destroys, a growing arena of its
// own, which is correct but costs one address-space reservation per call.
allocator: mem.Allocator,
// Tokens the whole run may consume, nested forms, Type 3 glyphs, soft
// masks and tiling replays included. 0 selects DEFAULT_CONTENT_FUEL.
max_ops: int,
// Draw every optional-content group, whatever the catalog's /OCProperties
// says is switched off. Text extraction usually wants this -- hidden text
// is still text -- and a renderer usually does not.
ignore_optional_content: bool,
// State shared by every run of ONE page. A page is not one run: the
// content stream is one and each annotation's appearance stream is
// another, and the things that have to be bounded per page rather than per
// run -- the soft-mask and inline-image budgets -- live here.
//
// It MUST come from the same memory as `allocator` and live exactly as
// long, because the soft masks it remembers are allocated from that
// allocator. `page_run` puts both in the page's scratch arena, which is
// what makes that automatic. Leave it nil and the run gets one of its own,
// which is correct for a single stream and is what every caller outside
// `page.odin` wants.
shared: ^Content_Shared,
}
// Content_Shared is the per-page state described on `Content_Options.shared`.
@(private)
Content_Shared :: struct {
n_soft_masks: int,
n_inline: int,
// The map every BI parses its dictionary into, cleared and reused. Only a
// BI that goes on to build an Image gets a dictionary of its own, because
// only that one outlives the run.
inline_dict: ^Dict,
// The ExtGState /SMask dictionary and CTM of the most recent install, and
// the Soft_Mask built for it. `/GS gs` repeated -- the same ExtGState
// applied to run after run of content -- is the common shape, and building
// a fresh Soft_Mask for each is an allocation and a slot of the budget
// spent on something already in hand.
last_smd: ^Dict,
last_ctm: Matrix,
last_mask: ^Soft_Mask,
}
// ---------------------------------------------------------------------------
// Run state
// ---------------------------------------------------------------------------
// Content_Ctx is what every stream in one run shares: the sink, the budget,
// and the guards against a stream that draws itself.
@(private)
Content_Ctx :: struct {
doc: ^Document,
dev: ^Device,
alloc: mem.Allocator,
opts: Content_Options,
// Tokens left. Shared, so a form cannot buy itself more by recursing.
fuel: int,
depth: int,
// Streams currently being executed, keyed by address. A form or a pattern
// cell that invokes itself is stopped here; one invoked twice in sequence
// is not, because the entry is removed on the way out.
visited: map[uintptr]bool,
// Optional-content groups the catalog's default configuration switches
// off, keyed by the address of their dictionary. `doc.cache` gives one
// address per object number, which is what makes the key meaningful.
oc_off: map[uintptr]bool,
// Per-page state, shared with the other runs of this page. Never nil: a
// run given none in its options gets `own_shared` below.
shared: ^Content_Shared,
own_shared: Content_Shared,
// One set of scratch buffers per recursion level. Nesting is strictly
// stacked and depth-capped, so level N's buffers are free the moment
// level N returns -- and a tiling pattern that replays its cell eight
// thousand times must not allocate eight thousand operand stacks. The
// scratch allocator is an arena and reclaims nothing until the run ends,
// so reuse here is the difference between kilobytes and hundreds of
// megabytes.
bufs: [MAX_DEPTH]Content_Buffers,
}
// Content_Buffers is everything a Content_Runner allocates, kept so the next
// run at the same depth can have it back with its capacity intact.
@(private)
Content_Buffers :: struct {
ready: bool,
stack: Gstate_Stack,
rect: Path,
boxes: [dynamic]Rect,
path: Path,
text: Text,
tclip: Text,
codes: [dynamic]Font_Code,
mc: [dynamic]Mc_Entry,
}
@(private)
Content_Flag :: enum u8 {
// Colour operators do nothing: inside an uncoloured tiling pattern's cell
// and inside a Type 3 glyph that declared itself a stencil with `d1`.
Ignore_Color,
// The stream describes a Type 3 glyph, so `d0` and `d1` mean something.
Type3_Glyph,
}
@(private)
Content_Flags :: bit_set[Content_Flag; u8]
@(private)
Pending_Clip :: enum u8 {
None,
Nonzero,
Even_Odd,
}
// Mc_Entry is one open marked-content section, so that the right `EMC` closes
// the right thing.
@(private)
Mc_Entry :: struct {
// dev_begin_layer was called and the matching EMC must end it.
layer: bool,
// This section is the one that switched drawing off.
hides: bool,
}
@(private)
Content_Runner :: struct {
ctx: ^Content_Ctx,
res: ^Dict,
gs: Graphics_State,
stack: Gstate_Stack,
// A conservative device-space bound on the clip, used to size the area a
// tiling pattern has to cover. It is saved and restored with the graphics
// state; `Graphics_State` is shared with three devices and frozen, so the
// box rides in a stack of its own pushed in lockstep with that one.
clip_box: Rect,
box_stack: [dynamic]Rect,
// The default space of this stream: what pattern space is anchored to.
// For a page it is the CTM `run_content` was given, for a form the CTM
// including the form's /Matrix, for a Type 3 glyph the glyph's own.
base_ctm: Matrix,
ops: [MAX_CONTENT_OPERANDS]Object,
n_ops: int,
path: Path,
// A scratch path for the rectangles the interpreter builds itself -- a
// form's /BBox, a tiling cell's, a Type 3 glyph's. Reused rather than
// built per use: a pattern replayed four thousand times would otherwise
// leave four thousand of them in an arena that reclaims nothing.
rect: Path,
pending_clip: Pending_Clip,
// Device clip entries this stream has pushed and not yet popped.
clip_depth: int,
// `q`s that overflowed the gstate cap. Their `Q`s must be swallowed
// rather than popping somebody else's state.
q_overflow: int,
in_text: bool,
tm, tlm: Matrix,
text: Text,
codes: [dynamic]Font_Code,
// Glyphs accumulated for a text clip, which is the union of everything
// shown in clipping mode between BT and ET and is installed at ET.
tclip: Text,
tclip_on: bool,
mc: [dynamic]Mc_Entry,
mc_over: int,
n_hidden: int,
// BX/EX nesting: inside it an unknown operator is expected, not damage.
compat: int,
flags: Content_Flags,
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
// run_content executes one content stream against a Device.
//
// `content` is the concatenated bytes of the stream -- `page_content` builds
// that for a page -- `res` is the /Resources dictionary every name in it is
// looked up in, and `ctm` maps the stream's default user space to device
// space. For a page that is the flip from PDF's y-up space to the bitmap's
// y-down one, composed with the zoom and /Rotate.
//
// Nothing here is fatal. An operator that cannot run records a `Repair` on the
// Document and the next one runs, so a page whose ninth operator is garbage
// still draws the other several thousand. .Invalid_Argument comes back only
// when there is no Document or the matrix is not a matrix; every other return
// is .None.
//
// The Device may be nil, which runs the whole interpreter and draws nothing.
// That is not a curiosity: it is how the interpreter is measured, and it is
// what a caller that only wants the repairs a page produces asks for.
@(private)
run_content :: proc(doc: ^Document, dev: ^Device, content: []byte, res: ^Dict,
ctm: Matrix, opts := Content_Options{}) -> Error {
if doc == nil {
return .Invalid_Argument
}
if !matrix_is_finite(ctm) {
return .Invalid_Argument
}
if len(content) == 0 {
return .None
}
// A caller that supplies no scratch allocator gets one that is certainly
// an arena, because the run frees nothing individually and handing it the
// context allocator would leak every string the lexer materialises.
arena: virtual.Arena
alloc := opts.allocator
if alloc.procedure == nil {
if arena_err := virtual.arena_init_growing(&arena); arena_err != nil {
return .Out_Of_Memory
}
alloc = virtual.arena_allocator(&arena)
}
defer if opts.allocator.procedure == nil {
virtual.arena_destroy(&arena)
}
ctx := Content_Ctx{
doc = doc,
dev = dev,
alloc = alloc,
opts = opts,
fuel = opts.max_ops > 0 ? opts.max_ops : DEFAULT_CONTENT_FUEL,
}
ctx.shared = opts.shared != nil ? opts.shared : &ctx.own_shared
ctx.visited = make(map[uintptr]bool, 8, alloc)
ctx.oc_off = make(map[uintptr]bool, 8, alloc)
defer delete(ctx.visited)
defer delete(ctx.oc_off)
content_load_oc(&ctx)
gs := gstate_default(ctm)
// 8.6.8: both colours start as black in DeviceGray. gstate_default cannot
// fill that in itself -- it has no Document to resolve the space with.
gs.fill.cs = colorspace_device_gray(doc)
gs.fill.color = colorspace_default_color(gs.fill.cs)
gs.stroke.cs = gs.fill.cs
gs.stroke.color = gs.fill.color
return content_exec(&ctx, content, res, gs, ctm, RECT_INFINITE, {})
}
// content_exec runs one stream: the page, a form, a Type 3 glyph, a pattern
// cell or a soft mask's group. Everything that differs between them is in the
// arguments.
@(private)
content_exec :: proc(ctx: ^Content_Ctx, data: []byte, res: ^Dict, gs: Graphics_State,
base_ctm: Matrix, clip_box: Rect, flags: Content_Flags) -> Error {
if ctx == nil || ctx.doc == nil {
return .Invalid_Argument
}
if ctx.depth >= MAX_DEPTH {
add_repair(ctx.doc, .Recursion_Limit_Hit, 0)
return .Recursion_Limit
}
b := &ctx.bufs[ctx.depth]
ctx.depth += 1
defer ctx.depth -= 1
r := Content_Runner{
ctx = ctx,
res = res,
gs = gs,
base_ctm = base_ctm,
clip_box = clip_box,
flags = flags,
tm = MATRIX_IDENTITY,
tlm = MATRIX_IDENTITY,
}
content_runner_acquire(&r, b)
defer content_runner_release(&r, b)
lex := lexer_make(data)
for {
if ctx.fuel <= 0 {
// Out of budget. Whatever has been drawn stays drawn.
add_repair(ctx.doc, .Content_Syntax, i64(lex.pos))
break
}
ctx.fuel -= 1
tok := lexer_next(&lex, ctx.alloc)
switch tok.kind {
case .EOF:
content_finish(&r)
return .None
case .Integer:
content_push(&r, tok.int_value)
case .Real:
content_push(&r, tok.real_value)
case .String:
content_push(&r, String(tok.bytes))
case .Name:
content_push(&r, Name(tok.text))
case .Array_Open, .Dict_Open:
// Composite operands -- a TJ array, a `d` dash array, a BDC
// property dictionary -- go through the object parser rather than
// being rebuilt here. Depth is seeded at 1 so that `<<...>>`
// cannot be read as the head of an indirect stream: in a content
// stream it never is.
p := parser_make(ctx.doc, data, ctx.alloc)
p.lex.pos = tok.start
p.depth = 1
obj, perr := parse_object(&p)
lex.pos = max(p.lex.pos, tok.end)
if perr == .None {
content_push(&r, obj)
}
case .Keyword:
switch tok.text {
case "true": content_push(&r, true)
case "false": content_push(&r, false)
case "null": content_push(&r, nil)
case "BI":
content_inline_image(&r, &lex)
r.n_ops = 0
case:
content_op(&r, tok.text)
r.n_ops = 0
}
case .Array_Close, .Dict_Close, .Brace_Open, .Brace_Close, .Error:
// A stray delimiter. The lexer consumed a byte, so progress is
// guaranteed; dropping it is what every viewer does.
if r.compat == 0 {
add_repair(ctx.doc, .Content_Syntax, i64(tok.start))
}
}
}
content_finish(&r)
return .None
}
// content_finish closes everything the stream left open. A truncated stream is
// the normal case, not an exceptional one, and the device's clip stack has to
// balance whatever the bytes did.
@(private)
content_finish :: proc(r: ^Content_Runner) {
if r.in_text {
content_text_end(r)
}
// Layers close in the order they opened.
for len(r.mc) > 0 {
e := pop(&r.mc)
if e.layer {
dev_end_layer(r.ctx.dev)
}
}
if gstate_depth(&r.stack) > 0 || r.q_overflow > 0 {
add_repair(r.ctx.doc, .Unbalanced_Q, 0)
}
for r.clip_depth > 0 {
dev_pop_clip(r.ctx.dev)
r.clip_depth -= 1
}
}
// content_runner_acquire hands the runner this depth's buffers, empty but
// with whatever capacity a previous run at the same depth grew them to.
@(private)
content_runner_acquire :: proc(r: ^Content_Runner, b: ^Content_Buffers) {
if !b.ready {
b.ready = true
b.stack = gstate_stack_make(r.ctx.alloc)
b.boxes = make([dynamic]Rect, 0, 8, r.ctx.alloc)
b.path = path_make(r.ctx.alloc, 32)
b.rect = path_make(r.ctx.alloc, 8)
b.text = text_make(r.ctx.alloc)
b.tclip = text_make(r.ctx.alloc)
b.codes = make([dynamic]Font_Code, 0, 32, r.ctx.alloc)
b.mc = make([dynamic]Mc_Entry, 0, 8, r.ctx.alloc)
} else {
clear(&b.stack.items)
clear(&b.boxes)
path_clear(&b.path)
path_clear(&b.rect)
clear(&b.text.items)
clear(&b.tclip.items)
clear(&b.codes)
clear(&b.mc)
}
b.text.font = nil
b.tclip.font = nil
r.stack = b.stack
r.box_stack = b.boxes
r.path = b.path
r.rect = b.rect
r.text = b.text
r.tclip = b.tclip
r.codes = b.codes
r.mc = b.mc
}
// content_runner_release gives them back. The arrays are copied out by value,
// so anything the run grew has to be written back or the capacity is lost --
// and, worse, a stale pointer would be handed to the next run at this depth.
@(private)
content_runner_release :: proc(r: ^Content_Runner, b: ^Content_Buffers) {
b.stack = r.stack
b.boxes = r.box_stack
b.path = r.path
b.rect = r.rect
b.text = r.text
b.tclip = r.tclip
b.codes = r.codes
b.mc = r.mc
r^ = {}
}
// ---------------------------------------------------------------------------
// The operand stack
//
// Operators read their arguments from the END of the stack, so a stream that
// pushes more operands than an operator takes -- which damaged files do
// constantly -- still runs it with the right ones.
// ---------------------------------------------------------------------------
@(private)
content_push :: proc(r: ^Content_Runner, o: Object) {
if r.n_ops >= MAX_CONTENT_OPERANDS {
// Full. Drop the oldest: the operator that eventually arrives wants
// the operands nearest to it, not the ones a hostile stream led with.
copy(r.ops[0:MAX_CONTENT_OPERANDS - 1], r.ops[1:MAX_CONTENT_OPERANDS])
r.ops[MAX_CONTENT_OPERANDS - 1] = o
return
}
r.ops[r.n_ops] = o
r.n_ops += 1
}
// content_arg is argument `i` of an operator taking `n` of them.
@(private)
content_arg :: proc(r: ^Content_Runner, n, i: int) -> Object {
idx := r.n_ops - n + i
if idx < 0 || idx >= r.n_ops {
return nil
}
return r.ops[idx]
}
@(private)
content_num :: proc(r: ^Content_Runner, n, i: int) -> f64 {
v, ok := to_number(content_arg(r, n, i))
if !ok || !is_finite(v) {
return 0
}
return v
}
@(private)
content_name :: proc(r: ^Content_Runner, n, i: int) -> string {
v, _ := to_name(content_arg(r, n, i))
return v
}
// content_need reports whether the operator has its operands. Too few is a
// malformed stream, not a fatal one: the operator is skipped and the next runs.
@(private)
content_need :: proc(r: ^Content_Runner, n: int) -> bool {
if r.n_ops >= n {
return true
}
if r.compat == 0 {
add_repair(r.ctx.doc, .Content_Syntax, 0)
}
return false
}
// ---------------------------------------------------------------------------
// Dispatch
//
// One switch over every operator in Annex A. An operator that is not here
// falls through to the default, which records a repair (unless a BX/EX
// bracket said to expect it) and lets the stack be cleared -- which is what
// makes an unknown operator harmless rather than fatal.
// ---------------------------------------------------------------------------
@(private)
content_op :: proc(r: ^Content_Runner, op: string) {
switch op {
// -- graphics state (8.4.4) -------------------------------------------
case "q":
saved := r.gs
saved.clip_depth = r.clip_depth
if !gstate_push(&r.stack, saved) {
// The cap was hit. Remember that this `q` saved nothing, so its
// `Q` pops nothing: the alternative is an underflow that leaves
// the rest of the page in an enclosing scope's state.
r.q_overflow += 1
add_repair(r.ctx.doc, .Unbalanced_Q, 0)
return
}
append(&r.box_stack, r.clip_box)
case "Q":
if r.q_overflow > 0 {
r.q_overflow -= 1
return
}
saved, ok := gstate_pop(&r.stack)
if !ok {
// 8.4.4: a `Q` with nothing to restore. Ignoring it is what every
// viewer does; the alternative is discarding the page.
add_repair(r.ctx.doc, .Unbalanced_Q, 0)
return
}
for r.clip_depth > saved.clip_depth {
dev_pop_clip(r.ctx.dev)
r.clip_depth -= 1
}
r.gs = saved
if len(r.box_stack) > 0 {
r.clip_box = pop(&r.box_stack)
}
case "cm":
if !content_need(r, 6) { return }
m := Matrix{
content_num(r, 6, 0), content_num(r, 6, 1), content_num(r, 6, 2),
content_num(r, 6, 3), content_num(r, 6, 4), content_num(r, 6, 5),
}
next := matrix_mul(m, r.gs.ctm)
if !matrix_is_finite(next) {
add_repair(r.ctx.doc, .Content_Syntax, 0)
return
}
r.gs.ctm = next
case "w":
if !content_need(r, 1) { return }
r.gs.stroke_state.line_width = max(content_num(r, 1, 0), 0)
case "J":
if !content_need(r, 1) { return }
r.gs.stroke_state.line_cap = Line_Cap(u8(clamp(content_num(r, 1, 0), 0, 2)))
case "j":
if !content_need(r, 1) { return }
r.gs.stroke_state.line_join = Line_Join(u8(clamp(content_num(r, 1, 0), 0, 2)))
case "M":
if !content_need(r, 1) { return }
// A miter limit below 1 is geometrically impossible and turns every
// join into a bevel; clamping keeps the stroker's ratio test sane.
r.gs.stroke_state.miter_limit = max(content_num(r, 1, 0), 1)
case "d":
if !content_need(r, 2) { return }
content_set_dash(r, content_arg(r, 2, 0), content_num(r, 2, 1))
case "ri":
if !content_need(r, 1) { return }
r.gs.render_intent = content_name(r, 1, 0)
case "i":
if !content_need(r, 1) { return }
r.gs.flatness = clamp(content_num(r, 1, 0), 0, 100)
case "gs":
if !content_need(r, 1) { return }
content_ext_gstate(r, content_name(r, 1, 0))
// -- path construction (8.5.2) ----------------------------------------
case "m":
if !content_need(r, 2) { return }
path_move_to(&r.path, content_num(r, 2, 0), content_num(r, 2, 1))
case "l":
if !content_need(r, 2) { return }
path_line_to(&r.path, content_num(r, 2, 0), content_num(r, 2, 1))
case "c":
if !content_need(r, 6) { return }
path_curve_to(&r.path,
content_num(r, 6, 0), content_num(r, 6, 1),
content_num(r, 6, 2), content_num(r, 6, 3),
content_num(r, 6, 4), content_num(r, 6, 5))
case "v":
if !content_need(r, 4) { return }
path_curve_to_v(&r.path,
content_num(r, 4, 0), content_num(r, 4, 1),
content_num(r, 4, 2), content_num(r, 4, 3))
case "y":
if !content_need(r, 4) { return }
path_curve_to_y(&r.path,
content_num(r, 4, 0), content_num(r, 4, 1),
content_num(r, 4, 2), content_num(r, 4, 3))
case "h":
path_close(&r.path)
case "re":
if !content_need(r, 4) { return }
path_rect(&r.path,
content_num(r, 4, 0), content_num(r, 4, 1),
content_num(r, 4, 2), content_num(r, 4, 3))
// -- path painting (8.5.3) --------------------------------------------
case "S": content_paint(r, false, false, true, false)
case "s": content_paint(r, true, false, true, false)
case "f", "F":
content_paint(r, false, true, false, false)
case "f*": content_paint(r, false, true, false, true)
case "B": content_paint(r, false, true, true, false)
case "B*": content_paint(r, false, true, true, true)
case "b": content_paint(r, true, true, true, false)
case "b*": content_paint(r, true, true, true, true)
case "n": content_paint(r, false, false, false, false)
// -- clipping (8.5.4) -------------------------------------------------
// W and W* do not clip. They arm a clip that the next painting operator
// installs, out of the path that operator painted.
case "W": r.pending_clip = .Nonzero
case "W*": r.pending_clip = .Even_Odd
// -- text objects and state (9.4.1, 9.3) ------------------------------
case "BT": content_text_begin(r)
case "ET": content_text_end(r)
case "Tc":
if !content_need(r, 1) { return }
r.gs.text.char_spacing = content_num(r, 1, 0)
case "Tw":
if !content_need(r, 1) { return }
r.gs.text.word_spacing = content_num(r, 1, 0)
case "Tz":
if !content_need(r, 1) { return }
// The operand is a percentage; Text_State holds the fraction.
r.gs.text.horiz_scale = content_num(r, 1, 0) / 100
case "TL":
if !content_need(r, 1) { return }
r.gs.text.leading = content_num(r, 1, 0)
case "Ts":
if !content_need(r, 1) { return }
r.gs.text.rise = content_num(r, 1, 0)
case "Tr":
if !content_need(r, 1) { return }
r.gs.text.render_mode = Text_Render_Mode(u8(clamp(content_num(r, 1, 0), 0, 7)))
case "Tf":
if !content_need(r, 2) { return }
r.gs.text.size = content_num(r, 2, 1)
content_set_font(r, content_name(r, 2, 0))
case "Td":
if !content_need(r, 2) { return }
content_text_move(r, content_num(r, 2, 0), content_num(r, 2, 1))
case "TD":
if !content_need(r, 2) { return }
r.gs.text.leading = -content_num(r, 2, 1)
content_text_move(r, content_num(r, 2, 0), content_num(r, 2, 1))
case "Tm":
if !content_need(r, 6) { return }
m := Matrix{
content_num(r, 6, 0), content_num(r, 6, 1), content_num(r, 6, 2),
content_num(r, 6, 3), content_num(r, 6, 4), content_num(r, 6, 5),
}
if !matrix_is_finite(m) {
add_repair(r.ctx.doc, .Content_Syntax, 0)
return
}
r.tm = m
r.tlm = m
case "T*":
content_text_move(r, 0, -r.gs.text.leading)
case "Tj":
if !content_need(r, 1) { return }
if s, ok := to_bytes(content_arg(r, 1, 0)); ok {
content_show(r, s)
}
case "TJ":
if !content_need(r, 1) { return }
content_show_array(r, content_arg(r, 1, 0))
case "'":
if !content_need(r, 1) { return }
content_text_move(r, 0, -r.gs.text.leading)
if s, ok := to_bytes(content_arg(r, 1, 0)); ok {
content_show(r, s)
}
case "\"":
if !content_need(r, 3) { return }
r.gs.text.word_spacing = content_num(r, 3, 0)
r.gs.text.char_spacing = content_num(r, 3, 1)
content_text_move(r, 0, -r.gs.text.leading)
if s, ok := to_bytes(content_arg(r, 3, 2)); ok {
content_show(r, s)
}
// -- Type 3 glyph metrics (9.6.4) -------------------------------------
case "d0":
// The advance is taken from /Widths, which is where every other part
// of the library reads it; d0 is accepted and adds nothing.
if !content_need(r, 2) { return }
case "d1":
if !content_need(r, 6) { return }
content_type3_d1(r)
// -- colour (8.6.8) ---------------------------------------------------
case "CS":
if !content_need(r, 1) { return }
content_set_colorspace(r, &r.gs.stroke, content_name(r, 1, 0))
case "cs":
if !content_need(r, 1) { return }
content_set_colorspace(r, &r.gs.fill, content_name(r, 1, 0))
case "SC", "SCN":
content_set_color(r, &r.gs.stroke)
case "sc", "scn":
content_set_color(r, &r.gs.fill)
case "G":
if !content_need(r, 1) { return }
content_set_device_color(r, &r.gs.stroke, colorspace_device_gray(r.ctx.doc), 1)
case "g":
if !content_need(r, 1) { return }
content_set_device_color(r, &r.gs.fill, colorspace_device_gray(r.ctx.doc), 1)
case "RG":
if !content_need(r, 3) { return }
content_set_device_color(r, &r.gs.stroke, colorspace_device_rgb(r.ctx.doc), 3)
case "rg":
if !content_need(r, 3) { return }
content_set_device_color(r, &r.gs.fill, colorspace_device_rgb(r.ctx.doc), 3)
case "K":
if !content_need(r, 4) { return }
content_set_device_color(r, &r.gs.stroke, colorspace_device_cmyk(r.ctx.doc), 4)
case "k":
if !content_need(r, 4) { return }
content_set_device_color(r, &r.gs.fill, colorspace_device_cmyk(r.ctx.doc), 4)
// -- shadings and XObjects --------------------------------------------
case "sh":
if !content_need(r, 1) { return }
content_shading(r, content_name(r, 1, 0))
case "Do":
if !content_need(r, 1) { return }
content_do(r, content_name(r, 1, 0))
// -- marked content (14.6) --------------------------------------------
case "MP", "DP":
// A marked-content point. It opens nothing, so it closes nothing.
case "BMC":
content_mc_begin(r, "", nil)
case "BDC":
if !content_need(r, 2) {
// A short BDC still OPENS a section. Returning here would leave
// the matching EMC to close the enclosing one instead: hidden
// optional content would become visible for the rest of the outer
// section, and a device layer would end around the wrong span of
// drawing. An anonymous section is what BMC already opens, and it
// keeps the pairing right.
content_mc_begin(r, "", nil)
return
}
content_mc_begin(r, content_name(r, 2, 0), content_arg(r, 2, 1))
case "EMC":
content_mc_end(r)
// -- compatibility (8.2) ----------------------------------------------
case "BX":
r.compat += 1
case "EX":
if r.compat > 0 {
r.compat -= 1
}
// -- inline images ----------------------------------------------------
case "ID", "EI":
// Reached only when a BI was lost or its data was mis-measured. The
// stack is cleared and the stream resynchronises on the next operator.
if r.compat == 0 {
add_repair(r.ctx.doc, .Content_Syntax, 0)
}
case:
// Annex A is closed, but files invent operators and PDF 2.0 added
// some this library does not implement. Inside BX/EX that is
// explicitly allowed and is not damage.
if r.compat == 0 {
add_repair(r.ctx.doc, .Content_Syntax, 0)
}
}
}
@(private)
content_set_dash :: proc(r: ^Content_Runner, arr: Object, phase: f64) {
a, ok := to_array(arr)
if !ok {
// `[] 0 d` is written as a bare `0 d` by some producers. Either way,
// no array means no dashing.
r.gs.stroke_state.dash_count = 0
r.gs.stroke_state.dash_phase = 0
return
}
n := min(array_len(a), MAX_DASH_ENTRIES)
for i in 0 ..< n {
v := array_num(r.ctx.doc, a, i, 0)
r.gs.stroke_state.dash[i] = is_finite(v) ? v : 0
}
r.gs.stroke_state.dash_count = n
r.gs.stroke_state.dash_phase = is_finite(phase) ? phase : 0
}
// ---------------------------------------------------------------------------
// Colour
// ---------------------------------------------------------------------------
// content_components reads the last `want` numbers of the `avail` operands.
// Reading from the end is what makes an operator survive a stream that pushed
// more operands than it takes, which damaged files do constantly.
@(private)
content_components :: proc(r: ^Content_Runner, avail, want: int) -> (c: Color) {
c.n = clamp(want, 0, MAX_COLOR_COMPONENTS)
for i in 0 ..< c.n {
idx := avail - c.n + i
if idx < 0 || idx >= avail || idx >= MAX_CONTENT_OPERANDS {
continue
}
if v, ok := to_number(r.ops[idx]); ok && is_finite(v) {
c.v[i] = v
}
}
return
}
@(private)
content_set_colorspace :: proc(r: ^Content_Runner, src: ^Paint_Source, name: string) {
if .Ignore_Color in r.flags {
return
}
cs, err := colorspace_load(r.ctx.doc, Name(name), r.res)
if err != .None || cs == nil {
add_repair(r.ctx.doc, .Bad_Resource, 0)
return
}
// 8.6.8: selecting a space also resets the colour to that space's initial
// value -- black, not whatever the previous space's components were.
src.cs = cs
src.color = colorspace_default_color(cs)
src.pattern = nil
}
@(private)
content_set_device_color :: proc(r: ^Content_Runner, src: ^Paint_Source, cs: ^Colorspace, n: int) {
if .Ignore_Color in r.flags {
return
}
src.cs = cs
src.color = content_components(r, r.n_ops, n)
src.pattern = nil
}
@(private)
content_set_color :: proc(r: ^Content_Runner, src: ^Paint_Source) {
if .Ignore_Color in r.flags {
return
}
cs := src.cs
if cs == nil {
cs = colorspace_device_gray(r.ctx.doc)
src.cs = cs
}
if cs.kind != .Pattern {
src.color = content_components(r, r.n_ops, cs.n)
src.pattern = nil
return
}
// In a Pattern space the last operand names the pattern. Anything before
// it is the colour an uncoloured cell paints in, and lives in the
// underlying space (8.7.3.1).
avail := r.n_ops
name := ""
if avail > 0 {
if nm, ok := to_name(r.ops[avail - 1]); ok {
name = nm
avail -= 1
}
}
if name == "" {
add_repair(r.ctx.doc, .Content_Syntax, 0)
return
}
if cs.under != nil {
src.color = content_components(r, avail, cs.under.n)
}
pats := dict_dict(r.ctx.doc, r.res, "Pattern")
entry := dict_get_raw(pats, name)
if entry == nil {
add_repair(r.ctx.doc, .Bad_Resource, 0)
src.pattern = nil
return
}
pat, err := pattern_load(r.ctx.doc, entry, r.res)
if err != .None || pat == nil {
add_repair(r.ctx.doc, .Bad_Resource, 0)
src.pattern = nil
return
}
src.pattern = pat
}
// content_paint_of resolves everything a device needs for one operation.
@(private)
content_paint_of :: proc(r: ^Content_Runner, src: ^Paint_Source, alpha: f64) -> (p: Paint) {
p.cs = src.cs
p.color = src.color
p.pattern = src.pattern
p.alpha = clamp(alpha, 0, 1)
p.blend = r.gs.blend
p.soft_mask = r.gs.soft_mask
if p.cs == nil {
p.cs = colorspace_device_gray(r.ctx.doc)
}
// An uncoloured pattern's components are in the underlying space, so that
// is what converts them; a coloured one carries no components at all.
conv := p.cs
if conv.kind == .Pattern {
conv = conv.under
}
if conv != nil && p.color.n > 0 {
p.rgb = colorspace_to_rgb(conv, p.color)
}
return
}
// ---------------------------------------------------------------------------
// Path painting
// ---------------------------------------------------------------------------
@(private)
content_paint :: proc(r: ^Content_Runner, close, fill, stroke, even_odd: bool) {
if close {
path_close(&r.path)
}
if r.n_hidden == 0 && !path_is_empty(&r.path) {
if fill {
content_fill(r, even_odd)
}
if stroke {
content_stroke(r)
}
}
// The clip comes last, and out of the path that was just painted.
content_apply_clip(r)
path_clear(&r.path)
}
@(private)
content_fill :: proc(r: ^Content_Runner, even_odd: bool) {
src := &r.gs.fill
if src.cs != nil && src.cs.is_none {
// A /Separation /None colorant marks nothing at all (8.6.6.4).
return
}
if src.pattern != nil {
content_paint_pattern(r, src, even_odd, false)
return
}
if src.cs != nil && src.cs.kind == .Pattern {
// A Pattern space with no pattern selected paints nothing.
return
}
dev_fill_path(r.ctx.dev, &r.path, even_odd, r.gs.ctm,
content_paint_of(r, src, r.gs.fill_alpha))
}
@(private)
content_stroke :: proc(r: ^Content_Runner) {
src := &r.gs.stroke
if src.cs != nil && src.cs.is_none {
return
}
if src.pattern != nil {
content_paint_pattern(r, src, false, true)
return
}
if src.cs != nil && src.cs.kind == .Pattern {
return
}
dev_stroke_path(r.ctx.dev, &r.path, &r.gs.stroke_state, r.gs.ctm,
content_paint_of(r, src, r.gs.stroke_alpha))
}
@(private)
content_apply_clip :: proc(r: ^Content_Runner) {
if r.pending_clip == .None {
return
}
even_odd := r.pending_clip == .Even_Odd
r.pending_clip = .None
dev_clip_path(r.ctx.dev, &r.path, even_odd, r.gs.ctm)
r.clip_depth += 1
r.clip_box = rect_intersect(r.clip_box, path_tight_bounds(&r.path, r.gs.ctm))
}
// content_shape_area is the device-space region the current path can mark,
// intersected with what is left of the clip. It is what bounds a tiling
// pattern: a lattice only has to cover the shape being filled.
@(private)
content_shape_area :: proc(r: ^Content_Runner, stroking: bool) -> Rect {
area := path_tight_bounds(&r.path, r.gs.ctm)
if stroking {
// Half the pen sticks out either side, and a miter further still.
w := r.gs.stroke_state.line_width * matrix_max_expansion(r.gs.ctm)
area = rect_expand(area, max(w * r.gs.stroke_state.miter_limit, 1))
}
if !rect_is_finite(area) {
return RECT_EMPTY
}
return rect_intersect(r.clip_box, area)
}
// ---------------------------------------------------------------------------
// Patterns (8.7.3, 8.7.4)
//
// The interpreter resolves patterns itself rather than handing them to the
// device: a shading pattern becomes a clipped `fill_shade`, and a tiling
// pattern becomes the cell's content stream replayed once per placement. That
// is why `Paint.pattern` is cleared on the way out -- by the time a device
// sees the operation there is nothing left for it to resolve.
// ---------------------------------------------------------------------------
@(private)
content_paint_pattern :: proc(r: ^Content_Runner, src: ^Paint_Source, even_odd, stroking: bool) {
pat := src.pattern
if pat == nil {
return
}
// Clip to the shape, so the pattern marks only inside it.
if stroking {
dev_clip_stroke_path(r.ctx.dev, &r.path, &r.gs.stroke_state, r.gs.ctm)
} else {
dev_clip_path(r.ctx.dev, &r.path, even_odd, r.gs.ctm)
}
r.clip_depth += 1
defer {
dev_pop_clip(r.ctx.dev)
r.clip_depth -= 1
}
content_pattern_over(r, src, content_shape_area(r, stroking),
stroking ? r.gs.stroke_alpha : r.gs.fill_alpha)
}
// content_pattern_over paints the selected pattern over `area`, through
// whatever clip the caller has already installed: the shape for a fill, the
// stencil for an image mask.
@(private)
content_pattern_over :: proc(r: ^Content_Runner, src: ^Paint_Source, area: Rect, alpha: f64) {
pat := src.pattern
if pat == nil {
return
}
switch pat.kind {
case .Shading:
if !dev_wants_shades(r.ctx.dev) {
return
}
// Pattern space is anchored to the stream's default space, never to
// the CTM of the operation being painted (8.7.3.1).
m := pattern_matrix(pat, r.base_ctm)
if !matrix_is_finite(m) || pat.shading == nil {
return
}
paint := content_paint_of(r, src, alpha)
paint.pattern = nil
// 8.7.4.3: a shading pattern may carry an /ExtGState whose parameters
// are put into effect while it paints. It paints no path and shows no
// text, so the compositing parameters are the whole of what can
// matter, and the format says they replace rather than compound.
if pat.ext_gstate != nil {
doc := r.ctx.doc
if v, ok := to_number(dict_get(doc, pat.ext_gstate, "ca")); ok && is_finite(v) {
paint.alpha = clamp(v, 0, 1)
}
if nm, ok := to_name(dict_get(doc, pat.ext_gstate, "BM")); ok {
paint.blend = blend_mode_from_name(nm)
}
}
dev_fill_shade(r.ctx.dev, pat.shading, m, paint)
case .Tiling:
content_run_tiles(r, pat, src, area, alpha)
}
}
@(private)
content_run_tiles :: proc(r: ^Content_Runner, pat: ^Pattern, src: ^Paint_Source,
area: Rect, alpha: f64) {
ctx := r.ctx
if pat.content == nil {
return
}
key := uintptr(rawptr(pat.content))
if key in ctx.visited {
// A cell that fills with its own pattern.
add_repair(ctx.doc, .Content_Syntax, 0)
return
}
data, derr := stream_data(ctx.doc, pat.content)
if derr != .None || len(data) == 0 {
return
}
m := pattern_matrix(pat, r.base_ctm)
if !matrix_is_finite(m) {
return
}
// The cell starts from a clean graphics state at the cell's own CTM: it
// inherits nothing from the fill but the compositing parameters, which is
// what makes a patterned fill at 50% alpha 50% transparent.
base := gstate_default(m)
base.fill_alpha = clamp(alpha, 0, 1)
base.stroke_alpha = base.fill_alpha
base.blend = r.gs.blend
base.soft_mask = r.gs.soft_mask
base.fill.cs = colorspace_device_gray(ctx.doc)
base.fill.color = colorspace_default_color(base.fill.cs)
base.stroke.cs = base.fill.cs
base.stroke.color = base.fill.color
flags := r.flags
if pattern_needs_color(pat) {
// /PaintType 2: the cell is a stencil and every colour operator in it
// is ignored in favour of the colour that came with the pattern name.
under := src.cs != nil ? src.cs.under : nil
if under == nil {
under = colorspace_device_gray(ctx.doc)
}
base.fill.cs = under
base.fill.color = src.color
base.stroke.cs = under
base.stroke.color = src.color
flags += {.Ignore_Color}
}
cells := dev_begin_tile(ctx.dev, area, pat.bbox, pat.xstep, pat.ystep, m)
defer dev_end_tile(ctx.dev)
if cells <= 0 {
// The device places the copies itself and wants the cell drawn once.
content_run_cell(r, pat, data, base, m, flags)
return
}
tiles := pattern_tiles(pat, area, r.base_ctm, ctx.alloc)
defer delete(tiles, ctx.alloc)
n := min(len(tiles), cells, MAX_TILE_REPLAYS)
for i in 0 ..< n {
content_run_cell(r, pat, data, base, tiles[i], flags)
}
}
@(private)
content_run_cell :: proc(r: ^Content_Runner, pat: ^Pattern, data: []byte,
base: Graphics_State, ctm: Matrix, flags: Content_Flags) {
ctx := r.ctx
if !matrix_is_finite(ctm) {
return
}
gs := base
gs.ctm = ctm
box := r.clip_box
clipped := false
if !rect_is_empty(pat.bbox) {
path_clear(&r.rect)
path_rect(&r.rect, pat.bbox.x0, pat.bbox.y0, rect_width(pat.bbox), rect_height(pat.bbox))
dev_clip_path(ctx.dev, &r.rect, false, ctm)
r.clip_depth += 1
clipped = true
box = rect_intersect(box, path_tight_bounds(&r.rect, ctm))
}
res := pat.resources
if res == nil {
res = r.res
}
key := uintptr(rawptr(pat.content))
ctx.visited[key] = true
content_exec(ctx, data, res, gs, ctm, box, flags)
delete_key(&ctx.visited, key)
if clipped {
dev_pop_clip(ctx.dev)
r.clip_depth -= 1
}
}
// ---------------------------------------------------------------------------
// Text (9.4)
//
// THE TEXT MATRIX
//
// Tm is the only operator that sets it outright. Td, TD, T*, ' and " all
// translate the LINE matrix and copy it across; every glyph shown translates
// the text matrix alone, so the next line starts from where the last one
// began rather than from where it ended.
//
// One glyph's rendering matrix is
//
// [ Tfs*Th 0 0 ]
// [ 0 Tfs 0 ] x Tm x CTM
// [ 0 Ts 1 ]
//
// with the font matrix in front of it to get out of glyph space, and the pen
// then advances by (w0*Tfs + Tc + Tw) * Th. The horizontal scale multiplies
// the advance and the glyph but never the rise; the rise moves the glyph and
// never the pen.
// ---------------------------------------------------------------------------
@(private)
content_text_begin :: proc(r: ^Content_Runner) {
r.in_text = true
r.tm = MATRIX_IDENTITY
r.tlm = MATRIX_IDENTITY
clear(&r.tclip.items)
r.tclip.font = nil
r.tclip_on = false
}
@(private)
content_text_end :: proc(r: ^Content_Runner) {
r.in_text = false
if r.tclip_on {
// 9.3.6: the clip is the union of every glyph shown in a clipping
// mode anywhere between BT and ET, and it takes effect here.
dev_clip_text(r.ctx.dev, &r.tclip, r.gs.ctm)
r.clip_depth += 1
}
r.tclip_on = false
clear(&r.tclip.items)
r.tclip.font = nil
}
// content_text_move is Td: it translates the line matrix and restarts the
// text matrix from it.
@(private)
content_text_move :: proc(r: ^Content_Runner, tx, ty: f64) {
m := matrix_mul(translate_matrix(tx, ty), r.tlm)
if !matrix_is_finite(m) {
add_repair(r.ctx.doc, .Content_Syntax, 0)
return
}
r.tlm = m
r.tm = m
}
@(private)
content_set_font :: proc(r: ^Content_Runner, name: string) {
fonts := dict_dict(r.ctx.doc, r.res, "Font")
fd := dict_dict(r.ctx.doc, fonts, name)
if fd == nil {
// No font: the size is still set, so a later Tf that does resolve
// lands in the right state. Showing text meanwhile draws nothing.
add_repair(r.ctx.doc, .Bad_Resource, 0)
r.gs.text.font = nil
return
}
f, err := font_load(r.ctx.doc, fd)
if err != .None || f == nil {
add_repair(r.ctx.doc, .Bad_Font_Object, 0)
r.gs.text.font = nil
return
}
r.gs.text.font = f
}
// content_show_array is TJ: strings are shown, numbers move the pen.
@(private)
content_show_array :: proc(r: ^Content_Runner, obj: Object) {
a, ok := to_array(obj)
if !ok {
return
}
for i in 0 ..< array_len(a) {
if r.ctx.fuel <= 0 {
return
}
r.ctx.fuel -= 1
#partial switch v in array_get_raw(a, i) {
case String:
content_show(r, transmute([]byte)v)
case i64:
content_tj_adjust(r, f64(v))
case f64:
content_tj_adjust(r, v)
}
}
}
// content_tj_adjust applies one number from a TJ array. It is a displacement
// in thousandths of a text-space unit and it is SUBTRACTED from the current
// position, so a positive number pulls the following glyphs closer -- which is
// how every kerning pair in every PDF is written.
@(private)
content_tj_adjust :: proc(r: ^Content_Runner, amount: f64) {
if !is_finite(amount) {
return
}
ts := &r.gs.text
d := -amount / 1000 * ts.size
m: Matrix
if ts.font != nil && ts.font.vertical {
m = matrix_mul(translate_matrix(0, d), r.tm)
} else {
m = matrix_mul(translate_matrix(d * ts.horiz_scale, 0), r.tm)
}
if matrix_is_finite(m) {
r.tm = m
}
}
// content_show is Tj: decode the string, place every glyph, advance the pen,
// and hand the run to the device.
@(private)
content_show :: proc(r: ^Content_Runner, s: []byte) {
ctx := r.ctx
f := r.gs.text.font
if f == nil || len(s) == 0 {
return
}
font_decode(f, s, &r.codes)
if len(r.codes) == 0 {
return
}
// Glyphs cost budget too, or a stream of a few operators showing megabytes
// of text would run unbounded.
if ctx.fuel < len(r.codes) {
ctx.fuel = 0
} else {
ctx.fuel -= len(r.codes)
}
ts := &r.gs.text
mode := ts.render_mode
vertical := f.vertical
fm := font_matrix(f)
clear(&r.text.items)
r.text.font = f
r.text.wmode = vertical ? 1 : 0
r.text.render_mode = mode
for fc in r.codes {
// Text space to device space, for this glyph, at this pen position.
tsm := Matrix{ts.size * ts.horiz_scale, 0, 0, ts.size, 0, ts.rise}
trm := matrix_mul(tsm, matrix_mul(r.tm, r.gs.ctm))
gm := fm
if vertical && (fc.vx != 0 || fc.vy != 0) {
// 9.7.4.3: the position vector moves the glyph off the vertical
// baseline. It is in thousandths of a text-space unit whatever
// units the font program itself uses, so it is applied after the
// font matrix rather than inside glyph space.
gm = matrix_mul(fm, translate_matrix(-fc.vx * 0.001, -fc.vy * 0.001))
}
// 9.3.3: word spacing applies to a single-byte code 32 and to nothing
// else. font_decode has already made that judgement.
ws := fc.is_space ? ts.word_spacing : 0
adv: f64
if vertical {
// fc.width is w1y, the vertical displacement, already in
// thousandths and normally negative.
adv = fc.width * 0.001 * ts.size + ts.char_spacing + ws
} else {
w0 := fc.width * 0.001
if f.kind == .Type3 {
// A Type 3 width is in the font's own glyph space, and
// /FontMatrix -- not a fixed 1/1000 -- is what maps it.
w0 = transform_vector(fm, Point{fc.width, 0}).x
}
adv = w0 * ts.size + ts.char_spacing + ws
}
if !is_finite(adv) {
adv = 0
}
item := Text_Item{
gid = fc.gid,
code = fc.code,
cid = fc.cid,
trm = matrix_mul(gm, trm),
advance = adv,
unicode = fc.unicode,
is_space = fc.is_space,
}
if matrix_is_finite(item.trm) {
append(&r.text.items, item)
}
next: Matrix
if vertical {
next = matrix_mul(translate_matrix(0, adv), r.tm)
} else {
next = matrix_mul(translate_matrix(adv * ts.horiz_scale, 0), r.tm)
}
if matrix_is_finite(next) {
r.tm = next
}
}
if r.n_hidden == 0 {
content_emit_text(r, mode)
}
}
@(private)
content_emit_text :: proc(r: ^Content_Runner, mode: Text_Render_Mode) {
ctx := r.ctx
if font_is_type3(r.text.font) {
// A Type 3 glyph is drawn by running its content stream, so the
// device's text hooks cannot draw it. It still sees the run, which is
// what keeps extraction working over a Type 3 font.
dev_ignore_text(ctx.dev, &r.text, r.gs.ctm)
wants_outlines := ctx.dev == nil || .No_Glyph_Outlines not_in ctx.dev.hints
if mode != .Invisible && mode != .Clip && wants_outlines {
content_run_type3(r)
}
} else if mode == .Invisible || mode == .Clip {
// Modes 3 and 7 paint nothing at all, and are still handed over: an
// OCR layer under a scan is written in mode 3 and is the whole of
// what such a page has to extract.
dev_ignore_text(ctx.dev, &r.text, r.gs.ctm)
} else {
if text_render_mode_fills(mode) {
dev_fill_text(ctx.dev, &r.text, r.gs.ctm,
content_paint_of(r, &r.gs.fill, r.gs.fill_alpha))
}
if text_render_mode_strokes(mode) {
dev_stroke_text(ctx.dev, &r.text, &r.gs.stroke_state, r.gs.ctm,
content_paint_of(r, &r.gs.stroke, r.gs.stroke_alpha))
}
}
if text_render_mode_clips(mode) {
content_accumulate_clip(r)
}
}
@(private)
content_accumulate_clip :: proc(r: ^Content_Runner) {
if r.tclip.font == nil {
r.tclip.font = r.text.font
r.tclip.wmode = r.text.wmode
r.tclip.render_mode = r.text.render_mode
} else if r.tclip.font != r.text.font {
// A Text carries one font, so the vtable cannot express the union of
// glyph runs from two of them. Keeping the first run shows more than
// was asked for; taking the intersection instead would usually show
// nothing at all, which is the worse of the two failures.
add_repair(r.ctx.doc, .Content_Syntax, 0)
r.tclip_on = true
return
}
r.tclip_on = true
for it in r.text.items {
if len(r.tclip.items) >= MAX_TEXT_CLIP_ITEMS {
break
}
append(&r.tclip.items, it)
}
}
// ---------------------------------------------------------------------------
// Type 3 glyphs (9.6.4)
// ---------------------------------------------------------------------------
@(private)
content_run_type3 :: proc(r: ^Content_Runner) {
f := r.text.font
for it in r.text.items {
if r.ctx.fuel <= 0 {
return
}
content_run_type3_glyph(r, f, it.code, it.trm)
}
}
@(private)
content_run_type3_glyph :: proc(r: ^Content_Runner, f: ^Font, code: u32, trm: Matrix) {
ctx := r.ctx
if !matrix_is_finite(trm) {
return
}
stm := font_type3_proc(f, code)
if stm == nil {
return
}
key := uintptr(rawptr(stm))
if key in ctx.visited {
// A glyph description that shows its own character.
add_repair(ctx.doc, .Content_Syntax, 0)
return
}
data, err := stream_data(ctx.doc, stm)
if err != .None || len(data) == 0 {
return
}
gs := r.gs
gs.ctm = trm
// The glyph description starts with no text state of its own. Leaving the
// font in place would let a glyph that shows text re-enter its own font
// through a path the visited set cannot see.
gs.text = TEXT_STATE_DEFAULT
res := f.t3_resources
if res == nil {
res = r.res
}
ctx.visited[key] = true
// The glyph's own space is what a pattern named in it is anchored to.
content_exec(ctx, data, res, gs, trm, r.clip_box, r.flags + {.Type3_Glyph})
delete_key(&ctx.visited, key)
}
@(private)
content_type3_d1 :: proc(r: ^Content_Runner) {
if .Type3_Glyph not_in r.flags {
return
}
// d1 declares the glyph a shape: it is painted entirely in the colour
// current when the glyph was shown, and every colour operator inside the
// description is ignored (9.6.4).
r.flags += {.Ignore_Color}
bbox := rect_normalize(Rect{
content_num(r, 6, 2), content_num(r, 6, 3),
content_num(r, 6, 4), content_num(r, 6, 5),
})
// Four zeros mean "no bounding box declared", not "an empty glyph".
if rect_is_empty(bbox) {
return
}
path_clear(&r.rect)
path_rect(&r.rect, bbox.x0, bbox.y0, rect_width(bbox), rect_height(bbox))
dev_clip_path(r.ctx.dev, &r.rect, false, r.gs.ctm)
r.clip_depth += 1
r.clip_box = rect_intersect(r.clip_box, path_tight_bounds(&r.rect, r.gs.ctm))
}
// ---------------------------------------------------------------------------
// XObjects (8.10)
// ---------------------------------------------------------------------------
@(private)
content_do :: proc(r: ^Content_Runner, name: string) {
ctx := r.ctx
doc := ctx.doc
if r.n_hidden > 0 {
return
}
xd := dict_dict(doc, r.res, "XObject")
stm, ok := to_stream(dict_get(doc, xd, name))
if !ok {
add_repair(doc, .Bad_Resource, 0)
return
}
// An XObject may declare its own optional-content membership (8.10.1).
if !content_oc_visible(ctx, dict_get_raw(stm.dict, "OC")) {
return
}
switch dict_name(doc, stm.dict, "Subtype") {
case "Image":
content_do_image(r, stm)
case "Form":
content_do_form(r, stm)
case "PS":
// A PostScript XObject is never drawn by a PDF reader (8.10.4).
case:
// /Subtype is required and is nonetheless sometimes missing. The
// other keys say unambiguously which kind it is.
if dict_has(stm.dict, "BBox") {
content_do_form(r, stm)
} else if dict_has(stm.dict, "Width") || dict_has(stm.dict, "W") {
content_do_image(r, stm)
} else {
add_repair(doc, .Bad_Resource, 0)
}
}
}
@(private)
content_do_form :: proc(r: ^Content_Runner, stm: ^Stream) {
ctx := r.ctx
doc := ctx.doc
key := uintptr(rawptr(stm))
if key in ctx.visited {
// A form that invokes itself, directly or through another form.
add_repair(doc, .Content_Syntax, 0)
return
}
data, derr := stream_data(doc, stm)
if derr != .None || len(data) == 0 {
return
}
d := stm.dict
ctm := matrix_mul(dict_matrix(doc, d, "Matrix"), r.gs.ctm)
if !matrix_is_finite(ctm) {
add_repair(doc, .Content_Syntax, 0)
return
}
gs := r.gs
gs.ctm = ctm
res := dict_dict(doc, d, "Resources")
if res == nil {
// A form with no /Resources inherits the invoking stream's, which is
// not what the format says and is what producers rely on.
res = r.res
}
box := r.clip_box
clips := 0
bbox, has_bbox := object_to_rect(doc, dict_get(doc, d, "BBox"))
// A degenerate /BBox is still a clip (8.10.2), and clipping to it is what
// makes the form draw nothing. Skipping the push instead let the form paint
// its whole content over the page -- the opposite of what the box asks for.
// `object_to_rect` already normalises reversed corners, so the only boxes
// that reach here empty are genuinely zero-area ones.
if has_bbox {
path_clear(&r.rect)
path_rect(&r.rect, bbox.x0, bbox.y0, rect_width(bbox), rect_height(bbox))
dev_clip_path(ctx.dev, &r.rect, false, ctm)
r.clip_depth += 1
clips += 1
box = rect_intersect(box, path_tight_bounds(&r.rect, ctm))
}
grp := dict_dict(doc, d, "Group")
group := grp != nil && dict_name(doc, grp, "S") == "Transparency"
if group {
area := has_bbox ? transform_rect(ctm, bbox) : box
// All three compositing parameters go to the device, the soft mask
// included: it is the group's RESULT they apply to, and `begin_group`
// is the only channel left once they are cleared below.
dev_begin_group(ctx.dev, area, dict_bool(doc, grp, "I"), dict_bool(doc, grp, "K"),
gs.blend, gs.fill_alpha, gs.soft_mask)
// 11.6.6: the contents composite into the group with the parameters
// reset. It is the group's RESULT that the outer alpha, blend mode and
// soft mask apply to, and applying them twice is the classic way to
// render a transparency group too faint.
gs.fill_alpha = 1
gs.stroke_alpha = 1
gs.blend = .Normal
gs.soft_mask = nil
}
ctx.visited[key] = true
// A form re-anchors pattern space to its own default space (8.7.3.1).
content_exec(ctx, data, res, gs, ctm, box, r.flags - {.Type3_Glyph})
delete_key(&ctx.visited, key)
if group {
dev_end_group(ctx.dev)
}
for _ in 0 ..< clips {
dev_pop_clip(ctx.dev)
r.clip_depth -= 1
}
}
@(private)
content_do_image :: proc(r: ^Content_Runner, stm: ^Stream) {
ctx := r.ctx
if !dev_wants_images(ctx.dev) {
return
}
img, err := image_load(ctx.doc, stm, r.res)
if err != .None || img == nil {
add_repair(ctx.doc, .Bad_Resource, 0)
return
}
content_draw_image(r, img)
}
// content_draw_image paints one image over the unit square of the CTM, which
// is where the format puts every image (8.9.5.2): the `cm` before the `Do` is
// what gives it a size and a place.
@(private)
content_draw_image :: proc(r: ^Content_Runner, img: ^Image) {
src := &r.gs.fill
if !img.is_mask {
dev_fill_image(r.ctx.dev, img, r.gs.ctm, content_paint_of(r, src, r.gs.fill_alpha))
return
}
// An /ImageMask is a stencil painted in the current fill colour, so a
// pattern fill means clipping to the stencil and painting the pattern
// through it -- which is what clip_image_mask is for.
if src.pattern != nil {
dev_clip_image_mask(r.ctx.dev, img, r.gs.ctm)
r.clip_depth += 1
unit := Rect{0, 0, 1, 1}
area := rect_intersect(r.clip_box, transform_rect(r.gs.ctm, unit))
content_pattern_over(r, src, area, r.gs.fill_alpha)
dev_pop_clip(r.ctx.dev)
r.clip_depth -= 1
return
}
if src.cs != nil && src.cs.kind == .Pattern {
return
}
paint := content_paint_of(r, src, r.gs.fill_alpha)
paint.is_mask = true
dev_fill_image_mask(r.ctx.dev, img, r.gs.ctm, paint)
}
// ---------------------------------------------------------------------------
// Shadings (8.7.4.2)
// ---------------------------------------------------------------------------
@(private)
content_shading :: proc(r: ^Content_Runner, name: string) {
ctx := r.ctx
if r.n_hidden > 0 || !dev_wants_shades(ctx.dev) {
return
}
all := dict_dict(ctx.doc, r.res, "Shading")
entry := dict_get_raw(all, name)
if entry == nil {
add_repair(ctx.doc, .Bad_Resource, 0)
return
}
sh, err := shading_load(ctx.doc, entry, r.res)
if err != .None || sh == nil {
add_repair(ctx.doc, .Bad_Resource, 0)
return
}
// `sh` fills the current clip with the shading's own extent. Unlike a
// shading pattern it has no /Matrix and no /Background of its own, and it
// takes nothing from the current colour.
paint := content_paint_of(r, &r.gs.fill, r.gs.fill_alpha)
paint.pattern = nil
dev_fill_shade(ctx.dev, sh, r.gs.ctm, paint)
}
// ---------------------------------------------------------------------------
// ExtGState (8.4.5, Table 57)
// ---------------------------------------------------------------------------
@(private)
content_ext_gstate :: proc(r: ^Content_Runner, name: string) {
ctx := r.ctx
doc := ctx.doc
all := dict_dict(doc, r.res, "ExtGState")
d := dict_dict(doc, all, name)
if d == nil {
add_repair(doc, .Bad_Resource, 0)
return
}
if v, ok := to_number(dict_get(doc, d, "LW")); ok && is_finite(v) {
r.gs.stroke_state.line_width = max(v, 0)
}
if v, ok := to_int(dict_get(doc, d, "LC")); ok {
r.gs.stroke_state.line_cap = Line_Cap(u8(clamp(v, 0, 2)))
}
if v, ok := to_int(dict_get(doc, d, "LJ")); ok {
r.gs.stroke_state.line_join = Line_Join(u8(clamp(v, 0, 2)))
}
if v, ok := to_number(dict_get(doc, d, "ML")); ok && is_finite(v) {
r.gs.stroke_state.miter_limit = max(v, 1)
}
if a, ok := to_array(dict_get(doc, d, "D")); ok && array_len(a) >= 2 {
content_set_dash(r, array_get(doc, a, 0), array_num(doc, a, 1, 0))
}
if v, ok := to_name(dict_get(doc, d, "RI")); ok {
r.gs.render_intent = v
}
if v, ok := to_number(dict_get(doc, d, "CA")); ok && is_finite(v) {
r.gs.stroke_alpha = clamp(v, 0, 1)
}
if v, ok := to_number(dict_get(doc, d, "ca")); ok && is_finite(v) {
r.gs.fill_alpha = clamp(v, 0, 1)
}
if v, ok := to_number(dict_get(doc, d, "FL")); ok && is_finite(v) {
r.gs.flatness = clamp(v, 0, 100)
}
if v, ok := to_number(dict_get(doc, d, "SM")); ok && is_finite(v) {
r.gs.smoothness = clamp(v, 0, 1)
}
if v, ok := to_bool(dict_get(doc, d, "SA")); ok {
r.gs.stroke_adjust = v
}
if v, ok := to_bool(dict_get(doc, d, "AIS")); ok {
r.gs.alpha_is_shape = v
}
if v, ok := to_bool(dict_get(doc, d, "TK")); ok {
r.gs.text.knockout = v
}
if v, ok := to_bool(dict_get(doc, d, "UCR")); ok {
_ = v // undercolour removal is a printing control, not a rendering one
}
// /OP sets both overprints unless /op overrides the fill one (Table 57).
if v, ok := to_bool(dict_get(doc, d, "OP")); ok {
r.gs.overprint_stroke = v
r.gs.overprint_fill = v
}
if v, ok := to_bool(dict_get(doc, d, "op")); ok {
r.gs.overprint_fill = v
}
if v, ok := to_int(dict_get(doc, d, "OPM")); ok {
r.gs.overprint_mode = int(clamp(v, 0, 1))
}
// 11.3.5.2: /BM may be an array of names, oldest first; the first one a
// reader recognises wins, and an unrecognised name is Normal.
#partial switch v in dict_get(doc, d, "BM") {
case Name:
r.gs.blend = blend_mode_from_name(string(v))
case ^Array:
for i in 0 ..< min(array_len(v), 8) {
if nm := array_name(doc, v, i); nm != "" {
r.gs.blend = blend_mode_from_name(nm)
break
}
}
}
if a, ok := to_array(dict_get(doc, d, "Font")); ok && array_len(a) >= 2 {
if fd, has := to_dict(array_get(doc, a, 0)); has {
if f, ferr := font_load(doc, fd); ferr == .None {
r.gs.text.font = f
}
}
if sz, sok := to_number(array_get(doc, a, 1)); sok && is_finite(sz) {
r.gs.text.size = sz
}
}
if sm := dict_get(doc, d, "SMask"); sm != nil {
if nm, is_name := to_name(sm); is_name {
if nm == "None" {
r.gs.soft_mask = nil
}
} else if smd, is_dict := to_dict(sm); is_dict {
content_install_soft_mask(r, smd)
}
}
}
// ---------------------------------------------------------------------------
// Soft masks (11.6.5)
// ---------------------------------------------------------------------------
@(private)
content_install_soft_mask :: proc(r: ^Content_Runner, smd: ^Dict) {
ctx := r.ctx
doc := ctx.doc
sh := ctx.shared
g := dict_stream(doc, smd, "G")
if g == nil {
add_repair(doc, .Bad_Resource, 0)
r.gs.soft_mask = nil
return
}
// The same ExtGState applied again under the same CTM is the same mask.
// Rebuilding it would cost an allocation, a slot of the page's budget and
// -- because the device would not recognise it -- a whole re-render.
if sh.last_mask != nil && sh.last_smd == smd && sh.last_ctm == r.gs.ctm {
r.gs.soft_mask = sh.last_mask
return
}
if sh.n_soft_masks >= MAX_SOFT_MASKS {
// A Soft_Mask outlives the operator that installed it -- the graphics
// state stack can bring it back after a `Q` -- so a stream that
// installs one per operator has to stop somewhere.
add_repair(doc, .Content_Syntax, 0)
return
}
sh.n_soft_masks += 1
// Interpreter scratch, not the Document arena: a Soft_Mask has to outlive
// the OPERATOR, not the page. `device_list` copies it by value for exactly
// that reason, and putting it on the Document made every render of every
// page add to a pool that only `close` reclaims.
m := new(Soft_Mask, ctx.alloc)
m.form = g
m.luminosity = dict_name(doc, smd, "S") == "Luminosity"
// The mask is positioned by the CTM in force when it was installed, not
// by the one in force when something is painted through it.
m.ctm = r.gs.ctm
if tr := dict_get_raw(smd, "TR"); tr != nil && !name_is(resolve(doc, tr), "Identity") {
if fn, ferr := function_load(doc, tr); ferr == .None {
m.transfer = fn
}
}
// The backdrop colour is in the group's own colour space (11.6.5.2), and
// matters only for a luminosity mask; an alpha mask has no backdrop.
grp := dict_dict(doc, g.dict, "Group")
gcs, _ := colorspace_load(doc, dict_get_raw(grp, "CS"), r.res)
if bc := dict_array(doc, smd, "BC"); bc != nil && gcs != nil {
n := min(array_len(bc), gcs.n, MAX_COLOR_COMPONENTS)
m.backdrop.n = n
for i in 0 ..< n {
m.backdrop.v[i] = array_num(doc, bc, i, 0)
}
}
r.gs.soft_mask = m
sh.last_smd = smd
sh.last_ctm = r.gs.ctm
sh.last_mask = m
// A device that already holds this mask's plane does not need it drawn
// again. Everything the render depends on is in the Soft_Mask itself, so
// "equal to one I have" is the whole test the device has to make.
if dev_has_soft_mask(ctx.dev, m) {
return
}
content_render_soft_mask(r, m, gcs)
}
// content_render_soft_mask draws the mask's group between begin_mask and
// end_mask, where what is painted becomes the mask rather than page content.
@(private)
content_render_soft_mask :: proc(r: ^Content_Runner, m: ^Soft_Mask, gcs: ^Colorspace) {
ctx := r.ctx
doc := ctx.doc
stm := m.form
key := uintptr(rawptr(stm))
if key in ctx.visited {
add_repair(doc, .Content_Syntax, 0)
return
}
data, derr := stream_data(doc, stm)
if derr != .None || len(data) == 0 {
return
}
d := stm.dict
ctm := matrix_mul(dict_matrix(doc, d, "Matrix"), m.ctm)
if !matrix_is_finite(ctm) {
return
}
bbox, has_bbox := object_to_rect(doc, dict_get(doc, d, "BBox"))
area := has_bbox ? transform_rect(ctm, bbox) : r.clip_box
backdrop: Paint
backdrop.cs = gcs
backdrop.color = m.backdrop
backdrop.alpha = 1
if gcs != nil && m.backdrop.n > 0 {
backdrop.rgb = colorspace_to_rgb(gcs, m.backdrop)
}
dev_begin_mask(ctx.dev, area, m.luminosity, backdrop)
// The group is rendered from a clean state: nothing outside it, including
// the alpha and blend mode that will be applied to whatever the mask ends
// up modulating, may affect the mask itself.
gs := gstate_default(ctm)
gs.fill.cs = colorspace_device_gray(doc)
gs.fill.color = colorspace_default_color(gs.fill.cs)
gs.stroke.cs = gs.fill.cs
gs.stroke.color = gs.fill.color
box := area
clips := 0
if has_bbox {
path_clear(&r.rect)
path_rect(&r.rect, bbox.x0, bbox.y0, rect_width(bbox), rect_height(bbox))
dev_clip_path(ctx.dev, &r.rect, false, ctm)
r.clip_depth += 1
clips = 1
box = rect_intersect(box, path_tight_bounds(&r.rect, ctm))
}
res := dict_dict(doc, d, "Resources")
if res == nil {
res = r.res
}
ctx.visited[key] = true
content_exec(ctx, data, res, gs, ctm, box, {})
delete_key(&ctx.visited, key)
for _ in 0 ..< clips {
dev_pop_clip(ctx.dev)
r.clip_depth -= 1
}
dev_end_mask(ctx.dev, m, m.transfer)
}
// ---------------------------------------------------------------------------
// Inline images (8.9.7)
//
// BI collects a dictionary with abbreviated keys, ID is followed by exactly
// one whitespace byte and then by binary data, and EI ends it. The data is not
// lexed and cannot be: it is arbitrary bytes, and "EI" occurs inside real
// image data often enough to matter. Getting the end wrong does not lose one
// image, it desynchronises every operator on the rest of the page, so the
// length is computed whenever it can be and scanned for only as a last resort.
// ---------------------------------------------------------------------------
@(private)
content_inline_image :: proc(r: ^Content_Runner, lex: ^Lexer) {
ctx := r.ctx
doc := ctx.doc
sh := ctx.shared
// Every BI is counted, whether or not anything is drawn from it.
over := sh.n_inline >= MAX_INLINE_IMAGES
if over {
if sh.n_inline == MAX_INLINE_IMAGES {
// Counted one past the cap so that a stream which is nothing but
// inline images records this once rather than filling the repair
// list with the same entry.
sh.n_inline += 1
add_repair(doc, .Content_Syntax, i64(lex.pos))
}
} else {
sh.n_inline += 1
}
// The dictionary is parsed into the page's one reusable map, which is
// scratch and is cleared for the next BI; only an image that is actually
// built gets a dictionary of its own, at the end. Every bail-out below --
// no ID, no data, oversize data, a device that does not want images --
// used to leave one on the Document instead, reclaimable only by `close`,
// and `BI ID EI` is two tokens.
//
// The VALUES go to the Document while an image may still come of them,
// because the Image outlives this run: a display list hands one to a
// caller long after the page's scratch is gone. Past the cap they go to
// scratch too, since nothing will be built.
d := content_inline_dict(ctx)
alloc := over ? ctx.alloc : doc.allocator
got_id := false
collect: for _ in 0 ..< MAX_INLINE_KEYS {
if ctx.fuel <= 0 {
return
}
ctx.fuel -= 1
key_tok := lexer_next(lex, alloc)
#partial switch key_tok.kind {
case .EOF:
return
case .Keyword:
switch key_tok.text {
case "ID":
got_id = true
break collect
case "EI", "BI":
// The dictionary never closed. Nothing can be drawn, and the
// stream resynchronises here rather than eating the page.
add_repair(doc, .Content_Syntax, i64(key_tok.start))
return
}
continue collect
case .Name:
case:
// Junk between entries. The token was consumed, so this advances.
continue collect
}
val_tok := lexer_next(lex, alloc)
#partial switch val_tok.kind {
case .Integer: dict_set(d, key_tok.text, val_tok.int_value)
case .Real: dict_set(d, key_tok.text, val_tok.real_value)
case .Name: dict_set(d, key_tok.text, Name(val_tok.text))
case .String: dict_set(d, key_tok.text, String(val_tok.bytes))
case .Keyword:
switch val_tok.text {
case "true": dict_set(d, key_tok.text, true)
case "false": dict_set(d, key_tok.text, false)
case "null":
case "ID":
// A key with no value, immediately before the data.
got_id = true
break collect
}
case .Array_Open, .Dict_Open:
// Composite values -- /Filter, /Decode, /DecodeParms -- go through
// the object parser. Only these do: the parser reads ahead to tell
// "1 0 R" from "1 0", and its lookahead does not survive being
// spliced back into this lexer, whereas a bracketed object always
// ends where the parser leaves off.
p := parser_make(doc, lex.data, alloc)
p.lex.pos = val_tok.start
p.depth = 1
val, perr := parse_object(&p)
lex.pos = max(p.lex.pos, val_tok.end)
if perr == .None && val != nil {
dict_set(d, key_tok.text, val)
}
case .EOF:
return
}
}
if !got_id {
add_repair(doc, .Content_Syntax, i64(lex.pos))
return
}
// A refused image still needs its exact length, or the operators after it
// are read out of its pixels (8.9.7) -- which is why the dictionary is
// read at all past the cap. The one entry that cannot be read from scratch
// is a colour space written as an inline ARRAY: `colorspace_load` caches
// by the address of the object it is handed, and an address inside a
// per-page arena is handed straight back out to the next page. A NAME has
// no address to key on and is what an inline image writes; anything else
// is dropped here, which costs this refused image the exact byte count and
// nothing else.
if over {
for key in ([2]string{"ColorSpace", "CS"}) {
if v := dict_get_raw(d, key); v != nil {
if _, is_name := v.(Name); !is_name {
delete_key(d, key)
}
}
}
}
// Exactly one whitespace byte separates ID from the data (8.9.7).
if lex.pos < len(lex.data) && is_pdf_whitespace(lex.data[lex.pos]) {
lex.pos += 1
}
start := lex.pos
end, after := content_inline_end(doc, d, r.res, lex.data, start)
lex.pos = clamp(after, start, len(lex.data))
if end <= start {
return
}
if end - start > MAX_INLINE_IMAGE_BYTES {
add_repair(doc, .Content_Syntax, i64(start))
return
}
if over || r.n_hidden > 0 || !dev_wants_images(ctx.dev) {
return
}
img, err := image_load_inline(doc, content_inline_own(doc, d), lex.data[start:end], r.res)
if err != .None || img == nil {
add_repair(doc, .Bad_Resource, i64(start))
return
}
content_draw_image(r, img)
}
// content_inline_dict is the map the next BI parses into.
@(private)
content_inline_dict :: proc(ctx: ^Content_Ctx) -> ^Dict {
sh := ctx.shared
if sh.inline_dict == nil {
sh.inline_dict = new_dict(ctx.alloc, 8)
} else {
clear(sh.inline_dict)
}
return sh.inline_dict
}
// content_inline_own copies the parsed dictionary into Document memory for an
// Image that outlives the run -- a display list hands one to a caller long
// after the page's scratch is gone.
//
// The copy is DEEP, and it has to be: the lexer does not copy a name or a
// dictionary key that needs no unescaping, it slices the bytes it read them
// from -- and those bytes are the page's content, which is scratch that
// `page_scratch_end` unmaps. Everything else in a PDF is lexed out of the
// file, which lives as long as the Document; an inline image is the one place
// where it is not, and reading `/Filter` back out of such a dictionary --
// which `stream_data` does the first time the image is decoded, and for a
// display list that is after the caller has it -- read freed address space.
@(private)
content_inline_own :: proc(doc: ^Document, d: ^Dict) -> ^Dict {
own := new_dict(doc.allocator, max(len(d^), 1))
for k, v in d^ {
own[content_own_bytes(doc, transmute([]byte)k)] = content_own_value(doc, v, 0)
}
return own
}
// content_own_bytes copies a run of bytes into the Document, as a string.
@(private)
content_own_bytes :: proc(doc: ^Document, b: []byte) -> string {
if len(b) == 0 { return "" }
out, err := make([]byte, len(b), doc.allocator)
if err != nil { return "" }
copy(out, b)
return string(out)
}
// content_own_value is the deep copy `content_inline_own` needs. Numbers and
// booleans carry no memory; a reference points at the Document already. Only
// the four that can point into the content buffer are rebuilt, and the two
// that nest are depth-capped like every other walk over file-derived data.
@(private)
content_own_value :: proc(doc: ^Document, o: Object, depth: int) -> Object {
if depth >= MAX_DEPTH { return nil }
#partial switch v in o {
case Name:
return Name(content_own_bytes(doc, transmute([]byte)string(v)))
case String:
return String(transmute([]byte)content_own_bytes(doc, ([]byte)(v)))
case ^Array:
a := new_array(doc.allocator, array_len(v))
for i in 0 ..< array_len(v) {
append(&a.items, content_own_value(doc, array_get_raw(v, i), depth + 1))
}
return a
case ^Dict:
nd := new_dict(doc.allocator, max(len(v^), 1))
for k, e in v^ {
nd[content_own_bytes(doc, transmute([]byte)k)] = content_own_value(doc, e, depth + 1)
}
return nd
case ^Stream:
// An inline image's dictionary is written between BI and ID; there is
// no syntax in it for a stream.
return nil
}
return o
}
// content_inline_end finds the end of the data and the position just past EI.
@(private)
content_inline_end :: proc(doc: ^Document, d: ^Dict, res: ^Dict, data: []byte, start: int) -> (end, after: int) {
// PDF 2.0 added /L (/Length): a producer that states the length has
// removed all the guesswork, so it is believed first -- but only if EI is
// where it says.
if n, ok := to_int(dict_get_alt(doc, d, "Length", "L")); ok && n >= 0 &&
n <= i64(len(data) - start) {
e := start + int(n)
probe := Lexer{data = data, pos = e}
if lexer_accept_keyword(&probe, "EI") {
return e, probe.pos
}
}
// Unfiltered data has exactly one possible length, and computing it is
// the only way to be right about data that contains the bytes "EI".
if !content_inline_filtered(doc, d) {
if n, ok := content_inline_raw_size(doc, d, res); ok && n <= len(data) - start {
e := start + n
probe := Lexer{data = data, pos = e}
if lexer_accept_keyword(&probe, "EI") {
return e, probe.pos
}
}
}
// Last resort: the first EI that is bounded by whitespace on the left and
// by whitespace, a delimiter or the end of the stream on the right.
for i := start; i + 1 < len(data); i += 1 {
if data[i] != 'E' || data[i + 1] != 'I' {
continue
}
if i > start && !is_pdf_whitespace(data[i - 1]) {
continue
}
if i + 2 < len(data) && !is_pdf_whitespace(data[i + 2]) && !is_pdf_delimiter(data[i + 2]) {
continue
}
e := i
// The whitespace before EI is a separator, not a sample.
if e > start && is_pdf_whitespace(data[e - 1]) {
e -= 1
}
return e, i + 2
}
// No EI at all: the stream is truncated inside the image.
add_repair(doc, .Content_Syntax, i64(start))
return len(data), len(data)
}
@(private)
content_inline_filtered :: proc(doc: ^Document, d: ^Dict) -> bool {
f := dict_get_alt(doc, d, "Filter", "F")
#partial switch v in f {
case Name: return true
case ^Array: return array_len(v) > 0
}
return false
}
// content_inline_raw_size is the exact byte count of unfiltered sample data.
@(private)
content_inline_raw_size :: proc(doc: ^Document, d: ^Dict, res: ^Dict) -> (int, bool) {
w := dict_int(doc, d, "Width", dict_int(doc, d, "W", 0))
h := dict_int(doc, d, "Height", dict_int(doc, d, "H", 0))
if w <= 0 || h <= 0 || w > MAX_IMAGE_DIM || h > MAX_IMAGE_DIM {
return 0, false
}
ncomp := 1
bpc := i64(8)
if dict_bool(doc, d, "ImageMask", dict_bool(doc, d, "IM", false)) {
bpc = 1
} else {
bpc = dict_int(doc, d, "BitsPerComponent", dict_int(doc, d, "BPC", 8))
cs, err := colorspace_load(doc, dict_get_alt(doc, d, "ColorSpace", "CS"), res)
if err != .None || cs == nil {
return 0, false
}
ncomp = cs.n
}
switch bpc {
case 1, 2, 4, 8, 16:
case:
return 0, false
}
if ncomp < 1 || ncomp > MAX_COLOR_COMPONENTS {
return 0, false
}
// Rows are padded to a byte boundary (8.9.5.1). The arithmetic is in i64
// because w, h and ncomp all came out of a file.
bits := i64(w) * i64(ncomp) * bpc
row := (bits + 7) / 8
total := row * i64(h)
if total <= 0 || total > i64(MAX_INLINE_IMAGE_BYTES) {
return 0, false
}
return int(total), true
}
// ---------------------------------------------------------------------------
// Marked content and optional content (14.6, 8.11)
// ---------------------------------------------------------------------------
@(private)
content_mc_begin :: proc(r: ^Content_Runner, tag: string, props: Object) {
ctx := r.ctx
e: Mc_Entry
if tag == "OC" {
oc := props
if nm, is_name := to_name(props); is_name {
// A name is looked up in the /Properties sub-dictionary of the
// resources, which is where BDC's operand lives (14.6.2).
oc = dict_get_raw(dict_dict(ctx.doc, r.res, "Properties"), nm)
}
if !content_oc_visible(ctx, oc) {
e.hides = true
} else if r.n_hidden == 0 {
dev_begin_layer(ctx.dev, content_layer_name(ctx, oc, tag))
e.layer = true
}
}
if len(r.mc) >= MAX_MARKED_DEPTH {
// Too deep to record. The section is counted so that its EMC still
// balances; what it would have hidden is drawn instead, which shows
// too much rather than losing the rest of the page.
if e.layer {
dev_end_layer(ctx.dev)
}
r.mc_over += 1
return
}
if e.hides {
r.n_hidden += 1
}
append(&r.mc, e)
}
@(private)
content_mc_end :: proc(r: ^Content_Runner) {
if r.mc_over > 0 {
r.mc_over -= 1
return
}
if len(r.mc) == 0 {
add_repair(r.ctx.doc, .Content_Syntax, 0)
return
}
e := pop(&r.mc)
if e.hides && r.n_hidden > 0 {
r.n_hidden -= 1
}
if e.layer {
dev_end_layer(r.ctx.dev)
}
}
@(private)
content_layer_name :: proc(ctx: ^Content_Ctx, oc: Object, fallback: string) -> string {
d, ok := to_dict(resolve(ctx.doc, oc))
if !ok {
return fallback
}
if b := dict_bytes(ctx.doc, d, "Name"); len(b) > 0 {
return decode_text_string(b, ctx.alloc)
}
return fallback
}
// content_load_oc reads the catalog's default optional-content configuration
// into a set of groups that are switched off. It is read once per run: a page
// that names the same layer in four hundred BDCs must not walk the arrays
// four hundred times.
@(private)
content_load_oc :: proc(ctx: ^Content_Ctx) {
if ctx.opts.ignore_optional_content {
return
}
doc := ctx.doc
ocp := dict_dict(doc, doc.catalog, "OCProperties")
if ocp == nil {
return
}
cfg := dict_dict(doc, ocp, "D")
if cfg == nil {
return
}
add_all :: proc(ctx: ^Content_Ctx, a: ^Array, on: bool) {
for i in 0 ..< min(array_len(a), MAX_OC_GROUPS) {
d, ok := to_dict(array_get(ctx.doc, a, i))
if !ok {
continue
}
key := uintptr(rawptr(d))
if on {
delete_key(&ctx.oc_off, key)
} else {
ctx.oc_off[key] = true
}
}
}
// /BaseState OFF means everything in /OCGs starts hidden and /ON is the
// exception list (8.11.4.3). It is rare and cheap to honour.
if dict_name(doc, cfg, "BaseState") == "OFF" {
add_all(ctx, dict_array(doc, ocp, "OCGs"), false)
add_all(ctx, dict_array(doc, cfg, "ON"), true)
}
add_all(ctx, dict_array(doc, cfg, "OFF"), false)
}
// content_oc_visible answers whether content governed by `obj` -- an OCG, an
// OCMD, or nothing at all -- should be drawn.
@(private)
content_oc_visible :: proc(ctx: ^Content_Ctx, obj: Object) -> bool {
if ctx.opts.ignore_optional_content || obj == nil {
return true
}
d, ok := to_dict(resolve(ctx.doc, obj))
if !ok {
// A /OC that names nothing resolvable governs nothing.
return true
}
if dict_type(ctx.doc, d) != "OCMD" {
return !(uintptr(rawptr(d)) in ctx.oc_off)
}
// An OCMD combines several groups under a policy (8.11.2.3). Its /VE
// visibility expression is not evaluated; /P is, and is what producers
// actually write.
on, total := 0, 0
tally :: proc(ctx: ^Content_Ctx, o: Object, on, total: ^int) {
dd, is_dict := to_dict(resolve(ctx.doc, o))
if !is_dict {
return
}
total^ += 1
if !(uintptr(rawptr(dd)) in ctx.oc_off) {
on^ += 1
}
}
ocgs := dict_get(ctx.doc, d, "OCGs")
if a, is_array := to_array(ocgs); is_array {
for i in 0 ..< min(array_len(a), MAX_OC_GROUPS) {
tally(ctx, array_get_raw(a, i), &on, &total)
}
} else {
tally(ctx, ocgs, &on, &total)
}
if total == 0 {
return true
}
switch dict_name(ctx.doc, d, "P", "AnyOn") {
case "AllOn": return on == total
case "AnyOff": return on < total
case "AllOff": return on == 0
}
return on > 0 // AnyOn, the default
}