PDF export: transparent content pixels take the backdrop colour (fixes #1455)

The content raster's fully transparent pixels were RGB 0,0,0. A viewer that
smooths the image blends colour and mask separately, so the black showed as a
grey hairline around white pictures and white-filled shapes.
This commit is contained in:
Harrison Booth committed 2026-10-27 18:52:43 -05:00
1 parent 55e5ee56b4
commit d6f532bb0d
2 files changed
+44 -6

No files matched your search

+19 -3
View File
@@ -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();
+25 -3
View File
@@ -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<u8> = straight.as_chunks::<4>().0.iter().flat_map(|px| px[..3].iter().copied()).collect();
let want_alpha: Vec<u8> = 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).