diff --git a/crates/typst-pdf/src/krilla.rs b/crates/typst-pdf/src/krilla.rs index 1b49be48c..cb1dbb4b9 100644 --- a/crates/typst-pdf/src/krilla.rs +++ b/crates/typst-pdf/src/krilla.rs @@ -3,12 +3,12 @@ use crate::link::handle_link; use crate::metadata::build_metadata; use crate::outline::build_outline; use crate::page::PageLabelExt; +use crate::shape::handle_shape; use crate::text::handle_text; -use crate::util::{build_path, display_font, AbsExt, TransformExt}; +use crate::util::{convert_path, display_font, AbsExt, TransformExt}; use crate::{paint, PdfOptions}; use krilla::destination::{NamedDestination, XyzDestination}; use krilla::error::KrillaError; -use krilla::geom::Rect; use krilla::page::PageLabel; use krilla::path::PathBuilder; use krilla::surface::Surface; @@ -54,11 +54,11 @@ impl State { } } - pub fn size(&mut self, size: Size) { + pub(crate) fn size(&mut self, size: Size) { self.size = size; } - pub fn transform(&mut self, transform: Transform) { + pub(crate) fn transform(&mut self, transform: Transform) { self.transform = self.transform.pre_concat(transform); self.transform_chain = self.transform_chain.pre_concat(transform); } @@ -68,7 +68,7 @@ impl State { } /// Creates the [`Transforms`] structure for the current item. - pub fn transforms(&self, size: Size) -> Transforms { + pub(crate) fn transforms(&self, size: Size) -> Transforms { Transforms { transform_chain_: self.transform_chain, container_transform_chain: self.container_transform_chain, @@ -84,26 +84,26 @@ pub(crate) struct FrameContext { } impl FrameContext { - pub fn new(size: Size) -> Self { + pub(crate) fn new(size: Size) -> Self { Self { states: vec![State::new(size, Transform::identity(), Transform::identity())], annotations: vec![], } } - pub fn push(&mut self) { + pub(crate) fn push(&mut self) { self.states.push(self.states.last().unwrap().clone()); } - pub fn pop(&mut self) { + pub(crate) fn pop(&mut self) { self.states.pop(); } - pub fn state(&self) -> &State { + pub(crate) fn state(&self) -> &State { self.states.last().unwrap() } - pub fn state_mut(&mut self) -> &mut State { + pub(crate) fn state_mut(&mut self) -> &mut State { self.states.last_mut().unwrap() } } @@ -112,16 +112,16 @@ impl FrameContext { #[derive(Debug, Clone, Copy)] pub(super) struct Transforms { /// The full transform chain. - pub transform_chain_: Transform, + pub(crate) transform_chain_: Transform, /// The transform of first hard frame in the hierarchy. - pub container_transform_chain: Transform, + pub(crate) container_transform_chain: Transform, /// The size of the first hard frame in the hierarchy. - pub container_size: Size, + pub(crate) container_size: Size, /// The size of the item. - pub size: Size, + pub(crate) size: Size, } -pub struct GlobalContext<'a> { +pub(crate) struct GlobalContext<'a> { /// Cache the conversion between krilla and Typst fonts (forward and backward). pub(crate) fonts_forward: HashMap, pub(crate) fonts_backward: HashMap, @@ -265,7 +265,7 @@ pub fn pdf( let mut page = document.start_page_with(settings); let mut surface = page.surface(); let mut fc = FrameContext::new(typst_page.frame.size()); - process_frame( + handle_frame( &mut fc, &typst_page.frame, typst_page.fill_or_transparent(), @@ -286,6 +286,82 @@ pub fn pdf( finish(document, gc) } +pub(crate) fn handle_frame( + fc: &mut FrameContext, + frame: &Frame, + fill: Option, + surface: &mut Surface, + gc: &mut GlobalContext, +) -> SourceResult<()> { + fc.push(); + + if frame.kind().is_hard() { + fc.state_mut().set_container_transform(); + fc.state_mut().size(frame.size()); + } + + if let Some(fill) = fill { + let shape = Geometry::Rect(frame.size()).filled(fill); + handle_shape(fc, &shape, surface, gc)?; + } + + for (point, item) in frame.items() { + fc.push(); + fc.state_mut().transform(Transform::translate(point.x, point.y)); + + match item { + FrameItem::Group(g) => handle_group(fc, g, surface, gc)?, + FrameItem::Text(t) => handle_text(fc, t, surface, gc)?, + FrameItem::Shape(s, _) => handle_shape(fc, s, surface, gc)?, + FrameItem::Image(image, size, span) => { + handle_image(gc, fc, image, *size, surface, *span)? + } + FrameItem::Link(d, s) => handle_link(fc, gc, d, *s), + FrameItem::Tag(_) => {} + } + + fc.pop(); + } + + fc.pop(); + + Ok(()) +} + +pub(crate) fn handle_group( + fc: &mut FrameContext, + group: &GroupItem, + surface: &mut Surface, + context: &mut GlobalContext, +) -> SourceResult<()> { + fc.push(); + fc.state_mut().transform(group.transform); + + let clip_path = group + .clip_path + .as_ref() + .and_then(|p| { + let mut builder = PathBuilder::new(); + convert_path(p, &mut builder); + builder.finish() + }) + .and_then(|p| p.transform(fc.state().transform.to_krilla())); + + if let Some(clip_path) = &clip_path { + surface.push_clip_path(clip_path, &krilla::path::FillRule::NonZero); + } + + handle_frame(fc, &group.frame, None, surface, context)?; + + if clip_path.is_some() { + surface.pop(); + } + + fc.pop(); + + Ok(()) +} + /// Finish a krilla document and handle export errors. fn finish(document: Document, gc: GlobalContext) -> SourceResult> { match document.finish() { @@ -430,157 +506,3 @@ fn get_version(options: &PdfOptions) -> SourceResult { } } } - -pub fn process_frame( - fc: &mut FrameContext, - frame: &Frame, - fill: Option, - surface: &mut Surface, - gc: &mut GlobalContext, -) -> SourceResult<()> { - fc.push(); - - if frame.kind().is_hard() { - fc.state_mut().set_container_transform(); - fc.state_mut().size(frame.size()); - } - - if let Some(fill) = fill { - let shape = Geometry::Rect(frame.size()).filled(fill); - handle_shape(fc, &shape, surface, gc)?; - } - - for (point, item) in frame.items() { - fc.push(); - fc.state_mut().transform(Transform::translate(point.x, point.y)); - - match item { - FrameItem::Group(g) => handle_group(fc, g, surface, gc)?, - FrameItem::Text(t) => handle_text(fc, t, surface, gc)?, - FrameItem::Shape(s, _) => handle_shape(fc, s, surface, gc)?, - FrameItem::Image(image, size, span) => { - handle_image(gc, fc, image, *size, surface, *span)? - } - FrameItem::Link(d, s) => handle_link(fc, gc, d, *s), - FrameItem::Tag(_) => {} - } - - fc.pop(); - } - - fc.pop(); - - Ok(()) -} - -pub fn handle_group( - fc: &mut FrameContext, - group: &GroupItem, - surface: &mut Surface, - context: &mut GlobalContext, -) -> SourceResult<()> { - fc.push(); - fc.state_mut().transform(group.transform); - - let clip_path = group - .clip_path - .as_ref() - .and_then(|p| { - let mut builder = PathBuilder::new(); - build_path(p, &mut builder); - builder.finish() - }) - .and_then(|p| p.transform(fc.state().transform.to_krilla())); - - if let Some(clip_path) = &clip_path { - surface.push_clip_path(clip_path, &krilla::path::FillRule::NonZero); - } - - process_frame(fc, &group.frame, None, surface, context)?; - - if clip_path.is_some() { - surface.pop(); - } - - fc.pop(); - - Ok(()) -} - -pub fn handle_shape( - fc: &mut FrameContext, - shape: &Shape, - surface: &mut Surface, - gc: &mut GlobalContext, -) -> SourceResult<()> { - let mut path_builder = PathBuilder::new(); - - match &shape.geometry { - Geometry::Line(l) => { - path_builder.move_to(0.0, 0.0); - path_builder.line_to(l.x.to_f32(), l.y.to_f32()); - } - Geometry::Rect(size) => { - let w = size.x.to_f32(); - let h = size.y.to_f32(); - let rect = if w < 0.0 || h < 0.0 { - // Skia doesn't normally allow for negative dimensions, but - // Typst supports them, so we apply a transform if needed - // Because this operation is expensive according to tiny-skia's - // docs, we prefer to not apply it if not needed - let transform = - krilla::geom::Transform::from_scale(w.signum(), h.signum()); - Rect::from_xywh(0.0, 0.0, w.abs(), h.abs()) - .and_then(|rect| rect.transform(transform)) - } else { - Rect::from_xywh(0.0, 0.0, w, h) - }; - - if let Some(rect) = rect { - path_builder.push_rect(rect); - } - } - Geometry::Path(p) => { - build_path(p, &mut path_builder); - } - } - - surface.push_transform(&fc.state().transform.to_krilla()); - - if let Some(path) = path_builder.finish() { - if let Some(paint) = &shape.fill { - let fill = paint::fill( - gc, - &paint, - shape.fill_rule, - false, - surface, - fc.state().transforms(shape.geometry.bbox_size()), - )?; - surface.fill_path(&path, fill); - } - - let stroke = shape.stroke.as_ref().and_then(|stroke| { - if stroke.thickness.to_f32() > 0.0 { - Some(stroke) - } else { - None - } - }); - - if let Some(stroke) = &stroke { - let stroke = paint::stroke( - gc, - stroke, - false, - surface, - fc.state().transforms(shape.geometry.bbox_size()), - )?; - surface.stroke_path(&path, stroke); - } - } - - surface.pop(); - - Ok(()) -} diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 87af9bbb2..aefd5a7e5 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -7,6 +7,7 @@ mod metadata; mod outline; mod page; mod paint; +mod shape; mod text; mod util; diff --git a/crates/typst-pdf/src/paint.rs b/crates/typst-pdf/src/paint.rs index 5d5de9b73..cc0cdf71a 100644 --- a/crates/typst-pdf/src/paint.rs +++ b/crates/typst-pdf/src/paint.rs @@ -1,6 +1,6 @@ //! Convert paint types from typst to krilla. -use crate::krilla::{process_frame, FrameContext, GlobalContext, Transforms}; +use crate::krilla::{handle_frame, FrameContext, GlobalContext, Transforms}; use crate::util::{AbsExt, ColorExt, FillRuleExt, LineCapExt, LineJoinExt, TransformExt}; use krilla::geom::NormalizedF32; use krilla::paint::SpreadMethod; @@ -127,7 +127,7 @@ pub(crate) fn convert_pattern( let mut stream_builder = surface.stream_builder(); let mut surface = stream_builder.surface(); let mut fc = FrameContext::new(pattern.frame().size()); - process_frame(&mut fc, pattern.frame(), None, &mut surface, gc)?; + handle_frame(&mut fc, pattern.frame(), None, &mut surface, gc)?; surface.finish(); let stream = stream_builder.finish(); let pattern = krilla::paint::Pattern { diff --git a/crates/typst-pdf/src/shape.rs b/crates/typst-pdf/src/shape.rs new file mode 100644 index 000000000..9c269f8d5 --- /dev/null +++ b/crates/typst-pdf/src/shape.rs @@ -0,0 +1,92 @@ +use crate::krilla::{FrameContext, GlobalContext}; +use crate::paint; +use crate::util::{convert_path, AbsExt, TransformExt}; +use krilla::geom::Rect; +use krilla::path::{Path, PathBuilder}; +use krilla::surface::Surface; +use typst_library::diag::SourceResult; +use typst_library::visualize::{Geometry, Shape}; + +pub(crate) fn handle_shape( + fc: &mut FrameContext, + shape: &Shape, + surface: &mut Surface, + gc: &mut GlobalContext, +) -> SourceResult<()> { + surface.push_transform(&fc.state().transform.to_krilla()); + + if let Some(path) = convert_geometry(&shape.geometry) { + if let Some(paint) = &shape.fill { + let fill = paint::fill( + gc, + &paint, + shape.fill_rule, + false, + surface, + fc.state().transforms(shape.geometry.bbox_size()), + )?; + + surface.fill_path(&path, fill); + } + + let stroke = shape.stroke.as_ref().and_then(|stroke| { + if stroke.thickness.to_f32() > 0.0 { + Some(stroke) + } else { + None + } + }); + + if let Some(stroke) = &stroke { + let stroke = paint::stroke( + gc, + stroke, + false, + surface, + fc.state().transforms(shape.geometry.bbox_size()), + )?; + + surface.stroke_path(&path, stroke); + } + } + + surface.pop(); + + Ok(()) +} + +fn convert_geometry(geometry: &Geometry) -> Option { + let mut path_builder = PathBuilder::new(); + + match geometry { + Geometry::Line(l) => { + path_builder.move_to(0.0, 0.0); + path_builder.line_to(l.x.to_f32(), l.y.to_f32()); + } + Geometry::Rect(size) => { + let w = size.x.to_f32(); + let h = size.y.to_f32(); + let rect = if w < 0.0 || h < 0.0 { + // Skia doesn't normally allow for negative dimensions, but + // Typst supports them, so we apply a transform if needed + // Because this operation is expensive according to tiny-skia's + // docs, we prefer to not apply it if not needed + let transform = + krilla::geom::Transform::from_scale(w.signum(), h.signum()); + Rect::from_xywh(0.0, 0.0, w.abs(), h.abs()) + .and_then(|rect| rect.transform(transform)) + } else { + Rect::from_xywh(0.0, 0.0, w, h) + }; + + if let Some(rect) = rect { + path_builder.push_rect(rect); + } + } + Geometry::Path(p) => { + convert_path(p, &mut path_builder); + } + } + + path_builder.finish() +} diff --git a/crates/typst-pdf/src/util.rs b/crates/typst-pdf/src/util.rs index df292ec26..bc7d1f7c6 100644 --- a/crates/typst-pdf/src/util.rs +++ b/crates/typst-pdf/src/util.rs @@ -121,7 +121,7 @@ pub(crate) fn display_font(font: &Font) -> String { } /// Build a typst path using a path builder. -pub(crate) fn build_path(path: &Path, builder: &mut PathBuilder) { +pub(crate) fn convert_path(path: &Path, builder: &mut PathBuilder) { for item in &path.0 { match item { PathItem::MoveTo(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()),