1pub mod error;
2
3pub use error::*;
4
5use std::{collections::HashMap, fmt::Write as _, io::Write, path::Path, sync::Arc};
6
7use graphitepdf_font::{FontDescriptor, FontSource, StandardFont};
8use graphitepdf_image::{Image, ImageSource as AssetImageSource, resolve_image};
9use graphitepdf_kit::{
10 Font as PdfFont, ImageRenderOptions, Metadata as PdfMetadata, PdfWriter, SvgRenderOptions,
11 render_image_to_page_content_with_options, render_svg_node_to_page_content_with_options,
12};
13use graphitepdf_layout::{
14 Document as SourceLayoutDocument, EdgeInsets, LayoutContent,
15 LayoutDocument as LegacyLayoutDocument, LayoutEngine, LayoutMetadata,
16 LayoutNode as LegacyLayoutNode, SafeFont, SafeLayoutDocument, SafeLayoutNode, SafeLayoutPage,
17 SafeLayoutStyle, SafeNodeKind,
18};
19use graphitepdf_primitives::{Bounds, Color, Pt, Size};
20use graphitepdf_svg::SvgNode;
21use graphitepdf_textkit::{TextBlock, TextLayout};
22use graphitepdf_utils::{match_percent, parse_float};
23
24#[derive(Clone, Debug, Default, PartialEq)]
25pub struct RenderDocument {
26 pub metadata: LayoutMetadata,
27 pub pages: Vec<RenderPage>,
28}
29
30#[derive(Clone, Debug, Default, PartialEq)]
31pub struct RenderPage {
32 pub size: Size,
33 pub source_page_index: usize,
34 pub commands: Vec<RenderCommand>,
35}
36
37#[derive(Clone, Debug, PartialEq)]
38pub enum RenderCommand {
39 FillRect(FillRectOp),
40 StrokeBorder(BorderRenderOp),
41 DrawBox(BoxRenderOp),
42 DrawText(TextRenderOp),
43 DrawImage(ImageRenderOp),
44 DrawSvg(SvgRenderOp),
45 PushTransform(TransformRenderOp),
46 PopTransform(RenderContext),
47 DrawDebug(DebugRenderOp),
48 DrawForm(FormRenderOp),
49}
50
51#[derive(Clone, Debug, PartialEq)]
52pub struct FillRectOp {
53 pub context: RenderContext,
54 pub bounds: Bounds,
55 pub color: Color,
56 pub role: PaintRole,
57}
58
59#[derive(Clone, Debug, PartialEq)]
60pub struct BorderRenderOp {
61 pub context: RenderContext,
62 pub bounds: Bounds,
63 pub side: BorderSidePosition,
64 pub border: BorderSide,
65}
66
67#[derive(Clone, Debug, PartialEq)]
68pub struct BoxRenderOp {
69 pub context: RenderContext,
70}
71
72#[derive(Clone, Debug, PartialEq)]
73pub struct TextRenderOp {
74 pub context: RenderContext,
75 pub text: String,
76 pub color: Color,
77 pub font: Option<FontDescriptor>,
78 pub font_source: Option<FontSource>,
79 pub font_size: Pt,
80 pub line_height: Option<Pt>,
81 pub block: Option<TextBlock>,
82 pub layout: Option<TextLayout>,
83}
84
85#[derive(Clone, Debug, PartialEq)]
86pub struct ImageRenderOp {
87 pub context: RenderContext,
88 pub source: RenderImageSource,
89 pub fit: ObjectFit,
90 pub destination: Bounds,
91 pub source_size: Option<Size>,
92}
93
94#[derive(Clone, Debug, PartialEq)]
95pub struct SvgRenderOp {
96 pub context: RenderContext,
97 pub source: SvgRenderSource,
98 pub natural_size: Size,
99 pub view_box: Option<ViewBox>,
100 pub fit: ObjectFit,
101 pub destination: Bounds,
102}
103
104#[derive(Clone, Debug, PartialEq)]
105pub struct TransformRenderOp {
106 pub context: RenderContext,
107 pub operations: Vec<TransformOperation>,
108 pub matrix: AffineTransform,
109}
110
111#[derive(Clone, Debug, PartialEq)]
112pub struct DebugRenderOp {
113 pub context: RenderContext,
114 pub label: String,
115 pub color: Color,
116 pub content_color: Option<Color>,
117}
118
119#[derive(Clone, Debug, PartialEq)]
120pub struct FormRenderOp {
121 pub context: RenderContext,
122 pub name: String,
123 pub bounds: Bounds,
124 pub commands: Vec<RenderCommand>,
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub enum PaintRole {
129 PageBackground,
130 Background,
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134pub enum BorderSidePosition {
135 Top,
136 Right,
137 Bottom,
138 Left,
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub enum BorderStyle {
143 Solid,
144 Dashed,
145 Dotted,
146}
147
148#[derive(Clone, Copy, Debug, PartialEq)]
149pub struct BorderSide {
150 pub width: Pt,
151 pub color: Color,
152 pub style: BorderStyle,
153}
154
155impl BorderSide {
156 pub const fn new(width: Pt, color: Color, style: BorderStyle) -> Self {
157 Self {
158 width,
159 color,
160 style,
161 }
162 }
163}
164
165#[derive(Clone, Copy, Debug, Default, PartialEq)]
166pub struct BorderRadius {
167 pub top_left: Pt,
168 pub top_right: Pt,
169 pub bottom_right: Pt,
170 pub bottom_left: Pt,
171}
172
173#[derive(Clone, Debug, Default, PartialEq)]
174pub struct Border {
175 pub top: Option<BorderSide>,
176 pub right: Option<BorderSide>,
177 pub bottom: Option<BorderSide>,
178 pub left: Option<BorderSide>,
179 pub radius: BorderRadius,
180}
181
182impl Border {
183 pub fn all(side: BorderSide) -> Self {
184 Self {
185 top: Some(side),
186 right: Some(side),
187 bottom: Some(side),
188 left: Some(side),
189 radius: BorderRadius::default(),
190 }
191 }
192}
193
194#[derive(Clone, Debug, PartialEq)]
195pub enum RenderImageSource {
196 Asset(Image),
197 Source(AssetImageSource),
198}
199
200#[derive(Clone, Debug, PartialEq)]
201pub enum SvgRenderSource {
202 Svg(SvgNode),
203 Math { source: String, svg: SvgNode },
204}
205
206#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
207pub enum ObjectFit {
208 Fill,
209 #[default]
210 Contain,
211 Cover,
212 None,
213 ScaleDown,
214}
215
216#[derive(Clone, Copy, Debug, PartialEq)]
217pub struct ObjectFitResult {
218 pub bounds: Bounds,
219 pub scale_x: f32,
220 pub scale_y: f32,
221}
222
223#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
224pub enum RenderNodeKind {
225 #[default]
226 Page,
227 View,
228 Box,
229 Text,
230 ImageAsset,
231 ImageSource,
232 Svg,
233 Math,
234}
235
236impl RenderNodeKind {
237 fn as_str(self) -> &'static str {
238 match self {
239 Self::Page => "page",
240 Self::View => "view",
241 Self::Box => "box",
242 Self::Text => "text",
243 Self::ImageAsset => "image-asset",
244 Self::ImageSource => "image-source",
245 Self::Svg => "svg",
246 Self::Math => "math",
247 }
248 }
249}
250
251#[derive(Clone, Debug, PartialEq)]
252pub struct RenderContext {
253 pub page_index: usize,
254 pub source_page_index: usize,
255 pub path: Vec<usize>,
256 pub node_kind: RenderNodeKind,
257 pub z_index: i32,
258 pub frame: Bounds,
259 pub content_frame: Bounds,
260}
261
262impl RenderContext {
263 pub fn label(&self) -> String {
264 if self.path.is_empty() {
265 format!("{}:{}", self.node_kind.as_str(), self.page_index)
266 } else {
267 let path = self
268 .path
269 .iter()
270 .map(usize::to_string)
271 .collect::<Vec<_>>()
272 .join(".");
273 format!("{}:{}:{}", self.node_kind.as_str(), self.page_index, path)
274 }
275 }
276}
277
278#[derive(Clone, Copy, Debug, PartialEq)]
279pub struct ViewBox {
280 pub x: f32,
281 pub y: f32,
282 pub width: f32,
283 pub height: f32,
284}
285
286#[derive(Clone, Copy, Debug, PartialEq)]
287pub enum TransformOperation {
288 Translate {
289 x: f32,
290 y: f32,
291 },
292 Scale {
293 x: f32,
294 y: f32,
295 },
296 Rotate {
297 degrees: f32,
298 cx: f32,
299 cy: f32,
300 },
301 Skew {
302 x_degrees: f32,
303 y_degrees: f32,
304 },
305 Matrix {
306 a: f32,
307 b: f32,
308 c: f32,
309 d: f32,
310 e: f32,
311 f: f32,
312 },
313}
314
315#[derive(Clone, Copy, Debug, PartialEq)]
316pub struct AffineTransform {
317 pub a: f32,
318 pub b: f32,
319 pub c: f32,
320 pub d: f32,
321 pub e: f32,
322 pub f: f32,
323}
324
325impl AffineTransform {
326 pub const fn identity() -> Self {
327 Self {
328 a: 1.0,
329 b: 0.0,
330 c: 0.0,
331 d: 1.0,
332 e: 0.0,
333 f: 0.0,
334 }
335 }
336
337 pub const fn translate(x: f32, y: f32) -> Self {
338 Self {
339 a: 1.0,
340 b: 0.0,
341 c: 0.0,
342 d: 1.0,
343 e: x,
344 f: y,
345 }
346 }
347
348 pub const fn scale(x: f32, y: f32) -> Self {
349 Self {
350 a: x,
351 b: 0.0,
352 c: 0.0,
353 d: y,
354 e: 0.0,
355 f: 0.0,
356 }
357 }
358
359 pub fn rotate(degrees: f32) -> Self {
360 let radians = degrees.to_radians();
361 let cos = radians.cos();
362 let sin = radians.sin();
363 Self {
364 a: cos,
365 b: sin,
366 c: -sin,
367 d: cos,
368 e: 0.0,
369 f: 0.0,
370 }
371 }
372
373 pub fn skew(x_degrees: f32, y_degrees: f32) -> Self {
374 Self {
375 a: 1.0,
376 b: y_degrees.to_radians().tan(),
377 c: x_degrees.to_radians().tan(),
378 d: 1.0,
379 e: 0.0,
380 f: 0.0,
381 }
382 }
383
384 pub fn multiply(self, other: Self) -> Self {
385 Self {
386 a: self.a * other.a + self.c * other.b,
387 b: self.b * other.a + self.d * other.b,
388 c: self.a * other.c + self.c * other.d,
389 d: self.b * other.c + self.d * other.d,
390 e: self.a * other.e + self.c * other.f + self.e,
391 f: self.b * other.e + self.d * other.f + self.f,
392 }
393 }
394
395 pub fn is_identity(self) -> bool {
396 const EPSILON: f32 = 0.000_1;
397 (self.a - 1.0).abs() < EPSILON
398 && self.b.abs() < EPSILON
399 && self.c.abs() < EPSILON
400 && (self.d - 1.0).abs() < EPSILON
401 && self.e.abs() < EPSILON
402 && self.f.abs() < EPSILON
403 }
404}
405
406impl Default for AffineTransform {
407 fn default() -> Self {
408 Self::identity()
409 }
410}
411
412#[derive(Clone, Debug, PartialEq)]
413pub struct DebugRenderOptions {
414 pub color: Color,
415 pub content_color: Option<Color>,
416 pub label_nodes: bool,
417}
418
419impl Default for DebugRenderOptions {
420 fn default() -> Self {
421 Self {
422 color: Color::rgba(255, 0, 255, 160),
423 content_color: Some(Color::rgba(0, 255, 255, 160)),
424 label_nodes: true,
425 }
426 }
427}
428
429#[derive(Clone, Debug, PartialEq)]
430pub struct RenderEngineOptions {
431 pub image_fit: ObjectFit,
432 pub svg_fit: ObjectFit,
433 pub wrap_views_in_forms: bool,
434 pub debug: Option<DebugRenderOptions>,
435}
436
437impl Default for RenderEngineOptions {
438 fn default() -> Self {
439 Self {
440 image_fit: ObjectFit::Contain,
441 svg_fit: ObjectFit::Contain,
442 wrap_views_in_forms: false,
443 debug: None,
444 }
445 }
446}
447
448#[derive(Clone, Debug, Default, PartialEq)]
449pub struct RenderEngine {
450 options: RenderEngineOptions,
451}
452
453impl RenderEngine {
454 pub fn new() -> Self {
455 Self::default()
456 }
457
458 pub fn with_options(mut self, options: RenderEngineOptions) -> Self {
459 self.options = options;
460 self
461 }
462
463 pub fn options(&self) -> &RenderEngineOptions {
464 &self.options
465 }
466
467 pub fn build<T>(&self, source: &T) -> Result<RenderDocument>
468 where
469 T: RenderSource + ?Sized,
470 {
471 source.build_render_document(self)
472 }
473
474 fn build_safe_document(&self, layout: &SafeLayoutDocument) -> Result<RenderDocument> {
475 let pages = layout
476 .pages
477 .iter()
478 .enumerate()
479 .map(|(page_index, page)| self.render_safe_page(page_index, page))
480 .collect::<Result<Vec<_>>>()?;
481
482 Ok(RenderDocument {
483 metadata: layout.metadata.clone(),
484 pages,
485 })
486 }
487
488 fn build_legacy_document(&self, layout: &LegacyLayoutDocument) -> Result<RenderDocument> {
489 let pages = layout
490 .pages
491 .iter()
492 .enumerate()
493 .map(|(page_index, page)| {
494 let commands = page
495 .nodes
496 .iter()
497 .enumerate()
498 .map(|(node_index, node)| self.render_legacy_node(page_index, node_index, node))
499 .collect::<Vec<_>>();
500
501 Ok(RenderPage {
502 size: page.size,
503 source_page_index: page_index,
504 commands: commands.into_iter().flatten().collect(),
505 })
506 })
507 .collect::<Result<Vec<_>>>()?;
508
509 Ok(RenderDocument {
510 metadata: LayoutMetadata::default(),
511 pages,
512 })
513 }
514
515 fn render_safe_page(&self, page_index: usize, page: &SafeLayoutPage) -> Result<RenderPage> {
516 let mut commands = Vec::new();
517 let context = RenderContext {
518 page_index,
519 source_page_index: page.source_page_index,
520 path: Vec::new(),
521 node_kind: RenderNodeKind::Page,
522 z_index: page.style.z_index,
523 frame: Bounds::from_origin_size(0.0, 0.0, page.size.width, page.size.height),
524 content_frame: inset_bounds(
525 Bounds::from_origin_size(0.0, 0.0, page.size.width, page.size.height),
526 page.style.padding,
527 ),
528 };
529
530 commands.extend(background_commands(
531 &context,
532 page.style.background_color,
533 PaintRole::PageBackground,
534 ));
535
536 if let Some(debug) = &self.options.debug {
537 commands.push(RenderCommand::DrawDebug(DebugRenderOp {
538 context: context.clone(),
539 label: maybe_label(&context, debug.label_nodes),
540 color: debug.color,
541 content_color: debug.content_color,
542 }));
543 }
544
545 for (node_index, node) in page.nodes.iter().enumerate() {
546 commands.extend(self.render_safe_node(
547 page_index,
548 page.source_page_index,
549 vec![node_index],
550 node,
551 )?);
552 }
553
554 Ok(RenderPage {
555 size: page.size,
556 source_page_index: page.source_page_index,
557 commands,
558 })
559 }
560
561 fn render_safe_node(
562 &self,
563 page_index: usize,
564 source_page_index: usize,
565 path: Vec<usize>,
566 node: &SafeLayoutNode,
567 ) -> Result<Vec<RenderCommand>> {
568 let context = RenderContext {
569 page_index,
570 source_page_index,
571 path: path.clone(),
572 node_kind: safe_node_kind(node),
573 z_index: node.z_index(),
574 frame: node.frame,
575 content_frame: node.content_frame,
576 };
577
578 if self.options.wrap_views_in_forms && matches!(node.kind, SafeNodeKind::View) {
579 let commands = self.render_safe_node_commands(&context, node, path)?;
580 return Ok(vec![RenderCommand::DrawForm(FormRenderOp {
581 name: format!("form-{}", context.label().replace(':', "-")),
582 bounds: context.frame,
583 context,
584 commands,
585 })]);
586 }
587
588 self.render_safe_node_commands(&context, node, path)
589 }
590
591 fn render_safe_node_commands(
592 &self,
593 context: &RenderContext,
594 node: &SafeLayoutNode,
595 path: Vec<usize>,
596 ) -> Result<Vec<RenderCommand>> {
597 let mut commands =
598 background_commands(context, node.style.background_color, PaintRole::Background);
599
600 match &node.kind {
601 SafeNodeKind::View => {}
602 SafeNodeKind::Box => {
603 commands.push(RenderCommand::DrawBox(BoxRenderOp {
604 context: context.clone(),
605 }));
606 }
607 SafeNodeKind::Text { block, layout } => {
608 commands.push(RenderCommand::DrawText(TextRenderOp {
609 context: context.clone(),
610 text: block.plain_text(),
611 color: node.style.color,
612 font: Some(node.style.font.descriptor.clone()),
613 font_source: node.style.font.source.clone(),
614 font_size: node.style.font_size,
615 line_height: Some(node.style.line_height),
616 block: Some(block.clone()),
617 layout: Some(layout.clone()),
618 }));
619 }
620 SafeNodeKind::ImageAsset { asset } => {
621 let fit = self.options.image_fit;
622 let destination = fit_object(
623 Size::new(asset.width(), asset.height()),
624 node.content_frame,
625 fit,
626 )
627 .bounds;
628 commands.push(RenderCommand::DrawImage(ImageRenderOp {
629 context: context.clone(),
630 source: RenderImageSource::Asset(asset.clone()),
631 fit,
632 destination,
633 source_size: Some(Size::new(asset.width(), asset.height())),
634 }));
635 }
636 SafeNodeKind::ImageSource { source } => {
637 commands.push(RenderCommand::DrawImage(ImageRenderOp {
638 context: context.clone(),
639 source: RenderImageSource::Source(source.clone()),
640 fit: self.options.image_fit,
641 destination: node.content_frame,
642 source_size: None,
643 }));
644 }
645 SafeNodeKind::Svg { svg } => {
646 commands.extend(self.render_svg_like_node(
647 context,
648 SvgRenderSource::Svg(svg.clone()),
649 svg,
650 )?);
651 }
652 SafeNodeKind::Math { source, svg } => {
653 commands.extend(self.render_svg_like_node(
654 context,
655 SvgRenderSource::Math {
656 source: source.clone(),
657 svg: svg.clone(),
658 },
659 svg,
660 )?);
661 }
662 }
663
664 for (child_index, child) in node.children.iter().enumerate() {
665 let mut child_path = path.clone();
666 child_path.push(child_index);
667 commands.extend(self.render_safe_node(
668 context.page_index,
669 context.source_page_index,
670 child_path,
671 child,
672 )?);
673 }
674
675 if let Some(debug) = &self.options.debug {
676 commands.push(RenderCommand::DrawDebug(DebugRenderOp {
677 context: context.clone(),
678 label: maybe_label(context, debug.label_nodes),
679 color: debug.color,
680 content_color: debug.content_color,
681 }));
682 }
683
684 Ok(commands)
685 }
686
687 fn render_svg_like_node(
688 &self,
689 context: &RenderContext,
690 source: SvgRenderSource,
691 svg: &SvgNode,
692 ) -> Result<Vec<RenderCommand>> {
693 let natural_size = resolve_svg_size(svg)?;
694 let view_box = svg
695 .props
696 .get("viewBox")
697 .and_then(|value| parse_view_box(value));
698 let fit = self.options.svg_fit;
699 let fitted = fit_object(natural_size, context.content_frame, fit);
700 let mut commands = Vec::new();
701 let transform = svg_fit_transform(context, natural_size, fitted, view_box);
702
703 if !transform.matrix.is_identity() {
704 commands.push(RenderCommand::PushTransform(TransformRenderOp {
705 context: context.clone(),
706 operations: transform.operations,
707 matrix: transform.matrix,
708 }));
709 }
710
711 commands.push(RenderCommand::DrawSvg(SvgRenderOp {
712 context: context.clone(),
713 source,
714 natural_size,
715 view_box,
716 fit,
717 destination: fitted.bounds,
718 }));
719
720 if !transform.matrix.is_identity() {
721 commands.push(RenderCommand::PopTransform(context.clone()));
722 }
723
724 Ok(commands)
725 }
726
727 fn render_legacy_node(
728 &self,
729 page_index: usize,
730 node_index: usize,
731 node: &LegacyLayoutNode,
732 ) -> Vec<RenderCommand> {
733 let kind = match &node.content {
734 LayoutContent::Text(_) => RenderNodeKind::Text,
735 LayoutContent::Box => RenderNodeKind::Box,
736 };
737 let context = RenderContext {
738 page_index,
739 source_page_index: page_index,
740 path: vec![node_index],
741 node_kind: kind,
742 z_index: 0,
743 frame: node.frame,
744 content_frame: node.frame,
745 };
746
747 match &node.content {
748 LayoutContent::Text(block) => {
749 let first_span = block.spans().first();
750 vec![RenderCommand::DrawText(TextRenderOp {
751 context,
752 text: block.plain_text(),
753 color: Color::BLACK,
754 font: node.font_descriptor().cloned(),
755 font_source: None,
756 font_size: first_span
757 .map(|span| span.font_size())
758 .unwrap_or(Pt::new(12.0)),
759 line_height: None,
760 block: Some(block.clone()),
761 layout: None,
762 })]
763 }
764 LayoutContent::Box => vec![RenderCommand::DrawBox(BoxRenderOp { context })],
765 }
766 }
767}
768
769pub trait RenderSource {
770 fn build_render_document(&self, engine: &RenderEngine) -> Result<RenderDocument>;
771}
772
773impl RenderSource for SafeLayoutDocument {
774 fn build_render_document(&self, engine: &RenderEngine) -> Result<RenderDocument> {
775 engine.build_safe_document(self)
776 }
777}
778
779impl RenderSource for LegacyLayoutDocument {
780 fn build_render_document(&self, engine: &RenderEngine) -> Result<RenderDocument> {
781 engine.build_legacy_document(self)
782 }
783}
784
785pub fn background_commands(
786 context: &RenderContext,
787 color: Option<Color>,
788 role: PaintRole,
789) -> Vec<RenderCommand> {
790 color
791 .map(|color| {
792 vec![RenderCommand::FillRect(FillRectOp {
793 context: context.clone(),
794 bounds: context.frame,
795 color,
796 role,
797 })]
798 })
799 .unwrap_or_default()
800}
801
802pub fn border_commands(context: &RenderContext, border: &Border) -> Vec<RenderCommand> {
803 let mut commands = Vec::new();
804
805 for (side, value) in [
806 (BorderSidePosition::Top, border.top),
807 (BorderSidePosition::Right, border.right),
808 (BorderSidePosition::Bottom, border.bottom),
809 (BorderSidePosition::Left, border.left),
810 ] {
811 if let Some(border) = value
812 && border.width.value() > 0.0
813 {
814 commands.push(RenderCommand::StrokeBorder(BorderRenderOp {
815 context: context.clone(),
816 bounds: context.frame,
817 side,
818 border,
819 }));
820 }
821 }
822
823 commands
824}
825
826pub fn fit_object(source: Size, container: Bounds, fit: ObjectFit) -> ObjectFitResult {
827 if container.size.width <= 0.0 || container.size.height <= 0.0 {
828 return ObjectFitResult {
829 bounds: Bounds::from_origin_size(container.origin.x, container.origin.y, 0.0, 0.0),
830 scale_x: 0.0,
831 scale_y: 0.0,
832 };
833 }
834
835 let valid_source = source.width > 0.0 && source.height > 0.0;
836 if !valid_source {
837 return ObjectFitResult {
838 bounds: container,
839 scale_x: 1.0,
840 scale_y: 1.0,
841 };
842 }
843
844 let source_width = source.width.abs();
845 let source_height = source.height.abs();
846 let x_scale = container.size.width / source_width;
847 let y_scale = container.size.height / source_height;
848
849 let (scale_x, scale_y) = match fit {
850 ObjectFit::Fill => (x_scale, y_scale),
851 ObjectFit::Contain => {
852 let scale = x_scale.min(y_scale);
853 (scale, scale)
854 }
855 ObjectFit::Cover => {
856 let scale = x_scale.max(y_scale);
857 (scale, scale)
858 }
859 ObjectFit::None => (1.0, 1.0),
860 ObjectFit::ScaleDown => {
861 if source_width <= container.size.width && source_height <= container.size.height {
862 (1.0, 1.0)
863 } else {
864 let scale = x_scale.min(y_scale);
865 (scale, scale)
866 }
867 }
868 };
869
870 let fitted_width = source_width * scale_x;
871 let fitted_height = source_height * scale_y;
872 let x = container.origin.x + ((container.size.width - fitted_width) * 0.5);
873 let y = container.origin.y + ((container.size.height - fitted_height) * 0.5);
874
875 ObjectFitResult {
876 bounds: Bounds::from_origin_size(x, y, fitted_width, fitted_height),
877 scale_x,
878 scale_y,
879 }
880}
881
882pub fn parse_color(input: &str) -> Result<Color> {
883 let trimmed = input.trim();
884 let lower = trimmed.to_ascii_lowercase();
885
886 if let Some(color) = named_color(lower.as_str()) {
887 return Ok(color);
888 }
889
890 if let Some(color) = parse_hex_color(trimmed) {
891 return Ok(color);
892 }
893
894 if lower.starts_with("rgb(") && lower.ends_with(')') {
895 let inner = &trimmed[4..trimmed.len() - 1];
896 let parts = split_csv(inner);
897 if parts.len() == 3 {
898 return Ok(Color::rgb(
899 parse_rgb_channel(parts[0])?,
900 parse_rgb_channel(parts[1])?,
901 parse_rgb_channel(parts[2])?,
902 ));
903 }
904 }
905
906 if lower.starts_with("rgba(") && lower.ends_with(')') {
907 let inner = &trimmed[5..trimmed.len() - 1];
908 let parts = split_csv(inner);
909 if parts.len() == 4 {
910 return Ok(Color::rgba(
911 parse_rgb_channel(parts[0])?,
912 parse_rgb_channel(parts[1])?,
913 parse_rgb_channel(parts[2])?,
914 parse_alpha_channel(parts[3])?,
915 ));
916 }
917 }
918
919 Err(Error::InvalidColor {
920 input: input.to_string(),
921 })
922}
923
924pub fn parse_transform(input: &str) -> Result<Vec<TransformOperation>> {
925 let mut operations = Vec::new();
926 let mut remainder = input.trim();
927
928 while !remainder.is_empty() {
929 let Some(start) = remainder.find('(') else {
930 return Err(Error::InvalidTransform {
931 input: input.to_string(),
932 });
933 };
934 let name = remainder[..start].trim();
935 let after_start = &remainder[start + 1..];
936 let Some(end) = after_start.find(')') else {
937 return Err(Error::InvalidTransform {
938 input: input.to_string(),
939 });
940 };
941 let values = parse_transform_values(&after_start[..end]);
942 operations.push(normalize_transform(name, &values, input)?);
943 remainder = after_start[end + 1..].trim_start();
944 }
945
946 if operations.is_empty() {
947 Err(Error::InvalidTransform {
948 input: input.to_string(),
949 })
950 } else {
951 Ok(operations)
952 }
953}
954
955pub fn compose_transform(operations: &[TransformOperation]) -> AffineTransform {
956 operations
957 .iter()
958 .fold(AffineTransform::identity(), |matrix, op| {
959 matrix.multiply(transform_matrix(*op))
960 })
961}
962
963pub fn parse_view_box(input: &str) -> Option<ViewBox> {
964 let values = input
965 .split(|character: char| character.is_ascii_whitespace() || character == ',')
966 .filter(|part| !part.is_empty())
967 .map(|part| part.parse::<f32>().ok())
968 .collect::<Option<Vec<_>>>()?;
969
970 match values.as_slice() {
971 [x, y, width, height] if *width > 0.0 && *height > 0.0 => Some(ViewBox {
972 x: *x,
973 y: *y,
974 width: *width,
975 height: *height,
976 }),
977 _ => None,
978 }
979}
980
981pub fn resolve_svg_size(svg: &SvgNode) -> Result<Size> {
982 let view_box = svg
983 .props
984 .get("viewBox")
985 .and_then(|value| parse_view_box(value));
986 let width = svg
987 .props
988 .get("width")
989 .and_then(|value| parse_dimension(value).ok())
990 .or_else(|| view_box.map(|view_box| view_box.width))
991 .unwrap_or(0.0);
992 let height = svg
993 .props
994 .get("height")
995 .and_then(|value| parse_dimension(value).ok())
996 .or_else(|| view_box.map(|view_box| view_box.height))
997 .unwrap_or(0.0);
998
999 if width <= 0.0 || height <= 0.0 {
1000 Err(Error::InvalidSvgDimensions)
1001 } else {
1002 Ok(Size::new(width, height))
1003 }
1004}
1005
1006pub fn parse_dimension(input: &str) -> Result<f32> {
1007 let trimmed = input.trim();
1008 let number = parse_float(trimmed).ok_or_else(|| Error::InvalidDimension {
1009 input: input.to_string(),
1010 })?;
1011 let suffix = trimmed
1012 .trim_start_matches(|character: char| {
1013 character.is_ascii_digit() || matches!(character, '.' | '+' | '-')
1014 })
1015 .trim()
1016 .to_ascii_lowercase();
1017
1018 let scaled = match suffix.as_str() {
1019 "" | "px" | "pt" => number,
1020 "in" => number * 72.0,
1021 "cm" => number * 72.0 / 2.54,
1022 "mm" => number * 72.0 / 25.4,
1023 _ => number,
1024 };
1025
1026 Ok(scaled.abs())
1027}
1028
1029fn inset_bounds(bounds: Bounds, insets: EdgeInsets) -> Bounds {
1030 Bounds::from_origin_size(
1031 bounds.origin.x + insets.left.value(),
1032 bounds.origin.y + insets.top.value(),
1033 (bounds.size.width - insets.left.value() - insets.right.value()).max(0.0),
1034 (bounds.size.height - insets.top.value() - insets.bottom.value()).max(0.0),
1035 )
1036}
1037
1038fn safe_node_kind(node: &SafeLayoutNode) -> RenderNodeKind {
1039 match node.kind {
1040 SafeNodeKind::View => RenderNodeKind::View,
1041 SafeNodeKind::Box => RenderNodeKind::Box,
1042 SafeNodeKind::Text { .. } => RenderNodeKind::Text,
1043 SafeNodeKind::ImageAsset { .. } => RenderNodeKind::ImageAsset,
1044 SafeNodeKind::ImageSource { .. } => RenderNodeKind::ImageSource,
1045 SafeNodeKind::Svg { .. } => RenderNodeKind::Svg,
1046 SafeNodeKind::Math { .. } => RenderNodeKind::Math,
1047 }
1048}
1049
1050fn maybe_label(context: &RenderContext, enabled: bool) -> String {
1051 if enabled {
1052 context.label()
1053 } else {
1054 String::new()
1055 }
1056}
1057
1058fn named_color(name: &str) -> Option<Color> {
1059 match name {
1060 "black" => Some(Color::BLACK),
1061 "white" => Some(Color::WHITE),
1062 "red" => Some(Color::rgb(255, 0, 0)),
1063 "green" => Some(Color::rgb(0, 128, 0)),
1064 "blue" => Some(Color::rgb(0, 0, 255)),
1065 "transparent" => Some(Color::rgba(0, 0, 0, 0)),
1066 _ => None,
1067 }
1068}
1069
1070fn parse_hex_color(input: &str) -> Option<Color> {
1071 let hex = input.strip_prefix('#')?;
1072 match hex.len() {
1073 3 => Some(Color::rgb(
1074 expand_hex_digit(hex.as_bytes()[0])?,
1075 expand_hex_digit(hex.as_bytes()[1])?,
1076 expand_hex_digit(hex.as_bytes()[2])?,
1077 )),
1078 4 => Some(Color::rgba(
1079 expand_hex_digit(hex.as_bytes()[0])?,
1080 expand_hex_digit(hex.as_bytes()[1])?,
1081 expand_hex_digit(hex.as_bytes()[2])?,
1082 expand_hex_digit(hex.as_bytes()[3])?,
1083 )),
1084 6 => Some(Color::rgb(
1085 u8::from_str_radix(&hex[0..2], 16).ok()?,
1086 u8::from_str_radix(&hex[2..4], 16).ok()?,
1087 u8::from_str_radix(&hex[4..6], 16).ok()?,
1088 )),
1089 8 => Some(Color::rgba(
1090 u8::from_str_radix(&hex[0..2], 16).ok()?,
1091 u8::from_str_radix(&hex[2..4], 16).ok()?,
1092 u8::from_str_radix(&hex[4..6], 16).ok()?,
1093 u8::from_str_radix(&hex[6..8], 16).ok()?,
1094 )),
1095 _ => None,
1096 }
1097}
1098
1099fn expand_hex_digit(value: u8) -> Option<u8> {
1100 let digit = char::from(value).to_digit(16)? as u8;
1101 Some((digit << 4) | digit)
1102}
1103
1104fn split_csv(input: &str) -> Vec<&str> {
1105 input
1106 .split(',')
1107 .map(str::trim)
1108 .filter(|value| !value.is_empty())
1109 .collect()
1110}
1111
1112fn parse_rgb_channel(input: &str) -> Result<u8> {
1113 if let Some(percent) = match_percent(input) {
1114 return Ok(clamp_u8(percent.percent * 255.0));
1115 }
1116
1117 let value = parse_float(input).ok_or_else(|| Error::InvalidColor {
1118 input: input.to_string(),
1119 })?;
1120 Ok(clamp_u8(value))
1121}
1122
1123fn parse_alpha_channel(input: &str) -> Result<u8> {
1124 if let Some(percent) = match_percent(input) {
1125 return Ok(clamp_u8(percent.percent * 255.0));
1126 }
1127
1128 let value = parse_float(input).ok_or_else(|| Error::InvalidColor {
1129 input: input.to_string(),
1130 })?;
1131 let alpha = if value <= 1.0 { value * 255.0 } else { value };
1132 Ok(clamp_u8(alpha))
1133}
1134
1135fn clamp_u8(value: f32) -> u8 {
1136 value.round().clamp(0.0, 255.0) as u8
1137}
1138
1139fn parse_transform_values(input: &str) -> Vec<&str> {
1140 if input.contains(',') {
1141 input
1142 .split(',')
1143 .map(str::trim)
1144 .filter(|value| !value.is_empty())
1145 .collect()
1146 } else {
1147 input.split_whitespace().collect()
1148 }
1149}
1150
1151fn normalize_transform(name: &str, values: &[&str], original: &str) -> Result<TransformOperation> {
1152 match name {
1153 "translate" => Ok(TransformOperation::Translate {
1154 x: parse_required_f32(values.first().copied(), original)?,
1155 y: parse_optional_f32(values.get(1).copied()).unwrap_or(0.0),
1156 }),
1157 "translateX" => Ok(TransformOperation::Translate {
1158 x: parse_required_f32(values.first().copied(), original)?,
1159 y: 0.0,
1160 }),
1161 "translateY" => Ok(TransformOperation::Translate {
1162 x: 0.0,
1163 y: parse_required_f32(values.first().copied(), original)?,
1164 }),
1165 "scale" => {
1166 let x = parse_required_f32(values.first().copied(), original)?;
1167 Ok(TransformOperation::Scale {
1168 x,
1169 y: parse_optional_f32(values.get(1).copied()).unwrap_or(x),
1170 })
1171 }
1172 "scaleX" => Ok(TransformOperation::Scale {
1173 x: parse_required_f32(values.first().copied(), original)?,
1174 y: 1.0,
1175 }),
1176 "scaleY" => Ok(TransformOperation::Scale {
1177 x: 1.0,
1178 y: parse_required_f32(values.first().copied(), original)?,
1179 }),
1180 "rotate" => Ok(TransformOperation::Rotate {
1181 degrees: parse_angle(values.first().copied(), original)?,
1182 cx: parse_optional_f32(values.get(1).copied()).unwrap_or(0.0),
1183 cy: parse_optional_f32(values.get(2).copied()).unwrap_or(0.0),
1184 }),
1185 "skew" => Ok(TransformOperation::Skew {
1186 x_degrees: parse_angle(values.first().copied(), original)?,
1187 y_degrees: values
1188 .get(1)
1189 .copied()
1190 .map(|value| parse_angle(Some(value), original))
1191 .transpose()?
1192 .unwrap_or(0.0),
1193 }),
1194 "skewX" => Ok(TransformOperation::Skew {
1195 x_degrees: parse_angle(values.first().copied(), original)?,
1196 y_degrees: 0.0,
1197 }),
1198 "skewY" => Ok(TransformOperation::Skew {
1199 x_degrees: 0.0,
1200 y_degrees: parse_angle(values.first().copied(), original)?,
1201 }),
1202 "matrix" if values.len() == 6 => Ok(TransformOperation::Matrix {
1203 a: parse_required_f32(values.first().copied(), original)?,
1204 b: parse_required_f32(values.get(1).copied(), original)?,
1205 c: parse_required_f32(values.get(2).copied(), original)?,
1206 d: parse_required_f32(values.get(3).copied(), original)?,
1207 e: parse_required_f32(values.get(4).copied(), original)?,
1208 f: parse_required_f32(values.get(5).copied(), original)?,
1209 }),
1210 _ => Err(Error::InvalidTransform {
1211 input: original.to_string(),
1212 }),
1213 }
1214}
1215
1216fn parse_required_f32(value: Option<&str>, original: &str) -> Result<f32> {
1217 parse_optional_f32(value).ok_or_else(|| Error::InvalidTransform {
1218 input: original.to_string(),
1219 })
1220}
1221
1222fn parse_optional_f32(value: Option<&str>) -> Option<f32> {
1223 parse_float(value?.trim())
1224}
1225
1226fn parse_angle(value: Option<&str>, original: &str) -> Result<f32> {
1227 let value = value.ok_or_else(|| Error::InvalidTransform {
1228 input: original.to_string(),
1229 })?;
1230 let trimmed = value.trim();
1231 if let Some(raw) = trimmed.strip_suffix("rad") {
1232 let radians = parse_float(raw.trim()).ok_or_else(|| Error::InvalidTransform {
1233 input: original.to_string(),
1234 })?;
1235 Ok(radians.to_degrees())
1236 } else {
1237 parse_float(trimmed.trim_end_matches("deg")).ok_or_else(|| Error::InvalidTransform {
1238 input: original.to_string(),
1239 })
1240 }
1241}
1242
1243fn transform_matrix(operation: TransformOperation) -> AffineTransform {
1244 match operation {
1245 TransformOperation::Translate { x, y } => AffineTransform::translate(x, y),
1246 TransformOperation::Scale { x, y } => AffineTransform::scale(x, y),
1247 TransformOperation::Rotate { degrees, cx, cy } => AffineTransform::translate(cx, cy)
1248 .multiply(AffineTransform::rotate(degrees))
1249 .multiply(AffineTransform::translate(-cx, -cy)),
1250 TransformOperation::Skew {
1251 x_degrees,
1252 y_degrees,
1253 } => AffineTransform::skew(x_degrees, y_degrees),
1254 TransformOperation::Matrix { a, b, c, d, e, f } => AffineTransform { a, b, c, d, e, f },
1255 }
1256}
1257
1258struct SvgFitTransform {
1259 operations: Vec<TransformOperation>,
1260 matrix: AffineTransform,
1261}
1262
1263fn svg_fit_transform(
1264 context: &RenderContext,
1265 natural_size: Size,
1266 fitted: ObjectFitResult,
1267 view_box: Option<ViewBox>,
1268) -> SvgFitTransform {
1269 let scale_x = if natural_size.width > 0.0 {
1270 fitted.bounds.size.width / natural_size.width
1271 } else {
1272 1.0
1273 };
1274 let scale_y = if natural_size.height > 0.0 {
1275 fitted.bounds.size.height / natural_size.height
1276 } else {
1277 1.0
1278 };
1279
1280 let mut operations = vec![TransformOperation::Translate {
1281 x: fitted.bounds.origin.x,
1282 y: fitted.bounds.origin.y,
1283 }];
1284 if let Some(view_box) = view_box {
1285 operations.push(TransformOperation::Translate {
1286 x: -view_box.x,
1287 y: -view_box.y,
1288 });
1289 }
1290 operations.push(TransformOperation::Scale {
1291 x: scale_x,
1292 y: scale_y,
1293 });
1294
1295 let mut matrix = compose_transform(&operations);
1296 if context.content_frame.origin == fitted.bounds.origin
1297 && context.content_frame.size == fitted.bounds.size
1298 && scale_x == 1.0
1299 && scale_y == 1.0
1300 && view_box.is_none()
1301 {
1302 matrix = AffineTransform::identity();
1303 }
1304
1305 SvgFitTransform { operations, matrix }
1306}
1307
1308fn default_safe_font() -> SafeFont {
1309 SafeFont {
1310 descriptor: FontDescriptor::new(StandardFont::Helvetica.family_name()),
1311 source: Some(FontSource::standard(StandardFont::Helvetica)),
1312 }
1313}
1314
1315#[allow(dead_code)]
1316fn default_safe_style() -> SafeLayoutStyle {
1317 SafeLayoutStyle {
1318 width: None,
1319 height: None,
1320 margin: EdgeInsets::default(),
1321 padding: EdgeInsets::default(),
1322 background_color: None,
1323 color: Color::BLACK,
1324 font: default_safe_font(),
1325 font_size: Pt::new(12.0),
1326 line_height: Pt::new(14.4),
1327 z_index: 0,
1328 page_break_before: false,
1329 page_break_after: false,
1330 }
1331}
1332
1333pub trait RenderBackend {
1334 type Output;
1335
1336 fn render_document(&mut self, document: &RenderDocument) -> Result<Self::Output>;
1337}
1338
1339#[derive(Debug, Default)]
1340pub struct NoopRenderBackend;
1341
1342impl RenderBackend for NoopRenderBackend {
1343 type Output = ();
1344
1345 fn render_document(&mut self, _document: &RenderDocument) -> Result<Self::Output> {
1346 Ok(())
1347 }
1348}
1349
1350#[derive(Debug)]
1351pub struct Renderer<B: RenderBackend> {
1352 backend: B,
1353}
1354
1355impl<B: RenderBackend> Renderer<B> {
1356 pub fn new(backend: B) -> Self {
1357 Self { backend }
1358 }
1359
1360 pub fn backend(&self) -> &B {
1361 &self.backend
1362 }
1363
1364 pub fn backend_mut(&mut self) -> &mut B {
1365 &mut self.backend
1366 }
1367
1368 pub fn render(&mut self, document: &RenderDocument) -> Result<B::Output> {
1369 self.backend.render_document(document)
1370 }
1371}
1372
1373pub trait RendererDocumentSource {
1374 fn build_render_document(
1375 &self,
1376 layout_engine: &LayoutEngine,
1377 render_engine: &RenderEngine,
1378 ) -> Result<RenderDocument>;
1379}
1380
1381impl RendererDocumentSource for SourceLayoutDocument {
1382 fn build_render_document(
1383 &self,
1384 layout_engine: &LayoutEngine,
1385 render_engine: &RenderEngine,
1386 ) -> Result<RenderDocument> {
1387 let layout = layout_engine.layout_document(self)?;
1388 render_engine.build(&layout)
1389 }
1390}
1391
1392impl RendererDocumentSource for SafeLayoutDocument {
1393 fn build_render_document(
1394 &self,
1395 _layout_engine: &LayoutEngine,
1396 render_engine: &RenderEngine,
1397 ) -> Result<RenderDocument> {
1398 render_engine.build(self)
1399 }
1400}
1401
1402impl RendererDocumentSource for LegacyLayoutDocument {
1403 fn build_render_document(
1404 &self,
1405 _layout_engine: &LayoutEngine,
1406 render_engine: &RenderEngine,
1407 ) -> Result<RenderDocument> {
1408 render_engine.build(self)
1409 }
1410}
1411
1412impl RendererDocumentSource for RenderDocument {
1413 fn build_render_document(
1414 &self,
1415 _layout_engine: &LayoutEngine,
1416 _render_engine: &RenderEngine,
1417 ) -> Result<RenderDocument> {
1418 Ok(self.clone())
1419 }
1420}
1421
1422#[derive(Clone, Debug)]
1423pub struct DocumentContainer<T> {
1424 document: T,
1425 revision: u64,
1426}
1427
1428impl<T> DocumentContainer<T> {
1429 pub fn new(document: T) -> Self {
1430 Self {
1431 document,
1432 revision: 0,
1433 }
1434 }
1435
1436 pub fn revision(&self) -> u64 {
1437 self.revision
1438 }
1439
1440 pub fn document(&self) -> &T {
1441 &self.document
1442 }
1443
1444 pub fn into_inner(self) -> T {
1445 self.document
1446 }
1447
1448 pub fn replace(&mut self, document: T) -> u64 {
1449 self.document = document;
1450 self.bump_revision()
1451 }
1452
1453 pub fn update(&mut self, update: impl FnOnce(&mut T)) -> u64 {
1454 update(&mut self.document);
1455 self.bump_revision()
1456 }
1457
1458 fn bump_revision(&mut self) -> u64 {
1459 self.revision = self.revision.saturating_add(1);
1460 self.revision
1461 }
1462}
1463
1464#[derive(Clone, Debug, PartialEq)]
1465pub struct RenderSnapshot {
1466 revision: u64,
1467 document: RenderDocument,
1468}
1469
1470impl RenderSnapshot {
1471 pub fn revision(&self) -> u64 {
1472 self.revision
1473 }
1474
1475 pub fn document(&self) -> &RenderDocument {
1476 &self.document
1477 }
1478
1479 pub fn into_document(self) -> RenderDocument {
1480 self.document
1481 }
1482}
1483
1484pub struct RendererSession<T> {
1485 container: DocumentContainer<T>,
1486 layout_engine: LayoutEngine,
1487 render_engine: RenderEngine,
1488 rendered: Option<RenderSnapshot>,
1489}
1490
1491impl<T> RendererSession<T> {
1492 pub fn new(document: T) -> Self {
1493 Self {
1494 container: DocumentContainer::new(document),
1495 layout_engine: LayoutEngine::new(),
1496 render_engine: RenderEngine::new(),
1497 rendered: None,
1498 }
1499 }
1500
1501 pub fn with_layout_engine(mut self, layout_engine: LayoutEngine) -> Self {
1502 self.layout_engine = layout_engine;
1503 self
1504 }
1505
1506 pub fn with_render_engine(mut self, render_engine: RenderEngine) -> Self {
1507 self.render_engine = render_engine;
1508 self
1509 }
1510
1511 pub fn revision(&self) -> u64 {
1512 self.container.revision()
1513 }
1514
1515 pub fn document(&self) -> &T {
1516 self.container.document()
1517 }
1518
1519 pub fn rendered(&self) -> Option<&RenderSnapshot> {
1520 self.rendered.as_ref()
1521 }
1522
1523 pub fn replace_document(&mut self, document: T) -> u64 {
1524 self.rendered = None;
1525 self.container.replace(document)
1526 }
1527
1528 pub fn update_document(&mut self, update: impl FnOnce(&mut T)) -> u64 {
1529 self.rendered = None;
1530 self.container.update(update)
1531 }
1532}
1533
1534impl<T> RendererSession<T>
1535where
1536 T: RendererDocumentSource,
1537{
1538 pub fn render_snapshot(&mut self) -> Result<&RenderSnapshot> {
1539 let revision = self.revision();
1540 let should_render = self
1541 .rendered
1542 .as_ref()
1543 .map(|snapshot| snapshot.revision() != revision)
1544 .unwrap_or(true);
1545
1546 if should_render {
1547 let document = self
1548 .container
1549 .document()
1550 .build_render_document(&self.layout_engine, &self.render_engine)?;
1551 self.rendered = Some(RenderSnapshot { revision, document });
1552 }
1553
1554 Ok(self
1555 .rendered
1556 .as_ref()
1557 .expect("renderer session should populate a snapshot before returning"))
1558 }
1559
1560 pub fn render_document(&mut self) -> Result<&RenderDocument> {
1561 Ok(self.render_snapshot()?.document())
1562 }
1563
1564 pub fn render_with<B: RenderBackend>(
1565 &mut self,
1566 renderer: &mut Renderer<B>,
1567 ) -> Result<B::Output> {
1568 renderer.render(self.render_document()?)
1569 }
1570
1571 pub fn to_bytes(&mut self) -> Result<Vec<u8>> {
1572 let mut renderer = Renderer::new(PdfRenderBackend::default());
1573 self.render_with(&mut renderer)
1574 }
1575
1576 pub fn write<W: Write>(&mut self, writer: W) -> Result<()> {
1577 let mut backend = PdfRenderBackend::default();
1578 backend.render_to_writer(self.render_document()?, writer)
1579 }
1580
1581 pub fn save(&mut self, path: impl AsRef<Path>) -> Result<()> {
1582 let mut backend = PdfRenderBackend::default();
1583 backend.render_to_file(self.render_document()?, path)
1584 }
1585}
1586
1587#[derive(Debug, Default)]
1588pub struct PdfRenderBackend {
1589 registered_fonts: HashMap<FontBinding, String>,
1590 fallback_font_name: Option<String>,
1591}
1592
1593impl PdfRenderBackend {
1594 pub fn new() -> Self {
1595 Self::default()
1596 }
1597
1598 pub fn render_to_writer<W: Write>(
1599 &mut self,
1600 document: &RenderDocument,
1601 mut writer: W,
1602 ) -> Result<()> {
1603 let bytes = self.render_document(document)?;
1604 writer.write_all(&bytes)?;
1605 Ok(())
1606 }
1607
1608 pub fn render_to_file(
1609 &mut self,
1610 document: &RenderDocument,
1611 path: impl AsRef<Path>,
1612 ) -> Result<()> {
1613 let mut file = std::fs::File::create(path)?;
1614 self.render_to_writer(document, &mut file)
1615 }
1616
1617 fn encode_document(&mut self, document: &RenderDocument) -> Result<Vec<u8>> {
1618 let mut writer = PdfWriter::with_metadata(metadata_to_pdf(&document.metadata));
1619 self.registered_fonts.clear();
1620 self.fallback_font_name = None;
1621
1622 for page in &document.pages {
1623 let content = self.encode_page(page, &mut writer)?;
1624 writer.add_page(
1625 graphitepdf_kit::PageSize::new(page.size.width as f64, page.size.height as f64),
1626 content,
1627 );
1628 }
1629
1630 Ok(writer.write_all()?)
1631 }
1632
1633 fn encode_page(&mut self, page: &RenderPage, writer: &mut PdfWriter) -> Result<Vec<u8>> {
1634 let mut content = String::new();
1635 writeln!(
1636 &mut content,
1637 "% graphitepdf-render page {}",
1638 page.source_page_index
1639 )
1640 .map_err(string_write_error)?;
1641
1642 for command in &page.commands {
1643 self.encode_command(page, command, writer, &mut content)?;
1644 }
1645
1646 Ok(content.into_bytes())
1647 }
1648
1649 fn encode_command(
1650 &mut self,
1651 page: &RenderPage,
1652 command: &RenderCommand,
1653 writer: &mut PdfWriter,
1654 content: &mut String,
1655 ) -> Result<()> {
1656 match command {
1657 RenderCommand::FillRect(operation) => self.write_fill_rect(page, operation, content),
1658 RenderCommand::StrokeBorder(operation) => self.write_border(page, operation, content),
1659 RenderCommand::DrawBox(operation) => {
1660 write_comment(content, &format!("box {}", operation.context.label()))
1661 }
1662 RenderCommand::DrawText(operation) => self.write_text(page, operation, writer, content),
1663 RenderCommand::DrawImage(operation) => self.write_image(page, operation, content),
1664 RenderCommand::DrawSvg(operation) => self.write_svg(page, operation, content),
1665 RenderCommand::PushTransform(operation) => self.write_transform(operation, content),
1666 RenderCommand::PopTransform(context) => {
1667 write_comment(content, &format!("pop {}", context.label()))?;
1668 content.push_str("Q\n");
1669 Ok(())
1670 }
1671 RenderCommand::DrawDebug(operation) => self.write_debug(page, operation, content),
1672 RenderCommand::DrawForm(operation) => self.write_form(page, operation, writer, content),
1673 }
1674 }
1675
1676 fn write_fill_rect(
1677 &self,
1678 page: &RenderPage,
1679 operation: &FillRectOp,
1680 content: &mut String,
1681 ) -> Result<()> {
1682 if operation.color.alpha == 0 {
1683 return Ok(());
1684 }
1685
1686 let bounds = pdf_bounds(page, operation.bounds);
1687 write_comment(content, &format!("fill {}", operation.context.label()))?;
1688 content.push_str("q\n");
1689 push_fill_color(content, operation.color)?;
1690 writeln!(
1691 content,
1692 "{} {} {} {} re",
1693 bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height
1694 )
1695 .map_err(string_write_error)?;
1696 content.push_str("f\nQ\n");
1697 Ok(())
1698 }
1699
1700 fn write_border(
1701 &self,
1702 page: &RenderPage,
1703 operation: &BorderRenderOp,
1704 content: &mut String,
1705 ) -> Result<()> {
1706 if operation.border.width.value() <= 0.0 || operation.border.color.alpha == 0 {
1707 return Ok(());
1708 }
1709
1710 let bounds = pdf_bounds(page, operation.bounds);
1711 let half_width = operation.border.width.value() * 0.5;
1712 let (x1, y1, x2, y2) = match operation.side {
1713 BorderSidePosition::Top => (
1714 bounds.origin.x,
1715 bounds.origin.y + bounds.size.height - half_width,
1716 bounds.origin.x + bounds.size.width,
1717 bounds.origin.y + bounds.size.height - half_width,
1718 ),
1719 BorderSidePosition::Right => (
1720 bounds.origin.x + bounds.size.width - half_width,
1721 bounds.origin.y,
1722 bounds.origin.x + bounds.size.width - half_width,
1723 bounds.origin.y + bounds.size.height,
1724 ),
1725 BorderSidePosition::Bottom => (
1726 bounds.origin.x,
1727 bounds.origin.y + half_width,
1728 bounds.origin.x + bounds.size.width,
1729 bounds.origin.y + half_width,
1730 ),
1731 BorderSidePosition::Left => (
1732 bounds.origin.x + half_width,
1733 bounds.origin.y,
1734 bounds.origin.x + half_width,
1735 bounds.origin.y + bounds.size.height,
1736 ),
1737 };
1738
1739 write_comment(content, &format!("border {}", operation.context.label()))?;
1740 content.push_str("q\n");
1741 push_stroke_color(content, operation.border.color)?;
1742 writeln!(content, "{} w", operation.border.width.value()).map_err(string_write_error)?;
1743 writeln!(content, "{} {} m", x1, y1).map_err(string_write_error)?;
1744 writeln!(content, "{} {} l", x2, y2).map_err(string_write_error)?;
1745 content.push_str("S\nQ\n");
1746 Ok(())
1747 }
1748
1749 fn write_text(
1750 &mut self,
1751 page: &RenderPage,
1752 operation: &TextRenderOp,
1753 writer: &mut PdfWriter,
1754 content: &mut String,
1755 ) -> Result<()> {
1756 if operation.text.is_empty() || operation.color.alpha == 0 {
1757 return Ok(());
1758 }
1759
1760 write_comment(content, &format!("text {}", operation.context.label()))?;
1761 if let Some(layout) = &operation.layout {
1762 return self.write_text_layout(page, operation, layout, writer, content);
1763 }
1764
1765 let font_name = self.ensure_font(
1766 writer,
1767 operation.font.as_ref(),
1768 operation.font_source.as_ref(),
1769 );
1770 let origin = text_origin(page, operation);
1771 let line_height = operation
1772 .line_height
1773 .map(|value| value.value())
1774 .unwrap_or_else(|| operation.font_size.value() * 1.2);
1775
1776 content.push_str("BT\n");
1777 writeln!(content, "/{} {} Tf", font_name, operation.font_size.value())
1778 .map_err(string_write_error)?;
1779 push_fill_color(content, operation.color)?;
1780 writeln!(content, "{} TL", line_height).map_err(string_write_error)?;
1781 writeln!(content, "1 0 0 1 {} {} Tm", origin.0, origin.1).map_err(string_write_error)?;
1782
1783 let mut lines = operation.text.lines();
1784 if let Some(first_line) = lines.next() {
1785 writeln!(content, "({}) Tj", escape_pdf_text(first_line))
1786 .map_err(string_write_error)?;
1787 for line in lines {
1788 content.push_str("T*\n");
1789 writeln!(content, "({}) Tj", escape_pdf_text(line)).map_err(string_write_error)?;
1790 }
1791 }
1792
1793 content.push_str("ET\n");
1794 Ok(())
1795 }
1796
1797 fn write_text_layout(
1798 &mut self,
1799 page: &RenderPage,
1800 operation: &TextRenderOp,
1801 layout: &TextLayout,
1802 writer: &mut PdfWriter,
1803 content: &mut String,
1804 ) -> Result<()> {
1805 for fragment in layout.fragments() {
1806 if fragment.text().is_empty() {
1807 continue;
1808 }
1809
1810 let font_source = layout
1811 .runs()
1812 .iter()
1813 .find(|run| {
1814 run.range().start() <= fragment.range().start()
1815 && fragment.range().end() <= run.range().end()
1816 })
1817 .and_then(|run| run.font_source().cloned())
1818 .or_else(|| operation.font_source.clone());
1819 let font_name = self.ensure_font(writer, Some(fragment.font()), font_source.as_ref());
1820 let x = operation.context.content_frame.origin.x + fragment.rect().x.value();
1821 let y = page.size.height
1822 - (operation.context.content_frame.origin.y + fragment.baseline().value());
1823
1824 content.push_str("BT\n");
1825 writeln!(
1826 content,
1827 "/{} {} Tf",
1828 font_name,
1829 fragment.font_size().value()
1830 )
1831 .map_err(string_write_error)?;
1832 push_fill_color(content, operation.color)?;
1833 writeln!(content, "1 0 0 1 {} {} Tm", x, y).map_err(string_write_error)?;
1834 writeln!(content, "({}) Tj", escape_pdf_text(fragment.text()))
1835 .map_err(string_write_error)?;
1836 content.push_str("ET\n");
1837 }
1838
1839 Ok(())
1840 }
1841
1842 fn write_image(
1843 &self,
1844 page: &RenderPage,
1845 operation: &ImageRenderOp,
1846 content: &mut String,
1847 ) -> Result<()> {
1848 let pdf_y =
1849 page.size.height - operation.destination.origin.y - operation.destination.size.height;
1850
1851 let rendered = match &operation.source {
1852 RenderImageSource::Asset(image) => render_image_to_page_content_with_options(
1853 image,
1854 &ImageRenderOptions::new()
1855 .position(operation.destination.origin.x as f64, pdf_y as f64)
1856 .size(
1857 operation.destination.size.width as f64,
1858 operation.destination.size.height as f64,
1859 ),
1860 )?,
1861 RenderImageSource::Source(source) => {
1862 let image = resolve_image_source_blocking(source.clone())?;
1863 render_image_to_page_content_with_options(
1864 image.as_ref(),
1865 &ImageRenderOptions::new()
1866 .position(operation.destination.origin.x as f64, pdf_y as f64)
1867 .size(
1868 operation.destination.size.width as f64,
1869 operation.destination.size.height as f64,
1870 ),
1871 )?
1872 }
1873 };
1874
1875 write_comment(content, &format!("image {}", operation.context.label()))?;
1876 content.push_str(
1877 std::str::from_utf8(&rendered).map_err(|error| Error::Backend {
1878 message: format!("image content was not valid UTF-8: {error}"),
1879 })?,
1880 );
1881 if !content.ends_with('\n') {
1882 content.push('\n');
1883 }
1884 Ok(())
1885 }
1886
1887 fn write_svg(
1888 &self,
1889 page: &RenderPage,
1890 operation: &SvgRenderOp,
1891 content: &mut String,
1892 ) -> Result<()> {
1893 let pdf_y =
1894 page.size.height - operation.destination.origin.y - operation.destination.size.height;
1895 let options = SvgRenderOptions::new()
1896 .position(operation.destination.origin.x as f64, pdf_y as f64)
1897 .size(
1898 operation.destination.size.width as f64,
1899 operation.destination.size.height as f64,
1900 );
1901
1902 let bytes = match &operation.source {
1903 SvgRenderSource::Svg(svg) => {
1904 render_svg_node_to_page_content_with_options(svg, &options)?
1905 }
1906 SvgRenderSource::Math { svg, .. } => {
1907 render_svg_node_to_page_content_with_options(svg, &options)?
1908 }
1909 };
1910
1911 write_comment(content, &format!("svg {}", operation.context.label()))?;
1912 content.push_str(std::str::from_utf8(&bytes).map_err(|error| Error::Backend {
1913 message: format!("svg content was not valid UTF-8: {error}"),
1914 })?);
1915 if !content.ends_with('\n') {
1916 content.push('\n');
1917 }
1918 Ok(())
1919 }
1920
1921 fn write_transform(&self, operation: &TransformRenderOp, content: &mut String) -> Result<()> {
1922 write_comment(content, &format!("push {}", operation.context.label()))?;
1923 content.push_str("q\n");
1924 writeln!(
1925 content,
1926 "{} {} {} {} {} {} cm",
1927 operation.matrix.a,
1928 operation.matrix.b,
1929 operation.matrix.c,
1930 operation.matrix.d,
1931 operation.matrix.e,
1932 operation.matrix.f
1933 )
1934 .map_err(string_write_error)?;
1935 Ok(())
1936 }
1937
1938 fn write_debug(
1939 &self,
1940 page: &RenderPage,
1941 operation: &DebugRenderOp,
1942 content: &mut String,
1943 ) -> Result<()> {
1944 let bounds = pdf_bounds(page, operation.context.frame);
1945 write_comment(content, &format!("debug {}", operation.context.label()))?;
1946 content.push_str("q\n");
1947 push_stroke_color(content, operation.color)?;
1948 writeln!(content, "0.75 w").map_err(string_write_error)?;
1949 writeln!(
1950 content,
1951 "{} {} {} {} re",
1952 bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height
1953 )
1954 .map_err(string_write_error)?;
1955 content.push_str("S\nQ\n");
1956 Ok(())
1957 }
1958
1959 fn write_form(
1960 &mut self,
1961 page: &RenderPage,
1962 operation: &FormRenderOp,
1963 writer: &mut PdfWriter,
1964 content: &mut String,
1965 ) -> Result<()> {
1966 write_comment(
1967 content,
1968 &format!("form {} {}", operation.name, operation.context.label()),
1969 )?;
1970 content.push_str("q\n");
1971 for command in &operation.commands {
1972 self.encode_command(page, command, writer, content)?;
1973 }
1974 content.push_str("Q\n");
1975 Ok(())
1976 }
1977
1978 fn ensure_font(
1979 &mut self,
1980 writer: &mut PdfWriter,
1981 descriptor: Option<&FontDescriptor>,
1982 source: Option<&FontSource>,
1983 ) -> String {
1984 let binding = FontBinding {
1985 descriptor: descriptor.cloned(),
1986 source: source.cloned(),
1987 };
1988
1989 if let Some(name) = self.registered_fonts.get(&binding) {
1990 return name.clone();
1991 }
1992
1993 let font_name = match source {
1994 Some(FontSource::Standard(font)) => self.standard_font_name(writer, *font),
1995 _ => match descriptor.and_then(resolve_standard_font) {
1996 Some(font) => self.standard_font_name(writer, font),
1997 None => self.fallback_font_name(writer),
1998 },
1999 };
2000
2001 self.registered_fonts.insert(binding, font_name.clone());
2002 font_name
2003 }
2004
2005 fn standard_font_name(&mut self, writer: &mut PdfWriter, font: StandardFont) -> String {
2006 if font == StandardFont::Helvetica {
2007 return String::from("F1");
2008 }
2009
2010 let binding = FontBinding {
2011 descriptor: Some(
2012 FontDescriptor::new(font.family_name())
2013 .with_style(font.font_style())
2014 .with_weight(font.font_weight()),
2015 ),
2016 source: Some(FontSource::standard(font)),
2017 };
2018
2019 if let Some(name) = self.registered_fonts.get(&binding) {
2020 return name.clone();
2021 }
2022
2023 let name = writer.add_font(PdfFont::standard(font));
2024 self.registered_fonts.insert(binding, name.clone());
2025 name
2026 }
2027
2028 fn fallback_font_name(&mut self, writer: &mut PdfWriter) -> String {
2029 if let Some(name) = &self.fallback_font_name {
2030 return name.clone();
2031 }
2032
2033 let name = String::from("F1");
2034 let _ = writer;
2035 self.fallback_font_name = Some(name.clone());
2036 name
2037 }
2038}
2039
2040fn resolve_image_source_blocking(source: AssetImageSource) -> Result<Arc<Image>> {
2041 if tokio::runtime::Handle::try_current().is_ok() {
2042 let join_handle = std::thread::spawn(move || -> Result<Arc<Image>> {
2043 let runtime = tokio::runtime::Builder::new_current_thread()
2044 .enable_all()
2045 .build()
2046 .map_err(|error| Error::Backend {
2047 message: format!("failed to build image resolution runtime: {error}"),
2048 })?;
2049 runtime.block_on(resolve_image(source)).map_err(Into::into)
2050 });
2051
2052 return join_handle.join().map_err(|_| Error::Backend {
2053 message: String::from("image resolution thread panicked"),
2054 })?;
2055 }
2056
2057 let runtime = tokio::runtime::Builder::new_current_thread()
2058 .enable_all()
2059 .build()
2060 .map_err(|error| Error::Backend {
2061 message: format!("failed to build image resolution runtime: {error}"),
2062 })?;
2063
2064 runtime.block_on(resolve_image(source)).map_err(Into::into)
2065}
2066
2067impl RenderBackend for PdfRenderBackend {
2068 type Output = Vec<u8>;
2069
2070 fn render_document(&mut self, document: &RenderDocument) -> Result<Self::Output> {
2071 self.encode_document(document)
2072 }
2073}
2074
2075pub fn render_to_bytes<T>(document: &T) -> Result<Vec<u8>>
2076where
2077 T: RendererDocumentSource + ?Sized,
2078{
2079 let render_document =
2080 document.build_render_document(&LayoutEngine::new(), &RenderEngine::new())?;
2081 let mut backend = PdfRenderBackend::default();
2082 backend.render_document(&render_document)
2083}
2084
2085pub fn render_to_writer<T, W>(document: &T, mut writer: W) -> Result<()>
2086where
2087 T: RendererDocumentSource + ?Sized,
2088 W: Write,
2089{
2090 let bytes = render_to_bytes(document)?;
2091 writer.write_all(&bytes)?;
2092 Ok(())
2093}
2094
2095pub fn render_to_file<T>(document: &T, path: impl AsRef<Path>) -> Result<()>
2096where
2097 T: RendererDocumentSource + ?Sized,
2098{
2099 let mut file = std::fs::File::create(path)?;
2100 render_to_writer(document, &mut file)
2101}
2102
2103#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2104struct FontBinding {
2105 descriptor: Option<FontDescriptor>,
2106 source: Option<FontSource>,
2107}
2108
2109fn metadata_to_pdf(metadata: &LayoutMetadata) -> PdfMetadata {
2110 let mut pdf = PdfMetadata::new();
2111 pdf.title = metadata.title.clone();
2112 pdf.author = metadata.author.clone();
2113 pdf.subject = metadata.subject.clone();
2114 pdf.keywords = metadata.keywords.clone();
2115 pdf.creator = metadata.creator.clone();
2116 pdf.producer = metadata.producer.clone();
2117 pdf
2118}
2119
2120fn pdf_bounds(page: &RenderPage, bounds: Bounds) -> Bounds {
2121 Bounds::from_origin_size(
2122 bounds.origin.x,
2123 page.size.height - bounds.origin.y - bounds.size.height,
2124 bounds.size.width,
2125 bounds.size.height,
2126 )
2127}
2128
2129fn text_origin(page: &RenderPage, operation: &TextRenderOp) -> (f32, f32) {
2130 let x = operation.context.content_frame.origin.x;
2131 let y =
2132 page.size.height - operation.context.content_frame.origin.y - operation.font_size.value();
2133 (x, y)
2134}
2135
2136fn write_comment(content: &mut String, comment: &str) -> Result<()> {
2137 writeln!(content, "% {comment}").map_err(string_write_error)
2138}
2139
2140fn push_fill_color(content: &mut String, color: Color) -> Result<()> {
2141 let (r, g, b) = pdf_color(color);
2142 writeln!(content, "{r} {g} {b} rg").map_err(string_write_error)
2143}
2144
2145fn push_stroke_color(content: &mut String, color: Color) -> Result<()> {
2146 let (r, g, b) = pdf_color(color);
2147 writeln!(content, "{r} {g} {b} RG").map_err(string_write_error)
2148}
2149
2150fn pdf_color(color: Color) -> (f32, f32, f32) {
2151 (
2152 f32::from(color.red) / 255.0,
2153 f32::from(color.green) / 255.0,
2154 f32::from(color.blue) / 255.0,
2155 )
2156}
2157
2158fn resolve_standard_font(descriptor: &FontDescriptor) -> Option<StandardFont> {
2159 let family = descriptor.family().trim();
2160 let is_bold = descriptor.font_weight() >= graphitepdf_font::FontWeight::BOLD;
2161
2162 if family.eq_ignore_ascii_case("Helvetica") {
2163 return Some(match (descriptor.font_style(), is_bold) {
2164 (graphitepdf_font::FontStyle::Italic | graphitepdf_font::FontStyle::Oblique, true) => {
2165 StandardFont::HelveticaBoldOblique
2166 }
2167 (graphitepdf_font::FontStyle::Italic | graphitepdf_font::FontStyle::Oblique, false) => {
2168 StandardFont::HelveticaOblique
2169 }
2170 (_, true) => StandardFont::HelveticaBold,
2171 _ => StandardFont::Helvetica,
2172 });
2173 }
2174
2175 if family.eq_ignore_ascii_case("Times")
2176 || family.eq_ignore_ascii_case("Times-Roman")
2177 || family.eq_ignore_ascii_case("Times New Roman")
2178 {
2179 return Some(match (descriptor.font_style(), is_bold) {
2180 (graphitepdf_font::FontStyle::Italic | graphitepdf_font::FontStyle::Oblique, true) => {
2181 StandardFont::TimesBoldItalic
2182 }
2183 (graphitepdf_font::FontStyle::Italic | graphitepdf_font::FontStyle::Oblique, false) => {
2184 StandardFont::TimesItalic
2185 }
2186 (_, true) => StandardFont::TimesBold,
2187 _ => StandardFont::TimesRoman,
2188 });
2189 }
2190
2191 if family.eq_ignore_ascii_case("Courier") {
2192 return Some(match (descriptor.font_style(), is_bold) {
2193 (graphitepdf_font::FontStyle::Italic | graphitepdf_font::FontStyle::Oblique, true) => {
2194 StandardFont::CourierBoldOblique
2195 }
2196 (graphitepdf_font::FontStyle::Italic | graphitepdf_font::FontStyle::Oblique, false) => {
2197 StandardFont::CourierOblique
2198 }
2199 (_, true) => StandardFont::CourierBold,
2200 _ => StandardFont::Courier,
2201 });
2202 }
2203
2204 if family.eq_ignore_ascii_case("Symbol") {
2205 return Some(StandardFont::Symbol);
2206 }
2207
2208 if family.eq_ignore_ascii_case("ZapfDingbats") {
2209 return Some(StandardFont::ZapfDingbats);
2210 }
2211
2212 None
2213}
2214
2215fn escape_pdf_text(value: &str) -> String {
2216 value
2217 .chars()
2218 .map(|character| match character {
2219 '(' => String::from("\\("),
2220 ')' => String::from("\\)"),
2221 '\\' => String::from("\\\\"),
2222 '\n' => String::from("\\n"),
2223 '\r' => String::from("\\r"),
2224 '\t' => String::from("\\t"),
2225 '\x08' => String::from("\\b"),
2226 '\x0c' => String::from("\\f"),
2227 _ => character.to_string(),
2228 })
2229 .collect()
2230}
2231
2232fn string_write_error(error: std::fmt::Error) -> Error {
2233 Error::Backend {
2234 message: format!("failed to build PDF content stream: {error}"),
2235 }
2236}
2237
2238#[cfg(test)]
2239mod tests {
2240 use super::*;
2241
2242 use graphitepdf_image::{ImageFormat, RasterImage};
2243 use graphitepdf_layout::{Document, LayoutEngine, LayoutStyle, Node, Page};
2244 use graphitepdf_svg::parse_svg;
2245 use graphitepdf_textkit::{TextBlock, TextSpan};
2246
2247 fn render_document(document: Document, options: RenderEngineOptions) -> RenderDocument {
2248 let layout = LayoutEngine::new()
2249 .layout_document(&document)
2250 .expect("document should layout");
2251 RenderEngine::new()
2252 .with_options(options)
2253 .build(&layout)
2254 .expect("document should render")
2255 }
2256
2257 fn text_block(value: &str) -> TextBlock {
2258 TextBlock::from(TextSpan::new(value).expect("text span should be valid"))
2259 }
2260
2261 #[test]
2262 fn renders_safe_layout_documents_with_page_backgrounds_forms_and_debug() {
2263 let document = Document::new().with_page(
2264 Page::new([
2265 Node::view([Node::text(text_block("Hello render"))]).with_style(
2266 LayoutStyle::new().with_background_color(Color::rgb(0xee, 0xf2, 0xff)),
2267 ),
2268 ])
2269 .with_size(Size::new(220.0, 140.0))
2270 .with_style(
2271 LayoutStyle::new()
2272 .with_padding(EdgeInsets::all(Pt::new(12.0)))
2273 .with_background_color(Color::rgb(0x11, 0x22, 0x33)),
2274 ),
2275 );
2276
2277 let rendered = render_document(
2278 document,
2279 RenderEngineOptions {
2280 wrap_views_in_forms: true,
2281 debug: Some(DebugRenderOptions::default()),
2282 ..RenderEngineOptions::default()
2283 },
2284 );
2285
2286 assert_eq!(rendered.pages.len(), 1);
2287 assert_eq!(rendered.pages[0].size, Size::new(220.0, 140.0));
2288 assert!(matches!(
2289 &rendered.pages[0].commands[0],
2290 RenderCommand::FillRect(FillRectOp {
2291 role: PaintRole::PageBackground,
2292 color,
2293 ..
2294 }) if *color == Color::rgb(0x11, 0x22, 0x33)
2295 ));
2296 assert!(
2297 rendered.pages[0]
2298 .commands
2299 .iter()
2300 .any(|command| matches!(command, RenderCommand::DrawDebug(_)))
2301 );
2302
2303 let form = rendered.pages[0]
2304 .commands
2305 .iter()
2306 .find_map(|command| match command {
2307 RenderCommand::DrawForm(form) => Some(form),
2308 _ => None,
2309 })
2310 .expect("view should render as form");
2311 assert!(
2312 form.commands
2313 .iter()
2314 .any(|command| matches!(command, RenderCommand::DrawText(_)))
2315 );
2316 assert!(
2317 form.commands
2318 .iter()
2319 .any(|command| matches!(command, RenderCommand::FillRect(_)))
2320 );
2321 }
2322
2323 #[test]
2324 fn renders_text_images_svg_and_math_with_typed_operations() {
2325 let image = Image::Raster(RasterImage {
2326 width: 200,
2327 height: 100,
2328 data: vec![1, 2, 3, 4],
2329 format: ImageFormat::Png,
2330 key: Some(String::from("hero")),
2331 });
2332 let svg = parse_svg(r#"<svg viewBox="0 0 20 10"><rect width="20" height="10"/></svg>"#);
2333 let document = Document::new().with_page(
2334 Page::new([
2335 Node::text(text_block("Typed text")),
2336 Node::image_asset(image).with_style(LayoutStyle::new().with_width(Pt::new(50.0))),
2337 Node::svg(svg).with_style(LayoutStyle::new().with_width(Pt::new(40.0))),
2338 Node::math("x^2 + y^2").with_style(LayoutStyle::new().with_width(Pt::new(60.0))),
2339 ])
2340 .with_size(Size::new(240.0, 240.0))
2341 .with_style(LayoutStyle::new().with_padding(EdgeInsets::all(Pt::new(10.0)))),
2342 );
2343
2344 let rendered = render_document(document, RenderEngineOptions::default());
2345 let commands = &rendered.pages[0].commands;
2346
2347 assert!(commands.iter().any(|command| match command {
2348 RenderCommand::DrawText(operation) => {
2349 operation.text == "Typed text" && operation.layout.is_some()
2350 }
2351 _ => false,
2352 }));
2353 assert!(commands.iter().any(|command| match command {
2354 RenderCommand::DrawImage(operation) => {
2355 matches!(operation.source, RenderImageSource::Asset(_))
2356 && operation.destination.size.width == 50.0
2357 && operation.destination.size.height == 25.0
2358 }
2359 _ => false,
2360 }));
2361 assert!(
2362 commands
2363 .iter()
2364 .any(|command| matches!(command, RenderCommand::PushTransform(_)))
2365 );
2366 assert!(commands.iter().any(|command| match command {
2367 RenderCommand::DrawSvg(operation) =>
2368 matches!(operation.source, SvgRenderSource::Svg(_)),
2369 _ => false,
2370 }));
2371 assert!(commands.iter().any(|command| match command {
2372 RenderCommand::DrawSvg(operation) => {
2373 matches!(operation.source, SvgRenderSource::Math { .. })
2374 }
2375 _ => false,
2376 }));
2377 }
2378
2379 #[test]
2380 fn supports_legacy_layout_documents_for_basic_text_and_box_rendering() {
2381 let legacy = LayoutEngine::new()
2382 .layout_text_block(Size::new(180.0, 60.0), text_block("Legacy text"))
2383 .expect("legacy layout should build");
2384 let rendered = RenderEngine::new()
2385 .build(&legacy)
2386 .expect("legacy layout should render");
2387
2388 assert_eq!(rendered.pages.len(), 1);
2389 assert!(matches!(
2390 &rendered.pages[0].commands[0],
2391 RenderCommand::DrawText(TextRenderOp { text, layout, .. })
2392 if text == "Legacy text" && layout.is_none()
2393 ));
2394 }
2395
2396 #[test]
2397 fn parses_colors_and_dimensions() {
2398 assert_eq!(
2399 parse_color("#1234").expect("short hex color should parse"),
2400 Color::rgba(0x11, 0x22, 0x33, 0x44)
2401 );
2402 assert_eq!(
2403 parse_color("rgba(255, 0, 0, 0.5)").expect("rgba color should parse"),
2404 Color::rgba(255, 0, 0, 128)
2405 );
2406 assert_eq!(
2407 parse_color("rgb(100%, 0%, 0%)").expect("percent rgb color should parse"),
2408 Color::rgb(255, 0, 0)
2409 );
2410 assert_eq!(parse_dimension("2.54cm").expect("cm should parse"), 72.0);
2411 }
2412
2413 #[test]
2414 fn fits_objects_and_parses_transform_matrices() {
2415 let fitted = fit_object(
2416 Size::new(200.0, 100.0),
2417 Bounds::from_origin_size(0.0, 0.0, 50.0, 50.0),
2418 ObjectFit::Contain,
2419 );
2420 assert_eq!(fitted.bounds.size.width, 50.0);
2421 assert_eq!(fitted.bounds.size.height, 25.0);
2422 assert_eq!(fitted.bounds.origin.y, 12.5);
2423
2424 let operations =
2425 parse_transform("translate(10, 20) scale(2) rotate(90)").expect("transform parses");
2426 assert_eq!(
2427 operations,
2428 vec![
2429 TransformOperation::Translate { x: 10.0, y: 20.0 },
2430 TransformOperation::Scale { x: 2.0, y: 2.0 },
2431 TransformOperation::Rotate {
2432 degrees: 90.0,
2433 cx: 0.0,
2434 cy: 0.0,
2435 },
2436 ]
2437 );
2438
2439 let matrix = compose_transform(&operations);
2440 assert!(!matrix.is_identity());
2441 }
2442
2443 #[test]
2444 fn emits_border_commands_for_each_visible_side() {
2445 let context = RenderContext {
2446 page_index: 0,
2447 source_page_index: 0,
2448 path: vec![0],
2449 node_kind: RenderNodeKind::Box,
2450 z_index: 0,
2451 frame: Bounds::from_origin_size(10.0, 20.0, 30.0, 40.0),
2452 content_frame: Bounds::from_origin_size(10.0, 20.0, 30.0, 40.0),
2453 };
2454 let border = Border::all(BorderSide::new(
2455 Pt::new(2.0),
2456 Color::rgb(0x11, 0x22, 0x33),
2457 BorderStyle::Solid,
2458 ));
2459
2460 let commands = border_commands(&context, &border);
2461 assert_eq!(commands.len(), 4);
2462 assert!(matches!(
2463 &commands[0],
2464 RenderCommand::StrokeBorder(BorderRenderOp {
2465 side: BorderSidePosition::Top,
2466 ..
2467 })
2468 ));
2469 }
2470}