From ac322e342b4736bea189f5e7ac39561c0e5a3127 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Sun, 14 Jul 2024 16:02:50 +0200 Subject: [PATCH] Save and restore graphics state for every frame (#4496) Co-authored-by: Laurenz --- crates/typst-pdf/src/content.rs | 49 +++++++++++---------- tests/ref/issue-4361-transparency-leak.png | Bin 0 -> 3515 bytes tests/suite/visualize/color.typ | 7 +++ 3 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 tests/ref/issue-4361-transparency-leak.png diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index 8ae2c424d..da9e4ed44 100644 --- a/crates/typst-pdf/src/content.rs +++ b/crates/typst-pdf/src/content.rs @@ -92,7 +92,7 @@ pub struct Builder<'a, R = ()> { state: State, /// Stack of saved graphic states. saves: Vec, - /// Wheter any stroke or fill was not totally opaque. + /// Whether any stroke or fill was not totally opaque. uses_opacities: bool, /// All clickable links that are present in this content. links: Vec<(Destination, Rect)>, @@ -129,7 +129,7 @@ struct State { /// The color space of the current fill paint. fill_space: Option>, /// The current external graphic state. - external_graphics_state: Option, + external_graphics_state: ExtGState, /// The current stroke paint. stroke: Option, /// The color space of the current stroke paint. @@ -148,7 +148,7 @@ impl State { font: None, fill: None, fill_space: None, - external_graphics_state: None, + external_graphics_state: ExtGState::default(), stroke: None, stroke_space: None, text_rendering_mode: TextRenderingMode::Fill, @@ -191,12 +191,13 @@ impl Builder<'_, ()> { } 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) { + let current_state = &self.state.external_graphics_state; + if current_state != graphics_state { let index = self.resources.ext_gs.insert(*graphics_state); let name = eco_format!("Gs{index}"); self.content.set_parameters(Name(name.as_bytes())); + self.state.external_graphics_state = *graphics_state; if graphics_state.uses_opacities() { self.uses_opacities = true; } @@ -204,29 +205,27 @@ impl Builder<'_, ()> { } 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(_) | Paint::Pattern(_) => return 255, - }; + let get_opacity = |paint: &Paint| { + let color = match paint { + Paint::Solid(color) => *color, + Paint::Gradient(_) | Paint::Pattern(_) => 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(_) | Paint::Pattern(_) => return 255, - }; + color.alpha().map_or(255, |v| (v * 255.0).round() as u8) + }; - color.alpha().map_or(255, |v| (v * 255.0).round() as u8) - }) - .unwrap_or(255); + let stroke_opacity = stroke.map_or(255, |stroke| get_opacity(&stroke.paint)); + let fill_opacity = fill.map_or(255, get_opacity); self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); } + fn reset_opacities(&mut self) { + self.set_external_graphics_state(&ExtGState { + stroke_opacity: 255, + fill_opacity: 255, + }); + } + pub fn transform(&mut self, transform: Transform) { let Transform { sx, ky, kx, sy, tx, ty } = transform; self.state.transform = self.state.transform.pre_concat(transform); @@ -542,6 +541,8 @@ fn write_color_glyphs(ctx: &mut Builder, pos: Point, text: TextItemView) { let mut last_font = None; + ctx.reset_opacities(); + ctx.content.begin_text(); ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); // So that the next call to ctx.set_font() will change the font to one that @@ -671,6 +672,8 @@ fn write_image(ctx: &mut Builder, x: f32, y: f32, image: &Image, size: Size) { image }); + ctx.reset_opacities(); + let name = eco_format!("Im{index}"); let w = size.x.to_f32(); let h = size.y.to_f32(); diff --git a/tests/ref/issue-4361-transparency-leak.png b/tests/ref/issue-4361-transparency-leak.png new file mode 100644 index 0000000000000000000000000000000000000000..4060d43ac442e67e674245685f3fbfbe1d555157 GIT binary patch literal 3515 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 literal 0 HcmV?d00001 diff --git a/tests/suite/visualize/color.typ b/tests/suite/visualize/color.typ index 45000ab24..bc8f8be5b 100644 --- a/tests/suite/visualize/color.typ +++ b/tests/suite/visualize/color.typ @@ -333,3 +333,10 @@ --- issue-color-mix-luma --- // When mixing luma colors, we accidentally used the wrong component. #rect(fill: gradient.linear(black, silver, space: luma)) + +--- issue-4361-transparency-leak --- +// Ensure that transparency doesn't leak from shapes to images in PDF. The PNG +// test doesn't validate it, but at least we can discover regressions on the PDF +// output with a PDF comparison script. +#rect(fill: red.transparentize(50%)) +#image("/assets/images/tiger.jpg", width: 45pt)