2642 lines
91 KiB
Odin
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
|
||
|
|
}
|