mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #3285 from dotCooCoo/fix/legacy-ai-radial-gradient-stop-order
Legacy .ai export: a radial gradient's color stops are written from ramp point 0 to 100
This commit is contained in:
@@ -204,8 +204,8 @@ const RADIAL_AND_LINEAR: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" widt
|
||||
</svg>"##;
|
||||
|
||||
/// A radial gradient is written with its highlight, `0 0 0 0 Bh` at the center (§3.5.5, §3.5.6).
|
||||
/// #2687's file had none, and the reference app drew the circle in its outer color. The linear one
|
||||
/// has none. Both open again with their type, stops and place.
|
||||
/// #2687's file had none. The linear one has none. Both open again with their type, stops and
|
||||
/// place.
|
||||
#[test]
|
||||
fn a_radial_gradient_is_written_with_its_highlight() {
|
||||
let mut s = Session::new();
|
||||
@@ -244,6 +244,34 @@ fn a_radial_gradient_is_written_with_its_highlight() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #2687's radial gradient lists its stops from white at ramp point 0 to #1b2a6b at 100, as
|
||||
/// §3.5.2's radial definition lists them (10, then 100); the linear one lists them from 100 to 0
|
||||
/// (§1.4.3). The reference app read the radial gradient's two stops listed from 100 both at ramp
|
||||
/// point 100 and drew the circle in #1b2a6b (reported on #2807).
|
||||
#[test]
|
||||
fn a_radial_gradient_lists_its_stops_from_the_first() {
|
||||
let mut s = Session::new();
|
||||
let svg = vectorcraft_format::base64_encode(RADIAL_AND_LINEAR.as_bytes());
|
||||
s.execute("document.open", &json!({"name": "t_radial.svg", "dataBase64": svg})).unwrap();
|
||||
let r = s.execute("file.saveCopy", &json!({"format": "ai", "aiVersion": "7"})).unwrap();
|
||||
let text = String::from_utf8(vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap()).unwrap();
|
||||
// Each `%_Bs` line of a definition as its RGB color (0-255) and ramp point.
|
||||
let listed = |head: &str| -> Vec<([u8; 3], String)> {
|
||||
let def = text.split(head).nth(1).and_then(|t| t.split("BD\n").next()).unwrap_or_else(|| panic!("no {head:?} in\n{text}"));
|
||||
def.lines()
|
||||
.map(|l| {
|
||||
let n: Vec<&str> = l.split_whitespace().collect();
|
||||
let [_, _, _, _, r, g, b, "2", "50", ramp, "%_Bs"] = n.as_slice() else { panic!("{l}") };
|
||||
let c = [r, g, b].map(|v| (v.parse::<f64>().unwrap() * 255.0).round() as u8);
|
||||
(c, (*ramp).to_string())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let (white, navy) = ([255, 255, 255], [27, 42, 107]);
|
||||
assert_eq!(listed("(Gradient) 1 2 Bd\n[\n"), [(white, "0".to_string()), (navy, "100".to_string())], "{text}");
|
||||
assert_eq!(listed("(Gradient 2) 0 2 Bd\n[\n"), [(navy, "100".to_string()), (white, "0".to_string())], "{text}");
|
||||
}
|
||||
|
||||
/// A `w`×`h` PNG of one colour (`rgba`), 72 ppi.
|
||||
fn png(w: u32, h: u32, rgba: [u8; 4]) -> Vec<u8> {
|
||||
let mut out = vec![];
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
//! `%AI5_ArtSize` and an empty `%AI3_TemplateBox` at its centre, which puts the ruler origin at
|
||||
//! its bottom-left corner (§2.2), `%AI5_NumLayers`.
|
||||
//! - Setup (§3): `n Bn`, then each gradient: `%AI5_BeginGradient: (name)`, `(name) type n Bd`,
|
||||
//! `[`, its stops last first (`colorSpec colorStyle midPoint rampPoint %_Bs`), `BD`,
|
||||
//! `%AI5_EndGradient`.
|
||||
//! `[`, its stops (`colorSpec colorStyle midPoint rampPoint %_Bs`), listed from the last for a
|
||||
//! linear gradient and listed from the first for a radial one, `BD`, `%AI5_EndGradient`.
|
||||
//! - Layers (§9): `%AI5_BeginLayer`, `visible preview enabled printing dimmed 0 colorIndex r g b
|
||||
//! Lb`, `(name) Ln`, the art, `LB`, `%AI5_EndLayer--`.
|
||||
//! - Groups `u` … `U`, clipping groups `q` … `Q` with the clipping path painted `h W n` (§5.6),
|
||||
@@ -338,12 +338,20 @@ end
|
||||
const PS_HEADER: &str = "%!PS-Adobe-3.0"; // brand-ok: the PostScript header comment
|
||||
|
||||
/// A gradient definition: `%AI5_BeginGradient: (name)`, `(name) type stops Bd`, `[`, the stops
|
||||
/// last first (`colorSpec colorStyle midPoint rampPoint %_Bs`), `BD`, `%AI5_EndGradient`.
|
||||
/// (`colorSpec colorStyle midPoint rampPoint %_Bs`, each with its midpoint to the next stop), `BD`,
|
||||
/// `%AI5_EndGradient`. A linear gradient's stops are listed from the last to the first (§1.4.3). A
|
||||
/// radial gradient's stops are listed from the first to the last, as in §3.5.2's radial
|
||||
/// definition (10, then 100).
|
||||
fn gradient_def(s: &mut String, w: &Writer, name: &str, g: &Gradient) {
|
||||
let name = latin_string(name);
|
||||
let kind = if g.kind == GradientKind::Radial { 1 } else { 0 };
|
||||
let radial = g.kind == GradientKind::Radial;
|
||||
let kind = if radial { 1 } else { 0 };
|
||||
let _ = writeln!(s, "%AI5_BeginGradient: {name}\n{name} {kind} {} Bd\n[", g.stops.len());
|
||||
for stop in g.stops.iter().rev() {
|
||||
let mut stops: Vec<_> = g.stops.iter().collect();
|
||||
if !radial {
|
||||
stops.reverse();
|
||||
}
|
||||
for stop in stops {
|
||||
let paint = Paint::Solid { color: stop.color, swatch: stop.swatch.clone(), tint: stop.tint };
|
||||
match w.ink(&paint) {
|
||||
Ink::Gray(v) => {
|
||||
|
||||
@@ -462,11 +462,10 @@ fn radials() -> Document {
|
||||
}
|
||||
|
||||
/// Every radial gradient instance has its highlight `Bh` (§3.5.5): `0 0 0 0 Bh` when it is at the
|
||||
/// center (#2687: without it the reference app drew the circle in its outer color), on fills,
|
||||
/// strokes, in groups, compound paths and clipping groups, and an off-center one within the
|
||||
/// gradient's circle. Linear instances have none. VectorCraft reads back one linear and at least
|
||||
/// seven radial gradients, each with its stops, and the circle, the square and the off-center
|
||||
/// square in their place.
|
||||
/// center (§3.5.6), on fills, strokes, in groups, compound paths and clipping groups, and an
|
||||
/// off-center one within the gradient's circle. Linear instances have none. VectorCraft reads
|
||||
/// back one linear and at least seven radial gradients, each with its stops, and the circle, the
|
||||
/// square and the off-center square in their place.
|
||||
#[test]
|
||||
fn radial_gradients_have_their_highlight() {
|
||||
let out = write(&radials());
|
||||
@@ -540,6 +539,66 @@ fn radial_gradients_have_their_highlight() {
|
||||
assert!(near(off.end, 320.0, 240.0) && off.focal.is_some_and(|f| near(f, 300.0, 220.0)), "{off:?}");
|
||||
}
|
||||
|
||||
/// A radial gradient's stops are listed from the first to the last, as §3.5.2's radial definition
|
||||
/// lists them (10, then 100), and a linear gradient's stops are listed from the last to the first
|
||||
/// (§1.4.3). Each stop keeps its color and its midpoint to the next stop. The reference app read a
|
||||
/// radial gradient's stops listed from the last as one color (two stops) or inverted (three
|
||||
/// stops), reported on #2807. Both gradients read back with their stops.
|
||||
#[test]
|
||||
fn radial_gradient_stops_are_listed_from_the_first() {
|
||||
let white = Color::rgb(1.0, 1.0, 1.0);
|
||||
let red = Color::rgb(1.0, 0.0, 0.0);
|
||||
let navy = Color::rgb(27.0 / 255.0, 42.0 / 255.0, 107.0 / 255.0);
|
||||
let stop = |offset: f32, color: Color, midpoint: f32| GradientStop { midpoint, ..GradientStop::new(offset, color) };
|
||||
let stops = vec![stop(0.0, white, 0.3), stop(0.3, red, 0.7), stop(1.0, navy, 0.5)];
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
for (kind, x) in [(GradientKind::Radial, 10.0), (GradientKind::Linear, 110.0)] {
|
||||
let paint = Paint::Gradient(Box::new(GradientPaint::new(Gradient { kind, stops: stops.clone() })));
|
||||
let n = shape(&mut doc, shapes::rectangle(Rect::new(x, 10.0, x + 80.0, 90.0)), paint, Paint::None);
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
}
|
||||
let out = write(&doc);
|
||||
let text = String::from_utf8(out.bytes.clone()).unwrap();
|
||||
// Each `%_Bs` line of a definition: its RGB color (`c m y k r g b 2`), midpoint and ramp point.
|
||||
let listed = |head: &str| -> Vec<([f64; 3], f64, f64)> {
|
||||
let def = text.split(head).nth(1).and_then(|t| t.split("BD\n").next()).unwrap_or_else(|| panic!("no {head:?} in\n{text}"));
|
||||
def.lines()
|
||||
.map(|l| {
|
||||
let n: Vec<f64> = l.trim_end_matches(" %_Bs").split_whitespace().map(|v| v.parse().unwrap()).collect();
|
||||
let [_, _, _, _, r, g, b, style, mid, ramp] = n.as_slice() else { panic!("{l}") };
|
||||
assert_eq!(*style, 2.0, "{l}");
|
||||
([*r, *g, *b], *mid, *ramp)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let rgb = |c: Color| c.to_rgb().map(f64::from);
|
||||
let first_first = [(rgb(white), 30.0, 0.0), (rgb(red), 70.0, 30.0), (rgb(navy), 50.0, 100.0)];
|
||||
let mut last_first = first_first;
|
||||
last_first.reverse();
|
||||
let same = |got: &[([f64; 3], f64, f64)], want: &[([f64; 3], f64, f64)]| {
|
||||
got.len() == want.len()
|
||||
&& got.iter().zip(want).all(|((c, m, r), (wc, wm, wr))| {
|
||||
c.iter().zip(wc).all(|(a, b)| (a - b).abs() < 1e-3) && (m - wm).abs() < 1e-3 && (r - wr).abs() < 1e-3
|
||||
})
|
||||
};
|
||||
let radial = listed("(Gradient) 1 3 Bd\n[\n");
|
||||
assert!(same(&radial, &first_first), "{radial:?}\n{text}");
|
||||
let linear = listed("(Gradient 2) 0 3 Bd\n[\n");
|
||||
assert!(same(&linear, &last_first), "{linear:?}\n{text}");
|
||||
// Read back: both gradients, each stop at its offset with its color and midpoint.
|
||||
let doc = crate::import(&out.bytes).unwrap().document;
|
||||
let read: Vec<_> = children(&doc.layers[0])
|
||||
.iter()
|
||||
.filter_map(|n| if let Paint::Gradient(g) = n.appearance.fill_paint() { Some(g.gradient.clone()) } else { None })
|
||||
.collect();
|
||||
assert_eq!(read.iter().map(|g| g.kind).collect::<Vec<_>>(), [GradientKind::Radial, GradientKind::Linear], "{read:#?}");
|
||||
for g in &read {
|
||||
let got: Vec<_> = g.stops.iter().map(|s| (rgb(s.color), f64::from(s.midpoint) * 100.0, f64::from(s.offset) * 100.0)).collect();
|
||||
assert!(same(&got, &first_first), "{g:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_back_with_layers_groups_and_paths() {
|
||||
let out = write(&sample());
|
||||
|
||||
Reference in New Issue
Block a user