Skip to main content

graphitepdf_render/
lib.rs

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}