This commit is contained in:
Laurenz Stampfl 2024-12-14 20:36:57 +01:00
parent 4ad90c1f38
commit e1895fea7b
3 changed files with 205 additions and 55 deletions

View File

@ -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<State>
}
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<Font, krilla::font::Font>,
cur_transform: typst_library::layout::Transform,
annotations: Vec<krilla::annotation::Annotation>,
}
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<u8> {
pub fn pdf(typst_document: &PagedDocument) -> Vec<u8> {
let settings = SerializeSettings {
compress_content_streams: true,
no_device_cs: false,
@ -88,7 +156,7 @@ pub fn pdf(typst_document: &Document) -> Vec<u8> {
};
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<u8> {
document.finish().unwrap()
}
pub fn process_frame(frame: &Frame, fill: Option<Paint>, 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();

View File

@ -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<Vec<u8>> {
pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult<Vec<u8>> {
return Ok(krilla::pdf(document));
PdfBuilder::new(document, options)
.phase(|builder| builder.run(traverse_pages))?
@ -181,7 +180,7 @@ struct PdfBuilder<S> {
/// 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<Option<EncodedPage>>, 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<Option<EncodedPage>>,
/// 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<Option<EncodedPage>>,
@ -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<Option<EncodedPage>>,
@ -341,7 +340,7 @@ impl<'a> From<(WithRefs<'a>, Ref)> for WithEverything<'a> {
impl<'a> PdfBuilder<WithDocument<'a>> {
/// 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(),

View File

@ -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 {
@ -113,3 +115,68 @@ impl PageLabelExt for 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(_) => {}
}
}