mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Render: placed images are premultiplied in their decoded buffer
Decoding a placed raster built a second full-size pixel vector from the RGBA image and premultiplied every pixel into it. The pixels are now premultiplied in place, opaque pixels are skipped, and the buffer becomes the pixmap's pixels without a copy (bytemuck's cast_vec, with a copy when the capacity is not a whole number of pixels). The rendered SVG path reuses its buffer the same way. For a 6000 x 4000 opaque image the step takes 7 ms instead of 120 ms, with identical pixels.
This commit is contained in:
1 parent
f5f7084fc3
commit
4e363e1bfa
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"
|
||||
|
||||
@@ -388,6 +388,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
## Progress log
|
||||
|
||||
- **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-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.
|
||||
- **2026-11-04** · Esperanto is a text language (Character › Language, `type.char {language: "Esperanto"}`, IDML `$ID/Esperanto`): tag `eo`, English-style quotes; not hyphenated yet (#1201). 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