retex-engine 1.0.0-rc.2

The TeX typesetting engine for retex
Documentation
//! Annotations, links and destinations at shipout, and the colour stacks
//! at the start of a page, following LuaTeX 1.22 (black-box):
//!
//! * Rectangles: a whatsit at (h, v) in a box spans from h to h + width,
//!   from v + depth to v − height; a running width ends at the box's right
//!   edge, a running height or depth is the box's own (measured from its
//!   baseline), in horizontal and vertical lists alike.
//! * Links get `\pdfvariable linkmargin` on all sides of their first
//!   rectangle. When the box holding the startlink ends first, that
//!   rectangle ends at the box's right edge; every later hlist at the same
//!   nesting level (`cur_s`), on this page or the next ones (running heads
//!   included), gets a rectangle of its own covering the box, without
//!   margin, with a new object number; the last one ends at the endlink.
//! * `/Annots` lists the annotations, then the links, each in the order
//!   they were met.
//! * Destinations take the whatsit's position; only `fitr` rectangles get
//!   `\pdfvariable destmargin`. A second destination with the same
//!   identifier is ignored with a warning.
//! * Page colour stacks: at the start of every page the current value of
//!   each stack declared with `page` is written, stack 0 only when it
//!   differs from its initial value.

use crate::Engine;
use crate::nodes::RUNNING;
use crate::pdfext::{
    ActiveLink, DestId, DestKind, DestPos, DestView, LinkPiece, ObjKind, PdfAction,
};
use crate::shipout::Walker;

/// The box around a whatsit: its left edge and baseline in TeX
/// coordinates (v downwards), and its dimensions.
#[derive(Clone, Copy, Debug)]
pub(crate) struct BoxCtx {
    pub left: i32,
    pub base: i32,
    pub width: i32,
    pub height: i32,
    pub depth: i32,
}

/// `\pdfvariable linkmargin`, `destmargin`.
const LINK_MARGIN: u32 = 32;
const DEST_MARGIN: u32 = 31;

impl Engine {
    /// The rectangle (page coordinates) of a whatsit at (`h`, `v`) with
    /// dimensions `dims`, grown by `margin`.
    fn whatsit_rect(
        w: &Walker,
        ctx: &BoxCtx,
        h: i32,
        v: i32,
        dims: [i32; 3],
        margin: i32,
    ) -> [i32; 4] {
        let x2 = if dims[0] == RUNNING {
            ctx.left + ctx.width
        } else {
            h + dims[0]
        };
        let top = if dims[1] == RUNNING {
            ctx.base - ctx.height
        } else {
            v - dims[1]
        };
        let bottom = if dims[2] == RUNNING {
            ctx.base + ctx.depth
        } else {
            v + dims[2]
        };
        let (x1, y1) = w.pos(h, bottom);
        let (x2, y2) = w.pos(x2, top);
        [x1 - margin, y1 - margin, x2 + margin, y2 + margin]
    }

    /// `\pdfextension annot` at (`h`, `v`).
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn ship_annot(
        &mut self,
        w: &Walker,
        ctx: &BoxCtx,
        h: i32,
        v: i32,
        dims: [i32; 3],
        objnum: i32,
        data: &[u8],
    ) {
        if !w.page {
            return;
        }
        let rect = Self::whatsit_rect(w, ctx, h, v, dims, 0);
        self.pdf
            .page
            .annots
            .push((objnum, Some((rect, data.to_vec()))));
    }

    /// `\pdfextension startlink` at (`h`, `v`).
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn ship_start_link(
        &mut self,
        w: &Walker,
        ctx: &BoxCtx,
        h: i32,
        v: i32,
        dims: [i32; 3],
        objnum: i32,
        attr: &[u8],
        action: &PdfAction,
    ) {
        if !w.page {
            return;
        }
        let margin = self.eqtb.dimen(LINK_MARGIN);
        let mut rect = Self::whatsit_rect(w, ctx, h, v, dims, margin);
        let fixed_width = dims[0] != RUNNING;
        if !fixed_width {
            // The right edge is set by the endlink or the end of the box.
            rect[2] = rect[0];
        }
        self.pdf.page.links.push(LinkPiece {
            objnum,
            rect,
            attr: attr.to_vec(),
            action: Vec::new(),
            user: matches!(action, PdfAction::User(_)),
            source: action.clone(),
        });
        self.pdf.active_links.push(ActiveLink {
            level: w.cur_s,
            piece: Some(self.pdf.page.links.len() - 1),
            first_open: !fixed_width,
            margin,
            continued: false,
            attr: attr.to_vec(),
            action: action.clone(),
        });
    }

    /// `\pdfextension endlink` at (`h`, `v`).
    pub(crate) fn ship_end_link(&mut self, w: &Walker, h: i32, v: i32) {
        if !w.page {
            return;
        }
        let Some(l) = self.pdf.active_links.last().cloned() else {
            // Observed: an endlink without a link is ignored.
            return;
        };
        if l.level != w.cur_s {
            self.pdf_fatal("'endlink' ended up in different nesting level than 'startlink'");
        }
        self.pdf.active_links.pop();
        let margin = if l.continued { 0 } else { l.margin };
        if (l.first_open || l.continued)
            && let Some(i) = l.piece
            && let Some(p) = self.pdf.page.links.get_mut(i)
        {
            p.rect[2] = w.pos(h, v).0 + margin;
        }
    }

    /// At the start of an hlist: links started in an earlier box at this
    /// level get a rectangle covering the box.
    pub(crate) fn ship_continue_links(&mut self, w: &Walker, ctx: &BoxCtx) {
        if !w.page || self.pdf.active_links.is_empty() {
            return;
        }
        for i in 0..self.pdf.active_links.len() {
            if self.pdf.active_links[i].level != w.cur_s {
                continue;
            }
            let objnum = self.pdf_alloc_obj(ObjKind::Link);
            let rect = Self::whatsit_rect(w, ctx, ctx.left, ctx.base, [RUNNING; 3], 0);
            let l = &self.pdf.active_links[i];
            let piece = LinkPiece {
                objnum,
                rect,
                attr: l.attr.clone(),
                action: Vec::new(),
                user: matches!(l.action, PdfAction::User(_)),
                source: l.action.clone(),
            };
            self.pdf.page.links.push(piece);
            let n = self.pdf.page.links.len() - 1;
            let l = &mut self.pdf.active_links[i];
            l.piece = Some(n);
            l.first_open = false;
            l.continued = true;
        }
    }

    /// At the end of an hlist: open first rectangles of links at this
    /// level end at the box's right edge.
    pub(crate) fn ship_end_box_links(&mut self, w: &Walker, ctx: &BoxCtx) {
        if !w.page {
            return;
        }
        let right = w.pos(ctx.left + ctx.width, ctx.base).0;
        for l in &mut self.pdf.active_links {
            if l.level == w.cur_s && l.first_open {
                if let Some(p) = l.piece.and_then(|i| self.pdf.page.links.get_mut(i)) {
                    p.rect[2] = right + l.margin;
                }
                l.first_open = false;
            }
        }
    }

    /// `\pdfextension dest` at (`h`, `v`).
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn ship_dest(
        &mut self,
        w: &Walker,
        ctx: &BoxCtx,
        h: i32,
        v: i32,
        id: &DestId,
        view: &DestView,
        structure: Option<i32>,
    ) {
        if !w.page {
            return;
        }
        // A structure destination: an array starting with the structure
        // element, kept apart from the named and numbered destinations.
        let objnum = if structure.is_some() {
            if self.pdf.struct_dests.get(id).is_some_and(|d| d.defined) {
                return;
            }
            let n = self.pdf_struct_dest_obj(id);
            if let Some(d) = self.pdf.struct_dests.get_mut(id) {
                d.defined = true;
            }
            n
        } else {
            0
        };
        if structure.is_none() && self.pdf.dests.get(id).is_some_and(|d| d.defined) {
            let what = match id {
                DestId::Name(n) => format!("name '{}'", String::from_utf8_lossy(n)),
                DestId::Num(n) => format!("num '{n}'"),
            };
            self.pdf_warning(&format!("ignoring duplicate destination with the {what}"));
            return;
        }
        let objnum = if structure.is_some() {
            objnum
        } else {
            let n = self.pdf_dest_obj(id);
            if let Some(d) = self.pdf.dests.get_mut(id) {
                d.defined = true;
            }
            n
        };
        let (x, y) = w.pos(h, v);
        let pos = match view.kind {
            DestKind::Xyz => DestPos::Xyz {
                x,
                y,
                zoom: view.zoom,
            },
            DestKind::Fit => DestPos::Fit,
            DestKind::FitB => DestPos::FitB,
            DestKind::FitH => DestPos::FitH(y),
            DestKind::FitBH => DestPos::FitBH(y),
            DestKind::FitV => DestPos::FitV(x),
            DestKind::FitBV => DestPos::FitBV(x),
            DestKind::FitR => {
                let m = self.eqtb.dimen(DEST_MARGIN);
                DestPos::FitR(Self::whatsit_rect(w, ctx, h, v, view.dims, m))
            }
        };
        let named = matches!(id, DestId::Name(_)) && structure.is_none();
        self.pdf.page.dests.push((objnum, named, pos, structure));
    }

    /// `\pdfextension refobj`: the object goes out with the page or box
    /// resource (observed: right after its content stream).
    pub(crate) fn ship_refobj(&mut self, w: &Walker, n: i32) {
        if !w.page {
            if let Some(obj) = self.pdf_take_page_object(n) {
                match self.form_objects.last_mut() {
                    Some(list) => list.push(obj),
                    None => self.pdf.pending.push(obj),
                }
            }
            return;
        }
        if let Some(obj) = self.pdf_take_page_object(n) {
            self.pdf.page.objects.push(obj);
        }
    }

    /// The literals the page colour stacks write at the start of a page:
    /// (literal mode, text).
    pub(crate) fn page_colorstack_literals(&mut self) -> Vec<(u8, Vec<u8>)> {
        self.pdf_ensure_colorstacks();
        let mut out = Vec::new();
        for (i, cs) in self.pdf.colorstacks.iter().enumerate() {
            if !cs.page {
                continue;
            }
            let cur = self
                .colorstack_values
                .get(&(i as i32))
                .and_then(|s| s.last())
                .unwrap_or(&cs.init);
            if i == 0 && *cur == cs.init {
                continue;
            }
            out.push((cs.mode, cur.clone()));
        }
        out
    }

    /// The page attributes and resources for the page being shipped:
    /// `\pdfvariable pageattr`/`pageresources` and their Lua counterparts.
    pub(crate) fn collect_page_texts(&mut self) {
        let toks = |e: &mut Engine, i: u32| {
            let list = e.eqtb.toks_param(i).clone();
            e.print_to_string(|e| e.show_token_list(&list, 0, list.len(), i64::MAX))
        };
        let attr = toks(self, crate::backend::PAGE_ATTR);
        let res = toks(self, crate::backend::PAGE_RESOURCES);
        self.pdf.page.attr = (
            attr,
            self.pdf.lua.page_attributes.clone().unwrap_or_default(),
        );
        self.pdf.page.resources = (res, self.pdf.lua.page_resources.clone().unwrap_or_default());
    }

    /// Turns the actions of the page's links into PDF text (allocating the
    /// destinations they refer to).
    pub(crate) fn resolve_link_actions(&mut self) {
        for i in 0..self.pdf.page.links.len() {
            let source = self.pdf.page.links[i].source.clone();
            let text = self.pdf_action_text(&source);
            self.pdf.page.links[i].action = text;
        }
    }
}