mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 13:46:52 -06:00
110 lines
4.6 KiB
Rust
110 lines
4.6 KiB
Rust
//! A swatch applied to a placed greyscale or bitmap image (#1094). A greyscale pixel becomes the
|
|
//! swatch at the pixel's ink coverage (its darkness), white staying opaque; a bitmap's black
|
|
//! takes the swatch and its white turns transparent, so the frame's fill shows through.
|
|
|
|
use std::collections::HashMap;
|
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
|
use std::sync::{Arc, Mutex, PoisonError};
|
|
|
|
use designcraft_color::swatch::apply_tint;
|
|
use designcraft_doc::{Document, Fill};
|
|
use designcraft_images::Tone;
|
|
use vello_cpu::Pixmap;
|
|
use vello_cpu::color::PremulRgba8;
|
|
|
|
/// The tone of each image file a swatch was applied to, by the address of its bytes (kept alive
|
|
/// so the address stays theirs). A GIF is decoded to know, so this is not done every frame.
|
|
fn tone_of(bytes: &Arc<Vec<u8>>) -> Option<Tone> {
|
|
type Tones = HashMap<usize, (Arc<Vec<u8>>, Option<Tone>)>;
|
|
const MAX: usize = 256;
|
|
static TONES: Mutex<Option<Tones>> = Mutex::new(None);
|
|
let key = Arc::as_ptr(bytes) as usize;
|
|
if let Some((_, t)) = TONES.lock().unwrap_or_else(PoisonError::into_inner).as_ref().and_then(|m| m.get(&key)) {
|
|
return *t;
|
|
}
|
|
let tone = designcraft_images::tone(bytes);
|
|
let mut guard = TONES.lock().unwrap_or_else(PoisonError::into_inner);
|
|
let map = guard.get_or_insert_with(HashMap::new);
|
|
if map.len() >= MAX {
|
|
map.clear();
|
|
}
|
|
map.insert(key, (bytes.clone(), tone));
|
|
tone
|
|
}
|
|
|
|
/// `pm` (decoded from `bytes`) coloured by `fill`, cached per image and on-screen colour. `None`
|
|
/// when the image keeps its own colours: no swatch, one that no longer exists, or a colour image.
|
|
pub(crate) fn colorized(doc: &Document, fill: &Fill, bytes: &Arc<Vec<u8>>, pm: &Arc<Pixmap>) -> Option<Arc<Pixmap>> {
|
|
if fill.is_none() {
|
|
return None;
|
|
}
|
|
let color = doc.resolve_color(&fill.swatch, fill.tint)?;
|
|
let tone = tone_of(bytes)?;
|
|
// The swatch at each ink coverage as the screen shows it (or the plate being previewed).
|
|
let lut: Vec<[u8; 4]> = (0..=255u8)
|
|
.map(|i| {
|
|
let c = crate::color_of(&apply_tint(color, f32::from(i) / 255.0), 1.0).to_rgba8();
|
|
[c.r, c.g, c.b, c.a]
|
|
})
|
|
.collect();
|
|
let mut h = DefaultHasher::new();
|
|
lut.hash(&mut h);
|
|
let key = (Arc::as_ptr(pm) as usize, tone, h.finish());
|
|
|
|
// As for plate previews: source pixmaps are kept alive (their addresses are keys), new colour
|
|
// settings start over, and the derived pixels have a memory budget.
|
|
type Entry = (Arc<Pixmap>, Arc<Pixmap>);
|
|
type Cache = (designcraft_color::cms::ColorSettings, HashMap<(usize, Tone, u64), Entry>, usize);
|
|
const BUDGET: usize = 64 << 20;
|
|
static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
|
|
let settings = designcraft_color::cms::active_settings();
|
|
{
|
|
let mut guard = CACHE.lock().unwrap_or_else(PoisonError::into_inner);
|
|
if let Some((previous, map, _)) = guard.as_ref() {
|
|
if *previous == settings {
|
|
if let Some((_, derived)) = map.get(&key) {
|
|
return Some(derived.clone());
|
|
}
|
|
} else {
|
|
*guard = None;
|
|
}
|
|
}
|
|
}
|
|
let premul = |[r, g, b, _]: [u8; 4], a: u8| {
|
|
let m = |v: u8| ((u16::from(v) * u16::from(a) + 127) / 255) as u8;
|
|
PremulRgba8 { r: m(r), g: m(g), b: m(b), a }
|
|
};
|
|
let solid = lut.last().copied().unwrap_or([0, 0, 0, 255]);
|
|
let data: Vec<PremulRgba8> = pm
|
|
.data()
|
|
.iter()
|
|
.map(|p| {
|
|
if p.a == 0 {
|
|
return *p;
|
|
}
|
|
// The image is grey: its red is its grey level (premultiplied).
|
|
let grey = (u16::from(p.r) * 255 / u16::from(p.a)).min(255);
|
|
let coverage = 255 - grey;
|
|
match tone {
|
|
Tone::Gray => premul(lut.get(usize::from(coverage)).copied().unwrap_or(solid), p.a),
|
|
Tone::Bitmap => premul(solid, ((coverage * u16::from(p.a) + 127) / 255) as u8),
|
|
}
|
|
})
|
|
.collect();
|
|
let out = Arc::new(Pixmap::from_parts(data, pm.width(), pm.height()));
|
|
let bytes = (pm.width() as usize).saturating_mul(pm.height() as usize).saturating_mul(8);
|
|
if bytes <= BUDGET {
|
|
let mut guard = CACHE.lock().unwrap_or_else(PoisonError::into_inner);
|
|
let (previous, map, used) = guard.get_or_insert_with(|| (settings.clone(), HashMap::new(), 0));
|
|
if *previous != settings || used.saturating_add(bytes) > BUDGET {
|
|
map.clear();
|
|
*used = 0;
|
|
*previous = settings;
|
|
}
|
|
if map.insert(key, (pm.clone(), out.clone())).is_none() {
|
|
*used += bytes;
|
|
}
|
|
}
|
|
Some(out)
|
|
}
|