Skip to main content

latexml_post/
math_images.rs

1//! Math image generation processor.
2//!
3//! Port of `LaTeXML::Post::MathImages`.
4//! Extends both MathProcessor and LaTeXImages to generate images for math.
5
6use libxml::tree::Node;
7
8use crate::{
9  document::PostDocument,
10  math_processor::{MathConversion, MathProcessor},
11  processor::{ProcessResult, Processor},
12};
13
14const MIME_TYPES: &[(&str, &str)] = &[
15  ("gif", "image/gif"),
16  ("jpeg", "image/jpeg"),
17  ("png", "image/png"),
18  ("svg", "image/svg+xml"),
19];
20
21/// MathImages post-processor: generates images for math.
22///
23/// Port of `LaTeXML::Post::MathImages`.
24pub struct MathImages {
25  name:               String,
26  is_secondary:       bool,
27  resource_directory: String,
28  resource_prefix:    String,
29  image_type:         String,
30}
31
32impl MathImages {
33  pub fn new(image_type: &str) -> Self {
34    MathImages {
35      name:               "MathImages".to_string(),
36      is_secondary:       false,
37      resource_directory: "mi".to_string(),
38      resource_prefix:    "mi".to_string(),
39      image_type:         image_type.to_string(),
40    }
41  }
42
43  /// Extract the TeX string for a Math node.
44  ///
45  /// Port of `MathImages::extractTeX`.
46  fn extract_tex(&self, node: &Node) -> Option<String> {
47    let mode = node
48      .get_attribute("mode")
49      .map(|m| m.to_uppercase())
50      .unwrap_or_else(|| "INLINE".to_string());
51    let mut tex = node.get_attribute("tex")?;
52    let display = if tex.trim_start().starts_with("\\displaystyle") {
53      tex = tex
54        .trim_start()
55        .strip_prefix("\\displaystyle")?
56        .trim_start()
57        .to_string();
58      "DISPLAY"
59    } else {
60      &mode
61    };
62    if tex.trim().is_empty() {
63      return None;
64    }
65    Some(format!("\\begin{} {}\\end{}", display, tex, display))
66  }
67}
68
69impl Processor for MathImages {
70  fn get_name(&self) -> &str { &self.name }
71
72  fn to_process(&self, doc: &PostDocument) -> Vec<Node> { doc.findnodes("//ltx:Math") }
73
74  fn resource_directory(&self) -> Option<&str> { Some(&self.resource_directory) }
75
76  fn resource_prefix(&self) -> Option<&str> { Some(&self.resource_prefix) }
77
78  fn process(&mut self, doc: PostDocument, _nodes: Vec<Node>) -> ProcessResult { Ok(vec![doc]) }
79}
80
81impl MathProcessor for MathImages {
82  fn convert_node(&self, doc: &PostDocument, xmath: &Node) -> Option<MathConversion> {
83    let math = xmath.get_parent()?;
84    let tex = self.extract_tex(&math)?;
85
86    let key = format!("MathImages:{}:{}", self.image_type, tex);
87    if let Some(cached) = doc.cache_lookup(&key) {
88      // Parse cached value: "path;width;height;depth"
89      let parts: Vec<&str> = cached.split(';').collect();
90      if parts.len() == 4 {
91        let mimetype = MIME_TYPES
92          .iter()
93          .find(|(ext, _)| *ext == self.image_type)
94          .map(|(_, mime)| mime.to_string());
95        return Some(MathConversion {
96          processor_name: self.name.clone(),
97          mimetype,
98          xml: None,
99          string: None,
100          src: Some(parts[0].to_string()),
101          width: Some(parts[1].to_string()),
102          height: Some(parts[2].to_string()),
103          depth: Some(parts[3].to_string()),
104        });
105      }
106    }
107
108    Warn!(
109      "missing_file",
110      "math_images",
111      "MathImages: no cached image for '{}'",
112      key
113    );
114    Some(MathConversion {
115      processor_name: self.name.clone(),
116      mimetype:       None,
117      xml:            None,
118      string:         None,
119      src:            None,
120      width:          None,
121      height:         None,
122      depth:          None,
123    })
124  }
125
126  fn raw_id_suffix(&self) -> &str { ".mi" }
127
128  fn is_secondary(&self) -> bool { self.is_secondary }
129
130  fn preprocess(&self, _doc: &PostDocument, nodes: &[Node]) {
131    Info!(
132      "math_images",
133      "generate",
134      "MathImages: would generate {} images",
135      nodes.len()
136    );
137  }
138}