mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-28 18:26:55 +01:00
match smart object re-renders to the document's mode and depth
This commit is contained in:
1 parent
6d3443c098
commit
cac01f79b8
3 files changed
+105
-22
No files matched your search
@@ -698,30 +698,35 @@ fn convert_depth(s: &mut Session, depth: SampleType) -> Result<Value> {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
s.edit("Bit Depth", |doc, _| {
|
||||
// 32-bit documents hold linear light (as Photoshop's do: grey 128 becomes 0.2159, measured
|
||||
// in Photoshop 27.11), so the colour samples cross the document's tone curve when the
|
||||
// depth crosses 32 bits: decoded after widening, encoded before narrowing (so no linear
|
||||
// value is quantised to 8 or 16 bits). Masks, alpha and channels are not colour.
|
||||
let crosses = (current == SampleType::F32) != (depth == SampleType::F32) && matches!(doc.mode, ColorMode::Rgb | ColorMode::Grayscale);
|
||||
let to_linear = depth == SampleType::F32;
|
||||
let transfer = photocraft_compose::adjust::Transfer::for_document(doc.mode, if to_linear { current } else { depth });
|
||||
if crosses && !to_linear {
|
||||
for_each_surface(&mut doc.layers, false, &mut |surf, _| surf.map_color(|v| transfer.encode(v).clamp(0.0, 1.0)));
|
||||
}
|
||||
convert_layers_depth(&mut doc.layers, depth);
|
||||
if crosses && to_linear {
|
||||
for_each_surface(&mut doc.layers, false, &mut |surf, _| surf.map_color(|v| transfer.decode(v)));
|
||||
}
|
||||
for ch in doc.channels.iter_mut().chain(doc.quick_mask.as_mut()) {
|
||||
let f = ch.surface.format().with_sample(depth);
|
||||
ch.surface = ch.surface.convert(f);
|
||||
}
|
||||
doc.depth = depth;
|
||||
change_depth(doc, depth);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
pub(crate) fn change_depth(doc: &mut Document, depth: SampleType) {
|
||||
let current = doc.depth;
|
||||
// 32-bit documents hold linear light (as Photoshop's do: grey 128 becomes 0.2159, measured
|
||||
// in Photoshop 27.11), so the colour samples cross the document's tone curve when the
|
||||
// depth crosses 32 bits: decoded after widening, encoded before narrowing (so no linear
|
||||
// value is quantised to 8 or 16 bits). Masks, alpha and channels are not colour.
|
||||
let crosses = (current == SampleType::F32) != (depth == SampleType::F32) && matches!(doc.mode, ColorMode::Rgb | ColorMode::Grayscale);
|
||||
let to_linear = depth == SampleType::F32;
|
||||
let transfer = photocraft_compose::adjust::Transfer::for_document(doc.mode, if to_linear { current } else { depth });
|
||||
if crosses && !to_linear {
|
||||
for_each_surface(&mut doc.layers, false, &mut |surf, _| surf.map_color(|v| transfer.encode(v).clamp(0.0, 1.0)));
|
||||
}
|
||||
convert_layers_depth(&mut doc.layers, depth);
|
||||
if crosses && to_linear {
|
||||
for_each_surface(&mut doc.layers, false, &mut |surf, _| surf.map_color(|v| transfer.decode(v)));
|
||||
}
|
||||
for ch in doc.channels.iter_mut().chain(doc.quick_mask.as_mut()) {
|
||||
let f = ch.surface.format().with_sample(depth);
|
||||
ch.surface = ch.surface.convert(f);
|
||||
}
|
||||
doc.depth = depth;
|
||||
}
|
||||
|
||||
/// Image → Duplicate.
|
||||
fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d = s.active().ok_or(EngineError::NoDocument)?;
|
||||
|
||||
@@ -27,7 +27,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use photocraft_algo::resample::translate_surface;
|
||||
use photocraft_algo::transform::Homography;
|
||||
use photocraft_color::{BlendMode, PixelFormat};
|
||||
use photocraft_color::{BlendMode, ColorMode, PixelFormat, SampleType};
|
||||
use photocraft_doc::{DocId, Document, Layer, LayerContent, LayerId, LayerMask, Metadata, SmartContentsId, SmartFilter, SmartObject, SmartSource};
|
||||
use photocraft_geom::{Affine, Rect, Size};
|
||||
use photocraft_raster::Surface;
|
||||
@@ -189,6 +189,30 @@ fn buffer_to_surface(buf: &photocraft_compose::Buffer, fmt: PixelFormat) -> Surf
|
||||
s
|
||||
}
|
||||
|
||||
fn source_surface(src: &Document, buf: &photocraft_compose::Buffer, fmt: PixelFormat) -> Result<Surface> {
|
||||
let gray = fmt.mode == ColorMode::Grayscale && src.pixel_format().mode != ColorMode::Grayscale;
|
||||
let linear = fmt.sample == SampleType::F32 && src.depth != SampleType::F32;
|
||||
if !gray && !linear {
|
||||
return Ok(buffer_to_surface(buf, fmt));
|
||||
}
|
||||
let mut doc = Document::new(src.name.clone(), src.size, src.mode, src.depth);
|
||||
doc.icc_profile = src.icc_profile.clone();
|
||||
doc.layers = vec![Layer::new("source", LayerContent::Raster(buffer_to_surface(buf, src.pixel_format())))];
|
||||
if gray {
|
||||
let gray_profile = crate::color_cmds::working_profile(ColorMode::Grayscale);
|
||||
crate::color_cmds::convert_document(&mut doc, &gray_profile, photocraft_cms::Intent::RelativeColorimetric, true)?;
|
||||
}
|
||||
if linear {
|
||||
crate::image_cmds::change_depth(&mut doc, SampleType::F32);
|
||||
}
|
||||
let surface = doc.layers.pop().and_then(|l| match l.content {
|
||||
LayerContent::Raster(s) => Some(s),
|
||||
_ => None,
|
||||
});
|
||||
let surface = surface.ok_or_else(|| other("the smart object's source could not be converted"))?;
|
||||
Ok(if surface.format() == fmt { surface } else { surface.convert(fmt) })
|
||||
}
|
||||
|
||||
/// An SVG source rendered at `scale` times its own size (cached per scale), so a vector smart
|
||||
/// object stays sharp however it is placed.
|
||||
pub fn svg_source_image(bytes: &[u8], fmt: PixelFormat, scale: f64) -> Result<SourceImage> {
|
||||
@@ -224,7 +248,7 @@ pub fn source_image(file_name: &str, bytes: &[u8], fmt: PixelFormat) -> Result<S
|
||||
}
|
||||
let doc = decode_source(file_name, bytes)?;
|
||||
let buf = photocraft_compose::flatten(&doc);
|
||||
let img = SourceImage { surface: Arc::new(buffer_to_surface(&buf, fmt)), bounds: doc.bounds() };
|
||||
let img = SourceImage { surface: Arc::new(source_surface(&doc, &buf, fmt)?), bounds: doc.bounds() };
|
||||
cache_put(key, img.clone());
|
||||
Ok(img)
|
||||
}
|
||||
@@ -271,7 +295,7 @@ pub fn stack_image(file_name: &str, bytes: &[u8], fmt: PixelFormat, mode: photoc
|
||||
photocraft_doc::StackMode::Variance => Stat::Variance,
|
||||
};
|
||||
let buf = photocraft_compose::Buffer { rect: bounds, px: photocraft_algo::stack::combine(&frames, stat) };
|
||||
let img = SourceImage { surface: Arc::new(buffer_to_surface(&buf, fmt)), bounds };
|
||||
let img = SourceImage { surface: Arc::new(source_surface(&doc, &buf, fmt)?), bounds };
|
||||
cache_put(key, img.clone());
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
use photocraft_doc::{LayerContent, LayerId};
|
||||
use photocraft_engine::{Session, smart_cmds};
|
||||
use photocraft_raster::Surface;
|
||||
use serde_json::json;
|
||||
|
||||
fn mean_difference(a: &Surface, b: &Surface) -> f64 {
|
||||
let r = a.content_bounds().intersect(&b.content_bounds());
|
||||
let (mut sum, mut n) = (0f64, 0u32);
|
||||
for y in r.y0..r.y1 {
|
||||
for x in r.x0..r.x1 {
|
||||
let (p, q) = (a.rgba(x, y), b.rgba(x, y));
|
||||
for c in 0..3 {
|
||||
sum += f64::from((p[c] - q[c]).abs());
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
sum / f64::from(n.max(1)) * 255.0
|
||||
}
|
||||
|
||||
fn converted(cmd: &str) -> (Session, LayerId, LayerId) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width": 64, "height": 48})).unwrap();
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
s.execute("paint.stroke", json!({"points": [[5, 10], [60, 40]], "size": 20, "hardness": 0.3, "color": "#e04a20"})).unwrap();
|
||||
s.execute("paint.stroke", json!({"points": [[5, 40], [60, 5]], "size": 14, "hardness": 0.6, "color": "#2a7fd0"})).unwrap();
|
||||
let pixel = s.active().unwrap().active_layer.unwrap();
|
||||
s.execute("layer.duplicate", json!({})).unwrap();
|
||||
s.execute("layer.smartObjects.convertToSmartObject", json!({})).unwrap();
|
||||
let smart = s.active().unwrap().active_layer.unwrap();
|
||||
s.execute(cmd, json!({})).unwrap();
|
||||
(s, pixel, smart)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_objects_re_render_with_the_tones_of_the_converted_document() {
|
||||
for cmd in ["image.mode.bits32", "image.mode.grayscale"] {
|
||||
let (s, pixel, smart) = converted(cmd);
|
||||
let st = s.active().unwrap();
|
||||
let layer = st.doc.layer(smart).unwrap().clone();
|
||||
let mut rerendered = layer.clone();
|
||||
smart_cmds::refresh_layer(&st.doc, &mut rerendered).unwrap();
|
||||
let (LayerContent::Smart(stored), LayerContent::Smart(fresh)) = (&layer.content, &rerendered.content) else { panic!("not a smart object") };
|
||||
let (stored, fresh) = (stored.cache.as_ref().unwrap(), fresh.cache.as_ref().unwrap());
|
||||
let reference = st.doc.layer(pixel).unwrap().surface().unwrap();
|
||||
let drift = mean_difference(stored, fresh);
|
||||
assert!(drift < 1.0, "{cmd}: a re-render drifts {drift:.2}/255 from the converted rendering");
|
||||
let (off, baseline) = (mean_difference(fresh, reference), mean_difference(stored, reference));
|
||||
assert!(
|
||||
off <= baseline + 0.5,
|
||||
"{cmd}: the re-render is {off:.2}/255 from a pixel layer converted the same way, the converted rendering {baseline:.2}/255"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user