Skip to main content

retex_engine/
misc.rs

1//! Smaller commands: extensions (`\write`, `\openout`, `\special`),
2//! LuaTeX's `normal` commands, hyphenation data, directions, random
3//! numbers, Lua calls and backend hooks.
4
5use std::rc::Rc;
6
7use crate::Engine;
8use crate::cmd::Cmd;
9use crate::input::TokKind;
10use crate::primitives_gen as pg;
11use crate::token::{Token, TokenList};
12
13/// `extension` modifiers.
14#[allow(missing_docs)]
15pub mod ext {
16    pub const OPEN: u32 = 0;
17    pub const WRITE: u32 = 1;
18    pub const CLOSE: u32 = 2;
19    pub const IMMEDIATE: u32 = 4;
20    pub const DEFERRED: u32 = 5;
21    pub const SPECIAL: u32 = 6;
22    pub const SAVE_BOX_RESOURCE: u32 = 7;
23    pub const USE_BOX_RESOURCE: u32 = 8;
24    pub const SAVE_IMAGE_RESOURCE: u32 = 9;
25    pub const USE_IMAGE_RESOURCE: u32 = 10;
26    pub const END_LOCAL_CONTROL: u32 = 11;
27    pub const DVI_EXTENSION: u32 = 12;
28    pub const PDF_EXTENSION: u32 = 13;
29}
30
31/// Whether `\write` stream `j` goes to the terminal (and log).
32fn j_is_terminal(j: i32, open: bool) -> bool {
33    j >= 0 && !((0..=127).contains(&j) && open)
34}
35
36impl Engine {
37    /// Records a LaTeX/package warning written to the terminal (default
38    /// output mode).
39    pub fn record_latex_warning(&mut self, text: &str) {
40        if !self.record_latex_message(text) {
41            self.record_note(text);
42        }
43    }
44
45    /// Records `text` if it is a LaTeX or package warning, or an error
46    /// LaTeX reports this way; returns whether it was.
47    fn record_latex_message(&mut self, text: &str) -> bool {
48        // LaTeX also reports some errors this way, before asking on the
49        // terminal (a missing file: "! LaTeX Error: File `x.sty' not
50        // found.", then "Enter file name:").
51        if let Some((origin, message)) = crate::diag::parse_latex_typed_error(text) {
52            let mut d = crate::diag::Diagnostic::new(crate::diag::Severity::Error, message);
53            d.origin = Some(origin);
54            if let Some((file, line, source, column)) = self.source_location() {
55                d.file = Some(file);
56                d.line = Some(line);
57                d.source = Some(source);
58                d.column = Some(column);
59            }
60            if let Some(list) = self.diagnostics.as_mut() {
61                list.push(d);
62            }
63            return true;
64        }
65        let Some((origin, message, line)) = crate::diag::parse_latex_warning(text) else {
66            return false;
67        };
68        let mut d = crate::diag::Diagnostic::new(crate::diag::Severity::Warning, message);
69        d.origin = Some(origin);
70        if let Some((file, cur, _, _)) = self.source_location() {
71            d.file = Some(file);
72            d.line = Some(line.unwrap_or(cur));
73        }
74        if let Some(list) = self.diagnostics.as_mut() {
75            list.push(d);
76        }
77        true
78    }
79
80    /// Extension commands (`do_extension`).
81    pub fn do_extension(&mut self) {
82        match self.cur_chr {
83            ext::IMMEDIATE => {
84                self.get_x_token();
85                if self.cur_cmd == Cmd::Extension {
86                    match self.cur_chr {
87                        ext::OPEN => {
88                            self.scan_stream_and_open(true);
89                            return;
90                        }
91                        ext::WRITE => {
92                            let (stream, list) = self.scan_write();
93                            self.write_out(stream, &list);
94                            return;
95                        }
96                        ext::CLOSE => {
97                            self.scan_int();
98                            let n = self.cur_val;
99                            self.close_write_stream(n);
100                            return;
101                        }
102                        ext::PDF_EXTENSION => {
103                            self.pdf_extension(true);
104                            return;
105                        }
106                        ext::SAVE_IMAGE_RESOURCE => {
107                            self.save_image_resource(true);
108                            return;
109                        }
110                        ext::SAVE_BOX_RESOURCE => {
111                            self.save_box_resource(true);
112                            return;
113                        }
114                        _ => {}
115                    }
116                }
117                self.back_input();
118            }
119            ext::OPEN => self.scan_stream_and_open(false),
120            ext::WRITE => {
121                let (stream, list) = self.scan_write();
122                self.append_ext_whatsit(crate::nodes::Whatsit::Write { stream, data: list });
123            }
124            ext::CLOSE => {
125                self.scan_limited_stream();
126                let stream = self.cur_val as u32;
127                self.append_ext_whatsit(crate::nodes::Whatsit::Close { stream });
128            }
129            ext::SPECIAL => {
130                let toks = self.scan_toks_expanded();
131                let data =
132                    self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
133                self.append_ext_whatsit(crate::nodes::Whatsit::Special { data });
134            }
135            ext::PDF_EXTENSION => self.pdf_extension(false),
136            ext::SAVE_IMAGE_RESOURCE => self.save_image_resource(false),
137            ext::USE_IMAGE_RESOURCE => self.use_image_resource(),
138            ext::SAVE_BOX_RESOURCE => self.save_box_resource(false),
139            ext::USE_BOX_RESOURCE => self.use_box_resource(),
140            ext::END_LOCAL_CONTROL => {}
141            _ => {
142                let (c, r) = (self.cur_cmd, self.cur_chr);
143                let s = self.print_to_string(|e| e.print_cmd_chr(c, r));
144                self.not_implemented(&String::from_utf8_lossy(&s));
145            }
146        }
147    }
148
149    fn scan_stream_and_open(&mut self, immediate: bool) {
150        self.scan_limited_stream();
151        let n = self.cur_val;
152        self.scan_optional_equals();
153        let name = self.scan_file_name();
154        if immediate {
155            self.open_write_stream(n, &name);
156        } else {
157            self.append_ext_whatsit(crate::nodes::Whatsit::Open {
158                stream: n as u32,
159                name,
160            });
161        }
162    }
163
164    /// Scans a `\openout`/`\closeout` stream number (0–127).
165    fn scan_limited_stream(&mut self) {
166        self.scan_int();
167        let n = self.cur_val;
168        if !(0..=127).contains(&n) {
169            self.print_err("Bad number");
170            self.printer.print_str(" (");
171            self.printer.print_int(i64::from(n));
172            self.printer.print_char(')' as u32);
173            self.help(&[
174                "Since I expected to read a number between 0 and 127,",
175                "I changed this one to zero.",
176            ]);
177            self.int_error_plain();
178            self.cur_val = 0;
179        }
180    }
181
182    /// Completes an error without printing the value again.
183    fn int_error_plain(&mut self) {
184        self.error();
185    }
186
187    /// Appends a whatsit node to the current list.
188    pub fn append_ext_whatsit(&mut self, w: crate::nodes::Whatsit) {
189        let sub = whatsit_subtype(&w);
190        let p = self.new_node(
191            crate::nodes::id::WHATSIT,
192            sub,
193            crate::nodes::NodeData::Whatsit(Box::new(w)),
194        );
195        self.tail_append(p);
196    }
197
198    /// Opens `\write` stream `n` on file `name` (`.tex` added if no
199    /// extension).
200    pub fn open_write_stream(&mut self, n: i32, name: &[u8]) {
201        if !(0..=127).contains(&n) {
202            return;
203        }
204        let mut s = String::from_utf8_lossy(name).into_owned();
205        if std::path::Path::new(&s).extension().is_none() {
206            s.push_str(".tex");
207        }
208        if !self.name_ok(&s, retex_kpath::Access::Write) {
209            self.prompt_file_name(s.as_bytes(), "output file name", ".tex");
210        }
211        let mut path = std::path::PathBuf::from(&s);
212        if let Some(dir) = &self.files.output_dir {
213            path = dir.join(path);
214        }
215        match std::fs::File::create(&path) {
216            Ok(f) => {
217                let w = crate::files::RecordingWriter::new(f, path.clone(), self.written_sink());
218                if self.write_paths.len() <= n as usize {
219                    self.write_paths.resize(n as usize + 1, None);
220                }
221                self.write_paths[n as usize] = Some(path.clone());
222                self.printer.open_write(n as u32, Box::new(w));
223                // LuaTeX logs \openout as a raw line in the log, on a line of
224                // its own; TeX's line positions are left unchanged.
225                if self.printer.log_opened() {
226                    let mut line = format!("\n\\openout{n} = ").into_bytes();
227                    line.extend_from_slice(s.as_bytes());
228                    line.push(b'\n');
229                    self.printer.log_raw(&line);
230                }
231            }
232            Err(_) => {
233                self.prompt_file_name(s.as_bytes(), "output file name", ".tex");
234            }
235        }
236    }
237
238    /// Closes `\write` stream `n`.
239    pub fn close_write_stream(&mut self, n: i32) {
240        if (0..=127).contains(&n) {
241            self.printer.close_write(n as u32);
242        }
243    }
244
245    /// Scans `\write <n> {…}`; returns the stream and the unexpanded list.
246    fn scan_write(&mut self) -> (i32, TokenList) {
247        self.scan_int();
248        let n = self.cur_val;
249        let list = self.scan_general_text();
250        (n, list)
251    }
252
253    /// Expands and writes a `\write` text (`write_out`).
254    pub fn write_out(&mut self, stream: i32, list: &TokenList) {
255        // To the log or the terminal only, unless a file is open.
256        if self.figures.recording() && self.write_file_open(stream) {
257            self.figures.taint("\\immediate\\write");
258        }
259        // Expand the list as \edef does, with \endwrite as terminator.
260        let mut toks: Vec<Token> = Vec::with_capacity(list.len() + 2);
261        toks.push(Token::RIGHT_BRACE);
262        toks.push(Token::cs(self.cs.end_write));
263        let q: TokenList = Rc::from(toks);
264        self.ins_list(q);
265        self.begin_token_list(list.clone(), TokKind::WriteText);
266        let q: TokenList = Rc::from(vec![Token::LEFT_BRACE]);
267        self.ins_list(q);
268        let old_mode = self.mode;
269        self.mode = crate::maincontrol::Mode::None;
270        self.cur_cs = self.cs.end_write;
271        let save_def = self.def_ref_cs;
272        self.def_ref_cs = self.cs.end_write;
273        let expanded = self.scan_toks(false, true);
274        self.def_ref_cs = save_def;
275        self.get_token();
276        if self.cur_tok != Token::cs(self.cs.end_write) {
277            self.print_err("Unbalanced write command");
278            self.help(&[
279                "On this page there's a \\write with fewer real {'s than }'s.",
280                "I can't handle that very well; good luck.",
281            ]);
282            self.error();
283            loop {
284                self.get_token();
285                if self.cur_tok == Token::cs(self.cs.end_write) {
286                    break;
287                }
288            }
289        }
290        self.mode = old_mode;
291        let old = self.printer.selector;
292        if self.diagnostics.is_some()
293            && j_is_terminal(stream, self.printer.write_open(stream as u32))
294        {
295            let text = self
296                .print_to_string(|e| e.token_show_list(&expanded, 0, expanded.len(), 10_000_000));
297            self.record_latex_warning(&String::from_utf8_lossy(&text));
298        }
299        // Streams 0-127 are files; a negative stream writes to the log
300        // only, any other stream (or a closed one) to the terminal and log.
301        let j = stream;
302        let to_file = (0..=127).contains(&j) && self.printer.write_open(j as u32);
303        if to_file {
304            self.printer.selector = crate::print::Selector::File(j as u32);
305        } else {
306            if j < 0 && old == crate::print::Selector::TermAndLog {
307                self.printer.selector = crate::print::Selector::LogOnly;
308            }
309            self.printer.print_nl("");
310        }
311        self.token_show_list(&expanded, 0, expanded.len(), 10_000_000);
312        self.printer.print_ln();
313        self.printer.selector = old;
314    }
315
316    /// LuaTeX's `normal` commands (`\savecatcodetable`, …).
317    pub fn do_normal(&mut self) {
318        match self.cur_chr {
319            0 => {
320                // \savecatcodetable n
321                self.scan_int();
322                let n = self.cur_val as u32;
323                let table = self.eqtb.cur_cat_table();
324                let copy = self
325                    .eqtb
326                    .catcode_tables
327                    .get(&table)
328                    .cloned()
329                    .unwrap_or_else(|| crate::eqtb::CodeTable::new(crate::eqtb::initex_catcode));
330                self.eqtb.catcode_tables.insert(n, copy);
331            }
332            1 => {
333                // \initcatcodetable n
334                self.scan_int();
335                let n = self.cur_val as u32;
336                self.eqtb
337                    .catcode_tables
338                    .insert(n, crate::eqtb::CodeTable::new(crate::eqtb::initex_catcode));
339            }
340            2 => {
341                // \setrandomseed: \randomseed shows the seed's absolute
342                // value; the generator ignores its sign anyway.
343                self.scan_int();
344                self.random_seed = self.cur_val.wrapping_abs();
345                self.randoms.init(self.cur_val);
346            }
347            3 => self.append_ext_whatsit(crate::nodes::Whatsit::SavePos),
348            4 | 5 => {
349                // \latelua: a whatsit executed at shipout.
350                let data = self.scan_general_text();
351                self.append_ext_whatsit(crate::nodes::Whatsit::LateLua {
352                    data: crate::nodes::LateData::Tokens(data),
353                    name: Vec::new(),
354                });
355            }
356            6 => {
357                // \expandglyphsinfont <font> <stretch> <shrink> <step>
358                // (microtype's \pdffontexpand).
359                self.scan_font_ident();
360                let f = self.cur_val as u32;
361                self.scan_int();
362                let stretch = self.cur_val;
363                self.scan_int();
364                let shrink = self.cur_val;
365                self.scan_int();
366                let step = self.cur_val;
367                self.set_font_expansion(f, stretch, shrink, step);
368                if self.scan_keyword("autoexpand") {
369                    self.scan_optional_space();
370                    self.luatex_warning("font expansion", "autoexpand not supported");
371                }
372            }
373            _ => {
374                self.scan_font_ident();
375            }
376        }
377    }
378
379    /// Scans a direction specifier (`TLT`, `TRT`, `RTT`, `LTL`).
380    pub fn scan_direction_spec(&mut self) -> i32 {
381        for (i, name) in ["TLT", "TRT", "LTL", "RTT"].iter().enumerate() {
382            if self.scan_keyword(name) {
383                self.scan_optional_space();
384                return i as i32;
385            }
386        }
387        // LuaTeX also takes the value of another direction parameter
388        // (`\pagedir\bodydir`, as babel does), but no number.
389        self.get_x_token();
390        if self.cur_cmd == Cmd::AssignDir {
391            return self.dir_param(self.cur_chr);
392        }
393        self.back_input();
394        self.print_err("Bad direction");
395        self.help(&[]);
396        self.error();
397        0
398    }
399
400    /// Stores a direction parameter (`\textdir` etc.). Observed in
401    /// LuaTeX: the assignment is always local, even with `\global`.
402    pub fn assign_dir_param(&mut self, global: bool, p: u32, d: i32) {
403        let _ = global;
404        if (p as usize) < self.eqtb.dir_par.len() {
405            self.define(
406                false,
407                crate::eqtb::Loc::DirPar(p),
408                crate::eqtb::Val::Int(i64::from(d)),
409            );
410        }
411    }
412
413    /// Reads a direction parameter.
414    pub fn dir_param(&self, p: u32) -> i32 {
415        self.eqtb.dir_par.get(p as usize).map_or(0, |s| s.val)
416    }
417
418    /// Sets the direction of a box register.
419    pub fn set_box_dir(&mut self, _n: u32, _d: i32) {}
420
421    /// `\uniformdeviate n`: METAFONT's `unif_rand(n)`, between 0
422    /// (inclusive) and `n` (exclusive).
423    pub fn uniform_deviate(&mut self, n: i32) -> i32 {
424        self.figures.taint("random numbers");
425        self.randoms.unif_rand(n)
426    }
427
428    /// `\normaldeviate`: METAFONT's `norm_rand`, a scaled normal deviate.
429    pub fn normal_deviate(&mut self) -> i32 {
430        self.figures.taint("random numbers");
431        self.randoms.norm_rand()
432    }
433
434    /// LuaTeX's `\immediateassignment` / `\immediateassigned`.
435    pub fn immediate_assignment(&mut self, group: bool) {
436        if group {
437            let list = self.scan_general_text();
438            let _ = list;
439        } else {
440            self.get_x_token();
441            if self.cur_cmd > Cmd::MAX_NON_PREFIXED {
442                self.prefixed_command();
443            }
444        }
445    }
446
447    /// `\pdfvariable` and `\pdffeedback` in expansion contexts.
448    pub fn backend_expandable(&mut self) {
449        if self.cur_cmd == Cmd::Variable {
450            self.expand_backend_variable();
451        } else {
452            self.expand_backend_feedback();
453        }
454    }
455
456    /// The last saved box/image resource indices.
457    pub fn last_saved_resource(&self, m: u32) -> i32 {
458        use crate::scan::last_item as li;
459        match m {
460            li::LAST_SAVED_IMAGE_RESOURCE_INDEX => self.last_image_index,
461            li::LAST_SAVED_IMAGE_RESOURCE_PAGES => self.last_image_pages,
462            li::LAST_SAVED_BOX_RESOURCE_INDEX => self.last_box_index,
463            _ => 0,
464        }
465    }
466
467    /// `\luadef`: an expandable Lua call, or a non-expandable one when
468    /// `\protected`.
469    pub fn def_lua_call(&mut self, global: bool, protected: bool) {
470        self.get_r_token();
471        let p = self.cur_cs;
472        self.scan_optional_equals();
473        self.scan_int();
474        let cmd = if protected {
475            Cmd::LuaCall
476        } else {
477            Cmd::LuaExpandableCall
478        };
479        let m = crate::eqtb::Meaning::prim(cmd, self.cur_val as u32);
480        self.define(
481            global,
482            crate::eqtb::Loc::Cs(p),
483            crate::eqtb::Val::Meaning(m),
484        );
485    }
486
487    /// The Lua code for the token list of `\directlua` or `\latelua`:
488    /// the list as text, without control sequences named `par` (LuaTeX
489    /// drops them, so blank lines in Lua code are harmless).
490    pub fn lua_code(&mut self, toks: &[Token]) -> Vec<u8> {
491        let par = self.hash.lookup(b"par").map(Token::cs);
492        let kept: Vec<Token>;
493        let toks = if par.is_some_and(|p| toks.contains(&p)) {
494            kept = toks.iter().copied().filter(|t| Some(*t) != par).collect();
495            &kept[..]
496        } else {
497            toks
498        };
499        self.print_to_string(|e| e.show_token_list(toks, 0, toks.len(), i64::MAX))
500    }
501
502    /// Runs Lua code for `\directlua`.
503    pub fn run_lua(&mut self, code: &[u8]) {
504        let Some(host) = self.lua.clone() else {
505            self.print_err("retex: \\directlua needs the Lua interpreter");
506            self.help(&[]);
507            self.error();
508            return;
509        };
510        self.figures.taint("Lua code");
511        let t0 = std::time::Instant::now();
512        let r = host.run(self, "\\directlua", code);
513        crate::timing::LUA.with(|a| a.add(t0));
514        if crate::timing::enabled() && t0.elapsed().as_millis() >= 5 {
515            let head = String::from_utf8_lossy(&code[..code.len().min(70)]).replace('\n', " ");
516            crate::timing::mark(&format!(
517                "slow Lua chunk ({:.1} ms): {head}",
518                t0.elapsed().as_secs_f64() * 1e3
519            ));
520        }
521        if let Err(msg) = r {
522            // RETEX_DEBUG_LUA=1 shows the failing chunk as expanded.
523            if std::env::var_os("RETEX_DEBUG_LUA").is_some() {
524                eprintln!(
525                    "--- failing Lua chunk ---\n{}\n---",
526                    String::from_utf8_lossy(code)
527                );
528            }
529            self.lua_error(&msg);
530        }
531    }
532
533    /// Reports a Lua error the way LuaTeX does.
534    pub fn lua_error(&mut self, msg: &str) {
535        if self.diagnostics.is_some() {
536            self.print_err("LuaTeX error ");
537            self.printer.print_str(msg);
538        } else {
539            // LuaTeX prints the message and its traceback as they are,
540            // without "! " or a line break first (observed).
541            for (i, part) in msg.split('\n').enumerate() {
542                if i > 0 {
543                    self.printer.print_ln();
544                }
545                self.printer.print_str(part);
546            }
547        }
548        self.help(&[
549            "The lua interpreter ran into a problem, so the",
550            "remainder of this lua chunk will be ignored.",
551        ]);
552        self.error();
553    }
554
555    /// Calls a registered Lua function (`\luafunction n`).
556    pub fn call_lua_function(&mut self, n: i32) {
557        // The function receives its slot number. It is called directly:
558        // compiling a chunk calling it took most of the time of documents
559        // making millions of calls (expl3's intarrays, l3regex). Observed:
560        // a slot holding no function is ignored.
561        let Some(host) = self.lua.clone() else {
562            return self.run_lua(b"");
563        };
564        self.figure_lua_function();
565        let t0 = std::time::Instant::now();
566        let r = host.call_function(self, n);
567        crate::timing::LUA.with(|a| a.add(t0));
568        if let Some(Err(msg)) = r {
569            self.lua_error(&msg);
570        }
571    }
572
573    /// Calls stored Lua bytecode (`\luabytecode n`).
574    pub fn call_lua_bytecode(&mut self, n: i32) {
575        let code = format!("return lua.bytecode[{n}]()");
576        self.run_lua(code.as_bytes());
577    }
578
579    /// Expandable Lua calls defined by `\luadef`.
580    pub fn lua_expandable_call(&mut self) {
581        let n = self.cur_chr as i32;
582        self.call_lua_function(n);
583    }
584
585    /// Finishes output files at the end of the run (PDF backend hook).
586    pub fn finish_output(&mut self) {
587        self.finish_backend();
588        if let Some(hook) = self.finish_hook.take() {
589            hook(self);
590        }
591    }
592
593    /// The sink where written file contents are recorded.
594    fn written_sink(&self) -> crate::files::WrittenSink {
595        self.written_sink.clone()
596    }
597
598    /// Stores a muglue `\Umath…` parameter.
599    pub fn store_math_param_glue(&mut self, g: crate::glue::Glue) -> i32 {
600        self.math_glues.push(g);
601        (self.math_glues.len() - 1) as i32
602    }
603
604    /// Retrieves a muglue `\Umath…` parameter.
605    pub fn math_param_glue(&self, v: i32) -> crate::glue::Glue {
606        self.math_glues
607            .get(v as usize)
608            .copied()
609            .unwrap_or(crate::glue::Glue::ZERO)
610    }
611}
612
613/// Avoids an unused import warning while `pg` is not used directly.
614#[allow(dead_code)]
615const _PG: u32 = pg::assign_int::COUNT;
616
617impl Engine {
618    /// Converts a dimension string like `"1.5pt"` to scaled points by
619    /// running TeX's dimension scanner on it (for the Lua API).
620    pub fn dimen_from_string(&mut self, s: &str) -> i64 {
621        let mut toks = crate::the::str_toks(s.as_bytes());
622        let stop = Token::cs(self.cs.frozen_relax);
623        toks.push(stop);
624        let depth = self.input.stack.len();
625        self.ins_list(Rc::from(toks));
626        self.scan_normal_dimen();
627        let v = self.cur_val;
628        // Discard whatever the scanner left, up to the stop marker.
629        while self.input.stack.len() > depth {
630            self.get_token();
631            if self.cur_tok == stop {
632                break;
633            }
634        }
635        i64::from(v)
636    }
637}
638
639/// The LuaTeX whatsit subtype of a whatsit payload.
640fn whatsit_subtype(w: &crate::nodes::Whatsit) -> u16 {
641    use crate::nodes::{Whatsit, whatsit_sub as ws};
642    match w {
643        Whatsit::Open { .. } => ws::OPEN,
644        Whatsit::Write { .. } => ws::WRITE,
645        Whatsit::Close { .. } => ws::CLOSE,
646        Whatsit::Special { .. } => ws::SPECIAL,
647        Whatsit::LateLua { .. } => ws::LATE_LUA,
648        Whatsit::PdfLiteral { late, .. } => {
649            if *late {
650                ws::PDF_LATE_LITERAL
651            } else {
652                ws::PDF_LITERAL
653            }
654        }
655        Whatsit::SavePos => ws::SAVE_POS,
656        Whatsit::PdfColorstack { .. } => ws::PDF_COLORSTACK,
657        Whatsit::PdfSetmatrix { .. } => ws::PDF_SETMATRIX,
658        Whatsit::PdfSave => ws::PDF_SAVE,
659        Whatsit::PdfRestore => ws::PDF_RESTORE,
660        Whatsit::PdfAnnot { .. } => ws::PDF_ANNOT,
661        Whatsit::PdfStartLink { .. } => ws::PDF_START_LINK,
662        Whatsit::PdfEndLink => ws::PDF_END_LINK,
663        Whatsit::PdfDest { .. } => ws::PDF_DEST,
664        Whatsit::PdfRefobj { .. } => ws::PDF_REFOBJ,
665        Whatsit::Other { .. } | Whatsit::UserDefined { .. } => ws::USER_DEFINED,
666        Whatsit::PdfLinkState { .. } => ws::PDF_LINK_STATE,
667    }
668}