package pdf import "core:mem/virtual" // Annotations and their appearance streams (ISO 32000-2, 12.5). // // An annotation is a dictionary in a page's /Annots array: a link, a form // field, a stamp, a signature, a highlight, a sticky note. None of them say // what they look like. What is drawn is an APPEARANCE STREAM -- a form XObject // under /AP -- mapped onto the annotation's /Rect. /Annots is in 53% of the // files in the corpus, so a renderer that stops at the page's content stream // draws the page perfectly and loses every form field and every stamp on it. // // THIS LIBRARY NEVER SYNTHESISES AN APPEARANCE // // An annotation with no /AP draws nothing at all: no border for a /Square, no // leader line for a /Line, no laid-out text for a /FreeText or for a widget's // /DA string. That is a deliberate scope decision, not an oversight. // Generating appearances is the whole of 12.5.6's per-subtype geometry plus // 12.7.4.3's variable-text layout, which is what a form-*filling* library // owes; a reader owes what the file says the page looks like. Every producer // that expects its annotations to be seen writes an /AP, and the ones that do // not set /NeedAppearances, which announces that the consumer is expected to // be an editor. // // PLACEMENT IS THE WHOLE JOB // // `annot_matrix` is the algorithm of 12.5.5, step by named step. Getting it // wrong does not fail loudly -- it silently misplaces or wrongly scales every // stamp and form field on the page -- so each step is named in the code and // pinned by tests against hand-computed matrices. // // Once placed, the appearance is drawn by the ordinary content interpreter, // through two synthesised tokens: `/Ap Do`. Invoking a form XObject is exactly // what `Do` is for, and going through the interpreter rather than around it is // what gets the /BBox clip, a transparency /Group, the form's own /Matrix, the // /OC test and the self-reference guard right -- all of it already written and // tested in content.odin. // --------------------------------------------------------------------------- // Limits // --------------------------------------------------------------------------- // Entries read from one page's /Annots. A table of contents can carry a link // per line and a form can carry a field per cell, so this is generous; past it // the array is not a page's annotations. @(private) MAX_ANNOTS :: 4096 // --------------------------------------------------------------------------- // Flags // --------------------------------------------------------------------------- // Annot_Flag is one bit of /F (12.5.3, table 167). // // The format numbers its flag bits from 1; these values are the same bits // numbered from 0, which is what a bit_set indexes by. `.Hidden` is therefore // the format's "bit position 2". Annot_Flag :: enum u32 { Invisible = 0, Hidden = 1, Print = 2, No_Zoom = 3, No_Rotate = 4, No_View = 5, Read_Only = 6, Locked = 7, Toggle_No_View = 8, Locked_Contents = 9, } Annot_Flags :: bit_set[Annot_Flag; u32] // annot_flags_read turns /F into a set. Bits above the ten the format defines // are dropped rather than kept: they are reserved, and a set that holds // members no enumerator names prints as garbage. @(private) annot_flags_read :: proc(raw: i64) -> (f: Annot_Flags) { for bit in 0 ..< len(Annot_Flag) { if raw & (i64(1) << uint(bit)) != 0 { f += {Annot_Flag(bit)} } } return } // --------------------------------------------------------------------------- // Annot // --------------------------------------------------------------------------- // Annot is one annotation of a page with its appearance already chosen. // // It borrows from the Document: `dict`, `ap`, `subtype` and `state` all point // into the Document's arena or the file bytes and become invalid when `close` // is called. The struct is a value and holds no state of its own. Annot :: struct { // The annotation dictionary itself, for the many per-subtype entries this // library does not interpret (/Contents, /T, /Dest, /FT, /V ...). dict: ^Dict, // Object number, or {0,0} when /Annots held the dictionary directly. ref: Ref, // /Subtype verbatim: "Link", "Widget", "Stamp", "Popup", ... Empty when // the file omitted it, which is malformed and does happen. subtype: string, // /Rect, with its corners sorted: the format allows either order and // producers use both. rect: Rect, flags: Annot_Flags, // The appearance stream to draw, after /AS has chosen among the states of // an /AP /N sub-dictionary. nil when the annotation has no appearance, and // then nothing is drawn -- see the note at the top of this file. ap: ^Stream, // The appearance state /AS asked for, or the sole state of a /N // sub-dictionary that had only one. Empty when /N was a stream outright. // It is set even when the state named nothing and `ap` is therefore nil, // which is the ordinary way an unticked checkbox is written. state: string, // False when this annotation is not part of the page's visible content: // /F hides it, or it is a popup. It says nothing about /OC, which is // resolved when the appearance runs -- a text-extraction pass deliberately // draws layers a renderer would leave switched off, and that choice is // `Content_Options.ignore_optional_content`, made per run and not here. visible: bool, } // --------------------------------------------------------------------------- // Loading // --------------------------------------------------------------------------- // annot_load reads a page's /Annots. // // Every entry that resolves to a dictionary comes back, including the ones // that will not be drawn: a viewer needs a /Link's rectangle to make it // clickable and a /Widget's dictionary to fill it in, whether or not either // has an appearance. `visible` and `ap` say what `annot_run` will do with it. // // The slice is the caller's, allocated with `allocator`, and is released with // one `delete`; everything the Annots point at is Document memory and must not // be freed. An error comes back only for a nil argument -- a page with no // /Annots, or with an /Annots that is not an array, has no annotations, which // is not a failure. annot_load :: proc(doc: ^Document, page: ^Page, allocator := context.allocator) -> ([]Annot, Error) { if doc == nil || page == nil || page.dict == nil { return nil, .Invalid_Argument } arr := dict_array(doc, page.dict, "Annots") n := min(array_len(arr), MAX_ANNOTS) if n == 0 { return nil, .None } list := make([dynamic]Annot, 0, min(n, 64), allocator) defer delete(list) for i in 0 ..< n { raw := array_get_raw(arr, i) d, ok := to_dict(resolve(doc, raw)) if !ok { // A null entry, or a reference to an object that is gone, is how // a producer deletes an annotation without rewriting the array. continue } a := annot_read(doc, d) a.ref, _ = to_ref(raw) append(&list, a) } // Handed back at its exact length: a caller frees this with `delete`, and // freeing a slice that was sized as a dynamic array's capacity is a // mismatch for every allocator that checks. out := make([]Annot, len(list), allocator) copy(out, list[:]) return out, .None } @(private) annot_read :: proc(doc: ^Document, d: ^Dict) -> (a: Annot) { a.dict = d a.subtype = dict_name(doc, d, "Subtype") // object_to_rect sorts the corners and rejects a non-finite one, so // everything downstream can subtract without checking. a.rect = dict_rect(doc, d, "Rect") a.flags = annot_flags_read(dict_int(doc, d, "F", 0)) a.ap, a.state = annot_appearance(doc, d) // 12.5.3: bit 2 Hidden means do not render it anywhere. Bit 6 NoView means // do not render it on a screen -- and this library renders for viewing, so // it is honoured the same way; a consumer producing a print stream is the // one that would want the opposite and would have to say so. // // Bit 1 Invisible does NOT suppress an appearance stream. It applies only // to an annotation whose subtype the reader has no handler for, and it // says "then draw nothing rather than a placeholder"; an annotation with // an /AP is handled by definition (12.5.3, table 167). a.visible = !(.Hidden in a.flags) && !(.No_View in a.flags) // A popup is the window a markup annotation opens when its parent is // clicked. It is never part of the page's own appearance, even though it // sits in /Annots and carries a /Rect (12.5.6.14). if a.subtype == "Popup" { a.visible = false } // /NoZoom and /NoRotate ask for an appearance that ignores the viewer's // zoom and the page's /Rotate. Both are properties of an interactive // view, not of the page: honouring them needs the zoom factor teased back // out of the CTM, and a static render has no zoom to be independent of. return } // annot_appearance picks the stream to draw out of /AP. // // Only /N is page content. /R (rollover) and /D (down) are responses to a // pointer that is not there and never appear in a rendering of the page // (12.5.5). @(private) annot_appearance :: proc(doc: ^Document, d: ^Dict) -> (^Stream, string) { n := dict_get(doc, dict_dict(doc, d, "AP"), "N") if stm, ok := to_stream(n); ok { return stm, "" } // A sub-dictionary instead of a stream is keyed by appearance state: it is // how a checkbox carries both its /Off and its /Yes look in one place and // /AS says which of them it is showing. sub, is_dict := to_dict(n) if !is_dict { return nil, "" } if state := dict_name(doc, d, "AS"); state != "" { // A state that names nothing draws nothing. A checkbox whose /N holds // only /Yes and whose /AS is /Off is unchecked, and the absent entry // IS its appearance; falling back to the only stream present would // tick every empty box on the page. stm, ok := to_stream(dict_get(doc, sub, state)) return ok ? stm : nil, state } // /AS is required as soon as /N is a sub-dictionary and is nonetheless // left out. With exactly one state there is nothing to disambiguate and // every viewer draws it; with more there is no way to choose, and drawing // the wrong state is worse than drawing none. if len(sub^) == 1 { for k, v in sub^ { if stm, ok := to_stream(resolve(doc, v)); ok { return stm, k } } } if len(sub^) > 1 { add_repair(doc, .Bad_Resource, 0) } return nil, "" } // --------------------------------------------------------------------------- // Placement -- ISO 32000-2, 12.5.5, "Algorithm: appearance streams" // --------------------------------------------------------------------------- // annot_matrix maps an appearance stream onto its annotation's rectangle. // // The algorithm, exactly as the clause gives it: // // 1. Map the appearance's /BBox through its /Matrix -- transform its four // corners. // 2. Take the bounding box of those four transformed points. For a /Matrix // that rotates or skews this is bigger than the shape inside it, and // that is the whole reason the step exists. // 3. Compute A: the matrix that maps that box onto the annotation's /Rect. // 4. The appearance is drawn under /Matrix * A * the page's CTM. // // `form` is that final matrix -- the one the appearance's own coordinates are // drawn under, and what a caller asking where an annotation landed wants. // `invoke` is the same thing without the form's own /Matrix (A * page_ctm), // because the interpreter applies /Matrix itself when it runs the stream and // applying it here as well would apply it twice. // // `ok` is false when there is nothing to place: an annotation with no area, or // a transformed appearance box with no width or no height. The scale factor of // step 3 would be a division by zero, and a zero-area appearance has no // picture in it to stretch. @(private) annot_matrix :: proc(doc: ^Document, a: ^Annot, page_ctm: Matrix) -> (form, invoke: Matrix, ok: bool) { if doc == nil || a == nil || a.ap == nil { return MATRIX_IDENTITY, MATRIX_IDENTITY, false } if rect_is_empty(a.rect) { // A missing /Rect reads as the empty rectangle, and an annotation with // no area shows nothing however its appearance is scaled. return MATRIX_IDENTITY, MATRIX_IDENTITY, false } // A /Matrix that is short, non-numeric or infinite arrives as the identity: // object_to_matrix is the finiteness boundary for a matrix out of a file, // and the identity is both the format's default and the only safe guess. mtx := dict_matrix(doc, a.ap.dict, "Matrix") place := MATRIX_IDENTITY if bbox, has_bbox := object_to_rect(doc, dict_get(doc, a.ap.dict, "BBox")); has_bbox { // Steps 1 and 2. transform_rect maps the four corners and returns // their bounding box, which is precisely what the two steps ask for. tb := transform_rect(mtx, bbox) tw := rect_width(tb) th := rect_height(tb) if !(tw > 0) || !(th > 0) { return MATRIX_IDENTITY, MATRIX_IDENTITY, false } // Step 3. A maps tb onto /Rect: scale each axis by the ratio of the // sides, then translate tb's low corner onto /Rect's. sx := rect_width(a.rect) / tw sy := rect_height(a.rect) / th place = Matrix{sx, 0, 0, sy, a.rect.x0 - tb.x0 * sx, a.rect.y0 - tb.y0 * sy} } else { // /BBox is required of every form XObject and there is nothing to map // onto the /Rect without it. Drawing the appearance where its own // coordinates put it is what content.odin does with the same damage, // and it shows something that is at least in the right units. add_repair(doc, .Bad_Resource, 0) } // Step 4. invoke = matrix_mul(place, page_ctm) form = matrix_mul(mtx, invoke) if !matrix_is_finite(form) || !matrix_is_finite(invoke) { return MATRIX_IDENTITY, MATRIX_IDENTITY, false } return form, invoke, true } // --------------------------------------------------------------------------- // Drawing // --------------------------------------------------------------------------- // The synthesised content stream that invokes the appearance, and the names it // uses. The names are looked up only in the dictionary built below, so they // cannot collide with anything in the file. @(private = "file") ANNOT_INVOKE :: "/Ap Do" @(private = "file") ANNOT_INVOKE_OC :: "/OC /Oc BDC /Ap Do EMC" @(private = "file") ANNOT_AP_NAME :: "Ap" @(private = "file") ANNOT_OC_NAME :: "Oc" // annot_run draws one annotation's appearance stream. // // `page_ctm` is the same matrix the page's own content stream was run with: // annotations live in default user space, so the page flip, /Rotate and zoom // apply to them unchanged. // // An annotation with no appearance, or one /F or its subtype hides, draws // nothing and is not an error -- most pages are mostly those. `opts` is passed // through to the interpreter, which is how a caller shares one scratch arena // across a page and how a text-extraction pass asks for switched-off layers. annot_run :: proc(doc: ^Document, a: ^Annot, dev: ^Device, page_ctm: Matrix, opts := Content_Options{}) -> Error { if doc == nil || a == nil { return .Invalid_Argument } if !matrix_is_finite(page_ctm) { return .Invalid_Argument } if a.ap == nil || !a.visible { return .None } // `form` is the complete appearance matrix; the interpreter arrives at it // itself by multiplying the form's /Matrix onto the CTM it is invoked // with, which is why it is `invoke` that is handed over. _, invoke, ok := annot_matrix(doc, a, page_ctm) if !ok { return .None } o := opts arena: virtual.Arena if o.allocator.procedure == nil { // Same contract as run_content: the run frees nothing individually, so // its allocator has to be one that reclaims in bulk. if arena_err := virtual.arena_init_growing(&arena); arena_err != nil { return .Out_Of_Memory } o.allocator = virtual.arena_allocator(&arena) } defer if opts.allocator.procedure == nil { virtual.arena_destroy(&arena) } // The appearance is a form XObject, so it is placed the way every form is // placed: by invoking it with `Do` from a stream whose CTM is the // placement matrix. // // The dictionary this is looked up in holds nothing but the appearance // itself. An appearance stream that has no /Resources of its own therefore // resolves no names at all, and that is right twice over: an annotation is // not page content and inherits nothing from the page (12.5.5), and a // dictionary that dies with this call must not be reachable from anything // that outlives it -- a tiling pattern found through the invoking // resources keeps them on the Document (pattern.odin), which would leave a // dangling pointer behind every annotation. res := new_dict(o.allocator, 2) xobj := new_dict(o.allocator, 1) dict_set(xobj, ANNOT_AP_NAME, a.ap) dict_set(res, "XObject", xobj) // An annotation may carry its own optional-content membership (12.5.2). // Wrapping the invocation in a marked-content section hands it to the code // that already evaluates a page's layers, OCMD policies and all, instead // of growing a second copy of it here. src := ANNOT_INVOKE if oc := dict_get_raw(a.dict, "OC"); oc != nil { props := new_dict(o.allocator, 1) dict_set(props, ANNOT_OC_NAME, oc) dict_set(res, "Properties", props) src = ANNOT_INVOKE_OC } return run_content(doc, dev, transmute([]byte)src, res, invoke, o) } // annot_run_page draws every drawable annotation of a page, in /Annots order, // which is the order they paint over one another in. // // This is the whole of what a renderer has to call: `render_page` runs the // page's content stream and then this, with the same CTM. annot_run_page :: proc(doc: ^Document, page: ^Page, dev: ^Device, page_ctm: Matrix, opts := Content_Options{}) -> Error { if doc == nil || page == nil { return .Invalid_Argument } if !matrix_is_finite(page_ctm) { return .Invalid_Argument } // The annotation list is scratch. When the caller supplied an allocator it // is theirs -- a per-page arena reclaimed in one call -- and freeing out of // it here would be at best a no-op and at worst a mismatch. alloc := opts.allocator.procedure != nil ? opts.allocator : context.allocator annots, err := annot_load(doc, page, alloc) if err != .None { return err } defer if opts.allocator.procedure == nil { delete(annots, alloc) } n := 0 for &a in annots { if a.visible && a.ap != nil { n += 1 } } if n == 0 { return .None } // The page's annotations share the token budget the page itself gets. A // file with five hundred annotations all naming one enormous appearance // stream is otherwise five hundred times the work of the page it is on, // and it costs three lines to write. o := opts budget := opts.max_ops > 0 ? opts.max_ops : DEFAULT_CONTENT_FUEL o.max_ops = max(budget / n, 1) for &a in annots { // One annotation cannot fail the page. The only error `annot_run` can // return from here is a scratch arena it could not reserve, and the // next annotation may well get one. annot_run(doc, &a, dev, page_ctm, o) } return .None }