1use std::collections::HashMap;
2use std::path::Path;
3
4use ab_glyph::{Font, FontRef};
5use ratex_font::FontId;
6use ratex_types::color::Color;
7use ratex_types::display_item::{DisplayItem, DisplayList};
8use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Stroke, Transform};
9
10pub struct RenderOptions {
11 pub font_size: f32,
12 pub padding: f32,
13 pub font_dir: String,
14 pub device_pixel_ratio: f32,
17}
18
19impl Default for RenderOptions {
20 fn default() -> Self {
21 Self {
22 font_size: 40.0,
23 padding: 10.0,
24 font_dir: String::new(),
25 device_pixel_ratio: 1.0,
26 }
27 }
28}
29
30pub fn render_to_png(
31 display_list: &DisplayList,
32 options: &RenderOptions,
33) -> Result<Vec<u8>, String> {
34 let em = options.font_size;
35 let pad = options.padding;
36 let dpr = options.device_pixel_ratio.clamp(0.01, 16.0);
37 let em_px = em * dpr;
38 let pad_px = pad * dpr;
39
40 let total_h = display_list.height + display_list.depth;
41 let img_w = (display_list.width as f32 * em_px + 2.0 * pad_px).ceil() as u32;
42 let img_h = (total_h as f32 * em_px + 2.0 * pad_px).ceil() as u32;
43
44 let img_w = img_w.max(1);
45 let img_h = img_h.max(1);
46
47 let mut pixmap = Pixmap::new(img_w, img_h)
48 .ok_or_else(|| format!("Failed to create pixmap {}x{}", img_w, img_h))?;
49
50 pixmap.fill(tiny_skia::Color::WHITE);
51
52 let font_data = load_all_fonts(&options.font_dir)?;
53 let font_cache = build_font_cache(&font_data)?;
54
55 for item in &display_list.items {
56 match item {
57 DisplayItem::GlyphPath {
58 x,
59 y,
60 scale,
61 font,
62 char_code,
63 commands: _,
64 color,
65 } => {
66 let glyph_em = em_px * *scale as f32;
67 render_glyph(
68 &mut pixmap,
69 *x as f32 * em_px + pad_px,
70 *y as f32 * em_px + pad_px,
71 font,
72 *char_code,
73 color,
74 &font_cache,
75 glyph_em,
76 );
77 }
78 DisplayItem::Line {
79 x,
80 y,
81 width,
82 thickness,
83 color,
84 } => {
85 render_line(
86 &mut pixmap,
87 *x as f32 * em_px + pad_px,
88 *y as f32 * em_px + pad_px,
89 *width as f32 * em_px,
90 *thickness as f32 * em_px,
91 color,
92 );
93 }
94 DisplayItem::Rect {
95 x,
96 y,
97 width,
98 height,
99 color,
100 } => {
101 render_rect(
102 &mut pixmap,
103 *x as f32 * em_px + pad_px,
104 *y as f32 * em_px + pad_px,
105 *width as f32 * em_px,
106 *height as f32 * em_px,
107 color,
108 );
109 }
110 DisplayItem::Path {
111 x,
112 y,
113 commands,
114 fill,
115 color,
116 } => {
117 render_path(
118 &mut pixmap,
119 *x as f32 * em_px + pad_px,
120 *y as f32 * em_px + pad_px,
121 commands,
122 *fill,
123 color,
124 em_px,
125 1.5 * dpr,
126 );
127 }
128 }
129 }
130
131 encode_png(&pixmap)
132}
133
134fn load_all_fonts(font_dir: &str) -> Result<HashMap<FontId, Vec<u8>>, String> {
135 let mut data = HashMap::new();
136 let font_map = [
137 (FontId::MainRegular, "KaTeX_Main-Regular.ttf"),
138 (FontId::MainBold, "KaTeX_Main-Bold.ttf"),
139 (FontId::MainItalic, "KaTeX_Main-Italic.ttf"),
140 (FontId::MainBoldItalic, "KaTeX_Main-BoldItalic.ttf"),
141 (FontId::MathItalic, "KaTeX_Math-Italic.ttf"),
142 (FontId::MathBoldItalic, "KaTeX_Math-BoldItalic.ttf"),
143 (FontId::AmsRegular, "KaTeX_AMS-Regular.ttf"),
144 (FontId::CaligraphicRegular, "KaTeX_Caligraphic-Regular.ttf"),
145 (FontId::FrakturRegular, "KaTeX_Fraktur-Regular.ttf"),
146 (FontId::SansSerifRegular, "KaTeX_SansSerif-Regular.ttf"),
147 (FontId::SansSerifBold, "KaTeX_SansSerif-Bold.ttf"),
148 (FontId::SansSerifItalic, "KaTeX_SansSerif-Italic.ttf"),
149 (FontId::ScriptRegular, "KaTeX_Script-Regular.ttf"),
150 (FontId::TypewriterRegular, "KaTeX_Typewriter-Regular.ttf"),
151 (FontId::Size1Regular, "KaTeX_Size1-Regular.ttf"),
152 (FontId::Size2Regular, "KaTeX_Size2-Regular.ttf"),
153 (FontId::Size3Regular, "KaTeX_Size3-Regular.ttf"),
154 (FontId::Size4Regular, "KaTeX_Size4-Regular.ttf"),
155 ];
156
157 let dir = Path::new(font_dir);
158 for (id, filename) in &font_map {
159 let path = dir.join(filename);
160 if path.exists() {
161 let bytes = std::fs::read(&path)
162 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
163 data.insert(*id, bytes);
164 }
165 }
166
167 if data.is_empty() {
168 return Err(format!("No fonts found in {}", font_dir));
169 }
170
171 Ok(data)
172}
173
174fn build_font_cache(data: &HashMap<FontId, Vec<u8>>) -> Result<HashMap<FontId, FontRef<'_>>, String> {
175 let mut cache = HashMap::new();
176 for (id, bytes) in data {
177 let font = FontRef::try_from_slice(bytes)
178 .map_err(|e| format!("Failed to parse font {:?}: {}", id, e))?;
179 cache.insert(*id, font);
180 }
181 Ok(cache)
182}
183
184#[allow(clippy::too_many_arguments)]
185fn render_glyph(
186 pixmap: &mut Pixmap,
187 px: f32,
188 py: f32,
189 font_name: &str,
190 char_code: u32,
191 color: &Color,
192 font_cache: &HashMap<FontId, FontRef<'_>>,
193 em: f32,
194) {
195 let font_id = FontId::parse(font_name).unwrap_or(FontId::MainRegular);
196 let font = match font_cache.get(&font_id) {
197 Some(f) => f,
198 None => match font_cache.get(&FontId::MainRegular) {
199 Some(f) => f,
200 None => return,
201 },
202 };
203
204 let ch = char::from_u32(char_code).unwrap_or('?');
205 let glyph_id = font.glyph_id(ch);
206
207 if glyph_id.0 == 0 {
208 if let Some(fallback) = font_cache.get(&FontId::MainRegular) {
209 let fid = fallback.glyph_id(ch);
210 if fid.0 != 0 {
211 return render_glyph_with_font(pixmap, px, py, fallback, fid, color, em);
212 }
213 }
214 return;
215 }
216
217 render_glyph_with_font(pixmap, px, py, font, glyph_id, color, em);
218}
219
220fn render_glyph_with_font(
221 pixmap: &mut Pixmap,
222 px: f32,
223 py: f32,
224 font: &FontRef<'_>,
225 glyph_id: ab_glyph::GlyphId,
226 color: &Color,
227 em: f32,
228) {
229 let outline = match font.outline(glyph_id) {
230 Some(o) => o,
231 None => return,
232 };
233
234 let units_per_em = font.units_per_em().unwrap_or(1000.0);
235 let scale = em / units_per_em;
236
237 let mut builder = PathBuilder::new();
238 let mut last_end: Option<(f32, f32)> = None;
239
240 for curve in &outline.curves {
241 use ab_glyph::OutlineCurve;
242 let (start, end) = match curve {
243 OutlineCurve::Line(p0, p1) => {
244 let sx = px + p0.x * scale;
245 let sy = py - p0.y * scale;
246 let ex = px + p1.x * scale;
247 let ey = py - p1.y * scale;
248 ((sx, sy), (ex, ey))
249 }
250 OutlineCurve::Quad(p0, _, p2) => {
251 let sx = px + p0.x * scale;
252 let sy = py - p0.y * scale;
253 let ex = px + p2.x * scale;
254 let ey = py - p2.y * scale;
255 ((sx, sy), (ex, ey))
256 }
257 OutlineCurve::Cubic(p0, _, _, p3) => {
258 let sx = px + p0.x * scale;
259 let sy = py - p0.y * scale;
260 let ex = px + p3.x * scale;
261 let ey = py - p3.y * scale;
262 ((sx, sy), (ex, ey))
263 }
264 };
265
266 let need_move = match last_end {
268 None => true,
269 Some((lx, ly)) => (lx - start.0).abs() > 0.01 || (ly - start.1).abs() > 0.01,
270 };
271
272 if need_move {
273 if last_end.is_some() {
274 builder.close();
275 }
276 builder.move_to(start.0, start.1);
277 }
278
279 match curve {
280 OutlineCurve::Line(_, p1) => {
281 builder.line_to(px + p1.x * scale, py - p1.y * scale);
282 }
283 OutlineCurve::Quad(_, p1, p2) => {
284 builder.quad_to(
285 px + p1.x * scale,
286 py - p1.y * scale,
287 px + p2.x * scale,
288 py - p2.y * scale,
289 );
290 }
291 OutlineCurve::Cubic(_, p1, p2, p3) => {
292 builder.cubic_to(
293 px + p1.x * scale,
294 py - p1.y * scale,
295 px + p2.x * scale,
296 py - p2.y * scale,
297 px + p3.x * scale,
298 py - p3.y * scale,
299 );
300 }
301 }
302
303 last_end = Some(end);
304 }
305
306 if last_end.is_some() {
307 builder.close();
308 }
309
310 if let Some(path) = builder.finish() {
311 let mut paint = Paint::default();
312 paint.set_color_rgba8(
313 (color.r * 255.0) as u8,
314 (color.g * 255.0) as u8,
315 (color.b * 255.0) as u8,
316 255,
317 );
318 paint.anti_alias = true;
319 pixmap.fill_path(
320 &path,
321 &paint,
322 tiny_skia::FillRule::EvenOdd,
323 Transform::identity(),
324 None,
325 );
326 }
327}
328
329fn render_line(pixmap: &mut Pixmap, x: f32, y: f32, width: f32, thickness: f32, color: &Color) {
330 let t = thickness.max(1.0);
331 let rect = tiny_skia::Rect::from_xywh(x, y - t / 2.0, width, t);
332 if let Some(rect) = rect {
333 let mut paint = Paint::default();
334 paint.set_color_rgba8(
335 (color.r * 255.0) as u8,
336 (color.g * 255.0) as u8,
337 (color.b * 255.0) as u8,
338 255,
339 );
340 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
341 }
342}
343
344fn render_rect(pixmap: &mut Pixmap, x: f32, y: f32, width: f32, height: f32, color: &Color) {
345 let rect = tiny_skia::Rect::from_xywh(x, y, width, height);
346 if let Some(rect) = rect {
347 let mut paint = Paint::default();
348 paint.set_color_rgba8(
349 (color.r * 255.0) as u8,
350 (color.g * 255.0) as u8,
351 (color.b * 255.0) as u8,
352 255,
353 );
354 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
355 }
356}
357
358#[allow(clippy::too_many_arguments)]
359fn render_path(
360 pixmap: &mut Pixmap,
361 x: f32,
362 y: f32,
363 commands: &[ratex_types::path_command::PathCommand],
364 fill: bool,
365 color: &Color,
366 em: f32,
367 stroke_width_px: f32,
368) {
369 if fill {
376 let mut start = 0;
377 for i in 1..commands.len() {
378 if matches!(commands[i], ratex_types::path_command::PathCommand::MoveTo { .. }) {
379 render_path_segment(pixmap, x, y, &commands[start..i], fill, color, em, stroke_width_px);
380 start = i;
381 }
382 }
383 render_path_segment(pixmap, x, y, &commands[start..], fill, color, em, stroke_width_px);
384 return;
385 }
386 render_path_segment(pixmap, x, y, commands, fill, color, em, stroke_width_px);
387}
388
389#[allow(clippy::too_many_arguments)]
390fn render_path_segment(
391 pixmap: &mut Pixmap,
392 x: f32,
393 y: f32,
394 commands: &[ratex_types::path_command::PathCommand],
395 fill: bool,
396 color: &Color,
397 em: f32,
398 stroke_width_px: f32,
399) {
400 let mut builder = PathBuilder::new();
401 for cmd in commands {
402 match cmd {
403 ratex_types::path_command::PathCommand::MoveTo { x: cx, y: cy } => {
404 builder.move_to(x + *cx as f32 * em, y + *cy as f32 * em);
405 }
406 ratex_types::path_command::PathCommand::LineTo { x: cx, y: cy } => {
407 builder.line_to(x + *cx as f32 * em, y + *cy as f32 * em);
408 }
409 ratex_types::path_command::PathCommand::CubicTo {
410 x1,
411 y1,
412 x2,
413 y2,
414 x: cx,
415 y: cy,
416 } => {
417 builder.cubic_to(
418 x + *x1 as f32 * em,
419 y + *y1 as f32 * em,
420 x + *x2 as f32 * em,
421 y + *y2 as f32 * em,
422 x + *cx as f32 * em,
423 y + *cy as f32 * em,
424 );
425 }
426 ratex_types::path_command::PathCommand::QuadTo { x1, y1, x: cx, y: cy } => {
427 builder.quad_to(
428 x + *x1 as f32 * em,
429 y + *y1 as f32 * em,
430 x + *cx as f32 * em,
431 y + *cy as f32 * em,
432 );
433 }
434 ratex_types::path_command::PathCommand::Close => {
435 builder.close();
436 }
437 }
438 }
439
440 if let Some(path) = builder.finish() {
441 let mut paint = Paint::default();
442 paint.set_color_rgba8(
443 (color.r * 255.0) as u8,
444 (color.g * 255.0) as u8,
445 (color.b * 255.0) as u8,
446 255,
447 );
448 if fill {
449 paint.anti_alias = true;
450 pixmap.fill_path(
451 &path,
452 &paint,
453 FillRule::Winding,
454 Transform::identity(),
455 None,
456 );
457 } else {
458 let stroke = Stroke {
459 width: stroke_width_px,
460 ..Default::default()
461 };
462 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
463 }
464 }
465}
466
467fn encode_png(pixmap: &Pixmap) -> Result<Vec<u8>, String> {
468 let mut buf = Vec::new();
469 {
470 let mut encoder = png::Encoder::new(&mut buf, pixmap.width(), pixmap.height());
471 encoder.set_color(png::ColorType::Rgba);
472 encoder.set_depth(png::BitDepth::Eight);
473 let mut writer = encoder
474 .write_header()
475 .map_err(|e| format!("PNG header error: {}", e))?;
476 writer
477 .write_image_data(pixmap.data())
478 .map_err(|e| format!("PNG write error: {}", e))?;
479 }
480 Ok(buf)
481}