1use std::collections::HashMap;
2
3use ab_glyph::{Font, FontRef};
4use ratex_font::FontId;
5use ratex_font_loader::FontSet;
6use ratex_types::color::Color;
7use ratex_types::display_item::{DisplayItem, DisplayList};
8use tiny_skia::{
9 FillRule, FilterQuality, Paint, PathBuilder, Pixmap, PixmapPaint, Stroke, Transform,
10};
11
12pub struct RenderOptions {
14 pub font_size: f32,
15 pub padding: f32,
16 pub background_color: Color,
18 pub font_dir: String,
20 pub device_pixel_ratio: f32,
23}
24
25impl Default for RenderOptions {
26 fn default() -> Self {
27 Self {
28 font_size: 40.0,
29 padding: 10.0,
30 background_color: Color::WHITE,
31 font_dir: String::new(),
32 device_pixel_ratio: 1.0,
33 }
34 }
35}
36
37pub fn render_to_png(
38 display_list: &DisplayList,
39 options: &RenderOptions,
40) -> Result<Vec<u8>, String> {
41 let em = options.font_size;
42 let pad = options.padding;
43 let dpr = options.device_pixel_ratio.clamp(0.01, 16.0);
44 let em_px = em * dpr;
45 let pad_px = pad * dpr;
46
47 let total_h = display_list.height + display_list.depth;
48 let img_w = (display_list.width as f32 * em_px + 2.0 * pad_px).ceil() as u32;
49 let img_h = (total_h as f32 * em_px + 2.0 * pad_px).ceil() as u32;
50
51 let img_w = img_w.max(1);
52 let img_h = img_h.max(1);
53
54 let mut pixmap = Pixmap::new(img_w, img_h)
55 .ok_or_else(|| format!("Failed to create pixmap {}x{}", img_w, img_h))?;
56
57 pixmap.fill(to_tiny_skia_color(options.background_color));
58
59 render_with_fonts(&mut pixmap, display_list, options, em_px, pad_px, dpr)?;
61
62 encode_png(&pixmap)
63}
64
65fn render_with_fonts(
67 pixmap: &mut Pixmap,
68 display_list: &DisplayList,
69 options: &RenderOptions,
70 em_px: f32,
71 pad_px: f32,
72 dpr: f32,
73) -> Result<(), String> {
74 let fonts = ratex_font_loader::load_fonts_for_items(&options.font_dir, &display_list.items)?;
75 let font_refs = build_font_refs(&fonts)?;
76 render_display_list(pixmap, display_list, &font_refs, em_px, pad_px, dpr);
77 Ok(())
78}
79
80fn to_tiny_skia_color(color: Color) -> tiny_skia::Color {
81 tiny_skia::Color::from_rgba(
82 color.r.clamp(0.0, 1.0),
83 color.g.clamp(0.0, 1.0),
84 color.b.clamp(0.0, 1.0),
85 color.a.clamp(0.0, 1.0),
86 )
87 .unwrap_or(tiny_skia::Color::TRANSPARENT)
88}
89
90fn build_font_refs(data: &FontSet) -> Result<HashMap<FontId, FontRef<'_>>, String> {
92 let mut font_refs = HashMap::new();
93 for (id, bytes) in data.iter() {
94 let font = FontRef::try_from_slice_and_index(bytes, sfnt_collection_index(*id))
95 .map_err(|e| format!("Failed to parse font {:?}: {}", id, e))?;
96 font_refs.insert(*id, font);
97 }
98
99 if !font_refs.contains_key(&FontId::MainRegular) {
100 return Err("Main-Regular font not found".to_string());
101 }
102
103 Ok(font_refs)
104}
105
106fn render_display_list(
108 pixmap: &mut Pixmap,
109 display_list: &DisplayList,
110 font_cache: &HashMap<FontId, FontRef<'_>>,
111 em_px: f32,
112 pad_px: f32,
113 dpr: f32,
114) {
115 let mut font_id_cache: HashMap<&str, FontId> = HashMap::new();
116 for item in &display_list.items {
117 match item {
118 DisplayItem::GlyphPath {
119 x,
120 y,
121 scale,
122 font,
123 char_code,
124 color,
125 } => {
126 let glyph_em = em_px * *scale as f32;
127 let font_id = *font_id_cache
128 .entry(font.as_str())
129 .or_insert_with(|| FontId::parse(font).unwrap_or(FontId::MainRegular));
130 render_glyph(
131 pixmap,
132 *x as f32 * em_px + pad_px,
133 *y as f32 * em_px + pad_px,
134 font_id,
135 *char_code,
136 color,
137 font_cache,
138 glyph_em,
139 );
140 }
141 DisplayItem::Line {
142 x,
143 y,
144 width,
145 thickness,
146 color,
147 dashed,
148 } => {
149 render_line(
150 pixmap,
151 *x as f32 * em_px + pad_px,
152 *y as f32 * em_px + pad_px,
153 *width as f32 * em_px,
154 *thickness as f32 * em_px,
155 color,
156 *dashed,
157 );
158 }
159 DisplayItem::Rect {
160 x,
161 y,
162 width,
163 height,
164 color,
165 } => {
166 render_rect(
167 pixmap,
168 *x as f32 * em_px + pad_px,
169 *y as f32 * em_px + pad_px,
170 *width as f32 * em_px,
171 *height as f32 * em_px,
172 color,
173 );
174 }
175 DisplayItem::Path {
176 x,
177 y,
178 commands,
179 fill,
180 color,
181 } => {
182 render_path(
183 pixmap,
184 *x as f32 * em_px + pad_px,
185 *y as f32 * em_px + pad_px,
186 commands,
187 *fill,
188 color,
189 em_px,
190 1.5 * dpr,
191 );
192 }
193 }
194 }
195}
196
197fn sfnt_collection_index(id: FontId) -> u32 {
198 match id {
199 FontId::EmojiFallback => ratex_unicode_font::emoji_font_face_index().unwrap_or(0),
200 FontId::CjkRegular => ratex_unicode_font::unicode_font_face_index().unwrap_or(0),
201 FontId::CjkFallback => ratex_unicode_font::fallback_font_face_index().unwrap_or(0),
202 _ => 0,
203 }
204}
205
206#[allow(clippy::too_many_arguments)]
214fn try_system_unicode_fallback(
215 pixmap: &mut Pixmap,
216 px: f32,
217 py: f32,
218 ch: char,
219 color: &Color,
220 em: f32,
221 font_cache: &HashMap<FontId, FontRef<'_>>,
222 skip_main_regular: bool,
223) -> bool {
224 if !skip_main_regular {
225 if let Some(fallback) = font_cache.get(&FontId::MainRegular) {
226 let fid = fallback.glyph_id(ch);
227 if fid.0 != 0
228 && render_glyph_with_font(
229 pixmap,
230 px,
231 py,
232 FontGlyph {
233 font_id: FontId::MainRegular,
234 font: fallback,
235 glyph_id: fid,
236 },
237 color,
238 em,
239 )
240 {
241 return true;
242 }
243 }
244 }
245 if let Some(cjk_font) = font_cache.get(&FontId::CjkRegular) {
246 let fid = cjk_font.glyph_id(ch);
247 if fid.0 != 0
248 && render_glyph_with_font(
249 pixmap,
250 px,
251 py,
252 FontGlyph {
253 font_id: FontId::CjkRegular,
254 font: cjk_font,
255 glyph_id: fid,
256 },
257 color,
258 em,
259 )
260 {
261 return true;
262 }
263 }
264 if try_emoji_vector_then_bitmap(pixmap, px, py, ch, color, em, font_cache) {
265 return true;
266 }
267 if let Some(fb_font) = font_cache.get(&FontId::CjkFallback) {
268 let fid = fb_font.glyph_id(ch);
269 if fid.0 != 0
270 && render_glyph_with_font(
271 pixmap,
272 px,
273 py,
274 FontGlyph {
275 font_id: FontId::CjkFallback,
276 font: fb_font,
277 glyph_id: fid,
278 },
279 color,
280 em,
281 )
282 {
283 return true;
284 }
285 }
286 false
287}
288
289#[allow(clippy::too_many_arguments)]
293fn try_emoji_vector_then_bitmap(
294 pixmap: &mut Pixmap,
295 px: f32,
296 py: f32,
297 ch: char,
298 color: &Color,
299 em: f32,
300 font_cache: &HashMap<FontId, FontRef<'_>>,
301) -> bool {
302 if try_blit_emoji_raster_fallback(pixmap, px, py, em, ch) {
303 return true;
304 }
305 if let Some(emoji_font) = font_cache.get(&FontId::EmojiFallback) {
306 let eid = emoji_font.glyph_id(ch);
307 if eid.0 != 0
308 && render_glyph_with_font(
309 pixmap,
310 px,
311 py,
312 FontGlyph {
313 font_id: FontId::EmojiFallback,
314 font: emoji_font,
315 glyph_id: eid,
316 },
317 color,
318 em,
319 )
320 {
321 return true;
322 }
323 }
324 false
325}
326
327#[allow(clippy::too_many_arguments)]
328fn render_glyph(
329 pixmap: &mut Pixmap,
330 px: f32,
331 py: f32,
332 font_id: FontId,
333 char_code: u32,
334 color: &Color,
335 font_cache: &HashMap<FontId, FontRef<'_>>,
336 em: f32,
337) {
338 let font = match font_cache.get(&font_id) {
339 Some(f) => f,
340 None => match font_cache.get(&FontId::MainRegular) {
341 Some(f) => f,
342 None => return,
343 },
344 };
345
346 let ch = ratex_font::katex_ttf_glyph_char(font_id, char_code);
347 let glyph_id = font.glyph_id(ch);
348
349 if glyph_id.0 == 0 {
350 let _ = try_system_unicode_fallback(pixmap, px, py, ch, color, em, font_cache, false);
351 return;
352 }
353
354 if font_id == FontId::EmojiFallback {
355 if try_blit_emoji_raster_fallback(pixmap, px, py, em, ch) {
356 return;
357 }
358 let _ = render_glyph_with_font(
359 pixmap,
360 px,
361 py,
362 FontGlyph {
363 font_id,
364 font,
365 glyph_id,
366 },
367 color,
368 em,
369 );
370 return;
371 }
372
373 if font_id == FontId::CjkRegular {
375 if render_glyph_with_font(
376 pixmap,
377 px,
378 py,
379 FontGlyph {
380 font_id: FontId::CjkRegular,
381 font,
382 glyph_id,
383 },
384 color,
385 em,
386 ) {
387 return;
388 }
389 if try_emoji_vector_then_bitmap(pixmap, px, py, ch, color, em, font_cache) {
390 return;
391 }
392 if let Some(fb_font) = font_cache.get(&FontId::CjkFallback) {
393 let fid = fb_font.glyph_id(ch);
394 if fid.0 != 0
395 && render_glyph_with_font(
396 pixmap,
397 px,
398 py,
399 FontGlyph {
400 font_id: FontId::CjkFallback,
401 font: fb_font,
402 glyph_id: fid,
403 },
404 color,
405 em,
406 )
407 {
408 return;
409 }
410 }
411 return;
412 }
413
414 if font_id == FontId::CjkFallback {
415 if render_glyph_with_font(
416 pixmap,
417 px,
418 py,
419 FontGlyph {
420 font_id: FontId::CjkFallback,
421 font,
422 glyph_id,
423 },
424 color,
425 em,
426 ) {
427 return;
428 }
429 let _ = try_emoji_vector_then_bitmap(pixmap, px, py, ch, color, em, font_cache);
430 return;
431 }
432
433 if render_glyph_with_font(
434 pixmap,
435 px,
436 py,
437 FontGlyph {
438 font_id,
439 font,
440 glyph_id,
441 },
442 color,
443 em,
444 ) {
445 return;
446 }
447 let skip_main = font_id == FontId::MainRegular;
449 let _ = try_system_unicode_fallback(pixmap, px, py, ch, color, em, font_cache, skip_main);
450}
451
452struct FontGlyph<'a> {
453 font_id: FontId,
454 font: &'a FontRef<'a>,
455 glyph_id: ab_glyph::GlyphId,
456}
457
458fn render_glyph_with_font(
459 pixmap: &mut Pixmap,
460 px: f32,
461 py: f32,
462 g: FontGlyph<'_>,
463 color: &Color,
464 em: f32,
465) -> bool {
466 let curves = match ratex_font_loader::outline_cache::get_or_compute_outline(
467 g.font_id, g.font, g.glyph_id,
468 ) {
469 Some(c) => c,
470 None => return false,
471 };
472 if curves.is_empty() {
473 return false;
474 }
475
476 let units_per_em = g.font.units_per_em().unwrap_or(1000.0);
477 let mut scale = em / units_per_em;
478
479 if g.font_id == FontId::EmojiFallback {
482 let actual_advance = g.font.h_advance_unscaled(g.glyph_id);
483 let actual_advance_em = actual_advance / units_per_em;
484 let assumed_width = 1.0;
485 if actual_advance_em > 0.01 && actual_advance_em > assumed_width * 1.01 {
486 scale *= assumed_width / actual_advance_em;
487 }
488 }
489
490 let mut builder = PathBuilder::new();
491 let mut last_end: Option<(f32, f32)> = None;
492
493 for curve in curves.iter() {
494 use ab_glyph::OutlineCurve;
495 let (start, end) = match curve {
496 OutlineCurve::Line(p0, p1) => {
497 let sx = px + p0.x * scale;
498 let sy = py - p0.y * scale;
499 let ex = px + p1.x * scale;
500 let ey = py - p1.y * scale;
501 ((sx, sy), (ex, ey))
502 }
503 OutlineCurve::Quad(p0, _, p2) => {
504 let sx = px + p0.x * scale;
505 let sy = py - p0.y * scale;
506 let ex = px + p2.x * scale;
507 let ey = py - p2.y * scale;
508 ((sx, sy), (ex, ey))
509 }
510 OutlineCurve::Cubic(p0, _, _, p3) => {
511 let sx = px + p0.x * scale;
512 let sy = py - p0.y * scale;
513 let ex = px + p3.x * scale;
514 let ey = py - p3.y * scale;
515 ((sx, sy), (ex, ey))
516 }
517 };
518
519 let need_move = match last_end {
521 None => true,
522 Some((lx, ly)) => (lx - start.0).abs() > 0.01 || (ly - start.1).abs() > 0.01,
523 };
524
525 if need_move {
526 if last_end.is_some() {
527 builder.close();
528 }
529 builder.move_to(start.0, start.1);
530 }
531
532 match curve {
533 OutlineCurve::Line(_, p1) => {
534 builder.line_to(px + p1.x * scale, py - p1.y * scale);
535 }
536 OutlineCurve::Quad(_, p1, p2) => {
537 builder.quad_to(
538 px + p1.x * scale,
539 py - p1.y * scale,
540 px + p2.x * scale,
541 py - p2.y * scale,
542 );
543 }
544 OutlineCurve::Cubic(_, p1, p2, p3) => {
545 builder.cubic_to(
546 px + p1.x * scale,
547 py - p1.y * scale,
548 px + p2.x * scale,
549 py - p2.y * scale,
550 px + p3.x * scale,
551 py - p3.y * scale,
552 );
553 }
554 }
555
556 last_end = Some(end);
557 }
558
559 if last_end.is_some() {
560 builder.close();
561 }
562
563 if let Some(path) = builder.finish() {
564 let mut paint = Paint::default();
565 paint.set_color_rgba8(
566 (color.r * 255.0) as u8,
567 (color.g * 255.0) as u8,
568 (color.b * 255.0) as u8,
569 255,
570 );
571 paint.anti_alias = true;
572 pixmap.fill_path(
573 &path,
574 &paint,
575 tiny_skia::FillRule::Winding,
576 Transform::identity(),
577 None,
578 );
579 true
580 } else {
581 false
582 }
583}
584
585fn try_blit_emoji_raster_fallback(
587 pixmap: &mut Pixmap,
588 px: f32,
589 py: f32,
590 em: f32,
591 ch: char,
592) -> bool {
593 let Some(bytes) = ratex_unicode_font::load_emoji_font_arc() else {
594 return false;
595 };
596 let idx = ratex_unicode_font::emoji_font_face_index().unwrap_or(0);
597 try_blit_raster_glyph(pixmap, px, py, em, ch, bytes.as_slice(), idx)
598}
599
600fn try_blit_raster_glyph(
601 pixmap: &mut Pixmap,
602 px: f32,
603 py: f32,
604 em: f32,
605 ch: char,
606 font_bytes: &[u8],
607 face_index: u32,
608) -> bool {
609 let face = match ttf_parser::Face::parse(font_bytes, face_index) {
610 Ok(f) => f,
611 Err(_) => return false,
612 };
613 let gid = match face.glyph_index(ch) {
614 Some(g) => g,
615 None => return false,
616 };
617 let strike = em.round().clamp(8.0, 256.0) as u16;
618 let img = face
619 .glyph_raster_image(gid, strike)
620 .or_else(|| face.glyph_raster_image(gid, u16::MAX));
621 let Some(img) = img else {
622 return false;
623 };
624 let glyph_pm = match raster_glyph_image_to_pixmap(&img) {
625 Some(p) => p,
626 None => return false,
627 };
628 let ppm = f32::from(img.pixels_per_em.max(1));
629 let mut scale = em / ppm;
630 let actual_width_em = f32::from(img.width) / ppm;
632 let assumed_width = 1.0;
633 if actual_width_em > 0.01 && actual_width_em > assumed_width * 1.01 {
634 scale *= assumed_width / actual_width_em;
635 }
636 let top_x = px + f32::from(img.x) * scale;
637 let mut top_y = py - (f32::from(img.y) + f32::from(img.height)) * scale;
641 let center_strike = (f32::from(img.y) + f32::from(img.height) / 2.0) / ppm;
646 let axis = ratex_font::get_global_metrics(0).axis_height as f32;
647 top_y += (center_strike - axis) * em;
648 let paint = PixmapPaint {
649 quality: FilterQuality::Bilinear,
650 ..Default::default()
651 };
652 let transform = Transform::from_row(scale, 0.0, 0.0, scale, top_x, top_y);
653 pixmap.draw_pixmap(0, 0, glyph_pm.as_ref(), &paint, transform, None);
654 true
655}
656
657fn raster_glyph_image_to_pixmap(img: &ttf_parser::RasterGlyphImage<'_>) -> Option<Pixmap> {
658 use ttf_parser::RasterImageFormat;
659 let w = u32::from(img.width);
660 let h = u32::from(img.height);
661 let size = tiny_skia::IntSize::from_wh(w, h)?;
662 match img.format {
663 RasterImageFormat::PNG => Pixmap::decode_png(img.data).ok(),
664 RasterImageFormat::BitmapPremulBgra32 => {
665 let expected = 4usize * w as usize * h as usize;
666 if img.data.len() != expected {
667 return None;
668 }
669 let mut v = Vec::with_capacity(expected);
670 for px in img.data.chunks_exact(4) {
671 let b = px[0];
672 let g = px[1];
673 let r = px[2];
674 let a = px[3];
675 v.extend_from_slice(&[r, g, b, a]);
676 }
677 Pixmap::from_vec(v, size)
678 }
679 RasterImageFormat::BitmapGray8 => {
680 let mut v = Vec::with_capacity(4 * img.data.len());
681 for &g in img.data {
682 v.extend_from_slice(&[g, g, g, 255]);
683 }
684 Pixmap::from_vec(v, size)
685 }
686 _ => None,
687 }
688}
689
690fn render_line(
691 pixmap: &mut Pixmap,
692 x: f32,
693 y: f32,
694 width: f32,
695 thickness: f32,
696 color: &Color,
697 dashed: bool,
698) {
699 let t = thickness.max(1.0);
700 let mut paint = Paint::default();
701 paint.set_color_rgba8(
702 (color.r * 255.0) as u8,
703 (color.g * 255.0) as u8,
704 (color.b * 255.0) as u8,
705 255,
706 );
707
708 if dashed {
709 let dash_len = (4.0 * t).max(2.0);
711 let gap_len = (4.0 * t).max(2.0);
712 let period = dash_len + gap_len;
713 let top = y - t / 2.0;
714 let mut cur_x = x;
715 while cur_x < x + width {
716 let seg_width = (dash_len).min(x + width - cur_x);
717 let seg_width = seg_width.max(2.0);
718 if let Some(rect) = tiny_skia::Rect::from_xywh(cur_x, top, seg_width, t) {
719 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
720 }
721 cur_x += period;
722 }
723 } else if let Some(rect) = tiny_skia::Rect::from_xywh(x, y - t / 2.0, width, t) {
724 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
725 }
726}
727
728fn render_rect(pixmap: &mut Pixmap, x: f32, y: f32, width: f32, height: f32, color: &Color) {
729 let width = width.max(2.0);
733 let height = height.max(2.0);
734 let rect = tiny_skia::Rect::from_xywh(x, y, width, height);
735 if let Some(rect) = rect {
736 let mut paint = Paint::default();
737 paint.set_color_rgba8(
738 (color.r * 255.0) as u8,
739 (color.g * 255.0) as u8,
740 (color.b * 255.0) as u8,
741 255,
742 );
743 pixmap.fill_rect(rect, &paint, Transform::identity(), None);
744 }
745}
746
747#[allow(clippy::too_many_arguments)]
748fn render_path(
749 pixmap: &mut Pixmap,
750 x: f32,
751 y: f32,
752 commands: &[ratex_types::path_command::PathCommand],
753 fill: bool,
754 color: &Color,
755 em: f32,
756 stroke_width_px: f32,
757) {
758 if fill {
765 let mut start = 0;
766 for i in 1..commands.len() {
767 if matches!(
768 commands[i],
769 ratex_types::path_command::PathCommand::MoveTo { .. }
770 ) {
771 render_path_segment(
772 pixmap,
773 x,
774 y,
775 &commands[start..i],
776 fill,
777 color,
778 em,
779 stroke_width_px,
780 );
781 start = i;
782 }
783 }
784 render_path_segment(
785 pixmap,
786 x,
787 y,
788 &commands[start..],
789 fill,
790 color,
791 em,
792 stroke_width_px,
793 );
794 return;
795 }
796 render_path_segment(pixmap, x, y, commands, fill, color, em, stroke_width_px);
797}
798
799#[allow(clippy::too_many_arguments)]
800fn render_path_segment(
801 pixmap: &mut Pixmap,
802 x: f32,
803 y: f32,
804 commands: &[ratex_types::path_command::PathCommand],
805 fill: bool,
806 color: &Color,
807 em: f32,
808 stroke_width_px: f32,
809) {
810 let mut builder = PathBuilder::new();
811 for cmd in commands {
812 match cmd {
813 ratex_types::path_command::PathCommand::MoveTo { x: cx, y: cy } => {
814 builder.move_to(x + *cx as f32 * em, y + *cy as f32 * em);
815 }
816 ratex_types::path_command::PathCommand::LineTo { x: cx, y: cy } => {
817 builder.line_to(x + *cx as f32 * em, y + *cy as f32 * em);
818 }
819 ratex_types::path_command::PathCommand::CubicTo {
820 x1,
821 y1,
822 x2,
823 y2,
824 x: cx,
825 y: cy,
826 } => {
827 builder.cubic_to(
828 x + *x1 as f32 * em,
829 y + *y1 as f32 * em,
830 x + *x2 as f32 * em,
831 y + *y2 as f32 * em,
832 x + *cx as f32 * em,
833 y + *cy as f32 * em,
834 );
835 }
836 ratex_types::path_command::PathCommand::QuadTo {
837 x1,
838 y1,
839 x: cx,
840 y: cy,
841 } => {
842 builder.quad_to(
843 x + *x1 as f32 * em,
844 y + *y1 as f32 * em,
845 x + *cx as f32 * em,
846 y + *cy as f32 * em,
847 );
848 }
849 ratex_types::path_command::PathCommand::Close => {
850 builder.close();
851 }
852 }
853 }
854
855 if let Some(path) = builder.finish() {
856 let mut paint = Paint::default();
857 paint.set_color_rgba8(
858 (color.r * 255.0) as u8,
859 (color.g * 255.0) as u8,
860 (color.b * 255.0) as u8,
861 255,
862 );
863 if fill {
864 paint.anti_alias = true;
865 pixmap.fill_path(
868 &path,
869 &paint,
870 FillRule::EvenOdd,
871 Transform::identity(),
872 None,
873 );
874 } else {
875 let stroke = Stroke {
876 width: stroke_width_px,
877 ..Default::default()
878 };
879 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
880 }
881 }
882}
883
884fn encode_png(pixmap: &Pixmap) -> Result<Vec<u8>, String> {
885 let mut buf = Vec::new();
886 {
887 let mut encoder = png::Encoder::new(&mut buf, pixmap.width(), pixmap.height());
888 encoder.set_color(png::ColorType::Rgba);
889 encoder.set_depth(png::BitDepth::Eight);
890 let mut writer = encoder
891 .write_header()
892 .map_err(|e| format!("PNG header error: {}", e))?;
893 writer
894 .write_image_data(pixmap.data())
895 .map_err(|e| format!("PNG write error: {}", e))?;
896 }
897 Ok(buf)
898}