Add support for opacities (#1844)

This commit is contained in:
Laurenz Stampfl 2023-08-05 12:03:26 +02:00 committed by GitHub
parent ba0990f189
commit 49282626e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 113 additions and 4 deletions

View File

@ -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"))

View File

@ -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();
}
}

View File

@ -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(),
}

View File

@ -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();