Skip to main content

retex_engine/
pdfext.rs

1//! `\pdfextension` (LuaTeX manual §3.2.2): PDF literals, matrices,
2//! colour stacks, objects, annotations, links, destinations, outlines and
3//! document-level dictionaries.
4//!
5//! Commands that act at shipout become whatsit nodes in the current list;
6//! document-level data is collected in [`PdfState`] for the backend.
7//!
8//! # Object numbers
9//!
10//! The engine and the PDF writer share one object counter
11//! ([`PdfState::counter`]), so the numbers TeX and Lua see
12//! (`\pdffeedback lastobj`, `pdf.getpageref`, …) are those in the file.
13//! Observed in LuaTeX 1.22:
14//!
15//! * `\pdfextension annot` and `startlink` take their number when they are
16//!   scanned; continuation rectangles of a link get theirs at shipout.
17//! * A named or numbered destination gets its number the first time it is
18//!   referred to (by an outline or open action when it is scanned, by a
19//!   link when the page is written) or met at shipout, whichever comes
20//!   first. Pages likewise (`goto page`, `\pdffeedback pageref`).
21//! * `\pdfextension outline` allocates the action (written at once), the
22//!   numbers of the destination it refers to, the entry, and the title
23//!   (written at once as `(title)` and a newline).
24//! * Destinations that were referred to but never defined are written at
25//!   the end as `[ last-page 0 R /Fit ]`, in the reverse order of their
26//!   allocation, each with a warning.
27
28use std::cell::Cell;
29use std::collections::HashMap;
30use std::rc::Rc;
31
32use crate::Engine;
33use crate::nodes::{LateData, NodeData, RUNNING, Whatsit, id, whatsit_sub};
34use crate::token::TokenList;
35
36/// Literal modes (`mode` of `pdf_literal` whatsits).
37#[allow(missing_docs)]
38pub mod literal_mode {
39    pub const ORIGIN: u8 = 0;
40    pub const PAGE: u8 = 1;
41    pub const DIRECT: u8 = 2;
42    pub const RAW: u8 = 3;
43}
44
45/// Colour-stack commands.
46#[allow(missing_docs)]
47pub mod colorstack_cmd {
48    pub const SET: u8 = 0;
49    pub const PUSH: u8 = 1;
50    pub const POP: u8 = 2;
51    pub const CURRENT: u8 = 3;
52}
53
54/// A PDF object created with `\pdfextension obj`.
55#[derive(Clone, Debug, Default)]
56pub struct PdfObject {
57    /// The PDF object number.
58    pub num: i32,
59    /// The object content (dictionary text, or stream data).
60    pub data: Vec<u8>,
61    /// Whether this is a stream object.
62    pub stream: bool,
63    /// Stream dictionary entries (`attr {…}`).
64    pub attr: Vec<u8>,
65    /// Whether `data` names a file whose content is the object.
66    pub file: bool,
67    /// Whether the object must not go into an object stream.
68    pub uncompressed: bool,
69    /// Whether the content has been given (not just reserved).
70    pub defined: bool,
71    /// Whether it has been written (immediately or at shipout).
72    pub written: bool,
73}
74
75/// An outline entry (`\pdfextension outline`).
76#[derive(Clone, Debug, Default)]
77pub struct Outline {
78    /// Extra dictionary entries.
79    pub attr: Vec<u8>,
80    /// The entry's object.
81    pub objnum: i32,
82    /// The title object.
83    pub title: i32,
84    /// The action object.
85    pub action: i32,
86    /// `count` (number of direct children; negative means closed).
87    pub count: i32,
88}
89
90/// Colour-stack definition.
91#[derive(Clone, Debug)]
92pub struct ColorStack {
93    /// Whether the stack's current value is written at the start of every
94    /// page (`page` keyword).
95    pub page: bool,
96    /// The literal mode of its literals (observed: `direct` for stack 0,
97    /// whose literals go inside a text object without ending it;
98    /// `origin` for stacks declared without `direct`).
99    pub mode: u8,
100    /// The initial value.
101    pub init: Vec<u8>,
102}
103
104/// Kinds of PDF object numbers, as `pdf.getobjtype` names them.
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum ObjKind {
107    /// `\pdfextension obj`, `pdf.obj`, `pdf.reserveobj()`.
108    Obj,
109    /// `\pdfextension annot`, `pdf.reserveobj("annot")`.
110    Annot,
111    /// Links (`\pdfextension startlink`); LuaTeX reports `others`.
112    Link,
113    /// Page objects (`pdf.getpageref`, `\pdffeedback pageref`).
114    Page,
115    /// Font objects (`pdf.getfontobjnum`, `\pdffeedback fontobjnum`).
116    Font,
117    /// Image XObjects (`\saveimageresource`).
118    Ximage,
119    /// Box resources (`\saveboxresource`).
120    Xform,
121    /// Destinations, outline entries, actions, threads.
122    Other,
123}
124
125impl ObjKind {
126    /// The name returned by `pdf.getobjtype`.
127    pub fn name(self) -> &'static str {
128        match self {
129            ObjKind::Obj => "obj",
130            ObjKind::Annot => "annot",
131            ObjKind::Link | ObjKind::Other => "others",
132            ObjKind::Page => "page",
133            ObjKind::Font => "font",
134            ObjKind::Ximage => "ximage",
135            ObjKind::Xform => "xform",
136        }
137    }
138}
139
140/// Dictionary texts set from Lua (`pdf.setcatalog` and friends). They are
141/// independent of the token lists set at the TeX end and are written after
142/// them; `None` means never set (the getters return `nil`).
143#[derive(Clone, Debug, Default)]
144pub struct LuaPdfTexts {
145    /// `pdf.setcatalog`.
146    pub catalog: Option<Vec<u8>>,
147    /// `pdf.setinfo`.
148    pub info: Option<Vec<u8>>,
149    /// `pdf.setnames`.
150    pub names: Option<Vec<u8>>,
151    /// `pdf.settrailer` (observed: LuaTeX 1.22 does not write it).
152    pub trailer: Option<Vec<u8>>,
153    /// `pdf.settrailerid`.
154    pub trailer_id: Option<Vec<u8>>,
155    /// `pdf.setpageattributes`.
156    pub page_attributes: Option<Vec<u8>>,
157    /// `pdf.setpageresources`.
158    pub page_resources: Option<Vec<u8>>,
159    /// `pdf.setpagesattributes`.
160    pub pages_attributes: Option<Vec<u8>>,
161    /// `pdf.setxformattributes`.
162    pub xform_attributes: Option<Vec<u8>>,
163    /// `pdf.setxformresources`.
164    pub xform_resources: Option<Vec<u8>>,
165}
166
167/// State of the `\latelua` call being executed at shipout.
168#[derive(Clone, Debug, Default)]
169pub struct LateLuaContext {
170    /// Literals queued by `pdf.print`: (literal mode, text).
171    pub prints: Vec<(u8, Vec<u8>)>,
172    /// Annotations added with `pdf.registerannot`.
173    pub annots: Vec<i32>,
174}
175
176/// Identifies a destination (`name {…}` or `num n`).
177#[derive(Clone, Debug, PartialEq, Eq, Hash)]
178pub enum DestId {
179    /// `name {…}`.
180    Name(Vec<u8>),
181    /// `num n`.
182    Num(i32),
183}
184
185impl DestId {
186    /// How warnings name the identifier: `name 'x'` or `num '3'`.
187    fn describe(&self) -> String {
188        match self {
189            DestId::Name(n) => format!("name '{}'", String::from_utf8_lossy(n)),
190            DestId::Num(n) => format!("num '{n}'"),
191        }
192    }
193}
194
195/// The destination types of `\pdfextension dest`.
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub enum DestKind {
198    /// `xyz`.
199    Xyz,
200    /// `fit`.
201    Fit,
202    /// `fith`.
203    FitH,
204    /// `fitv`.
205    FitV,
206    /// `fitb`.
207    FitB,
208    /// `fitbh`.
209    FitBH,
210    /// `fitbv`.
211    FitBV,
212    /// `fitr`.
213    FitR,
214}
215
216/// The view of `\pdfextension dest`.
217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
218pub struct DestView {
219    /// The type.
220    pub kind: DestKind,
221    /// `zoom` for `xyz` (0 when not given).
222    pub zoom: i32,
223    /// `width`, `height`, `depth` for `fitr` ([`RUNNING`] when not given).
224    pub dims: [i32; 3],
225}
226
227/// What an action refers to.
228#[derive(Clone, Debug, PartialEq, Eq)]
229pub enum ActionTarget {
230    /// `name {…}`.
231    Name(Vec<u8>),
232    /// `num n`.
233    Num(i32),
234    /// `page n {view}`.
235    Page(i32, Vec<u8>),
236}
237
238/// An action specification (`user`, `goto`, `thread`).
239#[derive(Clone, Debug, PartialEq, Eq)]
240pub enum PdfAction {
241    /// `user {…}`.
242    User(Vec<u8>),
243    /// `goto [file {…}] [struct …] …`.
244    Goto {
245        /// `file {…}`.
246        file: Option<Vec<u8>>,
247        /// `struct name {…}` or `struct num n`: the structure destination
248        /// (`/SD`).
249        structure: Option<ActionTarget>,
250        /// The target.
251        target: ActionTarget,
252        /// `newwindow` (true) or `nonewwindow` (false).
253        new_window: Option<bool>,
254    },
255    /// `thread [file {…}] …`.
256    Thread {
257        /// `file {…}`.
258        file: Option<Vec<u8>>,
259        /// The target.
260        target: ActionTarget,
261    },
262}
263
264/// A destination's view in page coordinates (sp), for the backend.
265#[derive(Clone, Copy, Debug, PartialEq, Eq)]
266pub enum DestPos {
267    /// `/XYZ x y zoom` (zoom in thousandths, 0 for `null`).
268    Xyz {
269        /// Left.
270        x: i32,
271        /// Top.
272        y: i32,
273        /// Zoom times 1000.
274        zoom: i32,
275    },
276    /// `/Fit`.
277    Fit,
278    /// `/FitB`.
279    FitB,
280    /// `/FitH y`.
281    FitH(i32),
282    /// `/FitBH y`.
283    FitBH(i32),
284    /// `/FitV x`.
285    FitV(i32),
286    /// `/FitBV x`.
287    FitBV(i32),
288    /// `/FitR x1 y1 x2 y2`.
289    FitR([i32; 4]),
290}
291
292/// One rectangle of a link, as written.
293#[derive(Clone, Debug)]
294pub struct LinkPiece {
295    /// The annotation's object number.
296    pub objnum: i32,
297    /// The rectangle (sp, page coordinates).
298    pub rect: [i32; 4],
299    /// `attr {…}`.
300    pub attr: Vec<u8>,
301    /// The action text: a dictionary, or the user text.
302    pub action: Vec<u8>,
303    /// Whether `action` is user text.
304    pub user: bool,
305    /// The action, turned into `action` when the page is written (LuaTeX
306    /// allocates the destinations links refer to then).
307    pub source: PdfAction,
308}
309
310/// A link whose `\pdfextension endlink` has not been shipped yet.
311#[derive(Clone, Debug)]
312pub struct ActiveLink {
313    /// Box nesting level (`cur_s`) of the box that holds the startlink;
314    /// every later hlist at that level gets a rectangle of its own.
315    pub level: i32,
316    /// The rectangle being built on the current page, if any.
317    pub piece: Option<usize>,
318    /// Whether that rectangle is the first one, whose right edge is still
319    /// open.
320    pub first_open: bool,
321    /// `\pdfvariable linkmargin` at the start of the link.
322    pub margin: i32,
323    /// Whether the current rectangle continues the link from an earlier
324    /// box (it then ends at the endlink without margin).
325    pub continued: bool,
326    /// `attr {…}`.
327    pub attr: Vec<u8>,
328    /// The action.
329    pub action: PdfAction,
330}
331
332/// An annotation's rectangle (sp, page coordinates) and text.
333pub type AnnotBody = ([i32; 4], Vec<u8>);
334
335/// The interactive items of the page being shipped, handed to the
336/// backend when the page ends.
337#[derive(Clone, Debug, Default)]
338pub struct PageItems {
339    /// Annotations: object number and, unless written by other means
340    /// (`pdf.registerannot`), rectangle and text.
341    pub annots: Vec<(i32, Option<AnnotBody>)>,
342    /// Link rectangles, in the order they were started.
343    pub links: Vec<LinkPiece>,
344    /// Destinations: object number, whether named, view, and for a
345    /// structure destination the structure element's object.
346    pub dests: Vec<(i32, bool, DestPos, Option<i32>)>,
347    /// Objects to write with the page (`\pdfextension refobj`).
348    pub objects: Vec<PdfObject>,
349    /// Page attributes: the TeX and the Lua text.
350    pub attr: (Vec<u8>, Vec<u8>),
351    /// Page resources: the TeX and the Lua text.
352    pub resources: (Vec<u8>, Vec<u8>),
353}
354
355/// Document-level items computed at the end of the run for the backend.
356#[derive(Clone, Debug, Default)]
357pub struct DocItems {
358    /// Destinations referred to but never defined, in writing order.
359    pub missing_dests: Vec<i32>,
360    /// Threads referred to but never defined: object and title.
361    pub missing_threads: Vec<(i32, Vec<u8>)>,
362    /// Named destinations sorted by name.
363    pub dest_names: Vec<(Vec<u8>, i32)>,
364    /// `\pdfvariable pagesattr` at the end of the run.
365    pub pages_attr: Vec<u8>,
366    /// `\pdfvariable trailerid` at the end of the run.
367    pub trailer_id: Vec<u8>,
368}
369
370/// A destination known by its identifier.
371#[derive(Clone, Copy, Debug, Default)]
372pub struct DestEntry {
373    /// The object number.
374    pub objnum: i32,
375    /// Whether `\pdfextension dest` was shipped for it.
376    pub defined: bool,
377}
378
379/// Document-level PDF state.
380#[derive(Clone, Debug, Default)]
381pub struct PdfState {
382    /// Images written while shipping the current page, in order of first
383    /// use: their file names are printed at the end of the page.
384    pub page_images: Vec<u32>,
385    /// Objects by PDF object number.
386    pub objects: std::collections::BTreeMap<i32, PdfObject>,
387    /// `\pdfextension catalog` text.
388    pub catalog: Vec<u8>,
389    /// The catalog's open action (object number).
390    pub open_action: Option<i32>,
391    /// `\pdfextension info` text.
392    pub info: Vec<u8>,
393    /// `\pdfextension names` text.
394    pub names: Vec<u8>,
395    /// `\pdfextension trailer` text.
396    pub trailer: Vec<u8>,
397    /// Outline entries in order.
398    pub outlines: Vec<Outline>,
399    /// `\pdfextension glyphtounicode` pairs.
400    pub glyph_to_unicode: Vec<(Vec<u8>, Vec<u8>)>,
401    /// Colour stacks; stack 0 is the predefined colour stack.
402    pub colorstacks: Vec<ColorStack>,
403    /// `\pdffeedback lastobj`.
404    pub last_obj: i32,
405    /// `\pdffeedback lastannot`.
406    pub last_annot: i32,
407    /// `\pdffeedback lastlink`.
408    pub last_link: i32,
409    /// The highest object number allocated so far, shared with the PDF
410    /// writer.
411    pub counter: Rc<Cell<u32>>,
412    /// The kind of each allocated object number.
413    pub kinds: HashMap<i32, ObjKind>,
414    /// Page object numbers by page number (forward references included).
415    pub page_refs: HashMap<i32, i32>,
416    /// PDF fonts (resource name number, object number) by font, see
417    /// [`Engine::pdf_font`].
418    pub fonts: HashMap<u32, (u32, i32)>,
419    /// `pdf.setfontattributes` texts by font.
420    pub font_attrs: HashMap<u32, Vec<u8>>,
421    /// Map files and map lines (`pdf.mapfile`, `pdf.mapline`,
422    /// `\pdfextension mapfile`/`mapline`), in order.
423    pub map_entries: Vec<Vec<u8>>,
424    /// Texts set from Lua.
425    pub lua: LuaPdfTexts,
426    /// `\pdffeedback retval`.
427    pub retval: i32,
428    /// The position (PDF coordinates, sp) of the last `\latelua`, for
429    /// `pdf.getpos`.
430    pub pos: (i32, i32),
431    /// The `\latelua` being executed, if any.
432    pub late: Option<LateLuaContext>,
433    /// Annotations registered from Lua for the page being shipped.
434    pub page_annots: Vec<i32>,
435    /// Whether `pdf.obj` opened the output file (in DVI mode LuaTeX then
436    /// leaves an empty DVI file when no page is shipped out).
437    pub output_opened: bool,
438    /// Destinations by identifier.
439    pub dests: HashMap<DestId, DestEntry>,
440    /// Destination identifiers in the order their numbers were allocated.
441    pub dest_order: Vec<DestId>,
442    /// Structure destinations by identifier (`\pdfextension dest struct`).
443    pub struct_dests: HashMap<DestId, DestEntry>,
444    /// Threads referred to by actions: identifier and object number.
445    pub threads: Vec<(DestId, i32)>,
446    /// The interactive items of the page being shipped.
447    pub page: PageItems,
448    /// Objects to write as soon as the backend is available again (they
449    /// were written during shipout, while the backend is busy).
450    pub pending: Vec<PdfObject>,
451    /// Document-level items for the backend, computed at the end.
452    pub doc: DocItems,
453    /// A fatal backend error occurred: no PDF is written.
454    pub fatal: bool,
455    /// Objects written into object streams and the number of streams.
456    pub objstm_stats: (u32, u32),
457    /// Whether a stream (form, image or stream object) was written
458    /// immediately: with no pages, LuaTeX then fails at the end
459    /// ("already written content discarded").
460    pub stream_written: bool,
461    /// Words of extra memory LuaTeX reports for objects (see
462    /// [`Engine::pdf_define_object`]).
463    pub obj_words: usize,
464    /// Whether the font map note (`{…/pdftex.map}`) was printed.
465    pub map_note_done: bool,
466    /// Links started but not ended yet, innermost last.
467    pub active_links: Vec<ActiveLink>,
468}
469
470impl PdfState {
471    /// The highest object number allocated so far.
472    pub fn max_obj(&self) -> i32 {
473        self.counter.get() as i32
474    }
475
476    /// The number of named destinations (for the PDF statistics).
477    pub fn named_dests(&self) -> usize {
478        self.dests
479            .keys()
480            .filter(|k| matches!(k, DestId::Name(_)))
481            .count()
482    }
483}
484
485impl Engine {
486    /// Scans a balanced text (expanded) and returns its characters.
487    fn scan_pdf_string(&mut self) -> Vec<u8> {
488        let toks = self.scan_toks_expanded();
489        self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX))
490    }
491
492    /// Allocates a PDF object number of kind `kind`.
493    pub fn pdf_alloc_obj(&mut self, kind: ObjKind) -> i32 {
494        let n = self.pdf.counter.get() + 1;
495        self.pdf.counter.set(n);
496        let n = n as i32;
497        self.pdf.kinds.insert(n, kind);
498        n
499    }
500
501    /// The kind of object number `n`, if it was allocated.
502    pub fn pdf_obj_kind(&self, n: i32) -> Option<ObjKind> {
503        self.pdf.kinds.get(&n).copied()
504    }
505
506    /// The object number of page `page` (counting from 1), allocated on
507    /// first request (`pdf.getpageref`, `\pdffeedback pageref`).
508    pub fn pdf_page_ref(&mut self, page: i32) -> i32 {
509        if let Some(&n) = self.pdf.page_refs.get(&page) {
510            return n;
511        }
512        let n = self.pdf_alloc_obj(ObjKind::Page);
513        self.pdf.page_refs.insert(page, n);
514        n
515    }
516
517    /// The object number of destination `id`, allocated on first use.
518    pub fn pdf_dest_obj(&mut self, id: &DestId) -> i32 {
519        if let Some(d) = self.pdf.dests.get(id) {
520            return d.objnum;
521        }
522        let n = self.pdf_alloc_obj(ObjKind::Other);
523        self.pdf.dests.insert(
524            id.clone(),
525            DestEntry {
526                objnum: n,
527                defined: false,
528            },
529        );
530        self.pdf.dest_order.push(id.clone());
531        n
532    }
533
534    /// The object number of structure destination `id`, allocated on
535    /// first use.
536    pub fn pdf_struct_dest_obj(&mut self, id: &DestId) -> i32 {
537        if let Some(d) = self.pdf.struct_dests.get(id) {
538            return d.objnum;
539        }
540        let n = self.pdf_alloc_obj(ObjKind::Other);
541        self.pdf.struct_dests.insert(
542            id.clone(),
543            DestEntry {
544                objnum: n,
545                defined: false,
546            },
547        );
548        n
549    }
550
551    /// The object number of thread `id`, allocated on first use.
552    fn pdf_thread_obj(&mut self, id: DestId) -> i32 {
553        if let Some((_, n)) = self.pdf.threads.iter().find(|(t, _)| *t == id) {
554            return *n;
555        }
556        let n = self.pdf_alloc_obj(ObjKind::Other);
557        self.pdf.threads.push((id, n));
558        n
559    }
560
561    /// The PDF font of font `f`: the number in its resource name `/F<n>`
562    /// and its object number. Fonts with the same name share one PDF font.
563    /// It is created when first asked for (by `\pdffeedback fontname` or
564    /// `fontobjnum`, or their Lua counterparts), named after the font
565    /// asked for, and gets an object number then (observed: asking for
566    /// `cmr10 at 12pt` before `cmr10` names both after the former).
567    pub fn pdf_font(&mut self, f: u32) -> (u32, i32) {
568        if let Some(&r) = self.pdf.fonts.get(&f) {
569            return r;
570        }
571        let name = self.font(f).name.clone();
572        let shared = self
573            .pdf
574            .fonts
575            .iter()
576            .filter(|(g, _)| self.font_exists(**g as i32) && self.font(**g).name == name)
577            .map(|(_, r)| *r)
578            .min();
579        let r = match shared {
580            Some(r) => r,
581            None => (f, self.pdf_alloc_obj(ObjKind::Font)),
582        };
583        self.pdf.fonts.insert(f, r);
584        r
585    }
586
587    /// Makes sure the predefined colour stack 0 exists.
588    pub fn pdf_ensure_colorstacks(&mut self) {
589        if self.pdf.colorstacks.is_empty() {
590            self.pdf.colorstacks.push(ColorStack {
591                page: true,
592                mode: literal_mode::DIRECT,
593                init: b"0 g 0 G".to_vec(),
594            });
595        }
596    }
597
598    /// Defines a new colour stack and returns its number; stack 0 is the
599    /// predefined one (`\pdffeedback colorstackinit`, `pdf.newcolorstack`).
600    pub fn pdf_new_colorstack(&mut self, init: Vec<u8>, page: bool, mode: u8) -> i32 {
601        self.pdf_ensure_colorstacks();
602        self.pdf.colorstacks.push(ColorStack { page, mode, init });
603        (self.pdf.colorstacks.len() - 1) as i32
604    }
605
606    /// Appends a `pdf_refobj` whatsit for object `n` to the current list
607    /// (`\pdfextension refobj`, `pdf.refobj` in `\directlua`).
608    pub fn pdf_append_refobj(&mut self, n: i32) {
609        self.append_whatsit(whatsit_sub::PDF_REFOBJ, Whatsit::PdfRefobj { objnum: n });
610    }
611
612    /// Stores object `obj`, makes it `\pdffeedback lastobj`, and writes it
613    /// now if `immediate`.
614    ///
615    /// Observed: LuaTeX's "words of extra memory" grow by 5 for each
616    /// object from `\pdfextension obj` (immediate or not) and from
617    /// `pdf.obj`, but not from `pdf.immediateobj` or a reservation.
618    pub fn pdf_define_object(&mut self, obj: PdfObject, immediate: bool) {
619        if !immediate {
620            self.pdf.obj_words += 5;
621        }
622        let n = obj.num;
623        self.pdf.last_obj = n;
624        self.pdf.stream_written |= immediate && obj.stream;
625        self.pdf.objects.insert(n, obj);
626        if immediate {
627            self.pdf_write_object(n);
628        }
629    }
630
631    /// Appends a whatsit to the current list.
632    fn append_whatsit(&mut self, subtype: u16, w: Whatsit) {
633        // Observed: like every node, it carries the current attributes.
634        let p = self.new_node(id::WHATSIT, subtype, NodeData::Whatsit(Box::new(w)));
635        self.tail_append(p);
636    }
637
638    /// Scans `width`, `height`, `depth` keywords (`scan_rule_spec` for
639    /// annotations); unspecified dimensions are running.
640    fn scan_pdf_rule_spec(&mut self) -> [i32; 3] {
641        let mut d = [RUNNING; 3];
642        loop {
643            let i = if self.scan_keyword("width") {
644                0
645            } else if self.scan_keyword("height") {
646                1
647            } else if self.scan_keyword("depth") {
648                2
649            } else {
650                return d;
651            };
652            self.scan_normal_dimen();
653            d[i] = self.cur_val;
654        }
655    }
656
657    /// Reports a fatal backend error the way LuaTeX does and ends the run
658    /// without writing the PDF file.
659    pub fn pdf_fatal(&mut self, msg: &str) -> ! {
660        self.normalize_selector();
661        self.printer.print_nl("! error:  (pdf backend): ");
662        self.printer.print_str(msg);
663        self.printer
664            .print_nl("!  ==> Fatal error occurred, no output PDF file produced!");
665        self.printer.update_terminal();
666        // Observed: LuaTeX stops right there; the log ends without a
667        // newline and the PDF file is gone.
668        self.pdf.fatal = true;
669        self.err.history = crate::error::History::FatalErrorStop;
670        let _ = std::fs::remove_file(self.output_path("pdf"));
671        self.printer.close_log();
672        std::panic::resume_unwind(Box::new(crate::error::JumpOut));
673    }
674
675    /// Prints a backend warning on a line of its own
676    /// (`warning  (pdf backend): …`).
677    pub fn pdf_warning(&mut self, msg: &str) {
678        self.printer.print_nl("warning  (pdf backend): ");
679        self.printer.print_str(msg);
680        self.printer.print_ln();
681    }
682
683    /// Scans an action specification (`user {…}` | `goto …` | `thread …`).
684    fn scan_pdf_action(&mut self) -> Option<PdfAction> {
685        if self.scan_keyword("user") {
686            return Some(PdfAction::User(self.scan_pdf_string()));
687        }
688        let goto = if self.scan_keyword("goto") {
689            true
690        } else if self.scan_keyword("thread") {
691            false
692        } else {
693            self.pdf_fatal("action type missing");
694        };
695        let file = if self.scan_keyword("file") {
696            Some(self.scan_pdf_string())
697        } else {
698            None
699        };
700        // LuaTeX 1.22: `goto struct name {…}` or `struct num n` names the
701        // structure destination of the link.
702        let structure = if goto && self.scan_keyword("struct") {
703            if self.scan_keyword("name") {
704                Some(ActionTarget::Name(self.scan_pdf_string()))
705            } else if self.scan_keyword("num") {
706                self.scan_int();
707                Some(ActionTarget::Num(self.cur_val))
708            } else {
709                self.pdf_fatal("identifier type missing");
710            }
711        } else {
712            None
713        };
714        let target = if goto && self.scan_keyword("page") {
715            self.scan_int();
716            let page = self.cur_val;
717            ActionTarget::Page(page, self.scan_pdf_string())
718        } else if self.scan_keyword("name") {
719            ActionTarget::Name(self.scan_pdf_string())
720        } else if self.scan_keyword("num") {
721            if goto && file.is_some() {
722                self.pdf_fatal("'goto' option cannot be used with both 'file' and 'num'");
723            }
724            self.scan_int();
725            ActionTarget::Num(self.cur_val)
726        } else {
727            self.pdf_fatal("identifier type missing");
728        };
729        let new_window = if self.scan_keyword("newwindow") {
730            Some(true)
731        } else if self.scan_keyword("nonewwindow") {
732            Some(false)
733        } else {
734            None
735        };
736        if new_window.is_some() && !(goto && file.is_some()) {
737            self.pdf_fatal(
738                "'newwindow' or 'nonewwindow' must be used with 'goto' and 'file' option",
739            );
740        }
741        Some(if goto {
742            PdfAction::Goto {
743                file,
744                structure,
745                target,
746                new_window,
747            }
748        } else {
749            PdfAction::Thread { file, target }
750        })
751    }
752
753    /// The PDF text of an action (a dictionary, or the user text),
754    /// allocating the objects of the destinations, pages and threads it
755    /// refers to.
756    pub fn pdf_action_text(&mut self, action: &PdfAction) -> Vec<u8> {
757        fn string(out: &mut Vec<u8>, s: &[u8]) {
758            out.push(b'(');
759            out.extend_from_slice(s);
760            out.push(b')');
761        }
762        let mut d = b"<< ".to_vec();
763        match action {
764            PdfAction::User(u) => return u.clone(),
765            PdfAction::Goto {
766                file: None,
767                target,
768                structure,
769                ..
770            } => {
771                d.extend_from_slice(b"/S /GoTo /D ");
772                // Observed: ` /SD n 0 R` after the destination.
773                let sd = structure.as_ref().map(|s| {
774                    let id = match s {
775                        ActionTarget::Name(n) => DestId::Name(n.clone()),
776                        ActionTarget::Num(n) => DestId::Num(*n),
777                        ActionTarget::Page(..) => DestId::Num(0),
778                    };
779                    self.pdf_struct_dest_obj(&id)
780                });
781                match target {
782                    ActionTarget::Name(n) => {
783                        self.pdf_dest_obj(&DestId::Name(n.clone()));
784                        string(&mut d, n);
785                    }
786                    ActionTarget::Num(n) => {
787                        let o = self.pdf_dest_obj(&DestId::Num(*n));
788                        d.extend_from_slice(format!("{o} 0 R").as_bytes());
789                    }
790                    ActionTarget::Page(p, spec) => {
791                        let o = self.pdf_page_ref((*p).max(1));
792                        d.extend_from_slice(format!("[ {o} 0 R ").as_bytes());
793                        d.extend_from_slice(spec);
794                        d.extend_from_slice(b" ]");
795                    }
796                }
797                if let Some(o) = sd {
798                    d.extend_from_slice(format!(" /SD {o} 0 R").as_bytes());
799                }
800            }
801            PdfAction::Goto {
802                file: Some(f),
803                target,
804                new_window,
805                ..
806            } => {
807                d.extend_from_slice(b"/F ");
808                string(&mut d, f);
809                d.push(b' ');
810                match new_window {
811                    Some(true) => d.extend_from_slice(b" /NewWindow true"),
812                    Some(false) => d.extend_from_slice(b" /NewWindow false"),
813                    None => {}
814                }
815                match target {
816                    ActionTarget::Name(n) => {
817                        d.extend_from_slice(b" /S /GoToR /D ");
818                        string(&mut d, n);
819                    }
820                    ActionTarget::Page(p, spec) => {
821                        // Observed: /GoTo (not /GoToR), page counted from 0.
822                        d.extend_from_slice(format!(" /S /GoTo /D [{} ", p - 1).as_bytes());
823                        d.extend_from_slice(spec);
824                        d.extend_from_slice(b" ]");
825                    }
826                    ActionTarget::Num(_) => {}
827                }
828            }
829            PdfAction::Thread { file, target } => {
830                if let Some(f) = file {
831                    // Observed: the identifier is dropped.
832                    d.extend_from_slice(b"/F ");
833                    string(&mut d, f);
834                    d.extend_from_slice(b"  /S /Thread");
835                } else {
836                    d.extend_from_slice(b"/S /Thread /D ");
837                    match target {
838                        ActionTarget::Name(n) => {
839                            self.pdf_thread_obj(DestId::Name(n.clone()));
840                            string(&mut d, n);
841                        }
842                        ActionTarget::Num(n) => {
843                            let o = self.pdf_thread_obj(DestId::Num(*n));
844                            d.extend_from_slice(format!("{o} 0 R").as_bytes());
845                        }
846                        ActionTarget::Page(..) => {}
847                    }
848                }
849            }
850        }
851        d.extend_from_slice(b" >>");
852        d
853    }
854
855    /// Writes `body` as object `n` now (actions and titles of outlines,
856    /// the open action).
857    fn pdf_write_raw(&mut self, n: i32, body: Vec<u8>) {
858        let obj = PdfObject {
859            num: n,
860            data: body,
861            defined: true,
862            ..Default::default()
863        };
864        self.pdf_write_object_data(&obj);
865    }
866
867    /// Writes an action object for `action` now and returns its number;
868    /// user actions are written between newlines.
869    fn pdf_write_action(&mut self, action: &PdfAction) -> i32 {
870        let n = self.pdf_alloc_obj(ObjKind::Other);
871        if let Some(mut be) = self.backend.take() {
872            be.begin_object(self);
873            self.backend = Some(be);
874        }
875        let body = match action {
876            PdfAction::User(u) => {
877                let mut b = b"\n".to_vec();
878                b.extend_from_slice(u);
879                b.push(b'\n');
880                b
881            }
882            _ => self.pdf_action_text(action),
883        };
884        self.pdf_write_raw(n, body);
885        n
886    }
887
888    /// Executes `\pdfextension` (`immediate` when prefixed by
889    /// `\immediate`).
890    pub fn pdf_extension(&mut self, immediate: bool) {
891        if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) <= 0 {
892            // Observed: in DVI mode LuaTeX ignores `\pdfextension` without
893            // reading any keyword.
894            return;
895        }
896        if self.scan_keyword("literal") {
897            let late = self.scan_keyword("shipout");
898            let mode = if self.scan_keyword("direct") {
899                literal_mode::DIRECT
900            } else if self.scan_keyword("page") {
901                literal_mode::PAGE
902            } else if self.scan_keyword("raw") {
903                literal_mode::RAW
904            } else {
905                literal_mode::ORIGIN
906            };
907            let data = if late {
908                LateData::Tokens(self.scan_general_text())
909            } else {
910                LateData::Bytes(self.scan_pdf_string())
911            };
912            self.append_whatsit(
913                if late {
914                    whatsit_sub::PDF_LATE_LITERAL
915                } else {
916                    whatsit_sub::PDF_LITERAL
917                },
918                Whatsit::PdfLiteral { mode, data, late },
919            );
920        } else if self.scan_keyword("lateliteral") {
921            let data = LateData::Tokens(self.scan_general_text());
922            self.append_whatsit(
923                whatsit_sub::PDF_LATE_LITERAL,
924                Whatsit::PdfLiteral {
925                    mode: literal_mode::ORIGIN,
926                    data,
927                    late: true,
928                },
929            );
930        } else if self.scan_keyword("save") {
931            self.append_whatsit(whatsit_sub::PDF_SAVE, Whatsit::PdfSave);
932        } else if self.scan_keyword("restore") {
933            self.append_whatsit(whatsit_sub::PDF_RESTORE, Whatsit::PdfRestore);
934        } else if self.scan_keyword("setmatrix") {
935            let data = self.scan_pdf_string();
936            self.append_whatsit(whatsit_sub::PDF_SETMATRIX, Whatsit::PdfSetmatrix { data });
937        } else if self.scan_keyword("colorstack") {
938            self.scan_int();
939            let stack = self.cur_val;
940            let (cmd, data) = if self.scan_keyword("set") {
941                (colorstack_cmd::SET, self.scan_pdf_string())
942            } else if self.scan_keyword("push") {
943                (colorstack_cmd::PUSH, self.scan_pdf_string())
944            } else if self.scan_keyword("pop") {
945                (colorstack_cmd::POP, Vec::new())
946            } else if self.scan_keyword("current") {
947                (colorstack_cmd::CURRENT, Vec::new())
948            } else {
949                self.print_err("Color stack action is missing");
950                self.help(&[
951                    "The expected actions for \\pdfextension colorstack:",
952                    "    set, push, pop, current",
953                ]);
954                self.error();
955                return;
956            };
957            self.append_whatsit(
958                whatsit_sub::PDF_COLORSTACK,
959                Whatsit::PdfColorstack { stack, cmd, data },
960            );
961        } else if self.scan_keyword("obj") {
962            self.pdf_obj(immediate);
963        } else if self.scan_keyword("refobj") {
964            self.scan_int();
965            let n = self.cur_val;
966            self.pdf_append_refobj(n);
967        } else if self.scan_keyword("annot") {
968            let objnum = if self.scan_keyword("reserveobjnum") {
969                let n = self.pdf_alloc_obj(ObjKind::Annot);
970                self.pdf.last_annot = n;
971                self.scan_optional_space();
972                return;
973            } else if self.scan_keyword("useobjnum") {
974                self.scan_int();
975                self.cur_val
976            } else {
977                self.pdf_alloc_obj(ObjKind::Annot)
978            };
979            self.pdf.last_annot = objnum;
980            let dims = self.scan_pdf_rule_spec();
981            let data = self.scan_pdf_string();
982            self.append_whatsit(
983                whatsit_sub::PDF_ANNOT,
984                Whatsit::PdfAnnot { dims, objnum, data },
985            );
986        } else if self.scan_keyword("startlink") {
987            if self.mode.abs_kind() == crate::maincontrol::ModeKind::Vertical {
988                self.pdf_fatal("startlink cannot be used in vertical mode");
989            }
990            let dims = self.scan_pdf_rule_spec();
991            let mut attr = Vec::new();
992            if self.scan_keyword("attr") {
993                attr = self.scan_pdf_string();
994            }
995            let Some(action) = self.scan_pdf_action() else {
996                return;
997            };
998            let objnum = self.pdf_alloc_obj(ObjKind::Link);
999            self.pdf.last_link = objnum;
1000            self.append_whatsit(
1001                whatsit_sub::PDF_START_LINK,
1002                Whatsit::PdfStartLink {
1003                    dims,
1004                    objnum,
1005                    attr,
1006                    action,
1007                },
1008            );
1009        } else if self.scan_keyword("endlink") {
1010            if self.mode.abs_kind() == crate::maincontrol::ModeKind::Vertical {
1011                self.pdf_fatal("endlink cannot be used in vertical mode");
1012            }
1013            self.append_whatsit(whatsit_sub::PDF_END_LINK, Whatsit::PdfEndLink);
1014        } else if self.scan_keyword("dest") {
1015            self.pdf_dest_cmd();
1016        } else if self.scan_keyword("outline") {
1017            self.pdf_outline_cmd();
1018        } else if self.scan_keyword("glyphtounicode") {
1019            let a = self.scan_pdf_string();
1020            let b = self.scan_pdf_string();
1021            self.pdf.glyph_to_unicode.push((a, b));
1022        } else if self.scan_keyword("catalog") {
1023            let s = self.scan_pdf_string();
1024            self.pdf.catalog.extend_from_slice(&s);
1025            if self.scan_keyword("openaction") {
1026                if self.pdf.open_action.is_some() {
1027                    self.pdf_fatal("duplicate of openaction");
1028                }
1029                if let Some(a) = self.scan_pdf_action() {
1030                    let n = self.pdf_write_action(&a);
1031                    self.pdf.open_action = Some(n);
1032                }
1033            }
1034        } else if self.scan_keyword("info") {
1035            let s = self.scan_pdf_string();
1036            self.pdf.info.extend_from_slice(&s);
1037        } else if self.scan_keyword("names") {
1038            let s = self.scan_pdf_string();
1039            self.pdf.names.extend_from_slice(&s);
1040        } else if self.scan_keyword("trailer") {
1041            let s = self.scan_pdf_string();
1042            self.pdf.trailer.extend_from_slice(&s);
1043        } else if self.scan_keyword("fontattr") {
1044            self.scan_int();
1045            let _ = self.scan_pdf_string();
1046        } else if self.scan_keyword("mapfile") {
1047            let s = self.scan_pdf_string();
1048            // Observed: the map file is read at once (`{path}` in the log),
1049            // after the default one for `+` and `=`, and the default one is
1050            // then not announced at the first page. (Reading a file twice
1051            // warns about each duplicate entry; that is not reproduced.)
1052            if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
1053                let text = String::from_utf8_lossy(&s).trim().to_owned();
1054                let prefix = text.chars().next().filter(|c| matches!(c, '+' | '-' | '='));
1055                let name = text.trim_start_matches(['+', '-', '=']);
1056                if prefix != Some('-') {
1057                    if prefix.is_some() && !self.pdf.map_note_done {
1058                        self.print_map_file_note("pdftex.map");
1059                    }
1060                    self.print_map_file_note(name);
1061                    self.pdf.map_note_done = true;
1062                }
1063            }
1064            self.pdf.map_entries.push(s);
1065        } else if self.scan_keyword("mapline") {
1066            let s = self.scan_pdf_string();
1067            self.pdf.map_entries.push(s);
1068        } else if self.scan_keyword("includechars") {
1069            let _ = self.scan_pdf_string();
1070        } else if self.scan_keyword("linkstate") {
1071            self.scan_int();
1072            let state = self.cur_val;
1073            self.append_whatsit(
1074                crate::nodes::whatsit_sub::PDF_LINK_STATE,
1075                Whatsit::PdfLinkState { state },
1076            );
1077        } else if self.scan_keyword("startthread") || self.scan_keyword("thread") {
1078            // Article threads are not written yet; the syntax is accepted.
1079            let _ = self.scan_pdf_rule_spec();
1080            if self.scan_keyword("attr") {
1081                let _ = self.scan_pdf_string();
1082            }
1083            if self.scan_keyword("num") {
1084                self.scan_int();
1085            } else if self.scan_keyword("name") {
1086                let _ = self.scan_pdf_string();
1087            }
1088        } else if self.scan_keyword("endthread") {
1089        } else {
1090            // LuaTeX 1.22: lower case, no help text.
1091            self.print_err("unexpected use of \\pdfextension");
1092            self.help(&[]);
1093            self.error();
1094        }
1095    }
1096
1097    /// `\pdfextension dest …`.
1098    fn pdf_dest_cmd(&mut self) {
1099        let structure = if self.scan_keyword("struct") {
1100            self.scan_int();
1101            Some(self.cur_val)
1102        } else {
1103            None
1104        };
1105        let id = if self.scan_keyword("num") {
1106            self.scan_int();
1107            DestId::Num(self.cur_val)
1108        } else if self.scan_keyword("name") {
1109            DestId::Name(self.scan_pdf_string())
1110        } else {
1111            self.pdf_fatal("identifier type missing");
1112        };
1113        let mut view = DestView {
1114            kind: DestKind::Xyz,
1115            zoom: 0,
1116            dims: [RUNNING; 3],
1117        };
1118        if self.scan_keyword("xyz") {
1119            if self.scan_keyword("zoom") {
1120                self.scan_int();
1121                view.zoom = self.cur_val;
1122            }
1123        } else if self.scan_keyword("fitbh") {
1124            view.kind = DestKind::FitBH;
1125        } else if self.scan_keyword("fitbv") {
1126            view.kind = DestKind::FitBV;
1127        } else if self.scan_keyword("fitb") {
1128            view.kind = DestKind::FitB;
1129        } else if self.scan_keyword("fith") {
1130            view.kind = DestKind::FitH;
1131        } else if self.scan_keyword("fitv") {
1132            view.kind = DestKind::FitV;
1133        } else if self.scan_keyword("fitr") {
1134            view.kind = DestKind::FitR;
1135            view.dims = self.scan_pdf_rule_spec();
1136        } else if self.scan_keyword("fit") {
1137            view.kind = DestKind::Fit;
1138        } else {
1139            self.pdf_fatal("destination type missing");
1140        }
1141        self.append_whatsit(
1142            whatsit_sub::PDF_DEST,
1143            Whatsit::PdfDest {
1144                id,
1145                view,
1146                structure,
1147            },
1148        );
1149    }
1150
1151    /// `\pdfextension outline …`.
1152    fn pdf_outline_cmd(&mut self) {
1153        let mut o = Outline::default();
1154        if self.scan_keyword("attr") {
1155            o.attr = self.scan_pdf_string();
1156        }
1157        if self.scan_keyword("useobjnum") {
1158            self.scan_int();
1159            o.action = self.cur_val;
1160        } else {
1161            let Some(a) = self.scan_pdf_action() else {
1162                return;
1163            };
1164            o.action = self.pdf_write_action(&a);
1165        }
1166        if self.scan_keyword("count") {
1167            self.scan_int();
1168            o.count = self.cur_val;
1169        }
1170        let title = self.scan_pdf_string();
1171        o.objnum = self.pdf_alloc_obj(ObjKind::Other);
1172        o.title = self.pdf_alloc_obj(ObjKind::Other);
1173        let mut body = b"(".to_vec();
1174        body.extend_from_slice(&title);
1175        body.extend_from_slice(b")\n");
1176        self.pdf_write_raw(o.title, body);
1177        self.pdf.outlines.push(o);
1178    }
1179
1180    /// `\pdfextension obj …`.
1181    fn pdf_obj(&mut self, immediate: bool) {
1182        if self.scan_keyword("reserveobjnum") {
1183            let n = self.pdf_alloc_obj(ObjKind::Obj);
1184            self.pdf.objects.insert(
1185                n,
1186                PdfObject {
1187                    num: n,
1188                    ..Default::default()
1189                },
1190            );
1191            self.pdf.last_obj = n;
1192            // Observed: a space after the keyword is skipped.
1193            self.scan_optional_space();
1194            return;
1195        }
1196        let n = if self.scan_keyword("useobjnum") {
1197            self.scan_int();
1198            self.cur_val
1199        } else {
1200            self.pdf_alloc_obj(ObjKind::Obj)
1201        };
1202        let uncompressed = self.scan_keyword("uncompressed");
1203        let mut obj = PdfObject {
1204            num: n,
1205            uncompressed,
1206            defined: true,
1207            ..Default::default()
1208        };
1209        if self.scan_keyword("stream") {
1210            obj.stream = true;
1211            if self.scan_keyword("attr") {
1212                obj.attr = self.scan_pdf_string();
1213            }
1214        }
1215        if self.scan_keyword("file") {
1216            obj.file = true;
1217        }
1218        obj.data = self.scan_pdf_string();
1219        if immediate {
1220            self.pdf.obj_words += 5;
1221        }
1222        self.pdf_define_object(obj, immediate);
1223    }
1224
1225    /// Writes object `n` through the backend now.
1226    pub fn pdf_write_object(&mut self, n: i32) {
1227        let Some(obj) = self.pdf.objects.get(&n).cloned() else {
1228            return;
1229        };
1230        if obj.written || !obj.defined {
1231            return;
1232        }
1233        if let Some(o) = self.pdf.objects.get_mut(&n) {
1234            o.written = true;
1235        }
1236        self.pdf_write_object_data(&obj);
1237    }
1238
1239    /// Hands `obj` to the backend, or queues it while the backend is busy
1240    /// shipping a page. A file object prints `<<name>>` as LuaTeX does.
1241    fn pdf_write_object_data(&mut self, obj: &PdfObject) {
1242        let Some(obj) = self.pdf_object_payload(obj) else {
1243            return;
1244        };
1245        match self.backend.take() {
1246            Some(mut be) => {
1247                be.write_object(self, &obj);
1248                self.backend = Some(be);
1249            }
1250            None => self.pdf.pending.push(obj),
1251        }
1252    }
1253
1254    /// Takes object `n` (defined and not yet written) for writing with the
1255    /// current page (`\pdfextension refobj`).
1256    pub(crate) fn pdf_take_page_object(&mut self, n: i32) -> Option<PdfObject> {
1257        let obj = self.pdf.objects.get_mut(&n)?;
1258        if obj.written || !obj.defined {
1259            return None;
1260        }
1261        obj.written = true;
1262        let obj = obj.clone();
1263        self.pdf_object_payload(&obj)
1264    }
1265
1266    /// The object as written: a file object gets the file's contents
1267    /// (printing `<<name>>` as LuaTeX does).
1268    fn pdf_object_payload(&mut self, obj: &PdfObject) -> Option<PdfObject> {
1269        let mut obj = obj.clone();
1270        if obj.file {
1271            let name = String::from_utf8_lossy(&obj.data).into_owned();
1272            let path = self
1273                .kpse
1274                .as_ref()
1275                .and_then(|k| {
1276                    k.borrow_mut()
1277                        .find_file(&name, retex_kpath::FileFormat::Tex)
1278                })
1279                .unwrap_or_else(|| std::path::PathBuf::from(&name));
1280            match std::fs::read(&path) {
1281                Ok(d) => {
1282                    self.printer.print_str("<<");
1283                    self.printer.print_bytes(name.as_bytes());
1284                    self.printer.print_str(">>");
1285                    obj.data = d;
1286                    obj.file = false;
1287                }
1288                Err(_) => {
1289                    self.print_err("pdf backend: cannot open file for embedding");
1290                    self.help(&[]);
1291                    self.error();
1292                    return None;
1293                }
1294            }
1295        }
1296        Some(obj)
1297    }
1298
1299    /// A token list of whatsit data, for late expansion.
1300    pub fn whatsit_tokens(list: &TokenList) -> LateData {
1301        LateData::Tokens(list.clone())
1302    }
1303
1304    /// Works out what the backend needs at the end of the run, and prints
1305    /// LuaTeX's warnings about destinations and threads that were referred
1306    /// to but never defined.
1307    pub fn pdf_finish_document(&mut self) {
1308        let mut doc = DocItems::default();
1309        // Observed order: missing pages, then destinations (each in the
1310        // reverse order of allocation), then threads; the warnings are
1311        // separated by empty lines.
1312        let mut warnings = Vec::new();
1313        let mut pages: Vec<(i32, i32)> = self
1314            .pdf
1315            .page_refs
1316            .iter()
1317            .filter(|(p, _)| **p > self.total_pages as i32)
1318            .map(|(p, n)| (*n, *p))
1319            .collect();
1320        pages.sort_unstable_by(|a, b| b.cmp(a));
1321        for (_, p) in pages {
1322            warnings.push(format!("page {p} has been referenced but does not exist"));
1323        }
1324        let order = self.pdf.dest_order.clone();
1325        for id in order.iter().rev() {
1326            if let Some(d) = self.pdf.dests.get(id).copied()
1327                && !d.defined
1328            {
1329                warnings.push(format!("unreferenced destination with {}", id.describe()));
1330                doc.missing_dests.push(d.objnum);
1331            }
1332        }
1333        for (id, n) in self.pdf.threads.clone() {
1334            warnings.push(format!("unknown thread destination {}", id.describe()));
1335            let title = match id {
1336                DestId::Name(n) => n,
1337                DestId::Num(k) => k.to_string().into_bytes(),
1338            };
1339            doc.missing_threads.push((n, title));
1340        }
1341        for (i, w) in warnings.iter().enumerate() {
1342            if i > 0 {
1343                self.printer.print_ln();
1344            }
1345            self.pdf_warning(w);
1346        }
1347        let mut names: Vec<(Vec<u8>, i32)> = self
1348            .pdf
1349            .dests
1350            .iter()
1351            .filter_map(|(k, d)| match k {
1352                DestId::Name(n) => Some((n.clone(), d.objnum)),
1353                DestId::Num(_) => None,
1354            })
1355            .collect();
1356        names.sort();
1357        doc.dest_names = names;
1358        let toks = |e: &mut Engine, i: u32| {
1359            let list = e.eqtb.toks_param(i).clone();
1360            e.print_to_string(|e| e.show_token_list(&list, 0, list.len(), i64::MAX))
1361        };
1362        doc.pages_attr = toks(self, crate::backend::PAGES_ATTR);
1363        doc.trailer_id = toks(self, crate::backend::TRAILER_ID);
1364        self.pdf.doc = doc;
1365    }
1366}