1#![deny(
8 clippy::unwrap_used,
9 clippy::expect_used,
10 clippy::panic,
11 clippy::unimplemented,
12 clippy::todo,
13 clippy::unreachable
14)]
15#![forbid(unsafe_code)]
16
17mod chart;
18mod colr;
19
20mod compound;
21mod extent;
22mod images;
23mod metafile;
24mod morph_path;
25mod morph_text;
26mod paint;
27mod pattern;
28
29mod pathgrad;
30mod placed;
31mod ramp;
32mod table;
33mod warp;
34
35use std::collections::HashMap;
36use std::sync::Arc;
37
38use deckcraft_color::Rgba;
39use deckcraft_fonts::FontDb;
40use deckcraft_geom::preset::{self, FillMode, Geometry, SubPath};
41use deckcraft_model::resolve::{self, Ctx};
42use deckcraft_model::style::{Effects, Fill, Line};
43use deckcraft_model::text::TextBody;
44use deckcraft_model::{Geom, PhType, Presentation, Shape, ShapeId, ShapeKind, Slide};
45use deckcraft_text::datetime::{self, DateTime};
46use deckcraft_text::{Fields, Opts};
47use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape as _, Vec2};
48use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive};
49use vello_cpu::{RenderContext, Resources, peniko};
50
51pub use extent::{Region, TILE, layout_extent, pasteboard_region, slide_extent};
52pub use images::{decode as decode_image, gif_frame};
53pub use morph_path::PathMorph;
54pub use morph_text::TextMorph;
55pub use placed::{Placed, PlacedLink, PlacedText, place_slide};
56pub use table::row_heights as table_row_heights;
57
58pub const MAX_SIDE: u32 = 16_000;
60
61#[derive(Clone, Debug, Default)]
63pub struct Image {
64 pub width: u32,
65 pub height: u32,
66 pub pixels: Vec<u8>,
67}
68
69impl Image {
70 pub fn to_straight(&self) -> Vec<u8> {
72 let mut out = self.pixels.clone();
73 for px in out.as_chunks_mut::<4>().0 {
74 let a = px.get(3).copied().unwrap_or(255) as u32;
75 if a != 0 && a != 255 {
76 for c in px.iter_mut().take(3) {
77 *c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8;
78 }
79 }
80 }
81 out
82 }
83 pub fn to_png(&self) -> Vec<u8> {
84 let mut px = self.to_straight();
85 px.resize(self.width as usize * self.height as usize * 4, 0);
86 let Some(img) = image::RgbaImage::from_raw(self.width, self.height, px) else {
87 return Vec::new();
88 };
89 let mut buf = Vec::new();
90 if let Err(e) = img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png) {
91 log::error!("PNG encode: {e}");
92 return Vec::new();
93 }
94 buf
95 }
96 pub fn to_jpeg(&self, quality: u8) -> Vec<u8> {
97 let straight = self.to_straight();
98 let rgb: Vec<u8> = straight
99 .as_chunks::<4>()
100 .0
101 .iter()
102 .flat_map(|p| [p[0], p[1], p[2]])
103 .collect();
104 let Some(img) = image::RgbImage::from_raw(self.width, self.height, rgb) else {
105 return Vec::new();
106 };
107 let mut buf = Vec::new();
108 let enc =
109 image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, quality.clamp(1, 100));
110 if let Err(e) = img.write_with_encoder(enc) {
111 log::error!("JPEG encode: {e}");
112 return Vec::new();
113 }
114 buf
115 }
116 pub fn pixel(&self, x: u32, y: u32) -> [u8; 4] {
118 let i = (y as usize * self.width as usize + x as usize) * 4;
119 let p = self.pixels.get(i..i + 4).unwrap_or(&[0, 0, 0, 0]);
120 let a = p.get(3).copied().unwrap_or(0) as u32;
121 let un = |c: u8| {
122 (c as u32 * 255 + a / 2)
123 .checked_div(a)
124 .map(|v| v.min(255) as u8)
125 .unwrap_or(0)
126 };
127 [un(p[0]), un(p[1]), un(p[2]), a as u8]
128 }
129}
130
131#[derive(Clone, Debug)]
133pub struct ShapeState {
134 pub visible: bool,
135 pub opacity: f64,
136 pub offset: Vec2,
138 pub scale: (f64, f64),
139 pub rotate: f64,
140 pub clip: Option<[f64; 4]>,
142 pub tint: Option<Rgba>,
144 pub paras: Vec<(usize, ParaState)>,
146}
147
148impl Default for ShapeState {
149 fn default() -> Self {
150 ShapeState {
151 visible: true,
152 opacity: 1.0,
153 offset: Vec2::ZERO,
154 scale: (1.0, 1.0),
155 rotate: 0.0,
156 clip: None,
157 tint: None,
158 paras: vec![],
159 }
160 }
161}
162
163#[derive(Clone, Copy, Debug)]
165pub struct ParaState {
166 pub visible: bool,
167 pub opacity: f64,
168 pub offset: Vec2,
170}
171
172impl Default for ParaState {
173 fn default() -> Self {
174 ParaState {
175 visible: true,
176 opacity: 1.0,
177 offset: Vec2::ZERO,
178 }
179 }
180}
181
182pub struct RenderOpts<'a> {
183 pub scale: f64,
185 pub edit: bool,
187 pub show_hidden: bool,
189 pub state: Option<&'a dyn Fn(ShapeId) -> ShapeState>,
191 pub skip: &'a [ShapeId],
193 pub skip_text: Option<ShapeId>,
195 pub gif_times: &'a [(ShapeId, f64)],
197 pub threads: u16,
199 pub offset: (f64, f64),
201 pub size: Option<(u32, u32)>,
202 pub clear: Option<Rgba>,
204 pub date_text: Option<String>,
206 pub now: Option<DateTime>,
208}
209
210impl Default for RenderOpts<'_> {
211 fn default() -> Self {
212 RenderOpts {
213 scale: 1.0,
214 edit: false,
215 show_hidden: false,
216 state: None,
217 skip: &[],
218 skip_text: None,
219 gif_times: &[],
220 threads: 0,
221 offset: (0.0, 0.0),
222 size: None,
223 clear: None,
224 date_text: None,
225 now: None,
226 }
227 }
228}
229
230struct SlideFields {
231 num: u32,
232 date: Option<String>,
234 footer: String,
235 now: Option<DateTime>,
237}
238
239impl SlideFields {
240 fn new(num: u32, footer: String, opts: &RenderOpts) -> Self {
241 SlideFields {
242 num,
243 date: opts.date_text.clone(),
244 footer,
245 now: opts.now.or_else(DateTime::now_local),
246 }
247 }
248}
249
250impl Fields for SlideFields {
251 fn field(&self, kind: &str) -> Option<String> {
252 self.field_in(kind, None)
253 }
254
255 fn field_in(&self, kind: &str, lang: Option<&str>) -> Option<String> {
256 if kind == "slidenum" {
257 return Some(self.num.to_string());
258 }
259 if datetime::is_date_field(kind) {
260 return self
263 .date
264 .clone()
265 .or_else(|| datetime::field_text(kind, lang, self.now?));
266 }
267 if kind == "footer" {
268 return Some(self.footer.clone());
269 }
270 None
271 }
272}
273
274pub struct Renderer {
275 resources: Resources,
276 paths: Vec<PathMorph>,
278 bg: Option<(Fill, Option<Rgba>, Affine)>,
281 drawn: Option<Rect>,
284 view: Affine,
287}
288
289impl Default for Renderer {
290 fn default() -> Self {
291 Renderer {
292 resources: Resources::new(),
293 paths: Vec::new(),
294 bg: None,
295 drawn: None,
296 view: Affine::IDENTITY,
297 }
298 }
299}
300
301thread_local! {
302 static RENDERER: std::cell::RefCell<Renderer> = std::cell::RefCell::new(Renderer::default());
303}
304
305pub fn needs_filters(pres: &Presentation, slide: &Slide) -> bool {
307 let Some(rctx) = Ctx::for_slide(pres, slide) else {
308 return false;
309 };
310 let mut any = false;
311 let mut check = |shapes: &[Shape]| {
312 deckcraft_model::walk(shapes, &mut |s, _| {
313 if any {
314 return;
315 }
316 let (e, _) = resolve::effects(&rctx, s);
317 if e.is_some_and(|e| !e.is_empty()) || text_has_effects(&rctx, s) {
318 any = true;
319 }
320 });
321 };
322 check(&slide.shapes);
323 if let Some(l) = rctx.layout {
324 check(&l.shapes);
325 }
326 check(&rctx.master.shapes);
327 any
328}
329
330fn text_has_effects(rctx: &Ctx, s: &Shape) -> bool {
333 use deckcraft_model::text::{ListStyle, RunProps};
334 let fx = |r: &RunProps| {
335 r.shadow.as_ref().is_some_and(|s| !s.inner)
336 || r.glow.as_ref().is_some_and(|g| g.radius > 0.0)
337 };
338 let styled = |l: &ListStyle| l.levels.iter().flatten().any(|lv| fx(&lv.run));
339 let Some(t) = &s.text else { return false };
340 t.paragraphs
341 .iter()
342 .any(|p| p.runs.iter().any(|r| fx(&r.props)) || fx(&p.end_props))
343 || styled(&t.list_style)
344 || styled(&rctx.master.title_style)
345 || styled(&rctx.master.body_style)
346 || styled(&rctx.master.other_style)
347}
348
349pub fn render_slide(pres: &Presentation, index: usize, opts: &RenderOpts) -> Image {
352 render_slide_drawn(pres, index, opts).0
353}
354
355pub fn render_slide_drawn(pres: &Presentation, index: usize, opts: &RenderOpts) -> (Image, Rect) {
359 let page = Rect::new(0.0, 0.0, pres.slide_size.width, pres.slide_size.height);
360 let Some(slide) = pres.slides.get(index) else {
361 return (Image::default(), page);
362 };
363 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
364 RENDERER.with(|r| {
365 let mut r = r.borrow_mut();
366 r.drawn = None;
367 let img = r.slide(pres, slide, index, opts);
368 (img, r.drawn.take())
369 })
370 })) {
371 Ok((img, drawn)) => {
372 let k = opts.scale;
373 let to_slide = |d: Rect| {
374 Rect::new(
375 (d.x0 - opts.offset.0) / k,
376 (d.y0 - opts.offset.1) / k,
377 (d.x1 - opts.offset.0) / k,
378 (d.y1 - opts.offset.1) / k,
379 )
380 };
381 let extent = match drawn {
382 Some(d) if k.is_finite() && k > 0.0 && to_slide(d).is_finite() => {
383 page.union(to_slide(d))
384 }
385 _ => page,
386 };
387 (img, extent)
388 }
389 Err(_) => {
390 log::error!("rendering slide {index} panicked; showing a blank slide");
391 RENDERER.with(|r| *r.borrow_mut() = Renderer::default());
392 let (w, h) = output_size(pres, opts);
393 (
394 Image {
395 width: w as u32,
396 height: h as u32,
397 pixels: vec![255; w as usize * h as usize * 4],
398 },
399 page,
400 )
401 }
402 }
403}
404
405pub fn render_blend(
413 pres: &Presentation,
414 old: usize,
415 new: usize,
416 mix: f64,
417 shapes: &[(Shape, bool)],
418 text: &[TextMorph],
419 paths: &[PathMorph],
420 opts: &RenderOpts,
421) -> Image {
422 let (Some(a), Some(b)) = (pres.slides.get(old), pres.slides.get(new)) else {
423 return Image::default();
424 };
425 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
426 RENDERER.with(|r| {
427 r.borrow_mut()
428 .blend(pres, (a, old), (b, new), mix, shapes, text, paths, opts)
429 })
430 })) {
431 Ok(img) => img,
432 Err(_) => {
433 log::error!(
434 "rendering a frame between slides {old} and {new} panicked; showing the new slide"
435 );
436 RENDERER.with(|r| *r.borrow_mut() = Renderer::default());
437 render_slide(pres, new, opts)
438 }
439 }
440}
441
442pub fn render_layout(
444 pres: &Presentation,
445 master: usize,
446 layout: Option<usize>,
447 opts: &RenderOpts,
448) -> Image {
449 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
450 RENDERER.with(|r| r.borrow_mut().layout(pres, master, layout, opts))
451 })) {
452 Ok(img) => img,
453 Err(_) => {
454 log::error!("rendering a layout panicked; showing a blank image");
455 RENDERER.with(|r| *r.borrow_mut() = Renderer::default());
456 Image::default()
457 }
458 }
459}
460
461fn output_size(pres: &Presentation, opts: &RenderOpts) -> (u16, u16) {
462 let (w, h) = opts.size.unwrap_or((
463 (pres.slide_size.width * opts.scale).round().max(1.0) as u32,
464 (pres.slide_size.height * opts.scale).round().max(1.0) as u32,
465 ));
466 (w.clamp(1, MAX_SIDE) as u16, h.clamp(1, MAX_SIDE) as u16)
467}
468
469pub(crate) fn color(c: Rgba, alpha: f64) -> peniko::Color {
470 let a = (c.a as f64 * alpha.clamp(0.0, 1.0))
471 .round()
472 .clamp(0.0, 255.0) as u8;
473 peniko::Color::from_rgba8(c.r, c.g, c.b, a)
474}
475
476fn shade(c: Rgba, m: FillMode) -> Rgba {
477 match m {
478 FillMode::Lighten => c.lerp(Rgba::WHITE.with_alpha(c.a), 0.4),
479 FillMode::LightenLess => c.lerp(Rgba::WHITE.with_alpha(c.a), 0.2),
480 FillMode::Darken => c.lerp(Rgba::BLACK.with_alpha(c.a), 0.4),
481 FillMode::DarkenLess => c.lerp(Rgba::BLACK.with_alpha(c.a), 0.2),
482 _ => c,
483 }
484}
485
486pub fn shape_geometry(shape: &Shape, w: f64, h: f64) -> Geometry {
488 match &shape.geom {
489 Geom::Preset { name, adj } => preset::build(name, w, h, adj)
490 .or_else(|| preset::build("rect", w, h, &[]))
491 .unwrap_or(Geometry {
492 paths: vec![],
493 text_rect: Rect::new(0.0, 0.0, w, h),
494 handles: vec![],
495 sites: vec![],
496 }),
497 Geom::Custom { paths } => {
498 let mut out = vec![];
499 for p in paths {
500 let (pw, ph) = (
501 if p.w > 0.0 { p.w } else { w.max(1e-9) },
502 if p.h > 0.0 { p.h } else { h.max(1e-9) },
503 );
504 let mut bp = parse_path(&p.d);
505 bp.apply_affine(Affine::scale_non_uniform(w / pw, h / ph));
506 out.push(SubPath {
507 path: bp,
508 fill: p.fill,
509 stroke: p.stroke,
510 even_odd: false,
511 });
512 }
513 Geometry {
514 paths: out,
515 text_rect: Rect::new(0.0, 0.0, w, h),
516 handles: vec![],
517 sites: vec![],
518 }
519 }
520 }
521}
522
523pub fn parse_path(d: &str) -> BezPath {
525 let mut bp = BezPath::new();
526 let toks: Vec<&str> = d
527 .split(|c: char| c.is_whitespace() || c == ',')
528 .filter(|t| !t.is_empty())
529 .collect();
530 let mut i = 0;
531 let num = |i: usize| {
532 toks.get(i)
533 .and_then(|t| t.parse::<f64>().ok())
534 .filter(|v| v.is_finite())
535 };
536 let mut started = false;
537 while i < toks.len() {
538 let t = toks.get(i).copied().unwrap_or("");
539 match t {
540 "M" => {
541 if let (Some(x), Some(y)) = (num(i + 1), num(i + 2)) {
542 bp.move_to((x, y));
543 started = true;
544 }
545 i += 3;
546 }
547 "L" => {
548 if let (Some(x), Some(y)) = (num(i + 1), num(i + 2)) {
549 if !started {
550 bp.move_to((x, y));
551 started = true;
552 } else {
553 bp.line_to((x, y));
554 }
555 }
556 i += 3;
557 }
558 "C" => {
559 if let (Some(a), Some(b), Some(c), Some(d2), Some(e), Some(f)) = (
560 num(i + 1),
561 num(i + 2),
562 num(i + 3),
563 num(i + 4),
564 num(i + 5),
565 num(i + 6),
566 ) && started
567 {
568 bp.curve_to((a, b), (c, d2), (e, f));
569 }
570 i += 7;
571 }
572 "Q" => {
573 if let (Some(a), Some(b), Some(c), Some(d2)) =
574 (num(i + 1), num(i + 2), num(i + 3), num(i + 4))
575 && started
576 {
577 bp.quad_to((a, b), (c, d2));
578 }
579 i += 5;
580 }
581 "Z" | "z" => {
582 if started {
583 bp.close_path();
584 }
585 i += 1;
586 }
587 _ => i += 1,
588 }
589 }
590 bp
591}
592
593pub fn crop_dest(bounds: Rect, crop: [f64; 4]) -> Rect {
595 let [l, t, r, b] = crop.map(|v| {
596 if v.is_finite() {
597 v.clamp(-10.0, 0.99)
598 } else {
599 0.0
600 }
601 });
602 let vis_w = (1.0 - l - r).max(1e-6);
603 let vis_h = (1.0 - t - b).max(1e-6);
604 let fw = bounds.width() / vis_w;
605 let fh = bounds.height() / vis_h;
606 Rect::new(
607 bounds.x0 - l * fw,
608 bounds.y0 - t * fh,
609 bounds.x0 - l * fw + fw,
610 bounds.y0 - t * fh + fh,
611 )
612}
613
614struct Frame<'a> {
615 pres: &'a Presentation,
616 opts: &'a RenderOpts<'a>,
617 fields: SlideFields,
618}
619
620impl Renderer {
621 fn canvas(
622 pres: &Presentation,
623 opts: &RenderOpts,
624 filters: bool,
625 ) -> (RenderContext, u16, u16, Affine) {
626 let (w, h) = output_size(pres, opts);
627 let mut ctx = RenderContext::new_with(
628 w,
629 h,
630 vello_cpu::RenderSettings {
631 num_threads: if cfg!(target_arch = "wasm32") || filters {
632 0
633 } else {
634 opts.threads
635 },
636 ..Default::default()
637 },
638 );
639 if let Some(c) = opts.clear {
640 ctx.set_paint(color(c, 1.0));
641 ctx.fill_rect(&Rect::new(0.0, 0.0, w as f64, h as f64));
642 }
643 let view = Affine::translate((opts.offset.0, opts.offset.1)) * Affine::scale(opts.scale);
644 (ctx, w, h, view)
645 }
646
647 fn track(&mut self, m: Affine, r: Rect) {
649 let b = m.transform_rect_bbox(r);
650 if b.is_finite() {
651 self.drawn = Some(self.drawn.map_or(b, |d| d.union(b)));
652 }
653 }
654
655 fn fields(pres: &Presentation, index: usize, opts: &RenderOpts) -> SlideFields {
656 SlideFields::new(
657 pres.slide_number(index),
658 pres.header_footer.footer_text.clone(),
659 opts,
660 )
661 }
662
663 fn backdrop(
665 &mut self,
666 ctx: &mut RenderContext,
667 f: &Frame,
668 rctx: &Ctx,
669 slide: &Slide,
670 view: Affine,
671 ) {
672 self.background(ctx, f, rctx, Some(slide), view);
673 let (show_layout, show_master) = resolve::show_master_shapes(slide, rctx.layout);
674 if show_master {
675 let mctx = Ctx::for_master(f.pres, rctx.master);
676 for s in &rctx.master.shapes {
677 if s.ph.is_none() {
678 self.shape(ctx, f, &mctx, s, view, 0);
679 }
680 }
681 }
682 if show_layout && let Some(l) = rctx.layout {
683 let lctx = Ctx::for_layout(f.pres, rctx.master, l);
684 for s in &l.shapes {
685 if s.ph.is_none() {
686 self.shape(ctx, f, &lctx, s, view, 0);
687 }
688 }
689 }
690 }
691
692 fn slide(
693 &mut self,
694 pres: &Presentation,
695 slide: &Slide,
696 index: usize,
697 opts: &RenderOpts,
698 ) -> Image {
699 let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, slide));
700 let f = Frame {
701 pres,
702 opts,
703 fields: Self::fields(pres, index, opts),
704 };
705 if let Some(rctx) = Ctx::for_slide(pres, slide) {
706 self.backdrop(&mut ctx, &f, &rctx, slide, view);
707 for s in &slide.shapes {
708 self.shape(&mut ctx, &f, &rctx, s, view, 0);
709 }
710 }
711 self.finish(ctx, w, h)
712 }
713
714 #[allow(clippy::too_many_arguments)]
715 fn blend(
716 &mut self,
717 pres: &Presentation,
718 old: (&Slide, usize),
719 new: (&Slide, usize),
720 mix: f64,
721 shapes: &[(Shape, bool)],
722 text: &[TextMorph],
723 paths: &[PathMorph],
724 opts: &RenderOpts,
725 ) -> Image {
726 let mix = if mix.is_finite() {
727 mix.clamp(0.0, 1.0)
728 } else {
729 1.0
730 };
731 let (mut ctx, w, h, view) = Self::canvas(
732 pres,
733 opts,
734 needs_filters(pres, old.0) || needs_filters(pres, new.0),
735 );
736 let (Some(co), Some(cn)) = (Ctx::for_slide(pres, old.0), Ctx::for_slide(pres, new.0))
737 else {
738 return self.finish(ctx, w, h);
739 };
740 let bopts = RenderOpts {
742 state: None,
743 date_text: opts.date_text.clone(),
744 ..*opts
745 };
746 if mix < 1.0 {
747 let f = Frame {
748 pres,
749 opts: &bopts,
750 fields: Self::fields(pres, old.1, opts),
751 };
752 self.backdrop(&mut ctx, &f, &co, old.0, view);
753 }
754 if mix > 0.0 {
755 let f = Frame {
756 pres,
757 opts: &bopts,
758 fields: Self::fields(pres, new.1, opts),
759 };
760 let layered = mix < 1.0;
761 if layered {
762 ctx.push_layer(None, None, Some(mix as f32), None, None);
763 }
764 self.backdrop(&mut ctx, &f, &cn, new.0, view);
765 if layered {
766 ctx.pop_layer();
767 }
768 }
769 let fo = Frame {
770 pres,
771 opts,
772 fields: Self::fields(pres, old.1, opts),
773 };
774 let fnew = Frame {
775 pres,
776 opts,
777 fields: Self::fields(pres, new.1, opts),
778 };
779 let mut drawn: Vec<ShapeId> = Vec::new();
780 self.paths = paths.to_vec();
782 for (s, from_old) in shapes {
783 let (f, c) = if *from_old { (&fo, &co) } else { (&fnew, &cn) };
784 let Some(tm) = text.iter().find(|m| m.old == s.id || m.new == s.id) else {
785 self.shape(&mut ctx, f, c, s, view, 0);
786 continue;
787 };
788 self.shape(
789 &mut ctx,
790 f,
791 c,
792 &Shape {
793 text: None,
794 ..s.clone()
795 },
796 view,
797 0,
798 );
799 drawn.push(s.id);
800 if !(drawn.contains(&tm.old) && drawn.contains(&tm.new)) {
801 continue;
802 }
803 let find = |id: ShapeId| shapes.iter().map(|(s, _)| s).find(|s| s.id == id);
804 if let (Some(a), Some(b)) = (find(tm.old), find(tm.new)) {
805 let now = b.xfrm.unwrap_or_else(|| resolve::xfrm(&cn, b));
806 morph_text::draw(
807 &mut ctx,
808 (&co, a, &fo.fields),
809 (&cn, b, &fnew.fields),
810 tm,
811 now,
812 view,
813 );
814 }
815 }
816 self.paths.clear();
817 self.finish(ctx, w, h)
818 }
819
820 fn layout(
821 &mut self,
822 pres: &Presentation,
823 mi: usize,
824 li: Option<usize>,
825 opts: &RenderOpts,
826 ) -> Image {
827 let (w, h) = output_size(pres, opts);
828 let mut ctx = RenderContext::new_with(
829 w,
830 h,
831 vello_cpu::RenderSettings {
832 num_threads: 0,
833 ..Default::default()
834 },
835 );
836 let view = Affine::translate((opts.offset.0, opts.offset.1)) * Affine::scale(opts.scale);
837 let f = Frame {
838 pres,
839 opts,
840 fields: SlideFields::new(1, String::new(), opts),
841 };
842 if let Some(m) = pres.masters.get(mi) {
843 match li.and_then(|i| m.layouts.get(i)) {
844 Some(l) => {
845 let c = Ctx::for_layout(pres, m, l);
846 self.background(&mut ctx, &f, &c, None, view);
847 if l.show_master_shapes {
848 let mc = Ctx::for_master(pres, m);
849 for s in m.shapes.iter().filter(|s| s.ph.is_none()) {
850 self.shape(&mut ctx, &f, &mc, s, view, 0);
851 }
852 }
853 for s in &l.shapes {
854 self.shape(&mut ctx, &f, &c, s, view, 0);
855 }
856 }
857 None => {
858 let c = Ctx::for_master(pres, m);
859 self.background(&mut ctx, &f, &c, None, view);
860 for s in &m.shapes {
861 self.shape(&mut ctx, &f, &c, s, view, 0);
862 }
863 }
864 }
865 }
866 self.finish(ctx, w, h)
867 }
868
869 fn finish(&mut self, mut ctx: RenderContext, w: u16, h: u16) -> Image {
870 ctx.flush();
871 let mut pixels = vec![0u8; w as usize * h as usize * 4];
872 if let Some(pm) = vello_cpu::PixmapMut::new(w, h, &mut pixels) {
873 ctx.render(pm, &mut self.resources);
874 }
875 Image {
876 width: w as u32,
877 height: h as u32,
878 pixels,
879 }
880 }
881
882 fn background(
883 &mut self,
884 ctx: &mut RenderContext,
885 f: &Frame,
886 rctx: &Ctx,
887 slide: Option<&Slide>,
888 view: Affine,
889 ) {
890 let (fill, ph) = resolve::background(rctx, slide);
891 let r = Rect::new(0.0, 0.0, f.pres.slide_size.width, f.pres.slide_size.height);
892 ctx.set_transform(view);
893 let path = r.to_path(0.1);
894 ctx.set_paint(color(Rgba::WHITE, 1.0));
897 ctx.fill_path(&path);
898 paint::fill_path(
899 ctx,
900 rctx,
901 &fill,
902 ph,
903 &path,
904 r,
905 view,
906 FillMode::Norm,
907 1.0,
908 f.pres,
909 );
910 self.bg = Some((fill, ph, view));
911 }
912
913 fn shape(
914 &mut self,
915 ctx: &mut RenderContext,
916 f: &Frame,
917 rctx: &Ctx,
918 s: &Shape,
919 parent: Affine,
920 depth: usize,
921 ) {
922 if depth == 0 {
923 let outer = std::mem::replace(&mut self.view, parent);
924 self.shape_at(ctx, f, rctx, s, parent, depth);
925 self.view = outer;
926 } else {
927 self.shape_at(ctx, f, rctx, s, parent, depth);
928 }
929 }
930
931 fn line_space(&self, m: Affine, depth: usize) -> (Affine, Affine) {
936 let view = self.view;
937 if depth > 0 && view.determinant().abs() > 1e-12 && view.is_finite() {
938 (view, view.inverse() * m)
939 } else {
940 (m, Affine::IDENTITY)
941 }
942 }
943
944 fn text_space(&self, m: Affine, depth: usize, w: f64, h: f64) -> (f64, f64, Affine) {
948 let (_, rel) = self.line_space(m, depth);
949 let c = rel.as_coeffs();
950 let (sx, sy) = (c[0].hypot(c[1]), c[2].hypot(c[3]));
951 let ok = |k: f64| k.is_finite() && k > 1e-9;
952 if depth == 0 || !ok(sx) || !ok(sy) || ((sx - 1.0).abs() < 1e-9 && (sy - 1.0).abs() < 1e-9)
953 {
954 return (w, h, m);
955 }
956 let (tw, th) = (w * sx, h * sy);
957 if !tw.is_finite() || !th.is_finite() {
958 return (w, h, m);
959 }
960 (tw, th, m * Affine::scale_non_uniform(1.0 / sx, 1.0 / sy))
961 }
962
963 fn shape_at(
964 &mut self,
965 ctx: &mut RenderContext,
966 f: &Frame,
967 rctx: &Ctx,
968 s: &Shape,
969 parent: Affine,
970 depth: usize,
971 ) {
972 if depth > 32 || (s.hidden && !f.opts.show_hidden) || f.opts.skip.contains(&s.id) {
973 return;
974 }
975 let st = f.opts.state.map(|g| g(s.id)).unwrap_or_default();
977 if !st.visible || st.opacity <= 0.001 {
978 return;
979 }
980 let x = resolve::xfrm(rctx, s);
981 if !x.is_finite() {
982 return;
983 }
984 let mut m = parent * x.affine();
985 if st.offset != Vec2::ZERO || st.scale != (1.0, 1.0) || st.rotate != 0.0 {
986 let c = Point::new(x.w / 2.0, x.h / 2.0);
987 let around = Affine::translate(c.to_vec2())
988 * Affine::rotate(st.rotate.to_radians())
989 * Affine::scale_non_uniform(st.scale.0, st.scale.1)
990 * Affine::translate(-c.to_vec2());
991 m = parent * Affine::translate(st.offset) * x.affine() * around;
992 }
993 let layered = st.opacity < 0.999 || st.clip.is_some();
994 if layered {
995 ctx.set_transform(m);
996 let clip = st
997 .clip
998 .map(|[l, t, r, b]| Rect::new(l * x.w, t * x.h, r * x.w, b * x.h).to_path(0.1));
999 ctx.push_layer(
1000 clip.as_ref(),
1001 None,
1002 Some(st.opacity.clamp(0.0, 1.0) as f32),
1003 None,
1004 None,
1005 );
1006 }
1007 match &s.kind {
1008 ShapeKind::Group { children, child } => {
1009 let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
1010 let gm = parent * deckcraft_geom::group_child_affine(&x, ch);
1011 for c in children {
1012 self.shape(ctx, f, rctx, c, gm, depth + 1);
1013 }
1014 }
1015 ShapeKind::Table(t) => {
1016 let cells = table::draw(ctx, f.pres, rctx, t, m, x.w, x.h, &f.fields);
1018 self.track(
1019 m,
1020 cells
1021 .unwrap_or(Rect::new(0.0, 0.0, x.w, x.h))
1022 .inflate(2.0, 2.0),
1023 );
1024 }
1025 ShapeKind::Chart(c) => {
1026 self.track(m, Rect::new(0.0, 0.0, x.w, x.h));
1027 chart::draw(ctx, rctx, c, m, x.w, x.h)
1028 }
1029 ShapeKind::Ink { strokes } => {
1030 for sk in strokes {
1031 let w = if sk.width.is_finite() {
1032 sk.width.abs()
1033 } else {
1034 0.0
1035 };
1036 for (px, py, _) in &sk.points {
1037 self.track(parent, Rect::new(*px, *py, *px, *py).inflate(w, w));
1038 }
1039 let mut bp = BezPath::new();
1040 for (i, (px, py, _)) in sk.points.iter().enumerate() {
1041 if i == 0 {
1042 bp.move_to((*px, *py));
1043 } else {
1044 bp.line_to((*px, *py));
1045 }
1046 }
1047 let (base, rel) = self.line_space(parent, depth);
1048 bp.apply_affine(rel);
1049 ctx.set_transform(base);
1050 ctx.set_paint(color(sk.color, if sk.highlighter { 0.4 } else { 1.0 }));
1051 ctx.set_stroke(
1052 kurbo::Stroke::new(sk.width.max(0.1))
1053 .with_caps(kurbo::Cap::Round)
1054 .with_join(kurbo::Join::Round),
1055 );
1056 ctx.stroke_path(&bp);
1057 }
1058 }
1059 _ => self.geometry_shape(ctx, f, rctx, s, m, x.w, x.h, &st, depth),
1060 }
1061 if layered {
1062 ctx.pop_layer();
1063 }
1064 }
1065
1066 #[allow(clippy::too_many_arguments)]
1067 fn geometry_shape(
1068 &mut self,
1069 ctx: &mut RenderContext,
1070 f: &Frame,
1071 rctx: &Ctx,
1072 s: &Shape,
1073 m: Affine,
1074 w: f64,
1075 h: f64,
1076 st: &ShapeState,
1077 depth: usize,
1078 ) {
1079 let (geo, fades) = match self.paths.iter().find(|p| p.id == s.id) {
1080 Some(pm) => morph_path::geometry(pm, s, w, h),
1081 None => (shape_geometry(s, w, h), Vec::new()),
1082 };
1083 let fade = |i: usize| {
1084 fades
1085 .get(i)
1086 .copied()
1087 .unwrap_or(deckcraft_geom::morph::Fade {
1088 fill: 1.0,
1089 stroke: 1.0,
1090 })
1091 };
1092 let (fill, fill_ph) = resolve::fill(rctx, s);
1093 let (line, line_ph) = resolve::line(rctx, s);
1094 let (effects, fx_ph) = resolve::effects(rctx, s);
1095 let reach = extent::spill(Some(&line), effects.as_ref(), w, h);
1096 self.track(m, Rect::new(0.0, 0.0, w, h).inflate(reach, reach));
1097 let is_pic = matches!(s.kind, ShapeKind::Picture { .. });
1098 let outline = geo.outline();
1099 let empty_ph = s.ph.is_some()
1101 && s.text.as_ref().is_none_or(TextBody::is_empty)
1102 && !is_pic
1103 && !matches!(s.kind, ShapeKind::Media(_));
1104 let fill = if let Some(t) = st.tint {
1105 Some(Fill::solid(deckcraft_model::ColorRef::rgb(t)))
1106 } else {
1107 fill
1108 };
1109 if let Some(e) = &effects {
1111 let content = match (&s.kind, &fill) {
1113 (ShapeKind::Picture { fill: pf }, _) | (_, Some(Fill::Picture(pf))) => {
1114 Some(paint::resolved(rctx, pf, None).into_owned())
1115 }
1116 _ => None,
1117 };
1118 self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, content.as_ref(), m, w, h);
1119 }
1120 let soft = effects
1121 .as_ref()
1122 .and_then(|e| e.soft_edge)
1123 .filter(|r| *r > 0.0);
1124 if let Some(r) = soft {
1125 ctx.set_transform(Affine::IDENTITY);
1126 ctx.push_layer(None, None, None, None, None);
1127 let _ = r;
1128 }
1129 match &s.kind {
1131 ShapeKind::Picture { fill: pf } => {
1132 ctx.set_transform(m);
1133 let bounds = Rect::new(0.0, 0.0, w, h);
1134 ctx.push_clip_layer(&outline);
1135 let t = f.opts.gif_times.iter().find(|g| g.0 == s.id).map(|g| g.1);
1136 paint::picture(ctx, f.pres, &paint::resolved(rctx, pf, None), bounds, m, t);
1137 ctx.pop_layer();
1138 }
1139 ShapeKind::Opaque {
1142 preview: Some(media),
1143 ..
1144 } => {
1145 ctx.set_transform(m);
1146 let pf = deckcraft_model::style::PictureFill {
1147 media: *media,
1148 ..Default::default()
1149 };
1150 paint::picture(ctx, f.pres, &pf, Rect::new(0.0, 0.0, w, h), m, None);
1151 }
1152 ShapeKind::Media(mc) => {
1153 ctx.set_transform(m);
1154 let bounds = Rect::new(0.0, 0.0, w, h);
1155 if let Some(p) = mc.poster {
1156 let pf = deckcraft_model::style::PictureFill {
1157 media: p,
1158 ..Default::default()
1159 };
1160 paint::picture(ctx, f.pres, &pf, bounds, m, None);
1161 } else if mc.video {
1162 ctx.set_paint(peniko::Color::from_rgba8(20, 20, 24, 255));
1163 ctx.fill_rect(&bounds);
1164 }
1165 if !mc.video || mc.poster.is_none() {
1166 paint::media_glyph(ctx, mc.video, bounds);
1167 }
1168 }
1169 _ => {
1170 if let Some(fl) = &fill
1171 && !(empty_ph && f.opts.edit && s.fill.is_none())
1172 {
1173 for (i, sp) in geo.paths.iter().enumerate() {
1174 if sp.fill == FillMode::None || fade(i).fill <= 0.0 {
1175 continue;
1176 }
1177 ctx.set_transform(m);
1178 ctx.set_fill_rule(if sp.even_odd {
1179 peniko::Fill::EvenOdd
1180 } else {
1181 peniko::Fill::NonZero
1182 });
1183 match (fl, &self.bg) {
1184 (Fill::Background, Some((bg, ph, view))) => {
1187 let slide = Rect::new(
1188 0.0,
1189 0.0,
1190 f.pres.slide_size.width,
1191 f.pres.slide_size.height,
1192 );
1193 let path = (view.inverse() * m) * sp.path.clone();
1194 paint::fill_path(
1195 ctx,
1196 rctx,
1197 bg,
1198 *ph,
1199 &path,
1200 slide,
1201 *view,
1202 sp.fill,
1203 fade(i).fill,
1204 f.pres,
1205 );
1206 }
1207 _ => paint::fill_path(
1208 ctx,
1209 rctx,
1210 fl,
1211 fill_ph,
1212 &sp.path,
1213 Rect::new(0.0, 0.0, w, h),
1214 m,
1215 sp.fill,
1216 fade(i).fill,
1217 f.pres,
1218 ),
1219 }
1220 ctx.set_fill_rule(peniko::Fill::NonZero);
1221 }
1222 }
1223 }
1224 }
1225 if let Some(ov) = effects.as_ref().and_then(|e| e.fill_overlay.as_ref())
1228 && fill.as_ref().is_some_and(|f| !f.is_none())
1229 {
1230 let mix = match ov.blend.as_str() {
1231 "mult" => peniko::Mix::Multiply,
1232 "screen" => peniko::Mix::Screen,
1233 "darken" => peniko::Mix::Darken,
1234 "lighten" => peniko::Mix::Lighten,
1235 _ => peniko::Mix::Normal,
1236 };
1237 for (i, sp) in geo
1238 .paths
1239 .iter()
1240 .enumerate()
1241 .filter(|(i, sp)| sp.fill != FillMode::None && fade(*i).fill > 0.0)
1242 {
1243 ctx.set_transform(m);
1244 ctx.push_layer(
1245 Some(&sp.path),
1246 Some(peniko::BlendMode::new(mix, peniko::Compose::SrcOver)),
1247 None,
1248 None,
1249 None,
1250 );
1251 paint::fill_path(
1252 ctx,
1253 rctx,
1254 &ov.fill,
1255 fx_ph,
1256 &sp.path,
1257 Rect::new(0.0, 0.0, w, h),
1258 m,
1259 FillMode::Norm,
1260 fade(i).fill,
1261 f.pres,
1262 );
1263 ctx.pop_layer();
1264 }
1265 }
1266 if line.fill.as_ref().is_some_and(|f| !f.is_none()) {
1268 let lw = line.width.unwrap_or(0.75).max(0.0);
1269 let (base, rel) = self.line_space(m, depth);
1270 for (i, sp) in geo
1271 .paths
1272 .iter()
1273 .enumerate()
1274 .filter(|(i, p)| p.stroke && fade(*i).stroke > 0.0)
1275 {
1276 let path = if rel == Affine::IDENTITY {
1277 std::borrow::Cow::Borrowed(&sp.path)
1278 } else {
1279 let mut moved = sp.path.clone();
1280 moved.apply_affine(rel);
1281 std::borrow::Cow::Owned(moved)
1282 };
1283 stroke(ctx, rctx, &line, line_ph, &path, base, lw, fade(i).stroke);
1284 }
1285 if s.is_line() || geo.is_open() {
1286 arrowheads(ctx, rctx, &line, line_ph, &geo, rel, base, lw);
1287 }
1288 }
1289 if let Some(r) = soft {
1290 ctx.set_transform(m);
1294 ctx.push_layer(
1295 None,
1296 Some(peniko::BlendMode::new(
1297 peniko::Mix::Normal,
1298 peniko::Compose::DestIn,
1299 )),
1300 None,
1301 None,
1302 Some(blur(r / 3.0 * scale_of(m))),
1303 );
1304 ctx.set_paint(peniko::Color::from_rgba8(0, 0, 0, 255));
1305 ctx.fill_path(&outline);
1306 ctx.push_blend_layer(peniko::BlendMode::new(
1307 peniko::Mix::Normal,
1308 peniko::Compose::DestOut,
1309 ));
1310 ctx.set_stroke(kurbo::Stroke::new(r * 1.7).with_join(kurbo::Join::Round));
1311 ctx.stroke_path(&outline);
1312 ctx.pop_layer();
1313 ctx.pop_layer();
1314 ctx.pop_layer();
1315 }
1316 if let Some(e) = &effects
1318 && let Some(sh) = &e.inner_shadow
1319 {
1320 ctx.set_transform(m);
1321 ctx.push_clip_layer(&outline);
1322 let c = rctx.color(&sh.color, fx_ph);
1323 let d = Vec2::new(sh.dir.to_radians().cos(), sh.dir.to_radians().sin()) * sh.dist;
1324 ctx.push_layer(
1328 None,
1329 None,
1330 None,
1331 None,
1332 Some(blur(sh.blur / 3.0 * scale_of(m))),
1333 );
1334 ctx.set_paint(color(c, 1.0));
1335 let mut ring = Rect::new(0.0, 0.0, w, h)
1336 .inflate(w.max(h), w.max(h))
1337 .to_path(0.1);
1338 let mut hole = outline.clone();
1339 hole.apply_affine(Affine::translate(-d));
1340 ring.extend(hole.iter());
1341 ctx.set_fill_rule(peniko::Fill::EvenOdd);
1342 ctx.fill_path(&ring);
1343 ctx.set_fill_rule(peniko::Fill::NonZero);
1344 ctx.pop_layer();
1345 ctx.pop_layer();
1346 }
1347 let skip_text = f.opts.skip_text == Some(s.id);
1349 if let Some(body) = &s.text
1350 && !skip_text
1351 {
1352 let (tw, th, m) = self.text_space(m, depth, w, h);
1355 let text_geo;
1356 let geo = if (tw, th) == (w, h) {
1357 &geo
1358 } else {
1359 text_geo = shape_geometry(s, tw, th);
1360 &text_geo
1361 };
1362 let tr = geo.text_rect;
1363 if !body.is_empty() {
1364 let l = deckcraft_text::layout(
1365 rctx,
1366 s,
1367 body,
1368 &Opts {
1369 rect: tr,
1370 fields: &f.fields,
1371 prompt_color: None,
1372 no_shrink: false,
1373 },
1374 );
1375 let tm = text_transform(m, tr, l.rotation);
1376 for b in l.lines.iter().filter_map(line_bounds) {
1377 let pad = b.height() * 0.3;
1379 self.track(tm, b.inflate(pad, pad));
1380 }
1381 match text_warp(rctx, s, tr).filter(|_| l.rotation == 0.0) {
1382 Some(w) => warp::draw(ctx, &l, tr, m, &st.paras, &w),
1383 None => draw_layout_with(ctx, &l, tr, m, &st.paras),
1384 }
1385 } else if f.opts.edit && s.ph.is_some() {
1386 let kind = s.ph_type().unwrap_or(PhType::Body);
1387 let prompt_body = prompt_body(rctx, s, kind);
1388 if !prompt_body.is_empty() {
1389 let grey = Rgba::rgb(0x59, 0x59, 0x59);
1390 draw_text(ctx, rctx, s, &prompt_body, tr, m, &f.fields, Some(grey));
1391 }
1392 }
1393 }
1394 if empty_ph && f.opts.edit {
1395 ctx.set_transform(m);
1397 let px = 1.0 / scale_of(m).max(1e-6);
1398 ctx.set_paint(peniko::Color::from_rgba8(0xA6, 0xA6, 0xA6, 255));
1399 ctx.set_stroke(kurbo::Stroke::new(px).with_dashes(0.0, [3.0 * px, 2.0 * px]));
1400 ctx.stroke_path(&Rect::new(0.0, 0.0, w, h).to_path(0.1));
1401 if matches!(
1402 s.ph_type(),
1403 Some(
1404 PhType::Picture
1405 | PhType::Chart
1406 | PhType::Table
1407 | PhType::Media
1408 | PhType::Diagram
1409 )
1410 ) {
1411 paint::placeholder_icon(
1412 ctx,
1413 s.ph_type().unwrap_or(PhType::Picture),
1414 Rect::new(0.0, 0.0, w, h),
1415 );
1416 }
1417 }
1418 }
1419
1420 fn effects_below(
1421 &mut self,
1422 ctx: &mut RenderContext,
1423 rctx: &Ctx,
1424 e: &Effects,
1425 ph: Option<Rgba>,
1426 geo: &Geometry,
1427 fill: &Option<Fill>,
1428 content: Option<&deckcraft_model::style::PictureFill>,
1429 m: Affine,
1430 w: f64,
1431 h: f64,
1432 ) {
1433 let silhouette = {
1434 let mut p = BezPath::new();
1435 for sp in &geo.paths {
1436 if sp.fill != FillMode::None || fill.is_none() {
1437 p.extend(sp.path.iter());
1438 }
1439 }
1440 if p.elements().is_empty() {
1441 geo.outline()
1442 } else {
1443 p
1444 }
1445 };
1446 let k = scale_of(m);
1447 if let Some(sh) = &e.outer_shadow {
1448 let c = rctx.color(&sh.color, ph);
1449 let d = Vec2::new(sh.dir.to_radians().cos(), sh.dir.to_radians().sin()) * sh.dist;
1450 let c0 = Point::new(w / 2.0, h / 2.0);
1451 let local = Affine::translate(c0.to_vec2())
1452 * Affine::new([
1453 sh.sx,
1454 sh.ky.to_radians().tan(),
1455 sh.kx.to_radians().tan(),
1456 sh.sy,
1457 0.0,
1458 0.0,
1459 ])
1460 * Affine::translate(-c0.to_vec2());
1461 let sm = if sh.rotate_with_shape {
1463 m * Affine::translate(d) * local
1464 } else {
1465 pre_translate(m, d * k) * local
1466 };
1467 ctx.set_transform(Affine::IDENTITY);
1468 ctx.push_layer(
1470 None,
1471 None,
1472 Some((c.a as f32) / 255.0),
1473 None,
1474 Some(blur(sh.blur / 3.0 * k)),
1475 );
1476 ctx.set_transform(sm);
1477 match content {
1478 Some(pf) => {
1482 ctx.push_clip_layer(&silhouette);
1483 paint::picture(ctx, rctx.pres, pf, Rect::new(0.0, 0.0, w, h), sm, None);
1484 ctx.push_layer(
1485 None,
1486 Some(peniko::BlendMode::new(
1487 peniko::Mix::Normal,
1488 peniko::Compose::SrcIn,
1489 )),
1490 None,
1491 None,
1492 None,
1493 );
1494 ctx.set_paint(color(c.with_alpha(255), 1.0));
1495 ctx.fill_rect(&Rect::new(0.0, 0.0, w, h).inflate(1.0, 1.0));
1496 ctx.pop_layer();
1497 ctx.pop_layer();
1498 }
1499 None => {
1500 ctx.set_paint(color(c.with_alpha(255), 1.0));
1501 ctx.fill_path(&silhouette);
1502 }
1503 }
1504 ctx.pop_layer();
1505 }
1506 if let Some(g) = &e.glow {
1507 let c = rctx.color(&g.color, ph);
1508 ctx.set_transform(Affine::IDENTITY);
1509 ctx.push_layer(
1512 None,
1513 None,
1514 Some((c.a as f32) / 255.0),
1515 None,
1516 Some(blur(g.radius * 0.17 * k)),
1517 );
1518 ctx.set_transform(m);
1519 ctx.set_paint(color(c.with_alpha(255), 1.0));
1520 ctx.set_stroke(
1521 kurbo::Stroke::new(g.radius.max(0.0) * 0.95).with_join(kurbo::Join::Round),
1522 );
1523 ctx.stroke_path(&silhouette);
1524 ctx.fill_path(&silhouette);
1525 ctx.pop_layer();
1526 }
1527 if let Some(rf) = &e.reflection {
1528 let mm = m
1530 * Affine::translate((0.0, 2.0 * h + rf.dist))
1531 * Affine::scale_non_uniform(1.0, -1.0);
1532 ctx.set_transform(Affine::IDENTITY);
1533 ctx.push_layer(
1534 None,
1535 None,
1536 Some(rf.start_alpha.clamp(0.0, 1.0) as f32 * 0.6),
1537 None,
1538 Some(blur(rf.blur * 0.5 * k)),
1539 );
1540 ctx.set_transform(mm);
1541 if let Some(fl) = fill {
1542 paint::fill_path(
1543 ctx,
1544 rctx,
1545 fl,
1546 ph,
1547 &silhouette,
1548 Rect::new(0.0, 0.0, w, h),
1549 mm,
1550 FillMode::Norm,
1551 1.0,
1552 rctx.pres,
1553 );
1554 }
1555 ctx.push_layer(
1557 None,
1558 Some(peniko::BlendMode::new(
1559 peniko::Mix::Normal,
1560 peniko::Compose::DestIn,
1561 )),
1562 None,
1563 None,
1564 None,
1565 );
1566 let grad = peniko::Gradient::new_linear(
1567 Point::new(0.0, h),
1568 Point::new(0.0, h * (1.0 - rf.end_pos.clamp(0.01, 1.0))),
1569 )
1570 .with_stops(
1571 [
1572 (0.0, peniko::Color::from_rgba8(0, 0, 0, 255)),
1573 (1.0, peniko::Color::from_rgba8(0, 0, 0, 0)),
1574 ]
1575 .as_slice(),
1576 );
1577 ctx.set_paint(grad);
1578 ctx.fill_rect(&Rect::new(0.0, 0.0, w, h).inflate(4.0, 4.0));
1579 ctx.pop_layer();
1580 ctx.pop_layer();
1581 }
1582 }
1583}
1584
1585fn pre_translate(m: Affine, d: Vec2) -> Affine {
1586 Affine::translate(d) * m
1587}
1588
1589pub(crate) fn scale_of(m: Affine) -> f64 {
1590 m.determinant().abs().sqrt()
1591}
1592
1593fn blur(sigma: f64) -> Filter {
1594 Filter::from_primitive(FilterPrimitive::GaussianBlur {
1595 std_deviation: sigma.clamp(0.0, 500.0) as f32,
1596 edge_mode: EdgeMode::None,
1597 })
1598}
1599
1600fn stroke(
1601 ctx: &mut RenderContext,
1602 rctx: &Ctx,
1603 line: &Line,
1604 ph: Option<Rgba>,
1605 path: &BezPath,
1606 m: Affine,
1607 lw: f64,
1608 alpha: f64,
1609) {
1610 use deckcraft_model::style::{LineCap, LineJoin};
1611 let cap = match line.cap.unwrap_or_default() {
1612 LineCap::Flat => kurbo::Cap::Butt,
1613 LineCap::Round => kurbo::Cap::Round,
1614 LineCap::Square => kurbo::Cap::Square,
1615 };
1616 let join = match line.join.unwrap_or_default() {
1617 LineJoin::Round => kurbo::Join::Round,
1618 LineJoin::Bevel => kurbo::Join::Bevel,
1619 LineJoin::Miter => kurbo::Join::Miter,
1620 };
1621 let min_w = 0.5 / scale_of(m).max(1e-6);
1623 let mut sk = kurbo::Stroke::new(lw.max(min_w))
1624 .with_caps(cap)
1625 .with_join(join);
1626 if let Some(d) = &line.dash {
1627 let pat: Vec<f64> = d.pattern().iter().map(|v| v * lw.max(0.75)).collect();
1628 if !pat.is_empty() && pat.iter().all(|v| *v > 0.0) {
1629 sk = sk.with_dashes(0.0, pat);
1630 }
1631 }
1632 ctx.set_transform(m);
1633 match &line.fill {
1634 Some(Fill::Solid { color: c }) => {
1635 ctx.set_paint(color(rctx.color(c, ph), alpha));
1636 }
1637 Some(Fill::Gradient(g)) => {
1638 let c = g
1639 .stops
1640 .first()
1641 .map(|s| rctx.color(&s.color, ph))
1642 .unwrap_or(Rgba::BLACK);
1643 ctx.set_paint(color(c, alpha));
1644 }
1645 _ => ctx.set_paint(color(Rgba::BLACK, alpha)),
1646 }
1647 let compound = line.compound.unwrap_or_default();
1649 if compound != deckcraft_model::style::Compound::Single && lw * scale_of(m) >= 1.5 {
1650 let tolerance = 0.1 / scale_of(m).max(1e-6);
1651 for &(a, b) in compound::bands(compound) {
1652 let d = (-0.5 + (a + b) / 2.0) * lw;
1653 let mut band = sk.clone();
1654 band.width = (b - a) * lw;
1655 ctx.set_stroke(band);
1656 ctx.stroke_path(&compound::offset(path, d, tolerance));
1657 }
1658 return;
1659 }
1660 ctx.set_stroke(sk);
1661 ctx.stroke_path(path);
1662}
1663
1664fn end_points(path: &BezPath) -> Option<((Point, Point), (Point, Point))> {
1665 let els: Vec<PathEl> = path.elements().to_vec();
1666 let mut pts: Vec<Point> = vec![];
1667 for e in &els {
1668 match e {
1669 PathEl::MoveTo(p) | PathEl::LineTo(p) => pts.push(*p),
1670 PathEl::QuadTo(a, b) => {
1671 pts.push(*a);
1672 pts.push(*b);
1673 }
1674 PathEl::CurveTo(a, b, c) => {
1675 pts.push(*a);
1676 pts.push(*b);
1677 pts.push(*c);
1678 }
1679 PathEl::ClosePath => {}
1680 }
1681 }
1682 if pts.len() < 2 {
1683 return None;
1684 }
1685 let first = *pts.first()?;
1686 let second = pts
1687 .iter()
1688 .skip(1)
1689 .find(|p| (**p - first).hypot() > 1e-6)
1690 .copied()
1691 .unwrap_or(*pts.get(1)?);
1692 let last = *pts.last()?;
1693 let prev = pts
1694 .iter()
1695 .rev()
1696 .skip(1)
1697 .find(|p| (**p - last).hypot() > 1e-6)
1698 .copied()
1699 .unwrap_or(*pts.get(pts.len() - 2)?);
1700 Some(((first, second), (last, prev)))
1701}
1702
1703#[allow(clippy::too_many_arguments)]
1704fn arrowheads(
1705 ctx: &mut RenderContext,
1706 rctx: &Ctx,
1707 line: &Line,
1708 ph: Option<Rgba>,
1709 geo: &Geometry,
1710 rel: Affine,
1711 m: Affine,
1712 lw: f64,
1713) {
1714 let Some(sp) = geo.paths.iter().find(|p| p.stroke) else {
1715 return;
1716 };
1717 let Some(((a, a2), (b, b2))) = end_points(&sp.path) else {
1718 return;
1719 };
1720 let (a, a2, b, b2) = (rel * a, rel * a2, rel * b, rel * b2);
1722 let col = match &line.fill {
1723 Some(Fill::Solid { color: c }) => rctx.color(c, ph),
1724 _ => Rgba::BLACK,
1725 };
1726 let lw = lw.max(0.75);
1727 for (end, tip, from) in [(&line.head, a, a2), (&line.tail, b, b2)] {
1728 let Some(e) = end else { continue };
1729 if e.kind == "none" {
1730 continue;
1731 }
1732 let sz = |s: &str| match s {
1733 "sm" => 2.0,
1734 "lg" => 5.0,
1735 _ => 3.0,
1736 };
1737 let (wl, ll) = (sz(&e.w) * lw, sz(&e.len) * lw);
1738 let dir = (tip - from).normalize();
1739 if !dir.x.is_finite() {
1740 continue;
1741 }
1742 let n = Vec2::new(-dir.y, dir.x);
1743 let base = tip - dir * ll;
1744 let mut p = BezPath::new();
1745 match e.kind.as_str() {
1746 "oval" => {
1747 let c = kurbo::Ellipse::new(tip, (ll / 2.0, wl / 2.0), dir.y.atan2(dir.x));
1748 p = c.to_path(0.05);
1749 }
1750 "diamond" => {
1751 p.move_to(tip + dir * ll / 2.0);
1752 p.line_to(tip + n * wl / 2.0);
1753 p.line_to(tip - dir * ll / 2.0);
1754 p.line_to(tip - n * wl / 2.0);
1755 p.close_path();
1756 }
1757 "stealth" => {
1758 p.move_to(tip);
1759 p.line_to(base + n * wl / 2.0);
1760 p.line_to(tip - dir * ll * 0.6);
1761 p.line_to(base - n * wl / 2.0);
1762 p.close_path();
1763 }
1764 "arrow" => {
1765 let mut o = BezPath::new();
1766 o.move_to(base + n * wl / 2.0);
1767 o.line_to(tip);
1768 o.line_to(base - n * wl / 2.0);
1769 ctx.set_transform(m);
1770 ctx.set_paint(color(col, 1.0));
1771 ctx.set_stroke(kurbo::Stroke::new(lw).with_join(kurbo::Join::Miter));
1772 ctx.stroke_path(&o);
1773 continue;
1774 }
1775 _ => {
1776 p.move_to(tip);
1777 p.line_to(base + n * wl / 2.0);
1778 p.line_to(base - n * wl / 2.0);
1779 p.close_path();
1780 }
1781 }
1782 ctx.set_transform(m);
1783 ctx.set_paint(color(col, 1.0));
1784 ctx.fill_path(&p);
1785 }
1786}
1787
1788fn prompt_body(rctx: &Ctx, s: &Shape, kind: PhType) -> TextBody {
1790 let (lp, _) = resolve::parents(rctx, s);
1792 let custom = lp
1793 .and_then(|l| {
1794 l.ph.as_ref()
1795 .filter(|p| p.has_custom_prompt)
1796 .and(l.text.as_ref())
1797 })
1798 .filter(|t| !t.is_empty());
1799 if let Some(t) = custom {
1800 return t.clone();
1801 }
1802 if matches!(rctx.owner, resolve::Owner::Layout | resolve::Owner::Master) {
1803 return TextBody::default();
1804 }
1805 let txt = kind.prompt();
1806 if txt.is_empty() || kind.is_footer_kind() {
1807 return TextBody::default();
1808 }
1809 let mut t = TextBody::from_text(txt);
1810 if matches!(
1811 kind,
1812 PhType::Picture
1813 | PhType::Chart
1814 | PhType::Table
1815 | PhType::Media
1816 | PhType::Diagram
1817 | PhType::ClipArt
1818 ) {
1819 t.body.anchor = Some(deckcraft_model::text::Anchor::Bottom);
1820 if let Some(p) = t.paragraphs.first_mut() {
1821 p.props.align = Some(deckcraft_model::text::Align::Center);
1822 p.props.bullet = Some(deckcraft_model::text::Bullet::None);
1823 p.props.margin_left = Some(0.0);
1824 p.props.indent = Some(0.0);
1825 for r in &mut p.runs {
1826 r.props.size = Some(14.0);
1827 }
1828 }
1829 }
1830 t
1831}
1832
1833thread_local! {
1834 static GLYPH_CACHE: std::cell::RefCell<HashMap<(u32, u32), Arc<BezPath>>> = std::cell::RefCell::new(HashMap::new());
1835}
1836
1837fn glyph(face: &deckcraft_fonts::FontFace, gid: u32) -> Arc<BezPath> {
1838 let key = (face.id(), gid);
1839 if let Some(p) = GLYPH_CACHE.with(|c| c.borrow().get(&key).cloned()) {
1840 return p;
1841 }
1842 let p = FontDb::global().outline(face, gid);
1843 GLYPH_CACHE.with(|c| {
1844 let mut c = c.borrow_mut();
1845 if c.len() > 200_000 {
1846 c.clear();
1847 }
1848 c.insert(key, p.clone());
1849 });
1850 p
1851}
1852
1853pub fn draw_text(
1855 ctx: &mut RenderContext,
1856 rctx: &Ctx,
1857 s: &Shape,
1858 body: &TextBody,
1859 tr: Rect,
1860 m: Affine,
1861 fields: &dyn Fields,
1862 prompt: Option<Rgba>,
1863) {
1864 let l = deckcraft_text::layout(
1865 rctx,
1866 s,
1867 body,
1868 &Opts {
1869 rect: tr,
1870 fields,
1871 prompt_color: prompt,
1872 no_shrink: false,
1873 },
1874 );
1875 draw_layout(ctx, &l, tr, m);
1876}
1877
1878pub fn text_transform(m: Affine, tr: Rect, rotation: f64) -> Affine {
1886 let c = tr.center().to_vec2();
1887 let mut t = m;
1888 if m.determinant() < 0.0 && c.is_finite() {
1889 t = t * Affine::translate(c) * Affine::scale_non_uniform(-1.0, 1.0) * Affine::translate(-c);
1890 }
1891 if rotation != 0.0 && rotation.is_finite() && c.is_finite() {
1892 t = t
1893 * Affine::translate(c)
1894 * Affine::rotate(rotation.to_radians())
1895 * Affine::translate(-c);
1896 }
1897 t
1898}
1899
1900pub fn text_warp(rctx: &Ctx, s: &Shape, tr: Rect) -> Option<deckcraft_geom::warp::Warp> {
1902 let bp = resolve::body(rctx, s);
1903 let name = bp.warp.as_deref()?;
1904 deckcraft_geom::warp::warp(name, &bp.warp_adj, tr)
1905}
1906
1907pub fn draw_layout(ctx: &mut RenderContext, l: &deckcraft_text::TextLayout, tr: Rect, m: Affine) {
1909 draw_layout_with(ctx, l, tr, m, &[]);
1910}
1911
1912pub fn draw_layout_with(
1914 ctx: &mut RenderContext,
1915 l: &deckcraft_text::TextLayout,
1916 tr: Rect,
1917 m: Affine,
1918 paras: &[(usize, ParaState)],
1919) {
1920 let para = |i: usize| {
1921 paras
1922 .iter()
1923 .find(|(p, _)| *p == i)
1924 .map(|(_, s)| *s)
1925 .unwrap_or_default()
1926 };
1927 let m = text_transform(m, tr, l.rotation);
1928 let deco = |ctx: &mut RenderContext, d: &deckcraft_text::Deco| {
1929 let ps = para(d.para);
1930 if !ps.visible || ps.opacity <= 0.001 {
1931 return;
1932 }
1933 ctx.set_transform(m * Affine::translate(ps.offset));
1934 ctx.set_paint(color(d.color, ps.opacity));
1935 ctx.fill_rect(&d.rect);
1936 };
1937 for d in l.decos.iter().filter(|d| d.behind) {
1938 deco(ctx, d);
1939 }
1940 for pass in [TextEffect::Shadow, TextEffect::Glow] {
1942 for run in &l.runs {
1943 let ps = para(run.para);
1944 if ps.visible && ps.opacity > 0.001 {
1945 text_effect(ctx, run, pass, m * Affine::translate(ps.offset), ps.opacity);
1946 }
1947 }
1948 }
1949 for run in &l.runs {
1950 if (run.alpha <= 0.0 || run.color.a == 0) && run.outline.is_none() {
1951 continue;
1952 }
1953 let ps = para(run.para);
1954 if !ps.visible || ps.opacity <= 0.001 {
1955 continue;
1956 }
1957 let m = m * Affine::translate(ps.offset);
1958 let mut grads: Vec<(usize, peniko::Gradient)> = Vec::new();
1961 for (gid, x, y) in &run.glyphs {
1962 let at = m * Affine::translate((*x, *y));
1963 let Some(g) = run.gradient.as_ref().filter(|g| !g.stops.is_empty()) else {
1964 draw_glyph(ctx, run, *gid, at, ps.opacity);
1965 continue;
1966 };
1967 let li = line_at(l, run.para, *y);
1968 if !grads.iter().any(|(i, _)| *i == li) {
1969 let scheme = deckcraft_color::ColorScheme {
1970 name: String::new(),
1971 colors: [Rgba::BLACK; 12],
1972 };
1973 let stops = g
1974 .stops
1975 .iter()
1976 .map(|s| (s.pos, s.color.resolve(&scheme, None)))
1977 .collect();
1978 let bounds = l.lines.get(li).and_then(line_bounds).unwrap_or(tr);
1979 grads.push((
1980 li,
1981 paint::gradient(&g.shape, stops, bounds, run.alpha * ps.opacity),
1982 ));
1983 }
1984 let fill = grads
1985 .iter()
1986 .find(|(i, _)| *i == li)
1987 .map(|(_, g)| (g, Affine::translate((-*x, -*y))));
1988 draw_glyph_with(ctx, run, *gid, at, ps.opacity, fill);
1989 }
1990 }
1991 for d in l.decos.iter().filter(|d| !d.behind) {
1992 deco(ctx, d);
1993 }
1994}
1995
1996fn line_at(l: &deckcraft_text::TextLayout, para: usize, y: f64) -> usize {
1998 l.lines
1999 .iter()
2000 .enumerate()
2001 .filter(|(_, li)| li.para == para)
2002 .min_by(|a, b| {
2003 (a.1.baseline - y)
2004 .abs()
2005 .total_cmp(&(b.1.baseline - y).abs())
2006 })
2007 .map(|(i, _)| i)
2008 .unwrap_or(0)
2009}
2010
2011fn line_bounds(li: &deckcraft_text::LineInfo) -> Option<Rect> {
2013 let (x0, x1) = li
2014 .caret_x
2015 .iter()
2016 .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), x| {
2017 (a.min(*x), b.max(*x))
2018 });
2019 let r = Rect::new(x0, li.top, x1, li.bottom);
2020 (r.is_finite() && r.width() > 0.0 && r.height() > 0.0).then_some(r)
2021}
2022
2023#[derive(Clone, Copy, PartialEq)]
2024enum TextEffect {
2025 Shadow,
2026 Glow,
2027}
2028
2029fn text_effect(
2031 ctx: &mut RenderContext,
2032 run: &deckcraft_text::GlyphRun,
2033 which: TextEffect,
2034 m: Affine,
2035 opacity: f64,
2036) {
2037 let Some(fx) = run.effects else { return };
2038 let k = run.size / run.face.upem.max(1.0);
2039 let skew = if run.fake_italic {
2040 Affine::new([1.0, 0.0, -0.2, 1.0, 0.0, 0.0])
2041 } else {
2042 Affine::IDENTITY
2043 };
2044 let px = scale_of(m);
2045 let (c, sigma, offset, grow) = match (which, fx.shadow, fx.glow) {
2046 (TextEffect::Shadow, Some(s), _) => {
2047 (s.color, s.blur * 0.5 * px, Vec2::new(s.dx, s.dy), 0.0)
2048 }
2049 (TextEffect::Glow, _, Some((gc, r))) => (gc, r * 0.15 * px, Vec2::ZERO, r * 0.68),
2052 _ => return,
2053 };
2054 let alpha = (c.a as f64 / 255.0) * opacity;
2055 if alpha <= 0.0 {
2056 return;
2057 }
2058 ctx.set_transform(Affine::IDENTITY);
2059 ctx.push_layer(None, None, Some(alpha as f32), None, Some(blur(sigma)));
2060 ctx.set_paint(color(c.with_alpha(255), 1.0));
2061 for (gid, x, y) in &run.glyphs {
2062 let path = glyph(&run.face, *gid);
2063 if path.elements().is_empty() {
2064 continue;
2065 }
2066 let at =
2068 pre_translate(m * Affine::translate((*x, *y)), offset * px) * skew * Affine::scale(k);
2069 ctx.set_transform(at);
2070 ctx.fill_path(&path);
2071 if grow > 0.0 {
2072 ctx.set_stroke(kurbo::Stroke::new(grow / k.max(1e-9)).with_join(kurbo::Join::Round));
2073 ctx.stroke_path(&path);
2074 }
2075 }
2076 ctx.pop_layer();
2077}
2078
2079pub(crate) fn draw_glyph(
2081 ctx: &mut RenderContext,
2082 run: &deckcraft_text::GlyphRun,
2083 gid: u32,
2084 at: Affine,
2085 opacity: f64,
2086) {
2087 draw_glyph_with(ctx, run, gid, at, opacity, None);
2088}
2089
2090fn draw_glyph_with(
2093 ctx: &mut RenderContext,
2094 run: &deckcraft_text::GlyphRun,
2095 gid: u32,
2096 at: Affine,
2097 opacity: f64,
2098 fill: Option<(&peniko::Gradient, Affine)>,
2099) {
2100 let alpha = run.alpha * opacity;
2101 if (alpha <= 0.0 || run.color.a == 0) && run.outline.is_none() {
2102 return;
2103 }
2104 let k = run.size / run.face.upem.max(1.0);
2105 if alpha > 0.0
2108 && run.outline.is_none()
2109 && run.face.is_color()
2110 && colr::paint(ctx, &run.face, gid, at, k, run.color, alpha)
2111 {
2112 return;
2113 }
2114 let path = glyph(&run.face, gid);
2115 if path.elements().is_empty() {
2116 return;
2117 }
2118 let skew = if run.fake_italic {
2119 Affine::new([1.0, 0.0, -0.2, 1.0, 0.0, 0.0])
2120 } else {
2121 Affine::IDENTITY
2122 };
2123 ctx.set_transform(at * skew * Affine::scale(k));
2124 if let Some((g, to_text)) = fill
2125 && alpha > 0.0
2126 {
2127 ctx.set_paint(g.clone());
2128 ctx.set_paint_transform((skew * Affine::scale(k)).inverse() * to_text);
2129 ctx.fill_path(&path);
2130 ctx.reset_paint_transform();
2131 } else if alpha > 0.0 && run.color.a > 0 {
2132 ctx.set_paint(color(run.color, alpha));
2133 ctx.fill_path(&path);
2134 if run.fake_bold {
2135 ctx.set_stroke(kurbo::Stroke::new(run.face.upem * 0.025).with_join(kurbo::Join::Round));
2136 ctx.stroke_path(&path);
2137 }
2138 }
2139 if let Some((oc, ow)) = run.outline {
2140 ctx.set_paint(color(oc, opacity));
2141 ctx.set_stroke(kurbo::Stroke::new(ow / k.max(1e-9)).with_join(kurbo::Join::Round));
2142 ctx.stroke_path(&path);
2143 }
2144}
2145
2146pub fn render_shape(pres: &Presentation, slide: &Slide, id: ShapeId, scale: f64) -> Option<Image> {
2148 let rctx = Ctx::for_slide(pres, slide)?;
2149 let s = slide.shape(id)?;
2150 let x = resolve::xfrm(&rctx, s);
2151 let b = x.bounds().inflate(8.0, 8.0);
2152 let w = (b.width() * scale).ceil().clamp(1.0, MAX_SIDE as f64) as u16;
2153 let h = (b.height() * scale).ceil().clamp(1.0, MAX_SIDE as f64) as u16;
2154 let mut ctx = RenderContext::new_with(
2155 w,
2156 h,
2157 vello_cpu::RenderSettings {
2158 num_threads: 0,
2159 ..Default::default()
2160 },
2161 );
2162 let opts = RenderOpts {
2163 scale,
2164 ..Default::default()
2165 };
2166 let f = Frame {
2167 pres,
2168 opts: &opts,
2169 fields: SlideFields::new(1, String::new(), &opts),
2170 };
2171 let view = Affine::scale(scale) * Affine::translate(-b.origin().to_vec2());
2172 RENDERER.with(|r| {
2173 let mut r = r.borrow_mut();
2174 r.shape(&mut ctx, &f, &rctx, s, view, 0);
2175 Some(r.finish(ctx, w, h))
2176 })
2177}
2178
2179#[cfg(test)]
2180mod tests;
2181
2182#[cfg(test)]
2183mod mt_tests {
2184 use super::*;
2185
2186 #[test]
2188 fn effects_render_with_threads_requested() {
2189 let mut s = deckcraft_model::Presentation::default();
2190 let sl = std::sync::Arc::make_mut(&mut s.slides[0]);
2191 let mut sh = deckcraft_model::Shape {
2192 id: ShapeId(900),
2193 xfrm: Some(deckcraft_geom::Xfrm::new(10.0, 10.0, 100.0, 100.0)),
2194 style: Some(deckcraft_model::ShapeStyle::accent(
2195 deckcraft_color::SchemeSlot::Accent1,
2196 )),
2197 ..Default::default()
2198 };
2199 sh.effects = Some(deckcraft_model::Effects {
2200 soft_edge: Some(5.0),
2201 ..Default::default()
2202 });
2203 sl.shapes.push(sh);
2204 assert!(needs_filters(&s, &s.slides[0]));
2205 let img = RENDERER.with(|r| {
2206 r.borrow_mut().slide(
2207 &s,
2208 &s.slides[0],
2209 0,
2210 &RenderOpts {
2211 scale: 0.5,
2212 threads: 4,
2213 ..Default::default()
2214 },
2215 )
2216 });
2217 assert_eq!(img.width, 480);
2218 assert_ne!(img.pixel(30, 30), [255, 255, 255, 255]);
2220 }
2221}