mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-31 03:32:08 +01:00
124 lines
5.1 KiB
Rust
124 lines
5.1 KiB
Rust
//! Downsampled proxy documents: every pixel surface scaled down by an integer factor `k`, so a
|
|
//! composite of the proxy approximates the document's composite at 1/k resolution for ~1/k² of
|
|
//! the work. Used for interactive previews and for thumbnails of large documents (navigator,
|
|
//! channel thumbnails, histograms, file previews), where a full-resolution composite of a 200 MP
|
|
//! document would cost seconds and gigabytes.
|
|
|
|
use photocraft_doc::{Document, Layer, LayerContent, Size};
|
|
use photocraft_geom::Rect;
|
|
use photocraft_raster::Surface;
|
|
|
|
/// Nearest-neighbour downsample of a surface by integer factor `k` (document coordinates / k).
|
|
pub fn downsample(s: &Surface, k: u32) -> Surface {
|
|
let mut out = Surface::with_default(s.format(), &s.default_pixel());
|
|
let b = s.content_bounds();
|
|
if b.is_empty() || k <= 1 {
|
|
return if k <= 1 { s.clone() } else { out };
|
|
}
|
|
// Surfaces span at most ±2^30 px; larger factors reduce everything to one pixel anyway.
|
|
let k = k.min(1 << 20) as i32;
|
|
let bpp = s.format().bytes_per_pixel();
|
|
let (x0, x1) = (b.x0.div_euclid(k), b.x1.div_euclid(k) + i32::from(b.x1.rem_euclid(k) != 0));
|
|
let (y0, y1) = (b.y0.div_euclid(k), b.y1.div_euclid(k) + i32::from(b.y1.rem_euclid(k) != 0));
|
|
let w = (x1 - x0).max(0) as usize;
|
|
let mut row = vec![0u8; w * bpp];
|
|
for oy in y0..y1 {
|
|
let src = s.to_interleaved(Rect::new(x0 * k, oy * k, x1 * k, oy * k + 1));
|
|
for (i, px) in row.chunks_exact_mut(bpp).enumerate() {
|
|
px.copy_from_slice(&src[i * k as usize * bpp..(i * k as usize + 1) * bpp]);
|
|
}
|
|
out.write_interleaved(Rect::new(x0, oy, x1, oy + 1), &row);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn shrink_layer(l: &mut Layer, k: u32) {
|
|
if let Some(m) = &mut l.mask {
|
|
m.surface = downsample(&m.surface, k);
|
|
}
|
|
if let Some(fc) = &mut l.fill_cache {
|
|
fc.surface = downsample(&fc.surface, k);
|
|
}
|
|
match &mut l.content {
|
|
LayerContent::Raster(s) => *s = downsample(s, k),
|
|
LayerContent::Group(g) => {
|
|
if let Some(ab) = &mut g.artboard {
|
|
let divisor = i64::from(k);
|
|
// Pixel x samples source x*k: both half-open edges round up, including negatives.
|
|
let edge = |v: i32| {
|
|
let v = i64::from(v);
|
|
(v.div_euclid(divisor) + i64::from(v.rem_euclid(divisor) != 0)) as i32
|
|
};
|
|
ab.rect = Rect::new(edge(ab.rect.x0), edge(ab.rect.y0), edge(ab.rect.x1), edge(ab.rect.y1));
|
|
}
|
|
for c in &mut g.children {
|
|
shrink_layer(c, k);
|
|
}
|
|
}
|
|
LayerContent::Text(t) => t.cache = t.cache.as_ref().map(|s| downsample(s, k)),
|
|
LayerContent::Shape(sh) => sh.cache = sh.cache.as_ref().map(|s| downsample(s, k)),
|
|
LayerContent::Smart(so) => so.cache = so.cache.as_ref().map(|s| downsample(s, k)),
|
|
LayerContent::Adjustment(_) | LayerContent::Fill(_) => {}
|
|
}
|
|
}
|
|
|
|
/// A copy of `doc` scaled down by `k` (same layer ids, so adjustments can be swapped in).
|
|
pub fn proxy_document(doc: &Document, k: u32) -> Document {
|
|
let mut p = doc.clone();
|
|
if k <= 1 {
|
|
return p;
|
|
}
|
|
p.size = Size::new(doc.size.width.div_ceil(k), doc.size.height.div_ceil(k));
|
|
for l in &mut p.layers {
|
|
shrink_layer(l, k);
|
|
}
|
|
// Spot channels are printed under Multichannel composites.
|
|
for c in &mut p.channels {
|
|
c.surface = downsample(&c.surface, k);
|
|
}
|
|
p.selection = None;
|
|
p
|
|
}
|
|
|
|
/// Whether a proxy composite is a faithful reduction of `doc`: layer effects (whose sizes are in
|
|
/// document pixels) and vector masks (paths in document coordinates) would not scale with it.
|
|
pub fn proxy_faithful(doc: &Document) -> bool {
|
|
doc.walk().iter().all(|(_, _, l)| !crate::effects::has_effects(l) && l.vector_mask.as_ref().is_none_or(|v| !v.enabled))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use photocraft_color::{Color, ColorMode, PixelFormat, SampleType};
|
|
|
|
#[test]
|
|
fn downsample_picks_every_kth_pixel() {
|
|
let mut s = Surface::new(PixelFormat::RGBA8);
|
|
s.fill_rect(Rect::new(0, 0, 8, 8), &[1.0, 0.0, 0.0, 1.0]);
|
|
s.write_pixel(4, 4, &[0.0, 0.0, 1.0, 1.0]);
|
|
let d = downsample(&s, 4);
|
|
assert_eq!(d.pixel(0, 0), vec![1.0, 0.0, 0.0, 1.0]);
|
|
assert_eq!(d.pixel(1, 1), vec![0.0, 0.0, 1.0, 1.0]);
|
|
assert_eq!(d.pixel(2, 2)[3], 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn downsample_handles_negative_coordinates() {
|
|
let mut s = Surface::new(PixelFormat::RGBA8);
|
|
s.fill_rect(Rect::new(-8, -8, 0, 0), &[0.0, 1.0, 0.0, 1.0]);
|
|
let d = downsample(&s, 4);
|
|
assert_eq!(d.pixel(-1, -1), vec![0.0, 1.0, 0.0, 1.0]);
|
|
assert_eq!(d.pixel(0, 0)[3], 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn proxy_keeps_structure_and_ids() {
|
|
let doc = Document::with_background("d", Size::new(4000, 3000), ColorMode::Rgb, SampleType::U8, Color::WHITE);
|
|
let p = proxy_document(&doc, 4);
|
|
assert_eq!((p.size.width, p.size.height), (1000, 750));
|
|
assert_eq!(p.layers[0].id, doc.layers[0].id);
|
|
assert_eq!(p.layers[0].surface().unwrap().pixel(999, 749), vec![1.0; 4]);
|
|
assert!(proxy_faithful(&doc));
|
|
}
|
|
}
|