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