Fix #4420: import attached recolor adjustment layers as color overlays

In Affinity Designer, Recolor Adjustment layers (RcRA) can be attached to
vector groups/shapes via AdCh. Lightness 1.0 recolors target elements to
white. Import visible RcRA layers as ColorOverlay effects and apply them
during node appearance evaluation.
This commit is contained in:
Taha-Mahmoodi
2026-10-30 00:15:58 +07:00
committed by bflatastic
parent 42884e4ee7
commit 887328db03
5 changed files with 65 additions and 3 deletions
+28
View File
@@ -441,8 +441,34 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
_ => self.warn("layer effects of other kinds (left out)"),
}
}
for a in s.objs(id, b"AdCh") {
if let Some(effect) = self.attached_recolor(a) {
out.push(effect);
}
}
out
}
fn attached_recolor(&mut self, a: ObjId) -> Option<Effect> {
let s = self.s;
if !s.is(a, b"RcRA") {
return None;
}
if s.bool(a, b"Visi") == Some(false) {
return None;
}
let adjp = s.obj(a, b"AdjP")?;
if !s.is(adjp, b"RecP") {
return None;
}
let h = s.f64(adjp, b"RecH").unwrap_or(0.0);
let sat = s.f64(adjp, b"RecS").unwrap_or(1.0);
let l = s.f64(adjp, b"RecL").unwrap_or(0.5);
let (r, g, b) = crate::paint::hsl(h, sat, l);
let color = crate::paint::Color::Rgb { r, g, b, a: 1.0 };
let opacity = s.f64(a, b"Opac").filter(|v| v.is_finite()).unwrap_or(1.0).clamp(0.0, 1.0);
let blend = self.effect_blend(a);
Some(Effect::ColorOverlay { color, opacity, blend })
}
/// A layer effect's blend mode (`BlnM`); unknown modes are Normal (warned).
fn effect_blend(&mut self, e: ObjId) -> Blend {
@@ -479,6 +505,8 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
Some(_) => self.warn("several pixel masks on one layer (only the first is used)"),
None => {}
}
} else if class == Tag::of(b"RcRA") {
// Handled in `effects` as a colour overlay.
} else {
self.warn(format!("{} layers attached to other layers (imported without them)", describe(class)));
}
+15 -1
View File
@@ -194,7 +194,7 @@ fn color_within(r: &mut Reader, id: ObjId, depth: u32) -> Option<Color> {
})
}
fn hsl(h: f64, s: f64, l: f64) -> (f64, f64, f64) {
pub(crate) fn hsl(h: f64, s: f64, l: f64) -> (f64, f64, f64) {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let hp = (h.rem_euclid(1.0)) * 6.0;
let x = c * (1.0 - (hp % 2.0 - 1.0).abs());
@@ -355,3 +355,17 @@ pub(crate) fn stroke(r: &mut Reader, pen: ObjId, ldsc: ObjId, local: Affine, beh
}
Some(Stroke { paint, width, cap, join, miter_limit, align, dash, behind })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hsl_lightness_1_is_white_and_0_is_black() {
assert_eq!(hsl(0.0, 1.0, 1.0), (1.0, 1.0, 1.0));
assert_eq!(hsl(0.5, 1.0, 1.0), (1.0, 1.0, 1.0));
assert_eq!(hsl(0.0, 1.0, 0.0), (0.0, 0.0, 0.0));
let (r, g, b) = hsl(0.0, 1.0, 0.5);
assert!((r - 1.0).abs() < 1e-6 && g.abs() < 1e-6 && b.abs() < 1e-6);
}
}
@@ -702,3 +702,21 @@ fn objects_beyond_the_document_range_are_left_out() {
assert!(names.iter().any(|n| n == "Magenta box") && !names.iter().any(|n| n == "Far box"), "{names:?}");
assert!(loaded.warnings.iter().any(|w| w.contains("beyond the document range")), "{:?}", loaded.warnings);
}
#[test]
fn test_af_g_symbol_fill_is_white() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test.af");
let Ok(bytes) = std::fs::read(&path) else {
return;
};
let loaded = vectorcraft_engine::cmd::fileio::load("test.af", &bytes).unwrap();
assert!(!loaded.preview_only, "{:?}", loaded.warnings);
let d = &loaded.doc;
// The visible "g41" group holds the "g" symbol (path43).
let g41 = all(d).into_iter().find(|n| n.visible && n.name.as_deref() == Some("g41")).expect("visible g41 group must be present");
let path43 = g41.children().and_then(|c| c.iter().find(|n| n.name.as_deref() == Some("path43"))).expect("path43 inside g41 must be present");
let Some(AppearanceItem::Fill(fill)) = path43.appearance.items.first() else {
panic!("path43 has no fill: {:?}", path43.appearance);
};
assert_eq!(fill.paint, Paint::solid(Color::rgb(1.0, 1.0, 1.0)), "The 'g' symbol must have a white fill in test.af, got {:?}", fill.paint);
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long