mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-31 15:48:18 +00:00
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:
1 parent
55e5ee56b4
commit
d6f532bb0d
2 files changed
+44
-6
No files matched your search
+19
-3
@@ -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
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user