mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Merge pull request #2709 from dotCooCoo/fix/opaque-image-premultiply
Render: placed images are premultiplied in their decoded buffer
This commit is contained in:
6 files changed
+62
-11
No files matched your search
Generated
+1
@@ -1574,6 +1574,7 @@ dependencies = [
|
||||
name = "designcraft-render"
|
||||
version = "0.20.1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"craft-pdf",
|
||||
"designcraft-color",
|
||||
"designcraft-compose",
|
||||
|
||||
@@ -64,6 +64,8 @@ skrifa = "0.47"
|
||||
harfrust = "0.13"
|
||||
vello_cpu = "0.2"
|
||||
vello_common = { version = "0.2", default-features = false }
|
||||
# Decoded image buffers become pixmap pixels without a copy; the version vello_common uses.
|
||||
bytemuck = { version = "1.25", features = ["extern_crate_alloc"] }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] }
|
||||
# CMYK TIFF samples for PDF export (`image` decodes them to RGB); the version `image` uses.
|
||||
tiff = "0.11"
|
||||
|
||||
@@ -396,6 +396,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
- **2026-11-12** · An edit of a long threaded story composes the paragraphs it can change and copies the other lines from the composition before the edit; every typed character composed the whole story. In a 240-paragraph story a typed character composes 3 paragraphs (240 before). An edit that moves lines into other frames composes on until a paragraph starts in the same frame and place as before, or to the story's end, and stories with footnotes, tables, anchored objects, balanced columns or keep options that move lines are composed whole. No readiness percentage change.
|
||||
- **2026-11-12** · Type shortcuts: Justify All Lines ⇧⌘F, Insert White Space › Em Space ⇧⌘M and En Space ⇧⌘N (shown in the menu, enabled at an insertion point), Paragraph Rules… ⌥⌘J (shown in the Paragraph panel menu). On the Mac a menu-bar insert (Em/En Space, Current Page Number) no longer types into the document while a text field has focus, a dialog or Quick Apply is open, or an input method is composing. An alignment the paragraphs already have records no undo step (#1138). No readiness percentage change.
|
||||
- **2026-11-12** · A decoded placed image becomes a screen pixmap in its own buffer, premultiplied in place, and opaque pixels are left as they are: 120 ms becomes 7 ms for a 24-megapixel opaque photo, with the same pixels. No readiness percentage change.
|
||||
- **2026-11-12** · The status bar's preflight count and the Preflight panel take their issues from a check on a background worker, which checks the newest state of the document and shows the issues last found meanwhile. Moving or resizing a frame and typing no longer wait for a check of the whole document on every drag step and keystroke. No readiness percentage change.
|
||||
- **2026-11-12** · Object › Text Frame Options… (Cmd+B) opens with the Type tool in a frame, so inset spacing and vertical justification can be changed while typing; it changes the frame that shows the insertion point, or every frame that holds part of the selected text (#2396). No readiness percentage change.
|
||||
- **2026-11-04** · Striped strokes retain out-and-back cubic segments whose second control point is away from the coincident endpoints and first control point. A geometry regression checks the offset extent and both stripe bands. No readiness percentage change.
|
||||
|
||||
@@ -16,6 +16,7 @@ designcraft-fonts = { workspace = true }
|
||||
designcraft-compose = { workspace = true }
|
||||
vello_cpu = { workspace = true }
|
||||
vello_common = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
image = { workspace = true }
|
||||
craft-pdf = { workspace = true, features = ["raster"] }
|
||||
designcraft-images = { workspace = true }
|
||||
|
||||
+27
-11
@@ -16,23 +16,39 @@ pub fn decode_pixmap_page(bytes: &[u8], page: u32) -> Option<Pixmap> {
|
||||
}
|
||||
if designcraft_images::is_svg(bytes) {
|
||||
let (px, w, h) = designcraft_images::render_svg(bytes, 3000)?;
|
||||
let data = px.as_chunks::<4>().0.iter().map(|p| vello_cpu::color::PremulRgba8 { r: p[0], g: p[1], b: p[2], a: p[3] }).collect();
|
||||
return Some(Pixmap::from_parts(data, w.min(u16::MAX as u32) as u16, h.min(u16::MAX as u32) as u16));
|
||||
return Some(Pixmap::from_parts(premul_pixels(px), w.min(u16::MAX as u32) as u16, h.min(u16::MAX as u32) as u16));
|
||||
}
|
||||
let img = designcraft_images::decode_rgba(bytes)?;
|
||||
let (w, h) = img.dimensions();
|
||||
if w == 0 || h == 0 || w > u16::MAX as u32 || h > u16::MAX as u32 {
|
||||
return None;
|
||||
}
|
||||
let data: Vec<vello_cpu::color::PremulRgba8> = img
|
||||
.pixels()
|
||||
.map(|p| {
|
||||
let a = p[3] as u16;
|
||||
let m = |c: u8| ((c as u16 * a + 127) / 255) as u8;
|
||||
vello_cpu::color::PremulRgba8 { r: m(p[0]), g: m(p[1]), b: m(p[2]), a: p[3] }
|
||||
})
|
||||
.collect();
|
||||
Some(Pixmap::from_parts(data, w as u16, h as u16))
|
||||
let mut px = img.into_raw();
|
||||
px.truncate(w as usize * h as usize * 4);
|
||||
Some(Pixmap::from_parts(premultiply(px), w as u16, h as u16))
|
||||
}
|
||||
|
||||
/// Premultiply straight-alpha RGBA bytes in place and return them as pixmap pixels. Opaque pixels
|
||||
/// are left as they are.
|
||||
pub(crate) fn premultiply(mut px: Vec<u8>) -> Vec<vello_cpu::color::PremulRgba8> {
|
||||
for p in px.as_chunks_mut::<4>().0 {
|
||||
let a = p[3];
|
||||
if a != 255 {
|
||||
let m = |c: u8| ((c as u16 * a as u16 + 127) / 255) as u8;
|
||||
p[0] = m(p[0]);
|
||||
p[1] = m(p[1]);
|
||||
p[2] = m(p[2]);
|
||||
}
|
||||
}
|
||||
premul_pixels(px)
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA bytes as pixmap pixels, in the same allocation when its capacity is a whole
|
||||
/// number of pixels and in a copy otherwise.
|
||||
fn premul_pixels(px: Vec<u8>) -> Vec<vello_cpu::color::PremulRgba8> {
|
||||
bytemuck::allocation::try_cast_vec(px).unwrap_or_else(|(_, px)| {
|
||||
px.as_chunks::<4>().0.iter().map(|p| vello_cpu::color::PremulRgba8 { r: p[0], g: p[1], b: p[2], a: p[3] }).collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Box-filter downsample by 2.
|
||||
|
||||
@@ -30,6 +30,36 @@ fn eps_source_decodes_like_its_preview() {
|
||||
assert!(image_size(corrupt).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoded_images_are_premultiplied_for_every_alpha() {
|
||||
// Column x holds colour value x, row y holds alpha y.
|
||||
let img = image::RgbaImage::from_fn(256, 256, |x, y| image::Rgba([x as u8, 255 - x as u8, (x / 2) as u8, y as u8]));
|
||||
let mut png = Vec::new();
|
||||
image::DynamicImage::ImageRgba8(img.clone()).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let pm = decode_pixmap(&png).unwrap();
|
||||
assert_eq!((pm.width(), pm.height()), (256, 256));
|
||||
for (p, q) in img.pixels().zip(pm.data()) {
|
||||
let m = |c: u8| ((c as u16 * p[3] as u16 + 127) / 255) as u8;
|
||||
assert_eq!([q.r, q.g, q.b, q.a], [m(p[0]), m(p[1]), m(p[2]), p[3]], "{p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn premultiplied_pixels_reuse_the_decoded_buffer() {
|
||||
use vello_cpu::color::PremulRgba8;
|
||||
let expected =
|
||||
[PremulRgba8 { r: 10, g: 20, b: 30, a: 255 }, PremulRgba8 { r: 100, g: 50, b: 25, a: 128 }, PremulRgba8 { r: 0, g: 0, b: 0, a: 0 }];
|
||||
let px = vec![10, 20, 30, 255, 200, 100, 50, 128, 1, 2, 3, 0];
|
||||
let ptr = px.as_ptr() as usize;
|
||||
let out = decode::premultiply(px);
|
||||
assert_eq!(out.as_ptr() as usize, ptr);
|
||||
assert_eq!(out, expected);
|
||||
// A capacity that is not a whole number of pixels is copied.
|
||||
let mut px = Vec::with_capacity(13);
|
||||
px.extend([10, 20, 30, 255, 200, 100, 50, 128, 1, 2, 3, 0]);
|
||||
assert_eq!(decode::premultiply(px), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn footnote_vector_objects_are_painted() {
|
||||
use designcraft_doc::{AnchorPosition, AnchoredObject, ItemId, Shape, SpreadRef};
|
||||
|
||||
Reference in new issue
Block a user