From 501df234997fabf2340944cb6e3ca03c25e3b446 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Thu, 12 Nov 2026 07:33:54 -0500 Subject: [PATCH] M6: the .fig export writes sanitized image adjustments (#2297) A document can hold image adjustments outside -1..=1 or not a number (a saved file, or a fills property); the renderer and the importer clamp them, but the export wrote the raw values for other readers. The export now writes the sanitized values, and a paint whose adjustments are all neutral after sanitizing writes no `paintFilter`. The round-trip test pins both cases and that all seven fields are written on a partly neutral paint. Co-Authored-By: Claude Sonnet 5.5 Model-Effort: Opus 5.5 drove; Fable 5.2 briefed; Sonnet 5.5 wrote at medium --- crates/format/src/fig/export/paint.rs | 4 ++-- crates/format/src/fig/image_filter_tests.rs | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/format/src/fig/export/paint.rs b/crates/format/src/fig/export/paint.rs index 4d87871f6..bec9a72c8 100644 --- a/crates/format/src/fig/export/paint.rs +++ b/crates/format/src/fig/export/paint.rs @@ -76,8 +76,8 @@ 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.is_identity() { - let x = &img.filters; + 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", diff --git a/crates/format/src/fig/image_filter_tests.rs b/crates/format/src/fig/image_filter_tests.rs index d1297293f..8923a4d7d 100644 --- a/crates/format/src/fig/image_filter_tests.rs +++ b/crates/format/src/fig/image_filter_tests.rs @@ -110,12 +110,24 @@ fn warnings_naming_unknown_filter_fields_are_capped() { #[test] fn image_adjustments_survive_a_fig_export() { - let (doc, _) = import(&image_filter_fixture(), "ImageFilters.fig").unwrap(); + 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 = 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(); @@ -150,6 +162,13 @@ fn image_adjustments_survive_a_fig_export() { 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();