diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 32da17da..6baeda31 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -451,6 +451,17 @@ fn opaque_px(rgba: &mut [u8]) { } } +/// Gives every fully transparent content pixel the backdrop's colour. The renderer leaves them +/// RGB 0,0,0; a viewer that smooths the image blends colour and mask separately, so the black +/// shows as a grey hairline around white pictures and shapes (#1455). +fn fill_clear(front: &mut [u8], back: &[u8]) { + for (f, b) in front.as_chunks_mut::<4>().0.iter_mut().zip(back.as_chunks::<4>().0) { + if f[3] == 0 { + f[..3].copy_from_slice(&b[..3]); + } + } +} + /// Most bytes of opaque backdrop rasters kept to recognise a backdrop seen before. const BACKDROP_CACHE_BYTES: usize = 32 << 20; @@ -515,7 +526,8 @@ impl Exporter<'_> { // Two rasters per slide: the backdrop (background, master and layout graphics) is the same // on every slide that shares its layout, so the writer stores it once, and the slide's own // shapes draw over it (#845). - let back = deckcraft_render::render_slide_part(pres, i, deckcraft_render::SlidePart::Backdrop, &ropts); + let mut back = deckcraft_render::render_slide_part(pres, i, deckcraft_render::SlidePart::Backdrop, &ropts); + opaque(&mut back.pixels); let front = deckcraft_render::render_slide_part(pres, i, deckcraft_render::SlidePart::Content, &ropts); let (w, h) = (back.width, back.height); if w > 0 @@ -523,11 +535,15 @@ impl Exporter<'_> { && let Some(size) = Size::from_wh(b.width() as f32, b.height() as f32) { s.push_transform(&Transform::from_translate(b.x0 as f32, b.y0 as f32)); - s.draw_image(self.backdrops.image(back), size); // Taken, not copied: a print-resolution slide is ~16 MB. let mut rgba = front.pixels; - if rgba.as_chunks::<4>().0.iter().any(|px| px[3] != 0) { + let content = rgba.as_chunks::<4>().0.iter().any(|px| px[3] != 0); + if content { + fill_clear(&mut rgba, &back.pixels); straight(&mut rgba); + } + s.draw_image(self.backdrops.image(back), size); + if content { s.draw_image(Image::from_rgba8(rgba, w, h), size); } s.pop(); diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index dde7a9b7..8bdbbce9 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -7,8 +7,8 @@ use deckcraft_render::RenderOpts; use kurbo::Rect; use super::{ - MAX_RASTER_PIXELS, PAPERS, PageLayout, PdfError, PdfOptions, Presentation, export, glyph_text_ranges, handout_boxes, master_text_pieces, - master_texts, notes_frames, notes_slide_box, opaque, paper_size, raster_scale, straight, + MAX_RASTER_PIXELS, PAPERS, PageLayout, PdfError, PdfOptions, Presentation, export, fill_clear, glyph_text_ranges, handout_boxes, + master_text_pieces, master_texts, notes_frames, notes_slide_box, opaque, paper_size, raster_scale, straight, }; use deckcraft_model::text::TextBody; @@ -582,7 +582,20 @@ fn slides_share_one_backdrop_raster_and_keep_their_pixels() { deckcraft_render::SlidePart::Content, &RenderOpts { scale: scale(&p), threads: 4, ..Default::default() }, ); - let straight = expect.to_straight(); + let mut straight = expect.to_straight(); + // Fully transparent pixels carry the backdrop colour, not black (#1455). + let mut back = deckcraft_render::render_slide_part( + &p, + 0, + deckcraft_render::SlidePart::Backdrop, + &RenderOpts { scale: scale(&p), threads: 4, ..Default::default() }, + ); + opaque(&mut back.pixels); + for (px, b) in straight.as_chunks_mut::<4>().0.iter_mut().zip(back.pixels.as_chunks::<4>().0) { + if px[3] == 0 { + px[..3].copy_from_slice(&b[..3]); + } + } let want_rgb: Vec = straight.as_chunks::<4>().0.iter().flat_map(|px| px[..3].iter().copied()).collect(); let want_alpha: Vec = straight.as_chunks::<4>().0.iter().map(|px| px[3]).collect(); assert_eq!(rgb, want_rgb, "content colors are straight, not premultiplied"); @@ -634,6 +647,15 @@ fn backdrop_and_content_pixels_convert_per_pixel() { } } +/// Fully transparent content pixels take the backdrop colour (not black) so a smoothing viewer +/// draws no hairline at the edge of a picture; every other pixel is untouched (#1455). +#[test] +fn transparent_content_pixels_take_the_backdrop_colour() { + let mut front = vec![0, 0, 0, 0, 10, 20, 30, 255, 5, 5, 5, 1, 0, 0, 0, 0]; + fill_clear(&mut front, &[255, 250, 240, 255].repeat(4)); + assert_eq!(front, [255, 250, 240, 0, 10, 20, 30, 255, 5, 5, 5, 1, 255, 250, 240, 0]); +} + #[test] fn glyph_ranges_follow_shaping_clusters() { // "fl" ligature: its glyph maps to both characters, the rest stay one to one (#164).