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 }