1use std::f64::consts::PI;
13
14use libxml::tree::Node;
15use rustc_hash::FxHashMap as HashMap;
16
17use crate::{
18 document::{NodeData, PostDocument, element_children_iter},
19 processor::{ProcessResult, Processor},
20};
21
22const SVG_URI: &str = "http://www.w3.org/2000/svg";
23const DPI: f64 = 96.0;
24
25pub struct SVG {
29 name: String,
30}
31
32impl Default for SVG {
33 fn default() -> Self { Self::new() }
34}
35
36impl SVG {
37 pub fn new() -> Self { SVG { name: "SVG".to_string() } }
38
39 fn process_svg(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
43 let h = node
44 .get_attribute("height")
45 .map(|s| to_px(&s))
46 .unwrap_or(0.0);
47
48 let children: Vec<NodeData> = element_children_iter(node)
50 .filter_map(|child| self.convert_node(doc, &child))
51 .collect();
52
53 let g_transform = format!("translate(0,{:.2}) scale(1,-1)", h);
54 let g = NodeData::Element {
55 tag: "svg:g".to_string(),
56 attributes: Some(HashMap::from_iter([("transform".to_string(), g_transform)])),
57 children,
58 };
59
60 let width = node.get_attribute("width").map(|s| to_px(&s));
62 let height = node.get_attribute("height").map(|s| to_px(&s));
63 let clip = node.get_attribute("clip").as_deref() == Some("true");
64
65 let mut svg_attrs = HashMap::default();
66 svg_attrs.insert("version".to_string(), "1.1".to_string());
67 if let Some(w) = width {
68 svg_attrs.insert("width".to_string(), format!("{:.2}", w));
69 }
70 if let Some(h) = height {
71 svg_attrs.insert("height".to_string(), format!("{:.2}", h));
72 }
73 if !clip {
74 svg_attrs.insert("overflow".to_string(), "visible".to_string());
75 }
76
77 Some(NodeData::Element {
78 tag: "svg:svg".to_string(),
79 attributes: Some(svg_attrs),
80 children: vec![g],
81 })
82 }
83
84 fn convert_node(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
88 let tag = doc.get_qname(node)?;
89 match tag.as_str() {
90 "ltx:picture" => self.convert_picture(doc, node),
91 "ltx:g" => self.convert_g(doc, node),
92 "ltx:path" => self.convert_path(doc, node),
93 "ltx:line" => self.convert_line(doc, node),
94 "ltx:polygon" => self.convert_polygon(doc, node),
95 "ltx:rect" => self.convert_rect(doc, node),
96 "ltx:circle" => self.convert_circle(doc, node),
97 "ltx:ellipse" => self.convert_ellipse(doc, node),
98 "ltx:bezier" => self.convert_bezier(doc, node),
99 "ltx:arc" => self.convert_arc(doc, node),
100 "ltx:wedge" => self.convert_wedge(doc, node),
101 "ltx:dots" => self.convert_dots(doc, node),
102 "ltx:text" => self.convert_text(doc, node),
103 _ => {
104 self.convert_foreign(doc, node)
106 },
107 }
108 }
109
110 fn convert_picture(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
111 let h = node
112 .get_attribute("height")
113 .map(|s| to_px(&s))
114 .unwrap_or(0.0);
115 let children = self.convert_children(doc, node);
116 Some(NodeData::Element {
117 tag: "svg:g".to_string(),
118 attributes: Some(HashMap::from_iter([(
119 "transform".to_string(),
120 format!("translate(0,{:.2}) scale(1,-1)", h),
121 )])),
122 children,
123 })
124 }
125
126 fn convert_g(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
127 let mut attrs = self.copy_valid_attrs(node);
128 if node.get_attribute("framed").as_deref() == Some("true")
130 && node.get_attribute("fillframe").as_deref() == Some("true")
131 {
132 let fill = node
133 .get_attribute("fill")
134 .unwrap_or_else(|| "white".to_string());
135 attrs.insert(
136 "filter".to_string(),
137 format!("url(#bg{})", fill.replace('#', "")),
138 );
139 }
140 let children = self.convert_children(doc, node);
141 Some(NodeData::Element {
142 tag: "svg:g".to_string(),
143 attributes: if attrs.is_empty() { None } else { Some(attrs) },
144 children,
145 })
146 }
147
148 fn convert_path(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
149 let attrs = self.copy_valid_attrs(node);
150 let children = self.convert_children(doc, node);
151 Some(NodeData::Element {
152 tag: "svg:path".to_string(),
153 attributes: if attrs.is_empty() { None } else { Some(attrs) },
154 children,
155 })
156 }
157
158 fn convert_line(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
159 let mut attrs = self.copy_valid_attrs(node);
160 if let Some(points) = node.get_attribute("points") {
161 attrs.insert("d".to_string(), format!("M {}", points));
162 }
163 let children = self.convert_children(doc, node);
164 Some(NodeData::Element {
165 tag: "svg:path".to_string(),
166 attributes: Some(attrs),
167 children,
168 })
169 }
170
171 fn convert_polygon(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
172 let mut attrs = self.copy_valid_attrs(node);
173 if let Some(points) = node.get_attribute("points") {
174 attrs.insert("d".to_string(), format!("M {} z", points));
175 }
176 let children = self.convert_children(doc, node);
177 Some(NodeData::Element {
178 tag: "svg:path".to_string(),
179 attributes: Some(attrs),
180 children,
181 })
182 }
183
184 fn convert_rect(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
185 let attrs = self.copy_valid_attrs(node);
186 if let Some(part) = node.get_attribute("part") {
187 let x = parse_dim(&node.get_attribute("x").unwrap_or_default());
189 let y = parse_dim(&node.get_attribute("y").unwrap_or_default());
190 let w = parse_dim(&node.get_attribute("width").unwrap_or_default());
191 let h = parse_dim(&node.get_attribute("height").unwrap_or_default());
192 let rx = parse_dim(&node.get_attribute("rx").unwrap_or_default());
193 let d = oval_path(&part, x, y, w, h, rx);
194 let mut path_attrs = attrs;
195 path_attrs.insert("d".to_string(), d);
196 Some(NodeData::Element {
197 tag: "svg:path".to_string(),
198 attributes: Some(path_attrs),
199 children: self.convert_children(doc, node),
200 })
201 } else {
202 Some(NodeData::Element {
203 tag: "svg:rect".to_string(),
204 attributes: if attrs.is_empty() { None } else { Some(attrs) },
205 children: self.convert_children(doc, node),
206 })
207 }
208 }
209
210 fn convert_circle(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
211 let mut attrs = self.copy_valid_attrs(node);
212 if let Some(x) = node.get_attribute("x") {
213 attrs.insert("cx".to_string(), x);
214 }
215 if let Some(y) = node.get_attribute("y") {
216 attrs.insert("cy".to_string(), y);
217 }
218 Some(NodeData::Element {
219 tag: "svg:circle".to_string(),
220 attributes: Some(attrs),
221 children: self.convert_children(doc, node),
222 })
223 }
224
225 fn convert_ellipse(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
226 let mut attrs = self.copy_valid_attrs(node);
227 if let Some(x) = node.get_attribute("x") {
228 attrs.insert("cx".to_string(), x);
229 }
230 if let Some(y) = node.get_attribute("y") {
231 attrs.insert("cy".to_string(), y);
232 }
233 Some(NodeData::Element {
234 tag: "svg:ellipse".to_string(),
235 attributes: Some(attrs),
236 children: self.convert_children(doc, node),
237 })
238 }
239
240 fn convert_bezier(&self, doc: &PostDocument, node: &Node) -> Option<NodeData> {
241 let mut attrs = self.copy_valid_attrs(node);
242 if let Some(points) = node.get_attribute("points") {
243 let coords: Vec<f64> = explode_coord(&points);
244 let n = coords.len() / 2;
245 if n >= 2 {
246 let x0 = coords[0];
247 let y0 = coords[1];
248 let cmd = match n {
249 4 => "C",
250 3 => "Q",
251 _ => "T",
252 };
253 let rest: String = coords[2..]
254 .chunks(2)
255 .map(|p| format!("{:.2},{:.2}", p[0], p.get(1).unwrap_or(&0.0)))
256 .collect::<Vec<_>>()
257 .join(" ");
258 attrs.insert(
259 "d".to_string(),
260 format!("M {:.2},{:.2} {} {}", x0, y0, cmd, rest),
261 );
262 }
263 }
264 if node.get_attribute("displayedpoints").is_some() {
265 attrs.insert("stroke-dasharray".to_string(), "2".to_string());
266 }
267 Some(NodeData::Element {
268 tag: "svg:path".to_string(),
269 attributes: Some(attrs),
270 children: self.convert_children(doc, node),
271 })
272 }
273
274 fn convert_arc(&self, _doc: &PostDocument, node: &Node) -> Option<NodeData> {
275 let x = parse_dim(&node.get_attribute("x").unwrap_or_default());
276 let y = parse_dim(&node.get_attribute("y").unwrap_or_default());
277 let r = parse_dim(&node.get_attribute("r").unwrap_or_default());
278 let a1 = parse_dim(&node.get_attribute("angle1").unwrap_or_default());
279 let a2 = parse_dim(&node.get_attribute("angle2").unwrap_or_default());
280
281 let mut bb = a2 - a1;
282 if bb < 0.0 {
283 bb += 360.0;
284 }
285 let large_arc = if bb > 180.0 { 1 } else { 0 };
286
287 let a1r = a1 * PI / 180.0;
288 let a2r = a2 * PI / 180.0;
289 let x1 = x + r * a1r.cos();
290 let y1 = y + r * a1r.sin();
291 let x2 = x + r * a2r.cos();
292 let y2 = y + r * a2r.sin();
293
294 let d = format!(
295 "M {:.2} {:.2} A {:.2} {:.2} 0 {} 1 {:.2} {:.2}",
296 x1, y1, r, r, large_arc, x2, y2
297 );
298
299 Some(NodeData::Element {
300 tag: "svg:path".to_string(),
301 attributes: Some(HashMap::from_iter([("d".to_string(), d)])),
302 children: vec![],
303 })
304 }
305
306 fn convert_wedge(&self, _doc: &PostDocument, node: &Node) -> Option<NodeData> {
307 let x = parse_dim(&node.get_attribute("x").unwrap_or_default());
308 let y = parse_dim(&node.get_attribute("y").unwrap_or_default());
309 let r = parse_dim(&node.get_attribute("r").unwrap_or_default());
310 let a1 = parse_dim(&node.get_attribute("angle1").unwrap_or_default());
311 let a2 = parse_dim(&node.get_attribute("angle2").unwrap_or_default());
312
313 let mut bb = a2 - a1;
314 if bb < 0.0 {
315 bb += 360.0;
316 }
317 let large_arc = if bb > 180.0 { 1 } else { 0 };
318
319 let a1r = a1 * PI / 180.0;
320 let a2r = a2 * PI / 180.0;
321 let x1 = x + r * a1r.cos();
322 let y1 = y + r * a1r.sin();
323 let x2 = x + r * a2r.cos();
324 let y2 = y + r * a2r.sin();
325
326 let d = format!(
327 "M {:.2} {:.2} L {:.2} {:.2} A {:.2} {:.2} 0 {} 1 {:.2} {:.2} z",
328 x, y, x1, y1, r, r, large_arc, x2, y2
329 );
330
331 let attrs = self.copy_valid_attrs(node);
332 let mut all_attrs = attrs;
333 all_attrs.insert("d".to_string(), d);
334
335 Some(NodeData::Element {
336 tag: "svg:path".to_string(),
337 attributes: Some(all_attrs),
338 children: vec![],
339 })
340 }
341
342 fn convert_dots(&self, _doc: &PostDocument, node: &Node) -> Option<NodeData> {
343 let points = node.get_attribute("points").unwrap_or_default();
344 let coords = explode_coord(&points);
345 let dotsize = node
346 .get_attribute("dotsize")
347 .unwrap_or_else(|| "2".to_string());
348
349 let mut circles = Vec::new();
350 for chunk in coords.chunks(2) {
351 if chunk.len() == 2 {
352 circles.push(NodeData::Element {
353 tag: "svg:circle".to_string(),
354 attributes: Some(HashMap::from_iter([
355 ("cx".to_string(), format!("{:.2}", chunk[0])),
356 ("cy".to_string(), format!("{:.2}", chunk[1])),
357 ("r".to_string(), dotsize.clone()),
358 ])),
359 children: vec![],
360 });
361 }
362 }
363
364 Some(NodeData::Element {
365 tag: "svg:g".to_string(),
366 attributes: None,
367 children: circles,
368 })
369 }
370
371 fn convert_text(&self, _doc: &PostDocument, node: &Node) -> Option<NodeData> {
372 let x = node.get_attribute("x").unwrap_or_else(|| "0".to_string());
373 let y = node.get_attribute("y").unwrap_or_else(|| "0".to_string());
374 let text = node.get_content();
375
376 let mut attrs = HashMap::default();
377 attrs.insert("x".to_string(), x);
378 attrs.insert("y".to_string(), y);
379 attrs.insert("transform".to_string(), "scale(1,-1)".to_string());
381
382 if let Some(fontsize) = node.get_attribute("fontsize") {
384 attrs.insert("font-size".to_string(), fontsize);
385 }
386 if let Some(font) = node.get_attribute("font") {
387 if font.contains("italic") {
388 attrs.insert("font-style".to_string(), "italic".to_string());
389 } else if font.contains("slanted") {
390 attrs.insert("font-style".to_string(), "oblique".to_string());
391 } else if font.contains("bold") {
392 attrs.insert("font-weight".to_string(), "bold".to_string());
393 } else if font.contains("smallcaps") {
394 attrs.insert("font-variant".to_string(), "small-caps".to_string());
395 }
396 }
397 if let Some(fill) = node.get_attribute("fill") {
398 attrs.insert("fill".to_string(), fill);
399 }
400
401 Some(NodeData::Element {
402 tag: "svg:text".to_string(),
403 attributes: Some(attrs),
404 children: vec![NodeData::Text(text)],
405 })
406 }
407
408 fn convert_foreign(&self, _doc: &PostDocument, node: &Node) -> Option<NodeData> {
410 let width = node
411 .get_attribute("width")
412 .or_else(|| node.get_attribute("imagewidth"))
413 .unwrap_or_else(|| "1pt".to_string());
414 let height = node
415 .get_attribute("height")
416 .or_else(|| node.get_attribute("imageheight"))
417 .unwrap_or_else(|| "1pt".to_string());
418 let depth = node
419 .get_attribute("depth")
420 .unwrap_or_else(|| "0pt".to_string());
421
422 let h_px = to_px(&height);
423 let d_px = to_px(&depth);
424 let y = h_px + d_px;
425
426 let fo = NodeData::Element {
427 tag: "svg:foreignObject".to_string(),
428 attributes: Some(HashMap::from_iter([
429 ("width".to_string(), format!("{:.2}", to_px(&width))),
430 ("height".to_string(), format!("{:.2}", h_px)),
431 ("overflow".to_string(), "visible".to_string()),
432 ])),
433 children: vec![NodeData::XmlNode(node.clone())],
434 };
435
436 Some(NodeData::Element {
437 tag: "svg:g".to_string(),
438 attributes: Some(HashMap::from_iter([(
439 "transform".to_string(),
440 format!("translate(0,{:.2}) scale(1,-1)", y),
441 )])),
442 children: vec![fo],
443 })
444 }
445
446 fn convert_children(&self, doc: &PostDocument, node: &Node) -> Vec<NodeData> {
448 element_children_iter(node)
449 .filter_map(|child| self.convert_node(doc, &child))
450 .collect()
451 }
452
453 fn copy_valid_attrs(&self, node: &Node) -> HashMap<String, String> {
455 let mut attrs = HashMap::default();
456 let props = node.get_properties();
457 for (key, value) in &props {
458 match key.as_str() {
459 "d" | "r" | "rx" | "ry" | "x" | "y" | "width" | "height" | "cx" | "cy" | "x1" | "y1"
460 | "x2" | "y2" | "fill" | "stroke" | "stroke-width" | "stroke-dasharray"
461 | "stroke-linecap" | "stroke-linejoin" | "opacity" | "fill-opacity" | "stroke-opacity"
462 | "transform" | "style" | "class" => {
463 attrs.insert(key.clone(), value.clone());
464 },
465 "xml:id" => {}, k if k.starts_with('_') => {}, _ => {},
468 }
469 }
470 attrs
471 }
472}
473
474impl Processor for SVG {
475 fn get_name(&self) -> &str { &self.name }
476
477 fn to_process(&self, doc: &PostDocument) -> Vec<Node> {
478 doc.findnodes("//ltx:picture[child::*[not(local-name()='svg')]]")
479 }
480
481 fn process(&mut self, mut doc: PostDocument, nodes: Vec<Node>) -> ProcessResult {
482 if !nodes.is_empty() {
483 doc.add_namespace("svg", SVG_URI);
484 }
485 for node in &nodes {
486 if let Some(svg) = self.process_svg(&doc, node) {
487 let node_mut = node.clone();
488 doc.replace_node(&node_mut, &[svg]);
489 }
490 }
491 Ok(vec![doc])
492 }
493}
494
495fn to_px(s: &str) -> f64 {
502 let trimmed = s.trim();
503 if let Some(pt) = trimmed.strip_suffix("pt") {
504 pt.trim().parse::<f64>().unwrap_or(0.0) * DPI / 72.27
505 } else if let Some(px) = trimmed.strip_suffix("px") {
506 px.trim().parse::<f64>().unwrap_or(0.0)
507 } else if let Some(em) = trimmed.strip_suffix("em") {
508 em.trim().parse::<f64>().unwrap_or(0.0) * 10.0 } else {
510 trimmed.parse::<f64>().unwrap_or(0.0)
511 }
512}
513
514fn parse_dim(s: &str) -> f64 {
516 let trimmed = s.trim();
517 let num: String = trimmed
518 .chars()
519 .take_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-' || *c == '+')
520 .collect();
521 num.parse::<f64>().unwrap_or(0.0)
522}
523
524fn explode_coord(s: &str) -> Vec<f64> {
526 s.split([' ', ','])
527 .filter(|s| !s.is_empty())
528 .filter_map(|s| s.trim().parse::<f64>().ok())
529 .collect()
530}
531
532fn oval_path(part: &str, x: f64, y: f64, w: f64, h: f64, r: f64) -> String {
536 match part {
537 "t" => format!(
538 "M {} {} L {} {} A {} {} 0 0 1 {} {} L {} {} A {} {} 0 0 1 {} {} L {} {}",
539 x,
540 y - h / 2.0,
541 x,
542 y - r,
543 r,
544 r,
545 x + r,
546 y,
547 x + w - r,
548 y,
549 r,
550 r,
551 x + w,
552 y - r,
553 x + w,
554 y - h / 2.0
555 ),
556 "b" => format!(
557 "M {} {} L {} {} A {} {} 0 0 1 {} {} L {} {} A {} {} 0 0 1 {} {} L {} {}",
558 x + w,
559 y - h / 2.0,
560 x + w,
561 y - h + r,
562 r,
563 r,
564 x + w - r,
565 y - h,
566 x + r,
567 y - h,
568 r,
569 r,
570 x,
571 y - h + r,
572 x,
573 y - h / 2.0
574 ),
575 _ => format!("M {} {} L {} {}", x, y, x + w, y), }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 fn approx(a: f64, b: f64) -> bool { (a - b).abs() < 1e-6 }
584
585 #[test]
586 fn to_px_pt_scales_by_dpi_over_72_27() {
587 assert!(approx(to_px("72.27pt"), DPI));
589 assert!(approx(to_px("0pt"), 0.0));
590 }
591
592 #[test]
593 fn to_px_px_is_identity() {
594 assert!(approx(to_px("42px"), 42.0));
595 assert!(approx(to_px(" 3.5px "), 3.5));
596 }
597
598 #[test]
599 fn to_px_em_rough_x10() {
600 assert!(approx(to_px("2em"), 20.0));
602 }
603
604 #[test]
605 fn to_px_bare_number_is_f64() {
606 assert!(approx(to_px("7"), 7.0));
607 assert!(approx(to_px(" -1.5 "), -1.5));
608 }
609
610 #[test]
611 fn to_px_unknown_unit_is_zero() {
612 assert!(approx(to_px("xyz"), 0.0));
614 }
615
616 #[test]
617 fn parse_dim_takes_leading_numeric_prefix() {
618 assert!(approx(parse_dim("12.5pt"), 12.5));
619 assert!(approx(parse_dim("-3.0em"), -3.0));
620 assert!(approx(parse_dim("+7px"), 7.0));
621 }
622
623 #[test]
624 fn parse_dim_no_leading_number_is_zero() {
625 assert!(approx(parse_dim("pt"), 0.0));
626 assert!(approx(parse_dim(""), 0.0));
627 }
628
629 #[test]
630 fn explode_coord_splits_on_space_and_comma() {
631 assert_eq!(explode_coord("1 2 3"), vec![1.0, 2.0, 3.0]);
632 assert_eq!(explode_coord("1,2,3"), vec![1.0, 2.0, 3.0]);
633 assert_eq!(explode_coord("1, 2 3"), vec![1.0, 2.0, 3.0]);
634 }
635
636 #[test]
637 fn explode_coord_skips_empty_and_non_numeric() {
638 assert_eq!(explode_coord(",, 4 5,,"), vec![4.0, 5.0]);
640 assert_eq!(explode_coord("1 abc 2"), vec![1.0, 2.0]);
641 }
642
643 #[test]
644 fn explode_coord_empty_input_is_empty() {
645 assert!(explode_coord("").is_empty());
646 assert!(explode_coord(" ").is_empty());
647 }
648
649 #[test]
650 fn oval_path_top_starts_and_ends_at_half_height() {
651 let s = oval_path("t", 10.0, 20.0, 100.0, 40.0, 5.0);
653 assert!(s.starts_with("M 10 0 ")); assert!(s.ends_with(" 110 0")); }
656
657 #[test]
658 fn oval_path_bottom_traces_reverse_direction() {
659 let s = oval_path("b", 10.0, 20.0, 100.0, 40.0, 5.0);
661 assert!(s.starts_with("M 110 0 "));
662 assert!(s.ends_with(" 10 0"));
663 }
664
665 #[test]
666 fn oval_path_unknown_part_uses_simple_line_fallback() {
667 let s = oval_path("?", 0.0, 0.0, 50.0, 10.0, 2.0);
668 assert_eq!(s, "M 0 0 L 50 0");
669 }
670}