diff --git a/crates/typst-pdf/src/krilla.rs b/crates/typst-pdf/src/krilla.rs index 55119b9e5..94eb67efc 100644 --- a/crates/typst-pdf/src/krilla.rs +++ b/crates/typst-pdf/src/krilla.rs @@ -1,12 +1,11 @@ -use crate::{paint, primitive, AbsExt}; +use crate::{paint, AbsExt}; use bytemuck::TransparentWrapper; -use image::{DynamicImage, GenericImageView, Rgba}; +use image::{GenericImageView}; use krilla::action::{Action, LinkAction}; use krilla::annotation::{LinkAnnotation, Target}; use krilla::destination::XyzDestination; use krilla::font::{GlyphId, GlyphUnits}; use krilla::geom::{Point, Transform}; -use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace}; use krilla::path::PathBuilder; use krilla::surface::Surface; use krilla::validation::Validator; @@ -15,17 +14,86 @@ use krilla::{PageSettings, SerializeSettings, SvgSettings}; use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::ops::Range; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; use svg2pdf::usvg::Rect; -use typst_library::layout::{Abs, Frame, FrameItem, GroupItem, Page, Size}; -use typst_library::model::{Destination, Document}; +use typst_library::layout::{Abs, Frame, FrameItem, GroupItem, PagedDocument, Size}; +use typst_library::model::Destination; use typst_library::text::{Font, Glyph, TextItem}; -use typst_library::visualize::{ - FillRule, Geometry, Image, ImageKind, Path, PathItem, RasterFormat, RasterImage, - Shape, -}; +use typst_library::visualize::{FillRule, Geometry, Image, ImageKind, Paint, Path, PathItem, Shape}; +use crate::content_old::Transforms; use crate::primitive::{PointExt, SizeExt, TransformExt}; +#[derive(Debug, Clone)] +struct State { + /// The transform of the current item. + transform: typst_library::layout::Transform, + /// The transform of first hard frame in the hierarchy. + container_transform: typst_library::layout::Transform, + /// The size of the first hard frame in the hierarchy. + size: Size, +} + +impl State { + /// Creates a new, clean state for a given `size`. + pub fn new(size: Size) -> Self { + Self { + transform: typst_library::layout::Transform::identity(), + container_transform: typst_library::layout::Transform::identity(), + size, + } + } + + pub fn transform(&mut self, transform: typst_library::layout::Transform) { + self.transform = self.transform.pre_concat(transform); + if self.container_transform.is_identity() { + self.container_transform = self.transform; + } + } + + fn group_transform(&mut self, transform: typst_library::layout::Transform) { + self.container_transform = + self.container_transform.pre_concat(transform); + } + + /// Creates the [`Transforms`] structure for the current item. + pub fn transforms(&self, size: Size, pos: typst_library::layout::Point) -> Transforms { + Transforms { + transform: self.transform.pre_concat(typst_library::layout::Transform::translate(pos.x, pos.y)), + container_transform: self.container_transform, + container_size: self.size, + size, + } + } +} + +struct FrameContext { + states: Vec +} + +impl FrameContext { + pub fn new(size: Size) -> Self { + Self { + states: vec![State::new(size)], + } + } + + pub fn push(&mut self) { + self.states.push(self.states.last().unwrap().clone()); + } + + pub fn pop(&mut self) { + self.states.pop(); + } + + pub fn state(&self) -> &State { + self.states.last().unwrap() + } + + pub fn state_mut(&mut self) -> &State { + self.states.last_mut().unwrap() + } +} + #[derive(TransparentWrapper)] #[repr(transparent)] struct PdfGlyph(Glyph); @@ -56,13 +124,13 @@ impl krilla::font::Glyph for PdfGlyph { } } -pub struct ExportContext { +pub struct GlobalContext { fonts: HashMap, cur_transform: typst_library::layout::Transform, annotations: Vec, } -impl ExportContext { +impl GlobalContext { pub fn new() -> Self { Self { fonts: Default::default(), @@ -75,7 +143,7 @@ impl ExportContext { // TODO: Change rustybuzz cluster behavior so it works with ActualText #[typst_macros::time(name = "write pdf")] -pub fn pdf(typst_document: &Document) -> Vec { +pub fn pdf(typst_document: &PagedDocument) -> Vec { let settings = SerializeSettings { compress_content_streams: true, no_device_cs: false, @@ -88,7 +156,7 @@ pub fn pdf(typst_document: &Document) -> Vec { }; let mut document = krilla::Document::new_with(settings); - let mut context = ExportContext::new(); + let mut context = GlobalContext::new(); for typst_page in &typst_document.pages { let settings = PageSettings::new( @@ -115,10 +183,34 @@ pub fn finish(document: krilla::Document) -> Vec { document.finish().unwrap() } +pub fn process_frame(frame: &Frame, fill: Option, surface: &mut Surface, gc: &mut GlobalContext) { + let mut fc = FrameContext::new(frame.size()); + + for (point, item) in frame.items() { + surface.push_transform(&Transform::from_translate( + point.x.to_f32(), + point.y.to_f32(), + )); + + match item { + FrameItem::Group(g) => handle_group(g, surface, gc), + FrameItem::Text(t) => handle_text(t, surface, gc), + FrameItem::Shape(s, _) => handle_shape(s, surface), + FrameItem::Image(image, size, span) => { + handle_image(image, *size, surface, gc) + } + FrameItem::Link(d, s) => handle_link(*point, d, *s, gc, surface), + FrameItem::Tag(_) => {} + } + + surface.pop(); + } +} + pub fn handle_group( group: &GroupItem, surface: &mut Surface, - context: &mut ExportContext, + context: &mut GlobalContext, ) { let old = context.cur_transform; context.cur_transform = context.cur_transform.pre_concat(group.transform); @@ -130,7 +222,7 @@ pub fn handle_group( surface.pop(); } -pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut ExportContext) { +pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut GlobalContext) { let font = context .fonts .entry(t.font.clone()) @@ -175,7 +267,7 @@ pub fn handle_image( image: &Image, size: Size, surface: &mut Surface, - _: &mut ExportContext, + _: &mut GlobalContext, ) { match image.kind() { ImageKind::Raster(raster) => { @@ -189,7 +281,7 @@ pub fn handle_image( } } -pub fn handle_shape(shape: &Shape, surface: &mut Surface) { +pub fn handle_shape(fc: &FrameContext, pos: Point, shape: &Shape, surface: &mut Surface) { let mut path_builder = PathBuilder::new(); match &shape.geometry { @@ -207,17 +299,31 @@ pub fn handle_shape(shape: &Shape, surface: &mut Surface) { } } + surface.push_transform(&fc.state().transform.as_krilla()); + surface.push_transform(&Transform::from_translate(pos.x, pos.y)); + if let Some(path) = path_builder.finish() { if let Some(paint) = &shape.fill { let fill = paint::fill(paint, shape.fill_rule); surface.fill_path(&path, fill); } - if let Some(stroke) = &shape.stroke { + 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(stroke); surface.stroke_path(&path, stroke); } } + + surface.pop(); + surface.pop(); } pub fn convert_path(path: &Path, builder: &mut PathBuilder) { @@ -238,33 +344,11 @@ pub fn convert_path(path: &Path, builder: &mut PathBuilder) { } } -pub fn process_frame(frame: &Frame, surface: &mut Surface, context: &mut ExportContext) { - for (point, item) in frame.items() { - surface.push_transform(&Transform::from_translate( - point.x.to_f32(), - point.y.to_f32(), - )); - - match item { - FrameItem::Group(g) => handle_group(g, surface, context), - FrameItem::Text(t) => handle_text(t, surface, context), - FrameItem::Shape(s, _) => handle_shape(s, surface), - FrameItem::Image(image, size, span) => { - handle_image(image, *size, surface, context) - } - FrameItem::Link(d, s) => handle_link(*point, d, *s, context, surface), - FrameItem::Tag(_) => {} - } - - surface.pop(); - } -} - fn handle_link( pos: typst_library::layout::Point, dest: &Destination, size: typst_library::layout::Size, - ctx: &mut ExportContext, + ctx: &mut GlobalContext, surface: &mut Surface, ) { let mut min_x = Abs::inf(); diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 2477f3123..a990cbee2 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -28,8 +28,7 @@ use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr}; use serde::{Deserialize, Serialize}; use typst_library::diag::{bail, SourceResult, StrResult}; use typst_library::foundations::{Datetime, Smart}; -use typst_library::layout::{Abs, Em, PageRanges, Transform}; -use typst_library::model::Document; +use typst_library::layout::{Abs, Em, PageRanges, PagedDocument, Transform}; use typst_library::text::Font; use typst_library::visualize::Image; use typst_syntax::Span; @@ -53,7 +52,7 @@ use crate::resources_old::{ /// /// Returns the raw bytes making up the PDF file. #[typst_macros::time(name = "pdf")] -pub fn pdf(document: &Document, options: &PdfOptions) -> SourceResult> { +pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult> { return Ok(krilla::pdf(document)); PdfBuilder::new(document, options) .phase(|builder| builder.run(traverse_pages))? @@ -181,7 +180,7 @@ struct PdfBuilder { /// this phase. struct WithDocument<'a> { /// The Typst document that is exported. - document: &'a Document, + document: &'a PagedDocument, /// Settings for PDF export. options: &'a PdfOptions<'a>, } @@ -191,7 +190,7 @@ struct WithDocument<'a> { /// /// This phase allocates some global references. struct WithResources<'a> { - document: &'a Document, + document: &'a PagedDocument, options: &'a PdfOptions<'a>, /// The content of the pages encoded as PDF content streams. /// @@ -240,7 +239,7 @@ impl<'a> From<(WithDocument<'a>, (Vec>, Resources<()>))> /// We are now writing objects corresponding to resources, and giving them references, /// that will be collected in [`References`]. struct WithGlobalRefs<'a> { - document: &'a Document, + document: &'a PagedDocument, options: &'a PdfOptions<'a>, pages: Vec>, /// Resources are the same as in previous phases, but each dictionary now has a reference. @@ -283,7 +282,7 @@ struct References { /// tree is going to be written, and given a reference. It is also at this point that /// the page contents is actually written. struct WithRefs<'a> { - document: &'a Document, + document: &'a PagedDocument, options: &'a PdfOptions<'a>, globals: GlobalRefs, pages: Vec>, @@ -309,7 +308,7 @@ impl<'a> From<(WithGlobalRefs<'a>, References)> for WithRefs<'a> { /// /// Each sub-resource gets its own isolated resource dictionary. struct WithEverything<'a> { - document: &'a Document, + document: &'a PagedDocument, options: &'a PdfOptions<'a>, globals: GlobalRefs, pages: Vec>, @@ -341,7 +340,7 @@ impl<'a> From<(WithRefs<'a>, Ref)> for WithEverything<'a> { impl<'a> PdfBuilder> { /// Start building a PDF for a Typst document. - fn new(document: &'a Document, options: &'a PdfOptions<'a>) -> Self { + fn new(document: &'a PagedDocument, options: &'a PdfOptions<'a>) -> Self { Self { alloc: Ref::new(1), pdf: Pdf::new(), diff --git a/crates/typst-pdf/src/paint.rs b/crates/typst-pdf/src/paint.rs index ae5d53a4c..41fa48502 100644 --- a/crates/typst-pdf/src/paint.rs +++ b/crates/typst-pdf/src/paint.rs @@ -3,11 +3,13 @@ use std::num::NonZeroUsize; use krilla::geom::NormalizedF32; use krilla::page::{NumberingStyle, PageLabel}; -use typst_library::layout::Abs; +use typst_library::layout::{Abs, Angle, Quadrant, Ratio, Transform}; use typst_library::model::Numbering; -use typst_library::visualize::{ColorSpace, DashPattern, FillRule, FixedStroke, Paint}; - -use crate::AbsExt; +use typst_library::visualize::{ColorSpace, DashPattern, FillRule, FixedStroke, Gradient, Paint, RelativeTo}; +use typst_utils::Numeric; +use crate::{content_old, AbsExt}; +use crate::content_old::Transforms; +use crate::gradient_old::PdfGradient; use crate::primitive::{FillRuleExt, LineCapExt, LineJoinExt}; pub(crate) fn fill(paint_: &Paint, fill_rule_: FillRule) -> krilla::path::Fill { @@ -112,4 +114,69 @@ impl PageLabelExt for PageLabel { fn arabic(number: usize) -> PageLabel { PageLabel::new(Some(NumberingStyle::Arabic), None, NonZeroUsize::new(number)) } +} + +// TODO: Anti-aliasing + +fn convert_gradient( + gradient: &Gradient, + on_text: bool, + mut transforms: Transforms, +) -> usize { + // Edge cases for strokes. + if transforms.size.x.is_zero() { + transforms.size.x = Abs::pt(1.0); + } + + if transforms.size.y.is_zero() { + transforms.size.y = Abs::pt(1.0); + } + let size = match gradient.unwrap_relative(on_text) { + RelativeTo::Self_ => transforms.size, + RelativeTo::Parent => transforms.container_size, + }; + + let rotation = gradient.angle().unwrap_or_else(Angle::zero); + + let transform = match gradient.unwrap_relative(on_text) { + RelativeTo::Self_ => transforms.transform, + RelativeTo::Parent => transforms.container_transform, + }; + + let scale_offset = match gradient { + Gradient::Conic(_) => 4.0_f64, + _ => 1.0, + }; + + let transform = transform + .pre_concat(Transform::translate( + offset_x * scale_offset, + offset_y * scale_offset, + )) + .pre_concat(Transform::scale( + Ratio::new(size.x.to_pt() * scale_offset), + Ratio::new(size.y.to_pt() * scale_offset), + )); + + let angle = Gradient::correct_aspect_ratio(rotation, size.aspect_ratio()); + + match &gradient { + Gradient::Linear(_) => { + let (mut sin, mut cos) = (angle.sin(), angle.cos()); + + // Scale to edges of unit square. + let factor = cos.abs() + sin.abs(); + sin *= factor; + cos *= factor; + + let (x1, y1, x2, y2): (f64, f64, f64, f64) = match angle.quadrant() { + Quadrant::First => (0.0, 0.0, cos, sin), + Quadrant::Second => (1.0, 0.0, cos + 1.0, sin), + Quadrant::Third => (1.0, 1.0, cos + 1.0, sin + 1.0), + Quadrant::Fourth => (0.0, 1.0, cos, sin + 1.0), + }; + } + Gradient::Radial(_) => {} + Gradient::Conic(_) => {} + } } \ No newline at end of file