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        // Expand the list as \edef does, with \endwrite as terminator.
256        let mut toks: Vec<Token> = Vec::with_capacity(list.len() + 2);
257        toks.push(Token::RIGHT_BRACE);
258        toks.push(Token::cs(self.cs.end_write));
259        let q: TokenList = Rc::from(toks);
260        self.ins_list(q);
261        self.begin_token_list(list.clone(), TokKind::WriteText);
262        let q: TokenList = Rc::from(vec![Token::LEFT_BRACE]);
263        self.ins_list(q);
264        let old_mode = self.mode;
265        self.mode = crate::maincontrol::Mode::None;
266        self.cur_cs = self.cs.end_write;
267        let save_def = self.def_ref_cs;
268        self.def_ref_cs = self.cs.end_write;
269        let expanded = self.scan_toks(false, true);
270        self.def_ref_cs = save_def;
271        self.get_token();
272        if self.cur_tok != Token::cs(self.cs.end_write) {
273            self.print_err("Unbalanced write command");
274            self.help(&[
275                "On this page there's a \\write with fewer real {'s than }'s.",
276                "I can't handle that very well; good luck.",
277            ]);
278            self.error();
279            loop {
280                self.get_token();
281                if self.cur_tok == Token::cs(self.cs.end_write) {
282                    break;
283                }
284            }
285        }
286        self.mode = old_mode;
287        let old = self.printer.selector;
288        if self.diagnostics.is_some()
289            && j_is_terminal(stream, self.printer.write_open(stream as u32))
290        {
291            let text = self
292                .print_to_string(|e| e.token_show_list(&expanded, 0, expanded.len(), 10_000_000));
293            self.record_latex_warning(&String::from_utf8_lossy(&text));
294        }
295        // Streams 0-127 are files; a negative stream writes to the log
296        // only, any other stream (or a closed one) to the terminal and log.
297        let j = stream;
298        let to_file = (0..=127).contains(&j) && self.printer.write_open(j as u32);
299        if to_file {
300            self.printer.selector = crate::print::Selector::File(j as u32);
301        } else {
302            if j < 0 && old == crate::print::Selector::TermAndLog {
303                self.printer.selector = crate::print::Selector::LogOnly;
304            }
305            self.printer.print_nl("");
306        }
307        self.token_show_list(&expanded, 0, expanded.len(), 10_000_000);
308        self.printer.print_ln();
309        self.printer.selector = old;
310    }
311
312    /// LuaTeX's `normal` commands (`\savecatcodetable`, …).
313    pub fn do_normal(&mut self) {
314        match self.cur_chr {
315            0 => {
316                // \savecatcodetable n
317                self.scan_int();
318                let n = self.cur_val as u32;
319                let table = self.eqtb.cur_cat_table();
320                let copy = self
321                    .eqtb
322                    .catcode_tables
323                    .get(&table)
324                    .cloned()
325                    .unwrap_or_else(|| crate::eqtb::CodeTable::new(crate::eqtb::initex_catcode));
326                self.eqtb.catcode_tables.insert(n, copy);
327            }
328            1 => {
329                // \initcatcodetable n
330                self.scan_int();
331                let n = self.cur_val as u32;
332                self.eqtb
333                    .catcode_tables
334                    .insert(n, crate::eqtb::CodeTable::new(crate::eqtb::initex_catcode));
335            }
336            2 => {
337                // \setrandomseed: \randomseed shows the seed's absolute
338                // value; the generator ignores its sign anyway.
339                self.scan_int();
340                self.random_seed = self.cur_val.wrapping_abs();
341                self.randoms.init(self.cur_val);
342            }
343            3 => self.append_ext_whatsit(crate::nodes::Whatsit::SavePos),
344            4 | 5 => {
345                // \latelua: a whatsit executed at shipout.
346                let data = self.scan_general_text();
347                self.append_ext_whatsit(crate::nodes::Whatsit::LateLua {
348                    data: crate::nodes::LateData::Tokens(data),
349                    name: Vec::new(),
350                });
351            }
352            6 => {
353                // \expandglyphsinfont <font> <stretch> <shrink> <step>
354                // (microtype's \pdffontexpand).
355                self.scan_font_ident();
356                let f = self.cur_val as u32;
357                self.scan_int();
358                let stretch = self.cur_val;
359                self.scan_int();
360                let shrink = self.cur_val;
361                self.scan_int();
362                let step = self.cur_val;
363                self.set_font_expansion(f, stretch, shrink, step);
364                if self.scan_keyword("autoexpand") {
365                    self.scan_optional_space();
366                    self.luatex_warning("font expansion", "autoexpand not supported");
367                }
368            }
369            _ => {
370                self.scan_font_ident();
371            }
372        }
373    }
374
375    /// Scans a direction specifier (`TLT`, `TRT`, `RTT`, `LTL`).
376    pub fn scan_direction_spec(&mut self) -> i32 {
377        for (i, name) in ["TLT", "TRT", "LTL", "RTT"].iter().enumerate() {
378            if self.scan_keyword(name) {
379                self.scan_optional_space();
380                return i as i32;
381            }
382        }
383        // LuaTeX also takes the value of another direction parameter
384        // (`\pagedir\bodydir`, as babel does), but no number.
385        self.get_x_token();
386        if self.cur_cmd == Cmd::AssignDir {
387            return self.dir_param(self.cur_chr);
388        }
389        self.back_input();
390        self.print_err("Bad direction");
391        self.help(&[]);
392        self.error();
393        0
394    }
395
396    /// Stores a direction parameter (`\textdir` etc.). Observed in
397    /// LuaTeX: the assignment is always local, even with `\global`.
398    pub fn assign_dir_param(&mut self, global: bool, p: u32, d: i32) {
399        let _ = global;
400        if (p as usize) < self.eqtb.dir_par.len() {
401            self.define(
402                false,
403                crate::eqtb::Loc::DirPar(p),
404                crate::eqtb::Val::Int(i64::from(d)),
405            );
406        }
407    }
408
409    /// Reads a direction parameter.
410    pub fn dir_param(&self, p: u32) -> i32 {
411        self.eqtb.dir_par.get(p as usize).map_or(0, |s| s.val)
412    }
413
414    /// Sets the direction of a box register.
415    pub fn set_box_dir(&mut self, _n: u32, _d: i32) {}
416
417    /// `\uniformdeviate n`: METAFONT's `unif_rand(n)`, between 0
418    /// (inclusive) and `n` (exclusive).
419    pub fn uniform_deviate(&mut self, n: i32) -> i32 {
420        self.randoms.unif_rand(n)
421    }
422
423    /// `\normaldeviate`: METAFONT's `norm_rand`, a scaled normal deviate.
424    pub fn normal_deviate(&mut self) -> i32 {
425        self.randoms.norm_rand()
426    }
427
428    /// LuaTeX's `\immediateassignment` / `\immediateassigned`.
429    pub fn immediate_assignment(&mut self, group: bool) {
430        if group {
431            let list = self.scan_general_text();
432            let _ = list;
433        } else {
434            self.get_x_token();
435            if self.cur_cmd > Cmd::MAX_NON_PREFIXED {
436                self.prefixed_command();
437            }
438        }
439    }
440
441    /// `\pdfvariable` and `\pdffeedback` in expansion contexts.
442    pub fn backend_expandable(&mut self) {
443        if self.cur_cmd == Cmd::Variable {
444            self.expand_backend_variable();
445        } else {
446            self.expand_backend_feedback();
447        }
448    }
449
450    /// The last saved box/image resource indices.
451    pub fn last_saved_resource(&self, m: u32) -> i32 {
452        use crate::scan::last_item as li;
453        match m {
454            li::LAST_SAVED_IMAGE_RESOURCE_INDEX => self.last_image_index,
455            li::LAST_SAVED_IMAGE_RESOURCE_PAGES => self.last_image_pages,
456            li::LAST_SAVED_BOX_RESOURCE_INDEX => self.last_box_index,
457            _ => 0,
458        }
459    }
460
461    /// `\luadef`: an expandable Lua call, or a non-expandable one when
462    /// `\protected`.
463    pub fn def_lua_call(&mut self, global: bool, protected: bool) {
464        self.get_r_token();
465        let p = self.cur_cs;
466        self.scan_optional_equals();
467        self.scan_int();
468        let cmd = if protected {
469            Cmd::LuaCall
470        } else {
471            Cmd::LuaExpandableCall
472        };
473        let m = crate::eqtb::Meaning::prim(cmd, self.cur_val as u32);
474        self.define(
475            global,
476            crate::eqtb::Loc::Cs(p),
477            crate::eqtb::Val::Meaning(m),
478        );
479    }
480
481    /// The Lua code for the token list of `\directlua` or `\latelua`:
482    /// the list as text, without control sequences named `par` (LuaTeX
483    /// drops them, so blank lines in Lua code are harmless).
484    pub fn lua_code(&mut self, toks: &[Token]) -> Vec<u8> {
485        let par = self.hash.lookup(b"par").map(Token::cs);
486        let kept: Vec<Token>;
487        let toks = if par.is_some_and(|p| toks.contains(&p)) {
488            kept = toks.iter().copied().filter(|t| Some(*t) != par).collect();
489            &kept[..]
490        } else {
491            toks
492        };
493        self.print_to_string(|e| e.show_token_list(toks, 0, toks.len(), i64::MAX))
494    }
495
496    /// Runs Lua code for `\directlua`.
497    pub fn run_lua(&mut self, code: &[u8]) {
498        let Some(host) = self.lua.clone() else {
499            self.print_err("retex: \\directlua needs the Lua interpreter");
500            self.help(&[]);
501            self.error();
502            return;
503        };
504        let t0 = std::time::Instant::now();
505        let r = host.run(self, "\\directlua", code);
506        crate::timing::LUA.with(|a| a.add(t0));
507        if crate::timing::enabled() && t0.elapsed().as_millis() >= 5 {
508            let head = String::from_utf8_lossy(&code[..code.len().min(70)]).replace('\n', " ");
509            crate::timing::mark(&format!(
510                "slow Lua chunk ({:.1} ms): {head}",
511                t0.elapsed().as_secs_f64() * 1e3
512            ));
513        }
514        if let Err(msg) = r {
515            // RETEX_DEBUG_LUA=1 shows the failing chunk as expanded.
516            if std::env::var_os("RETEX_DEBUG_LUA").is_some() {
517                eprintln!(
518                    "--- failing Lua chunk ---\n{}\n---",
519                    String::from_utf8_lossy(code)
520                );
521            }
522            self.lua_error(&msg);
523        }
524    }
525
526    /// Reports a Lua error the way LuaTeX does.
527    pub fn lua_error(&mut self, msg: &str) {
528        if self.diagnostics.is_some() {
529            self.print_err("LuaTeX error ");
530            self.printer.print_str(msg);
531        } else {
532            // LuaTeX prints the message and its traceback as they are,
533            // without "! " or a line break first (observed).
534            for (i, part) in msg.split('\n').enumerate() {
535                if i > 0 {
536                    self.printer.print_ln();
537                }
538                self.printer.print_str(part);
539            }
540        }
541        self.help(&[
542            "The lua interpreter ran into a problem, so the",
543            "remainder of this lua chunk will be ignored.",
544        ]);
545        self.error();
546    }
547
548    /// Calls a registered Lua function (`\luafunction n`).
549    pub fn call_lua_function(&mut self, n: i32) {
550        // The function receives its slot number. It is called directly:
551        // compiling a chunk calling it took most of the time of documents
552        // making millions of calls (expl3's intarrays, l3regex). Observed:
553        // a slot holding no function is ignored.
554        let Some(host) = self.lua.clone() else {
555            return self.run_lua(b"");
556        };
557        let t0 = std::time::Instant::now();
558        let r = host.call_function(self, n);
559        crate::timing::LUA.with(|a| a.add(t0));
560        if let Some(Err(msg)) = r {
561            self.lua_error(&msg);
562        }
563    }
564
565    /// Calls stored Lua bytecode (`\luabytecode n`).
566    pub fn call_lua_bytecode(&mut self, n: i32) {
567        let code = format!("return lua.bytecode[{n}]()");
568        self.run_lua(code.as_bytes());
569    }
570
571    /// Expandable Lua calls defined by `\luadef`.
572    pub fn lua_expandable_call(&mut self) {
573        let n = self.cur_chr as i32;
574        self.call_lua_function(n);
575    }
576
577    /// Finishes output files at the end of the run (PDF backend hook).
578    pub fn finish_output(&mut self) {
579        self.finish_backend();
580        if let Some(hook) = self.finish_hook.take() {
581            hook(self);
582        }
583    }
584
585    /// The sink where written file contents are recorded.
586    fn written_sink(&self) -> crate::files::WrittenSink {
587        self.written_sink.clone()
588    }
589
590    /// Stores a muglue `\Umath…` parameter.
591    pub fn store_math_param_glue(&mut self, g: crate::glue::Glue) -> i32 {
592        self.math_glues.push(g);
593        (self.math_glues.len() - 1) as i32
594    }
595
596    /// Retrieves a muglue `\Umath…` parameter.
597    pub fn math_param_glue(&self, v: i32) -> crate::glue::Glue {
598        self.math_glues
599            .get(v as usize)
600            .copied()
601            .unwrap_or(crate::glue::Glue::ZERO)
602    }
603}
604
605/// Avoids an unused import warning while `pg` is not used directly.
606#[allow(dead_code)]
607const _PG: u32 = pg::assign_int::COUNT;
608
609impl Engine {
610    /// Converts a dimension string like `"1.5pt"` to scaled points by
611    /// running TeX's dimension scanner on it (for the Lua API).
612    pub fn dimen_from_string(&mut self, s: &str) -> i64 {
613        let mut toks = crate::the::str_toks(s.as_bytes());
614        let stop = Token::cs(self.cs.frozen_relax);
615        toks.push(stop);
616        let depth = self.input.stack.len();
617        self.ins_list(Rc::from(toks));
618        self.scan_normal_dimen();
619        let v = self.cur_val;
620        // Discard whatever the scanner left, up to the stop marker.
621        while self.input.stack.len() > depth {
622            self.get_token();
623            if self.cur_tok == stop {
624                break;
625            }
626        }
627        i64::from(v)
628    }
629}
630
631/// The LuaTeX whatsit subtype of a whatsit payload.
632fn whatsit_subtype(w: &crate::nodes::Whatsit) -> u16 {
633    use crate::nodes::{Whatsit, whatsit_sub as ws};
634    match w {
635        Whatsit::Open { .. } => ws::OPEN,
636        Whatsit::Write { .. } => ws::WRITE,
637        Whatsit::Close { .. } => ws::CLOSE,
638        Whatsit::Special { .. } => ws::SPECIAL,
639        Whatsit::LateLua { .. } => ws::LATE_LUA,
640        Whatsit::PdfLiteral { late, .. } => {
641            if *late {
642                ws::PDF_LATE_LITERAL
643            } else {
644                ws::PDF_LITERAL
645            }
646        }
647        Whatsit::SavePos => ws::SAVE_POS,
648        Whatsit::PdfColorstack { .. } => ws::PDF_COLORSTACK,
649        Whatsit::PdfSetmatrix { .. } => ws::PDF_SETMATRIX,
650        Whatsit::PdfSave => ws::PDF_SAVE,
651        Whatsit::PdfRestore => ws::PDF_RESTORE,
652        Whatsit::PdfAnnot { .. } => ws::PDF_ANNOT,
653        Whatsit::PdfStartLink { .. } => ws::PDF_START_LINK,
654        Whatsit::PdfEndLink => ws::PDF_END_LINK,
655        Whatsit::PdfDest { .. } => ws::PDF_DEST,
656        Whatsit::PdfRefobj { .. } => ws::PDF_REFOBJ,
657        Whatsit::Other { .. } | Whatsit::UserDefined { .. } => ws::USER_DEFINED,
658        Whatsit::PdfLinkState { .. } => ws::PDF_LINK_STATE,
659    }
660}