Add support for opacities (#1844)
This commit is contained in:
parent
ba0990f189
commit
49282626e9
@ -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"))
|
||||
|
37
crates/typst/src/export/pdf/external_graphics_state.rs
Normal file
37
crates/typst/src/export/pdf/external_graphics_state.rs
Normal 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();
|
||||
}
|
||||
}
|
@ -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(),
|
||||
}
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user