From 46846a337e8084acd46c70bccc2fca2659e9fb9a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 8 Nov 2023 14:32:42 +0100 Subject: Extract `typst-pdf` crate --- Cargo.lock | 35 +- Cargo.toml | 5 + crates/typst-cli/Cargo.toml | 1 + crates/typst-cli/src/compile.rs | 2 +- crates/typst-library/src/meta/numbering.rs | 2 +- crates/typst-pdf/Cargo.toml | 34 ++ crates/typst-pdf/src/color.rs | 468 +++++++++++++++ crates/typst-pdf/src/extg.rs | 35 ++ crates/typst-pdf/src/font.rs | 268 +++++++++ crates/typst-pdf/src/gradient.rs | 581 ++++++++++++++++++ crates/typst-pdf/src/icc/sGrey-v4.icc | Bin 0 -> 360 bytes crates/typst-pdf/src/icc/sRGB-v4.icc | Bin 0 -> 480 bytes crates/typst-pdf/src/image.rs | 170 ++++++ crates/typst-pdf/src/lib.rs | 377 ++++++++++++ crates/typst-pdf/src/outline.rs | 191 ++++++ crates/typst-pdf/src/page.rs | 759 ++++++++++++++++++++++++ crates/typst-pdf/src/postscript/hsl.ps | 63 ++ crates/typst-pdf/src/postscript/hsv.ps | 62 ++ crates/typst-pdf/src/postscript/oklab.ps | 78 +++ crates/typst/Cargo.toml | 13 +- crates/typst/src/doc.rs | 40 +- crates/typst/src/eval/library.rs | 7 +- crates/typst/src/eval/mod.rs | 2 +- crates/typst/src/eval/ops.rs | 1 + crates/typst/src/eval/value.rs | 2 +- crates/typst/src/export/mod.rs | 5 - crates/typst/src/export/pdf/color.rs | 468 --------------- crates/typst/src/export/pdf/extg.rs | 35 -- crates/typst/src/export/pdf/font.rs | 268 --------- crates/typst/src/export/pdf/gradient.rs | 582 ------------------ crates/typst/src/export/pdf/icc/sGrey-v4.icc | Bin 360 -> 0 bytes crates/typst/src/export/pdf/icc/sRGB-v4.icc | Bin 480 -> 0 bytes crates/typst/src/export/pdf/image.rs | 170 ------ crates/typst/src/export/pdf/mod.rs | 431 -------------- crates/typst/src/export/pdf/outline.rs | 190 ------ crates/typst/src/export/pdf/page.rs | 752 ----------------------- crates/typst/src/export/pdf/postscript/hsl.ps | 63 -- crates/typst/src/export/pdf/postscript/hsv.ps | 62 -- crates/typst/src/export/pdf/postscript/oklab.ps | 78 --- crates/typst/src/geom/align.rs | 1 + crates/typst/src/geom/em.rs | 1 + crates/typst/src/lib.rs | 1 - crates/typst/src/model/realize.rs | 1 + crates/typst/src/model/selector.rs | 4 +- tests/Cargo.toml | 1 + tests/src/tests.rs | 2 +- 46 files changed, 3174 insertions(+), 3137 deletions(-) create mode 100644 crates/typst-pdf/Cargo.toml create mode 100644 crates/typst-pdf/src/color.rs create mode 100644 crates/typst-pdf/src/extg.rs create mode 100644 crates/typst-pdf/src/font.rs create mode 100644 crates/typst-pdf/src/gradient.rs create mode 100644 crates/typst-pdf/src/icc/sGrey-v4.icc create mode 100644 crates/typst-pdf/src/icc/sRGB-v4.icc create mode 100644 crates/typst-pdf/src/image.rs create mode 100644 crates/typst-pdf/src/lib.rs create mode 100644 crates/typst-pdf/src/outline.rs create mode 100644 crates/typst-pdf/src/page.rs create mode 100644 crates/typst-pdf/src/postscript/hsl.ps create mode 100644 crates/typst-pdf/src/postscript/hsv.ps create mode 100644 crates/typst-pdf/src/postscript/oklab.ps delete mode 100644 crates/typst/src/export/mod.rs delete mode 100644 crates/typst/src/export/pdf/color.rs delete mode 100644 crates/typst/src/export/pdf/extg.rs delete mode 100644 crates/typst/src/export/pdf/font.rs delete mode 100644 crates/typst/src/export/pdf/gradient.rs delete mode 100644 crates/typst/src/export/pdf/icc/sGrey-v4.icc delete mode 100644 crates/typst/src/export/pdf/icc/sRGB-v4.icc delete mode 100644 crates/typst/src/export/pdf/image.rs delete mode 100644 crates/typst/src/export/pdf/mod.rs delete mode 100644 crates/typst/src/export/pdf/outline.rs delete mode 100644 crates/typst/src/export/pdf/page.rs delete mode 100644 crates/typst/src/export/pdf/postscript/hsl.ps delete mode 100644 crates/typst/src/export/pdf/postscript/hsv.ps delete mode 100644 crates/typst/src/export/pdf/postscript/oklab.ps diff --git a/Cargo.lock b/Cargo.lock index 50bb4347..299e3a58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2892,22 +2892,17 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" name = "typst" version = "0.9.0" dependencies = [ - "base64", "bitflags 2.4.1", - "bytemuck", "comemo", "ecow", - "flate2", "fontdb", "image", "indexmap 2.0.2", "kurbo", "lasso", "log", - "miniz_oxide", "once_cell", "palette", - "pdf-writer", "regex", "roxmltree", "rustybuzz", @@ -2915,22 +2910,16 @@ dependencies = [ "siphasher", "smallvec", "stacker", - "subsetter", - "svg2pdf", "time", "toml", "tracing", "ttf-parser", "typst-macros", "typst-syntax", - "unicode-ident", "unicode-math-class", - "unicode-properties", "unicode-segmentation", - "unscanny", "usvg", "wasmi", - "xmp-writer", ] [[package]] @@ -2971,6 +2960,7 @@ dependencies = [ "tracing-subscriber", "typst", "typst-library", + "typst-pdf", "typst-render", "typst-svg", "ureq", @@ -3062,6 +3052,28 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "typst-pdf" +version = "0.9.0" +dependencies = [ + "base64", + "bytemuck", + "comemo", + "ecow", + "image", + "miniz_oxide", + "once_cell", + "pdf-writer", + "subsetter", + "svg2pdf", + "tracing", + "ttf-parser", + "typst", + "unicode-properties", + "unscanny", + "xmp-writer", +] + [[package]] name = "typst-render" version = "0.9.0" @@ -3124,6 +3136,7 @@ dependencies = [ "ttf-parser", "typst", "typst-library", + "typst-pdf", "typst-render", "typst-svg", "unscanny", diff --git a/Cargo.toml b/Cargo.toml index 08987494..b4eaf993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,12 @@ keywords = ["typst"] [workspace.dependencies] typst = { path = "crates/typst" } +typst-cli = { path = "crates/typst-cli" } +typst-docs = { path = "crates/typst-docs" } +typst-ide = { path = "crates/typst-ide" } typst-library = { path = "crates/typst-library" } typst-macros = { path = "crates/typst-macros" } +typst-pdf = { path = "crates/typst-pdf" } typst-render = { path = "crates/typst-render" } typst-svg = { path = "crates/typst-svg" } typst-syntax = { path = "crates/typst-syntax" } @@ -86,6 +90,7 @@ serde_json = "1" serde_yaml = "0.9" siphasher = "0.3" smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] } +stacker = "0.1.15" subsetter = "0.1.1" svg2pdf = "0.9" syn = { version = "2", features = ["full", "extra-traits"] } diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index 6e270bd8..d797555e 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -22,6 +22,7 @@ doc = false [dependencies] typst = { workspace = true } typst-library = { workspace = true } +typst-pdf = { workspace = true } typst-render = { workspace = true } typst-svg = { workspace = true } chrono = { workspace = true } diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 9eec36cb..48d6401c 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -153,7 +153,7 @@ fn export_pdf( world: &SystemWorld, ) -> StrResult<()> { let ident = world.input().to_string_lossy(); - let buffer = typst::export::pdf(document, Some(&ident), now()); + let buffer = typst_pdf::pdf(document, Some(&ident), now()); let output = command.output(); fs::write(output, buffer) .map_err(|err| eco_format!("failed to write PDF file ({err})"))?; diff --git a/crates/typst-library/src/meta/numbering.rs b/crates/typst-library/src/meta/numbering.rs index 7843757c..72906ff6 100644 --- a/crates/typst-library/src/meta/numbering.rs +++ b/crates/typst-library/src/meta/numbering.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese}; use ecow::EcoVec; -use typst::export::{PdfPageLabel, PdfPageLabelStyle}; +use typst::doc::{PdfPageLabel, PdfPageLabelStyle}; use crate::prelude::*; use crate::text::Case; diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml new file mode 100644 index 00000000..74e38a69 --- /dev/null +++ b/crates/typst-pdf/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "typst-pdf" +description = "PDF exporter for Typst." +version.workspace = true +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +categories.workspace = true +keywords.workspace = true + +[lib] +doctest = false +bench = false + +[dependencies] +typst = { workspace = true } +base64 = { workspace = true } +bytemuck = { workspace = true } +comemo = { workspace = true } +ecow = { workspace = true} +image = { workspace = true } +miniz_oxide = { workspace = true } +once_cell = { workspace = true } +pdf-writer = { workspace = true } +subsetter = { workspace = true } +svg2pdf = { workspace = true } +tracing = { workspace = true } +ttf-parser = { workspace = true } +unicode-properties = { workspace = true } +unscanny = { workspace = true } +xmp-writer = { workspace = true } diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs new file mode 100644 index 00000000..80d277ed --- /dev/null +++ b/crates/typst-pdf/src/color.rs @@ -0,0 +1,468 @@ +use once_cell::sync::Lazy; +use pdf_writer::types::DeviceNSubtype; +use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref}; +use typst::geom::{Color, ColorSpace, Paint}; + +use crate::deflate; +use crate::page::{PageContext, Transforms}; + +// The names of the color spaces. +pub const SRGB: Name<'static> = Name(b"srgb"); +pub const D65_GRAY: Name<'static> = Name(b"d65gray"); +pub const OKLAB: Name<'static> = Name(b"oklab"); +pub const HSV: Name<'static> = Name(b"hsv"); +pub const HSL: Name<'static> = Name(b"hsl"); +pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb"); + +// The names of the color components. +const OKLAB_L: Name<'static> = Name(b"L"); +const OKLAB_A: Name<'static> = Name(b"A"); +const OKLAB_B: Name<'static> = Name(b"B"); +const HSV_H: Name<'static> = Name(b"H"); +const HSV_S: Name<'static> = Name(b"S"); +const HSV_V: Name<'static> = Name(b"V"); +const HSL_H: Name<'static> = Name(b"H"); +const HSL_S: Name<'static> = Name(b"S"); +const HSL_L: Name<'static> = Name(b"L"); + +// The ICC profiles. +static SRGB_ICC_DEFLATED: Lazy> = + Lazy::new(|| deflate(include_bytes!("icc/sRGB-v4.icc"))); +static GRAY_ICC_DEFLATED: Lazy> = + Lazy::new(|| deflate(include_bytes!("icc/sGrey-v4.icc"))); + +// The PostScript functions for color spaces. +static OKLAB_DEFLATED: Lazy> = + Lazy::new(|| deflate(minify(include_str!("postscript/oklab.ps")).as_bytes())); +static HSV_DEFLATED: Lazy> = + Lazy::new(|| deflate(minify(include_str!("postscript/hsv.ps")).as_bytes())); +static HSL_DEFLATED: Lazy> = + Lazy::new(|| deflate(minify(include_str!("postscript/hsl.ps")).as_bytes())); + +/// The color spaces present in the PDF document +#[derive(Default)] +pub struct ColorSpaces { + oklab: Option, + srgb: Option, + d65_gray: Option, + hsv: Option, + hsl: Option, + use_linear_rgb: bool, +} + +impl ColorSpaces { + /// Get a reference to the oklab color space. + /// + /// # Warning + /// The A and B components of the color must be offset by +0.4 before being + /// encoded into the PDF file. + pub fn oklab(&mut self, alloc: &mut Ref) -> Ref { + *self.oklab.get_or_insert_with(|| alloc.bump()) + } + + /// Get a reference to the srgb color space. + pub fn srgb(&mut self, alloc: &mut Ref) -> Ref { + *self.srgb.get_or_insert_with(|| alloc.bump()) + } + + /// Get a reference to the gray color space. + pub fn d65_gray(&mut self, alloc: &mut Ref) -> Ref { + *self.d65_gray.get_or_insert_with(|| alloc.bump()) + } + + /// Get a reference to the hsv color space. + /// + /// # Warning + /// The Hue component of the color must be in degrees and must be divided + /// by 360.0 before being encoded into the PDF file. + pub fn hsv(&mut self, alloc: &mut Ref) -> Ref { + *self.hsv.get_or_insert_with(|| alloc.bump()) + } + + /// Get a reference to the hsl color space. + /// + /// # Warning + /// The Hue component of the color must be in degrees and must be divided + /// by 360.0 before being encoded into the PDF file. + pub fn hsl(&mut self, alloc: &mut Ref) -> Ref { + *self.hsl.get_or_insert_with(|| alloc.bump()) + } + + /// Mark linear RGB as used. + pub fn linear_rgb(&mut self) { + self.use_linear_rgb = true; + } + + /// Write the color space on usage. + pub fn write( + &mut self, + color_space: ColorSpace, + writer: writers::ColorSpace, + alloc: &mut Ref, + ) { + match color_space { + ColorSpace::Oklab => { + let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]); + self.write(ColorSpace::LinearRgb, oklab.alternate_color_space(), alloc); + oklab.tint_ref(self.oklab(alloc)); + oklab.attrs().subtype(DeviceNSubtype::DeviceN); + } + ColorSpace::Srgb => writer.icc_based(self.srgb(alloc)), + ColorSpace::D65Gray => writer.icc_based(self.d65_gray(alloc)), + ColorSpace::LinearRgb => { + writer.cal_rgb( + [0.9505, 1.0, 1.0888], + None, + Some([1.0, 1.0, 1.0]), + Some([ + 0.4124, 0.2126, 0.0193, 0.3576, 0.715, 0.1192, 0.1805, 0.0722, + 0.9505, + ]), + ); + } + ColorSpace::Hsl => { + let mut hsl = writer.device_n([HSL_H, HSL_S, HSL_L]); + self.write(ColorSpace::Srgb, hsl.alternate_color_space(), alloc); + hsl.tint_ref(self.hsl(alloc)); + hsl.attrs().subtype(DeviceNSubtype::DeviceN); + } + ColorSpace::Hsv => { + let mut hsv = writer.device_n([HSV_H, HSV_S, HSV_V]); + self.write(ColorSpace::Srgb, hsv.alternate_color_space(), alloc); + hsv.tint_ref(self.hsv(alloc)); + hsv.attrs().subtype(DeviceNSubtype::DeviceN); + } + ColorSpace::Cmyk => writer.device_cmyk(), + } + } + + // Write the color spaces to the PDF file. + pub fn write_color_spaces(&mut self, mut spaces: Dict, alloc: &mut Ref) { + if self.oklab.is_some() { + self.write(ColorSpace::Oklab, spaces.insert(OKLAB).start(), alloc); + } + + if self.srgb.is_some() { + self.write(ColorSpace::Srgb, spaces.insert(SRGB).start(), alloc); + } + + if self.d65_gray.is_some() { + self.write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), alloc); + } + + if self.hsv.is_some() { + self.write(ColorSpace::Hsv, spaces.insert(HSV).start(), alloc); + } + + if self.hsl.is_some() { + self.write(ColorSpace::Hsl, spaces.insert(HSL).start(), alloc); + } + + if self.use_linear_rgb { + self.write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), alloc); + } + } + + /// Write the necessary color spaces functions and ICC profiles to the + /// PDF file. + pub fn write_functions(&self, chunk: &mut Chunk) { + // Write the Oklab function & color space. + if let Some(oklab) = self.oklab { + chunk + .post_script_function(oklab, &OKLAB_DEFLATED) + .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) + .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) + .filter(Filter::FlateDecode); + } + + // Write the HSV function & color space. + if let Some(hsv) = self.hsv { + chunk + .post_script_function(hsv, &HSV_DEFLATED) + .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) + .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) + .filter(Filter::FlateDecode); + } + + // Write the HSL function & color space. + if let Some(hsl) = self.hsl { + chunk + .post_script_function(hsl, &HSL_DEFLATED) + .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) + .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) + .filter(Filter::FlateDecode); + } + + // Write the sRGB color space. + if let Some(srgb) = self.srgb { + chunk + .icc_profile(srgb, &SRGB_ICC_DEFLATED) + .n(3) + .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) + .filter(Filter::FlateDecode); + } + + // Write the gray color space. + if let Some(gray) = self.d65_gray { + chunk + .icc_profile(gray, &GRAY_ICC_DEFLATED) + .n(1) + .range([0.0, 1.0]) + .filter(Filter::FlateDecode); + } + } +} + +/// This function removes comments, line spaces and carriage returns from a +/// PostScript program. This is necessary to optimize the size of the PDF file. +fn minify(source: &str) -> String { + let mut buf = String::with_capacity(source.len()); + let mut s = unscanny::Scanner::new(source); + while let Some(c) = s.eat() { + match c { + '%' => { + s.eat_until('\n'); + } + c if c.is_whitespace() => { + s.eat_whitespace(); + if buf.ends_with(|c: char| !c.is_whitespace()) { + buf.push(' '); + } + } + _ => buf.push(c), + } + } + buf +} + +/// Encodes the color into four f32s, which can be used in a PDF file. +/// Ensures that the values are in the range [0.0, 1.0]. +/// +/// # Why? +/// - Oklab: The a and b components are in the range [-0.4, 0.4] and the PDF +/// specifies (and some readers enforce) that all color values be in the range +/// [0.0, 1.0]. This means that the PostScript function and the encoded color +/// must be offset by 0.4. +/// - HSV/HSL: The hue component is in the range [0.0, 360.0] and the PDF format +/// specifies that it must be in the range [0.0, 1.0]. This means that the +/// PostScript function and the encoded color must be divided by 360.0. +pub trait ColorEncode { + /// Performs the color to PDF f32 array conversion. + fn encode(&self, color: Color) -> [f32; 4]; +} + +impl ColorEncode for ColorSpace { + fn encode(&self, color: Color) -> [f32; 4] { + match self { + ColorSpace::Oklab => { + let [l, a, b, alpha] = color.to_oklab().to_vec4(); + [l, (a + 0.4).clamp(0.0, 1.0), (b + 0.4).clamp(0.0, 1.0), alpha] + } + ColorSpace::Hsl => { + let [h, s, l, _] = color.to_hsl().to_vec4(); + [h / 360.0, s, l, 0.0] + } + ColorSpace::Hsv => { + let [h, s, v, _] = color.to_hsv().to_vec4(); + [h / 360.0, s, v, 0.0] + } + _ => color.to_vec4(), + } + } +} + +/// Encodes a paint into either a fill or stroke color. +pub(super) trait PaintEncode { + /// Set the paint as the fill color. + fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms); + + /// Set the paint as the stroke color. + fn set_as_stroke(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms); +} + +impl PaintEncode for Paint { + fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) { + match self { + Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms), + Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms), + } + } + + fn set_as_stroke( + &self, + ctx: &mut PageContext, + on_text: bool, + transforms: Transforms, + ) { + match self { + Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms), + Self::Gradient(gradient) => gradient.set_as_stroke(ctx, on_text, transforms), + } + } +} + +impl PaintEncode for Color { + fn set_as_fill(&self, ctx: &mut PageContext, _: bool, _: Transforms) { + match self { + Color::Luma(_) => { + ctx.parent.colors.d65_gray(&mut ctx.parent.alloc); + ctx.set_fill_color_space(D65_GRAY); + + let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); + ctx.content.set_fill_color([l]); + } + Color::Oklab(_) => { + ctx.parent.colors.oklab(&mut ctx.parent.alloc); + ctx.set_fill_color_space(OKLAB); + + let [l, a, b, _] = ColorSpace::Oklab.encode(*self); + ctx.content.set_fill_color([l, a, b]); + } + Color::LinearRgb(_) => { + ctx.parent.colors.linear_rgb(); + ctx.set_fill_color_space(LINEAR_SRGB); + + let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); + ctx.content.set_fill_color([r, g, b]); + } + Color::Rgba(_) => { + ctx.parent.colors.srgb(&mut ctx.parent.alloc); + ctx.set_fill_color_space(SRGB); + + let [r, g, b, _] = ColorSpace::Srgb.encode(*self); + ctx.content.set_fill_color([r, g, b]); + } + Color::Cmyk(_) => { + ctx.reset_fill_color_space(); + + let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); + ctx.content.set_fill_cmyk(c, m, y, k); + } + Color::Hsl(_) => { + ctx.parent.colors.hsl(&mut ctx.parent.alloc); + ctx.set_fill_color_space(HSL); + + let [h, s, l, _] = ColorSpace::Hsl.encode(*self); + ctx.content.set_fill_color([h, s, l]); + } + Color::Hsv(_) => { + ctx.parent.colors.hsv(&mut ctx.parent.alloc); + ctx.set_fill_color_space(HSV); + + let [h, s, v, _] = ColorSpace::Hsv.encode(*self); + ctx.content.set_fill_color([h, s, v]); + } + } + } + + fn set_as_stroke(&self, ctx: &mut PageContext, _: bool, _: Transforms) { + match self { + Color::Luma(_) => { + ctx.parent.colors.d65_gray(&mut ctx.parent.alloc); + ctx.set_stroke_color_space(D65_GRAY); + + let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); + ctx.content.set_stroke_color([l]); + } + Color::Oklab(_) => { + ctx.parent.colors.oklab(&mut ctx.parent.alloc); + ctx.set_stroke_color_space(OKLAB); + + let [l, a, b, _] = ColorSpace::Oklab.encode(*self); + ctx.content.set_stroke_color([l, a, b]); + } + Color::LinearRgb(_) => { + ctx.parent.colors.linear_rgb(); + ctx.set_stroke_color_space(LINEAR_SRGB); + + let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); + ctx.content.set_stroke_color([r, g, b]); + } + Color::Rgba(_) => { + ctx.parent.colors.srgb(&mut ctx.parent.alloc); + ctx.set_stroke_color_space(SRGB); + + let [r, g, b, _] = ColorSpace::Srgb.encode(*self); + ctx.content.set_stroke_color([r, g, b]); + } + Color::Cmyk(_) => { + ctx.reset_stroke_color_space(); + + let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); + ctx.content.set_stroke_cmyk(c, m, y, k); + } + Color::Hsl(_) => { + ctx.parent.colors.hsl(&mut ctx.parent.alloc); + ctx.set_stroke_color_space(HSL); + + let [h, s, l, _] = ColorSpace::Hsl.encode(*self); + ctx.content.set_stroke_color([h, s, l]); + } + Color::Hsv(_) => { + ctx.parent.colors.hsv(&mut ctx.parent.alloc); + ctx.set_stroke_color_space(HSV); + + let [h, s, v, _] = ColorSpace::Hsv.encode(*self); + ctx.content.set_stroke_color([h, s, v]); + } + } + } +} + +/// Extra color space functions. +pub(super) trait ColorSpaceExt { + /// Returns the range of the color space. + fn range(self) -> [f32; 6]; + + /// Converts a color to the color space. + fn convert(self, color: Color) -> [U; 3]; +} + +impl ColorSpaceExt for ColorSpace { + fn range(self) -> [f32; 6] { + [0.0, 1.0, 0.0, 1.0, 0.0, 1.0] + } + + fn convert(self, color: Color) -> [U; 3] { + let range = self.range(); + let [x, y, z, _] = color.to_space(self).to_vec4(); + + // We need to add 0.4 to y and z for Oklab + // This is because DeviceN color spaces in PDF can **only** be in + // the range 0..1 and some readers enforce that. + // The oklab color space is in the range -0.4..0.4 + // Also map the angle range of HSV/HSL to 0..1 instead of 0..360 + let [x, y, z] = match self { + Self::Oklab => [x, y + 0.4, z + 0.4], + Self::Hsv | Self::Hsl => [x / 360.0, y, z], + _ => [x, y, z], + }; + + [ + U::quantize(x, [range[0], range[1]]), + U::quantize(y, [range[2], range[3]]), + U::quantize(z, [range[4], range[5]]), + ] + } +} + +/// Quantizes a color component to a specific type. +pub(super) trait QuantizedColor { + fn quantize(color: f32, range: [f32; 2]) -> Self; +} + +impl QuantizedColor for u16 { + fn quantize(color: f32, range: [f32; 2]) -> Self { + let value = (color - range[0]) / (range[1] - range[0]); + (value.max(0.0).min(1.0) * Self::MAX as f32) + .round() + .max(0.0) + .min(Self::MAX as f32) as Self + } +} + +impl QuantizedColor for f32 { + fn quantize(color: f32, [min, max]: [f32; 2]) -> Self { + color.clamp(min, max) + } +} diff --git a/crates/typst-pdf/src/extg.rs b/crates/typst-pdf/src/extg.rs new file mode 100644 index 00000000..f7bd116d --- /dev/null +++ b/crates/typst-pdf/src/extg.rs @@ -0,0 +1,35 @@ +use super::PdfContext; + +/// A PDF external graphics state. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct ExtGState { + // In the range 0-255, needs to be divided before being written into the graphics state! + pub stroke_opacity: u8, + // In the range 0-255, needs to be divided before being written into the graphics state! + pub fill_opacity: u8, +} + +impl Default for ExtGState { + fn default() -> Self { + Self { stroke_opacity: 255, fill_opacity: 255 } + } +} + +impl ExtGState { + pub fn uses_opacities(&self) -> bool { + self.stroke_opacity != 255 || self.fill_opacity != 255 + } +} + +/// Embed all used external graphics states into the PDF. +#[tracing::instrument(skip_all)] +pub fn write_external_graphics_states(ctx: &mut PdfContext) { + for external_gs in ctx.extg_map.items() { + let id = ctx.alloc.bump(); + ctx.ext_gs_refs.push(id); + ctx.pdf + .ext_graphics(id) + .non_stroking_alpha(external_gs.fill_opacity as f32 / 255.0) + .stroking_alpha(external_gs.stroke_opacity as f32 / 255.0); + } +} diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs new file mode 100644 index 00000000..ccf2f403 --- /dev/null +++ b/crates/typst-pdf/src/font.rs @@ -0,0 +1,268 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; +use pdf_writer::{Filter, Finish, Name, Rect, Str}; +use ttf_parser::{name_id, GlyphId, Tag}; +use typst::font::Font; +use typst::util::SliceExt; +use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; + +use crate::{deflate, EmExt, PdfContext}; + +const CFF: Tag = Tag::from_bytes(b"CFF "); +const CFF2: Tag = Tag::from_bytes(b"CFF2"); +const CMAP_NAME: Name = Name(b"Custom"); +const SYSTEM_INFO: SystemInfo = SystemInfo { + registry: Str(b"Adobe"), + ordering: Str(b"Identity"), + supplement: 0, +}; + +/// Embed all used fonts into the PDF. +#[tracing::instrument(skip_all)] +pub fn write_fonts(ctx: &mut PdfContext) { + for font in ctx.font_map.items() { + let type0_ref = ctx.alloc.bump(); + let cid_ref = ctx.alloc.bump(); + let descriptor_ref = ctx.alloc.bump(); + let cmap_ref = ctx.alloc.bump(); + let data_ref = ctx.alloc.bump(); + ctx.font_refs.push(type0_ref); + + let glyph_set = ctx.glyph_sets.get_mut(font).unwrap(); + let metrics = font.metrics(); + let ttf = font.ttf(); + + // Do we have a TrueType or CFF font? + // + // FIXME: CFF2 must be handled differently and requires PDF 2.0 + // (or we have to convert it to CFF). + let is_cff = ttf + .raw_face() + .table(CFF) + .or_else(|| ttf.raw_face().table(CFF2)) + .is_some(); + + let postscript_name = font + .find_name(name_id::POST_SCRIPT_NAME) + .unwrap_or_else(|| "unknown".to_string()); + + let subset_tag = subset_tag(glyph_set); + let base_font = eco_format!("{subset_tag}+{postscript_name}"); + let base_font_type0 = if is_cff { + eco_format!("{base_font}-Identity-H") + } else { + base_font.clone() + }; + + // Write the base font object referencing the CID font. + ctx.pdf + .type0_font(type0_ref) + .base_font(Name(base_font_type0.as_bytes())) + .encoding_predefined(Name(b"Identity-H")) + .descendant_font(cid_ref) + .to_unicode(cmap_ref); + + // Write the CID font referencing the font descriptor. + let mut cid = ctx.pdf.cid_font(cid_ref); + cid.subtype(if is_cff { CidFontType::Type0 } else { CidFontType::Type2 }); + cid.base_font(Name(base_font.as_bytes())); + cid.system_info(SYSTEM_INFO); + cid.font_descriptor(descriptor_ref); + cid.default_width(0.0); + if !is_cff { + cid.cid_to_gid_map_predefined(Name(b"Identity")); + } + + // Extract the widths of all glyphs. + let mut widths = vec![]; + for gid in std::iter::once(0).chain(glyph_set.keys().copied()) { + let width = ttf.glyph_hor_advance(GlyphId(gid)).unwrap_or(0); + let units = font.to_em(width).to_font_units(); + let cid = glyph_cid(font, gid); + if usize::from(cid) >= widths.len() { + widths.resize(usize::from(cid) + 1, 0.0); + widths[usize::from(cid)] = units; + } + } + + // Write all non-zero glyph widths. + let mut first = 0; + let mut width_writer = cid.widths(); + for (w, group) in widths.group_by_key(|&w| w) { + let end = first + group.len(); + if w != 0.0 { + let last = end - 1; + width_writer.same(first as u16, last as u16, w); + } + first = end; + } + + width_writer.finish(); + cid.finish(); + + let mut flags = FontFlags::empty(); + flags.set(FontFlags::SERIF, postscript_name.contains("Serif")); + flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); + flags.set(FontFlags::ITALIC, ttf.is_italic()); + flags.insert(FontFlags::SYMBOLIC); + flags.insert(FontFlags::SMALL_CAP); + + let global_bbox = ttf.global_bounding_box(); + let bbox = Rect::new( + font.to_em(global_bbox.x_min).to_font_units(), + font.to_em(global_bbox.y_min).to_font_units(), + font.to_em(global_bbox.x_max).to_font_units(), + font.to_em(global_bbox.y_max).to_font_units(), + ); + + let italic_angle = ttf.italic_angle().unwrap_or(0.0); + let ascender = metrics.ascender.to_font_units(); + let descender = metrics.descender.to_font_units(); + let cap_height = metrics.cap_height.to_font_units(); + let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); + + // Write the font descriptor (contains metrics about the font). + let mut font_descriptor = ctx.pdf.font_descriptor(descriptor_ref); + font_descriptor + .name(Name(base_font.as_bytes())) + .flags(flags) + .bbox(bbox) + .italic_angle(italic_angle) + .ascent(ascender) + .descent(descender) + .cap_height(cap_height) + .stem_v(stem_v); + + if is_cff { + font_descriptor.font_file3(data_ref); + } else { + font_descriptor.font_file2(data_ref); + } + + font_descriptor.finish(); + + // Write the /ToUnicode character map, which maps glyph ids back to + // unicode codepoints to enable copying out of the PDF. + let cmap = create_cmap(ttf, glyph_set); + ctx.pdf.cmap(cmap_ref, &cmap.finish()); + + // Subset and write the font's bytes. + let glyphs: Vec<_> = glyph_set.keys().copied().collect(); + let data = subset_font(font, &glyphs); + + let mut stream = ctx.pdf.stream(data_ref, &data); + stream.filter(Filter::FlateDecode); + if is_cff { + stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C")); + } + + stream.finish(); + } +} + +/// Subset a font to the given glyphs. +/// +/// - For a font with TrueType outlines, this returns the whole OpenType font. +/// - For a font with CFF outlines, this returns just the CFF font program. +#[comemo::memoize] +fn subset_font(font: &Font, glyphs: &[u16]) -> Arc> { + let data = font.data(); + let profile = subsetter::Profile::pdf(glyphs); + let subsetted = subsetter::subset(data, font.index(), profile); + let mut data = subsetted.as_deref().unwrap_or(data); + + // Extract the standalone CFF font program if applicable. + let raw = ttf_parser::RawFace::parse(data, 0).unwrap(); + if let Some(cff) = raw.table(CFF) { + data = cff; + } + + Arc::new(deflate(data)) +} + +/// Produce a unique 6 letter tag for a glyph set. +fn subset_tag(glyphs: &BTreeMap) -> EcoString { + const LEN: usize = 6; + const BASE: u128 = 26; + let mut hash = typst::util::hash128(&glyphs); + let mut letter = [b'A'; LEN]; + for l in letter.iter_mut() { + *l = b'A' + (hash % BASE) as u8; + hash /= BASE; + } + std::str::from_utf8(&letter).unwrap().into() +} + +/// Create a /ToUnicode CMap. +fn create_cmap( + ttf: &ttf_parser::Face, + glyph_set: &mut BTreeMap, +) -> UnicodeCmap { + // For glyphs that have codepoints mapping to them in the font's cmap table, + // we prefer them over pre-existing text mappings from the document. Only + // things that don't have a corresponding codepoint (or only a private-use + // one) like the "Th" in Linux Libertine get the text of their first + // occurrences in the document instead. + for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) { + if !subtable.is_unicode() { + continue; + } + + subtable.codepoints(|n| { + let Some(c) = std::char::from_u32(n) else { return }; + if c.general_category() == GeneralCategory::PrivateUse { + return; + } + + let Some(GlyphId(g)) = ttf.glyph_index(c) else { return }; + if glyph_set.contains_key(&g) { + glyph_set.insert(g, c.into()); + } + }); + } + + // Produce a reverse mapping from glyphs to unicode strings. + let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); + for (&g, text) in glyph_set.iter() { + if !text.is_empty() { + cmap.pair_with_multiple(g, text.chars()); + } + } + + cmap +} + +/// Get the CID for a glyph id. +/// +/// When writing text into a PDF, we have to specify CIDs (character ids) not +/// GIDs (glyph IDs). +/// +/// Most of the time, the mapping between these two is an identity mapping. In +/// particular, for TrueType fonts, the mapping is an identity mapping because +/// of this line above: +/// ```ignore +/// cid.cid_to_gid_map_predefined(Name(b"Identity")); +/// ``` +/// +/// However, CID-keyed CFF fonts may have a non-identity mapping defined in +/// their charset. For those, we must map the glyph IDs in a `TextItem` to CIDs. +/// The font defines the map through its charset. The charset usually maps +/// glyphs to SIDs (string ids) specifying the glyph's name. Not for CID-keyed +/// fonts though! For these, the SIDs are CIDs in disguise. Relevant quote from +/// the CFF spec: +/// +/// > The charset data, although in the same format as non-CIDFonts, will +/// > represent CIDs rather than SIDs, [...] +/// +/// This function performs the mapping from glyph ID to CID. It also works for +/// non CID-keyed fonts. Then, it will simply return the glyph ID. +pub(super) fn glyph_cid(font: &Font, glyph_id: u16) -> u16 { + font.ttf() + .tables() + .cff + .and_then(|cff| cff.glyph_cid(ttf_parser::GlyphId(glyph_id))) + .unwrap_or(glyph_id) +} diff --git a/crates/typst-pdf/src/gradient.rs b/crates/typst-pdf/src/gradient.rs new file mode 100644 index 00000000..e5cae30e --- /dev/null +++ b/crates/typst-pdf/src/gradient.rs @@ -0,0 +1,581 @@ +use std::f32::consts::{PI, TAU}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use pdf_writer::types::FunctionShadingType; +use pdf_writer::writers::StreamShadingType; +use pdf_writer::{types::ColorSpaceOperand, Name}; +use pdf_writer::{Filter, Finish, Ref}; +use typst::geom::{ + Abs, Angle, Color, ColorSpace, ConicGradient, Gradient, Numeric, Point, Quadrant, + Ratio, Relative, Transform, WeightedColor, +}; + +use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor}; +use crate::page::{PageContext, Transforms}; +use crate::{deflate, AbsExt, PdfContext}; + +/// A unique-transform-aspect-ratio combination that will be encoded into the +/// PDF. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct PdfGradient { + /// The transform to apply to the gradient. + pub transform: Transform, + /// The aspect ratio of the gradient. + /// Required for aspect ratio correction. + pub aspect_ratio: Ratio, + /// The gradient. + pub gradient: Gradient, + /// Whether the gradient is applied to text. + pub on_text: bool, +} + +/// Writes the actual gradients (shading patterns) to the PDF. +/// This is performed once after writing all pages. +pub fn write_gradients(ctx: &mut PdfContext) { + for PdfGradient { transform, aspect_ratio, gradient, on_text } in + ctx.gradient_map.items().cloned().collect::>() + { + let shading = ctx.alloc.bump(); + ctx.gradient_refs.push(shading); + + let mut shading_pattern = match &gradient { + Gradient::Linear(linear) => { + let shading_function = shading_function(ctx, &gradient); + let mut shading_pattern = ctx.pdf.shading_pattern(shading); + let mut shading = shading_pattern.function_shading(); + shading.shading_type(FunctionShadingType::Axial); + + ctx.colors + .write(gradient.space(), shading.color_space(), &mut ctx.alloc); + + let angle = Gradient::correct_aspect_ratio(linear.angle, aspect_ratio); + let (sin, cos) = (angle.sin(), angle.cos()); + let length = sin.abs() + cos.abs(); + + shading + .anti_alias(gradient.anti_alias()) + .function(shading_function) + .coords([0.0, 0.0, length as f32, 0.0]) + .extend([true; 2]); + + shading.finish(); + + shading_pattern + } + Gradient::Radial(radial) => { + let shading_function = shading_function(ctx, &gradient); + let mut shading_pattern = ctx.pdf.shading_pattern(shading); + let mut shading = shading_pattern.function_shading(); + shading.shading_type(FunctionShadingType::Radial); + + ctx.colors + .write(gradient.space(), shading.color_space(), &mut ctx.alloc); + + shading + .anti_alias(gradient.anti_alias()) + .function(shading_function) + .coords([ + radial.focal_center.x.get() as f32, + radial.focal_center.y.get() as f32, + radial.focal_radius.get() as f32, + radial.center.x.get() as f32, + radial.center.y.get() as f32, + radial.radius.get() as f32, + ]) + .extend([true; 2]); + + shading.finish(); + + shading_pattern + } + Gradient::Conic(conic) => { + let vertices = compute_vertex_stream(conic, aspect_ratio, on_text); + + let stream_shading_id = ctx.alloc.bump(); + let mut stream_shading = + ctx.pdf.stream_shading(stream_shading_id, &vertices); + + ctx.colors.write( + conic.space, + stream_shading.color_space(), + &mut ctx.alloc, + ); + + let range = conic.space.range(); + stream_shading + .bits_per_coordinate(16) + .bits_per_component(16) + .bits_per_flag(8) + .shading_type(StreamShadingType::CoonsPatch) + .decode([ + 0.0, 1.0, 0.0, 1.0, range[0], range[1], range[2], range[3], + range[4], range[5], + ]) + .anti_alias(gradient.anti_alias()) + .filter(Filter::FlateDecode); + + stream_shading.finish(); + + let mut shading_pattern = ctx.pdf.shading_pattern(shading); + shading_pattern.shading_ref(stream_shading_id); + shading_pattern + } + }; + + shading_pattern.matrix(transform_to_array(transform)); + } +} + +/// Writes an expotential or stitched function that expresses the gradient. +fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref { + let function = ctx.alloc.bump(); + let mut functions = vec![]; + let mut bounds = vec![]; + let mut encode = vec![]; + + // Create the individual gradient functions for each pair of stops. + for window in gradient.stops_ref().windows(2) { + let (first, second) = (window[0], window[1]); + + // Skip stops with the same position. + if first.1.get() == second.1.get() { + continue; + } + + // If the color space is HSL or HSV, and we cross the 0°/360° boundary, + // we need to create two separate stops. + if gradient.space() == ColorSpace::Hsl || gradient.space() == ColorSpace::Hsv { + let t1 = first.1.get() as f32; + let t2 = second.1.get() as f32; + let [h1, s1, x1, _] = first.0.to_space(gradient.space()).to_vec4(); + let [h2, s2, x2, _] = second.0.to_space(gradient.space()).to_vec4(); + + // Compute the intermediary stop at 360°. + if (h1 - h2).abs() > 180.0 { + let h1 = if h1 < h2 { h1 + 360.0 } else { h1 }; + let h2 = if h2 < h1 { h2 + 360.0 } else { h2 }; + + // We compute where the crossing happens between zero and one + let t = (360.0 - h1) / (h2 - h1); + // We then map it back to the original range. + let t_prime = t * (t2 - t1) + t1; + + // If the crossing happens between the two stops, + // we need to create an extra stop. + if t_prime <= t2 && t_prime >= t1 { + bounds.push(t_prime); + bounds.push(t_prime); + bounds.push(t2); + encode.extend([0.0, 1.0]); + encode.extend([0.0, 1.0]); + encode.extend([0.0, 1.0]); + + // These need to be individual function to encode 360.0 correctly. + let func1 = ctx.alloc.bump(); + ctx.pdf + .exponential_function(func1) + .range(gradient.space().range()) + .c0(gradient.space().convert(first.0)) + .c1([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) + .domain([0.0, 1.0]) + .n(1.0); + + let func2 = ctx.alloc.bump(); + ctx.pdf + .exponential_function(func2) + .range(gradient.space().range()) + .c0([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) + .c1([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) + .domain([0.0, 1.0]) + .n(1.0); + + let func3 = ctx.alloc.bump(); + ctx.pdf + .exponential_function(func3) + .range(gradient.space().range()) + .c0([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) + .c1(gradient.space().convert(second.0)) + .domain([0.0, 1.0]) + .n(1.0); + + functions.push(func1); + functions.push(func2); + functions.push(func3); + + continue; + } + } + } + + bounds.push(second.1.get() as f32); + functions.push(single_gradient(ctx, first.0, second.0, gradient.space())); + encode.extend([0.0, 1.0]); + } + + // Special case for gradients with only two stops. + if functions.len() == 1 { + return functions[0]; + } + + // Remove the last bound, since it's not needed for the stitching function. + bounds.pop(); + + // Create the stitching function. + ctx.pdf + .stitching_function(function) + .domain([0.0, 1.0]) + .range(gradient.space().range()) + .functions(functions) + .bounds(bounds) + .encode(encode); + + function +} + +/// Writes an expontential function that expresses a single segment (between two +/// stops) of a gradient. +fn single_gradient( + ctx: &mut PdfContext, + first_color: Color, + second_color: Color, + color_space: ColorSpace, +) -> Ref { + let reference = ctx.alloc.bump(); + + ctx.pdf + .exponential_function(reference) + .range(color_space.range()) + .c0(color_space.convert(first_color)) + .c1(color_space.convert(second_color)) + .domain([0.0, 1.0]) + .n(1.0); + + reference +} + +impl PaintEncode for Gradient { + fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) { + ctx.reset_fill_color_space(); + + let id = register_gradient(ctx, self, on_text, transforms); + let name = Name(id.as_bytes()); + + ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); + ctx.content.set_fill_pattern(None, name); + } + + fn set_as_stroke( + &self, + ctx: &mut PageContext, + on_text: bool, + transforms: Transforms, + ) { + ctx.reset_stroke_color_space(); + + let id = register_gradient(ctx, self, on_text, transforms); + let name = Name(id.as_bytes()); + + ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); + ctx.content.set_stroke_pattern(None, name); + } +} + +/// Deduplicates a gradient to a named PDF resource. +fn register_gradient( + ctx: &mut PageContext, + gradient: &Gradient, + on_text: bool, + mut transforms: Transforms, +) -> EcoString { + // 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) { + Relative::Self_ => transforms.size, + Relative::Parent => transforms.container_size, + }; + + // Correction for y-axis flipping on text. + let angle = gradient.angle().unwrap_or_else(Angle::zero); + let angle = if on_text { Angle::rad(TAU as f64) - angle } else { angle }; + + let (offset_x, offset_y) = match gradient { + Gradient::Conic(conic) => ( + -size.x * (1.0 - conic.center.x.get() / 2.0) / 2.0, + -size.y * (1.0 - conic.center.y.get() / 2.0) / 2.0, + ), + _ => match angle.quadrant() { + Quadrant::First => (Abs::zero(), Abs::zero()), + Quadrant::Second => (size.x, Abs::zero()), + Quadrant::Third => (size.x, size.y), + Quadrant::Fourth => (Abs::zero(), size.y), + }, + }; + + let rotation = match gradient { + Gradient::Conic(_) => Angle::zero(), + _ => angle, + }; + + let transform = match gradient.unwrap_relative(on_text) { + Relative::Self_ => transforms.transform, + Relative::Parent => transforms.container_transform, + }; + + let scale_offset = match gradient { + Gradient::Conic(_) => 4.0_f64, + _ => 1.0, + }; + + let pdf_gradient = PdfGradient { + aspect_ratio: size.aspect_ratio(), + 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), + )) + .pre_concat(Transform::rotate(Gradient::correct_aspect_ratio( + rotation, + size.aspect_ratio(), + ))), + gradient: gradient.clone(), + on_text, + }; + + let index = ctx.parent.gradient_map.insert(pdf_gradient); + eco_format!("Gr{}", index) +} + +/// Convert to an array of floats. +fn transform_to_array(ts: Transform) -> [f32; 6] { + [ + ts.sx.get() as f32, + ts.ky.get() as f32, + ts.kx.get() as f32, + ts.sy.get() as f32, + ts.tx.to_f32(), + ts.ty.to_f32(), + ] +} + +/// Writes a single Coons Patch as defined in the PDF specification +/// to a binary vec. +/// +/// Structure: +/// - flag: `u8` +/// - points: `[u16; 24]` +/// - colors: `[u16; 12]` +fn write_patch( + target: &mut Vec, + t: f32, + t1: f32, + c0: [u16; 3], + c1: [u16; 3], + angle: Angle, + on_text: bool, +) { + let mut theta = -TAU * t + angle.to_rad() as f32 + PI; + let mut theta1 = -TAU * t1 + angle.to_rad() as f32 + PI; + + // Correction for y-axis flipping on text. + if on_text { + theta = (TAU - theta).rem_euclid(TAU); + theta1 = (TAU - theta1).rem_euclid(TAU); + } + + let (cp1, cp2) = + control_point(Point::new(Abs::pt(0.5), Abs::pt(0.5)), 0.5, theta, theta1); + + // Push the flag + target.push(0); + + let p1 = + [u16::quantize(0.5, [0.0, 1.0]).to_be(), u16::quantize(0.5, [0.0, 1.0]).to_be()]; + + let p2 = [ + u16::quantize(theta.cos(), [-1.0, 1.0]).to_be(), + u16::quantize(theta.sin(), [-1.0, 1.0]).to_be(), + ]; + + let p3 = [ + u16::quantize(theta1.cos(), [-1.0, 1.0]).to_be(), + u16::quantize(theta1.sin(), [-1.0, 1.0]).to_be(), + ]; + + let cp1 = [ + u16::quantize(cp1.x.to_f32(), [0.0, 1.0]).to_be(), + u16::quantize(cp1.y.to_f32(), [0.0, 1.0]).to_be(), + ]; + + let cp2 = [ + u16::quantize(cp2.x.to_f32(), [0.0, 1.0]).to_be(), + u16::quantize(cp2.y.to_f32(), [0.0, 1.0]).to_be(), + ]; + + // Push the points + target.extend_from_slice(bytemuck::cast_slice(&[ + p1, p1, p2, p2, cp1, cp2, p3, p3, p1, p1, p1, p1, + ])); + + let colors = + [c0.map(u16::to_be), c0.map(u16::to_be), c1.map(u16::to_be), c1.map(u16::to_be)]; + + // Push the colors. + target.extend_from_slice(bytemuck::cast_slice(&colors)); +} + +fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point, Point) { + let n = (TAU / (angle_end - angle_start)).abs(); + let f = ((angle_end - angle_start) / n).tan() * 4.0 / 3.0; + + let p1 = c + Point::new( + Abs::pt((r * angle_start.cos() - f * r * angle_start.sin()) as f64), + Abs::pt((r * angle_start.sin() + f * r * angle_start.cos()) as f64), + ); + + let p2 = c + Point::new( + Abs::pt((r * angle_end.cos() + f * r * angle_end.sin()) as f64), + Abs::pt((r * angle_end.sin() - f * r * angle_end.cos()) as f64), + ); + + (p1, p2) +} + +#[comemo::memoize] +fn compute_vertex_stream( + conic: &ConicGradient, + aspect_ratio: Ratio, + on_text: bool, +) -> Arc> { + // Generated vertices for the Coons patches + let mut vertices = Vec::new(); + + // Correct the gradient's angle + let angle = Gradient::correct_aspect_ratio(conic.angle, aspect_ratio); + + // We want to generate a vertex based on some conditions, either: + // - At the boundary of a stop + // - At the boundary of a quadrant + // - When we cross the boundary of a hue turn (for HSV and HSL only) + for window in conic.stops.windows(2) { + let ((c0, t0), (c1, t1)) = (window[0], window[1]); + + // Skip stops with the same position + if t0 == t1 { + continue; + } + + // If the angle between the two stops is greater than 90 degrees, we need to + // generate a vertex at the boundary of the quadrant. + // However, we add more stops in-between to make the gradient smoother, so we + // need to generate a vertex at least every 5 degrees. + // If the colors are the same, we do it every quadrant only. + let slope = 1.0 / (t1.get() - t0.get()); + let mut t_x = t0.get(); + let dt = (t1.get() - t0.get()).min(0.25); + while t_x < t1.get() { + let t_next = (t_x + dt).min(t1.get()); + + let t1 = slope * (t_x - t0.get()); + let t2 = slope * (t_next - t0.get()); + + // We don't use `Gradient::sample` to avoid issues with sharp gradients. + let c = Color::mix_iter( + [WeightedColor::new(c0, 1.0 - t1), WeightedColor::new(c1, t1)], + conic.space, + ) + .unwrap(); + + let c_next = Color::mix_iter( + [WeightedColor::new(c0, 1.0 - t2), WeightedColor::new(c1, t2)], + conic.space, + ) + .unwrap(); + + // If the color space is HSL or HSV, and we cross the 0°/360° boundary, + // we need to create two separate stops. + if conic.space == ColorSpace::Hsl || conic.space == ColorSpace::Hsv { + let [h1, s1, x1, _] = c.to_space(conic.space).to_vec4(); + let [h2, s2, x2, _] = c_next.to_space(conic.space).to_vec4(); + + // Compute the intermediary stop at 360°. + if (h1 - h2).abs() > 180.0 { + let h1 = if h1 < h2 { h1 + 360.0 } else { h1 }; + let h2 = if h2 < h1 { h2 + 360.0 } else { h2 }; + + // We compute where the crossing happens between zero and one + let t = (360.0 - h1) / (h2 - h1); + // We then map it back to the original range. + let t_prime = t * (t_next as f32 - t_x as f32) + t_x as f32; + + // If the crossing happens between the two stops, + // we need to create an extra stop. + if t_prime <= t_next as f32 && t_prime >= t_x as f32 { + let c0 = [1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]; + let c1 = [0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]; + let c0 = c0.map(|c| u16::quantize(c, [0.0, 1.0])); + let c1 = c1.map(|c| u16::quantize(c, [0.0, 1.0])); + + write_patch( + &mut vertices, + t_x as f32, + t_prime, + conic.space.convert(c), + c0, + angle, + on_text, + ); + + write_patch( + &mut vertices, + t_prime, + t_prime, + c0, + c1, + angle, + on_text, + ); + + write_patch( + &mut vertices, + t_prime, + t_next as f32, + c1, + conic.space.convert(c_next), + angle, + on_text, + ); + + t_x = t_next; + continue; + } + } + } + + write_patch( + &mut vertices, + t_x as f32, + t_next as f32, + conic.space.convert(c), + conic.space.convert(c_next), + angle, + on_text, + ); + + t_x = t_next; + } + } + + Arc::new(deflate(&vertices)) +} diff --git a/crates/typst-pdf/src/icc/sGrey-v4.icc b/crates/typst-pdf/src/icc/sGrey-v4.icc new file mode 100644 index 00000000..2187b678 Binary files /dev/null and b/crates/typst-pdf/src/icc/sGrey-v4.icc differ diff --git a/crates/typst-pdf/src/icc/sRGB-v4.icc b/crates/typst-pdf/src/icc/sRGB-v4.icc new file mode 100644 index 00000000..d9f3c055 Binary files /dev/null and b/crates/typst-pdf/src/icc/sRGB-v4.icc differ diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs new file mode 100644 index 00000000..8d526efd --- /dev/null +++ b/crates/typst-pdf/src/image.rs @@ -0,0 +1,170 @@ +use std::collections::HashMap; +use std::io::Cursor; +use std::sync::Arc; + +use image::{DynamicImage, GenericImageView, Rgba}; +use pdf_writer::{Chunk, Filter, Finish, Ref}; +use typst::geom::ColorSpace; +use typst::image::{ImageKind, RasterFormat, RasterImage, SvgImage}; + +use crate::{deflate, PdfContext}; + +/// Embed all used images into the PDF. +#[tracing::instrument(skip_all)] +pub fn write_images(ctx: &mut PdfContext) { + for image in ctx.image_map.items() { + // Add the primary image. + match image.kind() { + ImageKind::Raster(raster) => { + // TODO: Error if image could not be encoded. + let (data, filter, has_color) = encode_raster_image(raster); + let width = image.width(); + let height = image.height(); + + let image_ref = ctx.alloc.bump(); + ctx.image_refs.push(image_ref); + + let mut image = ctx.pdf.image_xobject(image_ref, &data); + image.filter(filter); + image.width(width as i32); + image.height(height as i32); + image.bits_per_component(8); + + let mut icc_ref = None; + let space = image.color_space(); + if raster.icc().is_some() { + let id = ctx.alloc.bump(); + space.icc_based(id); + icc_ref = Some(id); + } else if has_color { + ctx.colors.write(ColorSpace::Srgb, space, &mut ctx.alloc); + } else { + ctx.colors.write(ColorSpace::D65Gray, space, &mut ctx.alloc); + } + + // Add a second gray-scale image containing the alpha values if + // this image has an alpha channel. + if raster.dynamic().color().has_alpha() { + let (alpha_data, alpha_filter) = encode_alpha(raster); + let mask_ref = ctx.alloc.bump(); + image.s_mask(mask_ref); + image.finish(); + + let mut mask = ctx.pdf.image_xobject(mask_ref, &alpha_data); + mask.filter(alpha_filter); + mask.width(width as i32); + mask.height(height as i32); + mask.color_space().device_gray(); + mask.bits_per_component(8); + } else { + image.finish(); + } + + if let (Some(icc), Some(icc_ref)) = (raster.icc(), icc_ref) { + let compressed = deflate(icc); + let mut stream = ctx.pdf.icc_profile(icc_ref, &compressed); + stream.filter(Filter::FlateDecode); + if has_color { + stream.n(3); + stream.alternate().srgb(); + } else { + stream.n(1); + stream.alternate().d65_gray(); + } + } + } + + ImageKind::Svg(svg) => { + let chunk = encode_svg(svg); + let mut map = HashMap::new(); + chunk.renumber_into(&mut ctx.pdf, |old| { + *map.entry(old).or_insert_with(|| ctx.alloc.bump()) + }); + ctx.image_refs.push(map[&Ref::new(1)]); + } + } + } +} + +/// Encode an image with a suitable filter and return the data, filter and +/// whether the image has color. +/// +/// Skips the alpha channel as that's encoded separately. +#[comemo::memoize] +#[tracing::instrument(skip_all)] +fn encode_raster_image(image: &RasterImage) -> (Arc>, Filter, bool) { + let dynamic = image.dynamic(); + match (image.format(), dynamic) { + // 8-bit gray JPEG. + (RasterFormat::Jpg, DynamicImage::ImageLuma8(_)) => { + let mut data = Cursor::new(vec![]); + dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); + (data.into_inner().into(), Filter::DctDecode, false) + } + + // 8-bit RGB JPEG (CMYK JPEGs get converted to RGB earlier). + (RasterFormat::Jpg, DynamicImage::ImageRgb8(_)) => { + let mut data = Cursor::new(vec![]); + dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); + (data.into_inner().into(), Filter::DctDecode, true) + } + + // TODO: Encode flate streams with PNG-predictor? + + // 8-bit gray PNG. + (RasterFormat::Png, DynamicImage::ImageLuma8(luma)) => { + let data = deflate(luma.as_raw()); + (data.into(), Filter::FlateDecode, false) + } + + // Anything else (including Rgb(a) PNGs). + (_, buf) => { + let (width, height) = buf.dimensions(); + let mut pixels = Vec::with_capacity(3 * width as usize * height as usize); + for (_, _, Rgba([r, g, b, _])) in buf.pixels() { + pixels.push(r); + pixels.push(g); + pixels.push(b); + } + + let data = deflate(&pixels); + (data.into(), Filter::FlateDecode, true) + } + } +} + +/// Encode an image's alpha channel if present. +#[comemo::memoize] +#[tracing::instrument(skip_all)] +fn encode_alpha(raster: &RasterImage) -> (Arc>, Filter) { + let pixels: Vec<_> = raster + .dynamic() + .pixels() + .map(|(_, _, Rgba([_, _, _, a]))| a) + .collect(); + (Arc::new(deflate(&pixels)), Filter::FlateDecode) +} + +/// Encode an SVG into a chunk of PDF objects. +/// +/// The main XObject will have ID 1. +#[comemo::memoize] +#[tracing::instrument(skip_all)] +fn encode_svg(svg: &SvgImage) -> Arc { + let mut chunk = Chunk::new(); + + // Safety: We do not keep any references to tree nodes beyond the + // scope of `with`. + unsafe { + svg.with(|tree| { + svg2pdf::convert_tree_into( + tree, + svg2pdf::Options::default(), + &mut chunk, + Ref::new(1), + ); + }); + } + + Arc::new(chunk) +} diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs new file mode 100644 index 00000000..92c84495 --- /dev/null +++ b/crates/typst-pdf/src/lib.rs @@ -0,0 +1,377 @@ +//! Exporting into PDF documents. + +mod color; +mod extg; +mod font; +mod gradient; +mod image; +mod outline; +mod page; + +use std::cmp::Eq; +use std::collections::{BTreeMap, HashMap}; +use std::hash::Hash; + +use base64::Engine; +use ecow::{eco_format, EcoString}; +use pdf_writer::types::Direction; +use pdf_writer::{Finish, Name, Pdf, Ref, TextStr}; +use typst::doc::{Document, Lang}; +use typst::eval::Datetime; +use typst::font::Font; +use typst::geom::{Abs, Dir, Em}; +use typst::image::Image; +use typst::model::Introspector; +use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter}; + +use crate::color::ColorSpaces; +use crate::extg::ExtGState; +use crate::gradient::PdfGradient; +use crate::page::Page; + +/// Export a document into a PDF file. +/// +/// Returns the raw bytes making up the PDF file. +/// +/// The `ident` parameter shall be a string that uniquely and stably identifies +/// the document. It should not change between compilations of the same +/// document. Its hash will be used to create a PDF document identifier (the +/// identifier itself is not leaked). If `ident` is `None`, a hash of the +/// document is used instead (which means that it _will_ change across +/// compilations). +/// +/// The `timestamp`, if given, is expected to be the creation date of the +/// document as a UTC datetime. It will only be used if `set document(date: ..)` +/// is `auto`. +#[tracing::instrument(skip_all)] +pub fn pdf( + document: &Document, + ident: Option<&str>, + timestamp: Option, +) -> Vec { + let mut ctx = PdfContext::new(document); + page::construct_pages(&mut ctx, &document.pages); + font::write_fonts(&mut ctx); + image::write_images(&mut ctx); + gradient::write_gradients(&mut ctx); + extg::write_external_graphics_states(&mut ctx); + page::write_page_tree(&mut ctx); + write_catalog(&mut ctx, ident, timestamp); + ctx.pdf.finish() +} + +/// Context for exporting a whole PDF document. +pub struct PdfContext<'a> { + /// The document that we're currently exporting. + document: &'a Document, + /// An introspector for the document, used to resolve locations links and + /// the document outline. + introspector: Introspector, + + /// The writer we are writing the PDF into. + pdf: Pdf, + /// Content of exported pages. + pages: Vec, + /// For each font a mapping from used glyphs to their text representation. + /// May contain multiple chars in case of ligatures or similar things. The + /// same glyph can have a different text representation within one document, + /// then we just save the first one. The resulting strings are used for the + /// PDF's /ToUnicode map for glyphs that don't have an entry in the font's + /// cmap. This is important for copy-paste and searching. + glyph_sets: HashMap>, + /// The number of glyphs for all referenced languages in the document. + /// We keep track of this to determine the main document language. + languages: HashMap, + + /// Allocator for indirect reference IDs. + alloc: Ref, + /// The ID of the page tree. + page_tree_ref: Ref, + /// The IDs of written pages. + page_refs: Vec, + /// The IDs of written fonts. + font_refs: Vec, + /// The IDs of written images. + image_refs: Vec, + /// The IDs of written gradients. + gradient_refs: Vec, + /// The IDs of written external graphics states. + ext_gs_refs: Vec, + /// Handles color space writing. + colors: ColorSpaces, + + /// Deduplicates fonts used across the document. + font_map: Remapper, + /// Deduplicates images used across the document. + image_map: Remapper, + /// Deduplicates gradients used across the document. + gradient_map: Remapper, + /// Deduplicates external graphics states used across the document. + extg_map: Remapper, +} + +impl<'a> PdfContext<'a> { + fn new(document: &'a Document) -> Self { + let mut alloc = Ref::new(1); + let page_tree_ref = alloc.bump(); + Self { + document, + introspector: Introspector::new(&document.pages), + pdf: Pdf::new(), + pages: vec![], + glyph_sets: HashMap::new(), + languages: HashMap::new(), + alloc, + page_tree_ref, + page_refs: vec![], + font_refs: vec![], + image_refs: vec![], + gradient_refs: vec![], + ext_gs_refs: vec![], + colors: ColorSpaces::default(), + font_map: Remapper::new(), + image_map: Remapper::new(), + gradient_map: Remapper::new(), + extg_map: Remapper::new(), + } + } +} + +/// Write the document catalog. +#[tracing::instrument(skip_all)] +fn write_catalog(ctx: &mut PdfContext, ident: Option<&str>, timestamp: Option) { + let lang = ctx + .languages + .iter() + .max_by_key(|(&lang, &count)| (count, lang)) + .map(|(&k, _)| k); + + let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { + Direction::R2L + } else { + Direction::L2R + }; + + // Write the outline tree. + let outline_root_id = outline::write_outline(ctx); + + // Write the page labels. + let page_labels = page::write_page_labels(ctx); + + // Write the document information. + let mut info = ctx.pdf.document_info(ctx.alloc.bump()); + let mut xmp = XmpWriter::new(); + if let Some(title) = &ctx.document.title { + info.title(TextStr(title)); + xmp.title([(None, title.as_str())]); + } + + let authors = &ctx.document.author; + if !authors.is_empty() { + info.author(TextStr(&authors.join(", "))); + xmp.creator(authors.iter().map(|s| s.as_str())); + } + + let creator = eco_format!("Typst {}", env!("CARGO_PKG_VERSION")); + info.creator(TextStr(&creator)); + xmp.creator_tool(&creator); + + let keywords = &ctx.document.keywords; + if !keywords.is_empty() { + let joined = keywords.join(", "); + info.keywords(TextStr(&joined)); + xmp.pdf_keywords(&joined); + } + + if let Some(date) = ctx.document.date.unwrap_or(timestamp) { + let tz = ctx.document.date.is_auto(); + if let Some(pdf_date) = pdf_date(date, tz) { + info.creation_date(pdf_date); + info.modified_date(pdf_date); + } + if let Some(xmp_date) = xmp_date(date, tz) { + xmp.create_date(xmp_date); + xmp.modify_date(xmp_date); + } + } + + info.finish(); + xmp.num_pages(ctx.document.pages.len() as u32); + xmp.format("application/pdf"); + xmp.language(ctx.languages.keys().map(|lang| LangId(lang.as_str()))); + + // A unique ID for this instance of the document. Changes if anything + // changes in the frames. + let instance_id = hash_base64(&ctx.pdf.as_bytes()); + + if let Some(ident) = ident { + // A unique ID for the document that stays stable across compilations. + let doc_id = hash_base64(&("PDF-1.7", ident)); + xmp.document_id(&doc_id); + xmp.instance_id(&instance_id); + ctx.pdf + .set_file_id((doc_id.clone().into_bytes(), instance_id.into_bytes())); + } else { + // This is not spec-compliant, but some PDF readers really want an ID. + let bytes = instance_id.into_bytes(); + ctx.pdf.set_file_id((bytes.clone(), bytes)); + } + + xmp.rendition_class(RenditionClass::Proof); + xmp.pdf_version("1.7"); + + let xmp_buf = xmp.finish(None); + let meta_ref = ctx.alloc.bump(); + ctx.pdf + .stream(meta_ref, xmp_buf.as_bytes()) + .pair(Name(b"Type"), Name(b"Metadata")) + .pair(Name(b"Subtype"), Name(b"XML")); + + // Write the document catalog. + let mut catalog = ctx.pdf.catalog(ctx.alloc.bump()); + catalog.pages(ctx.page_tree_ref); + catalog.viewer_preferences().direction(dir); + catalog.metadata(meta_ref); + + // Insert the page labels. + if !page_labels.is_empty() { + let mut num_tree = catalog.page_labels(); + let mut entries = num_tree.nums(); + for (n, r) in &page_labels { + entries.insert(n.get() as i32 - 1, *r); + } + } + + if let Some(outline_root_id) = outline_root_id { + catalog.outlines(outline_root_id); + } + + if let Some(lang) = lang { + catalog.lang(TextStr(lang.as_str())); + } +} + +/// Compress data with the DEFLATE algorithm. +#[tracing::instrument(skip_all)] +fn deflate(data: &[u8]) -> Vec { + const COMPRESSION_LEVEL: u8 = 6; + miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) +} + +/// Create a base64-encoded hash of the value. +fn hash_base64(value: &T) -> String { + base64::engine::general_purpose::STANDARD + .encode(typst::util::hash128(value).to_be_bytes()) +} + +/// Converts a datetime to a pdf-writer date. +fn pdf_date(datetime: Datetime, tz: bool) -> Option { + let year = datetime.year().filter(|&y| y >= 0)? as u16; + + let mut pdf_date = pdf_writer::Date::new(year); + + if let Some(month) = datetime.month() { + pdf_date = pdf_date.month(month); + } + + if let Some(day) = datetime.day() { + pdf_date = pdf_date.day(day); + } + + if let Some(h) = datetime.hour() { + pdf_date = pdf_date.hour(h); + } + + if let Some(m) = datetime.minute() { + pdf_date = pdf_date.minute(m); + } + + if let Some(s) = datetime.second() { + pdf_date = pdf_date.second(s); + } + + if tz { + pdf_date = pdf_date.utc_offset_hour(0).utc_offset_minute(0); + } + + Some(pdf_date) +} + +/// Converts a datetime to an xmp-writer datetime. +fn xmp_date(datetime: Datetime, tz: bool) -> Option { + let year = datetime.year().filter(|&y| y >= 0)? as u16; + Some(DateTime { + year, + month: datetime.month(), + day: datetime.day(), + hour: datetime.hour(), + minute: datetime.minute(), + second: datetime.second(), + timezone: if tz { Some(Timezone::Utc) } else { None }, + }) +} + +/// Assigns new, consecutive PDF-internal indices to items. +struct Remapper { + /// Forwards from the items to the pdf indices. + to_pdf: HashMap, + /// Backwards from the pdf indices to the items. + to_items: Vec, +} + +impl Remapper +where + T: Eq + Hash + Clone, +{ + fn new() -> Self { + Self { to_pdf: HashMap::new(), to_items: vec![] } + } + + fn insert(&mut self, item: T) -> usize { + let to_layout = &mut self.to_items; + *self.to_pdf.entry(item.clone()).or_insert_with(|| { + let pdf_index = to_layout.len(); + to_layout.push(item); + pdf_index + }) + } + + fn map(&self, item: &T) -> usize { + self.to_pdf[item] + } + + fn pdf_indices<'a>( + &'a self, + refs: &'a [Ref], + ) -> impl Iterator + 'a { + refs.iter().copied().zip(0..self.to_pdf.len()) + } + + fn items(&self) -> impl Iterator + '_ { + self.to_items.iter() + } +} + +/// Additional methods for [`Abs`]. +trait AbsExt { + /// Convert an to a number of points. + fn to_f32(self) -> f32; +} + +impl AbsExt for Abs { + fn to_f32(self) -> f32 { + self.to_pt() as f32 + } +} + +/// Additional methods for [`Em`]. +trait EmExt { + /// Convert an em length to a number of PDF font units. + fn to_font_units(self) -> f32; +} + +impl EmExt for Em { + fn to_font_units(self) -> f32 { + 1000.0 * self.get() as f32 + } +} diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs new file mode 100644 index 00000000..e51d85c3 --- /dev/null +++ b/crates/typst-pdf/src/outline.rs @@ -0,0 +1,191 @@ +use std::num::NonZeroUsize; + +use pdf_writer::{Finish, Ref, TextStr}; +use typst::eval::item; +use typst::geom::{Abs, Smart}; +use typst::model::Content; + +use crate::{AbsExt, PdfContext}; + +/// Construct the outline for the document. +#[tracing::instrument(skip_all)] +pub fn write_outline(ctx: &mut PdfContext) -> Option { + let mut tree: Vec = vec![]; + + // Stores the level of the topmost skipped ancestor of the next bookmarked + // heading. A skipped heading is a heading with 'bookmarked: false', that + // is, it is not added to the PDF outline, and so is not in the tree. + // Therefore, its next descendant must be added at its level, which is + // enforced in the manner shown below. + let mut last_skipped_level = None; + for heading in ctx.introspector.query(&item!(heading_elem).select()).iter() { + let leaf = HeadingNode::leaf((**heading).clone()); + + if leaf.bookmarked { + let mut children = &mut tree; + + // Descend the tree through the latest bookmarked heading of each + // level until either: + // - you reach a node whose children would be brothers of this + // heading (=> add the current heading as a child of this node); + // - you reach a node with no children (=> this heading probably + // skipped a few nesting levels in Typst, or one or more ancestors + // of this heading weren't bookmarked, so add it as a child of this + // node, which is its deepest bookmarked ancestor); + // - or, if the latest heading(s) was(/were) skipped + // ('bookmarked: false'), then stop if you reach a node whose + // children would be brothers of the latest skipped heading + // of lowest level (=> those skipped headings would be ancestors + // of the current heading, so add it as a 'brother' of the least + // deep skipped ancestor among them, as those ancestors weren't + // added to the bookmark tree, and the current heading should not + // be mistakenly added as a descendant of a brother of that + // ancestor.) + // + // That is, if you had a bookmarked heading of level N, a skipped + // heading of level N, a skipped heading of level N + 1, and then + // a bookmarked heading of level N + 2, that last one is bookmarked + // as a level N heading (taking the place of its topmost skipped + // ancestor), so that it is not mistakenly added as a descendant of + // the previous level N heading. + // + // In other words, a heading can be added to the bookmark tree + // at most as deep as its topmost skipped direct ancestor (if it + // exists), or at most as deep as its actual nesting level in Typst + // (not exceeding whichever is the most restrictive depth limit + // of those two). + while children.last().map_or(false, |last| { + last_skipped_level.map_or(true, |l| last.level < l) + && last.level < leaf.level + }) { + children = &mut children.last_mut().unwrap().children; + } + + // Since this heading was bookmarked, the next heading, if it is a + // child of this one, won't have a skipped direct ancestor (indeed, + // this heading would be its most direct ancestor, and wasn't + // skipped). Therefore, it can be added as a child of this one, if + // needed, following the usual rules listed above. + last_skipped_level = None; + children.push(leaf); + } else if last_skipped_level.map_or(true, |l| leaf.level < l) { + // Only the topmost / lowest-level skipped heading matters when you + // have consecutive skipped headings (since none of them are being + // added to the bookmark tree), hence the condition above. + // This ensures the next bookmarked heading will be placed + // at most as deep as its topmost skipped ancestors. Deeper + // ancestors do not matter as the nesting structure they create + // won't be visible in the PDF outline. + last_skipped_level = Some(leaf.level); + } + } + + if tree.is_empty() { + return None; + } + + let root_id = ctx.alloc.bump(); + let start_ref = ctx.alloc; + let len = tree.len(); + + let mut prev_ref = None; + for (i, node) in tree.iter().enumerate() { + prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len)); + } + + ctx.pdf + .outline(root_id) + .first(start_ref) + .last(Ref::new(ctx.alloc.get() - 1)) + .count(tree.len() as i32); + + Some(root_id) +} + +/// A heading in the outline panel. +#[derive(Debug, Clone)] +struct HeadingNode { + element: Content, + level: NonZeroUsize, + bookmarked: bool, + children: Vec, +} + +impl HeadingNode { + fn leaf(element: Content) -> Self { + HeadingNode { + level: element.expect_field_by_name::("level"), + // 'bookmarked' set to 'auto' falls back to the value of 'outlined'. + bookmarked: element + .expect_field_by_name::>("bookmarked") + .unwrap_or_else(|| element.expect_field_by_name::("outlined")), + element, + children: Vec::new(), + } + } + + fn len(&self) -> usize { + 1 + self.children.iter().map(Self::len).sum::() + } +} + +/// Write an outline item and all its children. +#[tracing::instrument(skip_all)] +fn write_outline_item( + ctx: &mut PdfContext, + node: &HeadingNode, + parent_ref: Ref, + prev_ref: Option, + is_last: bool, +) -> Ref { + let id = ctx.alloc.bump(); + let next_ref = Ref::new(id.get() + node.len() as i32); + + let mut outline = ctx.pdf.outline_item(id); + outline.parent(parent_ref); + + if !is_last { + outline.next(next_ref); + } + + if let Some(prev_rev) = prev_ref { + outline.prev(prev_rev); + } + + if !node.children.is_empty() { + let current_child = Ref::new(id.get() + 1); + outline.first(current_child); + outline.last(Ref::new(next_ref.get() - 1)); + outline.count(-(node.children.len() as i32)); + } + + let body = node.element.expect_field_by_name::("body"); + outline.title(TextStr(body.plain_text().trim())); + + let loc = node.element.location().unwrap(); + let pos = ctx.introspector.position(loc); + let index = pos.page.get() - 1; + if let Some(page) = ctx.pages.get(index) { + let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); + outline.dest().page(ctx.page_refs[index]).xyz( + pos.point.x.to_f32(), + (page.size.y - y).to_f32(), + None, + ); + } + + outline.finish(); + + let mut prev_ref = None; + for (i, child) in node.children.iter().enumerate() { + prev_ref = Some(write_outline_item( + ctx, + child, + id, + prev_ref, + i + 1 == node.children.len(), + )); + } + + id +} diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs new file mode 100644 index 00000000..4de472f0 --- /dev/null +++ b/crates/typst-pdf/src/page.rs @@ -0,0 +1,759 @@ +use std::num::NonZeroUsize; +use std::sync::Arc; + +use ecow::eco_format; +use pdf_writer::types::{ + ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle, + NumberingStyle, +}; +use pdf_writer::writers::PageLabel; +use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr}; +use typst::doc::{ + Destination, Frame, FrameItem, GroupItem, Meta, PdfPageLabel, PdfPageLabelStyle, + TextItem, +}; +use typst::font::Font; +use typst::geom::{ + self, Abs, Em, FixedStroke, Geometry, LineCap, LineJoin, Numeric, Paint, Point, + Ratio, Shape, Size, Transform, +}; +use typst::image::Image; + +use crate::color::PaintEncode; +use crate::extg::ExtGState; +use crate::{deflate, AbsExt, EmExt, PdfContext}; + +/// Construct page objects. +#[tracing::instrument(skip_all)] +pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) { + for frame in frames { + construct_page(ctx, frame); + } +} + +/// Construct a page object. +#[tracing::instrument(skip_all)] +pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) { + let page_ref = ctx.alloc.bump(); + ctx.page_refs.push(page_ref); + + let mut ctx = PageContext { + parent: ctx, + page_ref, + label: None, + uses_opacities: false, + content: Content::new(), + state: State::new(frame.size()), + saves: vec![], + bottom: 0.0, + links: vec![], + }; + + let size = frame.size(); + + // Make the coordinate system start at the top-left. + ctx.bottom = size.y.to_f32(); + ctx.transform(Transform { + sx: Ratio::one(), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::new(-1.0), + tx: Abs::zero(), + ty: size.y, + }); + + // Encode the page into the content stream. + write_frame(&mut ctx, frame); + + let page = Page { + size, + content: ctx.content.finish(), + id: ctx.page_ref, + uses_opacities: ctx.uses_opacities, + links: ctx.links, + label: ctx.label, + }; + + ctx.parent.pages.push(page); +} + +/// Write the page tree. +#[tracing::instrument(skip_all)] +pub fn write_page_tree(ctx: &mut PdfContext) { + for i in 0..ctx.pages.len() { + write_page(ctx, i); + } + + let mut pages = ctx.pdf.pages(ctx.page_tree_ref); + pages + .count(ctx.page_refs.len() as i32) + .kids(ctx.page_refs.iter().copied()); + + let mut resources = pages.resources(); + ctx.colors + .write_color_spaces(resources.color_spaces(), &mut ctx.alloc); + + let mut fonts = resources.fonts(); + for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) { + let name = eco_format!("F{}", f); + fonts.pair(Name(name.as_bytes()), font_ref); + } + + fonts.finish(); + + let mut images = resources.x_objects(); + for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) { + let name = eco_format!("Im{}", im); + images.pair(Name(name.as_bytes()), image_ref); + } + + images.finish(); + + let mut patterns = resources.patterns(); + for (gradient_ref, gr) in ctx.gradient_map.pdf_indices(&ctx.gradient_refs) { + let name = eco_format!("Gr{}", gr); + patterns.pair(Name(name.as_bytes()), gradient_ref); + } + + patterns.finish(); + + let mut ext_gs_states = resources.ext_g_states(); + for (gs_ref, gs) in ctx.extg_map.pdf_indices(&ctx.ext_gs_refs) { + let name = eco_format!("Gs{}", gs); + ext_gs_states.pair(Name(name.as_bytes()), gs_ref); + } + ext_gs_states.finish(); + + resources.finish(); + pages.finish(); + + // Write all of the functions used by the document. + ctx.colors.write_functions(&mut ctx.pdf); +} + +/// Write a page tree node. +#[tracing::instrument(skip_all)] +fn write_page(ctx: &mut PdfContext, i: usize) { + let page = &ctx.pages[i]; + let content_id = ctx.alloc.bump(); + + let mut page_writer = ctx.pdf.page(page.id); + page_writer.parent(ctx.page_tree_ref); + + let w = page.size.x.to_f32(); + let h = page.size.y.to_f32(); + page_writer.media_box(Rect::new(0.0, 0.0, w, h)); + page_writer.contents(content_id); + + if page.uses_opacities { + page_writer + .group() + .transparency() + .isolated(false) + .knockout(false) + .color_space() + .srgb(); + } + + let mut annotations = page_writer.annotations(); + for (dest, rect) in &page.links { + let mut annotation = annotations.push(); + annotation.subtype(AnnotationType::Link).rect(*rect); + annotation.border(0.0, 0.0, 0.0, None); + + let pos = match dest { + Destination::Url(uri) => { + annotation + .action() + .action_type(ActionType::Uri) + .uri(Str(uri.as_bytes())); + continue; + } + Destination::Position(pos) => *pos, + Destination::Location(loc) => ctx.introspector.position(*loc), + }; + + let index = pos.page.get() - 1; + let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); + if let Some(page) = ctx.pages.get(index) { + annotation + .action() + .action_type(ActionType::GoTo) + .destination() + .page(ctx.page_refs[index]) + .xyz(pos.point.x.to_f32(), (page.size.y - y).to_f32(), None); + } + } + + annotations.finish(); + page_writer.finish(); + + let data = deflate_content(&page.content); + ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode); +} + +/// Write the page labels. +#[tracing::instrument(skip_all)] +pub fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)> { + let mut result = vec![]; + let mut prev: Option<&PdfPageLabel> = None; + + for (i, page) in ctx.pages.iter().enumerate() { + let nr = NonZeroUsize::new(1 + i).unwrap(); + let Some(label) = &page.label else { continue }; + + // Don't create a label if neither style nor prefix are specified. + if label.prefix.is_none() && label.style.is_none() { + continue; + } + + if let Some(pre) = prev { + if label.prefix == pre.prefix + && label.style == pre.style + && label.offset == pre.offset.map(|n| n.saturating_add(1)) + { + prev = Some(label); + continue; + } + } + + let id = ctx.alloc.bump(); + let mut entry = ctx.pdf.indirect(id).start::(); + + // Only add what is actually provided. Don't add empty prefix string if + // it wasn't given for example. + if let Some(prefix) = &label.prefix { + entry.prefix(TextStr(prefix)); + } + + if let Some(style) = label.style { + entry.style(to_pdf_numbering_style(style)); + } + + if let Some(offset) = label.offset { + entry.offset(offset.get() as i32); + } + + result.push((nr, id)); + prev = Some(label); + } + + result +} + +/// Memoized version of [`deflate`] specialized for a page's content stream. +#[comemo::memoize] +fn deflate_content(content: &[u8]) -> Arc> { + Arc::new(deflate(content)) +} + +/// Data for an exported page. +pub struct Page { + /// The indirect object id of the page. + pub id: Ref, + /// The page's dimensions. + pub size: Size, + /// The page's content stream. + pub content: Vec, + /// Whether the page uses opacities. + pub uses_opacities: bool, + /// Links in the PDF coordinate system. + pub links: Vec<(Destination, Rect)>, + /// The page's PDF label. + pub label: Option, +} + +/// An exporter for the contents of a single PDF page. +pub struct PageContext<'a, 'b> { + pub parent: &'a mut PdfContext<'b>, + page_ref: Ref, + label: Option, + pub content: Content, + state: State, + saves: Vec, + bottom: f32, + uses_opacities: bool, + links: Vec<(Destination, Rect)>, +} + +/// A simulated graphics state used to deduplicate graphics state changes and +/// keep track of the current transformation matrix for link annotations. +#[derive(Debug, Clone)] +struct State { + /// The transform of the current item. + transform: Transform, + /// The transform of first hard frame in the hierarchy. + container_transform: Transform, + /// The size of the first hard frame in the hierarchy. + size: Size, + font: Option<(Font, Abs)>, + fill: Option, + fill_space: Option>, + external_graphics_state: Option, + stroke: Option, + stroke_space: Option>, +} + +impl State { + /// Creates a new, clean state for a given page `size`. + pub fn new(size: Size) -> Self { + Self { + transform: Transform::identity(), + container_transform: Transform::identity(), + size, + font: None, + fill: None, + fill_space: None, + external_graphics_state: None, + stroke: None, + stroke_space: None, + } + } + + /// Creates the [`Transforms`] structure for the current item. + pub fn transforms(&self, size: Size, pos: Point) -> Transforms { + Transforms { + transform: self.transform.pre_concat(Transform::translate(pos.x, pos.y)), + container_transform: self.container_transform, + container_size: self.size, + size, + } + } +} + +/// Subset of the state used to calculate the transform of gradients and patterns. +#[derive(Clone, Copy)] +pub(super) struct Transforms { + /// The transform of the current item. + pub transform: Transform, + /// The transform of first hard frame in the hierarchy. + pub container_transform: Transform, + /// The size of the first hard frame in the hierarchy. + pub container_size: Size, + /// The size of the item. + pub size: Size, +} + +impl PageContext<'_, '_> { + fn save_state(&mut self) { + self.saves.push(self.state.clone()); + self.content.save_state(); + } + + fn restore_state(&mut self) { + self.content.restore_state(); + self.state = self.saves.pop().expect("missing state save"); + } + + fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) { + let current_state = self.state.external_graphics_state.as_ref(); + if current_state != Some(graphics_state) { + self.parent.extg_map.insert(*graphics_state); + let name = eco_format!("Gs{}", self.parent.extg_map.map(graphics_state)); + self.content.set_parameters(Name(name.as_bytes())); + + if graphics_state.uses_opacities() { + self.uses_opacities = true; + } + } + } + + fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) { + let stroke_opacity = stroke + .map(|stroke| { + let color = match &stroke.paint { + Paint::Solid(color) => *color, + Paint::Gradient(_) => return 255, + }; + + color.alpha().map_or(255, |v| (v * 255.0).round() as u8) + }) + .unwrap_or(255); + let fill_opacity = fill + .map(|paint| { + let color = match paint { + Paint::Solid(color) => *color, + Paint::Gradient(_) => return 255, + }; + + color.alpha().map_or(255, |v| (v * 255.0).round() as u8) + }) + .unwrap_or(255); + self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); + } + + fn transform(&mut self, transform: Transform) { + let Transform { sx, ky, kx, sy, tx, ty } = transform; + self.state.transform = self.state.transform.pre_concat(transform); + self.content.transform([ + sx.get() as _, + ky.get() as _, + kx.get() as _, + sy.get() as _, + tx.to_f32(), + ty.to_f32(), + ]); + } + + fn group_transform(&mut self, transform: Transform) { + self.state.container_transform = + self.state.container_transform.pre_concat(transform); + } + + fn set_font(&mut self, font: &Font, size: Abs) { + if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) { + self.parent.font_map.insert(font.clone()); + let name = eco_format!("F{}", self.parent.font_map.map(font)); + self.content.set_font(Name(name.as_bytes()), size.to_f32()); + self.state.font = Some((font.clone(), size)); + } + } + + fn size(&mut self, size: Size) { + self.state.size = size; + } + + fn set_fill(&mut self, fill: &Paint, on_text: bool, transforms: Transforms) { + if self.state.fill.as_ref() != Some(fill) + || matches!(self.state.fill, Some(Paint::Gradient(_))) + { + fill.set_as_fill(self, on_text, transforms); + self.state.fill = Some(fill.clone()); + } + } + + pub fn set_fill_color_space(&mut self, space: Name<'static>) { + if self.state.fill_space != Some(space) { + self.content.set_fill_color_space(ColorSpaceOperand::Named(space)); + self.state.fill_space = Some(space); + } + } + + pub fn reset_fill_color_space(&mut self) { + self.state.fill_space = None; + } + + fn set_stroke(&mut self, stroke: &FixedStroke, transforms: Transforms) { + if self.state.stroke.as_ref() != Some(stroke) + || matches!( + self.state.stroke.as_ref().map(|s| &s.paint), + Some(Paint::Gradient(_)) + ) + { + let FixedStroke { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + } = stroke; + + paint.set_as_stroke(self, false, transforms); + + self.content.set_line_width(thickness.to_f32()); + if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) { + self.content.set_line_cap(to_pdf_line_cap(*line_cap)); + } + if self.state.stroke.as_ref().map(|s| &s.line_join) != Some(line_join) { + self.content.set_line_join(to_pdf_line_join(*line_join)); + } + if self.state.stroke.as_ref().map(|s| &s.dash_pattern) != Some(dash_pattern) { + if let Some(pattern) = dash_pattern { + self.content.set_dash_pattern( + pattern.array.iter().map(|l| l.to_f32()), + pattern.phase.to_f32(), + ); + } else { + self.content.set_dash_pattern([], 0.0); + } + } + if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) { + self.content.set_miter_limit(miter_limit.get() as f32); + } + self.state.stroke = Some(stroke.clone()); + } + } + + pub fn set_stroke_color_space(&mut self, space: Name<'static>) { + if self.state.stroke_space != Some(space) { + self.content.set_stroke_color_space(ColorSpaceOperand::Named(space)); + self.state.stroke_space = Some(space); + } + } + + pub fn reset_stroke_color_space(&mut self) { + self.state.stroke_space = None; + } +} + +/// Encode a frame into the content stream. +fn write_frame(ctx: &mut PageContext, frame: &Frame) { + for &(pos, ref item) in frame.items() { + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); + + match item { + FrameItem::Group(group) => write_group(ctx, pos, group), + FrameItem::Text(text) => write_text(ctx, pos, text), + FrameItem::Shape(shape, _) => write_shape(ctx, pos, shape), + FrameItem::Image(image, size, _) => write_image(ctx, x, y, image, *size), + FrameItem::Meta(meta, size) => match meta { + Meta::Link(dest) => write_link(ctx, pos, dest, *size), + Meta::Elem(_) => {} + Meta::Hide => {} + Meta::PageNumbering(_) => {} + Meta::PdfPageLabel(label) => ctx.label = Some(label.clone()), + }, + } + } +} + +/// Encode a group into the content stream. +fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) { + let translation = Transform::translate(pos.x, pos.y); + + ctx.save_state(); + + if group.frame.kind().is_hard() { + ctx.group_transform( + translation + .pre_concat( + ctx.state + .transform + .post_concat(ctx.state.container_transform.invert().unwrap()), + ) + .pre_concat(group.transform), + ); + ctx.size(group.frame.size()); + } + + ctx.transform(translation.pre_concat(group.transform)); + if let Some(clip_path) = &group.clip_path { + write_path(ctx, 0.0, 0.0, clip_path); + ctx.content.clip_nonzero(); + ctx.content.end_path(); + } + + write_frame(ctx, &group.frame); + ctx.restore_state(); +} + +/// Encode a text run into the content stream. +fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); + + *ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len(); + + let glyph_set = ctx.parent.glyph_sets.entry(text.font.clone()).or_default(); + for g in &text.glyphs { + let segment = &text.text[g.range()]; + glyph_set.entry(g.id).or_insert_with(|| segment.into()); + } + + ctx.set_fill(&text.fill, true, ctx.state.transforms(Size::zero(), pos)); + ctx.set_font(&text.font, text.size); + ctx.set_opacities(None, Some(&text.fill)); + ctx.content.begin_text(); + + // Positiosn the text. + ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); + + let mut positioned = ctx.content.show_positioned(); + let mut items = positioned.items(); + let mut adjustment = Em::zero(); + let mut encoded = vec![]; + + // Write the glyphs with kerning adjustments. + for glyph in &text.glyphs { + adjustment += glyph.x_offset; + + if !adjustment.is_zero() { + if !encoded.is_empty() { + items.show(Str(&encoded)); + encoded.clear(); + } + + items.adjust(-adjustment.to_font_units()); + adjustment = Em::zero(); + } + + let cid = super::font::glyph_cid(&text.font, glyph.id); + encoded.push((cid >> 8) as u8); + encoded.push((cid & 0xff) as u8); + + if let Some(advance) = text.font.advance(glyph.id) { + adjustment += glyph.x_advance - advance; + } + + adjustment -= glyph.x_offset; + } + + if !encoded.is_empty() { + items.show(Str(&encoded)); + } + + items.finish(); + positioned.finish(); + ctx.content.end_text(); +} + +/// Encode a geometrical shape into the content stream. +fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) { + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); + + let stroke = shape.stroke.as_ref().and_then(|stroke| { + if stroke.thickness.to_f32() > 0.0 { + Some(stroke) + } else { + None + } + }); + + if shape.fill.is_none() && stroke.is_none() { + return; + } + + if let Some(fill) = &shape.fill { + ctx.set_fill(fill, false, ctx.state.transforms(shape.geometry.bbox_size(), pos)); + } + + if let Some(stroke) = stroke { + ctx.set_stroke(stroke, ctx.state.transforms(shape.geometry.bbox_size(), pos)); + } + + ctx.set_opacities(stroke, shape.fill.as_ref()); + + match shape.geometry { + Geometry::Line(target) => { + let dx = target.x.to_f32(); + let dy = target.y.to_f32(); + ctx.content.move_to(x, y); + ctx.content.line_to(x + dx, y + dy); + } + Geometry::Rect(size) => { + let w = size.x.to_f32(); + let h = size.y.to_f32(); + if w > 0.0 && h > 0.0 { + ctx.content.rect(x, y, w, h); + } + } + Geometry::Path(ref path) => { + write_path(ctx, x, y, path); + } + } + + match (&shape.fill, stroke) { + (None, None) => unreachable!(), + (Some(_), None) => ctx.content.fill_nonzero(), + (None, Some(_)) => ctx.content.stroke(), + (Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(), + }; +} + +/// Encode a bezier path into the content stream. +fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) { + for elem in &path.0 { + match elem { + geom::PathItem::MoveTo(p) => { + ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()) + } + geom::PathItem::LineTo(p) => { + ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()) + } + geom::PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to( + x + p1.x.to_f32(), + y + p1.y.to_f32(), + x + p2.x.to_f32(), + y + p2.y.to_f32(), + x + p3.x.to_f32(), + y + p3.y.to_f32(), + ), + geom::PathItem::ClosePath => ctx.content.close_path(), + }; + } +} + +/// Encode a vector or raster image into the content stream. +fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) { + ctx.parent.image_map.insert(image.clone()); + let name = eco_format!("Im{}", ctx.parent.image_map.map(image)); + let w = size.x.to_f32(); + let h = size.y.to_f32(); + ctx.content.save_state(); + ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]); + + if let Some(alt) = image.alt() { + let mut image_span = + ctx.content.begin_marked_content_with_properties(Name(b"Span")); + let mut image_alt = image_span.properties(); + image_alt.pair(Name(b"Alt"), pdf_writer::Str(alt.as_bytes())); + image_alt.finish(); + image_span.finish(); + + ctx.content.x_object(Name(name.as_bytes())); + ctx.content.end_marked_content(); + } else { + ctx.content.x_object(Name(name.as_bytes())); + } + + ctx.content.restore_state(); +} + +/// Save a link for later writing in the annotations dictionary. +fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size) { + let mut min_x = Abs::inf(); + let mut min_y = Abs::inf(); + let mut max_x = -Abs::inf(); + let mut max_y = -Abs::inf(); + + // Compute the bounding box of the transformed link. + for point in [ + pos, + pos + Point::with_x(size.x), + pos + Point::with_y(size.y), + pos + size.to_point(), + ] { + let t = point.transform(ctx.state.transform); + min_x.set_min(t.x); + min_y.set_min(t.y); + max_x.set_max(t.x); + max_y.set_max(t.y); + } + + let x1 = min_x.to_f32(); + let x2 = max_x.to_f32(); + let y1 = max_y.to_f32(); + let y2 = min_y.to_f32(); + let rect = Rect::new(x1, y1, x2, y2); + + ctx.links.push((dest.clone(), rect)); +} + +fn to_pdf_line_cap(cap: LineCap) -> LineCapStyle { + match cap { + LineCap::Butt => LineCapStyle::ButtCap, + LineCap::Round => LineCapStyle::RoundCap, + LineCap::Square => LineCapStyle::ProjectingSquareCap, + } +} + +fn to_pdf_line_join(join: LineJoin) -> LineJoinStyle { + match join { + LineJoin::Miter => LineJoinStyle::MiterJoin, + LineJoin::Round => LineJoinStyle::RoundJoin, + LineJoin::Bevel => LineJoinStyle::BevelJoin, + } +} + +fn to_pdf_numbering_style(style: PdfPageLabelStyle) -> NumberingStyle { + match style { + PdfPageLabelStyle::Arabic => NumberingStyle::Arabic, + PdfPageLabelStyle::LowerRoman => NumberingStyle::LowerRoman, + PdfPageLabelStyle::UpperRoman => NumberingStyle::UpperRoman, + PdfPageLabelStyle::LowerAlpha => NumberingStyle::LowerAlpha, + PdfPageLabelStyle::UpperAlpha => NumberingStyle::UpperAlpha, + } +} diff --git a/crates/typst-pdf/src/postscript/hsl.ps b/crates/typst-pdf/src/postscript/hsl.ps new file mode 100644 index 00000000..740bc3ed --- /dev/null +++ b/crates/typst-pdf/src/postscript/hsl.ps @@ -0,0 +1,63 @@ + +{ + % Starting stack: H, S, L + % /!\ WARNING: The hue component **MUST** be encoded + % in the range [0, 1] before calling this function. + % This is because the function assumes that the + % hue component are divided by a factor of 360 + % in order to meet the range requirements of the + % PDF specification. + + % First we do H = (H * 360.0) % 360 + 3 2 roll 360 mul 3 1 roll + + % Compute C = (1 - |2 * L - 1|) * S + dup 1 exch 2 mul 1 sub abs sub 3 2 roll mul + + % P = (H / 60) % 2 + 3 2 roll dup 60 div 2 + 2 copy div cvi mul exch sub abs + + % X = C * (1 - |P - 1|) + 1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul + + % Compute m = L - C / 2 + exch dup 2 div 5 4 roll exch sub + + % Rotate so H is top + 4 3 roll exch 4 1 roll + + % Construct the RGB stack + dup 60 lt { + % We need to build: (C, X, 0) + pop 0 3 1 roll + } { + dup 120 lt { + % We need to build: (X, C, 0) + pop exch 0 3 1 roll + } { + dup 180 lt { + % We need to build: (0, C, X) + pop 0 + } { + dup 240 lt { + % We need to build: (0, X, C) + pop exch 0 + } { + 300 lt { + % We need to build: (X, 0, C) + 0 3 2 roll + } { + % We need to build: (C, 0, X) + 0 exch + } ifelse + } ifelse + } ifelse + } ifelse + } ifelse + + 4 3 roll + + % Add m to each component + dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch +} \ No newline at end of file diff --git a/crates/typst-pdf/src/postscript/hsv.ps b/crates/typst-pdf/src/postscript/hsv.ps new file mode 100644 index 00000000..b29adf11 --- /dev/null +++ b/crates/typst-pdf/src/postscript/hsv.ps @@ -0,0 +1,62 @@ +{ + % Starting stack: H, S, V + % /!\ WARNING: The hue component **MUST** be encoded + % in the range [0, 1] before calling this function. + % This is because the function assumes that the + % hue component are divided by a factor of 360 + % in order to meet the range requirements of the + % PDF specification. + + % First we do H = (H * 360.0) % 360 + 3 2 roll 360 mul 3 1 roll + + % Compute C = V * S + dup 3 1 roll mul + + % P = (H / 60) % 2 + 3 2 roll dup 60 div 2 + 2 copy div cvi mul exch sub abs + + % X = C * (1 - |P - 1|) + 1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul + + % Compute m = V - C + exch dup 5 4 roll exch sub + + % Rotate so H is top + 4 3 roll exch 4 1 roll + + % Construct the RGB stack + dup 60 lt { + % We need to build: (C, X, 0) + pop 0 3 1 roll + } { + dup 120 lt { + % We need to build: (X, C, 0) + pop exch 0 3 1 roll + } { + dup 180 lt { + % We need to build: (0, C, X) + pop 0 + } { + dup 240 lt { + % We need to build: (0, X, C) + pop exch 0 + } { + 300 lt { + % We need to build: (X, 0, C) + 0 3 2 roll + } { + % We need to build: (C, 0, X) + 0 exch + } ifelse + } ifelse + } ifelse + } ifelse + } ifelse + + 4 3 roll + + % Add m to each component + dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch +} \ No newline at end of file diff --git a/crates/typst-pdf/src/postscript/oklab.ps b/crates/typst-pdf/src/postscript/oklab.ps new file mode 100644 index 00000000..4d6e9ad5 --- /dev/null +++ b/crates/typst-pdf/src/postscript/oklab.ps @@ -0,0 +1,78 @@ +{ + % Starting stack: L, A, B + % /!\ WARNING: The A and B components **MUST** be encoded + % in the range [0, 1] before calling this function. + % This is because the function assumes that the + % A and B components are offset by a factor of 0.4 + % in order to meet the range requirements of the + % PDF specification. + + exch 0.4 sub + exch 0.4 sub + + % Load L a and b into the stack + 2 index + 2 index + 2 index + + % Compute f1 = ((0.3963377774 * a) + (0.2158037573 * b) + L)^3 + 0.2158037573 mul exch + 0.3963377774 mul add add + dup dup mul mul + + % Load L, a, and b into the stack + 3 index + 3 index + 3 index + + % Compute f2 = ((-0.1055613458 * a) + (-0.0638541728 * b) + L)^3 + -0.0638541728 mul exch + -0.1055613458 mul add add + dup dup mul mul + + % Load L, a, and b into the stack + 4 index + 4 index + 4 index + + % Compute f3 = ((-0.0894841775 * a) + (-1.2914855480 * b) + L)^3 + -1.2914855480 mul exch + -0.0894841775 mul add add + dup dup mul mul + + % Discard L, a, and b by rolling the stack and popping + 6 3 roll pop pop pop + + % Load f1, f2, and f3 into the stack + 2 index + 2 index + 2 index + + % Compute R = f1 * 4.0767416621 + f2 * -3.3077115913 + f3 * 0.2309699292 + 0.2309699292 mul exch + -3.3077115913 mul add exch + 4.0767416621 mul add + + % Load f1, f2, and f3 into the stack + 3 index + 3 index + 3 index + + % Compute G = f1 * -1.2684380046 + f2 * 2.6097574011 + f3 * -0.3413193965 + -0.3413193965 mul exch + 2.6097574011 mul add exch + -1.2684380046 mul add + + % Load f1, f2, and f3 into the stack + 4 index + 4 index + 4 index + + % Compute B = f1 * -0.0041960863 + f2 * -0.7034186147 + f3 * 1.7076147010 + 1.7076147010 mul exch + -0.7034186147 mul add exch + -0.0041960863 mul add + + % Discard f1, f2, and f3 by rolling the stack and popping + 6 3 roll pop pop pop +} \ No newline at end of file diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index 4a095509..1edd9946 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -18,42 +18,31 @@ bench = false [dependencies] typst-macros = { workspace = true } typst-syntax = { workspace = true } -base64 = { workspace = true } bitflags = { workspace = true } -bytemuck = { workspace = true } comemo = { workspace = true } ecow = { workspace = true} -flate2 = { workspace = true } fontdb = { workspace = true } image = { workspace = true } indexmap = { workspace = true } kurbo = { workspace = true } lasso = { workspace = true } log = { workspace = true } -miniz_oxide = { workspace = true } once_cell = { workspace = true } palette = { workspace = true } -pdf-writer = { workspace = true } regex = { workspace = true } roxmltree = { workspace = true } rustybuzz = { workspace = true } serde = { workspace = true } siphasher = { workspace = true } smallvec = { workspace = true } -subsetter = { workspace = true } -svg2pdf = { workspace = true } time = { workspace = true } toml = { workspace = true } tracing = { workspace = true } ttf-parser = { workspace = true } -unicode-ident = { workspace = true } unicode-math-class = { workspace = true } -unicode-properties = { workspace = true } unicode-segmentation = { workspace = true } -unscanny = { workspace = true } usvg = { workspace = true } wasmi = { workspace = true } -xmp-writer = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -stacker = "0.1.15" +stacker = { workspace = true } diff --git a/crates/typst/src/doc.rs b/crates/typst/src/doc.rs index 742c6245..48f201dc 100644 --- a/crates/typst/src/doc.rs +++ b/crates/typst/src/doc.rs @@ -9,7 +9,6 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use crate::eval::{cast, dict, ty, Datetime, Dict, Repr, Value}; -use crate::export::PdfPageLabel; use crate::font::Font; use crate::geom::{ self, styled_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke, @@ -835,6 +834,45 @@ impl From for Dict { } } +/// Specification for a PDF page label. +#[derive(Debug, Clone, PartialEq, Hash, Default)] +pub struct PdfPageLabel { + /// Can be any string or none. Will always be prepended to the numbering style. + pub prefix: Option, + /// Based on the numbering pattern. + /// + /// If `None` or numbering is a function, the field will be empty. + pub style: Option, + /// Offset for the page label start. + /// + /// Describes where to start counting from when setting a style. + /// (Has to be greater or equal than 1) + pub offset: Option, +} + +impl Repr for PdfPageLabel { + fn repr(&self) -> EcoString { + eco_format!("{self:?}") + } +} + +/// A PDF page label number style. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum PdfPageLabelStyle { + /// Decimal arabic numerals (1, 2, 3). + Arabic, + /// Lowercase roman numerals (i, ii, iii). + LowerRoman, + /// Uppercase roman numerals (I, II, III). + UpperRoman, + /// Lowercase letters (`a` to `z` for the first 26 pages, + /// `aa` to `zz` and so on for the next). + LowerAlpha, + /// Uppercase letters (`A` to `Z` for the first 26 pages, + /// `AA` to `ZZ` and so on for the next). + UpperAlpha, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/typst/src/eval/library.rs b/crates/typst/src/eval/library.rs index c712adae..cd3d09d3 100644 --- a/crates/typst/src/eval/library.rs +++ b/crates/typst/src/eval/library.rs @@ -167,8 +167,13 @@ pub fn set_lang_items(items: LangItems) { } /// Access a lang item. -macro_rules! item { +#[macro_export] +#[doc(hidden)] +macro_rules! __item { ($name:ident) => { $crate::eval::LANG_ITEMS.get().unwrap().$name }; } + +#[doc(inline)] +pub use crate::__item as item; diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index 8ac2c40b..26de4399 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -55,7 +55,7 @@ pub use self::fields::fields_on; pub use self::func::{ func, CapturesVisitor, Func, NativeFunc, NativeFuncData, ParamInfo, }; -pub use self::library::{set_lang_items, LangItems, Library}; +pub use self::library::{item, set_lang_items, LangItems, Library}; pub use self::methods::mutable_methods_on; pub use self::module::Module; pub use self::none::NoneValue; diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs index 093eaa5f..e0c3b984 100644 --- a/crates/typst/src/eval/ops.rs +++ b/crates/typst/src/eval/ops.rs @@ -6,6 +6,7 @@ use ecow::eco_format; use super::{format_str, IntoValue, Regex, Repr, Value}; use crate::diag::{bail, StrResult}; +use crate::eval::item; use crate::geom::{Align, Length, Numeric, Rel, Smart, Stroke}; use Value::*; diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs index b87c8592..1d866418 100644 --- a/crates/typst/src/eval/value.rs +++ b/crates/typst/src/eval/value.rs @@ -17,7 +17,7 @@ use super::{ Version, }; use crate::diag::StrResult; -use crate::eval::Datetime; +use crate::eval::{item, Datetime}; use crate::geom::{Abs, Angle, Color, Em, Fr, Gradient, Length, Ratio, Rel}; use crate::model::{Label, Styles}; use crate::syntax::{ast, Span}; diff --git a/crates/typst/src/export/mod.rs b/crates/typst/src/export/mod.rs deleted file mode 100644 index bdcc9c40..00000000 --- a/crates/typst/src/export/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Exporting into external formats. - -mod pdf; - -pub use self::pdf::{pdf, PdfPageLabel, PdfPageLabelStyle}; diff --git a/crates/typst/src/export/pdf/color.rs b/crates/typst/src/export/pdf/color.rs deleted file mode 100644 index d5b6f3f1..00000000 --- a/crates/typst/src/export/pdf/color.rs +++ /dev/null @@ -1,468 +0,0 @@ -use once_cell::sync::Lazy; -use pdf_writer::types::DeviceNSubtype; -use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref}; - -use super::page::{PageContext, Transforms}; -use crate::export::pdf::deflate; -use crate::geom::{Color, ColorSpace, Paint}; - -// The names of the color spaces. -pub const SRGB: Name<'static> = Name(b"srgb"); -pub const D65_GRAY: Name<'static> = Name(b"d65gray"); -pub const OKLAB: Name<'static> = Name(b"oklab"); -pub const HSV: Name<'static> = Name(b"hsv"); -pub const HSL: Name<'static> = Name(b"hsl"); -pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb"); - -// The names of the color components. -const OKLAB_L: Name<'static> = Name(b"L"); -const OKLAB_A: Name<'static> = Name(b"A"); -const OKLAB_B: Name<'static> = Name(b"B"); -const HSV_H: Name<'static> = Name(b"H"); -const HSV_S: Name<'static> = Name(b"S"); -const HSV_V: Name<'static> = Name(b"V"); -const HSL_H: Name<'static> = Name(b"H"); -const HSL_S: Name<'static> = Name(b"S"); -const HSL_L: Name<'static> = Name(b"L"); - -// The ICC profiles. -static SRGB_ICC_DEFLATED: Lazy> = - Lazy::new(|| deflate(include_bytes!("icc/sRGB-v4.icc"))); -static GRAY_ICC_DEFLATED: Lazy> = - Lazy::new(|| deflate(include_bytes!("icc/sGrey-v4.icc"))); - -// The PostScript functions for color spaces. -static OKLAB_DEFLATED: Lazy> = - Lazy::new(|| deflate(minify(include_str!("postscript/oklab.ps")).as_bytes())); -static HSV_DEFLATED: Lazy> = - Lazy::new(|| deflate(minify(include_str!("postscript/hsv.ps")).as_bytes())); -static HSL_DEFLATED: Lazy> = - Lazy::new(|| deflate(minify(include_str!("postscript/hsl.ps")).as_bytes())); - -/// The color spaces present in the PDF document -#[derive(Default)] -pub struct ColorSpaces { - oklab: Option, - srgb: Option, - d65_gray: Option, - hsv: Option, - hsl: Option, - use_linear_rgb: bool, -} - -impl ColorSpaces { - /// Get a reference to the oklab color space. - /// - /// # Warning - /// The A and B components of the color must be offset by +0.4 before being - /// encoded into the PDF file. - pub fn oklab(&mut self, alloc: &mut Ref) -> Ref { - *self.oklab.get_or_insert_with(|| alloc.bump()) - } - - /// Get a reference to the srgb color space. - pub fn srgb(&mut self, alloc: &mut Ref) -> Ref { - *self.srgb.get_or_insert_with(|| alloc.bump()) - } - - /// Get a reference to the gray color space. - pub fn d65_gray(&mut self, alloc: &mut Ref) -> Ref { - *self.d65_gray.get_or_insert_with(|| alloc.bump()) - } - - /// Get a reference to the hsv color space. - /// - /// # Warning - /// The Hue component of the color must be in degrees and must be divided - /// by 360.0 before being encoded into the PDF file. - pub fn hsv(&mut self, alloc: &mut Ref) -> Ref { - *self.hsv.get_or_insert_with(|| alloc.bump()) - } - - /// Get a reference to the hsl color space. - /// - /// # Warning - /// The Hue component of the color must be in degrees and must be divided - /// by 360.0 before being encoded into the PDF file. - pub fn hsl(&mut self, alloc: &mut Ref) -> Ref { - *self.hsl.get_or_insert_with(|| alloc.bump()) - } - - /// Mark linear RGB as used. - pub fn linear_rgb(&mut self) { - self.use_linear_rgb = true; - } - - /// Write the color space on usage. - pub fn write( - &mut self, - color_space: ColorSpace, - writer: writers::ColorSpace, - alloc: &mut Ref, - ) { - match color_space { - ColorSpace::Oklab => { - let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]); - self.write(ColorSpace::LinearRgb, oklab.alternate_color_space(), alloc); - oklab.tint_ref(self.oklab(alloc)); - oklab.attrs().subtype(DeviceNSubtype::DeviceN); - } - ColorSpace::Srgb => writer.icc_based(self.srgb(alloc)), - ColorSpace::D65Gray => writer.icc_based(self.d65_gray(alloc)), - ColorSpace::LinearRgb => { - writer.cal_rgb( - [0.9505, 1.0, 1.0888], - None, - Some([1.0, 1.0, 1.0]), - Some([ - 0.4124, 0.2126, 0.0193, 0.3576, 0.715, 0.1192, 0.1805, 0.0722, - 0.9505, - ]), - ); - } - ColorSpace::Hsl => { - let mut hsl = writer.device_n([HSL_H, HSL_S, HSL_L]); - self.write(ColorSpace::Srgb, hsl.alternate_color_space(), alloc); - hsl.tint_ref(self.hsl(alloc)); - hsl.attrs().subtype(DeviceNSubtype::DeviceN); - } - ColorSpace::Hsv => { - let mut hsv = writer.device_n([HSV_H, HSV_S, HSV_V]); - self.write(ColorSpace::Srgb, hsv.alternate_color_space(), alloc); - hsv.tint_ref(self.hsv(alloc)); - hsv.attrs().subtype(DeviceNSubtype::DeviceN); - } - ColorSpace::Cmyk => writer.device_cmyk(), - } - } - - // Write the color spaces to the PDF file. - pub fn write_color_spaces(&mut self, mut spaces: Dict, alloc: &mut Ref) { - if self.oklab.is_some() { - self.write(ColorSpace::Oklab, spaces.insert(OKLAB).start(), alloc); - } - - if self.srgb.is_some() { - self.write(ColorSpace::Srgb, spaces.insert(SRGB).start(), alloc); - } - - if self.d65_gray.is_some() { - self.write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), alloc); - } - - if self.hsv.is_some() { - self.write(ColorSpace::Hsv, spaces.insert(HSV).start(), alloc); - } - - if self.hsl.is_some() { - self.write(ColorSpace::Hsl, spaces.insert(HSL).start(), alloc); - } - - if self.use_linear_rgb { - self.write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), alloc); - } - } - - /// Write the necessary color spaces functions and ICC profiles to the - /// PDF file. - pub fn write_functions(&self, chunk: &mut Chunk) { - // Write the Oklab function & color space. - if let Some(oklab) = self.oklab { - chunk - .post_script_function(oklab, &OKLAB_DEFLATED) - .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .filter(Filter::FlateDecode); - } - - // Write the HSV function & color space. - if let Some(hsv) = self.hsv { - chunk - .post_script_function(hsv, &HSV_DEFLATED) - .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .filter(Filter::FlateDecode); - } - - // Write the HSL function & color space. - if let Some(hsl) = self.hsl { - chunk - .post_script_function(hsl, &HSL_DEFLATED) - .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .filter(Filter::FlateDecode); - } - - // Write the sRGB color space. - if let Some(srgb) = self.srgb { - chunk - .icc_profile(srgb, &SRGB_ICC_DEFLATED) - .n(3) - .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .filter(Filter::FlateDecode); - } - - // Write the gray color space. - if let Some(gray) = self.d65_gray { - chunk - .icc_profile(gray, &GRAY_ICC_DEFLATED) - .n(1) - .range([0.0, 1.0]) - .filter(Filter::FlateDecode); - } - } -} - -/// This function removes comments, line spaces and carriage returns from a -/// PostScript program. This is necessary to optimize the size of the PDF file. -fn minify(source: &str) -> String { - let mut buf = String::with_capacity(source.len()); - let mut s = unscanny::Scanner::new(source); - while let Some(c) = s.eat() { - match c { - '%' => { - s.eat_until('\n'); - } - c if c.is_whitespace() => { - s.eat_whitespace(); - if buf.ends_with(|c: char| !c.is_whitespace()) { - buf.push(' '); - } - } - _ => buf.push(c), - } - } - buf -} - -/// Encodes the color into four f32s, which can be used in a PDF file. -/// Ensures that the values are in the range [0.0, 1.0]. -/// -/// # Why? -/// - Oklab: The a and b components are in the range [-0.4, 0.4] and the PDF -/// specifies (and some readers enforce) that all color values be in the range -/// [0.0, 1.0]. This means that the PostScript function and the encoded color -/// must be offset by 0.4. -/// - HSV/HSL: The hue component is in the range [0.0, 360.0] and the PDF format -/// specifies that it must be in the range [0.0, 1.0]. This means that the -/// PostScript function and the encoded color must be divided by 360.0. -pub trait ColorEncode { - /// Performs the color to PDF f32 array conversion. - fn encode(&self, color: Color) -> [f32; 4]; -} - -impl ColorEncode for ColorSpace { - fn encode(&self, color: Color) -> [f32; 4] { - match self { - ColorSpace::Oklab => { - let [l, a, b, alpha] = color.to_oklab().to_vec4(); - [l, (a + 0.4).clamp(0.0, 1.0), (b + 0.4).clamp(0.0, 1.0), alpha] - } - ColorSpace::Hsl => { - let [h, s, l, _] = color.to_hsl().to_vec4(); - [h / 360.0, s, l, 0.0] - } - ColorSpace::Hsv => { - let [h, s, v, _] = color.to_hsv().to_vec4(); - [h / 360.0, s, v, 0.0] - } - _ => color.to_vec4(), - } - } -} - -/// Encodes a paint into either a fill or stroke color. -pub(super) trait PaintEncode { - /// Set the paint as the fill color. - fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms); - - /// Set the paint as the stroke color. - fn set_as_stroke(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms); -} - -impl PaintEncode for Paint { - fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) { - match self { - Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms), - Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms), - } - } - - fn set_as_stroke( - &self, - ctx: &mut PageContext, - on_text: bool, - transforms: Transforms, - ) { - match self { - Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms), - Self::Gradient(gradient) => gradient.set_as_stroke(ctx, on_text, transforms), - } - } -} - -impl PaintEncode for Color { - fn set_as_fill(&self, ctx: &mut PageContext, _: bool, _: Transforms) { - match self { - Color::Luma(_) => { - ctx.parent.colors.d65_gray(&mut ctx.parent.alloc); - ctx.set_fill_color_space(D65_GRAY); - - let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); - ctx.content.set_fill_color([l]); - } - Color::Oklab(_) => { - ctx.parent.colors.oklab(&mut ctx.parent.alloc); - ctx.set_fill_color_space(OKLAB); - - let [l, a, b, _] = ColorSpace::Oklab.encode(*self); - ctx.content.set_fill_color([l, a, b]); - } - Color::LinearRgb(_) => { - ctx.parent.colors.linear_rgb(); - ctx.set_fill_color_space(LINEAR_SRGB); - - let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); - ctx.content.set_fill_color([r, g, b]); - } - Color::Rgba(_) => { - ctx.parent.colors.srgb(&mut ctx.parent.alloc); - ctx.set_fill_color_space(SRGB); - - let [r, g, b, _] = ColorSpace::Srgb.encode(*self); - ctx.content.set_fill_color([r, g, b]); - } - Color::Cmyk(_) => { - ctx.reset_fill_color_space(); - - let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); - ctx.content.set_fill_cmyk(c, m, y, k); - } - Color::Hsl(_) => { - ctx.parent.colors.hsl(&mut ctx.parent.alloc); - ctx.set_fill_color_space(HSL); - - let [h, s, l, _] = ColorSpace::Hsl.encode(*self); - ctx.content.set_fill_color([h, s, l]); - } - Color::Hsv(_) => { - ctx.parent.colors.hsv(&mut ctx.parent.alloc); - ctx.set_fill_color_space(HSV); - - let [h, s, v, _] = ColorSpace::Hsv.encode(*self); - ctx.content.set_fill_color([h, s, v]); - } - } - } - - fn set_as_stroke(&self, ctx: &mut PageContext, _: bool, _: Transforms) { - match self { - Color::Luma(_) => { - ctx.parent.colors.d65_gray(&mut ctx.parent.alloc); - ctx.set_stroke_color_space(D65_GRAY); - - let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); - ctx.content.set_stroke_color([l]); - } - Color::Oklab(_) => { - ctx.parent.colors.oklab(&mut ctx.parent.alloc); - ctx.set_stroke_color_space(OKLAB); - - let [l, a, b, _] = ColorSpace::Oklab.encode(*self); - ctx.content.set_stroke_color([l, a, b]); - } - Color::LinearRgb(_) => { - ctx.parent.colors.linear_rgb(); - ctx.set_stroke_color_space(LINEAR_SRGB); - - let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); - ctx.content.set_stroke_color([r, g, b]); - } - Color::Rgba(_) => { - ctx.parent.colors.srgb(&mut ctx.parent.alloc); - ctx.set_stroke_color_space(SRGB); - - let [r, g, b, _] = ColorSpace::Srgb.encode(*self); - ctx.content.set_stroke_color([r, g, b]); - } - Color::Cmyk(_) => { - ctx.reset_stroke_color_space(); - - let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); - ctx.content.set_stroke_cmyk(c, m, y, k); - } - Color::Hsl(_) => { - ctx.parent.colors.hsl(&mut ctx.parent.alloc); - ctx.set_stroke_color_space(HSL); - - let [h, s, l, _] = ColorSpace::Hsl.encode(*self); - ctx.content.set_stroke_color([h, s, l]); - } - Color::Hsv(_) => { - ctx.parent.colors.hsv(&mut ctx.parent.alloc); - ctx.set_stroke_color_space(HSV); - - let [h, s, v, _] = ColorSpace::Hsv.encode(*self); - ctx.content.set_stroke_color([h, s, v]); - } - } - } -} - -/// Extra color space functions. -pub(super) trait ColorSpaceExt { - /// Returns the range of the color space. - fn range(self) -> [f32; 6]; - - /// Converts a color to the color space. - fn convert(self, color: Color) -> [U; 3]; -} - -impl ColorSpaceExt for ColorSpace { - fn range(self) -> [f32; 6] { - [0.0, 1.0, 0.0, 1.0, 0.0, 1.0] - } - - fn convert(self, color: Color) -> [U; 3] { - let range = self.range(); - let [x, y, z, _] = color.to_space(self).to_vec4(); - - // We need to add 0.4 to y and z for Oklab - // This is because DeviceN color spaces in PDF can **only** be in - // the range 0..1 and some readers enforce that. - // The oklab color space is in the range -0.4..0.4 - // Also map the angle range of HSV/HSL to 0..1 instead of 0..360 - let [x, y, z] = match self { - Self::Oklab => [x, y + 0.4, z + 0.4], - Self::Hsv | Self::Hsl => [x / 360.0, y, z], - _ => [x, y, z], - }; - - [ - U::quantize(x, [range[0], range[1]]), - U::quantize(y, [range[2], range[3]]), - U::quantize(z, [range[4], range[5]]), - ] - } -} - -/// Quantizes a color component to a specific type. -pub(super) trait QuantizedColor { - fn quantize(color: f32, range: [f32; 2]) -> Self; -} - -impl QuantizedColor for u16 { - fn quantize(color: f32, range: [f32; 2]) -> Self { - let value = (color - range[0]) / (range[1] - range[0]); - (value.max(0.0).min(1.0) * Self::MAX as f32) - .round() - .max(0.0) - .min(Self::MAX as f32) as Self - } -} - -impl QuantizedColor for f32 { - fn quantize(color: f32, [min, max]: [f32; 2]) -> Self { - color.clamp(min, max) - } -} diff --git a/crates/typst/src/export/pdf/extg.rs b/crates/typst/src/export/pdf/extg.rs deleted file mode 100644 index f7bd116d..00000000 --- a/crates/typst/src/export/pdf/extg.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::PdfContext; - -/// A PDF external graphics state. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub struct ExtGState { - // In the range 0-255, needs to be divided before being written into the graphics state! - pub stroke_opacity: u8, - // In the range 0-255, needs to be divided before being written into the graphics state! - pub fill_opacity: u8, -} - -impl Default for ExtGState { - fn default() -> Self { - Self { stroke_opacity: 255, fill_opacity: 255 } - } -} - -impl ExtGState { - pub fn uses_opacities(&self) -> bool { - self.stroke_opacity != 255 || self.fill_opacity != 255 - } -} - -/// Embed all used external graphics states into the PDF. -#[tracing::instrument(skip_all)] -pub fn write_external_graphics_states(ctx: &mut PdfContext) { - for external_gs in ctx.extg_map.items() { - let id = ctx.alloc.bump(); - ctx.ext_gs_refs.push(id); - ctx.pdf - .ext_graphics(id) - .non_stroking_alpha(external_gs.fill_opacity as f32 / 255.0) - .stroking_alpha(external_gs.stroke_opacity as f32 / 255.0); - } -} diff --git a/crates/typst/src/export/pdf/font.rs b/crates/typst/src/export/pdf/font.rs deleted file mode 100644 index d9992704..00000000 --- a/crates/typst/src/export/pdf/font.rs +++ /dev/null @@ -1,268 +0,0 @@ -use std::collections::BTreeMap; -use std::sync::Arc; - -use ecow::{eco_format, EcoString}; -use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; -use pdf_writer::{Filter, Finish, Name, Rect, Str}; -use ttf_parser::{name_id, GlyphId, Tag}; -use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; - -use super::{deflate, EmExt, PdfContext}; -use crate::font::Font; -use crate::util::SliceExt; - -const CFF: Tag = Tag::from_bytes(b"CFF "); -const CFF2: Tag = Tag::from_bytes(b"CFF2"); -const CMAP_NAME: Name = Name(b"Custom"); -const SYSTEM_INFO: SystemInfo = SystemInfo { - registry: Str(b"Adobe"), - ordering: Str(b"Identity"), - supplement: 0, -}; - -/// Embed all used fonts into the PDF. -#[tracing::instrument(skip_all)] -pub fn write_fonts(ctx: &mut PdfContext) { - for font in ctx.font_map.items() { - let type0_ref = ctx.alloc.bump(); - let cid_ref = ctx.alloc.bump(); - let descriptor_ref = ctx.alloc.bump(); - let cmap_ref = ctx.alloc.bump(); - let data_ref = ctx.alloc.bump(); - ctx.font_refs.push(type0_ref); - - let glyph_set = ctx.glyph_sets.get_mut(font).unwrap(); - let metrics = font.metrics(); - let ttf = font.ttf(); - - // Do we have a TrueType or CFF font? - // - // FIXME: CFF2 must be handled differently and requires PDF 2.0 - // (or we have to convert it to CFF). - let is_cff = ttf - .raw_face() - .table(CFF) - .or_else(|| ttf.raw_face().table(CFF2)) - .is_some(); - - let postscript_name = font - .find_name(name_id::POST_SCRIPT_NAME) - .unwrap_or_else(|| "unknown".to_string()); - - let subset_tag = subset_tag(glyph_set); - let base_font = eco_format!("{subset_tag}+{postscript_name}"); - let base_font_type0 = if is_cff { - eco_format!("{base_font}-Identity-H") - } else { - base_font.clone() - }; - - // Write the base font object referencing the CID font. - ctx.pdf - .type0_font(type0_ref) - .base_font(Name(base_font_type0.as_bytes())) - .encoding_predefined(Name(b"Identity-H")) - .descendant_font(cid_ref) - .to_unicode(cmap_ref); - - // Write the CID font referencing the font descriptor. - let mut cid = ctx.pdf.cid_font(cid_ref); - cid.subtype(if is_cff { CidFontType::Type0 } else { CidFontType::Type2 }); - cid.base_font(Name(base_font.as_bytes())); - cid.system_info(SYSTEM_INFO); - cid.font_descriptor(descriptor_ref); - cid.default_width(0.0); - if !is_cff { - cid.cid_to_gid_map_predefined(Name(b"Identity")); - } - - // Extract the widths of all glyphs. - let mut widths = vec![]; - for gid in std::iter::once(0).chain(glyph_set.keys().copied()) { - let width = ttf.glyph_hor_advance(GlyphId(gid)).unwrap_or(0); - let units = font.to_em(width).to_font_units(); - let cid = glyph_cid(font, gid); - if usize::from(cid) >= widths.len() { - widths.resize(usize::from(cid) + 1, 0.0); - widths[usize::from(cid)] = units; - } - } - - // Write all non-zero glyph widths. - let mut first = 0; - let mut width_writer = cid.widths(); - for (w, group) in widths.group_by_key(|&w| w) { - let end = first + group.len(); - if w != 0.0 { - let last = end - 1; - width_writer.same(first as u16, last as u16, w); - } - first = end; - } - - width_writer.finish(); - cid.finish(); - - let mut flags = FontFlags::empty(); - flags.set(FontFlags::SERIF, postscript_name.contains("Serif")); - flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); - flags.set(FontFlags::ITALIC, ttf.is_italic()); - flags.insert(FontFlags::SYMBOLIC); - flags.insert(FontFlags::SMALL_CAP); - - let global_bbox = ttf.global_bounding_box(); - let bbox = Rect::new( - font.to_em(global_bbox.x_min).to_font_units(), - font.to_em(global_bbox.y_min).to_font_units(), - font.to_em(global_bbox.x_max).to_font_units(), - font.to_em(global_bbox.y_max).to_font_units(), - ); - - let italic_angle = ttf.italic_angle().unwrap_or(0.0); - let ascender = metrics.ascender.to_font_units(); - let descender = metrics.descender.to_font_units(); - let cap_height = metrics.cap_height.to_font_units(); - let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); - - // Write the font descriptor (contains metrics about the font). - let mut font_descriptor = ctx.pdf.font_descriptor(descriptor_ref); - font_descriptor - .name(Name(base_font.as_bytes())) - .flags(flags) - .bbox(bbox) - .italic_angle(italic_angle) - .ascent(ascender) - .descent(descender) - .cap_height(cap_height) - .stem_v(stem_v); - - if is_cff { - font_descriptor.font_file3(data_ref); - } else { - font_descriptor.font_file2(data_ref); - } - - font_descriptor.finish(); - - // Write the /ToUnicode character map, which maps glyph ids back to - // unicode codepoints to enable copying out of the PDF. - let cmap = create_cmap(ttf, glyph_set); - ctx.pdf.cmap(cmap_ref, &cmap.finish()); - - // Subset and write the font's bytes. - let glyphs: Vec<_> = glyph_set.keys().copied().collect(); - let data = subset_font(font, &glyphs); - - let mut stream = ctx.pdf.stream(data_ref, &data); - stream.filter(Filter::FlateDecode); - if is_cff { - stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C")); - } - - stream.finish(); - } -} - -/// Subset a font to the given glyphs. -/// -/// - For a font with TrueType outlines, this returns the whole OpenType font. -/// - For a font with CFF outlines, this returns just the CFF font program. -#[comemo::memoize] -fn subset_font(font: &Font, glyphs: &[u16]) -> Arc> { - let data = font.data(); - let profile = subsetter::Profile::pdf(glyphs); - let subsetted = subsetter::subset(data, font.index(), profile); - let mut data = subsetted.as_deref().unwrap_or(data); - - // Extract the standalone CFF font program if applicable. - let raw = ttf_parser::RawFace::parse(data, 0).unwrap(); - if let Some(cff) = raw.table(CFF) { - data = cff; - } - - Arc::new(deflate(data)) -} - -/// Produce a unique 6 letter tag for a glyph set. -fn subset_tag(glyphs: &BTreeMap) -> EcoString { - const LEN: usize = 6; - const BASE: u128 = 26; - let mut hash = crate::util::hash128(&glyphs); - let mut letter = [b'A'; LEN]; - for l in letter.iter_mut() { - *l = b'A' + (hash % BASE) as u8; - hash /= BASE; - } - std::str::from_utf8(&letter).unwrap().into() -} - -/// Create a /ToUnicode CMap. -fn create_cmap( - ttf: &ttf_parser::Face, - glyph_set: &mut BTreeMap, -) -> UnicodeCmap { - // For glyphs that have codepoints mapping to them in the font's cmap table, - // we prefer them over pre-existing text mappings from the document. Only - // things that don't have a corresponding codepoint (or only a private-use - // one) like the "Th" in Linux Libertine get the text of their first - // occurrences in the document instead. - for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) { - if !subtable.is_unicode() { - continue; - } - - subtable.codepoints(|n| { - let Some(c) = std::char::from_u32(n) else { return }; - if c.general_category() == GeneralCategory::PrivateUse { - return; - } - - let Some(GlyphId(g)) = ttf.glyph_index(c) else { return }; - if glyph_set.contains_key(&g) { - glyph_set.insert(g, c.into()); - } - }); - } - - // Produce a reverse mapping from glyphs to unicode strings. - let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); - for (&g, text) in glyph_set.iter() { - if !text.is_empty() { - cmap.pair_with_multiple(g, text.chars()); - } - } - - cmap -} - -/// Get the CID for a glyph id. -/// -/// When writing text into a PDF, we have to specify CIDs (character ids) not -/// GIDs (glyph IDs). -/// -/// Most of the time, the mapping between these two is an identity mapping. In -/// particular, for TrueType fonts, the mapping is an identity mapping because -/// of this line above: -/// ```ignore -/// cid.cid_to_gid_map_predefined(Name(b"Identity")); -/// ``` -/// -/// However, CID-keyed CFF fonts may have a non-identity mapping defined in -/// their charset. For those, we must map the glyph IDs in a `TextItem` to CIDs. -/// The font defines the map through its charset. The charset usually maps -/// glyphs to SIDs (string ids) specifying the glyph's name. Not for CID-keyed -/// fonts though! For these, the SIDs are CIDs in disguise. Relevant quote from -/// the CFF spec: -/// -/// > The charset data, although in the same format as non-CIDFonts, will -/// > represent CIDs rather than SIDs, [...] -/// -/// This function performs the mapping from glyph ID to CID. It also works for -/// non CID-keyed fonts. Then, it will simply return the glyph ID. -pub(super) fn glyph_cid(font: &Font, glyph_id: u16) -> u16 { - font.ttf() - .tables() - .cff - .and_then(|cff| cff.glyph_cid(ttf_parser::GlyphId(glyph_id))) - .unwrap_or(glyph_id) -} diff --git a/crates/typst/src/export/pdf/gradient.rs b/crates/typst/src/export/pdf/gradient.rs deleted file mode 100644 index 6ecf8588..00000000 --- a/crates/typst/src/export/pdf/gradient.rs +++ /dev/null @@ -1,582 +0,0 @@ -use std::f32::consts::{PI, TAU}; -use std::sync::Arc; - -use ecow::{eco_format, EcoString}; -use pdf_writer::types::FunctionShadingType; -use pdf_writer::writers::StreamShadingType; -use pdf_writer::{types::ColorSpaceOperand, Name}; -use pdf_writer::{Filter, Finish, Ref}; - -use super::color::{ColorSpaceExt, PaintEncode, QuantizedColor}; -use super::page::{PageContext, Transforms}; -use super::{AbsExt, PdfContext}; -use crate::export::pdf::deflate; -use crate::geom::{ - Abs, Angle, Color, ColorSpace, ConicGradient, Gradient, Numeric, Point, Quadrant, - Ratio, Relative, Transform, WeightedColor, -}; - -/// A unique-transform-aspect-ratio combination that will be encoded into the -/// PDF. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct PdfGradient { - /// The transform to apply to the gradient. - pub transform: Transform, - /// The aspect ratio of the gradient. - /// Required for aspect ratio correction. - pub aspect_ratio: Ratio, - /// The gradient. - pub gradient: Gradient, - /// Whether the gradient is applied to text. - pub on_text: bool, -} - -/// Writes the actual gradients (shading patterns) to the PDF. -/// This is performed once after writing all pages. -pub fn write_gradients(ctx: &mut PdfContext) { - for PdfGradient { transform, aspect_ratio, gradient, on_text } in - ctx.gradient_map.items().cloned().collect::>() - { - let shading = ctx.alloc.bump(); - ctx.gradient_refs.push(shading); - - let mut shading_pattern = match &gradient { - Gradient::Linear(linear) => { - let shading_function = shading_function(ctx, &gradient); - let mut shading_pattern = ctx.pdf.shading_pattern(shading); - let mut shading = shading_pattern.function_shading(); - shading.shading_type(FunctionShadingType::Axial); - - ctx.colors - .write(gradient.space(), shading.color_space(), &mut ctx.alloc); - - let angle = Gradient::correct_aspect_ratio(linear.angle, aspect_ratio); - let (sin, cos) = (angle.sin(), angle.cos()); - let length = sin.abs() + cos.abs(); - - shading - .anti_alias(gradient.anti_alias()) - .function(shading_function) - .coords([0.0, 0.0, length as f32, 0.0]) - .extend([true; 2]); - - shading.finish(); - - shading_pattern - } - Gradient::Radial(radial) => { - let shading_function = shading_function(ctx, &gradient); - let mut shading_pattern = ctx.pdf.shading_pattern(shading); - let mut shading = shading_pattern.function_shading(); - shading.shading_type(FunctionShadingType::Radial); - - ctx.colors - .write(gradient.space(), shading.color_space(), &mut ctx.alloc); - - shading - .anti_alias(gradient.anti_alias()) - .function(shading_function) - .coords([ - radial.focal_center.x.get() as f32, - radial.focal_center.y.get() as f32, - radial.focal_radius.get() as f32, - radial.center.x.get() as f32, - radial.center.y.get() as f32, - radial.radius.get() as f32, - ]) - .extend([true; 2]); - - shading.finish(); - - shading_pattern - } - Gradient::Conic(conic) => { - let vertices = compute_vertex_stream(conic, aspect_ratio, on_text); - - let stream_shading_id = ctx.alloc.bump(); - let mut stream_shading = - ctx.pdf.stream_shading(stream_shading_id, &vertices); - - ctx.colors.write( - conic.space, - stream_shading.color_space(), - &mut ctx.alloc, - ); - - let range = conic.space.range(); - stream_shading - .bits_per_coordinate(16) - .bits_per_component(16) - .bits_per_flag(8) - .shading_type(StreamShadingType::CoonsPatch) - .decode([ - 0.0, 1.0, 0.0, 1.0, range[0], range[1], range[2], range[3], - range[4], range[5], - ]) - .anti_alias(gradient.anti_alias()) - .filter(Filter::FlateDecode); - - stream_shading.finish(); - - let mut shading_pattern = ctx.pdf.shading_pattern(shading); - shading_pattern.shading_ref(stream_shading_id); - shading_pattern - } - }; - - shading_pattern.matrix(transform_to_array(transform)); - } -} - -/// Writes an expotential or stitched function that expresses the gradient. -fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref { - let function = ctx.alloc.bump(); - let mut functions = vec![]; - let mut bounds = vec![]; - let mut encode = vec![]; - - // Create the individual gradient functions for each pair of stops. - for window in gradient.stops_ref().windows(2) { - let (first, second) = (window[0], window[1]); - - // Skip stops with the same position. - if first.1.get() == second.1.get() { - continue; - } - - // If the color space is HSL or HSV, and we cross the 0°/360° boundary, - // we need to create two separate stops. - if gradient.space() == ColorSpace::Hsl || gradient.space() == ColorSpace::Hsv { - let t1 = first.1.get() as f32; - let t2 = second.1.get() as f32; - let [h1, s1, x1, _] = first.0.to_space(gradient.space()).to_vec4(); - let [h2, s2, x2, _] = second.0.to_space(gradient.space()).to_vec4(); - - // Compute the intermediary stop at 360°. - if (h1 - h2).abs() > 180.0 { - let h1 = if h1 < h2 { h1 + 360.0 } else { h1 }; - let h2 = if h2 < h1 { h2 + 360.0 } else { h2 }; - - // We compute where the crossing happens between zero and one - let t = (360.0 - h1) / (h2 - h1); - // We then map it back to the original range. - let t_prime = t * (t2 - t1) + t1; - - // If the crossing happens between the two stops, - // we need to create an extra stop. - if t_prime <= t2 && t_prime >= t1 { - bounds.push(t_prime); - bounds.push(t_prime); - bounds.push(t2); - encode.extend([0.0, 1.0]); - encode.extend([0.0, 1.0]); - encode.extend([0.0, 1.0]); - - // These need to be individual function to encode 360.0 correctly. - let func1 = ctx.alloc.bump(); - ctx.pdf - .exponential_function(func1) - .range(gradient.space().range()) - .c0(gradient.space().convert(first.0)) - .c1([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) - .domain([0.0, 1.0]) - .n(1.0); - - let func2 = ctx.alloc.bump(); - ctx.pdf - .exponential_function(func2) - .range(gradient.space().range()) - .c0([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) - .c1([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) - .domain([0.0, 1.0]) - .n(1.0); - - let func3 = ctx.alloc.bump(); - ctx.pdf - .exponential_function(func3) - .range(gradient.space().range()) - .c0([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) - .c1(gradient.space().convert(second.0)) - .domain([0.0, 1.0]) - .n(1.0); - - functions.push(func1); - functions.push(func2); - functions.push(func3); - - continue; - } - } - } - - bounds.push(second.1.get() as f32); - functions.push(single_gradient(ctx, first.0, second.0, gradient.space())); - encode.extend([0.0, 1.0]); - } - - // Special case for gradients with only two stops. - if functions.len() == 1 { - return functions[0]; - } - - // Remove the last bound, since it's not needed for the stitching function. - bounds.pop(); - - // Create the stitching function. - ctx.pdf - .stitching_function(function) - .domain([0.0, 1.0]) - .range(gradient.space().range()) - .functions(functions) - .bounds(bounds) - .encode(encode); - - function -} - -/// Writes an expontential function that expresses a single segment (between two -/// stops) of a gradient. -fn single_gradient( - ctx: &mut PdfContext, - first_color: Color, - second_color: Color, - color_space: ColorSpace, -) -> Ref { - let reference = ctx.alloc.bump(); - - ctx.pdf - .exponential_function(reference) - .range(color_space.range()) - .c0(color_space.convert(first_color)) - .c1(color_space.convert(second_color)) - .domain([0.0, 1.0]) - .n(1.0); - - reference -} - -impl PaintEncode for Gradient { - fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) { - ctx.reset_fill_color_space(); - - let id = register_gradient(ctx, self, on_text, transforms); - let name = Name(id.as_bytes()); - - ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_fill_pattern(None, name); - } - - fn set_as_stroke( - &self, - ctx: &mut PageContext, - on_text: bool, - transforms: Transforms, - ) { - ctx.reset_stroke_color_space(); - - let id = register_gradient(ctx, self, on_text, transforms); - let name = Name(id.as_bytes()); - - ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_stroke_pattern(None, name); - } -} - -/// Deduplicates a gradient to a named PDF resource. -fn register_gradient( - ctx: &mut PageContext, - gradient: &Gradient, - on_text: bool, - mut transforms: Transforms, -) -> EcoString { - // 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) { - Relative::Self_ => transforms.size, - Relative::Parent => transforms.container_size, - }; - - // Correction for y-axis flipping on text. - let angle = gradient.angle().unwrap_or_else(Angle::zero); - let angle = if on_text { Angle::rad(TAU as f64) - angle } else { angle }; - - let (offset_x, offset_y) = match gradient { - Gradient::Conic(conic) => ( - -size.x * (1.0 - conic.center.x.get() / 2.0) / 2.0, - -size.y * (1.0 - conic.center.y.get() / 2.0) / 2.0, - ), - _ => match angle.quadrant() { - Quadrant::First => (Abs::zero(), Abs::zero()), - Quadrant::Second => (size.x, Abs::zero()), - Quadrant::Third => (size.x, size.y), - Quadrant::Fourth => (Abs::zero(), size.y), - }, - }; - - let rotation = match gradient { - Gradient::Conic(_) => Angle::zero(), - _ => angle, - }; - - let transform = match gradient.unwrap_relative(on_text) { - Relative::Self_ => transforms.transform, - Relative::Parent => transforms.container_transform, - }; - - let scale_offset = match gradient { - Gradient::Conic(_) => 4.0_f64, - _ => 1.0, - }; - - let pdf_gradient = PdfGradient { - aspect_ratio: size.aspect_ratio(), - 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), - )) - .pre_concat(Transform::rotate(Gradient::correct_aspect_ratio( - rotation, - size.aspect_ratio(), - ))), - gradient: gradient.clone(), - on_text, - }; - - let index = ctx.parent.gradient_map.insert(pdf_gradient); - eco_format!("Gr{}", index) -} - -/// Convert to an array of floats. -fn transform_to_array(ts: Transform) -> [f32; 6] { - [ - ts.sx.get() as f32, - ts.ky.get() as f32, - ts.kx.get() as f32, - ts.sy.get() as f32, - ts.tx.to_f32(), - ts.ty.to_f32(), - ] -} - -/// Writes a single Coons Patch as defined in the PDF specification -/// to a binary vec. -/// -/// Structure: -/// - flag: `u8` -/// - points: `[u16; 24]` -/// - colors: `[u16; 12]` -fn write_patch( - target: &mut Vec, - t: f32, - t1: f32, - c0: [u16; 3], - c1: [u16; 3], - angle: Angle, - on_text: bool, -) { - let mut theta = -TAU * t + angle.to_rad() as f32 + PI; - let mut theta1 = -TAU * t1 + angle.to_rad() as f32 + PI; - - // Correction for y-axis flipping on text. - if on_text { - theta = (TAU - theta).rem_euclid(TAU); - theta1 = (TAU - theta1).rem_euclid(TAU); - } - - let (cp1, cp2) = - control_point(Point::new(Abs::pt(0.5), Abs::pt(0.5)), 0.5, theta, theta1); - - // Push the flag - target.push(0); - - let p1 = - [u16::quantize(0.5, [0.0, 1.0]).to_be(), u16::quantize(0.5, [0.0, 1.0]).to_be()]; - - let p2 = [ - u16::quantize(theta.cos(), [-1.0, 1.0]).to_be(), - u16::quantize(theta.sin(), [-1.0, 1.0]).to_be(), - ]; - - let p3 = [ - u16::quantize(theta1.cos(), [-1.0, 1.0]).to_be(), - u16::quantize(theta1.sin(), [-1.0, 1.0]).to_be(), - ]; - - let cp1 = [ - u16::quantize(cp1.x.to_f32(), [0.0, 1.0]).to_be(), - u16::quantize(cp1.y.to_f32(), [0.0, 1.0]).to_be(), - ]; - - let cp2 = [ - u16::quantize(cp2.x.to_f32(), [0.0, 1.0]).to_be(), - u16::quantize(cp2.y.to_f32(), [0.0, 1.0]).to_be(), - ]; - - // Push the points - target.extend_from_slice(bytemuck::cast_slice(&[ - p1, p1, p2, p2, cp1, cp2, p3, p3, p1, p1, p1, p1, - ])); - - let colors = - [c0.map(u16::to_be), c0.map(u16::to_be), c1.map(u16::to_be), c1.map(u16::to_be)]; - - // Push the colors. - target.extend_from_slice(bytemuck::cast_slice(&colors)); -} - -fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point, Point) { - let n = (TAU / (angle_end - angle_start)).abs(); - let f = ((angle_end - angle_start) / n).tan() * 4.0 / 3.0; - - let p1 = c + Point::new( - Abs::pt((r * angle_start.cos() - f * r * angle_start.sin()) as f64), - Abs::pt((r * angle_start.sin() + f * r * angle_start.cos()) as f64), - ); - - let p2 = c + Point::new( - Abs::pt((r * angle_end.cos() + f * r * angle_end.sin()) as f64), - Abs::pt((r * angle_end.sin() - f * r * angle_end.cos()) as f64), - ); - - (p1, p2) -} - -#[comemo::memoize] -fn compute_vertex_stream( - conic: &ConicGradient, - aspect_ratio: Ratio, - on_text: bool, -) -> Arc> { - // Generated vertices for the Coons patches - let mut vertices = Vec::new(); - - // Correct the gradient's angle - let angle = Gradient::correct_aspect_ratio(conic.angle, aspect_ratio); - - // We want to generate a vertex based on some conditions, either: - // - At the boundary of a stop - // - At the boundary of a quadrant - // - When we cross the boundary of a hue turn (for HSV and HSL only) - for window in conic.stops.windows(2) { - let ((c0, t0), (c1, t1)) = (window[0], window[1]); - - // Skip stops with the same position - if t0 == t1 { - continue; - } - - // If the angle between the two stops is greater than 90 degrees, we need to - // generate a vertex at the boundary of the quadrant. - // However, we add more stops in-between to make the gradient smoother, so we - // need to generate a vertex at least every 5 degrees. - // If the colors are the same, we do it every quadrant only. - let slope = 1.0 / (t1.get() - t0.get()); - let mut t_x = t0.get(); - let dt = (t1.get() - t0.get()).min(0.25); - while t_x < t1.get() { - let t_next = (t_x + dt).min(t1.get()); - - let t1 = slope * (t_x - t0.get()); - let t2 = slope * (t_next - t0.get()); - - // We don't use `Gradient::sample` to avoid issues with sharp gradients. - let c = Color::mix_iter( - [WeightedColor::new(c0, 1.0 - t1), WeightedColor::new(c1, t1)], - conic.space, - ) - .unwrap(); - - let c_next = Color::mix_iter( - [WeightedColor::new(c0, 1.0 - t2), WeightedColor::new(c1, t2)], - conic.space, - ) - .unwrap(); - - // If the color space is HSL or HSV, and we cross the 0°/360° boundary, - // we need to create two separate stops. - if conic.space == ColorSpace::Hsl || conic.space == ColorSpace::Hsv { - let [h1, s1, x1, _] = c.to_space(conic.space).to_vec4(); - let [h2, s2, x2, _] = c_next.to_space(conic.space).to_vec4(); - - // Compute the intermediary stop at 360°. - if (h1 - h2).abs() > 180.0 { - let h1 = if h1 < h2 { h1 + 360.0 } else { h1 }; - let h2 = if h2 < h1 { h2 + 360.0 } else { h2 }; - - // We compute where the crossing happens between zero and one - let t = (360.0 - h1) / (h2 - h1); - // We then map it back to the original range. - let t_prime = t * (t_next as f32 - t_x as f32) + t_x as f32; - - // If the crossing happens between the two stops, - // we need to create an extra stop. - if t_prime <= t_next as f32 && t_prime >= t_x as f32 { - let c0 = [1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]; - let c1 = [0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]; - let c0 = c0.map(|c| u16::quantize(c, [0.0, 1.0])); - let c1 = c1.map(|c| u16::quantize(c, [0.0, 1.0])); - - write_patch( - &mut vertices, - t_x as f32, - t_prime, - conic.space.convert(c), - c0, - angle, - on_text, - ); - - write_patch( - &mut vertices, - t_prime, - t_prime, - c0, - c1, - angle, - on_text, - ); - - write_patch( - &mut vertices, - t_prime, - t_next as f32, - c1, - conic.space.convert(c_next), - angle, - on_text, - ); - - t_x = t_next; - continue; - } - } - } - - write_patch( - &mut vertices, - t_x as f32, - t_next as f32, - conic.space.convert(c), - conic.space.convert(c_next), - angle, - on_text, - ); - - t_x = t_next; - } - } - - Arc::new(deflate(&vertices)) -} diff --git a/crates/typst/src/export/pdf/icc/sGrey-v4.icc b/crates/typst/src/export/pdf/icc/sGrey-v4.icc deleted file mode 100644 index 2187b678..00000000 Binary files a/crates/typst/src/export/pdf/icc/sGrey-v4.icc and /dev/null differ diff --git a/crates/typst/src/export/pdf/icc/sRGB-v4.icc b/crates/typst/src/export/pdf/icc/sRGB-v4.icc deleted file mode 100644 index d9f3c055..00000000 Binary files a/crates/typst/src/export/pdf/icc/sRGB-v4.icc and /dev/null differ diff --git a/crates/typst/src/export/pdf/image.rs b/crates/typst/src/export/pdf/image.rs deleted file mode 100644 index 7264c8c5..00000000 --- a/crates/typst/src/export/pdf/image.rs +++ /dev/null @@ -1,170 +0,0 @@ -use std::collections::HashMap; -use std::io::Cursor; -use std::sync::Arc; - -use image::{DynamicImage, GenericImageView, Rgba}; -use pdf_writer::{Chunk, Filter, Finish, Ref}; - -use super::{deflate, PdfContext}; -use crate::geom::ColorSpace; -use crate::image::{ImageKind, RasterFormat, RasterImage, SvgImage}; - -/// Embed all used images into the PDF. -#[tracing::instrument(skip_all)] -pub fn write_images(ctx: &mut PdfContext) { - for image in ctx.image_map.items() { - // Add the primary image. - match image.kind() { - ImageKind::Raster(raster) => { - // TODO: Error if image could not be encoded. - let (data, filter, has_color) = encode_raster_image(raster); - let width = image.width(); - let height = image.height(); - - let image_ref = ctx.alloc.bump(); - ctx.image_refs.push(image_ref); - - let mut image = ctx.pdf.image_xobject(image_ref, &data); - image.filter(filter); - image.width(width as i32); - image.height(height as i32); - image.bits_per_component(8); - - let mut icc_ref = None; - let space = image.color_space(); - if raster.icc().is_some() { - let id = ctx.alloc.bump(); - space.icc_based(id); - icc_ref = Some(id); - } else if has_color { - ctx.colors.write(ColorSpace::Srgb, space, &mut ctx.alloc); - } else { - ctx.colors.write(ColorSpace::D65Gray, space, &mut ctx.alloc); - } - - // Add a second gray-scale image containing the alpha values if - // this image has an alpha channel. - if raster.dynamic().color().has_alpha() { - let (alpha_data, alpha_filter) = encode_alpha(raster); - let mask_ref = ctx.alloc.bump(); - image.s_mask(mask_ref); - image.finish(); - - let mut mask = ctx.pdf.image_xobject(mask_ref, &alpha_data); - mask.filter(alpha_filter); - mask.width(width as i32); - mask.height(height as i32); - mask.color_space().device_gray(); - mask.bits_per_component(8); - } else { - image.finish(); - } - - if let (Some(icc), Some(icc_ref)) = (raster.icc(), icc_ref) { - let compressed = deflate(icc); - let mut stream = ctx.pdf.icc_profile(icc_ref, &compressed); - stream.filter(Filter::FlateDecode); - if has_color { - stream.n(3); - stream.alternate().srgb(); - } else { - stream.n(1); - stream.alternate().d65_gray(); - } - } - } - - ImageKind::Svg(svg) => { - let chunk = encode_svg(svg); - let mut map = HashMap::new(); - chunk.renumber_into(&mut ctx.pdf, |old| { - *map.entry(old).or_insert_with(|| ctx.alloc.bump()) - }); - ctx.image_refs.push(map[&Ref::new(1)]); - } - } - } -} - -/// Encode an image with a suitable filter and return the data, filter and -/// whether the image has color. -/// -/// Skips the alpha channel as that's encoded separately. -#[comemo::memoize] -#[tracing::instrument(skip_all)] -fn encode_raster_image(image: &RasterImage) -> (Arc>, Filter, bool) { - let dynamic = image.dynamic(); - match (image.format(), dynamic) { - // 8-bit gray JPEG. - (RasterFormat::Jpg, DynamicImage::ImageLuma8(_)) => { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (data.into_inner().into(), Filter::DctDecode, false) - } - - // 8-bit RGB JPEG (CMYK JPEGs get converted to RGB earlier). - (RasterFormat::Jpg, DynamicImage::ImageRgb8(_)) => { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (data.into_inner().into(), Filter::DctDecode, true) - } - - // TODO: Encode flate streams with PNG-predictor? - - // 8-bit gray PNG. - (RasterFormat::Png, DynamicImage::ImageLuma8(luma)) => { - let data = deflate(luma.as_raw()); - (data.into(), Filter::FlateDecode, false) - } - - // Anything else (including Rgb(a) PNGs). - (_, buf) => { - let (width, height) = buf.dimensions(); - let mut pixels = Vec::with_capacity(3 * width as usize * height as usize); - for (_, _, Rgba([r, g, b, _])) in buf.pixels() { - pixels.push(r); - pixels.push(g); - pixels.push(b); - } - - let data = deflate(&pixels); - (data.into(), Filter::FlateDecode, true) - } - } -} - -/// Encode an image's alpha channel if present. -#[comemo::memoize] -#[tracing::instrument(skip_all)] -fn encode_alpha(raster: &RasterImage) -> (Arc>, Filter) { - let pixels: Vec<_> = raster - .dynamic() - .pixels() - .map(|(_, _, Rgba([_, _, _, a]))| a) - .collect(); - (Arc::new(deflate(&pixels)), Filter::FlateDecode) -} - -/// Encode an SVG into a chunk of PDF objects. -/// -/// The main XObject will have ID 1. -#[comemo::memoize] -#[tracing::instrument(skip_all)] -fn encode_svg(svg: &SvgImage) -> Arc { - let mut chunk = Chunk::new(); - - // Safety: We do not keep any references to tree nodes beyond the - // scope of `with`. - unsafe { - svg.with(|tree| { - svg2pdf::convert_tree_into( - tree, - svg2pdf::Options::default(), - &mut chunk, - Ref::new(1), - ); - }); - } - - Arc::new(chunk) -} diff --git a/crates/typst/src/export/pdf/mod.rs b/crates/typst/src/export/pdf/mod.rs deleted file mode 100644 index 51ed6d92..00000000 --- a/crates/typst/src/export/pdf/mod.rs +++ /dev/null @@ -1,431 +0,0 @@ -//! Exporting into PDF documents. - -mod color; -mod extg; -mod font; -mod gradient; -mod image; -mod outline; -mod page; - -pub use self::color::{ColorEncode, ColorSpaces}; -pub use self::page::{PdfPageLabel, PdfPageLabelStyle}; - -use std::cmp::Eq; -use std::collections::{BTreeMap, HashMap}; -use std::hash::Hash; -use std::num::NonZeroUsize; - -use base64::Engine; -use ecow::{eco_format, EcoString}; -use pdf_writer::types::Direction; -use pdf_writer::writers::PageLabel; -use pdf_writer::{Finish, Name, Pdf, Ref, TextStr}; -use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter}; - -use self::gradient::PdfGradient; -use self::page::Page; -use crate::doc::{Document, Lang}; -use crate::eval::Datetime; -use crate::font::Font; -use crate::geom::{Abs, Dir, Em}; -use crate::image::Image; -use crate::model::Introspector; - -use extg::ExtGState; - -/// Export a document into a PDF file. -/// -/// Returns the raw bytes making up the PDF file. -/// -/// The `ident` parameter shall be a string that uniquely and stably identifies -/// the document. It should not change between compilations of the same -/// document. Its hash will be used to create a PDF document identifier (the -/// identifier itself is not leaked). If `ident` is `None`, a hash of the -/// document is used instead (which means that it _will_ change across -/// compilations). -/// -/// The `timestamp`, if given, is expected to be the creation date of the -/// document as a UTC datetime. It will only be used if `set document(date: ..)` -/// is `auto`. -#[tracing::instrument(skip_all)] -pub fn pdf( - document: &Document, - ident: Option<&str>, - timestamp: Option, -) -> Vec { - let mut ctx = PdfContext::new(document); - page::construct_pages(&mut ctx, &document.pages); - font::write_fonts(&mut ctx); - image::write_images(&mut ctx); - gradient::write_gradients(&mut ctx); - extg::write_external_graphics_states(&mut ctx); - page::write_page_tree(&mut ctx); - write_catalog(&mut ctx, ident, timestamp); - ctx.pdf.finish() -} - -/// Context for exporting a whole PDF document. -pub struct PdfContext<'a> { - /// The document that we're currently exporting. - document: &'a Document, - /// An introspector for the document, used to resolve locations links and - /// the document outline. - introspector: Introspector, - - /// The writer we are writing the PDF into. - pdf: Pdf, - /// Content of exported pages. - pages: Vec, - /// For each font a mapping from used glyphs to their text representation. - /// May contain multiple chars in case of ligatures or similar things. The - /// same glyph can have a different text representation within one document, - /// then we just save the first one. The resulting strings are used for the - /// PDF's /ToUnicode map for glyphs that don't have an entry in the font's - /// cmap. This is important for copy-paste and searching. - glyph_sets: HashMap>, - /// The number of glyphs for all referenced languages in the document. - /// We keep track of this to determine the main document language. - languages: HashMap, - - /// Allocator for indirect reference IDs. - alloc: Ref, - /// The ID of the page tree. - page_tree_ref: Ref, - /// The IDs of written pages. - page_refs: Vec, - /// The IDs of written fonts. - font_refs: Vec, - /// The IDs of written images. - image_refs: Vec, - /// The IDs of written gradients. - gradient_refs: Vec, - /// The IDs of written external graphics states. - ext_gs_refs: Vec, - /// Handles color space writing. - colors: ColorSpaces, - - /// Deduplicates fonts used across the document. - font_map: Remapper, - /// Deduplicates images used across the document. - image_map: Remapper, - /// Deduplicates gradients used across the document. - gradient_map: Remapper, - /// Deduplicates external graphics states used across the document. - extg_map: Remapper, -} - -impl<'a> PdfContext<'a> { - fn new(document: &'a Document) -> Self { - let mut alloc = Ref::new(1); - let page_tree_ref = alloc.bump(); - Self { - document, - introspector: Introspector::new(&document.pages), - pdf: Pdf::new(), - pages: vec![], - glyph_sets: HashMap::new(), - languages: HashMap::new(), - alloc, - page_tree_ref, - page_refs: vec![], - font_refs: vec![], - image_refs: vec![], - gradient_refs: vec![], - ext_gs_refs: vec![], - colors: ColorSpaces::default(), - font_map: Remapper::new(), - image_map: Remapper::new(), - gradient_map: Remapper::new(), - extg_map: Remapper::new(), - } - } -} - -/// Write the document catalog. -#[tracing::instrument(skip_all)] -fn write_catalog(ctx: &mut PdfContext, ident: Option<&str>, timestamp: Option) { - let lang = ctx - .languages - .iter() - .max_by_key(|(&lang, &count)| (count, lang)) - .map(|(&k, _)| k); - - let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { - Direction::R2L - } else { - Direction::L2R - }; - - // Write the outline tree. - let outline_root_id = outline::write_outline(ctx); - - // Write the page labels. - let page_labels = write_page_labels(ctx); - - // Write the document information. - let mut info = ctx.pdf.document_info(ctx.alloc.bump()); - let mut xmp = XmpWriter::new(); - if let Some(title) = &ctx.document.title { - info.title(TextStr(title)); - xmp.title([(None, title.as_str())]); - } - - let authors = &ctx.document.author; - if !authors.is_empty() { - info.author(TextStr(&authors.join(", "))); - xmp.creator(authors.iter().map(|s| s.as_str())); - } - - let creator = eco_format!("Typst {}", env!("CARGO_PKG_VERSION")); - info.creator(TextStr(&creator)); - xmp.creator_tool(&creator); - - let keywords = &ctx.document.keywords; - if !keywords.is_empty() { - let joined = keywords.join(", "); - info.keywords(TextStr(&joined)); - xmp.pdf_keywords(&joined); - } - - if let Some(date) = ctx.document.date.unwrap_or(timestamp) { - let tz = ctx.document.date.is_auto(); - if let Some(pdf_date) = pdf_date(date, tz) { - info.creation_date(pdf_date); - info.modified_date(pdf_date); - } - if let Some(xmp_date) = xmp_date(date, tz) { - xmp.create_date(xmp_date); - xmp.modify_date(xmp_date); - } - } - - info.finish(); - xmp.num_pages(ctx.document.pages.len() as u32); - xmp.format("application/pdf"); - xmp.language(ctx.languages.keys().map(|lang| LangId(lang.as_str()))); - - // A unique ID for this instance of the document. Changes if anything - // changes in the frames. - let instance_id = hash_base64(&ctx.pdf.as_bytes()); - - if let Some(ident) = ident { - // A unique ID for the document that stays stable across compilations. - let doc_id = hash_base64(&("PDF-1.7", ident)); - xmp.document_id(&doc_id); - xmp.instance_id(&instance_id); - ctx.pdf - .set_file_id((doc_id.clone().into_bytes(), instance_id.into_bytes())); - } else { - // This is not spec-compliant, but some PDF readers really want an ID. - let bytes = instance_id.into_bytes(); - ctx.pdf.set_file_id((bytes.clone(), bytes)); - } - - xmp.rendition_class(RenditionClass::Proof); - xmp.pdf_version("1.7"); - - let xmp_buf = xmp.finish(None); - let meta_ref = ctx.alloc.bump(); - ctx.pdf - .stream(meta_ref, xmp_buf.as_bytes()) - .pair(Name(b"Type"), Name(b"Metadata")) - .pair(Name(b"Subtype"), Name(b"XML")); - - // Write the document catalog. - let mut catalog = ctx.pdf.catalog(ctx.alloc.bump()); - catalog.pages(ctx.page_tree_ref); - catalog.viewer_preferences().direction(dir); - catalog.metadata(meta_ref); - - // Insert the page labels. - if !page_labels.is_empty() { - let mut num_tree = catalog.page_labels(); - let mut entries = num_tree.nums(); - for (n, r) in &page_labels { - entries.insert(n.get() as i32 - 1, *r); - } - } - - if let Some(outline_root_id) = outline_root_id { - catalog.outlines(outline_root_id); - } - - if let Some(lang) = lang { - catalog.lang(TextStr(lang.as_str())); - } -} - -/// Write the page labels. -#[tracing::instrument(skip_all)] -fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)> { - let mut result = vec![]; - let mut prev: Option<&PdfPageLabel> = None; - - for (i, page) in ctx.pages.iter().enumerate() { - let nr = NonZeroUsize::new(1 + i).unwrap(); - let Some(label) = &page.label else { continue }; - - // Don't create a label if neither style nor prefix are specified. - if label.prefix.is_none() && label.style.is_none() { - continue; - } - - if let Some(pre) = prev { - if label.prefix == pre.prefix - && label.style == pre.style - && label.offset == pre.offset.map(|n| n.saturating_add(1)) - { - prev = Some(label); - continue; - } - } - - let id = ctx.alloc.bump(); - let mut entry = ctx.pdf.indirect(id).start::(); - - // Only add what is actually provided. Don't add empty prefix string if - // it wasn't given for example. - if let Some(prefix) = &label.prefix { - entry.prefix(TextStr(prefix)); - } - - if let Some(style) = label.style { - entry.style(style.into()); - } - - if let Some(offset) = label.offset { - entry.offset(offset.get() as i32); - } - - result.push((nr, id)); - prev = Some(label); - } - - result -} - -/// Compress data with the DEFLATE algorithm. -#[tracing::instrument(skip_all)] -fn deflate(data: &[u8]) -> Vec { - const COMPRESSION_LEVEL: u8 = 6; - miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) -} - -/// Create a base64-encoded hash of the value. -fn hash_base64(value: &T) -> String { - base64::engine::general_purpose::STANDARD - .encode(crate::util::hash128(value).to_be_bytes()) -} - -/// Converts a datetime to a pdf-writer date. -fn pdf_date(datetime: Datetime, tz: bool) -> Option { - let year = datetime.year().filter(|&y| y >= 0)? as u16; - - let mut pdf_date = pdf_writer::Date::new(year); - - if let Some(month) = datetime.month() { - pdf_date = pdf_date.month(month); - } - - if let Some(day) = datetime.day() { - pdf_date = pdf_date.day(day); - } - - if let Some(h) = datetime.hour() { - pdf_date = pdf_date.hour(h); - } - - if let Some(m) = datetime.minute() { - pdf_date = pdf_date.minute(m); - } - - if let Some(s) = datetime.second() { - pdf_date = pdf_date.second(s); - } - - if tz { - pdf_date = pdf_date.utc_offset_hour(0).utc_offset_minute(0); - } - - Some(pdf_date) -} - -/// Converts a datetime to an xmp-writer datetime. -fn xmp_date(datetime: Datetime, tz: bool) -> Option { - let year = datetime.year().filter(|&y| y >= 0)? as u16; - Some(DateTime { - year, - month: datetime.month(), - day: datetime.day(), - hour: datetime.hour(), - minute: datetime.minute(), - second: datetime.second(), - timezone: if tz { Some(Timezone::Utc) } else { None }, - }) -} - -/// Assigns new, consecutive PDF-internal indices to items. -struct Remapper { - /// Forwards from the items to the pdf indices. - to_pdf: HashMap, - /// Backwards from the pdf indices to the items. - to_items: Vec, -} - -impl Remapper -where - T: Eq + Hash + Clone, -{ - fn new() -> Self { - Self { to_pdf: HashMap::new(), to_items: vec![] } - } - - fn insert(&mut self, item: T) -> usize { - let to_layout = &mut self.to_items; - *self.to_pdf.entry(item.clone()).or_insert_with(|| { - let pdf_index = to_layout.len(); - to_layout.push(item); - pdf_index - }) - } - - fn map(&self, item: &T) -> usize { - self.to_pdf[item] - } - - fn pdf_indices<'a>( - &'a self, - refs: &'a [Ref], - ) -> impl Iterator + 'a { - refs.iter().copied().zip(0..self.to_pdf.len()) - } - - fn items(&self) -> impl Iterator + '_ { - self.to_items.iter() - } -} - -/// Additional methods for [`Abs`]. -trait AbsExt { - /// Convert an to a number of points. - fn to_f32(self) -> f32; -} - -impl AbsExt for Abs { - fn to_f32(self) -> f32 { - self.to_pt() as f32 - } -} - -/// Additional methods for [`Em`]. -trait EmExt { - /// Convert an em length to a number of PDF font units. - fn to_font_units(self) -> f32; -} - -impl EmExt for Em { - fn to_font_units(self) -> f32 { - 1000.0 * self.get() as f32 - } -} diff --git a/crates/typst/src/export/pdf/outline.rs b/crates/typst/src/export/pdf/outline.rs deleted file mode 100644 index f6f9f09d..00000000 --- a/crates/typst/src/export/pdf/outline.rs +++ /dev/null @@ -1,190 +0,0 @@ -use std::num::NonZeroUsize; - -use pdf_writer::{Finish, Ref, TextStr}; - -use super::{AbsExt, PdfContext}; -use crate::geom::{Abs, Smart}; -use crate::model::Content; - -/// Construct the outline for the document. -#[tracing::instrument(skip_all)] -pub fn write_outline(ctx: &mut PdfContext) -> Option { - let mut tree: Vec = vec![]; - - // Stores the level of the topmost skipped ancestor of the next bookmarked - // heading. A skipped heading is a heading with 'bookmarked: false', that - // is, it is not added to the PDF outline, and so is not in the tree. - // Therefore, its next descendant must be added at its level, which is - // enforced in the manner shown below. - let mut last_skipped_level = None; - for heading in ctx.introspector.query(&item!(heading_elem).select()).iter() { - let leaf = HeadingNode::leaf((**heading).clone()); - - if leaf.bookmarked { - let mut children = &mut tree; - - // Descend the tree through the latest bookmarked heading of each - // level until either: - // - you reach a node whose children would be brothers of this - // heading (=> add the current heading as a child of this node); - // - you reach a node with no children (=> this heading probably - // skipped a few nesting levels in Typst, or one or more ancestors - // of this heading weren't bookmarked, so add it as a child of this - // node, which is its deepest bookmarked ancestor); - // - or, if the latest heading(s) was(/were) skipped - // ('bookmarked: false'), then stop if you reach a node whose - // children would be brothers of the latest skipped heading - // of lowest level (=> those skipped headings would be ancestors - // of the current heading, so add it as a 'brother' of the least - // deep skipped ancestor among them, as those ancestors weren't - // added to the bookmark tree, and the current heading should not - // be mistakenly added as a descendant of a brother of that - // ancestor.) - // - // That is, if you had a bookmarked heading of level N, a skipped - // heading of level N, a skipped heading of level N + 1, and then - // a bookmarked heading of level N + 2, that last one is bookmarked - // as a level N heading (taking the place of its topmost skipped - // ancestor), so that it is not mistakenly added as a descendant of - // the previous level N heading. - // - // In other words, a heading can be added to the bookmark tree - // at most as deep as its topmost skipped direct ancestor (if it - // exists), or at most as deep as its actual nesting level in Typst - // (not exceeding whichever is the most restrictive depth limit - // of those two). - while children.last().map_or(false, |last| { - last_skipped_level.map_or(true, |l| last.level < l) - && last.level < leaf.level - }) { - children = &mut children.last_mut().unwrap().children; - } - - // Since this heading was bookmarked, the next heading, if it is a - // child of this one, won't have a skipped direct ancestor (indeed, - // this heading would be its most direct ancestor, and wasn't - // skipped). Therefore, it can be added as a child of this one, if - // needed, following the usual rules listed above. - last_skipped_level = None; - children.push(leaf); - } else if last_skipped_level.map_or(true, |l| leaf.level < l) { - // Only the topmost / lowest-level skipped heading matters when you - // have consecutive skipped headings (since none of them are being - // added to the bookmark tree), hence the condition above. - // This ensures the next bookmarked heading will be placed - // at most as deep as its topmost skipped ancestors. Deeper - // ancestors do not matter as the nesting structure they create - // won't be visible in the PDF outline. - last_skipped_level = Some(leaf.level); - } - } - - if tree.is_empty() { - return None; - } - - let root_id = ctx.alloc.bump(); - let start_ref = ctx.alloc; - let len = tree.len(); - - let mut prev_ref = None; - for (i, node) in tree.iter().enumerate() { - prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len)); - } - - ctx.pdf - .outline(root_id) - .first(start_ref) - .last(Ref::new(ctx.alloc.get() - 1)) - .count(tree.len() as i32); - - Some(root_id) -} - -/// A heading in the outline panel. -#[derive(Debug, Clone)] -struct HeadingNode { - element: Content, - level: NonZeroUsize, - bookmarked: bool, - children: Vec, -} - -impl HeadingNode { - fn leaf(element: Content) -> Self { - HeadingNode { - level: element.expect_field_by_name::("level"), - // 'bookmarked' set to 'auto' falls back to the value of 'outlined'. - bookmarked: element - .expect_field_by_name::>("bookmarked") - .unwrap_or_else(|| element.expect_field_by_name::("outlined")), - element, - children: Vec::new(), - } - } - - fn len(&self) -> usize { - 1 + self.children.iter().map(Self::len).sum::() - } -} - -/// Write an outline item and all its children. -#[tracing::instrument(skip_all)] -fn write_outline_item( - ctx: &mut PdfContext, - node: &HeadingNode, - parent_ref: Ref, - prev_ref: Option, - is_last: bool, -) -> Ref { - let id = ctx.alloc.bump(); - let next_ref = Ref::new(id.get() + node.len() as i32); - - let mut outline = ctx.pdf.outline_item(id); - outline.parent(parent_ref); - - if !is_last { - outline.next(next_ref); - } - - if let Some(prev_rev) = prev_ref { - outline.prev(prev_rev); - } - - if !node.children.is_empty() { - let current_child = Ref::new(id.get() + 1); - outline.first(current_child); - outline.last(Ref::new(next_ref.get() - 1)); - outline.count(-(node.children.len() as i32)); - } - - let body = node.element.expect_field_by_name::("body"); - outline.title(TextStr(body.plain_text().trim())); - - let loc = node.element.location().unwrap(); - let pos = ctx.introspector.position(loc); - let index = pos.page.get() - 1; - if let Some(page) = ctx.pages.get(index) { - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - outline.dest().page(ctx.page_refs[index]).xyz( - pos.point.x.to_f32(), - (page.size.y - y).to_f32(), - None, - ); - } - - outline.finish(); - - let mut prev_ref = None; - for (i, child) in node.children.iter().enumerate() { - prev_ref = Some(write_outline_item( - ctx, - child, - id, - prev_ref, - i + 1 == node.children.len(), - )); - } - - id -} diff --git a/crates/typst/src/export/pdf/page.rs b/crates/typst/src/export/pdf/page.rs deleted file mode 100644 index 5e6cd2a2..00000000 --- a/crates/typst/src/export/pdf/page.rs +++ /dev/null @@ -1,752 +0,0 @@ -use std::num::NonZeroUsize; -use std::sync::Arc; - -use ecow::{eco_format, EcoString}; -use pdf_writer::types::{ - ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle, - NumberingStyle, -}; -use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; - -use super::color::PaintEncode; -use super::extg::ExtGState; -use super::{deflate, AbsExt, EmExt, PdfContext}; -use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem}; -use crate::eval::Repr; -use crate::font::Font; -use crate::geom::{ - self, Abs, Em, FixedStroke, Geometry, LineCap, LineJoin, Numeric, Paint, Point, - Ratio, Shape, Size, Transform, -}; -use crate::image::Image; - -/// Construct page objects. -#[tracing::instrument(skip_all)] -pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) { - for frame in frames { - construct_page(ctx, frame); - } -} - -/// Construct a page object. -#[tracing::instrument(skip_all)] -pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) { - let page_ref = ctx.alloc.bump(); - ctx.page_refs.push(page_ref); - - let mut ctx = PageContext { - parent: ctx, - page_ref, - label: None, - uses_opacities: false, - content: Content::new(), - state: State::new(frame.size()), - saves: vec![], - bottom: 0.0, - links: vec![], - }; - - let size = frame.size(); - - // Make the coordinate system start at the top-left. - ctx.bottom = size.y.to_f32(); - ctx.transform(Transform { - sx: Ratio::one(), - ky: Ratio::zero(), - kx: Ratio::zero(), - sy: Ratio::new(-1.0), - tx: Abs::zero(), - ty: size.y, - }); - - // Encode the page into the content stream. - write_frame(&mut ctx, frame); - - let page = Page { - size, - content: ctx.content.finish(), - id: ctx.page_ref, - uses_opacities: ctx.uses_opacities, - links: ctx.links, - label: ctx.label, - }; - - ctx.parent.pages.push(page); -} - -/// Write the page tree. -#[tracing::instrument(skip_all)] -pub fn write_page_tree(ctx: &mut PdfContext) { - for i in 0..ctx.pages.len() { - write_page(ctx, i); - } - - let mut pages = ctx.pdf.pages(ctx.page_tree_ref); - pages - .count(ctx.page_refs.len() as i32) - .kids(ctx.page_refs.iter().copied()); - - let mut resources = pages.resources(); - ctx.colors - .write_color_spaces(resources.color_spaces(), &mut ctx.alloc); - - let mut fonts = resources.fonts(); - for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) { - let name = eco_format!("F{}", f); - fonts.pair(Name(name.as_bytes()), font_ref); - } - - fonts.finish(); - - let mut images = resources.x_objects(); - for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) { - let name = eco_format!("Im{}", im); - images.pair(Name(name.as_bytes()), image_ref); - } - - images.finish(); - - let mut patterns = resources.patterns(); - for (gradient_ref, gr) in ctx.gradient_map.pdf_indices(&ctx.gradient_refs) { - let name = eco_format!("Gr{}", gr); - patterns.pair(Name(name.as_bytes()), gradient_ref); - } - - patterns.finish(); - - let mut ext_gs_states = resources.ext_g_states(); - for (gs_ref, gs) in ctx.extg_map.pdf_indices(&ctx.ext_gs_refs) { - let name = eco_format!("Gs{}", gs); - ext_gs_states.pair(Name(name.as_bytes()), gs_ref); - } - ext_gs_states.finish(); - - resources.finish(); - pages.finish(); - - // Write all of the functions used by the document. - ctx.colors.write_functions(&mut ctx.pdf); -} - -/// Write a page tree node. -#[tracing::instrument(skip_all)] -fn write_page(ctx: &mut PdfContext, i: usize) { - let page = &ctx.pages[i]; - let content_id = ctx.alloc.bump(); - - let mut page_writer = ctx.pdf.page(page.id); - page_writer.parent(ctx.page_tree_ref); - - let w = page.size.x.to_f32(); - let h = page.size.y.to_f32(); - page_writer.media_box(Rect::new(0.0, 0.0, w, h)); - page_writer.contents(content_id); - - if page.uses_opacities { - page_writer - .group() - .transparency() - .isolated(false) - .knockout(false) - .color_space() - .srgb(); - } - - let mut annotations = page_writer.annotations(); - for (dest, rect) in &page.links { - let mut annotation = annotations.push(); - annotation.subtype(AnnotationType::Link).rect(*rect); - annotation.border(0.0, 0.0, 0.0, None); - - let pos = match dest { - Destination::Url(uri) => { - annotation - .action() - .action_type(ActionType::Uri) - .uri(Str(uri.as_bytes())); - continue; - } - Destination::Position(pos) => *pos, - Destination::Location(loc) => ctx.introspector.position(*loc), - }; - - let index = pos.page.get() - 1; - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - if let Some(page) = ctx.pages.get(index) { - annotation - .action() - .action_type(ActionType::GoTo) - .destination() - .page(ctx.page_refs[index]) - .xyz(pos.point.x.to_f32(), (page.size.y - y).to_f32(), None); - } - } - - annotations.finish(); - page_writer.finish(); - - let data = deflate_content(&page.content); - ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode); -} - -/// Memoized version of [`deflate`] specialized for a page's content stream. -#[comemo::memoize] -fn deflate_content(content: &[u8]) -> Arc> { - Arc::new(deflate(content)) -} - -/// Data for an exported page. -pub struct Page { - /// The indirect object id of the page. - pub id: Ref, - /// The page's dimensions. - pub size: Size, - /// The page's content stream. - pub content: Vec, - /// Whether the page uses opacities. - pub uses_opacities: bool, - /// Links in the PDF coordinate system. - pub links: Vec<(Destination, Rect)>, - /// The page's PDF label. - pub label: Option, -} - -/// An exporter for the contents of a single PDF page. -pub struct PageContext<'a, 'b> { - pub parent: &'a mut PdfContext<'b>, - page_ref: Ref, - label: Option, - pub content: Content, - state: State, - saves: Vec, - bottom: f32, - uses_opacities: bool, - links: Vec<(Destination, Rect)>, -} - -/// A simulated graphics state used to deduplicate graphics state changes and -/// keep track of the current transformation matrix for link annotations. -#[derive(Debug, Clone)] -struct State { - /// The transform of the current item. - transform: Transform, - /// The transform of first hard frame in the hierarchy. - container_transform: Transform, - /// The size of the first hard frame in the hierarchy. - size: Size, - font: Option<(Font, Abs)>, - fill: Option, - fill_space: Option>, - external_graphics_state: Option, - stroke: Option, - stroke_space: Option>, -} - -impl State { - /// Creates a new, clean state for a given page `size`. - pub fn new(size: Size) -> Self { - Self { - transform: Transform::identity(), - container_transform: Transform::identity(), - size, - font: None, - fill: None, - fill_space: None, - external_graphics_state: None, - stroke: None, - stroke_space: None, - } - } - - /// Creates the [`Transforms`] structure for the current item. - pub fn transforms(&self, size: Size, pos: Point) -> Transforms { - Transforms { - transform: self.transform.pre_concat(Transform::translate(pos.x, pos.y)), - container_transform: self.container_transform, - container_size: self.size, - size, - } - } -} - -/// Subset of the state used to calculate the transform of gradients and patterns. -#[derive(Clone, Copy)] -pub(super) struct Transforms { - /// The transform of the current item. - pub transform: Transform, - /// The transform of first hard frame in the hierarchy. - pub container_transform: Transform, - /// The size of the first hard frame in the hierarchy. - pub container_size: Size, - /// The size of the item. - pub size: Size, -} - -impl PageContext<'_, '_> { - fn save_state(&mut self) { - self.saves.push(self.state.clone()); - self.content.save_state(); - } - - fn restore_state(&mut self) { - self.content.restore_state(); - self.state = self.saves.pop().expect("missing state save"); - } - - fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) { - let current_state = self.state.external_graphics_state.as_ref(); - if current_state != Some(graphics_state) { - self.parent.extg_map.insert(*graphics_state); - let name = eco_format!("Gs{}", self.parent.extg_map.map(graphics_state)); - self.content.set_parameters(Name(name.as_bytes())); - - if graphics_state.uses_opacities() { - self.uses_opacities = true; - } - } - } - - fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) { - let stroke_opacity = stroke - .map(|stroke| { - let color = match &stroke.paint { - Paint::Solid(color) => *color, - Paint::Gradient(_) => return 255, - }; - - color.alpha().map_or(255, |v| (v * 255.0).round() as u8) - }) - .unwrap_or(255); - let fill_opacity = fill - .map(|paint| { - let color = match paint { - Paint::Solid(color) => *color, - Paint::Gradient(_) => return 255, - }; - - color.alpha().map_or(255, |v| (v * 255.0).round() as u8) - }) - .unwrap_or(255); - self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); - } - - fn transform(&mut self, transform: Transform) { - let Transform { sx, ky, kx, sy, tx, ty } = transform; - self.state.transform = self.state.transform.pre_concat(transform); - self.content.transform([ - sx.get() as _, - ky.get() as _, - kx.get() as _, - sy.get() as _, - tx.to_f32(), - ty.to_f32(), - ]); - } - - fn group_transform(&mut self, transform: Transform) { - self.state.container_transform = - self.state.container_transform.pre_concat(transform); - } - - fn set_font(&mut self, font: &Font, size: Abs) { - if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) { - self.parent.font_map.insert(font.clone()); - let name = eco_format!("F{}", self.parent.font_map.map(font)); - self.content.set_font(Name(name.as_bytes()), size.to_f32()); - self.state.font = Some((font.clone(), size)); - } - } - - fn size(&mut self, size: Size) { - self.state.size = size; - } - - fn set_fill(&mut self, fill: &Paint, on_text: bool, transforms: Transforms) { - if self.state.fill.as_ref() != Some(fill) - || matches!(self.state.fill, Some(Paint::Gradient(_))) - { - fill.set_as_fill(self, on_text, transforms); - self.state.fill = Some(fill.clone()); - } - } - - pub fn set_fill_color_space(&mut self, space: Name<'static>) { - if self.state.fill_space != Some(space) { - self.content.set_fill_color_space(ColorSpaceOperand::Named(space)); - self.state.fill_space = Some(space); - } - } - - pub fn reset_fill_color_space(&mut self) { - self.state.fill_space = None; - } - - fn set_stroke(&mut self, stroke: &FixedStroke, transforms: Transforms) { - if self.state.stroke.as_ref() != Some(stroke) - || matches!( - self.state.stroke.as_ref().map(|s| &s.paint), - Some(Paint::Gradient(_)) - ) - { - let FixedStroke { - paint, - thickness, - line_cap, - line_join, - dash_pattern, - miter_limit, - } = stroke; - - paint.set_as_stroke(self, false, transforms); - - self.content.set_line_width(thickness.to_f32()); - if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) { - self.content.set_line_cap(line_cap.into()); - } - if self.state.stroke.as_ref().map(|s| &s.line_join) != Some(line_join) { - self.content.set_line_join(line_join.into()); - } - if self.state.stroke.as_ref().map(|s| &s.dash_pattern) != Some(dash_pattern) { - if let Some(pattern) = dash_pattern { - self.content.set_dash_pattern( - pattern.array.iter().map(|l| l.to_f32()), - pattern.phase.to_f32(), - ); - } else { - self.content.set_dash_pattern([], 0.0); - } - } - if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) { - self.content.set_miter_limit(miter_limit.get() as f32); - } - self.state.stroke = Some(stroke.clone()); - } - } - - pub fn set_stroke_color_space(&mut self, space: Name<'static>) { - if self.state.stroke_space != Some(space) { - self.content.set_stroke_color_space(ColorSpaceOperand::Named(space)); - self.state.stroke_space = Some(space); - } - } - - pub fn reset_stroke_color_space(&mut self) { - self.state.stroke_space = None; - } -} - -/// Encode a frame into the content stream. -fn write_frame(ctx: &mut PageContext, frame: &Frame) { - for &(pos, ref item) in frame.items() { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - - match item { - FrameItem::Group(group) => write_group(ctx, pos, group), - FrameItem::Text(text) => write_text(ctx, pos, text), - FrameItem::Shape(shape, _) => write_shape(ctx, pos, shape), - FrameItem::Image(image, size, _) => write_image(ctx, x, y, image, *size), - FrameItem::Meta(meta, size) => match meta { - Meta::Link(dest) => write_link(ctx, pos, dest, *size), - Meta::Elem(_) => {} - Meta::Hide => {} - Meta::PageNumbering(_) => {} - Meta::PdfPageLabel(label) => ctx.label = Some(label.clone()), - }, - } - } -} - -/// Encode a group into the content stream. -fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) { - let translation = Transform::translate(pos.x, pos.y); - - ctx.save_state(); - - if group.frame.kind().is_hard() { - ctx.group_transform( - translation - .pre_concat( - ctx.state - .transform - .post_concat(ctx.state.container_transform.invert().unwrap()), - ) - .pre_concat(group.transform), - ); - ctx.size(group.frame.size()); - } - - ctx.transform(translation.pre_concat(group.transform)); - if let Some(clip_path) = &group.clip_path { - write_path(ctx, 0.0, 0.0, clip_path); - ctx.content.clip_nonzero(); - ctx.content.end_path(); - } - - write_frame(ctx, &group.frame); - ctx.restore_state(); -} - -/// Encode a text run into the content stream. -fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - - *ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len(); - - let glyph_set = ctx.parent.glyph_sets.entry(text.font.clone()).or_default(); - for g in &text.glyphs { - let segment = &text.text[g.range()]; - glyph_set.entry(g.id).or_insert_with(|| segment.into()); - } - - ctx.set_fill(&text.fill, true, ctx.state.transforms(Size::zero(), pos)); - ctx.set_font(&text.font, text.size); - ctx.set_opacities(None, Some(&text.fill)); - ctx.content.begin_text(); - - // Positiosn the text. - ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); - - let mut positioned = ctx.content.show_positioned(); - let mut items = positioned.items(); - let mut adjustment = Em::zero(); - let mut encoded = vec![]; - - // Write the glyphs with kerning adjustments. - for glyph in &text.glyphs { - adjustment += glyph.x_offset; - - if !adjustment.is_zero() { - if !encoded.is_empty() { - items.show(Str(&encoded)); - encoded.clear(); - } - - items.adjust(-adjustment.to_font_units()); - adjustment = Em::zero(); - } - - let cid = super::font::glyph_cid(&text.font, glyph.id); - encoded.push((cid >> 8) as u8); - encoded.push((cid & 0xff) as u8); - - if let Some(advance) = text.font.advance(glyph.id) { - adjustment += glyph.x_advance - advance; - } - - adjustment -= glyph.x_offset; - } - - if !encoded.is_empty() { - items.show(Str(&encoded)); - } - - items.finish(); - positioned.finish(); - ctx.content.end_text(); -} - -/// Encode a geometrical shape into the content stream. -fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - - let stroke = shape.stroke.as_ref().and_then(|stroke| { - if stroke.thickness.to_f32() > 0.0 { - Some(stroke) - } else { - None - } - }); - - if shape.fill.is_none() && stroke.is_none() { - return; - } - - if let Some(fill) = &shape.fill { - ctx.set_fill(fill, false, ctx.state.transforms(shape.geometry.bbox_size(), pos)); - } - - if let Some(stroke) = stroke { - ctx.set_stroke(stroke, ctx.state.transforms(shape.geometry.bbox_size(), pos)); - } - - ctx.set_opacities(stroke, shape.fill.as_ref()); - - match shape.geometry { - Geometry::Line(target) => { - let dx = target.x.to_f32(); - let dy = target.y.to_f32(); - ctx.content.move_to(x, y); - ctx.content.line_to(x + dx, y + dy); - } - Geometry::Rect(size) => { - let w = size.x.to_f32(); - let h = size.y.to_f32(); - if w > 0.0 && h > 0.0 { - ctx.content.rect(x, y, w, h); - } - } - Geometry::Path(ref path) => { - write_path(ctx, x, y, path); - } - } - - match (&shape.fill, stroke) { - (None, None) => unreachable!(), - (Some(_), None) => ctx.content.fill_nonzero(), - (None, Some(_)) => ctx.content.stroke(), - (Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(), - }; -} - -/// Encode a bezier path into the content stream. -fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) { - for elem in &path.0 { - match elem { - geom::PathItem::MoveTo(p) => { - ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()) - } - geom::PathItem::LineTo(p) => { - ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()) - } - geom::PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to( - x + p1.x.to_f32(), - y + p1.y.to_f32(), - x + p2.x.to_f32(), - y + p2.y.to_f32(), - x + p3.x.to_f32(), - y + p3.y.to_f32(), - ), - geom::PathItem::ClosePath => ctx.content.close_path(), - }; - } -} - -/// Encode a vector or raster image into the content stream. -fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) { - ctx.parent.image_map.insert(image.clone()); - let name = eco_format!("Im{}", ctx.parent.image_map.map(image)); - let w = size.x.to_f32(); - let h = size.y.to_f32(); - ctx.content.save_state(); - ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]); - - if let Some(alt) = image.alt() { - let mut image_span = - ctx.content.begin_marked_content_with_properties(Name(b"Span")); - let mut image_alt = image_span.properties(); - image_alt.pair(Name(b"Alt"), pdf_writer::Str(alt.as_bytes())); - image_alt.finish(); - image_span.finish(); - - ctx.content.x_object(Name(name.as_bytes())); - ctx.content.end_marked_content(); - } else { - ctx.content.x_object(Name(name.as_bytes())); - } - - ctx.content.restore_state(); -} - -/// Save a link for later writing in the annotations dictionary. -fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size) { - let mut min_x = Abs::inf(); - let mut min_y = Abs::inf(); - let mut max_x = -Abs::inf(); - let mut max_y = -Abs::inf(); - - // Compute the bounding box of the transformed link. - for point in [ - pos, - pos + Point::with_x(size.x), - pos + Point::with_y(size.y), - pos + size.to_point(), - ] { - let t = point.transform(ctx.state.transform); - min_x.set_min(t.x); - min_y.set_min(t.y); - max_x.set_max(t.x); - max_y.set_max(t.y); - } - - let x1 = min_x.to_f32(); - let x2 = max_x.to_f32(); - let y1 = max_y.to_f32(); - let y2 = min_y.to_f32(); - let rect = Rect::new(x1, y1, x2, y2); - - ctx.links.push((dest.clone(), rect)); -} - -impl From<&LineCap> for LineCapStyle { - fn from(line_cap: &LineCap) -> Self { - match line_cap { - LineCap::Butt => LineCapStyle::ButtCap, - LineCap::Round => LineCapStyle::RoundCap, - LineCap::Square => LineCapStyle::ProjectingSquareCap, - } - } -} - -impl From<&LineJoin> for LineJoinStyle { - fn from(line_join: &LineJoin) -> Self { - match line_join { - LineJoin::Miter => LineJoinStyle::MiterJoin, - LineJoin::Round => LineJoinStyle::RoundJoin, - LineJoin::Bevel => LineJoinStyle::BevelJoin, - } - } -} - -/// Specification for a PDF page label. -#[derive(Debug, Clone, PartialEq, Hash, Default)] -pub struct PdfPageLabel { - /// Can be any string or none. Will always be prepended to the numbering style. - pub prefix: Option, - /// Based on the numbering pattern. - /// - /// If `None` or numbering is a function, the field will be empty. - pub style: Option, - /// Offset for the page label start. - /// - /// Describes where to start counting from when setting a style. - /// (Has to be greater or equal than 1) - pub offset: Option, -} - -impl Repr for PdfPageLabel { - fn repr(&self) -> EcoString { - eco_format!("{self:?}") - } -} - -/// A PDF page label number style. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum PdfPageLabelStyle { - /// Decimal arabic numerals (1, 2, 3). - Arabic, - /// Lowercase roman numerals (i, ii, iii). - LowerRoman, - /// Uppercase roman numerals (I, II, III). - UpperRoman, - /// Lowercase letters (`a` to `z` for the first 26 pages, - /// `aa` to `zz` and so on for the next). - LowerAlpha, - /// Uppercase letters (`A` to `Z` for the first 26 pages, - /// `AA` to `ZZ` and so on for the next). - UpperAlpha, -} - -impl From for NumberingStyle { - fn from(value: PdfPageLabelStyle) -> Self { - match value { - PdfPageLabelStyle::Arabic => Self::Arabic, - PdfPageLabelStyle::LowerRoman => Self::LowerRoman, - PdfPageLabelStyle::UpperRoman => Self::UpperRoman, - PdfPageLabelStyle::LowerAlpha => Self::LowerAlpha, - PdfPageLabelStyle::UpperAlpha => Self::UpperAlpha, - } - } -} diff --git a/crates/typst/src/export/pdf/postscript/hsl.ps b/crates/typst/src/export/pdf/postscript/hsl.ps deleted file mode 100644 index 740bc3ed..00000000 --- a/crates/typst/src/export/pdf/postscript/hsl.ps +++ /dev/null @@ -1,63 +0,0 @@ - -{ - % Starting stack: H, S, L - % /!\ WARNING: The hue component **MUST** be encoded - % in the range [0, 1] before calling this function. - % This is because the function assumes that the - % hue component are divided by a factor of 360 - % in order to meet the range requirements of the - % PDF specification. - - % First we do H = (H * 360.0) % 360 - 3 2 roll 360 mul 3 1 roll - - % Compute C = (1 - |2 * L - 1|) * S - dup 1 exch 2 mul 1 sub abs sub 3 2 roll mul - - % P = (H / 60) % 2 - 3 2 roll dup 60 div 2 - 2 copy div cvi mul exch sub abs - - % X = C * (1 - |P - 1|) - 1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul - - % Compute m = L - C / 2 - exch dup 2 div 5 4 roll exch sub - - % Rotate so H is top - 4 3 roll exch 4 1 roll - - % Construct the RGB stack - dup 60 lt { - % We need to build: (C, X, 0) - pop 0 3 1 roll - } { - dup 120 lt { - % We need to build: (X, C, 0) - pop exch 0 3 1 roll - } { - dup 180 lt { - % We need to build: (0, C, X) - pop 0 - } { - dup 240 lt { - % We need to build: (0, X, C) - pop exch 0 - } { - 300 lt { - % We need to build: (X, 0, C) - 0 3 2 roll - } { - % We need to build: (C, 0, X) - 0 exch - } ifelse - } ifelse - } ifelse - } ifelse - } ifelse - - 4 3 roll - - % Add m to each component - dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch -} \ No newline at end of file diff --git a/crates/typst/src/export/pdf/postscript/hsv.ps b/crates/typst/src/export/pdf/postscript/hsv.ps deleted file mode 100644 index b29adf11..00000000 --- a/crates/typst/src/export/pdf/postscript/hsv.ps +++ /dev/null @@ -1,62 +0,0 @@ -{ - % Starting stack: H, S, V - % /!\ WARNING: The hue component **MUST** be encoded - % in the range [0, 1] before calling this function. - % This is because the function assumes that the - % hue component are divided by a factor of 360 - % in order to meet the range requirements of the - % PDF specification. - - % First we do H = (H * 360.0) % 360 - 3 2 roll 360 mul 3 1 roll - - % Compute C = V * S - dup 3 1 roll mul - - % P = (H / 60) % 2 - 3 2 roll dup 60 div 2 - 2 copy div cvi mul exch sub abs - - % X = C * (1 - |P - 1|) - 1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul - - % Compute m = V - C - exch dup 5 4 roll exch sub - - % Rotate so H is top - 4 3 roll exch 4 1 roll - - % Construct the RGB stack - dup 60 lt { - % We need to build: (C, X, 0) - pop 0 3 1 roll - } { - dup 120 lt { - % We need to build: (X, C, 0) - pop exch 0 3 1 roll - } { - dup 180 lt { - % We need to build: (0, C, X) - pop 0 - } { - dup 240 lt { - % We need to build: (0, X, C) - pop exch 0 - } { - 300 lt { - % We need to build: (X, 0, C) - 0 3 2 roll - } { - % We need to build: (C, 0, X) - 0 exch - } ifelse - } ifelse - } ifelse - } ifelse - } ifelse - - 4 3 roll - - % Add m to each component - dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch -} \ No newline at end of file diff --git a/crates/typst/src/export/pdf/postscript/oklab.ps b/crates/typst/src/export/pdf/postscript/oklab.ps deleted file mode 100644 index 4d6e9ad5..00000000 --- a/crates/typst/src/export/pdf/postscript/oklab.ps +++ /dev/null @@ -1,78 +0,0 @@ -{ - % Starting stack: L, A, B - % /!\ WARNING: The A and B components **MUST** be encoded - % in the range [0, 1] before calling this function. - % This is because the function assumes that the - % A and B components are offset by a factor of 0.4 - % in order to meet the range requirements of the - % PDF specification. - - exch 0.4 sub - exch 0.4 sub - - % Load L a and b into the stack - 2 index - 2 index - 2 index - - % Compute f1 = ((0.3963377774 * a) + (0.2158037573 * b) + L)^3 - 0.2158037573 mul exch - 0.3963377774 mul add add - dup dup mul mul - - % Load L, a, and b into the stack - 3 index - 3 index - 3 index - - % Compute f2 = ((-0.1055613458 * a) + (-0.0638541728 * b) + L)^3 - -0.0638541728 mul exch - -0.1055613458 mul add add - dup dup mul mul - - % Load L, a, and b into the stack - 4 index - 4 index - 4 index - - % Compute f3 = ((-0.0894841775 * a) + (-1.2914855480 * b) + L)^3 - -1.2914855480 mul exch - -0.0894841775 mul add add - dup dup mul mul - - % Discard L, a, and b by rolling the stack and popping - 6 3 roll pop pop pop - - % Load f1, f2, and f3 into the stack - 2 index - 2 index - 2 index - - % Compute R = f1 * 4.0767416621 + f2 * -3.3077115913 + f3 * 0.2309699292 - 0.2309699292 mul exch - -3.3077115913 mul add exch - 4.0767416621 mul add - - % Load f1, f2, and f3 into the stack - 3 index - 3 index - 3 index - - % Compute G = f1 * -1.2684380046 + f2 * 2.6097574011 + f3 * -0.3413193965 - -0.3413193965 mul exch - 2.6097574011 mul add exch - -1.2684380046 mul add - - % Load f1, f2, and f3 into the stack - 4 index - 4 index - 4 index - - % Compute B = f1 * -0.0041960863 + f2 * -0.7034186147 + f3 * 1.7076147010 - 1.7076147010 mul exch - -0.7034186147 mul add exch - -0.0041960863 mul add - - % Discard f1, f2, and f3 by rolling the stack and popping - 6 3 roll pop pop pop -} \ No newline at end of file diff --git a/crates/typst/src/geom/align.rs b/crates/typst/src/geom/align.rs index 59e608b1..a1c0735a 100644 --- a/crates/typst/src/geom/align.rs +++ b/crates/typst/src/geom/align.rs @@ -1,4 +1,5 @@ use super::*; +use crate::eval::item; /// Where to [align]($align) something along an axis. /// diff --git a/crates/typst/src/geom/em.rs b/crates/typst/src/geom/em.rs index 9cb0c55a..e6bbee71 100644 --- a/crates/typst/src/geom/em.rs +++ b/crates/typst/src/geom/em.rs @@ -1,4 +1,5 @@ use super::*; +use crate::eval::item; /// A length that is relative to the font size. /// diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 7303eca8..1e55385c 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -43,7 +43,6 @@ pub mod util; pub mod eval; pub mod diag; pub mod doc; -pub mod export; pub mod font; pub mod geom; pub mod image; diff --git a/crates/typst/src/model/realize.rs b/crates/typst/src/model/realize.rs index 7e926b0f..02f19e91 100644 --- a/crates/typst/src/model/realize.rs +++ b/crates/typst/src/model/realize.rs @@ -7,6 +7,7 @@ use super::{ }; use crate::diag::SourceResult; use crate::doc::Meta; +use crate::eval::item; use crate::util::hash128; /// Whether the target is affected by show rules in the given style chain. diff --git a/crates/typst/src/model/selector.rs b/crates/typst/src/model/selector.rs index 6d117acd..89c518bc 100644 --- a/crates/typst/src/model/selector.rs +++ b/crates/typst/src/model/selector.rs @@ -8,8 +8,8 @@ use smallvec::SmallVec; use super::{Content, Element, Label, Locatable, Location}; use crate::diag::{bail, StrResult}; use crate::eval::{ - cast, func, scope, ty, CastInfo, Dict, FromValue, Func, Reflect, Regex, Repr, Str, - Symbol, Type, Value, + cast, func, item, scope, ty, CastInfo, Dict, FromValue, Func, Reflect, Regex, Repr, + Str, Symbol, Type, Value, }; use crate::util::pretty_array_like; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index cce7c6af..ca8c1c0c 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -9,6 +9,7 @@ publish = false [dev-dependencies] typst = { workspace = true } typst-library = { workspace = true } +typst-pdf = { workspace = true } typst-render = { workspace = true } typst-svg = { workspace = true } clap = { workspace = true } diff --git a/tests/src/tests.rs b/tests/src/tests.rs index 3cf8aa1c..0271839c 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -420,7 +420,7 @@ fn test( let document = Document { pages: frames, ..Default::default() }; if compare_ever { if let Some(pdf_path) = pdf_path { - let pdf_data = typst::export::pdf( + let pdf_data = typst_pdf::pdf( &document, Some(&format!("typst-test: {}", name.display())), world.today(Some(0)), -- cgit v1.2.3