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    /// The current rectangle before transformation and margin.
331    pub raw: [i32; 4],
332    /// The transformation (`\pdfextension setmatrix`) where it started.
333    pub matrix: Option<[f64; 6]>,
334}
335
336/// An annotation's rectangle (sp, page coordinates) and text.
337pub type AnnotBody = ([i32; 4], Vec<u8>);
338
339/// The interactive items of the page being shipped, handed to the
340/// backend when the page ends.
341#[derive(Clone, Debug, Default)]
342pub struct PageItems {
343    /// Annotations: object number and, unless written by other means
344    /// (`pdf.registerannot`), rectangle and text.
345    pub annots: Vec<(i32, Option<AnnotBody>)>,
346    /// Link rectangles, in the order they were started.
347    pub links: Vec<LinkPiece>,
348    /// Destinations: object number, whether named, view, and for a
349    /// structure destination the structure element's object.
350    pub dests: Vec<(i32, bool, DestPos, Option<i32>)>,
351    /// Objects to write with the page (`\pdfextension refobj`).
352    pub objects: Vec<PdfObject>,
353    /// Page attributes: the TeX and the Lua text.
354    pub attr: (Vec<u8>, Vec<u8>),
355    /// Page resources: the TeX and the Lua text.
356    pub resources: (Vec<u8>, Vec<u8>),
357}
358
359/// Document-level items computed at the end of the run for the backend.
360#[derive(Clone, Debug, Default)]
361pub struct DocItems {
362    /// Destinations referred to but never defined, in writing order.
363    pub missing_dests: Vec<i32>,
364    /// Threads referred to but never defined: object and title.
365    pub missing_threads: Vec<(i32, Vec<u8>)>,
366    /// Named destinations sorted by name.
367    pub dest_names: Vec<(Vec<u8>, i32)>,
368    /// `\pdfvariable pagesattr` at the end of the run.
369    pub pages_attr: Vec<u8>,
370    /// `\pdfvariable trailerid` at the end of the run.
371    pub trailer_id: Vec<u8>,
372}
373
374/// A destination known by its identifier.
375#[derive(Clone, Copy, Debug, Default)]
376pub struct DestEntry {
377    /// The object number.
378    pub objnum: i32,
379    /// Whether `\pdfextension dest` was shipped for it.
380    pub defined: bool,
381}
382
383/// Document-level PDF state.
384#[derive(Clone, Debug, Default)]
385pub struct PdfState {
386    /// Images written while shipping the current page, in order of first
387    /// use: their file names are printed at the end of the page.
388    pub page_images: Vec<u32>,
389    /// Objects by PDF object number.
390    pub objects: std::collections::BTreeMap<i32, PdfObject>,
391    /// `\pdfextension catalog` text.
392    pub catalog: Vec<u8>,
393    /// The catalog's open action (object number).
394    pub open_action: Option<i32>,
395    /// `\pdfextension info` text.
396    pub info: Vec<u8>,
397    /// `\pdfextension names` text.
398    pub names: Vec<u8>,
399    /// `\pdfextension trailer` text.
400    pub trailer: Vec<u8>,
401    /// Outline entries in order.
402    pub outlines: Vec<Outline>,
403    /// `\pdfextension glyphtounicode` pairs.
404    pub glyph_to_unicode: Vec<(Vec<u8>, Vec<u8>)>,
405    /// Colour stacks; stack 0 is the predefined colour stack.
406    pub colorstacks: Vec<ColorStack>,
407    /// `\pdffeedback lastobj`.
408    pub last_obj: i32,
409    /// `\pdffeedback lastannot`.
410    pub last_annot: i32,
411    /// `\pdffeedback lastlink`.
412    pub last_link: i32,
413    /// The highest object number allocated so far, shared with the PDF
414    /// writer.
415    pub counter: Rc<Cell<u32>>,
416    /// The kind of each allocated object number.
417    pub kinds: HashMap<i32, ObjKind>,
418    /// Page object numbers by page number (forward references included).
419    pub page_refs: HashMap<i32, i32>,
420    /// PDF fonts (resource name number, object number) by font, see
421    /// [`Engine::pdf_font`].
422    pub fonts: HashMap<u32, (u32, i32)>,
423    /// `\pdfextension fontattr` and `pdf.setfontattributes` texts by font.
424    pub font_attrs: HashMap<u32, Vec<u8>>,
425    /// Map files and map lines (`pdf.mapfile`, `pdf.mapline`,
426    /// `\pdfextension mapfile`/`mapline`), in order.
427    pub map_entries: Vec<Vec<u8>>,
428    /// Texts set from Lua.
429    pub lua: LuaPdfTexts,
430    /// `\pdffeedback retval`.
431    pub retval: i32,
432    /// The position (PDF coordinates, sp) of the last `\latelua`, for
433    /// `pdf.getpos`.
434    pub pos: (i32, i32),
435    /// The `\latelua` being executed, if any.
436    pub late: Option<LateLuaContext>,
437    /// Annotations registered from Lua for the page being shipped.
438    pub page_annots: Vec<i32>,
439    /// Whether `pdf.obj` opened the output file (in DVI mode LuaTeX then
440    /// leaves an empty DVI file when no page is shipped out).
441    pub output_opened: bool,
442    /// Destinations by identifier.
443    pub dests: HashMap<DestId, DestEntry>,
444    /// Destination identifiers in the order their numbers were allocated.
445    pub dest_order: Vec<DestId>,
446    /// Structure destinations by identifier (`\pdfextension dest struct`).
447    pub struct_dests: HashMap<DestId, DestEntry>,
448    /// Threads referred to by actions: identifier and object number.
449    pub threads: Vec<(DestId, i32)>,
450    /// The interactive items of the page being shipped.
451    pub page: PageItems,
452    /// Objects to write as soon as the backend is available again (they
453    /// were written during shipout, while the backend is busy).
454    pub pending: Vec<PdfObject>,
455    /// Document-level items for the backend, computed at the end.
456    pub doc: DocItems,
457    /// A fatal backend error occurred: no PDF is written.
458    pub fatal: bool,
459    /// Objects written into object streams and the number of streams.
460    pub objstm_stats: (u32, u32),
461    /// Whether a stream (form, image or stream object) was written
462    /// immediately: with no pages, LuaTeX then fails at the end
463    /// ("already written content discarded").
464    pub stream_written: bool,
465    /// Words of extra memory LuaTeX reports for objects (see
466    /// [`Engine::pdf_define_object`]).
467    pub obj_words: usize,
468    /// Whether the font map note (`{…/pdftex.map}`) was printed.
469    pub map_note_done: bool,
470    /// Links started but not ended yet, innermost last.
471    pub active_links: Vec<ActiveLink>,
472}
473
474impl PdfState {
475    /// The highest object number allocated so far.
476    pub fn max_obj(&self) -> i32 {
477        self.counter.get() as i32
478    }
479
480    /// The number of named destinations (for the PDF statistics).
481    pub fn named_dests(&self) -> usize {
482        self.dests
483            .keys()
484            .filter(|k| matches!(k, DestId::Name(_)))
485            .count()
486    }
487}
488
489impl Engine {
490    /// Scans a balanced text (expanded) and returns its characters.
491    fn scan_pdf_string(&mut self) -> Vec<u8> {
492        let toks = self.scan_toks_expanded();
493        self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX))
494    }
495
496    /// Allocates a PDF object number of kind `kind`.
497    pub fn pdf_alloc_obj(&mut self, kind: ObjKind) -> i32 {
498        self.figures.taint("PDF objects");
499        let n = self.pdf.counter.get() + 1;
500        self.pdf.counter.set(n);
501        let n = n as i32;
502        self.pdf.kinds.insert(n, kind);
503        n
504    }
505
506    /// The kind of object number `n`, if it was allocated.
507    pub fn pdf_obj_kind(&self, n: i32) -> Option<ObjKind> {
508        self.pdf.kinds.get(&n).copied()
509    }
510
511    /// The object number of page `page` (counting from 1), allocated on
512    /// first request (`pdf.getpageref`, `\pdffeedback pageref`).
513    pub fn pdf_page_ref(&mut self, page: i32) -> i32 {
514        if let Some(&n) = self.pdf.page_refs.get(&page) {
515            return n;
516        }
517        let n = self.pdf_alloc_obj(ObjKind::Page);
518        self.pdf.page_refs.insert(page, n);
519        n
520    }
521
522    /// The object number of destination `id`, allocated on first use.
523    pub fn pdf_dest_obj(&mut self, id: &DestId) -> i32 {
524        if let Some(d) = self.pdf.dests.get(id) {
525            return d.objnum;
526        }
527        let n = self.pdf_alloc_obj(ObjKind::Other);
528        self.pdf.dests.insert(
529            id.clone(),
530            DestEntry {
531                objnum: n,
532                defined: false,
533            },
534        );
535        self.pdf.dest_order.push(id.clone());
536        n
537    }
538
539    /// The object number of structure destination `id`, allocated on
540    /// first use.
541    pub fn pdf_struct_dest_obj(&mut self, id: &DestId) -> i32 {
542        if let Some(d) = self.pdf.struct_dests.get(id) {
543            return d.objnum;
544        }
545        let n = self.pdf_alloc_obj(ObjKind::Other);
546        self.pdf.struct_dests.insert(
547            id.clone(),
548            DestEntry {
549                objnum: n,
550                defined: false,
551            },
552        );
553        n
554    }
555
556    /// The object number of thread `id`, allocated on first use.
557    fn pdf_thread_obj(&mut self, id: DestId) -> i32 {
558        if let Some((_, n)) = self.pdf.threads.iter().find(|(t, _)| *t == id) {
559            return *n;
560        }
561        let n = self.pdf_alloc_obj(ObjKind::Other);
562        self.pdf.threads.push((id, n));
563        n
564    }
565
566    /// The PDF font of font `f`: the number in its resource name `/F<n>`
567    /// and its object number. Fonts with the same name share one PDF font.
568    /// It is created when first asked for (by `\pdffeedback fontname` or
569    /// `fontobjnum`, or their Lua counterparts), named after the font
570    /// asked for, and gets an object number then (observed: asking for
571    /// `cmr10 at 12pt` before `cmr10` names both after the former).
572    pub fn pdf_font(&mut self, f: u32) -> (u32, i32) {
573        if let Some(&r) = self.pdf.fonts.get(&f) {
574            return r;
575        }
576        let name = self.font(f).name.clone();
577        let shared = self
578            .pdf
579            .fonts
580            .iter()
581            .filter(|(g, _)| self.font_exists(**g as i32) && self.font(**g).name == name)
582            .map(|(_, r)| *r)
583            .min();
584        let r = match shared {
585            Some(r) => r,
586            None => (f, self.pdf_alloc_obj(ObjKind::Font)),
587        };
588        self.pdf.fonts.insert(f, r);
589        r
590    }
591
592    /// Makes sure the predefined colour stack 0 exists.
593    pub fn pdf_ensure_colorstacks(&mut self) {
594        if self.pdf.colorstacks.is_empty() {
595            self.figures.taint("makes a colour stack");
596            self.pdf.colorstacks.push(ColorStack {
597                page: true,
598                mode: literal_mode::DIRECT,
599                init: b"0 g 0 G".to_vec(),
600            });
601        }
602    }
603
604    /// Defines a new colour stack and returns its number; stack 0 is the
605    /// predefined one (`\pdffeedback colorstackinit`, `pdf.newcolorstack`).
606    pub fn pdf_new_colorstack(&mut self, init: Vec<u8>, page: bool, mode: u8) -> i32 {
607        self.pdf_ensure_colorstacks();
608        self.figures.taint("makes a colour stack");
609        self.pdf.colorstacks.push(ColorStack { page, mode, init });
610        (self.pdf.colorstacks.len() - 1) as i32
611    }
612
613    /// LuaTeX's checks of the object of `\pdfextension refobj` in PDF
614    /// mode (observed): it must be a defined `obj` object.
615    fn pdf_check_refobj(&mut self, n: i32) {
616        if n == 0 {
617            // Observed: object 0 counts as a font.
618            self.pdf_fatal("referenced object has wrong type font; should be obj");
619        }
620        match self.pdf_obj_kind(n) {
621            None => self.pdf_fatal("cannot find referenced object"),
622            Some(ObjKind::Obj) => {
623                if !self.pdf.objects.get(&n).is_some_and(|o| o.defined) {
624                    self.pdf_fatal("invalid object");
625                }
626            }
627            Some(k) => {
628                let msg = format!(
629                    "referenced object has wrong type {}; should be obj",
630                    k.name()
631                );
632                self.pdf_fatal(&msg);
633            }
634        }
635    }
636
637    /// Appends a `pdf_refobj` whatsit for object `n` to the current list
638    /// (`\pdfextension refobj`, `pdf.refobj` in `\directlua`).
639    pub fn pdf_append_refobj(&mut self, n: i32) {
640        self.append_whatsit(whatsit_sub::PDF_REFOBJ, Whatsit::PdfRefobj { objnum: n });
641    }
642
643    /// Stores object `obj`, makes it `\pdffeedback lastobj`, and writes it
644    /// now if `immediate`.
645    ///
646    /// Observed: LuaTeX's "words of extra memory" grow by 5 for each
647    /// object from `\pdfextension obj` (immediate or not) and from
648    /// `pdf.obj`, but not from `pdf.immediateobj` or a reservation.
649    pub fn pdf_define_object(&mut self, obj: PdfObject, immediate: bool) {
650        if !immediate {
651            self.pdf.obj_words += 5;
652        }
653        let n = obj.num;
654        self.pdf.last_obj = n;
655        self.pdf.stream_written |= immediate && obj.stream;
656        self.pdf.objects.insert(n, obj);
657        if immediate {
658            self.pdf_write_object(n);
659        }
660    }
661
662    /// Appends a whatsit to the current list.
663    fn append_whatsit(&mut self, subtype: u16, w: Whatsit) {
664        // Observed: like every node, it carries the current attributes.
665        let p = self.new_node(id::WHATSIT, subtype, NodeData::Whatsit(Box::new(w)));
666        self.tail_append(p);
667    }
668
669    /// Scans `width`, `height`, `depth` keywords (`scan_rule_spec` for
670    /// annotations); unspecified dimensions are running.
671    fn scan_pdf_rule_spec(&mut self) -> [i32; 3] {
672        let mut d = [RUNNING; 3];
673        loop {
674            let i = if self.scan_keyword("width") {
675                0
676            } else if self.scan_keyword("height") {
677                1
678            } else if self.scan_keyword("depth") {
679                2
680            } else {
681                return d;
682            };
683            self.scan_normal_dimen();
684            d[i] = self.cur_val;
685        }
686    }
687
688    /// Reports a fatal backend error the way LuaTeX does and ends the run
689    /// without writing the PDF file.
690    pub fn pdf_fatal(&mut self, msg: &str) -> ! {
691        self.pdf_fatal_in("pdf backend", msg)
692    }
693
694    /// [`Engine::pdf_fatal`] with another category.
695    pub fn pdf_fatal_in(&mut self, category: &str, msg: &str) -> ! {
696        self.normalize_selector();
697        self.printer.print_nl("! error:  (");
698        self.printer.print_str(category);
699        self.printer.print_str("): ");
700        self.printer.print_str(msg);
701        self.printer
702            .print_nl("!  ==> Fatal error occurred, no output PDF file produced!");
703        self.printer.update_terminal();
704        // Observed: LuaTeX stops right there; the log ends without a
705        // newline and the PDF file is gone.
706        self.pdf.fatal = true;
707        self.err.history = crate::error::History::FatalErrorStop;
708        let _ = std::fs::remove_file(self.output_path("pdf"));
709        self.printer.close_log();
710        std::panic::resume_unwind(Box::new(crate::error::JumpOut));
711    }
712
713    /// Prints a backend warning on a line of its own
714    /// (`warning  (pdf backend): …`).
715    pub fn pdf_warning(&mut self, msg: &str) {
716        self.printer.print_nl("warning  (pdf backend): ");
717        self.printer.print_str(msg);
718        self.printer.print_ln();
719    }
720
721    /// Scans an action specification (`user {…}` | `goto …` | `thread …`).
722    fn scan_pdf_action(&mut self) -> Option<PdfAction> {
723        if self.scan_keyword("user") {
724            return Some(PdfAction::User(self.scan_pdf_string()));
725        }
726        let goto = if self.scan_keyword("goto") {
727            true
728        } else if self.scan_keyword("thread") {
729            false
730        } else {
731            self.pdf_fatal("action type missing");
732        };
733        let file = if self.scan_keyword("file") {
734            Some(self.scan_pdf_string())
735        } else {
736            None
737        };
738        // LuaTeX 1.22: `goto struct name {…}` or `struct num n` names the
739        // structure destination of the link.
740        let structure = if goto && self.scan_keyword("struct") {
741            if self.scan_keyword("name") {
742                Some(ActionTarget::Name(self.scan_pdf_string()))
743            } else if self.scan_keyword("num") {
744                self.scan_int();
745                Some(ActionTarget::Num(self.cur_val))
746            } else {
747                self.pdf_fatal("identifier type missing");
748            }
749        } else {
750            None
751        };
752        let target = if goto && self.scan_keyword("page") {
753            self.scan_int();
754            let page = self.cur_val;
755            ActionTarget::Page(page, self.scan_pdf_string())
756        } else if self.scan_keyword("name") {
757            ActionTarget::Name(self.scan_pdf_string())
758        } else if self.scan_keyword("num") {
759            if goto && file.is_some() {
760                self.pdf_fatal("'goto' option cannot be used with both 'file' and 'num'");
761            }
762            self.scan_int();
763            ActionTarget::Num(self.cur_val)
764        } else {
765            self.pdf_fatal("identifier type missing");
766        };
767        let new_window = if self.scan_keyword("newwindow") {
768            Some(true)
769        } else if self.scan_keyword("nonewwindow") {
770            Some(false)
771        } else {
772            None
773        };
774        if new_window.is_some() && !(goto && file.is_some()) {
775            self.pdf_fatal(
776                "'newwindow' or 'nonewwindow' must be used with 'goto' and 'file' option",
777            );
778        }
779        Some(if goto {
780            PdfAction::Goto {
781                file,
782                structure,
783                target,
784                new_window,
785            }
786        } else {
787            PdfAction::Thread { file, target }
788        })
789    }
790
791    /// The PDF text of an action (a dictionary, or the user text),
792    /// allocating the objects of the destinations, pages and threads it
793    /// refers to.
794    pub fn pdf_action_text(&mut self, action: &PdfAction) -> Vec<u8> {
795        fn string(out: &mut Vec<u8>, s: &[u8]) {
796            out.push(b'(');
797            out.extend_from_slice(s);
798            out.push(b')');
799        }
800        let mut d = b"<< ".to_vec();
801        match action {
802            PdfAction::User(u) => return u.clone(),
803            PdfAction::Goto {
804                file: None,
805                target,
806                structure,
807                ..
808            } => {
809                d.extend_from_slice(b"/S /GoTo /D ");
810                match target {
811                    ActionTarget::Name(n) => {
812                        self.pdf_dest_obj(&DestId::Name(n.clone()));
813                        string(&mut d, n);
814                    }
815                    ActionTarget::Num(n) => {
816                        let o = self.pdf_dest_obj(&DestId::Num(*n));
817                        d.extend_from_slice(format!("{o} 0 R").as_bytes());
818                    }
819                    ActionTarget::Page(p, spec) => {
820                        let o = self.pdf_page_ref((*p).max(1));
821                        d.extend_from_slice(format!("[ {o} 0 R ").as_bytes());
822                        d.extend_from_slice(spec);
823                        d.extend_from_slice(b" ]");
824                    }
825                }
826                // Observed: ` /SD n 0 R` after the destination, numbered
827                // after it.
828                let sd = structure.as_ref().map(|s| {
829                    let id = match s {
830                        ActionTarget::Name(n) => DestId::Name(n.clone()),
831                        ActionTarget::Num(n) => DestId::Num(*n),
832                        ActionTarget::Page(..) => DestId::Num(0),
833                    };
834                    self.pdf_struct_dest_obj(&id)
835                });
836                if let Some(o) = sd {
837                    d.extend_from_slice(format!(" /SD {o} 0 R").as_bytes());
838                }
839            }
840            PdfAction::Goto {
841                file: Some(f),
842                target,
843                new_window,
844                ..
845            } => {
846                d.extend_from_slice(b"/F ");
847                string(&mut d, f);
848                d.push(b' ');
849                match new_window {
850                    Some(true) => d.extend_from_slice(b" /NewWindow true"),
851                    Some(false) => d.extend_from_slice(b" /NewWindow false"),
852                    None => {}
853                }
854                match target {
855                    ActionTarget::Name(n) => {
856                        d.extend_from_slice(b" /S /GoToR /D ");
857                        string(&mut d, n);
858                    }
859                    ActionTarget::Page(p, spec) => {
860                        // Observed: /GoTo (not /GoToR), page counted from 0.
861                        d.extend_from_slice(format!(" /S /GoTo /D [{} ", p - 1).as_bytes());
862                        d.extend_from_slice(spec);
863                        d.extend_from_slice(b" ]");
864                    }
865                    ActionTarget::Num(_) => {}
866                }
867            }
868            PdfAction::Thread { file, target } => {
869                if let Some(f) = file {
870                    // Observed: the identifier is dropped.
871                    d.extend_from_slice(b"/F ");
872                    string(&mut d, f);
873                    d.extend_from_slice(b"  /S /Thread");
874                } else {
875                    d.extend_from_slice(b"/S /Thread /D ");
876                    match target {
877                        ActionTarget::Name(n) => {
878                            self.pdf_thread_obj(DestId::Name(n.clone()));
879                            string(&mut d, n);
880                        }
881                        ActionTarget::Num(n) => {
882                            let o = self.pdf_thread_obj(DestId::Num(*n));
883                            d.extend_from_slice(format!("{o} 0 R").as_bytes());
884                        }
885                        ActionTarget::Page(..) => {}
886                    }
887                }
888            }
889        }
890        d.extend_from_slice(b" >>");
891        d
892    }
893
894    /// Writes `body` as object `n` now (actions and titles of outlines,
895    /// the open action).
896    fn pdf_write_raw(&mut self, n: i32, body: Vec<u8>) {
897        let obj = PdfObject {
898            num: n,
899            data: body,
900            defined: true,
901            ..Default::default()
902        };
903        self.pdf_write_object_data(&obj);
904    }
905
906    /// Writes an action object for `action` now and returns its number;
907    /// user actions are written between newlines.
908    fn pdf_write_action(&mut self, action: &PdfAction) -> i32 {
909        let n = self.pdf_alloc_obj(ObjKind::Other);
910        if let Some(mut be) = self.backend.take() {
911            be.begin_object(self);
912            self.backend = Some(be);
913        }
914        let body = match action {
915            PdfAction::User(u) => {
916                let mut b = b"\n".to_vec();
917                b.extend_from_slice(u);
918                b.push(b'\n');
919                b
920            }
921            _ => self.pdf_action_text(action),
922        };
923        self.pdf_write_raw(n, body);
924        n
925    }
926
927    /// Executes `\pdfextension` (`immediate` when prefixed by
928    /// `\immediate`).
929    pub fn pdf_extension(&mut self, immediate: bool) {
930        if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) <= 0 {
931            // Observed: in DVI mode LuaTeX ignores `\pdfextension` without
932            // reading any keyword.
933            return;
934        }
935        if self.scan_keyword("literal") {
936            let late = self.scan_keyword("shipout");
937            let mode = if self.scan_keyword("direct") {
938                literal_mode::DIRECT
939            } else if self.scan_keyword("page") {
940                literal_mode::PAGE
941            } else if self.scan_keyword("raw") {
942                literal_mode::RAW
943            } else {
944                literal_mode::ORIGIN
945            };
946            let data = if late {
947                LateData::Tokens(self.scan_general_text())
948            } else {
949                LateData::Bytes(self.scan_pdf_string())
950            };
951            self.append_whatsit(
952                if late {
953                    whatsit_sub::PDF_LATE_LITERAL
954                } else {
955                    whatsit_sub::PDF_LITERAL
956                },
957                Whatsit::PdfLiteral { mode, data, late },
958            );
959        } else if self.scan_keyword("lateliteral") {
960            let data = LateData::Tokens(self.scan_general_text());
961            self.append_whatsit(
962                whatsit_sub::PDF_LATE_LITERAL,
963                Whatsit::PdfLiteral {
964                    mode: literal_mode::ORIGIN,
965                    data,
966                    late: true,
967                },
968            );
969        } else if self.scan_keyword("save") {
970            self.append_whatsit(whatsit_sub::PDF_SAVE, Whatsit::PdfSave);
971        } else if self.scan_keyword("restore") {
972            self.append_whatsit(whatsit_sub::PDF_RESTORE, Whatsit::PdfRestore);
973        } else if self.scan_keyword("setmatrix") {
974            let data = self.scan_pdf_string();
975            self.append_whatsit(whatsit_sub::PDF_SETMATRIX, Whatsit::PdfSetmatrix { data });
976        } else if self.scan_keyword("colorstack") {
977            self.scan_int();
978            let stack = self.cur_val;
979            let (cmd, data) = if self.scan_keyword("set") {
980                (colorstack_cmd::SET, self.scan_pdf_string())
981            } else if self.scan_keyword("push") {
982                (colorstack_cmd::PUSH, self.scan_pdf_string())
983            } else if self.scan_keyword("pop") {
984                (colorstack_cmd::POP, Vec::new())
985            } else if self.scan_keyword("current") {
986                (colorstack_cmd::CURRENT, Vec::new())
987            } else {
988                self.print_err("Color stack action is missing");
989                self.help(&[
990                    "The expected actions for \\pdfextension colorstack:",
991                    "    set, push, pop, current",
992                ]);
993                self.error();
994                return;
995            };
996            self.append_whatsit(
997                whatsit_sub::PDF_COLORSTACK,
998                Whatsit::PdfColorstack { stack, cmd, data },
999            );
1000        } else if self.scan_keyword("obj") {
1001            self.pdf_obj(immediate);
1002        } else if self.scan_keyword("refobj") {
1003            self.scan_int();
1004            let n = self.cur_val;
1005            if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
1006                self.pdf_check_refobj(n);
1007            }
1008            self.pdf_append_refobj(n);
1009        } else if self.scan_keyword("annot") {
1010            let objnum = if self.scan_keyword("reserveobjnum") {
1011                let n = self.pdf_alloc_obj(ObjKind::Annot);
1012                self.pdf.last_annot = n;
1013                self.scan_optional_space();
1014                return;
1015            } else if self.scan_keyword("useobjnum") {
1016                self.scan_int();
1017                self.cur_val
1018            } else {
1019                self.pdf_alloc_obj(ObjKind::Annot)
1020            };
1021            self.pdf.last_annot = objnum;
1022            let dims = self.scan_pdf_rule_spec();
1023            let data = self.scan_pdf_string();
1024            self.append_whatsit(
1025                whatsit_sub::PDF_ANNOT,
1026                Whatsit::PdfAnnot { dims, objnum, data },
1027            );
1028        } else if self.scan_keyword("startlink") {
1029            if self.mode.abs_kind() == crate::maincontrol::ModeKind::Vertical {
1030                self.pdf_fatal("startlink cannot be used in vertical mode");
1031            }
1032            let dims = self.scan_pdf_rule_spec();
1033            let mut attr = Vec::new();
1034            if self.scan_keyword("attr") {
1035                attr = self.scan_pdf_string();
1036            }
1037            let Some(action) = self.scan_pdf_action() else {
1038                return;
1039            };
1040            let objnum = self.pdf_alloc_obj(ObjKind::Link);
1041            self.pdf.last_link = objnum;
1042            self.append_whatsit(
1043                whatsit_sub::PDF_START_LINK,
1044                Whatsit::PdfStartLink {
1045                    dims,
1046                    objnum,
1047                    attr,
1048                    action,
1049                },
1050            );
1051        } else if self.scan_keyword("endlink") {
1052            if self.mode.abs_kind() == crate::maincontrol::ModeKind::Vertical {
1053                self.pdf_fatal("\\pdfextension endlink cannot be used in vertical mode");
1054            }
1055            self.append_whatsit(whatsit_sub::PDF_END_LINK, Whatsit::PdfEndLink);
1056        } else if self.scan_keyword("dest") {
1057            self.pdf_dest_cmd();
1058        } else if self.scan_keyword("outline") {
1059            self.pdf_outline_cmd();
1060        } else if self.scan_keyword("glyphtounicode") {
1061            let a = self.scan_pdf_string();
1062            let b = self.scan_pdf_string();
1063            self.pdf.glyph_to_unicode.push((a, b));
1064        } else if self.scan_keyword("catalog") {
1065            let s = self.scan_pdf_string();
1066            self.pdf.catalog.extend_from_slice(&s);
1067            if self.scan_keyword("openaction") {
1068                if self.pdf.open_action.is_some() {
1069                    self.pdf_fatal("duplicate of openaction");
1070                }
1071                if let Some(a) = self.scan_pdf_action() {
1072                    let n = self.pdf_write_action(&a);
1073                    self.pdf.open_action = Some(n);
1074                }
1075            }
1076        } else if self.scan_keyword("info") {
1077            let s = self.scan_pdf_string();
1078            self.pdf.info.extend_from_slice(&s);
1079        } else if self.scan_keyword("names") {
1080            let s = self.scan_pdf_string();
1081            self.pdf.names.extend_from_slice(&s);
1082        } else if self.scan_keyword("trailer") {
1083            let s = self.scan_pdf_string();
1084            self.pdf.trailer.extend_from_slice(&s);
1085        } else if self.scan_keyword("fontattr") {
1086            self.scan_font_ident();
1087            let f = self.cur_val as u32;
1088            let s = self.scan_pdf_string();
1089            self.pdf.font_attrs.insert(f, s);
1090        } else if self.scan_keyword("mapfile") {
1091            let s = self.scan_pdf_string();
1092            // Observed: the map file is read at once (`{path}` in the log),
1093            // after the default one for `+` and `=`, and the default one is
1094            // then not announced at the first page. (Reading a file twice
1095            // warns about each duplicate entry; that is not reproduced.)
1096            if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
1097                let text = String::from_utf8_lossy(&s).trim().to_owned();
1098                let prefix = text.chars().next().filter(|c| matches!(c, '+' | '-' | '='));
1099                let name = text.trim_start_matches(['+', '-', '=']);
1100                if prefix != Some('-') {
1101                    if prefix.is_some() && !self.pdf.map_note_done {
1102                        self.print_map_file_note("pdftex.map");
1103                    }
1104                    self.print_map_file_note(name);
1105                    self.pdf.map_note_done = true;
1106                }
1107            }
1108            self.pdf.map_entries.push(s);
1109        } else if self.scan_keyword("mapline") {
1110            let s = self.scan_pdf_string();
1111            self.pdf.map_entries.push(s);
1112        } else if self.scan_keyword("includechars") {
1113            let _ = self.scan_pdf_string();
1114        } else if self.scan_keyword("linkstate") {
1115            self.scan_int();
1116            let state = self.cur_val;
1117            self.append_whatsit(
1118                crate::nodes::whatsit_sub::PDF_LINK_STATE,
1119                Whatsit::PdfLinkState { state },
1120            );
1121        } else if self.scan_keyword("startthread") || self.scan_keyword("thread") {
1122            // Article threads are not written yet; the syntax is accepted.
1123            let _ = self.scan_pdf_rule_spec();
1124            if self.scan_keyword("attr") {
1125                let _ = self.scan_pdf_string();
1126            }
1127            if self.scan_keyword("num") {
1128                self.scan_int();
1129            } else if self.scan_keyword("name") {
1130                let _ = self.scan_pdf_string();
1131            }
1132        } else if self.scan_keyword("endthread") {
1133        } else {
1134            // LuaTeX 1.22: lower case, no help text.
1135            self.print_err("unexpected use of \\pdfextension");
1136            self.help(&[]);
1137            self.error();
1138        }
1139    }
1140
1141    /// `\pdfextension dest …`.
1142    fn pdf_dest_cmd(&mut self) {
1143        let structure = if self.scan_keyword("struct") {
1144            self.scan_int();
1145            Some(self.cur_val)
1146        } else {
1147            None
1148        };
1149        let id = if self.scan_keyword("num") {
1150            self.scan_int();
1151            DestId::Num(self.cur_val)
1152        } else if self.scan_keyword("name") {
1153            DestId::Name(self.scan_pdf_string())
1154        } else {
1155            self.pdf_fatal("identifier type missing");
1156        };
1157        let mut view = DestView {
1158            kind: DestKind::Xyz,
1159            zoom: 0,
1160            dims: [RUNNING; 3],
1161        };
1162        if self.scan_keyword("xyz") {
1163            if self.scan_keyword("zoom") {
1164                self.scan_int();
1165                view.zoom = self.cur_val;
1166            }
1167        } else if self.scan_keyword("fitbh") {
1168            view.kind = DestKind::FitBH;
1169        } else if self.scan_keyword("fitbv") {
1170            view.kind = DestKind::FitBV;
1171        } else if self.scan_keyword("fitb") {
1172            view.kind = DestKind::FitB;
1173        } else if self.scan_keyword("fith") {
1174            view.kind = DestKind::FitH;
1175        } else if self.scan_keyword("fitv") {
1176            view.kind = DestKind::FitV;
1177        } else if self.scan_keyword("fitr") {
1178            view.kind = DestKind::FitR;
1179            view.dims = self.scan_pdf_rule_spec();
1180        } else if self.scan_keyword("fit") {
1181            view.kind = DestKind::Fit;
1182        } else {
1183            self.pdf_fatal("destination type missing");
1184        }
1185        self.append_whatsit(
1186            whatsit_sub::PDF_DEST,
1187            Whatsit::PdfDest {
1188                id,
1189                view,
1190                structure,
1191            },
1192        );
1193    }
1194
1195    /// `\pdfextension outline …`.
1196    fn pdf_outline_cmd(&mut self) {
1197        let mut o = Outline::default();
1198        if self.scan_keyword("attr") {
1199            o.attr = self.scan_pdf_string();
1200        }
1201        if self.scan_keyword("useobjnum") {
1202            self.scan_int();
1203            o.action = self.cur_val;
1204        } else {
1205            let Some(a) = self.scan_pdf_action() else {
1206                return;
1207            };
1208            o.action = self.pdf_write_action(&a);
1209        }
1210        if self.scan_keyword("count") {
1211            self.scan_int();
1212            o.count = self.cur_val;
1213        }
1214        let title = self.scan_pdf_string();
1215        o.objnum = self.pdf_alloc_obj(ObjKind::Other);
1216        o.title = self.pdf_alloc_obj(ObjKind::Other);
1217        let mut body = b"(".to_vec();
1218        body.extend_from_slice(&title);
1219        body.extend_from_slice(b")\n");
1220        self.pdf_write_raw(o.title, body);
1221        self.pdf.outlines.push(o);
1222    }
1223
1224    /// `\pdfextension obj …`.
1225    fn pdf_obj(&mut self, immediate: bool) {
1226        if self.scan_keyword("reserveobjnum") {
1227            let n = self.pdf_alloc_obj(ObjKind::Obj);
1228            self.pdf.objects.insert(
1229                n,
1230                PdfObject {
1231                    num: n,
1232                    ..Default::default()
1233                },
1234            );
1235            self.pdf.last_obj = n;
1236            // Observed: a space after the keyword is skipped.
1237            self.scan_optional_space();
1238            return;
1239        }
1240        let n = if self.scan_keyword("useobjnum") {
1241            self.scan_int();
1242            self.cur_val
1243        } else {
1244            self.pdf_alloc_obj(ObjKind::Obj)
1245        };
1246        let uncompressed = self.scan_keyword("uncompressed");
1247        let mut obj = PdfObject {
1248            num: n,
1249            uncompressed,
1250            defined: true,
1251            ..Default::default()
1252        };
1253        if self.scan_keyword("stream") {
1254            obj.stream = true;
1255            if self.scan_keyword("attr") {
1256                obj.attr = self.scan_pdf_string();
1257            }
1258        }
1259        if self.scan_keyword("file") {
1260            obj.file = true;
1261        }
1262        obj.data = self.scan_pdf_string();
1263        if immediate {
1264            self.pdf.obj_words += 5;
1265        }
1266        self.pdf_define_object(obj, immediate);
1267    }
1268
1269    /// Writes object `n` through the backend now.
1270    pub fn pdf_write_object(&mut self, n: i32) {
1271        let Some(obj) = self.pdf.objects.get(&n).cloned() else {
1272            return;
1273        };
1274        if obj.written || !obj.defined {
1275            return;
1276        }
1277        if let Some(o) = self.pdf.objects.get_mut(&n) {
1278            o.written = true;
1279        }
1280        self.pdf_write_object_data(&obj);
1281    }
1282
1283    /// Hands `obj` to the backend, or queues it while the backend is busy
1284    /// shipping a page. A file object prints `<<name>>` as LuaTeX does.
1285    fn pdf_write_object_data(&mut self, obj: &PdfObject) {
1286        let Some(obj) = self.pdf_object_payload(obj) else {
1287            return;
1288        };
1289        match self.backend.take() {
1290            Some(mut be) => {
1291                be.write_object(self, &obj);
1292                self.backend = Some(be);
1293            }
1294            None => self.pdf.pending.push(obj),
1295        }
1296    }
1297
1298    /// Takes object `n` (defined and not yet written) for writing with the
1299    /// current page (`\pdfextension refobj`).
1300    pub(crate) fn pdf_take_page_object(&mut self, n: i32) -> Option<PdfObject> {
1301        let obj = self.pdf.objects.get_mut(&n)?;
1302        if obj.written || !obj.defined {
1303            return None;
1304        }
1305        obj.written = true;
1306        let obj = obj.clone();
1307        self.pdf_object_payload(&obj)
1308    }
1309
1310    /// The object as written: a file object gets the file's contents
1311    /// (printing `<<name>>` as LuaTeX does).
1312    fn pdf_object_payload(&mut self, obj: &PdfObject) -> Option<PdfObject> {
1313        let mut obj = obj.clone();
1314        if obj.file {
1315            let name = String::from_utf8_lossy(&obj.data).into_owned();
1316            let path = self
1317                .kpse
1318                .as_ref()
1319                .and_then(|k| {
1320                    k.borrow_mut()
1321                        .find_file(&name, retex_kpath::FileFormat::Tex)
1322                })
1323                .unwrap_or_else(|| std::path::PathBuf::from(&name));
1324            match std::fs::read(&path) {
1325                Ok(d) => {
1326                    self.printer.print_str("<<");
1327                    self.printer.print_bytes(name.as_bytes());
1328                    self.printer.print_str(">>");
1329                    obj.data = d;
1330                    obj.file = false;
1331                }
1332                Err(_) => {
1333                    self.print_err("pdf backend: cannot open file for embedding");
1334                    self.help(&[]);
1335                    self.error();
1336                    return None;
1337                }
1338            }
1339        }
1340        Some(obj)
1341    }
1342
1343    /// A token list of whatsit data, for late expansion.
1344    pub fn whatsit_tokens(list: &TokenList) -> LateData {
1345        LateData::Tokens(list.clone())
1346    }
1347
1348    /// Works out what the backend needs at the end of the run, and prints
1349    /// LuaTeX's warnings about destinations and threads that were referred
1350    /// to but never defined.
1351    pub fn pdf_finish_document(&mut self) {
1352        let mut doc = DocItems::default();
1353        // Observed order: missing pages, then destinations (each in the
1354        // reverse order of allocation), then threads; the warnings are
1355        // separated by empty lines.
1356        let mut warnings = Vec::new();
1357        let mut pages: Vec<(i32, i32)> = self
1358            .pdf
1359            .page_refs
1360            .iter()
1361            .filter(|(p, _)| **p > self.total_pages as i32)
1362            .map(|(p, n)| (*n, *p))
1363            .collect();
1364        pages.sort_unstable_by(|a, b| b.cmp(a));
1365        for (_, p) in pages {
1366            warnings.push(format!("page {p} has been referenced but does not exist"));
1367        }
1368        let order = self.pdf.dest_order.clone();
1369        for id in order.iter().rev() {
1370            if let Some(d) = self.pdf.dests.get(id).copied()
1371                && !d.defined
1372            {
1373                warnings.push(format!("unreferenced destination with {}", id.describe()));
1374                doc.missing_dests.push(d.objnum);
1375            }
1376        }
1377        for (id, n) in self.pdf.threads.clone() {
1378            warnings.push(format!("unknown thread destination {}", id.describe()));
1379            let title = match id {
1380                DestId::Name(n) => n,
1381                DestId::Num(k) => k.to_string().into_bytes(),
1382            };
1383            doc.missing_threads.push((n, title));
1384        }
1385        for (i, w) in warnings.iter().enumerate() {
1386            if i > 0 {
1387                self.printer.print_ln();
1388            }
1389            self.pdf_warning(w);
1390        }
1391        let mut names: Vec<(Vec<u8>, i32)> = self
1392            .pdf
1393            .dests
1394            .iter()
1395            .filter_map(|(k, d)| match k {
1396                DestId::Name(n) => Some((n.clone(), d.objnum)),
1397                DestId::Num(_) => None,
1398            })
1399            .collect();
1400        names.sort();
1401        doc.dest_names = names;
1402        let toks = |e: &mut Engine, i: u32| {
1403            let list = e.eqtb.toks_param(i).clone();
1404            e.print_to_string(|e| e.show_token_list(&list, 0, list.len(), i64::MAX))
1405        };
1406        doc.pages_attr = toks(self, crate::backend::PAGES_ATTR);
1407        doc.trailer_id = toks(self, crate::backend::TRAILER_ID);
1408        self.pdf.doc = doc;
1409    }
1410}