diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs index 4329f259c..841bf4069 100644 --- a/crates/typst-library/src/compute/construct.rs +++ b/crates/typst-library/src/compute/construct.rs @@ -105,10 +105,6 @@ pub fn luma( /// /// The color is specified in the sRGB color space. /// -/// _Note:_ While you can specify transparent colors and Typst's preview will -/// render them correctly, the PDF export does not handle them properly at the -/// moment. This will be fixed in the future. -/// /// ## Example { #example } /// ```example /// #square(fill: rgb("#b1f2eb")) diff --git a/crates/typst/src/export/pdf/external_graphics_state.rs b/crates/typst/src/export/pdf/external_graphics_state.rs new file mode 100644 index 000000000..164de1b6e --- /dev/null +++ b/crates/typst/src/export/pdf/external_graphics_state.rs @@ -0,0 +1,37 @@ +use crate::export::pdf::{PdfContext, RefExt}; +use pdf_writer::Finish; + +/// A PDF external graphics state. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct ExternalGraphicsState { + // 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 ExternalGraphicsState { + fn default() -> Self { + Self { stroke_opacity: 255, fill_opacity: 255 } + } +} + +impl ExternalGraphicsState { + 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.ext_gs_map.items() { + let gs_ref = ctx.alloc.bump(); + ctx.ext_gs_refs.push(gs_ref); + + let mut gs = ctx.writer.ext_graphics(gs_ref); + gs.non_stroking_alpha(external_gs.fill_opacity as f32 / 255.0) + .stroking_alpha(external_gs.stroke_opacity as f32 / 255.0); + gs.finish(); + } +} diff --git a/crates/typst/src/export/pdf/mod.rs b/crates/typst/src/export/pdf/mod.rs index 484858628..b650d5b99 100644 --- a/crates/typst/src/export/pdf/mod.rs +++ b/crates/typst/src/export/pdf/mod.rs @@ -1,5 +1,6 @@ //! Exporting into PDF documents. +mod external_graphics_state; mod font; mod image; mod outline; @@ -21,6 +22,8 @@ use crate::geom::{Abs, Dir, Em}; use crate::image::Image; use crate::model::Introspector; +use external_graphics_state::ExternalGraphicsState; + /// Export a document into a PDF file. /// /// Returns the raw bytes making up the PDF file. @@ -30,6 +33,7 @@ pub fn pdf(document: &Document) -> Vec { page::construct_pages(&mut ctx, &document.pages); font::write_fonts(&mut ctx); image::write_images(&mut ctx); + external_graphics_state::write_external_graphics_states(&mut ctx); page::write_page_tree(&mut ctx); write_catalog(&mut ctx); ctx.writer.finish() @@ -50,9 +54,11 @@ pub struct PdfContext<'a> { page_tree_ref: Ref, font_refs: Vec, image_refs: Vec, + ext_gs_refs: Vec, page_refs: Vec, font_map: Remapper, image_map: Remapper, + ext_gs_map: Remapper, /// 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, @@ -78,8 +84,10 @@ impl<'a> PdfContext<'a> { page_refs: vec![], font_refs: vec![], image_refs: vec![], + ext_gs_refs: vec![], font_map: Remapper::new(), image_map: Remapper::new(), + ext_gs_map: Remapper::new(), glyph_sets: HashMap::new(), languages: HashMap::new(), } diff --git a/crates/typst/src/export/pdf/page.rs b/crates/typst/src/export/pdf/page.rs index 22e590d51..0c0d29571 100644 --- a/crates/typst/src/export/pdf/page.rs +++ b/crates/typst/src/export/pdf/page.rs @@ -5,6 +5,7 @@ use pdf_writer::types::{ use pdf_writer::writers::ColorSpace; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; +use super::external_graphics_state::ExternalGraphicsState; use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB}; use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem}; use crate::font::Font; @@ -32,6 +33,7 @@ pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) { let mut ctx = PageContext { parent: ctx, page_ref, + uses_opacities: false, content: Content::new(), state: State::default(), saves: vec![], @@ -59,6 +61,7 @@ pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) { size, content: ctx.content, id: ctx.page_ref, + uses_opacities: ctx.uses_opacities, links: ctx.links, }; @@ -98,6 +101,14 @@ pub fn write_page_tree(ctx: &mut PdfContext) { } images.finish(); + + let mut ext_gs_states = resources.ext_g_states(); + for (gs_ref, gs) in ctx.ext_gs_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(); } @@ -115,6 +126,16 @@ fn write_page(ctx: &mut PdfContext, page: Page) { 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(); @@ -161,6 +182,8 @@ pub struct Page { pub size: Size, /// The page's content stream. pub content: Content, + /// Whether the page uses opacities. + pub uses_opacities: bool, /// Links in the PDF coordinate system. pub links: Vec<(Destination, Rect)>, } @@ -173,6 +196,7 @@ struct PageContext<'a, 'b> { state: State, saves: Vec, bottom: f32, + uses_opacities: bool, links: Vec<(Destination, Rect)>, } @@ -184,6 +208,7 @@ struct State { font: Option<(Font, Abs)>, fill: Option, fill_space: Option>, + external_graphics_state: Option, stroke: Option, stroke_space: Option>, } @@ -199,6 +224,46 @@ impl PageContext<'_, '_> { self.state = self.saves.pop().expect("missing state save"); } + fn set_external_graphics_state(&mut self, graphics_state: &ExternalGraphicsState) { + let current_state = self.state.external_graphics_state.as_ref(); + if current_state != Some(graphics_state) { + self.parent.ext_gs_map.insert(*graphics_state); + let name = eco_format!("Gs{}", self.parent.ext_gs_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<&Stroke>, fill: Option<&Paint>) { + let stroke_opacity = stroke + .map(|stroke| { + let Paint::Solid(color) = stroke.paint; + if let Color::Rgba(rgba_color) = color { + rgba_color.a + } else { + 255 + } + }) + .unwrap_or(255); + let fill_opacity = fill + .map(|paint| { + let Paint::Solid(color) = paint; + if let Color::Rgba(rgba_color) = color { + rgba_color.a + } else { + 255 + } + }) + .unwrap_or(255); + self.set_external_graphics_state(&ExternalGraphicsState { + 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); @@ -373,6 +438,7 @@ fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &TextItem) { ctx.set_fill(&text.fill); ctx.set_font(&text.font, text.size); + ctx.set_opacities(None, Some(&text.fill)); ctx.content.begin_text(); // Positiosn the text. @@ -438,6 +504,8 @@ fn write_shape(ctx: &mut PageContext, x: f32, y: f32, shape: &Shape) { ctx.set_stroke(stroke); } + ctx.set_opacities(stroke, shape.fill.as_ref()); + match shape.geometry { Geometry::Line(target) => { let dx = target.x.to_f32();