retex-engine 1.0.0-rc.2

The TeX typesetting engine for retex
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
//! Image resources: `\saveimageresource`, `\useimageresource`,
//! `\lastsavedimageresourceindex` and `\lastsavedimageresourcepages`.
//!
//! Behaviour established black-box against LuaTeX 1.22:
//!
//! * `\saveimageresource` takes, in any order, `width`, `height`, `depth`
//!   (dimensions), `attr {…}`, `named {…}`, `page n`, `colorspace n`,
//!   `userpassword {…}`, `ownerpassword {…}` and a page box keyword
//!   (`mediabox`, `cropbox`, `bleedbox`, `trimbox`, `artbox`), then the file
//!   name in braces.
//! * A resource's index (`\lastsavedimageresourceindex`,
//!   `\lastsavedboxresourceindex`, the number `\useimageresource` and
//!   `\useboxresource` take) is the PDF object number reserved for it, so
//!   `\the\lastsavedboxresourceindex\space 0 R` refers to the form (pgf's
//!   soft masks rely on this). `pdf.getobjtype` reports `ximage` or `xform`
//!   for it.
//! * `\useimageresource [width…] [height…] [depth…] n` appends a rule node
//!   of subtype 2 (shown as `\image(h+d)xw`) in any mode.
//! * Sizes given to either command are completed from the image's natural
//!   aspect ratio: a width alone sets height = width × h₀ / w₀; a height
//!   (plus depth) alone sets width = (height + depth) × w₀ / h₀.
//! * The image is written to the PDF when it is first shipped out, or at
//!   once with `\immediate`. Its file name, as found, is printed as
//!   `<path>` then: at once with `\immediate`, otherwise at the end of the
//!   page (after the page's `\write`s), in the order of first use.

use std::path::PathBuf;

use retex_image::{Info, PageBox};

use crate::Engine;
use crate::nodes::NodeRef;

/// Rule subtype of image nodes.
pub const IMAGE_RULE: u16 = 2;

/// A saved image resource.
#[derive(Clone, Debug)]
pub struct ImageResource {
    /// The file as found.
    pub path: PathBuf,
    /// The name as given (for messages).
    pub name: String,
    /// What the file says about itself.
    pub info: Info,
    /// The page (PDF), 1-based.
    pub page: u32,
    /// The page box (PDF).
    pub page_box: PageBox,
    /// Extra entries for the image dictionary (`attr {…}`).
    pub attr: Vec<u8>,
    /// Size as saved: width, height, depth.
    pub size: (i32, i32, i32),
    /// Natural size: width, height.
    pub natural: (i32, i32),
    /// The PDF object number reserved for the image.
    pub obj: i32,
    /// Whether it has been written (and its name printed).
    pub written: bool,
}

/// Explicit dimensions of `\saveimageresource`/`\useimageresource`.
#[derive(Clone, Copy, Debug, Default)]
struct Dims {
    width: Option<i32>,
    height: Option<i32>,
    depth: Option<i32>,
}

/// `a × b / c` rounded, without overflow.
fn scale(a: i32, b: i32, c: i32) -> i32 {
    if c == 0 {
        return 0;
    }
    let (a, b, c) = (i64::from(a), i64::from(b), i64::from(c));
    ((a * b + c / 2) / c) as i32
}

/// Completes `d` from the natural size `(w0, h0)`.
fn complete(d: Dims, (w0, h0): (i32, i32)) -> (i32, i32, i32) {
    let depth = d.depth.unwrap_or(0);
    match (d.width, d.height, d.depth) {
        (None, None, None) => (w0, h0, 0),
        (Some(w), Some(h), _) => (w, h, depth),
        (Some(w), None, _) => (w, scale(w, h0, w0) - depth, depth),
        (None, Some(h), _) => (scale(h + depth, w0, h0), h, depth),
        (None, None, Some(dp)) => (w0, h0 - dp, dp),
    }
}

impl Engine {
    /// Scans `width`/`height`/`depth` keywords.
    fn scan_image_dims(&mut self, d: &mut Dims) -> bool {
        if self.scan_keyword("width") {
            self.scan_normal_dimen();
            d.width = Some(self.cur_val);
        } else if self.scan_keyword("height") {
            self.scan_normal_dimen();
            d.height = Some(self.cur_val);
        } else if self.scan_keyword("depth") {
            self.scan_normal_dimen();
            d.depth = Some(self.cur_val);
        } else {
            return false;
        }
        true
    }

    /// `\saveimageresource` (`immediate`: write the image at once).
    pub fn save_image_resource(&mut self, immediate: bool) {
        let mut dims = Dims::default();
        let mut attr = Vec::new();
        let mut page = 1u32;
        let mut page_box = PageBox::Crop;
        loop {
            if self.scan_image_dims(&mut dims) {
                continue;
            }
            if self.scan_keyword("attr") {
                let toks = self.scan_toks_expanded();
                attr = self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
            } else if self.scan_keyword("named") {
                // Named destinations of the PDF are not supported; LuaTeX
                // takes the page by name. Read and ignore it.
                let _ = self.scan_toks_expanded();
            } else if self.scan_keyword("page") {
                self.scan_int();
                page = self.cur_val.max(1) as u32;
            } else if self.scan_keyword("colorspace") {
                self.scan_int();
            } else if self.scan_keyword("userpassword") || self.scan_keyword("ownerpassword") {
                let _ = self.scan_toks_expanded();
            } else if self.scan_keyword("mediabox") {
                page_box = PageBox::Media;
            } else if self.scan_keyword("cropbox") {
                page_box = PageBox::Crop;
            } else if self.scan_keyword("bleedbox") {
                page_box = PageBox::Bleed;
            } else if self.scan_keyword("trimbox") {
                page_box = PageBox::Trim;
            } else if self.scan_keyword("artbox") {
                page_box = PageBox::Art;
            } else {
                break;
            }
        }
        let toks = self.scan_toks_expanded();
        let name_bytes =
            self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
        let name = String::from_utf8_lossy(&name_bytes).into_owned();
        let found = self.find_input_file(&name, retex_kpath::FileFormat::Tex);
        let info = found
            .as_ref()
            .map(|p| retex_image::probe_file(p, page, page_box));
        if let Some(p) = &found
            && !self.files.watched.contains(p)
        {
            self.files.watched.push(p.clone());
        }
        let (path, info) = match (found, info) {
            (Some(p), Some(Ok(i))) => (p, i),
            (Some(p), Some(Err(e))) if e.to_string().contains("does not exist") => {
                // Observed: a page that the PDF file does not have stops
                // LuaTeX, after its version warning (found by fuzzing,
                // \includegraphics[page=3]).
                if let Ok(i) = retex_image::probe_file(&p, 1, page_box) {
                    self.image_version_check(&name, &i);
                }
                let category = format!("file {name}) (pdf inclusion");
                let msg = format!("required page '{page}' does not exist");
                if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
                    self.pdf_fatal_in(&category, &msg);
                }
                self.luatex_fatal(&category, &msg);
            }
            (_, Some(Err(e))) => {
                self.image_error(&name, &e.to_string());
                return;
            }
            _ => {
                self.image_error(&name, "file not found");
                return;
            }
        };
        if !self.image_version_check(&name, &info) {
            return;
        }
        for (cat, msg) in &info.warnings {
            self.luatex_warning(&format!("file {name}) ({cat}"), msg);
        }

        let natural = retex_image::natural_size(&info);
        let size = complete(dims, natural);
        let obj = self.pdf_alloc_obj(crate::pdfext::ObjKind::Ximage);
        self.last_image_pages = info.pages as i32;
        self.images.push(ImageResource {
            path,
            name,
            info,
            page,
            page_box,
            attr,
            size,
            natural,
            obj,
            written: false,
        });
        self.last_image_index = obj;
        if immediate && let Some(mut be) = self.backend.take() {
            be.write_image(self, obj as u32);
            self.backend = Some(be);
            self.mark_image_written(obj as u32);
            self.print_image_name(obj as u32);
            self.pdf.stream_written = true;
        }
    }

    /// LuaTeX compares the file's header version with the output's: a
    /// warning, or an error with `\pdfvariable inclusionerrorlevel` > 0
    /// (then `false`).
    fn image_version_check(&mut self, name: &str, info: &retex_image::Info) -> bool {
        if let Some(found) = info.pdf_version {
            let int = |i: u32| self.eqtb.int(i).max(0) as u32;
            let allowed = (
                int(crate::backend::MAJOR_VERSION),
                int(crate::backend::MINOR_VERSION),
            );
            if let Some(msg) = retex_image::pdf::version_message(found, allowed) {
                if self.eqtb.int(145) > 0 {
                    self.image_error(name, &msg);
                    return false;
                }
                self.luatex_warning(&format!("file {name}) (pdf inclusion"), &msg);
            }
        }
        true
    }

    fn image_error(&mut self, name: &str, why: &str) {
        self.print_err("Image file `");
        self.printer.print_str(name);
        self.printer.print_str("': ");
        self.printer.print_str(why);
        self.help(&["The image could not be read, so nothing was saved."]);
        self.error();
    }

    /// `\useimageresource`: appends an image node.
    pub fn use_image_resource(&mut self) {
        let mut dims = Dims::default();
        while self.scan_image_dims(&mut dims) {}
        self.scan_int();
        let n = self.cur_val;
        let Some(img) = self.image(n as u32) else {
            self.print_err("Invalid image resource index ");
            self.printer.print_int(i64::from(n));
            self.help(&["\\useimageresource needs a number from \\lastsavedimageresourceindex."]);
            self.error();
            return;
        };
        let (w, h, d) = if dims.width.is_none() && dims.height.is_none() && dims.depth.is_none() {
            img.size
        } else {
            complete(dims, img.natural)
        };
        let p = self.nodes.new_rule(w, h, d);
        self.nodes.get_mut(p).subtype = IMAGE_RULE;
        if self.eqtb.attrs_in_use {
            let a = self.cur_attr();
            self.nodes.set_attr(p, a);
        }
        if let crate::nodes::NodeData::Rule { index, .. } = &mut self.nodes.get_mut(p).data {
            *index = n;
        }
        self.tail_append(p);
    }

    /// Whether node `p` is an image.
    pub fn is_image(&self, p: NodeRef) -> bool {
        let n = self.nodes.get(p);
        n.subtype == IMAGE_RULE && matches!(n.data, crate::nodes::NodeData::Rule { .. })
    }

    /// The image resource with index (object number) `n`.
    pub fn image(&self, n: u32) -> Option<&ImageResource> {
        // Object numbers grow, so the list is sorted by them.
        let i = self
            .images
            .binary_search_by_key(&(n as i32), |r| r.obj)
            .ok()?;
        self.images.get(i)
    }

    /// Records that image `n` is written; true the first time.
    pub fn mark_image_written(&mut self, n: u32) -> bool {
        let Ok(i) = self.images.binary_search_by_key(&(n as i32), |r| r.obj) else {
            return false;
        };
        !std::mem::replace(&mut self.images[i].written, true)
    }

    /// Prints `<path>` for image `n`, as when it is written.
    pub fn print_image_name(&mut self, n: u32) {
        let Some(path) = self.image(n).map(|i| i.path.to_string_lossy().into_owned()) else {
            return;
        };
        // Printed like a C string (log lines may reach 80 characters).
        let mut text = b"<".to_vec();
        text.extend_from_slice(path.as_bytes());
        text.push(b'>');
        self.printer.print_lua_bytes(&text);
    }

    /// The resource number of image `n` (`/Im<k>`: the k-th image saved).
    pub fn image_name(&self, n: u32) -> u32 {
        self.images
            .binary_search_by_key(&(n as i32), |r| r.obj)
            .map_or(n, |i| i as u32 + 1)
    }

    /// The box resource with index (object number) `n` and its resource
    /// number (`/Fm<k>`: the k-th box resource saved).
    pub fn box_resource(&self, n: u32) -> Option<(&BoxResource, u32)> {
        let i = self.box_resource_slot(n)?;
        Some((&self.box_resources[i], i as u32 + 1))
    }

    /// The position in [`Engine::box_resources`] of the box resource with
    /// index (object number) `n`.
    pub fn box_resource_slot(&self, n: u32) -> Option<usize> {
        self.box_resources
            .binary_search_by_key(&(n as i32), |r| r.obj)
            .ok()
    }
}

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

    #[test]
    fn completing_sizes() {
        let nat = (26312704, 19734528); // 401.5pt x 301.125pt
        let d = |w, h, dp| Dims {
            width: w,
            height: h,
            depth: dp,
        };
        // LuaTeX: width 2cm → 56.9055pt x 42.67912pt.
        assert_eq!(
            complete(d(Some(3729359), None, None), nat),
            (3729359, 2797019, 0)
        );
        // height 5pt depth 1pt → 8pt wide.
        assert_eq!(
            complete(d(None, Some(327680), Some(65536)), nat),
            (524288, 327680, 65536)
        );
    }
}

/// Rule subtype of box-resource nodes (`\useboxresource`).
pub const BOX_RESOURCE_RULE: u16 = 1;

/// A saved box resource (`\saveboxresource`): a TeX box written as a PDF
/// form XObject when first used (or at once with `\immediate`).
#[derive(Clone, Debug)]
pub struct BoxResource {
    /// The box (taken from its register).
    pub list: NodeRef,
    /// Width, height and depth of the box.
    pub size: (i32, i32, i32),
    /// Extra entries for the form dictionary (`attr {…}`).
    pub attr: Vec<u8>,
    /// Extra entries for the form's resources (`resources {…}`).
    pub resources: Vec<u8>,
    /// Whether the form has been written.
    pub written: bool,
    /// The PDF object number reserved for the form (also its index).
    pub obj: i32,
}

impl Engine {
    /// `\saveboxresource [attr {…}] [resources {…}] <register>`
    /// (`immediate`: write the form at once).
    pub fn save_box_resource(&mut self, immediate: bool) {
        let mut attr = Vec::new();
        let mut resources = Vec::new();
        loop {
            if self.scan_keyword("attr") {
                let toks = self.scan_toks_expanded();
                attr = self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
            } else if self.scan_keyword("resources") {
                let toks = self.scan_toks_expanded();
                resources =
                    self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
            } else if self.scan_keyword("type") {
                self.scan_int();
            } else if self.scan_keyword("margin") {
                self.scan_normal_dimen();
            } else {
                break;
            }
        }
        self.scan_register_num();
        let n = self.cur_val as usize;
        let list = self.eqtb.boxes[n].val;
        if list.is_null() {
            self.print_err("Invalid \\box");
            self.help(&["\\saveboxresource needs a box register that is not void."]);
            self.error();
            return;
        }
        self.eqtb.boxes[n].val = NodeRef::NULL;
        let obj = self.pdf_alloc_obj(crate::pdfext::ObjKind::Xform);
        let size = self
            .nodes
            .box_data(list)
            .map_or((0, 0, 0), |b| (b.width, b.height, b.depth));
        self.box_resources.push(BoxResource {
            list,
            size,
            attr,
            resources,
            written: false,
            obj,
        });
        self.last_box_index = obj;
        if immediate && let Some(be) = self.backend.take() {
            let be = self.write_box_resource(be, obj as u32);
            self.pdf.stream_written = true;
            self.backend = Some(be);
        }
    }

    /// `\useboxresource [width…] [height…] [depth…] n`.
    pub fn use_box_resource(&mut self) {
        let mut dims = Dims::default();
        while self.scan_image_dims(&mut dims) {}
        self.scan_int();
        let n = self.cur_val;
        let Some(res) = u32::try_from(n)
            .ok()
            .and_then(|n| self.box_resource_slot(n))
            .map(|i| &self.box_resources[i])
        else {
            self.print_err("Invalid box resource index ");
            self.printer.print_int(i64::from(n));
            self.help(&["\\useboxresource needs a number from \\lastsavedboxresourceindex."]);
            self.error();
            return;
        };
        let (w0, h0, d0) = res.size;
        // A given dimension scales the others in proportion.
        let (w, h, d) = match (dims.width, dims.height, dims.depth) {
            (None, None, None) => (w0, h0, d0),
            (Some(w), Some(h), Some(d)) => (w, h, d),
            (Some(w), h, d) => (
                w,
                h.unwrap_or_else(|| scale(h0, w, w0)),
                d.unwrap_or_else(|| scale(d0, w, w0)),
            ),
            (None, Some(h), d) => (scale(w0, h, h0), h, d.unwrap_or_else(|| scale(d0, h, h0))),
            (None, None, Some(d)) => (scale(w0, d, d0), scale(h0, d, d0), d),
        };
        let p = self.nodes.new_rule(w, h, d);
        self.nodes.get_mut(p).subtype = BOX_RESOURCE_RULE;
        if self.eqtb.attrs_in_use {
            let a = self.cur_attr();
            self.nodes.set_attr(p, a);
        }
        if let crate::nodes::NodeData::Rule { index, .. } = &mut self.nodes.get_mut(p).data {
            *index = n;
        }
        self.tail_append(p);
    }
}