1#[cfg(feature = "raster")]
19const EMBEDDED_FONT: &[u8] = include_bytes!("../fonts/Go-Regular.ttf");
20const EMBEDDED_FONT_BOLD: &[u8] = include_bytes!("../fonts/Go-Bold.ttf");
21const EMBEDDED_FONT_ITALIC: &[u8] = include_bytes!("../fonts/Go-Italic.ttf");
22const EMBEDDED_FONT_BOLD_ITALIC: &[u8] = include_bytes!("../fonts/Go-Bold-Italic.ttf");
23const EMBEDDED_FONT_MONO: &[u8] = include_bytes!("../fonts/Go-Mono.ttf");
24const EMBEDDED_FONT_MONO_BOLD: &[u8] = include_bytes!("../fonts/Go-Mono-Bold.ttf");
25#[cfg(feature = "raster")]
27const EMBEDDED_FONT_FAMILY: &str = "Go";
28const EMBEDDED_MONO_FAMILY: &str = "Go Mono";
30
31#[cfg(feature = "math")]
32pub mod math;
33
34#[cfg(feature = "raster")]
37fn load_embedded_fonts(db: &mut resvg::usvg::fontdb::Database) {
38 for data in [
39 EMBEDDED_FONT,
40 EMBEDDED_FONT_BOLD,
41 EMBEDDED_FONT_ITALIC,
42 EMBEDDED_FONT_BOLD_ITALIC,
43 EMBEDDED_FONT_MONO,
44 EMBEDDED_FONT_MONO_BOLD,
45 ] {
46 db.load_font_data(data.to_vec());
47 }
48 db.set_serif_family(EMBEDDED_FONT_FAMILY);
49 db.set_sans_serif_family(EMBEDDED_FONT_FAMILY);
50 db.set_monospace_family(EMBEDDED_MONO_FAMILY);
51 db.set_cursive_family(EMBEDDED_FONT_FAMILY);
52 db.set_fantasy_family(EMBEDDED_FONT_FAMILY);
53}
54
55#[cfg(feature = "raster")]
56pub fn to_png(svg: &str, scale: f32) -> Result<Vec<u8>, String> {
57 use resvg::{tiny_skia, usvg};
58
59 if !scale.is_finite() || scale <= 0.0 {
60 return Err("scale must be a positive finite number".into());
61 }
62
63 let mut opt = usvg::Options::default();
64 load_embedded_fonts(opt.fontdb_mut());
65 let tree = usvg::Tree::from_str(svg, &opt).map_err(|e| e.to_string())?;
66
67 let size = tree.size();
68 let w = ((size.width() * scale).ceil() as u32).max(1);
69 let h = ((size.height() * scale).ceil() as u32).max(1);
70 let mut pixmap = tiny_skia::Pixmap::new(w, h).ok_or("failed to allocate pixmap")?;
71 resvg::render(
72 &tree,
73 tiny_skia::Transform::from_scale(scale, scale),
74 &mut pixmap.as_mut(),
75 );
76 pixmap.encode_png().map_err(|e| e.to_string())
77}
78
79#[cfg(feature = "raster")]
81pub fn to_pdf(svg: &str) -> Result<Vec<u8>, String> {
82 use svg2pdf::usvg;
83
84 let mut opt = usvg::Options::default();
85 load_embedded_fonts(opt.fontdb_mut());
86 let tree = usvg::Tree::from_str(svg, &opt).map_err(|e| e.to_string())?;
87
88 svg2pdf::to_pdf(
89 &tree,
90 svg2pdf::ConversionOptions::default(),
91 svg2pdf::PageOptions::default(),
92 )
93 .map_err(|e| e.to_string())
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[cfg(feature = "raster")]
101 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>";
102 #[cfg(feature = "raster")]
103 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>";
104
105 #[cfg(feature = "raster")]
106 #[test]
107 fn png_has_magic_and_scales() {
108 let one = to_png(SVG, 1.0).unwrap();
109 assert_eq!(&one[..4], &[0x89, 0x50, 0x4E, 0x47]); let two = to_png(SVG, 2.0).unwrap();
111 assert!(two.len() > one.len()); }
113
114 #[cfg(feature = "raster")]
115 #[test]
116 fn png_rejects_invalid_scale() {
117 assert!(to_png(SVG, 0.0).is_err());
118 assert!(to_png(SVG, f32::NAN).is_err());
119 }
120
121 #[cfg(feature = "raster")]
122 #[test]
123 fn gradient_fill_rasterizes_offline() {
124 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>";
128 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>";
129 let grad = to_png(GRAD, 1.0).unwrap();
130 assert_eq!(&grad[..4], &[0x89, 0x50, 0x4E, 0x47]);
131 let flat = to_png(FLAT, 1.0).unwrap();
134 assert_ne!(grad, flat);
135 assert_eq!(&to_pdf(GRAD).unwrap()[..4], b"%PDF");
136 }
137
138 #[cfg(feature = "math")]
139 #[test]
140 fn math_fragment_root_is_unitless_px() {
141 let span = math::render_math("x", 11.0).unwrap();
145 let head = &span.svg[..span.svg.find('>').unwrap()];
146 assert!(!head.contains("pt\""), "{head}");
147 let w_attr: f64 = head
149 .split("width=\"")
150 .nth(1)
151 .and_then(|t| t.split('\"').next())
152 .unwrap()
153 .parse()
154 .unwrap();
155 assert!(
156 (w_attr - span.width * 96.0).abs() < 0.01,
157 "{w_attr} vs {}",
158 span.width * 96.0
159 );
160 }
161
162 #[cfg(all(feature = "math", feature = "raster"))]
163 #[test]
164 fn texlabels_math_renders_through_png_and_pdf() {
165 rpic_core::set_math_renderer(math::render_math);
169 let src = "texlabels = 1\nbox \"$-\\frac{T}{2}$\" wid 1 ht 0.7";
170 let d = rpic_core::compile(src).unwrap();
171 let svg = rpic_core::to_svg(&d);
172 assert!(svg.contains("<svg x=\""), "{svg}");
173 assert!(!svg.contains("frac"), "raw TeX must not leak: {svg}");
174
175 let png = to_png(&svg, 2.0).unwrap();
176 assert_eq!(&png[..4], &[0x89, 0x50, 0x4E, 0x47]);
177 let blank_d = rpic_core::compile("box wid 1 ht 0.7").unwrap();
180 let blank = to_png(&rpic_core::to_svg(&blank_d), 2.0).unwrap();
181 assert!(
182 png.len() > blank.len(),
183 "png {} <= blank {}",
184 png.len(),
185 blank.len()
186 );
187
188 assert_eq!(&to_pdf(&svg).unwrap()[..4], b"%PDF");
189 }
190
191 #[cfg(feature = "raster")]
192 #[test]
193 fn pdf_has_magic() {
194 let pdf = to_pdf(SVG).unwrap();
195 assert_eq!(&pdf[..4], b"%PDF");
196 }
197
198 #[cfg(feature = "raster")]
199 #[test]
200 fn bundled_font_rasterizes_text() {
201 let with_text = to_png(SVG_TEXT, 1.0).unwrap();
205 let blank = to_png(
206 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"120\" height=\"40\" viewBox=\"0 0 120 40\"></svg>",
207 1.0,
208 )
209 .unwrap();
210 assert!(
211 with_text.len() > blank.len() + 200,
212 "text PNG {} vs blank {}",
213 with_text.len(),
214 blank.len()
215 );
216 }
217}