summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz Stampfl <47084093+LaurenzV@users.noreply.github.com>2023-08-05 12:03:26 +0200
committerGitHub <noreply@github.com>2023-08-05 12:03:26 +0200
commit49282626e98cb1fbbb06c774cc0ae470b2854af6 (patch)
treed0e75480ee7d5a806a616dd9d7642ea4b8c8dd36
parentba0990f189ec7120d8be949c081998d721bbe5e6 (diff)
Add support for opacities (#1844)
-rw-r--r--crates/typst-library/src/compute/construct.rs4
-rw-r--r--crates/typst/src/export/pdf/external_graphics_state.rs37
-rw-r--r--crates/typst/src/export/pdf/mod.rs8
-rw-r--r--crates/typst/src/export/pdf/page.rs68
4 files changed, 113 insertions, 4 deletions
diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs
index 4329f259..841bf406 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 00000000..164de1b6
--- /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 48485862..b650d5b9 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<u8> {
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<Ref>,
image_refs: Vec<Ref>,
+ ext_gs_refs: Vec<Ref>,
page_refs: Vec<Ref>,
font_map: Remapper<Font>,
image_map: Remapper<Image>,
+ ext_gs_map: Remapper<ExternalGraphicsState>,
/// 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 22e590d5..0c0d2957 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<State>,
bottom: f32,
+ uses_opacities: bool,
links: Vec<(Destination, Rect)>,
}
@@ -184,6 +208,7 @@ struct State {
font: Option<(Font, Abs)>,
fill: Option<Paint>,
fill_space: Option<Name<'static>>,
+ external_graphics_state: Option<ExternalGraphicsState>,
stroke: Option<Stroke>,
stroke_space: Option<Name<'static>>,
}
@@ -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();