mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-15 23:02:32 +00:00
Merge pull request #2660 from dotCooCoo/fix/cmyk-view-conversion
CMYK view: convert the blended inks to screen colours on the render threads
This commit is contained in:
3 files changed
+100
-10
No files matched your search
@@ -12,6 +12,7 @@ use crate::{Frame, Placed, RenderOptions, Rendered, Renderer};
|
||||
use designcraft_color::BlendMode as DcBlend;
|
||||
use designcraft_doc::{Document, Item, SpreadRef};
|
||||
use designcraft_geom::{Affine, Rect, Shape};
|
||||
use vello_cpu::color::PremulRgba8;
|
||||
use vello_cpu::{RenderContext, peniko};
|
||||
|
||||
/// At most one viewport, capped at 64 MiB. The snapshot shares the document's large Arcs;
|
||||
@@ -284,6 +285,58 @@ fn srgb_in(table: &[[f32; 3]], cmyk: [f32; 4]) -> [f32; 3] {
|
||||
out
|
||||
}
|
||||
|
||||
/// Rows of the ink passes' area one conversion thread takes at least; the last band can hold fewer.
|
||||
const MIN_BAND_ROWS: usize = 32;
|
||||
|
||||
/// The blended inks, the C/M/Y complements in `cmy` and the K complement in `black` (`width`
|
||||
/// pixels a row), converted to screen colours in `cmy`. With render threads, bands of rows are
|
||||
/// converted on that many threads at once. Returns the number of bands.
|
||||
fn inks_to_screen(cmy: &mut [PremulRgba8], black: &[PremulRgba8], width: usize, rich_black: bool, threads: u16) -> usize {
|
||||
let table = srgb_table();
|
||||
let convert = |cmy: &mut [PremulRgba8], black: &[PremulRgba8]| {
|
||||
for (c, k) in cmy.iter_mut().zip(black) {
|
||||
if c.a == 0 {
|
||||
continue;
|
||||
}
|
||||
let a = c.a as f32;
|
||||
let inks = [1.0 - c.r as f32 / a, 1.0 - c.g as f32 / a, 1.0 - c.b as f32 / a, 1.0 - k.r as f32 / a];
|
||||
let rgb = if rich_black && inks[0..3].iter().all(|v| *v <= 1e-3) && inks[3] >= 0.999 { [0.0; 3] } else { srgb_in(&table, inks) };
|
||||
let [r, g, b] = rgb.map(|v| (v.clamp(0.0, 1.0) * a).round() as u8);
|
||||
(c.r, c.g, c.b) = (r, g, b);
|
||||
}
|
||||
};
|
||||
let rows = cmy.len().checked_div(width).unwrap_or(0);
|
||||
let bands = usize::from(threads).min(rows / MIN_BAND_ROWS).max(1);
|
||||
if cfg!(target_arch = "wasm32") || bands == 1 {
|
||||
convert(cmy, black);
|
||||
return 1;
|
||||
}
|
||||
let chunk = rows.div_ceil(bands) * width;
|
||||
let mut parts: Vec<(&mut [PremulRgba8], &[PremulRgba8])> = cmy.chunks_mut(chunk).zip(black.chunks(chunk)).collect();
|
||||
let convert = &convert;
|
||||
let mut failed = Vec::new();
|
||||
std::thread::scope(|s| {
|
||||
let mut each = parts.iter_mut().enumerate();
|
||||
let first = each.next();
|
||||
for (i, (c, k)) in each {
|
||||
if let Err(e) = std::thread::Builder::new().spawn_scoped(s, move || convert(c, k)) {
|
||||
log::warn!("CMYK view: a conversion thread did not start ({e})");
|
||||
failed.push(i);
|
||||
}
|
||||
}
|
||||
if let Some((_, (c, k))) = first {
|
||||
convert(c, k);
|
||||
}
|
||||
});
|
||||
// A band whose thread did not start is converted here.
|
||||
for i in failed {
|
||||
if let Some((c, k)) = parts.get_mut(i) {
|
||||
convert(c, k);
|
||||
}
|
||||
}
|
||||
parts.len()
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
/// The view pixels the spread's transparency can change: the union of its transparent
|
||||
/// objects (opacity, blend, effects, knockout, also nested or anchored) with their effects'
|
||||
@@ -413,16 +466,10 @@ impl Renderer {
|
||||
crate::PLATE.with(|p| p.set(f.opts.plate));
|
||||
self.stats.items = (self.stats.items - stats.items) / 2 + stats.items;
|
||||
self.stats.glyphs = (self.stats.glyphs - stats.glyphs) / 2 + stats.glyphs;
|
||||
let table = srgb_table();
|
||||
for (c, k) in cmy.data_mut().iter_mut().zip(black.data()) {
|
||||
if c.a == 0 {
|
||||
continue;
|
||||
}
|
||||
let a = c.a as f32;
|
||||
let inks = [1.0 - c.r as f32 / a, 1.0 - c.g as f32 / a, 1.0 - c.b as f32 / a, 1.0 - k.r as f32 / a];
|
||||
let rgb = if f.opts.rich_black && inks[0..3].iter().all(|v| *v <= 1e-3) && inks[3] >= 0.999 { [0.0; 3] } else { srgb_in(&table, inks) };
|
||||
let [r, g, b] = rgb.map(|v| (v.clamp(0.0, 1.0) * a).round() as u8);
|
||||
(c.r, c.g, c.b) = (r, g, b);
|
||||
let _bands = inks_to_screen(cmy.data_mut(), black.data(), usize::from(aw), f.opts.rich_black, threads);
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.cmyk_convert_bands = _bands;
|
||||
}
|
||||
ctx.set_transform(Affine::translate((area.x0, area.y0)));
|
||||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(std::sync::Arc::new(cmy)), sampler: peniko::ImageSampler::default() });
|
||||
@@ -669,6 +716,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// With render threads, the ink passes' colours are converted in that many bands of rows at
|
||||
/// once, each band by its own thread, and the result is the same as converting on one thread.
|
||||
#[test]
|
||||
fn ink_colours_convert_in_a_band_per_render_thread() {
|
||||
let mut doc = document();
|
||||
rectangle(&mut doc, SpreadRef::Doc(0), Rect::new(-20.0, -5.0, 180.0, 125.0), "C=100 M=0 Y=0 K=0", DcBlend::Multiply);
|
||||
let render = |threads: u16| {
|
||||
let mut renderer = Renderer::new();
|
||||
renderer.threads = threads;
|
||||
let image = renderer.render(
|
||||
&doc,
|
||||
&Cache::new(),
|
||||
&[Placed { spread: SpreadRef::Doc(0), xf: Affine::IDENTITY }],
|
||||
(400, 300),
|
||||
Affine::scale(2.0) * Affine::translate((30.0, 10.0)),
|
||||
&RenderOptions { blend_space_view: true, background: Some([180, 180, 180, 255]), ..Default::default() },
|
||||
);
|
||||
(image, renderer.cmyk_convert_bands)
|
||||
};
|
||||
let (one, bands) = render(0);
|
||||
assert_eq!(bands, 1);
|
||||
let (four, bands) = render(4);
|
||||
assert_eq!(bands, 4, "a band per render thread");
|
||||
assert_eq!(four.pixels, one.pixels);
|
||||
}
|
||||
|
||||
/// The returned count is the number of bands converted: 1,281 rows on 40 threads split into
|
||||
/// bands of 33 rows, which makes 39 bands.
|
||||
#[test]
|
||||
fn ink_conversion_returns_the_bands_it_converted() {
|
||||
let rows = 1281;
|
||||
let mut cmy = vec![PremulRgba8 { r: 0, g: 255, b: 255, a: 255 }; rows];
|
||||
let black = vec![PremulRgba8 { r: 255, g: 255, b: 255, a: 255 }; rows];
|
||||
assert_eq!(inks_to_screen(&mut cmy, &black, 1, false, 40), 39);
|
||||
assert!(cmy.iter().all(|c| *c == cmy[0]), "every band is converted");
|
||||
assert_ne!(cmy[0], PremulRgba8 { r: 0, g: 255, b: 255, a: 255 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmyk_nonseparable_modes_use_the_existing_rgb_approximation() {
|
||||
let mut doc = document();
|
||||
|
||||
@@ -21,6 +21,8 @@ impl Renderer {
|
||||
cmyk_pass_pixels: 0,
|
||||
#[cfg(test)]
|
||||
cmyk_pass_items: 0,
|
||||
#[cfg(test)]
|
||||
cmyk_convert_bands: 0,
|
||||
threads: default_threads(),
|
||||
stats: FrameStats::default(),
|
||||
}
|
||||
|
||||
@@ -163,6 +163,9 @@ pub struct Renderer {
|
||||
/// Items the last ink pass drew (tests).
|
||||
#[cfg(test)]
|
||||
cmyk_pass_items: usize,
|
||||
/// Bands of rows the last ink passes' colours were converted in (tests).
|
||||
#[cfg(test)]
|
||||
cmyk_convert_bands: usize,
|
||||
pub threads: u16,
|
||||
pub stats: FrameStats,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user