1use std::collections::HashMap;
2
3use ab_glyph::{Font, FontRef};
4use ratex_font::FontId;
5use ratex_types::color::Color;
6use ratex_types::display_item::{DisplayItem, DisplayList};
7use tiny_skia::{
8 FilterQuality, FillRule, Paint, PathBuilder, Pixmap, PixmapPaint, Stroke, Transform,
9};
10
11pub struct RenderOptions {
12 pub font_size: f32,
13 pub padding: f32,
14 pub font_dir: String,
17 pub device_pixel_ratio: f32,
20}
21
22impl Default for RenderOptions {
23 fn default() -> Self {
24 Self {
25 font_size: 40.0,
26 padding: 10.0,
27 font_dir: String::new(),
28 device_pixel_ratio: 1.0,
29 }
30 }
31}
32
33pub fn render_to_png(
34 display_list: &DisplayList,
35 options: &RenderOptions,
36) -> Result<Vec<u8>, String> {
37 let em = options.font_size;
38 let pad = options.padding;
39 let dpr = options.device_pixel_ratio.clamp(0.01, 16.0);
40 let em_px = em * dpr;
41 let pad_px = pad * dpr;
42
43 let total_h = display_list.height + display_list.depth;
44 let img_w = (display_list.width as f32 * em_px + 2.0 * pad_px).ceil() as u32;
45 let img_h = (total_h as f32 * em_px + 2.0 * pad_px).ceil() as u32;
46
47 let img_w = img_w.max(1);
48 let img_h = img_h.max(1);
49
50 let mut pixmap = Pixmap::new(img_w, img_h)
51 .ok_or_else(|| format!("Failed to create pixmap {}x{}", img_w, img_h))?;
52
53 pixmap.fill(tiny_skia::Color::WHITE);
54
55 let font_data = load_all_fonts(&options.font_dir)?;
56 let font_cache = build_font_cache(&font_data)?;
57
58 for item in &display_list.items {
59 match item {
60 DisplayItem::GlyphPath {
61 x,
62 y,
63 scale,
64 font,
65 char_code,
66 commands: _,
67 color,
68 } => {
69 let glyph_em = em_px * *scale as f32;
70 render_glyph(
71 &mut pixmap,
72 *x as f32 * em_px + pad_px,
73 *y as f32 * em_px + pad_px,
74 font,
75 *char_code,
76 color,
77 &font_cache,
78 glyph_em,
79 );
80 }
81 DisplayItem::Line {
82 x,
83 y,
84 width,
85 thickness,
86 color,
87 dashed,
88 } => {
89 render_line(
90 &mut pixmap,
91 *x as f32 * em_px + pad_px,
92 *y as f32 * em_px + pad_px,
93 *width as f32 * em_px,
94 *thickness as f32 * em_px,
95 color,
96 *dashed,
97 );
98 }
99 DisplayItem::Rect {
100 x,
101 y,
102 width,
103 height,
104 color,
105 } => {
106 render_rect(
107 &mut pixmap,
108 *x as f32 * em_px + pad_px,
109 *y as f32 * em_px + pad_px,
110 *width as f32 * em_px,
111 *height as f32 * em_px,
112 color,
113 );
114 }
115 DisplayItem::Path {
116 x,
117 y,
118 commands,
119 fill,
120 color,
121 } => {
122 render_path(
123 &mut pixmap,
124 *x as f32 * em_px + pad_px,
125 *y as f32 * em_px + pad_px,
126 commands,
127 *fill,
128 color,
129 em_px,
130 1.5 * dpr,
131 );
132 }
133 }
134 }
135
136 encode_png(&pixmap)
137}
138
139#[allow(unused_variables)]
142fn 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 #[cfg(not(feature = "embed-fonts"))]
167 {
168 let dir = std::path::Path::new(font_dir);
169 for (id, filename) in &font_map {
170 let path = dir.join(filename);
171 if path.exists() {
172 let bytes = std::fs::read(&path)
173 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
174 data.insert(*id, bytes);
175 }
176 }
177
178 if data.is_empty() {
179 return Err(format!("No fonts found in {font_dir}"));
180 }
181 }
182
183 #[cfg(feature = "embed-fonts")]
184 {
185 for (id, filename) in &font_map {
186 let font = ratex_katex_fonts::ttf_bytes(filename)
187 .ok_or_else(|| format!("Failed to get embedded font {filename}"))?;
188 data.insert(*id, font.to_vec());
189 }
190 }
191
192 if let Some(cjk_bytes) = ratex_unicode_font::load_unicode_font() {
194 data.entry(FontId::CjkRegular)
195 .or_insert_with(|| cjk_bytes.to_vec());
196 }
197 if let Some(fb_bytes) = ratex_unicode_font::load_fallback_font() {
199 data.entry(FontId::CjkFallback)
200 .or_insert_with(|| fb_bytes.to_vec());
201 }
202 if let Some(emoji_bytes) = ratex_unicode_font::load_emoji_font() {
203 data.entry(FontId::EmojiFallback)
204 .or_insert_with(|| emoji_bytes.to_vec());
205 }
206
207 Ok(data)
208}
209
210fn sfnt_collection_index(id: FontId) -> u32 {
211 match id {
212 FontId::EmojiFallback => ratex_unicode_font::emoji_font_face_index().unwrap_or(0),
213 FontId::CjkRegular => ratex_unicode_font::unicode_font_face_index().unwrap_or(0),
214 FontId::CjkFallback => ratex_unicode_font::fallback_font_face_index().unwrap_or(0),
215 _ => 0,
216 }
217}
218
219fn build_font_cache(data: &HashMap<FontId, Vec<u8>>) -> Result<HashMap<FontId, FontRef<'_>>, String> {
220 let mut cache = HashMap::new();
221 for (id, bytes) in data {
222 let font = FontRef::try_from_slice_and_index(bytes, sfnt_collection_index(*id))
223 .map_err(|e| format!("Failed to parse font {:?}: {}", id, e))?;
224 cache.insert(*id, font);
225 }
226 Ok(cache)
227}
228
229#[allow(clippy::too_many_arguments)]
237fn try_system_unicode_fallback(
238 pixmap: &mut Pixmap,
239 px: f32,
240 py: f32,
241 ch: char,
242 color: &Color,
243 em: f32,
244 font_cache: &HashMap<FontId, FontRef<'_>>,
245 skip_main_regular: bool,
246) -> bool {
247 if !skip_main_regular {
248 if let Some(fallback) = font_cache.get(&FontId::MainRegular) {
249 let fid = fallback.glyph_id(ch);
250 if fid.0 != 0 && render_glyph_with_font(pixmap, px, py, fallback, fid, color, em) {
251 return true;
252 }
253 }
254 }
255 if let Some(cjk_font) = font_cache.get(&FontId::CjkRegular) {
256 let fid = cjk_font.glyph_id(ch);
257 if fid.0 != 0 && render_glyph_with_font(pixmap, px, py, cjk_font, fid, color, em) {
258 return true;
259 }
260 }
261 if try_emoji_vector_then_bitmap(pixmap, px, py, ch, color, em, font_cache) {
262 return true;
263 }
264 if let Some(fb_font) = font_cache.get(&FontId::CjkFallback) {
265 let fid = fb_font.glyph_id(ch);
266 if fid.0 != 0 && render_glyph_with_font(pixmap, px, py, fb_font, fid, color, em) {
267 return true;
268 }
269 }
270 false
271}
272
273#[allow(clippy::too_many_arguments)]
277fn try_emoji_vector_then_bitmap(
278 pixmap: &mut Pixmap,
279 px: f32,
280 py: f32,
281 ch: char,
282 color: &Color,
283 em: f32,
284 font_cache: &HashMap<FontId, FontRef<'_>>,
285) -> bool {
286 if try_blit_emoji_raster_fallback(pixmap, px, py, em, ch) {
287 return true;
288 }
289 if let Some(emoji_font) = font_cache.get(&FontId::EmojiFallback) {
290 let eid = emoji_font.glyph_id(ch);
291 if eid.0 != 0 && render_glyph_with_font(pixmap, px, py, emoji_font, eid, color, em) {
292 return true;
293 }
294 }
295 false
296}
297
298#[allow(clippy::too_many_arguments)]
299fn render_glyph(
300 pixmap: &mut Pixmap,
301 px: f32,
302 py: f32,
303 font_name: &str,
304 char_code: u32,
305 color: &Color,
306 font_cache: &HashMap<FontId, FontRef<'_>>,
307 em: f32,
308) {
309 let font_id = FontId::parse(font_name).unwrap_or(FontId::MainRegular);
310 let font = match font_cache.get(&font_id) {
311 Some(f) => f,
312 None => match font_cache.get(&FontId::MainRegular) {
313 Some(f) => f,
314 None => return,
315 },
316 };
317
318 let ch = ratex_font::katex_ttf_glyph_char(font_id, char_code);
319 let glyph_id = font.glyph_id(ch);
320
321 if glyph_id.0 == 0 {
322 let _ = try_system_unicode_fallback(pixmap, px, py, ch, color, em, font_cache, false);
323 return;
324 }
325
326 if font_id == FontId::EmojiFallback {
327 if try_blit_emoji_raster_fallback(pixmap, px, py, em, ch) {
328 return;
329 }
330 let _ = render_glyph_with_font(pixmap, px, py, font, glyph_id, color, em);
331 return;
332 }
333
334 if font_id == FontId::CjkRegular {
336 if render_glyph_with_font(pixmap, px, py, font, glyph_id, color, em) {
337 return;
338 }
339 if try_emoji_vector_then_bitmap(pixmap, px, py, ch, color, em, font_cache) {
340 return;
341 }
342 if let Some(fb_font) = font_cache.get(&FontId::CjkFallback) {
343 let fid = fb_font.glyph_id(ch);
344 if fid.0 != 0 && render_glyph_with_font(pixmap, px, py, fb_font, fid, color, em) {
345 return;
346 }
347 }
348 return;
349 }
350
351 if font_id == FontId::CjkFallback {
352 if render_glyph_with_font(pixmap, px, py, font, glyph_id, color, em) {
353 return;
354 }
355 let _ = try_emoji_vector_then_bitmap(pixmap, px, py, ch, color, em, font_cache);
356 return;
357 }
358
359 if render_glyph_with_font(pixmap, px, py, font, glyph_id, color, em) {
360 return;
361 }
362 let skip_main = font_id == FontId::MainRegular;
364 let _ = try_system_unicode_fallback(pixmap, px, py, ch, color, em, font_cache, skip_main);
365}
366
367fn render_glyph_with_font(
368 pixmap: &mut Pixmap,
369 px: f32,
370 py: f32,
371 font: &FontRef<'_>,
372 glyph_id: ab_glyph::GlyphId,
373 color: &Color,
374 em: f32,
375) -> bool {
376 let outline = match font.outline(glyph_id) {
377 Some(o) => o,
378 None => return false,
379 };
380 if outline.curves.is_empty() {
381 return false;
382 }
383
384 let units_per_em = font.units_per_em().unwrap_or(1000.0);
385 let scale = em / units_per_em;
386
387 let mut builder = PathBuilder::new();
388 let mut last_end: Option<(f32, f32)> = None;
389
390 for curve in &outline.curves {
391 use ab_glyph::OutlineCurve;
392 let (start, end) = match curve {
393 OutlineCurve::Line(p0, p1) => {
394 let sx = px + p0.x * scale;
395 let sy = py - p0.y * scale;
396 let ex = px + p1.x * scale;
397 let ey = py - p1.y * scale;
398 ((sx, sy), (ex, ey))
399 }
400 OutlineCurve::Quad(p0, _, p2) => {
401 let sx = px + p0.x * scale;
402 let sy = py - p0.y * scale;
403 let ex = px + p2.x * scale;
404 let ey = py - p2.y * scale;
405 ((sx, sy), (ex, ey))
406 }
407 OutlineCurve::Cubic(p0, _, _, p3) => {
408 let sx = px + p0.x * scale;
409 let sy = py - p0.y * scale;
410 let ex = px + p3.x * scale;
411 let ey = py - p3.y * scale;
412 ((sx, sy), (ex, ey))
413 }
414 };
415
416 let need_move = match last_end {
418 None => true,
419 Some((lx, ly)) => (lx - start.0).abs() > 0.01 || (ly - start.1).abs() > 0.01,
420 };
421
422 if need_move {
423 if last_end.is_some() {
424 builder.close();
425 }
426 builder.move_to(start.0, start.1);
427 }
428
429 match curve {
430 OutlineCurve::Line(_, p1) => {
431 builder.line_to(px + p1.x * scale, py - p1.y * scale);
432 }
433 OutlineCurve::Quad(_, p1, p2) => {
434 builder.quad_to(
435 px + p1.x * scale,
436 py - p1.y * scale,
437 px + p2.x * scale,
438 py - p2.y * scale,
439 );
440 }
441 OutlineCurve::Cubic(_, p1, p2, p3) => {
442 builder.cubic_to(
443 px + p1.x * scale,
444 py - p1.y * scale,
445 px + p2.x * scale,
446 py - p2.y * scale,
447 px + p3.x * scale,
448 py - p3.y * scale,
449 );
450 }
451 }
452
453 last_end = Some(end);
454 }
455
456 if last_end.is_some() {
457 builder.close();
458 }
459
460 if let Some(path) = builder.finish() {
461 let mut paint = Paint::default();
462 paint.set_color_rgba8(
463 (color.r * 255.0) as u8,
464 (color.g * 255.0) as u8,
465 (color.b * 255.0) as u8,
466 255,
467 );
468 paint.anti_alias = true;
469 pixmap.fill_path(
470 &path,
471 &paint,
472 tiny_skia::FillRule::Winding,
473 Transform::identity(),
474 None,
475 );
476 true
477 } else {
478 false
479 }
480}
481
482fn try_blit_emoji_raster_fallback(pixmap: &mut Pixmap, px: f32, py: f32, em: f32, ch: char) -> bool {
484 let Some((bytes, idx)) = ratex_unicode_font::load_emoji_font_with_index() else {
485 return false;
486 };
487 try_blit_raster_glyph(pixmap, px, py, em, ch, bytes, idx)
488}
489
490fn try_blit_raster_glyph(
491 pixmap: &mut Pixmap,
492 px: f32,
493 py: f32,
494 em: f32,
495 ch: char,
496 font_bytes: &[u8],
497 face_index: u32,
498) -> bool {
499 let face = match ttf_parser::Face::parse(font_bytes, face_index) {
500 Ok(f) => f,
501 Err(_) => return false,
502 };
503 let gid = match face.glyph_index(ch) {
504 Some(g) => g,
505 None => return false,
506 };
507 let strike = em.round().clamp(8.0, 256.0) as u16;
508 let img = face
509 .glyph_raster_image(gid, strike)
510 .or_else(|| face.glyph_raster_image(gid, u16::MAX));
511 let Some(img) = img else {
512 return false;
513 };
514 let glyph_pm = match raster_glyph_image_to_pixmap(&img) {
515 Some(p) => p,
516 None => return false,
517 };
518 let scale = em / f32::from(img.pixels_per_em.max(1));
519 let top_x = px + f32::from(img.x) * scale;
520 let mut top_y = py - (f32::from(img.y) + f32::from(img.height)) * scale;
524 let ppem = f32::from(img.pixels_per_em.max(1));
529 let center_strike = (f32::from(img.y) + f32::from(img.height) / 2.0) / ppem;
530 let axis = ratex_font::get_global_metrics(0).axis_height as f32;
531 top_y += (center_strike - axis) * em;
532 let paint = PixmapPaint {
533 quality: FilterQuality::Bilinear,
534 ..Default::default()
535 };
536 let transform = Transform::from_row(scale, 0.0, 0.0, scale, top_x, top_y);
537 pixmap.draw_pixmap(0, 0, glyph_pm.as_ref(), &paint, transform, None);
538 true
539}
540
541fn raster_glyph_image_to_pixmap(img: &ttf_parser::RasterGlyphImage<'_>) -> Option<Pixmap> {
542 use ttf_parser::RasterImageFormat;
543 let w = u32::from(img.width);
544 let h = u32::from(img.height);
545 let size = tiny_skia::IntSize::from_wh(w, h)?;
546 match img.format {
547 RasterImageFormat::PNG => Pixmap::decode_png(img.data).ok(),
548 RasterImageFormat::BitmapPremulBgra32 => {
549 let expected = 4usize * w as usize * h as usize;
550 if img.data.len() != expected {
551 return None;
552 }
553 let mut v = Vec::with_capacity(expected);
554 for px in img.data.chunks_exact(4) {
555 let b = px[0];
556 let g = px[1];
557 let r = px[2];
558 let a = px[3];
559 v.extend_from_slice(&[r, g, b, a]);
560 }
561 Pixmap::from_vec(v, size)
562 }
563 RasterImageFormat::BitmapGray8 => {
564 let mut v = Vec::with_capacity(4 * img.data.len());
565 for &g in img.data {
566 v.extend_from_slice(&[g, g, g, 255]);
567 }
568 Pixmap::from_vec(v, size)
569 }
570 _ => None,
571 }
572}
573
574fn render_line(pixmap: &mut Pixmap, x: f32, y: f32, width: f32, thickness: f32, color: &Color, dashed: bool) {
575 let t = thickness.max(1.0);
576 let mut paint = Paint::default();
577 paint.set_color_rgba8(
578 (color.r * 255.0) as u8,
579 (color.g * 255.0) as u8,
580 (color.b * 255.0) as u8,
581 255,
582 );
583
584 if dashed {
585 let dash_len = (4.0 * t).max(2.0);
587 let gap_len = (4.0 * t).max(2.0);
588 let period = dash_len + gap_len;
589 let top = y - t / 2.0;
590 let mut cur_x = x;
591 while cur_x < x + width {
592 let seg_width = (dash_len).min(x + width - cur_x);
593 let seg_width = seg_width.max(2.0);
594 if let Some(rect) = tiny_skia::Rect::from_xywh(cur_x, top, seg_width, t) {
595 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
596 }
597 cur_x += period;
598 }
599 } else {
600 if let Some(rect) = tiny_skia::Rect::from_xywh(x, y - t / 2.0, width, t) {
601 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
602 }
603 }
604}
605
606fn render_rect(pixmap: &mut Pixmap, x: f32, y: f32, width: f32, height: f32, color: &Color) {
607 let width = width.max(2.0);
611 let height = height.max(2.0);
612 let rect = tiny_skia::Rect::from_xywh(x, y, width, height);
613 if let Some(rect) = rect {
614 let mut paint = Paint::default();
615 paint.set_color_rgba8(
616 (color.r * 255.0) as u8,
617 (color.g * 255.0) as u8,
618 (color.b * 255.0) as u8,
619 255,
620 );
621 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
622 }
623}
624
625#[allow(clippy::too_many_arguments)]
626fn render_path(
627 pixmap: &mut Pixmap,
628 x: f32,
629 y: f32,
630 commands: &[ratex_types::path_command::PathCommand],
631 fill: bool,
632 color: &Color,
633 em: f32,
634 stroke_width_px: f32,
635) {
636 if fill {
643 let mut start = 0;
644 for i in 1..commands.len() {
645 if matches!(commands[i], ratex_types::path_command::PathCommand::MoveTo { .. }) {
646 render_path_segment(pixmap, x, y, &commands[start..i], fill, color, em, stroke_width_px);
647 start = i;
648 }
649 }
650 render_path_segment(pixmap, x, y, &commands[start..], fill, color, em, stroke_width_px);
651 return;
652 }
653 render_path_segment(pixmap, x, y, commands, fill, color, em, stroke_width_px);
654}
655
656#[allow(clippy::too_many_arguments)]
657fn render_path_segment(
658 pixmap: &mut Pixmap,
659 x: f32,
660 y: f32,
661 commands: &[ratex_types::path_command::PathCommand],
662 fill: bool,
663 color: &Color,
664 em: f32,
665 stroke_width_px: f32,
666) {
667 let mut builder = PathBuilder::new();
668 for cmd in commands {
669 match cmd {
670 ratex_types::path_command::PathCommand::MoveTo { x: cx, y: cy } => {
671 builder.move_to(x + *cx as f32 * em, y + *cy as f32 * em);
672 }
673 ratex_types::path_command::PathCommand::LineTo { x: cx, y: cy } => {
674 builder.line_to(x + *cx as f32 * em, y + *cy as f32 * em);
675 }
676 ratex_types::path_command::PathCommand::CubicTo {
677 x1,
678 y1,
679 x2,
680 y2,
681 x: cx,
682 y: cy,
683 } => {
684 builder.cubic_to(
685 x + *x1 as f32 * em,
686 y + *y1 as f32 * em,
687 x + *x2 as f32 * em,
688 y + *y2 as f32 * em,
689 x + *cx as f32 * em,
690 y + *cy as f32 * em,
691 );
692 }
693 ratex_types::path_command::PathCommand::QuadTo { x1, y1, x: cx, y: cy } => {
694 builder.quad_to(
695 x + *x1 as f32 * em,
696 y + *y1 as f32 * em,
697 x + *cx as f32 * em,
698 y + *cy as f32 * em,
699 );
700 }
701 ratex_types::path_command::PathCommand::Close => {
702 builder.close();
703 }
704 }
705 }
706
707 if let Some(path) = builder.finish() {
708 let mut paint = Paint::default();
709 paint.set_color_rgba8(
710 (color.r * 255.0) as u8,
711 (color.g * 255.0) as u8,
712 (color.b * 255.0) as u8,
713 255,
714 );
715 if fill {
716 paint.anti_alias = true;
717 pixmap.fill_path(
720 &path,
721 &paint,
722 FillRule::EvenOdd,
723 Transform::identity(),
724 None,
725 );
726 } else {
727 let stroke = Stroke {
728 width: stroke_width_px,
729 ..Default::default()
730 };
731 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
732 }
733 }
734}
735
736fn encode_png(pixmap: &Pixmap) -> Result<Vec<u8>, String> {
737 let mut buf = Vec::new();
738 {
739 let mut encoder = png::Encoder::new(&mut buf, pixmap.width(), pixmap.height());
740 encoder.set_color(png::ColorType::Rgba);
741 encoder.set_depth(png::BitDepth::Eight);
742 let mut writer = encoder
743 .write_header()
744 .map_err(|e| format!("PNG header error: {}", e))?;
745 writer
746 .write_image_data(pixmap.data())
747 .map_err(|e| format!("PNG write error: {}", e))?;
748 }
749 Ok(buf)
750}