1#[cfg(feature = "raster")]
17const EMBEDDED_FONT: &[u8] = include_bytes!("../fonts/Go-Regular.ttf");
18#[cfg(feature = "raster")]
20const EMBEDDED_FONT_FAMILY: &str = "Go";
21
22#[cfg(feature = "math")]
23pub mod math;
24
25#[cfg(feature = "raster")]
28pub fn to_png(svg: &str, scale: f32) -> Result<Vec<u8>, String> {
29 use resvg::{tiny_skia, usvg};
30
31 if !scale.is_finite() || scale <= 0.0 {
32 return Err("scale must be a positive finite number".into());
33 }
34
35 let mut opt = usvg::Options::default();
36 {
37 let db = opt.fontdb_mut();
38 db.load_font_data(EMBEDDED_FONT.to_vec());
39 db.set_serif_family(EMBEDDED_FONT_FAMILY);
40 db.set_sans_serif_family(EMBEDDED_FONT_FAMILY);
41 db.set_monospace_family(EMBEDDED_FONT_FAMILY);
42 db.set_cursive_family(EMBEDDED_FONT_FAMILY);
43 db.set_fantasy_family(EMBEDDED_FONT_FAMILY);
44 }
45 let tree = usvg::Tree::from_str(svg, &opt).map_err(|e| e.to_string())?;
46
47 let size = tree.size();
48 let w = ((size.width() * scale).ceil() as u32).max(1);
49 let h = ((size.height() * scale).ceil() as u32).max(1);
50 let mut pixmap = tiny_skia::Pixmap::new(w, h).ok_or("failed to allocate pixmap")?;
51 resvg::render(
52 &tree,
53 tiny_skia::Transform::from_scale(scale, scale),
54 &mut pixmap.as_mut(),
55 );
56 pixmap.encode_png().map_err(|e| e.to_string())
57}
58
59#[cfg(feature = "raster")]
61pub fn to_pdf(svg: &str) -> Result<Vec<u8>, String> {
62 use svg2pdf::usvg;
63
64 let mut opt = usvg::Options::default();
65 {
66 let db = opt.fontdb_mut();
67 db.load_font_data(EMBEDDED_FONT.to_vec());
68 db.set_serif_family(EMBEDDED_FONT_FAMILY);
69 db.set_sans_serif_family(EMBEDDED_FONT_FAMILY);
70 db.set_monospace_family(EMBEDDED_FONT_FAMILY);
71 db.set_cursive_family(EMBEDDED_FONT_FAMILY);
72 db.set_fantasy_family(EMBEDDED_FONT_FAMILY);
73 }
74 let tree = usvg::Tree::from_str(svg, &opt).map_err(|e| e.to_string())?;
75
76 svg2pdf::to_pdf(
77 &tree,
78 svg2pdf::ConversionOptions::default(),
79 svg2pdf::PageOptions::default(),
80 )
81 .map_err(|e| e.to_string())
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[cfg(feature = "raster")]
89 const SVG: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"20\" viewBox=\"0 0 40 20\"><rect x=\"2\" y=\"2\" width=\"36\" height=\"16\" fill=\"none\" stroke=\"black\"/></svg>";
90 #[cfg(feature = "raster")]
91 const SVG_TEXT: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"120\" height=\"40\" viewBox=\"0 0 120 40\" font-family=\"sans-serif\" font-size=\"16\"><text x=\"4\" y=\"24\">Hello world</text></svg>";
92
93 #[cfg(feature = "raster")]
94 #[test]
95 fn png_has_magic_and_scales() {
96 let one = to_png(SVG, 1.0).unwrap();
97 assert_eq!(&one[..4], &[0x89, 0x50, 0x4E, 0x47]); let two = to_png(SVG, 2.0).unwrap();
99 assert!(two.len() > one.len()); }
101
102 #[cfg(feature = "raster")]
103 #[test]
104 fn png_rejects_invalid_scale() {
105 assert!(to_png(SVG, 0.0).is_err());
106 assert!(to_png(SVG, f32::NAN).is_err());
107 }
108
109 #[cfg(feature = "raster")]
110 #[test]
111 fn gradient_fill_rasterizes_offline() {
112 const GRAD: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"20\" viewBox=\"0 0 40 20\"><defs><linearGradient id=\"grad0\" gradientUnits=\"objectBoundingBox\" x1=\"0\" y1=\"0.5\" x2=\"1\" y2=\"0.5\"><stop offset=\"0\" stop-color=\"black\"/><stop offset=\"1\" stop-color=\"white\"/></linearGradient></defs><rect x=\"2\" y=\"2\" width=\"36\" height=\"16\" fill=\"url(#grad0)\"/></svg>";
116 const FLAT: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"20\" viewBox=\"0 0 40 20\"><rect x=\"2\" y=\"2\" width=\"36\" height=\"16\" fill=\"gray\"/></svg>";
117 let grad = to_png(GRAD, 1.0).unwrap();
118 assert_eq!(&grad[..4], &[0x89, 0x50, 0x4E, 0x47]);
119 let flat = to_png(FLAT, 1.0).unwrap();
122 assert_ne!(grad, flat);
123 assert_eq!(&to_pdf(GRAD).unwrap()[..4], b"%PDF");
124 }
125
126 #[cfg(feature = "math")]
127 #[test]
128 fn math_fragment_root_is_unitless_px() {
129 let span = math::render_math("x", 11.0).unwrap();
133 let head = &span.svg[..span.svg.find('>').unwrap()];
134 assert!(!head.contains("pt\""), "{head}");
135 let w_attr: f64 = head
137 .split("width=\"")
138 .nth(1)
139 .and_then(|t| t.split('\"').next())
140 .unwrap()
141 .parse()
142 .unwrap();
143 assert!(
144 (w_attr - span.width * 96.0).abs() < 0.01,
145 "{w_attr} vs {}",
146 span.width * 96.0
147 );
148 }
149
150 #[cfg(all(feature = "math", feature = "raster"))]
151 #[test]
152 fn texlabels_math_renders_through_png_and_pdf() {
153 rpic_core::set_math_renderer(math::render_math);
157 let src = "texlabels = 1\nbox \"$-\\\\frac{T}{2}$\" wid 1 ht 0.7";
158 let d = rpic_core::compile(src).unwrap();
159 let svg = rpic_core::to_svg(&d);
160 assert!(svg.contains("<svg x=\""), "{svg}");
161 assert!(!svg.contains("frac"), "raw TeX must not leak: {svg}");
162
163 let png = to_png(&svg, 2.0).unwrap();
164 assert_eq!(&png[..4], &[0x89, 0x50, 0x4E, 0x47]);
165 let blank_d = rpic_core::compile("box wid 1 ht 0.7").unwrap();
168 let blank = to_png(&rpic_core::to_svg(&blank_d), 2.0).unwrap();
169 assert!(
170 png.len() > blank.len(),
171 "png {} <= blank {}",
172 png.len(),
173 blank.len()
174 );
175
176 assert_eq!(&to_pdf(&svg).unwrap()[..4], b"%PDF");
177 }
178
179 #[cfg(feature = "raster")]
180 #[test]
181 fn pdf_has_magic() {
182 let pdf = to_pdf(SVG).unwrap();
183 assert_eq!(&pdf[..4], b"%PDF");
184 }
185
186 #[cfg(feature = "raster")]
187 #[test]
188 fn bundled_font_rasterizes_text() {
189 let with_text = to_png(SVG_TEXT, 1.0).unwrap();
193 let blank = to_png(
194 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"120\" height=\"40\" viewBox=\"0 0 120 40\"></svg>",
195 1.0,
196 )
197 .unwrap();
198 assert!(
199 with_text.len() > blank.len() + 200,
200 "text PNG {} vs blank {}",
201 with_text.len(),
202 blank.len()
203 );
204 }
205}