retex-engine 0.9.0

The TeX typesetting engine for retex
Documentation
//! The DVI writer (*TeX: The Program* §583–§602, §617–§642), as LuaTeX
//! uses it when `\outputmode=0`.
//!
//! A DVI file is a preamble, one `bop`…`eop` sequence per page and a
//! postamble that repeats the font definitions. The commands follow
//! TeX82, with these differences observed in LuaTeX's output:
//!
//! * movements are emitted lazily, just before the next character, rule
//!   or special, as `right`/`down` commands with the smallest length that
//!   TeX82's `movement` would pick; the `w`, `x`, `y`, `z` registers are
//!   never used (TeX82's §607–§615 optimization is absent);
//! * rules are always `set_rule`, also in vertical lists, so the reader's
//!   `h` moves by the rule width;
//! * a box nested inside the page is bracketed by `push`/`pop`; a `push`
//!   immediately followed by its `pop` is removed (`dvi_pop`, §601);
//! * characters and fonts use the shortest command for their number
//!   (`set1`…`set4`, `fnt1`…`fnt4`, `fnt_def1`…`fnt_def4`);
//! * the preamble comment is `" LuaTeX output YYYY.MM.DD:HHMM"`, from
//!   `\year`, `\month`, `\day` and `\time` at the first page.
//!
//! The walker in [`crate::shipout`] computes positions with TeX's rounding
//! rules and calls this writer with absolute DVI coordinates (`h` to the
//! right and `v` downwards from the DVI origin, `\hoffset` and `\voffset`
//! included).

use crate::Engine;
use crate::primitives_gen::assign_int as ip;

/// DVI opcodes (§586).
mod op {
    pub const SET1: u8 = 128;
    pub const SET_RULE: u8 = 132;
    pub const BOP: u8 = 139;
    pub const EOP: u8 = 140;
    pub const PUSH: u8 = 141;
    pub const POP: u8 = 142;
    pub const RIGHT1: u8 = 143;
    pub const DOWN1: u8 = 157;
    pub const FNT_NUM_0: u8 = 171;
    pub const FNT1: u8 = 235;
    pub const XXX1: u8 = 239;
    pub const XXX4: u8 = 242;
    pub const FNT_DEF1: u8 = 243;
    pub const PRE: u8 = 247;
    pub const POST: u8 = 248;
    pub const POST_POST: u8 = 249;
}

/// The DVI format identification byte (§587).
const ID_BYTE: u8 = 2;
/// The unit numerator and denominator: sp in units of 10^{-7} m (§587).
const NUM: i32 = 25_400_000;
const DEN: i32 = 473_628_672;

/// The state of the DVI file being written.
#[derive(Debug, Default)]
pub struct DviWriter {
    buf: Vec<u8>,
    /// The reader's position (`dvi_h`, `dvi_v`).
    h: i32,
    v: i32,
    /// The font currently selected (`dvi_f`), if any.
    font: Option<u32>,
    /// Fonts already defined in the file (`font_used`).
    used: Vec<bool>,
    /// Byte offset of the last `bop`, -1 before the first page.
    last_bop: i32,
    /// Pages written.
    pages: u32,
    /// Largest page height plus depth plus `\voffset` (`max_v`).
    max_v: i32,
    /// Largest page width plus `\hoffset` (`max_h`).
    max_h: i32,
    /// Deepest box nesting (`max_push`).
    max_push: i32,
    /// For each open `push`: the file length after it and the position
    /// it saved.
    stack: Vec<(usize, i32, i32)>,
}

impl DviWriter {
    /// The number of pages written so far.
    pub fn pages(&self) -> u32 {
        self.pages
    }

    fn out(&mut self, b: u8) {
        self.buf.push(b);
    }

    fn four(&mut self, x: i32) {
        self.buf.extend_from_slice(&x.to_be_bytes());
    }

    /// Writes `x` in `n` bytes (big-endian, two's complement).
    fn bytes(&mut self, x: u32, n: usize) {
        self.buf.extend_from_slice(&x.to_be_bytes()[4 - n..]);
    }

    /// Writes command `base + n - 1` with an `n`-byte unsigned argument,
    /// `n` as small as possible.
    fn unsigned_cmd(&mut self, base: u8, x: u32) {
        let n = match x {
            0..=0xff => 1,
            0x100..=0xffff => 2,
            0x1_0000..=0xff_ffff => 3,
            _ => 4,
        };
        self.out(base + n as u8 - 1);
        self.bytes(x, n);
    }

    /// A `right` or `down` command (`movement`, §607 without the
    /// `w`/`x`/`y`/`z` optimization; the length rule is TeX's).
    fn movement(&mut self, w: i32, base: u8) {
        let a = w.unsigned_abs();
        let n = if a >= 0o40000000 {
            4
        } else if a >= 0o100000 {
            3
        } else if a >= 0o200 {
            2
        } else {
            1
        };
        self.out(base + n as u8 - 1);
        self.bytes(w as u32, n);
    }

    /// Moves the reader to (`h`, `v`) (`synch_h`, `synch_v`).
    fn synch(&mut self, h: i32, v: i32) {
        if h != self.h {
            self.movement(h.wrapping_sub(self.h), op::RIGHT1);
            self.h = h;
        }
        if v != self.v {
            self.movement(v.wrapping_sub(self.v), op::DOWN1);
            self.v = v;
        }
    }

    /// Starts a page (§640): writes the preamble before the first page,
    /// then `bop` with `\count0`–`\count9`. `width`, `height` and `depth`
    /// are the box dimensions, `h_off` and `v_off` the page offsets.
    pub fn begin_page(&mut self, engine: &mut Engine, dims: (i32, i32, i32), offsets: (i32, i32)) {
        let (width, height, depth) = dims;
        let (h_off, v_off) = offsets;
        let t = height.wrapping_add(depth).wrapping_add(v_off);
        if t > self.max_v {
            self.max_v = t;
        }
        let t = width.wrapping_add(h_off);
        if t > self.max_h {
            self.max_h = t;
        }
        if self.pages == 0 {
            engine.prepare_mag();
            self.write_preamble(engine);
        }
        let page_loc = self.buf.len() as i32;
        self.out(op::BOP);
        for k in 0..10 {
            self.four(engine.eqtb.count[k].val);
        }
        self.four(self.last_bop);
        self.last_bop = page_loc;
        self.h = 0;
        self.v = 0;
        self.font = None;
        self.stack.clear();
    }

    /// The preamble (§617).
    fn write_preamble(&mut self, engine: &Engine) {
        self.last_bop = -1;
        self.out(op::PRE);
        self.out(ID_BYTE);
        self.four(NUM);
        self.four(DEN);
        self.four(engine.eqtb.int(ip::MAG));
        let int = |i: u32| engine.eqtb.int(i);
        let two = |n: i32| format!("{:02}", n.unsigned_abs() % 100);
        let time = int(ip::TIME);
        let comment = format!(
            " LuaTeX output {}.{}.{}:{}{}",
            int(ip::YEAR),
            two(int(ip::MONTH)),
            two(int(ip::DAY)),
            two(time / 60),
            two(time % 60)
        );
        let c = &comment.as_bytes()[..comment.len().min(255)];
        self.out(c.len() as u8);
        self.buf.extend_from_slice(c);
    }

    /// Finishes a page.
    pub fn end_page(&mut self) {
        self.out(op::EOP);
        self.pages += 1;
    }

    /// Opens a nested box (`push`); `depth` is its nesting level
    /// (`cur_s`, 1 for a box inside the page box).
    pub fn push(&mut self, depth: i32) {
        if depth > self.max_push {
            self.max_push = depth;
        }
        self.out(op::PUSH);
        self.stack.push((self.buf.len(), self.h, self.v));
    }

    /// Records the nesting level of the page box itself (0).
    pub fn note_depth(&mut self, depth: i32) {
        if depth > self.max_push {
            self.max_push = depth;
        }
    }

    /// Closes a nested box (`dvi_pop`, §601).
    pub fn pop(&mut self) {
        let Some((loc, h, v)) = self.stack.pop() else {
            return;
        };
        if loc == self.buf.len() {
            self.buf.pop();
        } else {
            self.out(op::POP);
        }
        self.h = h;
        self.v = v;
    }

    /// Typesets character `chr` of font `f` at (`h`, `v`) (§620); the
    /// reader then advances by `width`.
    pub fn glyph(&mut self, engine: &Engine, f: u32, chr: u32, h: i32, v: i32, width: i32) {
        if f == 0 {
            return;
        }
        self.synch(h, v);
        if self.font != Some(f) {
            let k = f - 1;
            let idx = f as usize;
            if self.used.len() <= idx {
                self.used.resize(idx + 1, false);
            }
            if !self.used[idx] {
                self.font_def(engine, f);
                self.used[idx] = true;
            }
            if k < 64 {
                self.out(op::FNT_NUM_0 + k as u8);
            } else {
                self.unsigned_cmd(op::FNT1, k);
            }
            self.font = Some(f);
        }
        if chr < 128 {
            self.out(chr as u8);
        } else {
            self.unsigned_cmd(op::SET1, chr);
        }
        self.h = h.wrapping_add(width);
    }

    /// A rule of the given width and height with its lower left corner at
    /// (`h`, `v`); the reader then advances by `width`.
    pub fn rule(&mut self, h: i32, v: i32, width: i32, height: i32) {
        self.synch(h, v);
        self.out(op::SET_RULE);
        self.four(height);
        self.four(width);
        self.h = h.wrapping_add(width);
    }

    /// A `\special` at (`h`, `v`) (§1368).
    pub fn special(&mut self, h: i32, v: i32, data: &[u8]) {
        self.synch(h, v);
        if data.len() < 256 {
            self.out(op::XXX1);
            self.out(data.len() as u8);
        } else {
            self.out(op::XXX4);
            self.four(data.len() as i32);
        }
        self.buf.extend_from_slice(data);
    }

    /// A font definition (`dvi_font_def`, §602): checksum, size, design
    /// size and the file name without its directory.
    fn font_def(&mut self, engine: &Engine, f: u32) {
        let fi = engine.font(f);
        let checksum = fi.checksum;
        let name = fi.name.rsplit(|&b| b == b'/').next().unwrap_or(&fi.name);
        let name = &name[..name.len().min(255)];
        let (size, dsize) = (fi.size, fi.design_size);
        self.unsigned_cmd(op::FNT_DEF1, f - 1);
        self.four(checksum as i32);
        self.four(size);
        self.four(dsize);
        self.out(0);
        self.out(name.len() as u8);
        self.buf.extend_from_slice(name);
    }

    /// Writes the postamble (§642) and returns the finished file.
    pub fn finish(&mut self, engine: &mut Engine) -> Vec<u8> {
        self.out(op::POST);
        self.four(self.last_bop);
        self.last_bop = self.buf.len() as i32 - 5;
        self.four(NUM);
        self.four(DEN);
        engine.prepare_mag();
        self.four(engine.eqtb.int(ip::MAG));
        self.four(self.max_v);
        self.four(self.max_h);
        self.bytes(self.max_push.clamp(0, 0xffff) as u32, 2);
        self.bytes(self.pages & 0xffff, 2);
        for f in (1..self.used.len()).rev() {
            if self.used[f] {
                self.font_def(engine, f as u32);
            }
        }
        self.out(op::POST_POST);
        self.four(self.last_bop);
        self.out(ID_BYTE);
        let k = 4 + (4 - self.buf.len() % 4) % 4;
        for _ in 0..k {
            self.out(223);
        }
        std::mem::take(&mut self.buf)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn movement_lengths_follow_tex() {
        let mut d = DviWriter::default();
        for w in [127, -127, -128, 32767, -32768, 8388607, -8388608] {
            d.movement(w, op::RIGHT1);
        }
        assert_eq!(
            d.buf,
            [
                143, 127, 143, 129, 144, 255, 128, 144, 127, 255, 145, 255, 128, 0, 145, 127, 255,
                255, 146, 255, 128, 0, 0
            ]
        );
    }

    #[test]
    fn push_pop_without_output_cancels() {
        let mut d = DviWriter::default();
        d.push(1);
        d.push(2);
        d.pop();
        d.pop();
        assert!(d.buf.is_empty());
        assert_eq!(d.max_push, 2);
        d.push(1);
        d.rule(5, 0, 1, 1);
        d.pop();
        assert_eq!(d.buf.first(), Some(&op::PUSH));
        assert_eq!(d.buf.last(), Some(&op::POP));
        assert_eq!((d.h, d.v), (0, 0));
    }
}