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