Skip to main content

retex_engine/
image.rs

1//! Image resources: `\saveimageresource`, `\useimageresource`,
2//! `\lastsavedimageresourceindex` and `\lastsavedimageresourcepages`.
3//!
4//! Behaviour established black-box against LuaTeX 1.22:
5//!
6//! * `\saveimageresource` takes, in any order, `width`, `height`, `depth`
7//!   (dimensions), `attr {…}`, `named {…}`, `page n`, `colorspace n`,
8//!   `userpassword {…}`, `ownerpassword {…}` and a page box keyword
9//!   (`mediabox`, `cropbox`, `bleedbox`, `trimbox`, `artbox`), then the file
10//!   name in braces.
11//! * A resource's index (`\lastsavedimageresourceindex`,
12//!   `\lastsavedboxresourceindex`, the number `\useimageresource` and
13//!   `\useboxresource` take) is the PDF object number reserved for it, so
14//!   `\the\lastsavedboxresourceindex\space 0 R` refers to the form (pgf's
15//!   soft masks rely on this). `pdf.getobjtype` reports `ximage` or `xform`
16//!   for it.
17//! * `\useimageresource [width…] [height…] [depth…] n` appends a rule node
18//!   of subtype 2 (shown as `\image(h+d)xw`) in any mode.
19//! * Sizes given to either command are completed from the image's natural
20//!   aspect ratio: a width alone sets height = width × h₀ / w₀; a height
21//!   (plus depth) alone sets width = (height + depth) × w₀ / h₀.
22//! * The image is written to the PDF when it is first shipped out, or at
23//!   once with `\immediate`. Its file name, as found, is printed as
24//!   `<path>` then: at once with `\immediate`, otherwise at the end of the
25//!   page (after the page's `\write`s), in the order of first use.
26
27use std::path::PathBuf;
28
29use retex_image::{Info, PageBox};
30
31use crate::Engine;
32use crate::nodes::NodeRef;
33
34/// Rule subtype of image nodes.
35pub const IMAGE_RULE: u16 = 2;
36
37/// A saved image resource.
38#[derive(Clone, Debug)]
39pub struct ImageResource {
40    /// The file as found.
41    pub path: PathBuf,
42    /// The name as given (for messages).
43    pub name: String,
44    /// What the file says about itself.
45    pub info: Info,
46    /// The page (PDF), 1-based.
47    pub page: u32,
48    /// The page box (PDF).
49    pub page_box: PageBox,
50    /// Extra entries for the image dictionary (`attr {…}`).
51    pub attr: Vec<u8>,
52    /// Size as saved: width, height, depth.
53    pub size: (i32, i32, i32),
54    /// Natural size: width, height.
55    pub natural: (i32, i32),
56    /// The PDF object number reserved for the image.
57    pub obj: i32,
58    /// Whether it has been written (and its name printed).
59    pub written: bool,
60}
61
62/// Explicit dimensions of `\saveimageresource`/`\useimageresource`.
63#[derive(Clone, Copy, Debug, Default)]
64struct Dims {
65    width: Option<i32>,
66    height: Option<i32>,
67    depth: Option<i32>,
68}
69
70/// `a × b / c` rounded, without overflow.
71fn scale(a: i32, b: i32, c: i32) -> i32 {
72    if c == 0 {
73        return 0;
74    }
75    let (a, b, c) = (i64::from(a), i64::from(b), i64::from(c));
76    ((a * b + c / 2) / c) as i32
77}
78
79/// Completes `d` from the natural size `(w0, h0)`.
80fn complete(d: Dims, (w0, h0): (i32, i32)) -> (i32, i32, i32) {
81    let depth = d.depth.unwrap_or(0);
82    match (d.width, d.height, d.depth) {
83        (None, None, None) => (w0, h0, 0),
84        (Some(w), Some(h), _) => (w, h, depth),
85        (Some(w), None, _) => (w, scale(w, h0, w0) - depth, depth),
86        (None, Some(h), _) => (scale(h + depth, w0, h0), h, depth),
87        (None, None, Some(dp)) => (w0, h0 - dp, dp),
88    }
89}
90
91impl Engine {
92    /// Scans `width`/`height`/`depth` keywords.
93    fn scan_image_dims(&mut self, d: &mut Dims) -> bool {
94        if self.scan_keyword("width") {
95            self.scan_normal_dimen();
96            d.width = Some(self.cur_val);
97        } else if self.scan_keyword("height") {
98            self.scan_normal_dimen();
99            d.height = Some(self.cur_val);
100        } else if self.scan_keyword("depth") {
101            self.scan_normal_dimen();
102            d.depth = Some(self.cur_val);
103        } else {
104            return false;
105        }
106        true
107    }
108
109    /// `\saveimageresource` (`immediate`: write the image at once).
110    pub fn save_image_resource(&mut self, immediate: bool) {
111        self.work_begin(crate::profile::Work::Images);
112        self.save_image_resource_inner(immediate);
113        self.work_end();
114    }
115
116    fn save_image_resource_inner(&mut self, immediate: bool) {
117        let mut dims = Dims::default();
118        let mut attr = Vec::new();
119        let mut page = 1u32;
120        let mut page_box = PageBox::Crop;
121        loop {
122            if self.scan_image_dims(&mut dims) {
123                continue;
124            }
125            if self.scan_keyword("attr") {
126                let toks = self.scan_toks_expanded();
127                attr = self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
128            } else if self.scan_keyword("named") {
129                // Named destinations of the PDF are not supported; LuaTeX
130                // takes the page by name. Read and ignore it.
131                let _ = self.scan_toks_expanded();
132            } else if self.scan_keyword("page") {
133                self.scan_int();
134                page = self.cur_val.max(1) as u32;
135            } else if self.scan_keyword("colorspace") {
136                self.scan_int();
137            } else if self.scan_keyword("userpassword") || self.scan_keyword("ownerpassword") {
138                let _ = self.scan_toks_expanded();
139            } else if self.scan_keyword("mediabox") {
140                page_box = PageBox::Media;
141            } else if self.scan_keyword("cropbox") {
142                page_box = PageBox::Crop;
143            } else if self.scan_keyword("bleedbox") {
144                page_box = PageBox::Bleed;
145            } else if self.scan_keyword("trimbox") {
146                page_box = PageBox::Trim;
147            } else if self.scan_keyword("artbox") {
148                page_box = PageBox::Art;
149            } else {
150                break;
151            }
152        }
153        let toks = self.scan_toks_expanded();
154        let name_bytes =
155            self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
156        let name = String::from_utf8_lossy(&name_bytes).into_owned();
157        let found = self.find_input_file(&name, retex_kpath::FileFormat::Tex);
158        if found.is_some() {
159            self.set_svg_font_finder();
160        }
161        let info = found
162            .as_ref()
163            .map(|p| retex_image::probe_file(p, page, page_box));
164        if let Some(p) = &found
165            && !self.files.watched.contains(p)
166        {
167            self.files.watched.push(p.clone());
168        }
169        let (path, info) = match (found, info) {
170            (Some(p), Some(Ok(i))) => (p, i),
171            (Some(p), Some(Err(e))) if e.to_string().contains("does not exist") => {
172                // Observed: a page that the PDF file does not have stops
173                // LuaTeX, after its version warning (found by fuzzing,
174                // \includegraphics[page=3]).
175                if let Ok(i) = retex_image::probe_file(&p, 1, page_box) {
176                    self.image_version_check(&name, &i);
177                }
178                let category = format!("file {name}) (pdf inclusion");
179                let msg = format!("required page '{page}' does not exist");
180                if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
181                    self.pdf_fatal_in(&category, &msg);
182                }
183                self.luatex_fatal(&category, &msg);
184            }
185            (_, Some(Err(e))) => {
186                self.image_error(&name, &e.to_string());
187                return;
188            }
189            _ => {
190                self.image_error(&name, "file not found");
191                return;
192            }
193        };
194        if !self.image_version_check(&name, &info) {
195            return;
196        }
197        for (cat, msg) in &info.warnings {
198            self.luatex_warning(&format!("file {name}) ({cat}"), msg);
199        }
200
201        let natural = retex_image::natural_size(&info);
202        let size = complete(dims, natural);
203        let obj = self.pdf_alloc_obj(crate::pdfext::ObjKind::Ximage);
204        self.last_image_pages = info.pages as i32;
205        self.images.push(ImageResource {
206            path,
207            name,
208            info,
209            page,
210            page_box,
211            attr,
212            size,
213            natural,
214            obj,
215            written: false,
216        });
217        self.last_image_index = obj;
218        if immediate && let Some(mut be) = self.backend.take() {
219            be.write_image(self, obj as u32);
220            self.backend = Some(be);
221            self.mark_image_written(obj as u32);
222            self.print_image_name(obj as u32);
223            self.pdf.stream_written = true;
224        }
225    }
226
227    /// LuaTeX compares the file's header version with the output's: a
228    /// warning, or an error with `\pdfvariable inclusionerrorlevel` > 0
229    /// (then `false`).
230    fn image_version_check(&mut self, name: &str, info: &retex_image::Info) -> bool {
231        if let Some(found) = info.pdf_version {
232            let int = |i: u32| self.eqtb.int(i).max(0) as u32;
233            let allowed = (
234                int(crate::backend::MAJOR_VERSION),
235                int(crate::backend::MINOR_VERSION),
236            );
237            if let Some(msg) = retex_image::pdf::version_message(found, allowed) {
238                if self.eqtb.int(145) > 0 {
239                    self.image_error(name, &msg);
240                    return false;
241                }
242                self.luatex_warning(&format!("file {name}) (pdf inclusion"), &msg);
243            }
244        }
245        true
246    }
247
248    fn image_error(&mut self, name: &str, why: &str) {
249        self.print_err("Image file `");
250        self.printer.print_str(name);
251        self.printer.print_str("': ");
252        self.printer.print_str(why);
253        self.help(&["The image could not be read, so nothing was saved."]);
254        self.error();
255    }
256
257    /// `\useimageresource`: appends an image node.
258    pub fn use_image_resource(&mut self) {
259        let mut dims = Dims::default();
260        while self.scan_image_dims(&mut dims) {}
261        self.scan_int();
262        let n = self.cur_val;
263        let Some(img) = self.image(n as u32) else {
264            self.print_err("Invalid image resource index ");
265            self.printer.print_int(i64::from(n));
266            self.help(&["\\useimageresource needs a number from \\lastsavedimageresourceindex."]);
267            self.error();
268            return;
269        };
270        let (w, h, d) = if dims.width.is_none() && dims.height.is_none() && dims.depth.is_none() {
271            img.size
272        } else {
273            complete(dims, img.natural)
274        };
275        let p = self.nodes.new_rule(w, h, d);
276        self.nodes.get_mut(p).subtype = IMAGE_RULE;
277        if self.eqtb.attrs_in_use {
278            let a = self.cur_attr();
279            self.nodes.set_attr(p, a);
280        }
281        if let crate::nodes::NodeData::Rule { index, .. } = &mut self.nodes.get_mut(p).data {
282            *index = n;
283        }
284        self.tail_append(p);
285    }
286
287    /// Whether node `p` is an image.
288    pub fn is_image(&self, p: NodeRef) -> bool {
289        let n = self.nodes.get(p);
290        n.subtype == IMAGE_RULE && matches!(n.data, crate::nodes::NodeData::Rule { .. })
291    }
292
293    /// The image resource with index (object number) `n`.
294    pub fn image(&self, n: u32) -> Option<&ImageResource> {
295        // Object numbers grow, so the list is sorted by them.
296        let i = self
297            .images
298            .binary_search_by_key(&(n as i32), |r| r.obj)
299            .ok()?;
300        self.images.get(i)
301    }
302
303    /// Records that image `n` is written; true the first time.
304    pub fn mark_image_written(&mut self, n: u32) -> bool {
305        let Ok(i) = self.images.binary_search_by_key(&(n as i32), |r| r.obj) else {
306            return false;
307        };
308        !std::mem::replace(&mut self.images[i].written, true)
309    }
310
311    /// Prints `<path>` for image `n`, as when it is written.
312    pub fn print_image_name(&mut self, n: u32) {
313        let Some(path) = self.image(n).map(|i| i.path.to_string_lossy().into_owned()) else {
314            return;
315        };
316        // Printed like a C string (log lines may reach 80 characters).
317        let mut text = b"<".to_vec();
318        text.extend_from_slice(path.as_bytes());
319        text.push(b'>');
320        self.printer.print_lua_bytes(&text);
321    }
322
323    /// The resource number of image `n` (`/Im<k>`: the k-th image saved).
324    pub fn image_name(&self, n: u32) -> u32 {
325        self.images
326            .binary_search_by_key(&(n as i32), |r| r.obj)
327            .map_or(n, |i| i as u32 + 1)
328    }
329
330    /// The box resource with index (object number) `n` and its resource
331    /// number (`/Fm<k>`: the k-th box resource saved).
332    pub fn box_resource(&self, n: u32) -> Option<(&BoxResource, u32)> {
333        let i = self.box_resource_slot(n)?;
334        Some((&self.box_resources[i], i as u32 + 1))
335    }
336
337    /// The position in [`Engine::box_resources`] of the box resource with
338    /// index (object number) `n`.
339    pub fn box_resource_slot(&self, n: u32) -> Option<usize> {
340        self.box_resources
341            .binary_search_by_key(&(n as i32), |r| r.obj)
342            .ok()
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn completing_sizes() {
352        let nat = (26312704, 19734528); // 401.5pt x 301.125pt
353        let d = |w, h, dp| Dims {
354            width: w,
355            height: h,
356            depth: dp,
357        };
358        // LuaTeX: width 2cm → 56.9055pt x 42.67912pt.
359        assert_eq!(
360            complete(d(Some(3729359), None, None), nat),
361            (3729359, 2797019, 0)
362        );
363        // height 5pt depth 1pt → 8pt wide.
364        assert_eq!(
365            complete(d(None, Some(327680), Some(65536)), nat),
366            (524288, 327680, 65536)
367        );
368    }
369}
370
371/// Rule subtype of box-resource nodes (`\useboxresource`).
372pub const BOX_RESOURCE_RULE: u16 = 1;
373
374/// A saved box resource (`\saveboxresource`): a TeX box written as a PDF
375/// form XObject when first used (or at once with `\immediate`).
376#[derive(Clone, Debug)]
377pub struct BoxResource {
378    /// The box (taken from its register).
379    pub list: NodeRef,
380    /// Width, height and depth of the box.
381    pub size: (i32, i32, i32),
382    /// Extra entries for the form dictionary (`attr {…}`).
383    pub attr: Vec<u8>,
384    /// Extra entries for the form's resources (`resources {…}`).
385    pub resources: Vec<u8>,
386    /// Whether the form has been written.
387    pub written: bool,
388    /// The PDF object number reserved for the form (also its index).
389    pub obj: i32,
390}
391
392impl Engine {
393    /// `\saveboxresource [attr {…}] [resources {…}] <register>`
394    /// (`immediate`: write the form at once).
395    pub fn save_box_resource(&mut self, immediate: bool) {
396        let mut attr = Vec::new();
397        let mut resources = Vec::new();
398        loop {
399            if self.scan_keyword("attr") {
400                let toks = self.scan_toks_expanded();
401                attr = self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
402            } else if self.scan_keyword("resources") {
403                let toks = self.scan_toks_expanded();
404                resources =
405                    self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
406            } else if self.scan_keyword("type") {
407                self.scan_int();
408            } else if self.scan_keyword("margin") {
409                self.scan_normal_dimen();
410            } else {
411                break;
412            }
413        }
414        self.scan_register_num();
415        let n = self.cur_val as usize;
416        let list = self.eqtb.boxes[n].val;
417        if list.is_null() {
418            self.print_err("Invalid \\box");
419            self.help(&["\\saveboxresource needs a box register that is not void."]);
420            self.error();
421            return;
422        }
423        self.eqtb.boxes[n].val = NodeRef::NULL;
424        let obj = self.pdf_alloc_obj(crate::pdfext::ObjKind::Xform);
425        let size = self
426            .nodes
427            .box_data(list)
428            .map_or((0, 0, 0), |b| (b.width, b.height, b.depth));
429        self.box_resources.push(BoxResource {
430            list,
431            size,
432            attr,
433            resources,
434            written: false,
435            obj,
436        });
437        self.last_box_index = obj;
438        if immediate && let Some(be) = self.backend.take() {
439            let be = self.write_box_resource(be, obj as u32);
440            self.pdf.stream_written = true;
441            self.backend = Some(be);
442        }
443    }
444
445    /// `\useboxresource [width…] [height…] [depth…] n`.
446    pub fn use_box_resource(&mut self) {
447        let mut dims = Dims::default();
448        while self.scan_image_dims(&mut dims) {}
449        self.scan_int();
450        let n = self.cur_val;
451        let Some(res) = u32::try_from(n)
452            .ok()
453            .and_then(|n| self.box_resource_slot(n))
454            .map(|i| &self.box_resources[i])
455        else {
456            self.print_err("Invalid box resource index ");
457            self.printer.print_int(i64::from(n));
458            self.help(&["\\useboxresource needs a number from \\lastsavedboxresourceindex."]);
459            self.error();
460            return;
461        };
462        let (w0, h0, d0) = res.size;
463        // A given dimension scales the others in proportion.
464        let (w, h, d) = match (dims.width, dims.height, dims.depth) {
465            (None, None, None) => (w0, h0, d0),
466            (Some(w), Some(h), Some(d)) => (w, h, d),
467            (Some(w), h, d) => (
468                w,
469                h.unwrap_or_else(|| scale(h0, w, w0)),
470                d.unwrap_or_else(|| scale(d0, w, w0)),
471            ),
472            (None, Some(h), d) => (scale(w0, h, h0), h, d.unwrap_or_else(|| scale(d0, h, h0))),
473            (None, None, Some(d)) => (scale(w0, d, d0), scale(h0, d, d0), d),
474        };
475        let p = self.nodes.new_rule(w, h, d);
476        self.nodes.get_mut(p).subtype = BOX_RESOURCE_RULE;
477        if self.eqtb.attrs_in_use {
478            let a = self.cur_attr();
479            self.nodes.set_attr(p, a);
480        }
481        if let crate::nodes::NodeData::Rule { index, .. } = &mut self.nodes.get_mut(p).data {
482            *index = n;
483        }
484        self.tail_append(p);
485    }
486}
487
488/// What the default output mode adds to package files, before their final
489/// `\endinput`: the file name, a text that identifies the file, and the
490/// addition. `--compat=lualatex` reads the files as they are.
491///
492/// * graphicx's driver `luatex.def`: `.svg` files are included as PDF
493///   pages (`retex-image` converts them), and found after the other
494///   extensions.
495/// * The `svg` package: `\includesvg` includes the SVG file itself with
496///   `\includegraphics` and the package's size and angle options, without
497///   Inkscape; `inkscapelatex` is ignored (the SVG file's own text is
498///   drawn).
499const PACKAGE_ADDITIONS: &[(&str, &[u8], &[u8])] = &[
500    (
501        "luatex.def",
502        b"\\def\\Gin@extensions",
503        b"\\@namedef{Gin@rule@.svg}#1{{pdf}{.svg}{#1}}%
504\\@namedef{Gin@rule@.SVG}#1{{pdf}{.SVG}{#1}}%
505\\edef\\Gin@extensions{\\Gin@extensions,.svg,.SVG}%
506",
507    ),
508    (
509        "svg.sty",
510        b"\\newcommand*\\includesvg",
511        b"\\renewcommand*\\includesvg[2][]{%
512  \\begingroup
513    \\svg@deprecated@param
514    \\svg@local@param@set{#1}%
515    \\@svg@ink@latexfalse
516    \\svg@get@path{#2}{}%
517    \\if@svg@file@found
518      \\svg@@input{\\svg@file@base.\\svg@file@ext}%
519    \\else
520      \\svg@file@missing[\\svg@file@ext]{\\svg@file@base}{}%
521    \\fi
522  \\endgroup
523}%
524",
525    ),
526];
527
528impl Engine {
529    /// The text of an input file, with its [`PACKAGE_ADDITIONS`] in the
530    /// default output mode.
531    pub(crate) fn extend_package(&self, path: &std::path::Path, data: Vec<u8>) -> Vec<u8> {
532        if self.diagnostics.is_none() {
533            return data;
534        }
535        let Some(&(_, _, addition)) = PACKAGE_ADDITIONS.iter().find(|(name, mark, _)| {
536            path.file_name() == Some(std::ffi::OsStr::new(name))
537                && data.windows(mark.len()).any(|w| w == *mark)
538        }) else {
539            return data;
540        };
541        let Some(at) = data
542            .windows(b"\\endinput".len())
543            .rposition(|w| w == b"\\endinput")
544            .filter(|&i| i == 0 || data[i - 1] == b'\n')
545        else {
546            return data;
547        };
548        [&data[..at], addition, &data[at..]].concat()
549    }
550}
551
552impl Engine {
553    /// Before the first image: SVG text finds its fonts by family name in
554    /// the font name database (the one of luaotfload's `name:` lookups,
555    /// scanned when first needed).
556    fn set_svg_font_finder(&mut self) {
557        if self.svg_fonts_set {
558            return;
559        }
560        self.svg_fonts_set = true;
561        let Some(k) = &self.kpse else {
562            return;
563        };
564        let mut k = k.borrow_mut();
565        let dirs = retex_font::loader::name_dirs(&mut k);
566        let cache = k.cache_dir().map(std::path::Path::to_path_buf);
567        retex_image::svg::set_font_finder(std::sync::Arc::new(move |family| {
568            retex_font::names::NameDatabase::shared(dirs.clone(), cache.as_deref())
569                .family_files(family)
570                .into_iter()
571                .map(std::path::Path::to_path_buf)
572                .collect()
573        }));
574    }
575}