Skip to main content

retex_engine/
shipout.rs

1//! Shipping pages out (*TeX: The Program* §592–§640, LuaTeX's PDF
2//! backend positions).
3//!
4//! `ship_out` prints the `[n]` progress report, walks the box exactly as
5//! TeX's `hlist_out`/`vlist_out` do (including the cumulative glue
6//! rounding and leaders, §619–§637), executes `\openout`/`\write`/
7//! `\closeout` whatsits, and hands glyphs, rules and literals either to a
8//! [`Backend`] in page coordinates (`\outputmode>0`) or to the
9//! [`crate::dvi`] writer in DVI coordinates (`\outputmode=0`).
10
11use crate::Engine;
12use crate::glue::Glue;
13use crate::nodes::{NodeData, NodeRef, RUNNING, Whatsit, glue_sub, id};
14use crate::primitives_gen::{assign_dimen as dp, assign_int as ip};
15
16/// A destination for shipped pages (the PDF writer). Coordinates are in
17/// scaled points from the lower-left corner of the page, y upwards.
18pub trait Backend {
19    /// Starts a page of the given size.
20    fn begin_page(&mut self, engine: &Engine, width: i32, height: i32);
21    /// Draws character `chr` of font `font` with its origin at (x, y).
22    fn glyph(&mut self, engine: &Engine, font: u32, chr: u32, x: i32, y: i32);
23    /// Draws a glyph expanded horizontally by `ef` millionths of its width
24    /// (font expansion; `ef` is never 0).
25    fn glyph_expanded(&mut self, engine: &Engine, font: u32, chr: u32, x: i32, y: i32, ef: i32) {
26        let _ = ef;
27        self.glyph(engine, font, chr, x, y);
28    }
29    /// Draws a filled rule with its lower-left corner at (x, y).
30    fn rule(&mut self, x: i32, y: i32, width: i32, height: i32);
31    /// Emits a literal (`\pdfextension literal`) at (x, y).
32    fn literal(&mut self, mode: u8, data: &[u8], x: i32, y: i32);
33    /// Finishes the page.
34    fn end_page(&mut self, engine: &Engine);
35    /// Finishes the document.
36    fn finish(&mut self, engine: &mut Engine) -> Option<Finished>;
37
38    /// `\pdfextension save` at (x, y).
39    fn save(&mut self, x: i32, y: i32) {
40        self.literal(0, b"q", x, y);
41    }
42
43    /// `\pdfextension restore` at (x, y).
44    fn restore(&mut self, x: i32, y: i32) {
45        self.literal(0, b"Q", x, y);
46    }
47
48    /// `\pdfextension setmatrix` at (x, y).
49    fn set_matrix(&mut self, x: i32, y: i32, matrix: &[u8]) {
50        let mut d = matrix.to_vec();
51        d.extend_from_slice(b" 0 0 cm");
52        self.literal(0, &d, x, y);
53    }
54
55    /// Before the process forks for a preamble snapshot: writes out what
56    /// is buffered (see [`resume`](Self::resume)).
57    fn suspend(&mut self) -> std::io::Result<()> {
58        Ok(())
59    }
60
61    /// In a process resumed from a preamble snapshot: reopens the output
62    /// file opened before the snapshot, cut back to its length then (the
63    /// forked processes share the file's position otherwise).
64    fn resume(&mut self) -> std::io::Result<()> {
65        Ok(())
66    }
67
68    /// Gives up the output file: with `remove`, it is deleted (after a
69    /// fatal error), otherwise left empty (no pages), as LuaTeX does.
70    fn abandon(&mut self, _remove: bool) {}
71
72    /// Prepares for writing an object whose text is still to be made
73    /// (allocates the object stream it will go into, as LuaTeX does).
74    fn begin_object(&mut self, _engine: &Engine) {}
75
76    /// The object number of the page begun last.
77    fn current_page_object(&self) -> Option<i32> {
78        None
79    }
80
81    /// Writes the page's contents and dictionary: the page attributes and
82    /// resources of `items` are final, and whether there are annotations.
83    fn close_page_contents(&mut self, _engine: &Engine, _items: &crate::pdfext::PageItems) {}
84
85    /// Hands over the page's annotations, links, destinations and
86    /// referenced objects, just before [`end_page`](Self::end_page).
87    fn page_items(&mut self, _engine: &Engine, _items: &crate::pdfext::PageItems) {}
88
89    /// Starts writing box resource `index` (natural size `size` = width,
90    /// height, depth) as a form XObject; drawing goes into the form until
91    /// [`end_box_form`](Self::end_box_form).
92    fn begin_box_form(
93        &mut self,
94        _engine: &Engine,
95        _index: u32,
96        _size: (i32, i32, i32),
97        _attr: &[u8],
98        _resources: &[u8],
99    ) {
100    }
101
102    /// Finishes the box resource being written (its content stream).
103    fn end_box_form(&mut self, _engine: &Engine) {}
104
105    /// Writes the resources of the box resource finished last, after the
106    /// forms it uses.
107    fn close_box_form_resources(&mut self, _engine: &Engine) {}
108
109    /// Writes the resources of the page ended last, after the forms it
110    /// uses.
111    fn close_page_resources(&mut self, _engine: &Engine) {}
112
113    /// Draws box resource `index` with its lower-left corner at (x, y),
114    /// scaled to `width` × `height` (height + depth).
115    fn box_form(
116        &mut self,
117        _engine: &Engine,
118        _index: u32,
119        _x: i32,
120        _y: i32,
121        _width: i32,
122        _height: i32,
123    ) {
124    }
125
126    /// Writes image resource `index` (its object number) now (`\immediate`).
127    fn write_image(&mut self, _engine: &Engine, _index: u32) {}
128
129    /// Draws image resource `index` with its lower-left corner at (x, y),
130    /// `width` wide and `height` (height + depth) tall.
131    fn image(
132        &mut self,
133        _engine: &Engine,
134        _index: u32,
135        _x: i32,
136        _y: i32,
137        _width: i32,
138        _height: i32,
139    ) {
140    }
141
142    /// The images written (when the pages and forms using them were
143    /// closed) since the last call, if the backend writes them that way;
144    /// `None` to have them announced at the end of the page using them.
145    fn written_images(&mut self) -> Option<Vec<u32>> {
146        None
147    }
148
149    /// Writes an object created with `\pdfextension obj`.
150    fn write_object(&mut self, _engine: &Engine, _obj: &crate::pdfext::PdfObject) {}
151}
152
153/// What the backend reports when the document is finished.
154#[derive(Clone, Debug, Default)]
155pub struct Finished {
156    /// The output file name as printed in "Output written on …".
157    pub file: String,
158    /// Its size in bytes.
159    pub size: u64,
160    /// Font files embedded, printed as `<path>` (LuaTeX order).
161    pub embedded: Vec<String>,
162    /// Objects written into object streams, and the number of streams.
163    pub objstm: (u32, u32),
164}
165
166/// The glue-rounding state of a box being shipped (`cur_g`, `cur_glue`).
167struct GlueState {
168    cur_g: i32,
169    cur_glue: f64,
170    order: u8,
171    sign: u8,
172    set: f64,
173}
174
175impl GlueState {
176    /// TeX's glue computation for one glue node: returns its width.
177    fn advance(&mut self, g: &Glue) -> i32 {
178        let mut w = g.width - self.cur_g;
179        if self.sign != 0 {
180            let applies = if self.sign == 1 {
181                g.stretch_order == self.order
182            } else {
183                g.shrink_order == self.order
184            };
185            if applies {
186                if self.sign == 1 {
187                    self.cur_glue += f64::from(g.stretch);
188                } else {
189                    self.cur_glue -= f64::from(g.shrink);
190                }
191                let t = (self.set * self.cur_glue).clamp(-1_000_000_000.0, 1_000_000_000.0);
192                self.cur_g = t.round() as i32;
193            }
194        }
195        w += self.cur_g;
196        w
197    }
198}
199
200impl Engine {
201    /// Calls the page-number callback `name` if one is registered; whether
202    /// it was.
203    fn page_number_callback(&mut self, name: &str) -> bool {
204        let Some(lua) = self.lua.clone() else {
205            return false;
206        };
207        if !lua.has_callback(name) {
208            return false;
209        }
210        if let Err(e) = lua.call_simple(self, name) {
211            self.lua_error(&e);
212        }
213        true
214    }
215
216    /// Ships box `p` out (`ship_out`).
217    pub fn ship_out(&mut self, p: NodeRef) {
218        self.pdf.page_annots.clear();
219        let tracing = self.eqtb.int(ip::TRACINGOUTPUT) > 0;
220        if tracing {
221            self.printer.print_nl("");
222            self.printer.print_ln();
223            self.printer.print_str("Completed box being shipped out");
224        }
225        // The `start_page_number` callback replaces ` [` and the page
226        // numbers, `stop_page_number` the closing `]` (observed).
227        if !self.page_number_callback("start_page_number") {
228            if self.printer.term_offset > self.printer.max_print_line.saturating_sub(9) {
229                self.printer.print_ln();
230            } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
231                self.printer.print_char(' ' as u32);
232            }
233            self.printer.print_char('[' as u32);
234            let mut j = 9;
235            while j > 0 && self.eqtb.count[j].val == 0 {
236                j -= 1;
237            }
238            for k in 0..=j {
239                self.printer.print_int(self.eqtb.count[k].val.into());
240                if k < j {
241                    self.printer.print_char('.' as u32);
242                }
243            }
244        }
245        self.printer.update_terminal();
246        if p.is_null() {
247            // `tex.shipout` of a void box (observed).
248            self.luatex_warning(
249                "backend",
250                "pagedir differs from bodydir, the output may be placed wrongly on the page",
251            );
252            let msg = "no vlist or hlist in (xform) shipout";
253            if self.eqtb.int(ip::OUTPUTMODE) > 0 {
254                self.pdf_fatal(msg);
255            }
256            self.luatex_fatal("pdf backend", msg);
257        }
258        if tracing {
259            if !self.page_number_callback("stop_page_number") {
260                self.printer.print_char(']' as u32);
261            }
262            self.begin_diagnostic();
263            self.show_box(p);
264            self.end_diagnostic(true);
265        }
266        self.ship_box(p);
267        if !tracing && !self.page_number_callback("stop_page_number") {
268            self.printer.print_char(']' as u32);
269        }
270        self.dead_cycles = 0;
271        self.printer.update_terminal();
272        self.nodes.flush_list(p);
273    }
274
275    /// Ships the contents of box `p`.
276    fn ship_box(&mut self, p: NodeRef) {
277        let Some(b) = self.nodes.box_data(p).cloned() else {
278            return;
279        };
280        let pdf = self.eqtb.int(ip::OUTPUTMODE) > 0;
281        // Page size and origin.
282        let h_origin = self.eqtb.dimen(crate::backend::H_ORIGIN);
283        let v_origin = self.eqtb.dimen(crate::backend::V_ORIGIN);
284        let h_off = self.eqtb.dimen(dp::HOFFSET);
285        let v_off = self.eqtb.dimen(dp::VOFFSET);
286        let mut width = self.eqtb.dimen(dp::PAGEWIDTH);
287        let mut height = self.eqtb.dimen(dp::PAGEHEIGHT);
288        // Observed: without \pagewidth (\pageheight), the page is the box
289        // plus 1in and the offset on each side, whatever the origin.
290        const ONE_INCH: i32 = 4_736_286;
291        if width <= 0 {
292            width = b.width + 2 * (ONE_INCH + h_off);
293        }
294        if height <= 0 {
295            height = b.height + b.depth + 2 * (ONE_INCH + v_off);
296        }
297        let mut backend = if pdf { self.backend.take() } else { None };
298        let forms_depth = self.pending_forms.len();
299        self.pending_forms.push(Vec::new());
300        if let Some(be) = backend.as_mut() {
301            self.pdf.page = Default::default();
302            be.begin_page(self, width, height);
303            if let Some(n) = be.current_page_object() {
304                let page = self.total_pages as i32 + 1;
305                self.pdf.page_refs.insert(page, n);
306                self.pdf.kinds.insert(n, crate::pdfext::ObjKind::Page);
307            }
308            // Observed: written at the page box's reference point.
309            let (x, y) = (h_origin + h_off, height - (v_origin + v_off + b.height));
310            for (mode, lit) in self.page_colorstack_literals() {
311                be.literal(mode, &lit, x, y);
312            }
313        }
314        let dvi = if pdf {
315            None
316        } else {
317            let mut d = std::mem::take(&mut self.dvi);
318            d.begin_page(self, (b.width, b.height, b.depth), (h_off, v_off));
319            Some(d)
320        };
321        let sync = self.synctex_recording();
322        if sync {
323            self.synctex_begin_page((h_origin + h_off, v_origin + v_off));
324        }
325        let mut walker = Walker {
326            backend,
327            dvi,
328            h_origin: h_origin + h_off,
329            v_origin: v_origin + v_off,
330            page_height: height,
331            dvi_h: h_off,
332            dvi_v: v_off,
333            cur_s: -1,
334            doing_leaders: false,
335            page: true,
336            sync,
337        };
338        let cur_h = 0;
339        let cur_v = b.height;
340        if self.nodes.id(p) == id::VLIST {
341            self.vlist_out(&mut walker, p, cur_h, cur_v);
342        } else {
343            self.hlist_out(&mut walker, p, cur_h, cur_v);
344        }
345        if let Some(mut be) = walker.backend.take() {
346            self.flush_pending_objects(be.as_mut());
347            self.collect_page_texts();
348            let items = std::mem::take(&mut self.pdf.page);
349            be.close_page_contents(self, &items);
350            self.pdf.page = items;
351            self.resolve_link_actions();
352            let items = std::mem::take(&mut self.pdf.page);
353            be.page_items(self, &items);
354            // Observed order: the page's own objects, the forms it uses,
355            // its resources.
356            be.end_page(self);
357            let mut be = self.write_pending_forms(be);
358            be.close_page_resources(self);
359            self.flush_pending_objects(be.as_mut());
360            self.backend = Some(be);
361            let page_images = std::mem::take(&mut self.pdf.page_images);
362            let written = self.backend.as_mut().and_then(|b| b.written_images());
363            for n in written.unwrap_or(page_images) {
364                self.print_image_name(n);
365            }
366        }
367        self.pending_forms.truncate(forms_depth);
368        if let Some(mut d) = walker.dvi.take() {
369            d.end_page();
370            self.dvi = d;
371        }
372        if sync {
373            self.synctex_end_page();
374        }
375        self.total_pages += 1;
376        if let Some(f) = self.progress_hook {
377            f(self);
378        }
379    }
380
381    /// Writes the objects queued while the backend was busy.
382    fn flush_pending_objects(&mut self, be: &mut dyn Backend) {
383        for obj in std::mem::take(&mut self.pdf.pending) {
384            be.write_object(self, &obj);
385        }
386    }
387
388    /// Prints LuaTeX's note about reading font map file `name`
389    /// (`{/path/pdftex.map}`), when it exists.
390    pub(crate) fn print_map_file_note(&mut self, name: &str) {
391        let path = self
392            .kpse
393            .as_ref()
394            .and_then(|k| k.borrow_mut().find_file(name, retex_kpath::FileFormat::Map));
395        if let Some(path) = path {
396            // Printed like a C string (log lines up to 80 characters).
397            let mut text = b"{".to_vec();
398            text.extend_from_slice(path.to_string_lossy().as_bytes());
399            text.push(b'}');
400            self.printer.print_lua_bytes(&text);
401        }
402    }
403
404    /// Outputs a glyph with its reference point at TeX position (`h`,
405    /// `v`).
406    fn out_glyph(&self, w: &mut Walker, font: u32, chr: u32, h: i32, v: i32, ef: i32) {
407        self.out_glyph_at_depth(w, font, chr, h, v, ef, 0);
408    }
409
410    /// [`out_glyph`](Self::out_glyph) inside `depth` virtual characters.
411    #[allow(clippy::too_many_arguments)]
412    fn out_glyph_at_depth(
413        &self,
414        w: &mut Walker,
415        font: u32,
416        chr: u32,
417        h: i32,
418        v: i32,
419        ef: i32,
420        depth: u32,
421    ) {
422        if let Some(vf) = &self.font(font).vf
423            && let Some(cmds) = vf.packets.get(&chr)
424        {
425            if depth < 8 {
426                self.out_packet(w, vf, cmds, h, v, ef, depth);
427            }
428            return;
429        }
430        let (x, y) = w.pos(h, v);
431        // Observed: a character missing from its font is not output.
432        if let Some(be) = w.backend.as_deref_mut()
433            && self.font(font).chars.contains_key(&chr)
434        {
435            if ef == 0 {
436                be.glyph(self, font, chr, x, y);
437            } else {
438                be.glyph_expanded(self, font, chr, x, y, ef);
439            }
440        }
441        if let Some(d) = w.dvi.as_mut()
442            && self.font(font).chars.contains_key(&chr)
443        {
444            let wd = crate::expansion::expanded_width(self.font_char_dimen(font, chr, 0), ef);
445            d.glyph(self, font, chr, w.dvi_h + h, w.dvi_v + v, wd);
446        }
447    }
448
449    /// Runs the packet of a virtual character whose reference point is at
450    /// (`h`, `v`): characters of the local fonts and rules are output at
451    /// the positions the packet moves to (in DVI as in PDF, LuaTeX writes
452    /// the local fonts' characters, not the virtual one). Specials in
453    /// packets are dropped, as LuaTeX does.
454    #[allow(clippy::too_many_arguments)]
455    fn out_packet(
456        &self,
457        w: &mut Walker,
458        vf: &crate::fonts::VirtualFont,
459        cmds: &[retex_font::vf::Command],
460        mut h: i32,
461        mut v: i32,
462        ef: i32,
463        depth: u32,
464    ) {
465        use retex_font::vf::Command;
466        let mut f = vf.fonts.first().copied().unwrap_or(0);
467        let mut stack: Vec<(i32, i32)> = Vec::new();
468        for c in cmds {
469            match c {
470                Command::Font(i) => f = vf.fonts.get(*i).copied().unwrap_or(0),
471                Command::Char(c) => {
472                    if f != 0 && self.font(f).chars.contains_key(c) {
473                        self.out_glyph_at_depth(w, f, *c, h, v, ef, depth + 1);
474                        h = h.wrapping_add(crate::expansion::expanded_width(
475                            self.font_char_dimen(f, *c, 0),
476                            ef,
477                        ));
478                    }
479                }
480                Command::Rule { height, width } => {
481                    if *height > 0 && *width > 0 {
482                        Self::out_rule(w, h, v, *width, *height);
483                    }
484                    h = h.wrapping_add(*width);
485                }
486                Command::Right(x) => h = h.wrapping_add(*x),
487                Command::Down(y) => v = v.wrapping_add(*y),
488                Command::Push => stack.push((h, v)),
489                Command::Pop => {
490                    if let Some((sh, sv)) = stack.pop() {
491                        (h, v) = (sh, sv);
492                    }
493                }
494                Command::Special(_) => {}
495            }
496        }
497    }
498
499    /// Outputs a rule with its lower left corner at TeX position (`h`,
500    /// `v`).
501    fn out_rule(w: &mut Walker, h: i32, v: i32, width: i32, height: i32) {
502        let (x, y) = w.pos(h, v);
503        if let Some(be) = w.backend.as_deref_mut() {
504            be.rule(x, y, width, height);
505        }
506        if let Some(d) = w.dvi.as_mut() {
507            d.rule(w.dvi_h + h, w.dvi_v + v, width, height);
508        }
509    }
510
511    /// Draws box resource `index` with its lower-left corner at (`h`, `v`).
512    /// A form not written yet is written after the page or form being
513    /// written (observed object numbering of LuaTeX).
514    fn out_box_resource(
515        &mut self,
516        w: &mut Walker,
517        index: i32,
518        h: i32,
519        v: i32,
520        width: i32,
521        height: i32,
522    ) {
523        let n = index as u32;
524        let (x, y) = w.pos(h, v);
525        let Some(mut be) = w.backend.take() else {
526            return;
527        };
528        if self
529            .box_resource_slot(n)
530            .is_some_and(|i| !self.box_resources[i].written)
531        {
532            match self.pending_forms.last_mut() {
533                Some(p) => {
534                    if !p.contains(&n) {
535                        p.push(n);
536                    }
537                }
538                None => be = self.write_box_resource(be, n),
539            }
540        }
541        be.box_form(self, n, x, y, width, height);
542        w.backend = Some(be);
543    }
544
545    /// Writes box resource `n` as a form: ships its box into the form with
546    /// the box's reference point at (0, depth), then the forms it uses that
547    /// are not written yet, then its resources.
548    pub fn write_box_resource(&mut self, be: Box<dyn Backend>, n: u32) -> Box<dyn Backend> {
549        let Some(slot) = self.box_resource_slot(n) else {
550            return be;
551        };
552        let r = self.box_resources[slot].clone();
553        if r.written {
554            return be;
555        }
556        // Observed: a form is shipped out like a page, so with
557        // \tracingoutput positive the line is shown (but neither the
558        // page number nor the box).
559        if self.eqtb.int(ip::TRACINGOUTPUT) > 0 {
560            self.printer.print_nl("");
561            self.printer.print_ln();
562            self.printer.print_str("Completed box being shipped out");
563        }
564        let mut be = be;
565        let (bw, bh, bd) = r.size;
566        self.pending_forms.push(Vec::new());
567        self.form_objects.push(Vec::new());
568        be.begin_box_form(self, n, (bw, bh, bd), &r.attr, &r.resources);
569        let sync = self.synctex_recording();
570        if sync {
571            self.synctex_begin_form(bh + bd);
572        }
573        let mut walker = Walker {
574            backend: Some(be),
575            dvi: None,
576            h_origin: 0,
577            v_origin: 0,
578            page_height: bh + bd,
579            dvi_h: 0,
580            dvi_v: 0,
581            cur_s: -1,
582            doing_leaders: false,
583            page: false,
584            sync,
585        };
586        if self.nodes.id(r.list) == id::VLIST {
587            self.vlist_out(&mut walker, r.list, 0, bh);
588        } else {
589            self.hlist_out(&mut walker, r.list, 0, bh);
590        }
591        let mut be = walker.backend.take().expect("the walker keeps the backend");
592        be.end_box_form(self);
593        self.box_resources[slot].written = true;
594        for obj in self.form_objects.pop().unwrap_or_default() {
595            be.write_object(self, &obj);
596        }
597        be = self.write_pending_forms(be);
598        // Observed: the forms this one uses are written inside its sheet.
599        if sync {
600            self.synctex_end_form();
601        }
602        be.close_box_form_resources(self);
603        // Observed: an image is announced when it is written, after the
604        // first form or page using it.
605        for n in be.written_images().unwrap_or_default() {
606            self.print_image_name(n);
607        }
608        be
609    }
610
611    /// Writes the forms used by the page or form just finished that are
612    /// not written yet.
613    fn write_pending_forms(&mut self, mut be: Box<dyn Backend>) -> Box<dyn Backend> {
614        for n in self.pending_forms.pop().unwrap_or_default() {
615            be = self.write_box_resource(be, n);
616        }
617        be
618    }
619
620    /// Draws image `index` with its lower-left corner at (`h`, `v`).
621    fn out_image(&mut self, w: &mut Walker, index: i32, h: i32, v: i32, width: i32, height: i32) {
622        let (x, y) = w.pos(h, v);
623        if let Some(be) = w.backend.as_deref_mut() {
624            be.image(self, index as u32, x, y, width, height);
625            if self.mark_image_written(index as u32) {
626                self.pdf.page_images.push(index as u32);
627            }
628        }
629    }
630
631    /// Outputs box `p` (`hlist_out` or `vlist_out`) with its reference
632    /// point at (`h`, `v`).
633    fn out_box(&mut self, w: &mut Walker, p: NodeRef, h: i32, v: i32) {
634        // A box that contains itself (made by Lua code) is not followed
635        // forever (LuaTeX crashes).
636        if w.cur_s >= crate::nodes::MAX_NESTING as i32 {
637            return;
638        }
639        if self.nodes.id(p) == id::VLIST {
640            self.vlist_out(w, p, h, v);
641        } else {
642            self.hlist_out(w, p, h, v);
643        }
644    }
645
646    /// Enters a box: `cur_s` goes up and nested boxes get a DVI `push`.
647    fn enter_box(w: &mut Walker) {
648        w.cur_s += 1;
649        if let Some(d) = w.dvi.as_mut() {
650            if w.cur_s > 0 {
651                d.push(w.cur_s);
652            } else {
653                d.note_depth(w.cur_s);
654            }
655        }
656    }
657
658    /// Leaves a box (`dvi_pop`).
659    fn leave_box(w: &mut Walker) {
660        if w.cur_s > 0
661            && let Some(d) = w.dvi.as_mut()
662        {
663            d.pop();
664        }
665        w.cur_s -= 1;
666    }
667
668    /// Outputs an hlist box (`hlist_out`); (`cur_h`, `cur_v`) is the box's
669    /// reference point in TeX coordinates (v downwards).
670    fn hlist_out(&mut self, w: &mut Walker, this_box: NodeRef, mut cur_h: i32, cur_v: i32) {
671        let Some(b) = self.nodes.box_data(this_box).cloned() else {
672            return;
673        };
674        let mut gs = GlueState {
675            cur_g: 0,
676            cur_glue: 0.0,
677            order: b.glue_order,
678            sign: b.glue_sign,
679            set: b.glue_set,
680        };
681        Self::enter_box(w);
682        let base_line = cur_v;
683        let left_edge = cur_h;
684        let ctx = crate::pdfship::BoxCtx {
685            left: left_edge,
686            base: base_line,
687            width: b.width,
688            height: b.height,
689            depth: b.depth,
690        };
691        if !w.doing_leaders {
692            self.ship_continue_links(w, &ctx);
693        }
694        if w.sync {
695            let f = self.sync_of(this_box);
696            self.synctex
697                .box_open(true, f, cur_h, cur_v, [b.width, b.height, b.depth]);
698        }
699        let mut p = b.head;
700        while !p.is_null() {
701            let next = self.nodes.next(p);
702            if w.sync {
703                self.sync_hlist_node(p, cur_h, base_line);
704            }
705            let n = self.nodes.get(p);
706            let n_id = n.id;
707            match &n.data {
708                NodeData::Glyph(g) => {
709                    let (font, chr, xo, yo, ef) =
710                        (g.font, g.chr, g.xoffset, g.yoffset, g.expansion_factor);
711                    if !self.pdf.map_note_done
712                        && w.backend.is_some()
713                        && self.fonts.char_metrics(font, chr).is_some()
714                    {
715                        // Observed: LuaTeX reads the map file when the
716                        // first character is output; a character missing
717                        // from its font (in \nullfont, say) is not.
718                        self.pdf.map_note_done = true;
719                        self.print_map_file_note("pdftex.map");
720                    }
721                    if self.fonts.char_metrics(font, chr).is_none() {
722                        self.shipout_lost_char(font, chr);
723                    }
724                    self.out_glyph(w, font, chr, cur_h + xo, base_line - yo, ef);
725                    cur_h +=
726                        crate::expansion::expanded_width(self.font_char_dimen(font, chr, 0), ef);
727                }
728                NodeData::Box(inner) => {
729                    let (iw, shift, head) = (inner.width, inner.shift, inner.head);
730                    if !head.is_null() {
731                        self.out_box(w, p, cur_h, base_line + shift);
732                    } else if w.sync {
733                        let size = [iw, inner.height, inner.depth];
734                        let f = self.sync_of(p);
735                        self.synctex
736                            .void_box(n_id == id::HLIST, f, cur_h, base_line, size);
737                    }
738                    cur_h += iw;
739                }
740                NodeData::Rule {
741                    width,
742                    depth,
743                    height,
744                    index,
745                    ..
746                } => {
747                    let (wd, ht, dpt, idx) = (*width, *height, *depth, *index);
748                    if n.subtype == crate::image::IMAGE_RULE {
749                        self.out_image(w, idx, cur_h, base_line + dpt, wd, ht + dpt);
750                    } else if n.subtype == crate::image::BOX_RESOURCE_RULE {
751                        self.out_box_resource(w, idx, cur_h, base_line + dpt, wd, ht + dpt);
752                    } else {
753                        Self::hlist_rule(w, cur_h, base_line, wd, ht, dpt, &b);
754                    }
755                    cur_h += wd;
756                    if w.sync {
757                        let f = self.sync_of(p);
758                        self.synctex.rule(f, cur_h, base_line, [wd, ht, dpt]);
759                    }
760                }
761                NodeData::Glue { spec, leader } => {
762                    let (spec, leader, sub) = (*spec, *leader, n.subtype);
763                    let wd = gs.advance(&spec);
764                    let box_leaders = sub >= glue_sub::A_LEADERS
765                        && !leader.is_null()
766                        && self.nodes.id(leader) != id::RULE;
767                    if sub >= glue_sub::A_LEADERS && !leader.is_null() {
768                        self.hlist_leaders(w, leader, sub, cur_h, left_edge, base_line, wd, &b);
769                    }
770                    cur_h += wd;
771                    if w.sync && !box_leaders {
772                        let f = self.sync_of(p);
773                        self.synctex.glue(f, cur_h, base_line);
774                    }
775                }
776                NodeData::Kern {
777                    kern,
778                    expansion_factor,
779                } => {
780                    let wd = kern + expansion_factor;
781                    if w.sync {
782                        // Observed: the width recorded is the kern's,
783                        // without font expansion.
784                        let f = self.sync_of(p);
785                        self.synctex.kern(f, *kern);
786                    }
787                    cur_h += wd;
788                }
789                NodeData::Math { surround, glue } => {
790                    let g = *glue;
791                    if w.sync {
792                        let f = self.sync_of(p);
793                        self.synctex.math(f, cur_h, base_line);
794                    }
795                    cur_h += surround;
796                    if g != Glue::ZERO {
797                        cur_h += gs.advance(&g);
798                    }
799                }
800                NodeData::MarginKern { width, .. } => cur_h += width,
801                NodeData::Whatsit(_) => self.out_what(w, p, cur_h, base_line, &ctx),
802                NodeData::Disc { replace, .. } => {
803                    // A discretionary that was not broken shows its
804                    // replacement text.
805                    let r = *replace;
806                    let mut q = r;
807                    while !q.is_null() {
808                        if let NodeData::Glyph(g) = &self.nodes.get(q).data {
809                            let (font, chr, ef) = (g.font, g.chr, g.expansion_factor);
810                            self.out_glyph(w, font, chr, cur_h, base_line, ef);
811                            if w.sync {
812                                self.synctex.glyph();
813                            }
814                            cur_h += crate::expansion::expanded_width(
815                                self.font_char_dimen(font, chr, 0),
816                                ef,
817                            );
818                        } else if let NodeData::Kern {
819                            kern,
820                            expansion_factor,
821                        } = &self.nodes.get(q).data
822                        {
823                            let wd = kern + expansion_factor;
824                            if w.sync {
825                                self.synctex.end_run(cur_h, base_line);
826                                let f = self.sync_of(q);
827                                self.synctex.kern(f, *kern);
828                            }
829                            cur_h += wd;
830                        }
831                        q = self.nodes.next(q);
832                    }
833                }
834                _ => {}
835            }
836            p = next;
837        }
838        if !w.doing_leaders {
839            self.ship_end_box_links(w, &ctx);
840        }
841        if w.sync {
842            self.synctex.end_run(cur_h, base_line);
843            let f = self.sync_of(this_box);
844            self.synctex.box_close(true, f, cur_h, base_line);
845        }
846        Self::leave_box(w);
847    }
848
849    /// SyncTeX before node `p` of an hlist: a glyph continues a run of
850    /// glyphs, the nodes that do not end one leave it, others end it.
851    #[inline]
852    fn sync_hlist_node(&mut self, p: NodeRef, cur_h: i32, base_line: i32) {
853        match self.nodes.id(p) {
854            id::GLYPH => self.synctex.glyph(),
855            id::MARK | id::INS | id::ADJUST | id::BOUNDARY | id::LOCAL_PAR => {}
856            _ => self.synctex.end_run(cur_h, base_line),
857        }
858    }
859
860    /// A rule in an hlist whose left end is at `cur_h` (TeX's `fin_rule`
861    /// in §624); running dimensions come from the enclosing box.
862    fn hlist_rule(
863        w: &mut Walker,
864        cur_h: i32,
865        base_line: i32,
866        wd: i32,
867        ht: i32,
868        dpt: i32,
869        this_box: &crate::nodes::BoxData,
870    ) {
871        let ht = if ht == RUNNING { this_box.height } else { ht };
872        let dpt = if dpt == RUNNING { this_box.depth } else { dpt };
873        let total = ht + dpt;
874        if total > 0 && wd > 0 {
875            Self::out_rule(w, cur_h, base_line + dpt, wd, total);
876        }
877    }
878
879    /// Outputs a vlist box (`vlist_out`).
880    fn vlist_out(&mut self, w: &mut Walker, this_box: NodeRef, cur_h: i32, cur_v: i32) {
881        let Some(b) = self.nodes.box_data(this_box).cloned() else {
882            return;
883        };
884        let mut gs = GlueState {
885            cur_g: 0,
886            cur_glue: 0.0,
887            order: b.glue_order,
888            sign: b.glue_sign,
889            set: b.glue_set,
890        };
891        Self::enter_box(w);
892        let left_edge = cur_h;
893        let ctx = crate::pdfship::BoxCtx {
894            left: left_edge,
895            base: cur_v,
896            width: b.width,
897            height: b.height,
898            depth: b.depth,
899        };
900        let mut cur_v = cur_v - b.height;
901        let top_edge = cur_v;
902        if w.sync {
903            let f = self.sync_of(this_box);
904            self.synctex
905                .box_open(false, f, cur_h, cur_v, [b.width, b.height, b.depth]);
906        }
907        let mut p = b.head;
908        while !p.is_null() {
909            let next = self.nodes.next(p);
910            let n = self.nodes.get(p);
911            match &n.data {
912                NodeData::Box(inner) => {
913                    let (ih, idp, shift, head) =
914                        (inner.height, inner.depth, inner.shift, inner.head);
915                    if head.is_null() {
916                        cur_v += ih + idp;
917                        if w.sync {
918                            let size = [inner.width, ih, idp];
919                            let hlist = n.id == id::HLIST;
920                            let f = self.sync_of(p);
921                            self.synctex.void_box(hlist, f, left_edge, cur_v, size);
922                        }
923                    } else {
924                        cur_v += ih;
925                        self.out_box(w, p, left_edge + shift, cur_v);
926                        cur_v += idp;
927                    }
928                }
929                NodeData::Rule {
930                    width,
931                    depth,
932                    height,
933                    index,
934                    ..
935                } => {
936                    let wd = if *width == RUNNING { b.width } else { *width };
937                    let total = height + depth;
938                    let idx = *index;
939                    cur_v += total;
940                    if n.subtype == crate::image::IMAGE_RULE {
941                        self.out_image(w, idx, left_edge, cur_v, wd, total);
942                    } else if n.subtype == crate::image::BOX_RESOURCE_RULE {
943                        self.out_box_resource(w, idx, left_edge, cur_v, wd, total);
944                    } else if total > 0 && wd > 0 {
945                        Self::out_rule(w, left_edge, cur_v, wd, total);
946                    }
947                }
948                NodeData::Glue { spec, leader } => {
949                    let (spec, leader, sub) = (*spec, *leader, n.subtype);
950                    let ht = gs.advance(&spec);
951                    if sub >= glue_sub::A_LEADERS && !leader.is_null() {
952                        cur_v =
953                            self.vlist_leaders(w, leader, sub, cur_v, left_edge, top_edge, ht, &b);
954                    } else {
955                        cur_v += ht;
956                    }
957                }
958                NodeData::Kern { kern, .. } => cur_v += kern,
959                NodeData::Whatsit(_) => self.out_what(w, p, left_edge, cur_v, &ctx),
960                _ => {}
961            }
962            p = next;
963        }
964        if w.sync {
965            let f = self.sync_of(this_box);
966            self.synctex.box_close(false, f, left_edge, cur_v);
967        }
968        Self::leave_box(w);
969    }
970
971    /// Where leaders of the given kind start and how much space follows
972    /// each copy (§626, §635): returns (start, gap) for a space of size
973    /// `size` at `cur` filled with boxes of size `step`.
974    fn leader_start(sub: u16, cur: i32, edge0: i32, size: i32, step: i32) -> (i32, i32) {
975        if sub == glue_sub::C_LEADERS || sub == glue_sub::X_LEADERS {
976            let lq = size / step;
977            let lr = size % step;
978            if sub == glue_sub::C_LEADERS {
979                (cur + lr / 2, 0)
980            } else {
981                let lx = lr / (lq + 1);
982                (cur + (lr - (lq - 1) * lx) / 2, lx)
983            }
984        } else {
985            let mut c = edge0 + step * ((cur - edge0) / step);
986            if c < cur {
987                c += step;
988            }
989            (c, 0)
990        }
991    }
992
993    /// Leaders in an hlist (§625–§628) filling `wd` from `cur_h`.
994    #[allow(clippy::too_many_arguments)]
995    fn hlist_leaders(
996        &mut self,
997        w: &mut Walker,
998        leader: NodeRef,
999        sub: u16,
1000        cur_h: i32,
1001        left_edge: i32,
1002        base_line: i32,
1003        wd: i32,
1004        this_box: &crate::nodes::BoxData,
1005    ) {
1006        let lb = match &self.nodes.get(leader).data {
1007            NodeData::Rule { height, depth, .. } => {
1008                let (ht, dpt) = (*height, *depth);
1009                Self::hlist_rule(w, cur_h, base_line, wd, ht, dpt, this_box);
1010                return;
1011            }
1012            NodeData::Box(lb) => lb.clone(),
1013            _ => return,
1014        };
1015        let leader_wd = lb.width;
1016        if leader_wd <= 0 || wd <= 0 {
1017            return;
1018        }
1019        let rule_wd = wd + 10; // compensates for floating-point rounding
1020        let edge = cur_h + rule_wd;
1021        let (mut h, lx) = Self::leader_start(sub, cur_h, left_edge, rule_wd, leader_wd);
1022        // The box is output even if it is empty, which counts towards the
1023        // DVI stack depth (§628).
1024        while h + leader_wd <= edge {
1025            let outer = w.doing_leaders;
1026            w.doing_leaders = true;
1027            self.out_box(w, leader, h, base_line + lb.shift);
1028            w.doing_leaders = outer;
1029            h += leader_wd + lx;
1030        }
1031    }
1032
1033    /// Leaders in a vlist (§634–§637) filling `ht` below `cur_v`; returns
1034    /// the new `cur_v`.
1035    #[allow(clippy::too_many_arguments)]
1036    fn vlist_leaders(
1037        &mut self,
1038        w: &mut Walker,
1039        leader: NodeRef,
1040        sub: u16,
1041        cur_v: i32,
1042        left_edge: i32,
1043        top_edge: i32,
1044        ht: i32,
1045        this_box: &crate::nodes::BoxData,
1046    ) -> i32 {
1047        let lb = match &self.nodes.get(leader).data {
1048            NodeData::Rule { width, .. } => {
1049                let wd = if *width == RUNNING {
1050                    this_box.width
1051                } else {
1052                    *width
1053                };
1054                if ht > 0 && wd > 0 {
1055                    Self::out_rule(w, left_edge, cur_v + ht, wd, ht);
1056                }
1057                return cur_v + ht;
1058            }
1059            NodeData::Box(lb) => lb.clone(),
1060            _ => return cur_v + ht,
1061        };
1062        let leader_ht = lb.height + lb.depth;
1063        if leader_ht <= 0 || ht <= 0 {
1064            return cur_v + ht;
1065        }
1066        let rule_ht = ht + 10; // compensates for floating-point rounding
1067        let edge = cur_v + rule_ht;
1068        let (mut v, lx) = Self::leader_start(sub, cur_v, top_edge, rule_ht, leader_ht);
1069        while v + leader_ht <= edge {
1070            let outer = w.doing_leaders;
1071            w.doing_leaders = true;
1072            self.out_box(w, leader, left_edge + lb.shift, v + lb.height);
1073            w.doing_leaders = outer;
1074            v += leader_ht + lx;
1075        }
1076        edge - 10
1077    }
1078
1079    /// Executes a whatsit at TeX position (`h`, `v`) (`out_what`).
1080    fn out_what(
1081        &mut self,
1082        w: &mut Walker,
1083        p: NodeRef,
1084        h: i32,
1085        v: i32,
1086        ctx: &crate::pdfship::BoxCtx,
1087    ) {
1088        let NodeData::Whatsit(wh) = &self.nodes.get(p).data else {
1089            return;
1090        };
1091        let (x, y) = w.pos(h, v);
1092        match (**wh).clone() {
1093            Whatsit::Open { stream, name } if !w.doing_leaders => {
1094                self.open_write_stream(stream as i32, &name)
1095            }
1096            Whatsit::Write { stream, data } if !w.doing_leaders => self.write_out(stream, &data),
1097            Whatsit::Close { stream } if !w.doing_leaders => self.close_write_stream(stream as i32),
1098            Whatsit::Special { data } => {
1099                if let Some(d) = w.dvi.as_mut() {
1100                    d.special(w.dvi_h + h, w.dvi_v + v, &data);
1101                } else if let Some(be) = w.backend.as_deref_mut() {
1102                    // Observed in PDF mode: `pdf:` specials (any case) are
1103                    // literals, with an optional mode keyword; every other
1104                    // special is dropped with a warning.
1105                    if data.len() >= 4 && data[..4].eq_ignore_ascii_case(b"pdf:") {
1106                        let rest = &data[4..];
1107                        let (mode, text) = [
1108                            (&b"direct:"[..], 2),
1109                            (b"page:", 1),
1110                            (b"raw:", 3),
1111                            (b"text:", 4),
1112                            (b"origin:", 0),
1113                        ]
1114                        .iter()
1115                        .find_map(|(k, m)| rest.strip_prefix(*k).map(|t| (*m, t)))
1116                        .unwrap_or((crate::pdfext::literal_mode::ORIGIN, rest));
1117                        be.literal(mode, text, x, y);
1118                    } else {
1119                        self.printer.print_nl("Non-PDF special ignored!");
1120                    }
1121                }
1122            }
1123            Whatsit::PdfLiteral { mode, data, .. } => {
1124                let bytes = match data {
1125                    crate::nodes::LateData::Bytes(b)
1126                    | crate::nodes::LateData::Lua { data: b, .. } => b,
1127                    crate::nodes::LateData::Tokens(t) => {
1128                        let toks = self.expand_late_tokens(&t);
1129                        self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX))
1130                    }
1131                };
1132                if let Some(be) = w.backend.as_deref_mut() {
1133                    be.literal(mode, &bytes, x, y);
1134                }
1135            }
1136            Whatsit::LateLua { data, .. } => {
1137                let code = match data {
1138                    crate::nodes::LateData::Bytes(b)
1139                    | crate::nodes::LateData::Lua { data: b, .. } => b,
1140                    crate::nodes::LateData::Tokens(t) => {
1141                        let toks = self.expand_late_tokens(&t);
1142                        self.lua_code(&toks)
1143                    }
1144                };
1145                // `pdf.getpos`, `pdf.print` and `pdf.registerannot` work on
1146                // this context while the code runs.
1147                self.pdf.pos = (x, y);
1148                let outer = self.pdf.late.replace(Default::default());
1149                self.run_lua(&code);
1150                let ctx = std::mem::replace(&mut self.pdf.late, outer).unwrap_or_default();
1151                if let Some(be) = w.backend.as_deref_mut() {
1152                    for (mode, text) in &ctx.prints {
1153                        be.literal(*mode, text, x, y);
1154                    }
1155                }
1156                self.pdf
1157                    .page
1158                    .annots
1159                    .extend(ctx.annots.into_iter().map(|a| (a, None)));
1160            }
1161            Whatsit::PdfRefobj { objnum } if !w.doing_leaders => self.ship_refobj(w, objnum),
1162            Whatsit::PdfAnnot { dims, objnum, data } if !w.doing_leaders => {
1163                self.ship_annot(w, ctx, h, v, dims, objnum, &data)
1164            }
1165            Whatsit::PdfStartLink {
1166                dims,
1167                objnum,
1168                attr,
1169                action,
1170            } if !w.doing_leaders => {
1171                self.ship_start_link(w, ctx, h, v, dims, objnum, &attr, &action)
1172            }
1173            Whatsit::PdfEndLink if !w.doing_leaders => self.ship_end_link(w, h, v),
1174            Whatsit::PdfDest {
1175                id,
1176                view,
1177                structure,
1178            } if !w.doing_leaders => self.ship_dest(w, ctx, h, v, &id, &view, structure),
1179            Whatsit::SavePos => {
1180                self.last_x_pos = x;
1181                self.last_y_pos = y;
1182            }
1183            Whatsit::PdfSave => {
1184                if let Some(be) = w.backend.as_deref_mut() {
1185                    be.save(x, y);
1186                }
1187            }
1188            Whatsit::PdfRestore => {
1189                if let Some(be) = w.backend.as_deref_mut() {
1190                    be.restore(x, y);
1191                }
1192            }
1193            Whatsit::PdfSetmatrix { data } => {
1194                if let Some(be) = w.backend.as_deref_mut() {
1195                    be.set_matrix(x, y, &data);
1196                }
1197            }
1198            Whatsit::PdfColorstack { stack, cmd, data } => {
1199                let out = self.colorstack_op(stack, cmd, data);
1200                if let (Some((mode, lit)), Some(be)) = (out, w.backend.as_deref_mut()) {
1201                    be.literal(mode, &lit, x, y);
1202                }
1203            }
1204            _ => {}
1205        }
1206    }
1207
1208    /// Applies a colour-stack command; returns the literal to emit.
1209    fn colorstack_op(&mut self, stack: i32, cmd: u8, data: Vec<u8>) -> Option<(u8, Vec<u8>)> {
1210        use crate::pdfext::colorstack_cmd as c;
1211        self.pdf_ensure_colorstacks();
1212        let def = self.pdf.colorstacks.get(stack.max(0) as usize).cloned()?;
1213        let st = self
1214            .colorstack_values
1215            .entry(stack)
1216            .or_insert_with(|| vec![def.init.clone()]);
1217        match cmd {
1218            c::SET => {
1219                if let Some(top) = st.last_mut() {
1220                    *top = data;
1221                } else {
1222                    st.push(data);
1223                }
1224            }
1225            c::PUSH => st.push(data),
1226            c::POP => {
1227                if st.len() > 1 {
1228                    st.pop();
1229                }
1230            }
1231            _ => {}
1232        }
1233        let lit = st.last().cloned().unwrap_or_default();
1234        Some((def.mode, lit))
1235    }
1236
1237    /// Expands a late token list (as `\write` does) and returns the
1238    /// expanded tokens.
1239    fn expand_late_tokens(&mut self, list: &crate::token::TokenList) -> Vec<crate::token::Token> {
1240        use crate::token::Token;
1241        use std::rc::Rc;
1242        self.ins_list(Rc::from(vec![
1243            Token::RIGHT_BRACE,
1244            Token::cs(self.cs.end_write),
1245        ]));
1246        self.begin_token_list(list.clone(), crate::input::TokKind::WriteText);
1247        self.ins_list(Rc::from(vec![Token::LEFT_BRACE]));
1248        let toks = self.scan_toks(false, true);
1249        self.get_token();
1250        toks
1251    }
1252
1253    /// Finishes the output file at the end of the run and reports it.
1254    pub fn finish_backend(&mut self) {
1255        if self.dvi.pages() > 0 {
1256            self.finish_dvi();
1257        } else if self.pdf.output_opened && self.eqtb.int(ip::OUTPUTMODE) <= 0 {
1258            // LuaTeX opened the DVI file for `pdf.obj` and leaves it empty.
1259            let _ = std::fs::write(self.output_path("dvi"), b"");
1260        }
1261        let Some(mut be) = self.backend.take() else {
1262            return;
1263        };
1264        if self.pdf.fatal || self.total_pages == 0 {
1265            be.abandon(self.pdf.fatal);
1266            self.backend = Some(be);
1267            return;
1268        }
1269        if self.eqtb.int(ip::DRAFTMODE) > 0 && self.eqtb.int(ip::OUTPUTMODE) > 0 {
1270            // `-draftmode`: the document is typeset but no PDF is written
1271            // (the backend wrote to a scratch file, removed here). LuaTeX
1272            // prints this warning; unlike LuaTeX, which empties an
1273            // existing PDF file, retex leaves it as it is.
1274            be.abandon(true);
1275            self.backend = Some(be);
1276            self.printer
1277                .print_nl("warning  (pdf backend): draftmode enabled, not changing output pdf");
1278            self.printer.print_ln();
1279            return;
1280        }
1281        if self.total_pages > 0 {
1282            self.pdf_finish_document();
1283            self.flush_pending_objects(be.as_mut());
1284        }
1285        if self.total_pages > 0
1286            && let Some(fin) = be.finish(self)
1287        {
1288            // Observed: printed one after the other like C strings (log
1289            // lines up to 80 characters), right after what came before.
1290            for f in &fin.embedded {
1291                let mut text = b"<".to_vec();
1292                text.extend_from_slice(f.as_bytes());
1293                text.push(b'>');
1294                self.printer.print_lua_bytes(&text);
1295            }
1296            self.pdf.objstm_stats = fin.objstm;
1297            self.print_output_written(&fin.file, self.total_pages, fin.size);
1298        }
1299        self.backend = Some(be);
1300    }
1301
1302    /// The path of output file `<jobname>.<ext>` (in `-output-directory`).
1303    pub(crate) fn output_path(&self, ext: &str) -> std::path::PathBuf {
1304        let job =
1305            String::from_utf8_lossy(self.job_name.as_deref().unwrap_or(b"texput")).into_owned();
1306        let name = format!("{job}.{ext}");
1307        match &self.files.output_dir {
1308            Some(d) => d.join(&name),
1309            None => std::path::PathBuf::from(&name),
1310        }
1311    }
1312
1313    /// Writes the postamble and the DVI file `<jobname>.dvi` (§642).
1314    fn finish_dvi(&mut self) {
1315        let mut dvi = std::mem::take(&mut self.dvi);
1316        let pages = dvi.pages();
1317        let data = dvi.finish(self);
1318        let path = self.output_path("dvi");
1319        let name = path
1320            .file_name()
1321            .map(|n| n.to_string_lossy().into_owned())
1322            .unwrap_or_default();
1323        if std::fs::write(&path, &data).is_err() {
1324            self.printer.print_nl("! I can't write on file `");
1325            self.printer.print_bytes(name.as_bytes());
1326            self.printer.print_str("'.");
1327            return;
1328        }
1329        self.print_output_written(&name, pages, data.len() as u64);
1330    }
1331
1332    /// Prints `Output written on NAME (N pages, B bytes).`.
1333    fn print_output_written(&mut self, name: &str, pages: u32, size: u64) {
1334        self.output_written = Some((name.to_owned(), pages, size));
1335        self.printer.print_nl("Output written on ");
1336        self.printer.print_bytes(name.as_bytes());
1337        self.printer.print_str(" (");
1338        self.printer.print_int(i64::from(pages));
1339        self.printer
1340            .print_str(if pages == 1 { " page" } else { " pages" });
1341        self.printer.print_str(", ");
1342        self.printer.print_int(size as i64);
1343        self.printer.print_str(" bytes).");
1344    }
1345}
1346
1347/// The state of a page being shipped: the outputs and the coordinate
1348/// systems they use.
1349pub(crate) struct Walker {
1350    /// The PDF backend (PDF mode).
1351    backend: Option<Box<dyn Backend>>,
1352    /// The DVI writer (DVI mode).
1353    dvi: Option<crate::dvi::DviWriter>,
1354    h_origin: i32,
1355    v_origin: i32,
1356    page_height: i32,
1357    /// The DVI position of TeX's origin (`\hoffset`, `\voffset`).
1358    dvi_h: i32,
1359    dvi_v: i32,
1360    /// Box nesting depth (`cur_s`): -1 outside the page box.
1361    pub(crate) cur_s: i32,
1362    /// Whether a leader box is being output (`doing_leaders`).
1363    doing_leaders: bool,
1364    /// Whether this is a page (not a box resource): annotations, links
1365    /// and destinations belong to pages.
1366    pub(crate) page: bool,
1367    /// Whether SyncTeX records this sheet.
1368    sync: bool,
1369}
1370
1371impl Walker {
1372    /// Page coordinates (from the lower left, y up) of TeX point
1373    /// (`h`, `v`) measured from the origin, v downwards.
1374    pub(crate) fn pos(&self, h: i32, v: i32) -> (i32, i32) {
1375        (self.h_origin + h, self.page_height - (self.v_origin + v))
1376    }
1377}