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