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
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 Ok(data)
193}
194
195fn build_font_cache(data: &HashMap<FontId, Vec<u8>>) -> Result<HashMap<FontId, FontRef<'_>>, String> {
196 let mut cache = HashMap::new();
197 for (id, bytes) in data {
198 let font = FontRef::try_from_slice(bytes)
199 .map_err(|e| format!("Failed to parse font {:?}: {}", id, e))?;
200 cache.insert(*id, font);
201 }
202 Ok(cache)
203}
204
205#[allow(clippy::too_many_arguments)]
206fn render_glyph(
207 pixmap: &mut Pixmap,
208 px: f32,
209 py: f32,
210 font_name: &str,
211 char_code: u32,
212 color: &Color,
213 font_cache: &HashMap<FontId, FontRef<'_>>,
214 em: f32,
215) {
216 let font_id = FontId::parse(font_name).unwrap_or(FontId::MainRegular);
217 let font = match font_cache.get(&font_id) {
218 Some(f) => f,
219 None => match font_cache.get(&FontId::MainRegular) {
220 Some(f) => f,
221 None => return,
222 },
223 };
224
225 let ch = ratex_font::katex_ttf_glyph_char(font_id, char_code);
226 let glyph_id = font.glyph_id(ch);
227
228 if glyph_id.0 == 0 {
229 if let Some(fallback) = font_cache.get(&FontId::MainRegular) {
230 let fid = fallback.glyph_id(ch);
231 if fid.0 != 0 {
232 return render_glyph_with_font(pixmap, px, py, fallback, fid, color, em);
233 }
234 }
235 if let Some(bytes) = unicode_fallback_font_bytes() {
238 if let Ok(fb) = FontRef::try_from_slice(bytes) {
239 let fid = fb.glyph_id(ch);
240 if fid.0 != 0 {
241 return render_glyph_with_font(pixmap, px, py, &fb, fid, color, em);
242 }
243 }
244 }
245 return;
246 }
247
248 render_glyph_with_font(pixmap, px, py, font, glyph_id, color, em);
249}
250
251fn render_glyph_with_font(
252 pixmap: &mut Pixmap,
253 px: f32,
254 py: f32,
255 font: &FontRef<'_>,
256 glyph_id: ab_glyph::GlyphId,
257 color: &Color,
258 em: f32,
259) {
260 let outline = match font.outline(glyph_id) {
261 Some(o) => o,
262 None => return,
263 };
264
265 let units_per_em = font.units_per_em().unwrap_or(1000.0);
266 let scale = em / units_per_em;
267
268 let mut builder = PathBuilder::new();
269 let mut last_end: Option<(f32, f32)> = None;
270
271 for curve in &outline.curves {
272 use ab_glyph::OutlineCurve;
273 let (start, end) = match curve {
274 OutlineCurve::Line(p0, p1) => {
275 let sx = px + p0.x * scale;
276 let sy = py - p0.y * scale;
277 let ex = px + p1.x * scale;
278 let ey = py - p1.y * scale;
279 ((sx, sy), (ex, ey))
280 }
281 OutlineCurve::Quad(p0, _, p2) => {
282 let sx = px + p0.x * scale;
283 let sy = py - p0.y * scale;
284 let ex = px + p2.x * scale;
285 let ey = py - p2.y * scale;
286 ((sx, sy), (ex, ey))
287 }
288 OutlineCurve::Cubic(p0, _, _, p3) => {
289 let sx = px + p0.x * scale;
290 let sy = py - p0.y * scale;
291 let ex = px + p3.x * scale;
292 let ey = py - p3.y * scale;
293 ((sx, sy), (ex, ey))
294 }
295 };
296
297 let need_move = match last_end {
299 None => true,
300 Some((lx, ly)) => (lx - start.0).abs() > 0.01 || (ly - start.1).abs() > 0.01,
301 };
302
303 if need_move {
304 if last_end.is_some() {
305 builder.close();
306 }
307 builder.move_to(start.0, start.1);
308 }
309
310 match curve {
311 OutlineCurve::Line(_, p1) => {
312 builder.line_to(px + p1.x * scale, py - p1.y * scale);
313 }
314 OutlineCurve::Quad(_, p1, p2) => {
315 builder.quad_to(
316 px + p1.x * scale,
317 py - p1.y * scale,
318 px + p2.x * scale,
319 py - p2.y * scale,
320 );
321 }
322 OutlineCurve::Cubic(_, p1, p2, p3) => {
323 builder.cubic_to(
324 px + p1.x * scale,
325 py - p1.y * scale,
326 px + p2.x * scale,
327 py - p2.y * scale,
328 px + p3.x * scale,
329 py - p3.y * scale,
330 );
331 }
332 }
333
334 last_end = Some(end);
335 }
336
337 if last_end.is_some() {
338 builder.close();
339 }
340
341 if let Some(path) = builder.finish() {
342 let mut paint = Paint::default();
343 paint.set_color_rgba8(
344 (color.r * 255.0) as u8,
345 (color.g * 255.0) as u8,
346 (color.b * 255.0) as u8,
347 255,
348 );
349 paint.anti_alias = true;
350 pixmap.fill_path(
351 &path,
352 &paint,
353 tiny_skia::FillRule::EvenOdd,
354 Transform::identity(),
355 None,
356 );
357 }
358}
359
360fn render_line(pixmap: &mut Pixmap, x: f32, y: f32, width: f32, thickness: f32, color: &Color, dashed: bool) {
361 let t = thickness.max(1.0);
362 let mut paint = Paint::default();
363 paint.set_color_rgba8(
364 (color.r * 255.0) as u8,
365 (color.g * 255.0) as u8,
366 (color.b * 255.0) as u8,
367 255,
368 );
369
370 if dashed {
371 let dash_len = (4.0 * t).max(2.0);
373 let gap_len = (4.0 * t).max(2.0);
374 let period = dash_len + gap_len;
375 let top = y - t / 2.0;
376 let mut cur_x = x;
377 while cur_x < x + width {
378 let seg_width = (dash_len).min(x + width - cur_x);
379 let seg_width = seg_width.max(2.0);
380 if let Some(rect) = tiny_skia::Rect::from_xywh(cur_x, top, seg_width, t) {
381 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
382 }
383 cur_x += period;
384 }
385 } else {
386 if let Some(rect) = tiny_skia::Rect::from_xywh(x, y - t / 2.0, width, t) {
387 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
388 }
389 }
390}
391
392fn render_rect(pixmap: &mut Pixmap, x: f32, y: f32, width: f32, height: f32, color: &Color) {
393 let width = width.max(2.0);
397 let height = height.max(2.0);
398 let rect = tiny_skia::Rect::from_xywh(x, y, width, height);
399 if let Some(rect) = rect {
400 let mut paint = Paint::default();
401 paint.set_color_rgba8(
402 (color.r * 255.0) as u8,
403 (color.g * 255.0) as u8,
404 (color.b * 255.0) as u8,
405 255,
406 );
407 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
408 }
409}
410
411#[allow(clippy::too_many_arguments)]
412fn render_path(
413 pixmap: &mut Pixmap,
414 x: f32,
415 y: f32,
416 commands: &[ratex_types::path_command::PathCommand],
417 fill: bool,
418 color: &Color,
419 em: f32,
420 stroke_width_px: f32,
421) {
422 if fill {
429 let mut start = 0;
430 for i in 1..commands.len() {
431 if matches!(commands[i], ratex_types::path_command::PathCommand::MoveTo { .. }) {
432 render_path_segment(pixmap, x, y, &commands[start..i], fill, color, em, stroke_width_px);
433 start = i;
434 }
435 }
436 render_path_segment(pixmap, x, y, &commands[start..], fill, color, em, stroke_width_px);
437 return;
438 }
439 render_path_segment(pixmap, x, y, commands, fill, color, em, stroke_width_px);
440}
441
442#[allow(clippy::too_many_arguments)]
443fn render_path_segment(
444 pixmap: &mut Pixmap,
445 x: f32,
446 y: f32,
447 commands: &[ratex_types::path_command::PathCommand],
448 fill: bool,
449 color: &Color,
450 em: f32,
451 stroke_width_px: f32,
452) {
453 let mut builder = PathBuilder::new();
454 for cmd in commands {
455 match cmd {
456 ratex_types::path_command::PathCommand::MoveTo { x: cx, y: cy } => {
457 builder.move_to(x + *cx as f32 * em, y + *cy as f32 * em);
458 }
459 ratex_types::path_command::PathCommand::LineTo { x: cx, y: cy } => {
460 builder.line_to(x + *cx as f32 * em, y + *cy as f32 * em);
461 }
462 ratex_types::path_command::PathCommand::CubicTo {
463 x1,
464 y1,
465 x2,
466 y2,
467 x: cx,
468 y: cy,
469 } => {
470 builder.cubic_to(
471 x + *x1 as f32 * em,
472 y + *y1 as f32 * em,
473 x + *x2 as f32 * em,
474 y + *y2 as f32 * em,
475 x + *cx as f32 * em,
476 y + *cy as f32 * em,
477 );
478 }
479 ratex_types::path_command::PathCommand::QuadTo { x1, y1, x: cx, y: cy } => {
480 builder.quad_to(
481 x + *x1 as f32 * em,
482 y + *y1 as f32 * em,
483 x + *cx as f32 * em,
484 y + *cy as f32 * em,
485 );
486 }
487 ratex_types::path_command::PathCommand::Close => {
488 builder.close();
489 }
490 }
491 }
492
493 if let Some(path) = builder.finish() {
494 let mut paint = Paint::default();
495 paint.set_color_rgba8(
496 (color.r * 255.0) as u8,
497 (color.g * 255.0) as u8,
498 (color.b * 255.0) as u8,
499 255,
500 );
501 if fill {
502 paint.anti_alias = true;
503 pixmap.fill_path(
506 &path,
507 &paint,
508 FillRule::EvenOdd,
509 Transform::identity(),
510 None,
511 );
512 } else {
513 let stroke = Stroke {
514 width: stroke_width_px,
515 ..Default::default()
516 };
517 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
518 }
519 }
520}
521
522fn encode_png(pixmap: &Pixmap) -> Result<Vec<u8>, String> {
523 let mut buf = Vec::new();
524 {
525 let mut encoder = png::Encoder::new(&mut buf, pixmap.width(), pixmap.height());
526 encoder.set_color(png::ColorType::Rgba);
527 encoder.set_depth(png::BitDepth::Eight);
528 let mut writer = encoder
529 .write_header()
530 .map_err(|e| format!("PNG header error: {}", e))?;
531 writer
532 .write_image_data(pixmap.data())
533 .map_err(|e| format!("PNG write error: {}", e))?;
534 }
535 Ok(buf)
536}