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 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
Trevor BrownandClaude Sonnet 5.5 committed 2026-11-12 07:33:54 -05:00
1 parent b54df860de
commit 501df23499
2 files changed
+22 -3

No files matched your search

+2 -2
View File
@@ -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",
+20 -1
View File
@@ -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<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();
@@ -150,6 +162,13 @@ fn image_adjustments_survive_a_fig_export() {
assert!((written(&current, "tint").unwrap() - 1.0).abs() < 1e-6);
assert!((written(&current, "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();