mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-14 03:01:11 +00:00
Merge pull request #2301 from TrevorBrown-dev/feat/fig-export-image-adjustments-2297
M6: .fig export writes image adjustments (#2297)
This commit is contained in:
@@ -142,7 +142,8 @@ message StyleId GUID guid=1
|
||||
message VariableID GUID guid=1
|
||||
message VariableSetID GUID guid=1
|
||||
message ColorStopVar Color color=1, VariableData colorVar=2, float position=3
|
||||
message Paint PaintType type=1, Color color=2, float opacity=3, bool visible=4, BlendMode blendMode=5, ColorStop[] stops=6, Matrix transform=7, Image image=8, ImageScaleMode imageScaleMode=10, float rotation=11, float scale=12, VariableData colorVar=21, ColorStopVar[] stopsVar=23
|
||||
message PaintFilterMessage float tint=1, float shadows=2, float highlights=3, float exposure=5, float temperature=7, float vibrance=8, float contrast=9
|
||||
message Paint PaintType type=1, Color color=2, float opacity=3, bool visible=4, BlendMode blendMode=5, ColorStop[] stops=6, Matrix transform=7, Image image=8, ImageScaleMode imageScaleMode=10, float rotation=11, float scale=12, PaintFilterMessage paintFilter=14, VariableData colorVar=21, ColorStopVar[] stopsVar=23
|
||||
message Effect EffectType type=1, Color color=2, Vector offset=3, float radius=4, bool visible=5, BlendMode blendMode=6, float spread=7, bool showShadowBehindNode=8, BlurOpType blurOpType=19, float startRadius=21, Vector startOffset=20, Vector endOffset=28, NoiseType noiseType=26, Vector noiseSize=22, float density=25, float opacity=27, Color secondaryColor=24, bool clipToShape=23, VariableData radiusVar=9, VariableData colorVar=10, VariableData spreadVar=11, VariableData xVar=12, VariableData yVar=13, VariableData startRadiusVar=45
|
||||
struct FontName string family, string style, string postscript
|
||||
struct Number float value, NumberUnits units
|
||||
|
||||
@@ -76,8 +76,21 @@ impl<'d> Exporter<'d> {
|
||||
if let (ImageScaleMode::Crop, Some(c)) = (img.scale_mode, img.crop) {
|
||||
f.push(("transform", matrix(Affine::new(c))));
|
||||
}
|
||||
if img.filters != Default::default() {
|
||||
self.warn("image adjustments (exposure, contrast, …) were left out");
|
||||
let x = img.filters.sanitized();
|
||||
if !x.is_identity() {
|
||||
// The file's tint is positive towards magenta, ours towards green; `0.0 -` keeps zero positive.
|
||||
f.push((
|
||||
"paintFilter",
|
||||
o(vec![
|
||||
("exposure", fl(f64::from(x.exposure))),
|
||||
("contrast", fl(f64::from(x.contrast))),
|
||||
("vibrance", fl(f64::from(x.saturation))),
|
||||
("temperature", fl(f64::from(x.temperature))),
|
||||
("tint", fl(0.0 - f64::from(x.tint))),
|
||||
("highlights", fl(f64::from(x.highlights))),
|
||||
("shadows", fl(f64::from(x.shadows))),
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
PaintKind::Pattern(_) => {
|
||||
|
||||
@@ -16,8 +16,8 @@ fn image_filter_schema() -> Schema {
|
||||
Schema::parse(&text).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_filters_import_from_current_container_and_skip_ambiguous_data() {
|
||||
/// A file with one image rectangle per filter case (current container, legacy, both, neutral, unknown fields).
|
||||
fn image_filter_fixture() -> Vec<u8> {
|
||||
let filter = |fields: Vec<(&'static str, Kv<'static>)>| o(fields);
|
||||
let image_paint = |id: u64, name: &'static str, mut extra: Vec<(&'static str, Kv<'static>)>| {
|
||||
let mut paint = vec![("type", e("IMAGE")), ("image", o(vec![("hash", Kv::Bytes(HASH.to_vec()))]))];
|
||||
@@ -54,7 +54,12 @@ fn image_filters_import_from_current_container_and_skip_ambiguous_data() {
|
||||
vec![("paintFilter", filter(vec![("brightness", f(0.4)), ("detail", f(-0.2)), ("vignette", f(0.3)), ("mystery", f(0.1))]))],
|
||||
),
|
||||
];
|
||||
let bytes = zip_file(&canvas(&image_filter_schema(), &o(vec![("nodeChanges", arr(nodes))])));
|
||||
zip_file(&canvas(&image_filter_schema(), &o(vec![("nodeChanges", arr(nodes))])))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_filters_import_from_current_container_and_skip_ambiguous_data() {
|
||||
let bytes = image_filter_fixture();
|
||||
let (doc, warnings) = import(&bytes, "ImageFilters.fig").unwrap();
|
||||
let get = |name: &str| {
|
||||
let node = doc.nodes().find(|node| node.name == name).unwrap();
|
||||
@@ -102,3 +107,76 @@ fn warnings_naming_unknown_filter_fields_are_capped() {
|
||||
let named = warnings.iter().filter(|w| w.contains("unsupported image filter")).count();
|
||||
assert!((100..=crate::rest::MAX_WARNINGS).contains(&named), "{named} of {} warnings", warnings.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_adjustments_survive_a_fig_export() {
|
||||
let (mut doc, _) = import(&image_filter_fixture(), "ImageFilters.fig").unwrap();
|
||||
let filters_of = |doc: &figcraft_doc::Document, name: &str| {
|
||||
let node = doc.nodes().find(|node| node.name == name).unwrap();
|
||||
let figcraft_doc::color::PaintKind::Image(image) = &node.fills[0].kind else { panic!("{name} is not an image fill") };
|
||||
image.filters
|
||||
};
|
||||
// Values a document can hold and the file can't: out of range, and not a number.
|
||||
let ids: Vec<figcraft_doc::NodeId> = doc.ids().collect();
|
||||
for id in ids {
|
||||
let Some(node) = doc.node_mut(id) else { continue };
|
||||
let name = node.name.clone();
|
||||
let Some(figcraft_doc::color::PaintKind::Image(image)) = node.fills.first_mut().map(|p| &mut p.kind) else { continue };
|
||||
match name.as_str() {
|
||||
"Legacy" => image.filters.exposure = 5.0,
|
||||
"Explicit neutral" => image.filters.contrast = f32::NAN,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let (bytes, warnings) = export::export(&doc, None).unwrap();
|
||||
assert!(!warnings.iter().any(|w| w.contains("image adjustments")), "{warnings:?}");
|
||||
let (back, _) = import(&bytes, "Back.fig").unwrap();
|
||||
for name in ["Current", "Negative tint", "Neutral"] {
|
||||
let (a, b) = (filters_of(&doc, name), filters_of(&back, name));
|
||||
for (x, y) in [
|
||||
(a.exposure, b.exposure),
|
||||
(a.contrast, b.contrast),
|
||||
(a.saturation, b.saturation),
|
||||
(a.temperature, b.temperature),
|
||||
(a.tint, b.tint),
|
||||
(a.highlights, b.highlights),
|
||||
(a.shadows, b.shadows),
|
||||
] {
|
||||
assert!((x - y).abs() < 1e-6, "{name}: {a:?} came back as {b:?}");
|
||||
}
|
||||
}
|
||||
assert!(filters_of(&back, "Neutral").is_identity());
|
||||
assert!(!filters_of(&back, "Current").is_identity());
|
||||
|
||||
// What is written: the file's names and signs.
|
||||
let archive = Archive::open(&bytes).unwrap();
|
||||
let (schema, message) = decode_canvas(&archive.canvas).unwrap();
|
||||
let msg = schema.decode_message(schema.find("Message").unwrap(), &message).unwrap();
|
||||
let fill_of = |name: &str| {
|
||||
let nc = msg.get("nodeChanges").unwrap().list().iter().find(|nc| nc.get("name").and_then(Kv::str) == Some(name)).unwrap();
|
||||
nc.get("fillPaints").unwrap().list()[0].clone()
|
||||
};
|
||||
let current = fill_of("Current");
|
||||
let written = |paint: &Kv, field: &str| paint.get("paintFilter").and_then(|f| f.get(field)).and_then(Kv::f64);
|
||||
assert!((written(¤t, "vibrance").unwrap() + 0.4).abs() < 1e-6);
|
||||
assert!((written(¤t, "tint").unwrap() - 1.0).abs() < 1e-6);
|
||||
assert!((written(¤t, "shadows").unwrap() - 0.7).abs() < 1e-6);
|
||||
assert!(fill_of("Neutral").get("paintFilter").is_none());
|
||||
assert!((written(&fill_of("Legacy"), "exposure").unwrap() - 1.0).abs() < 1e-6, "written within the file's range");
|
||||
assert!(fill_of("Explicit neutral").get("paintFilter").is_none(), "not a number is no adjustment");
|
||||
let partly = fill_of("Negative tint");
|
||||
for field in ["exposure", "contrast", "vibrance", "temperature", "tint", "highlights", "shadows"] {
|
||||
assert!(written(&partly, field).is_some(), "{field} is written");
|
||||
}
|
||||
assert!((written(&partly, "tint").unwrap() + 1.0).abs() < 1e-6);
|
||||
|
||||
// The numbers the format uses.
|
||||
let s = Schema::parse(export::SCHEMA).unwrap();
|
||||
let def = |n: &str| s.defs.iter().find(|d| d.name == n).unwrap();
|
||||
let id = |d: &str, f: &str| def(d).fields.iter().find(|x| x.name == f).unwrap().value;
|
||||
assert_eq!(id("Paint", "paintFilter"), 14);
|
||||
assert_eq!(
|
||||
["tint", "shadows", "highlights", "exposure", "temperature", "vibrance", "contrast"].map(|f| id("PaintFilterMessage", f)),
|
||||
[1, 2, 3, 5, 7, 8, 9]
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user