mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 08:56:08 +00:00
Merge pull request #6921 from dotCooCoo/fix/ai-save-cmyk-equivalents-stable
.ai save: RGB and named colors keep their CMYK equivalents when a reopened file is saved
This commit is contained in:
@@ -294,6 +294,134 @@ fn writing_again_gives_the_same_bytes() {
|
||||
assert!(first.warnings.is_empty(), "{:?}", first.warnings);
|
||||
}
|
||||
|
||||
/// The CMYK equivalent written beside RGB or Lab values is the one of the values as written (to four
|
||||
/// decimals, Lab clamped to the range a reader reads), so the reopened file is written with the same
|
||||
/// numbers: a new document's swatches, and colors of many decimals in fills, gradient stops, global
|
||||
/// process swatches (RGB and Lab), an RGB spot swatch (in a fill and in a stop), Lab spot swatches
|
||||
/// and a Lab spot swatch out of range.
|
||||
#[test]
|
||||
fn cmyk_equivalents_are_written_again_unchanged() {
|
||||
let mut doc = Document::new(300.0, 200.0);
|
||||
let v = |i: u16, s: f32| (f32::from(i) * s + 0.123_456_7).fract();
|
||||
let rgb = |i: u16| Color::rgb(v(i, 0.137_911_3), v(i, 0.271_828_2), v(i, 0.314_159_3));
|
||||
let lab = |i: u16| Color::lab(v(i, 0.137_911_3) * 100.0, v(i, 0.271_828_2) * 200.0 - 100.0, v(i, 0.314_159_3) * 200.0 - 100.0);
|
||||
let glaze = rgb(102);
|
||||
let swatch = |name: String, c: Color, spot: bool| Swatch { name, paint: solid(c), global: true, spot };
|
||||
let mut named: Vec<(String, Color, bool)> =
|
||||
vec![("Glaze".into(), glaze, true), ("Out".into(), Color::lab(104.567_89, -140.123_46, 131.987_65), true)];
|
||||
for i in 0..8u16 {
|
||||
named.push((format!("Leaf {i}"), rgb(i + 110), false));
|
||||
named.push((format!("Fern {i}"), lab(i + 120), false));
|
||||
named.push((format!("Moss {i}"), lab(i + 130), true));
|
||||
}
|
||||
let mut swatches: Vec<Swatch> = doc.swatches.iter().cloned().collect();
|
||||
swatches.extend(named.iter().map(|(name, c, spot)| swatch(name.clone(), *c, *spot)));
|
||||
doc.swatches = swatches.into();
|
||||
let layer = doc.layers[0].id;
|
||||
for i in 0..24u16 {
|
||||
let x = f64::from(i % 8) * 30.0 + 10.0;
|
||||
let y = f64::from(i / 8) * 30.0 + 10.0;
|
||||
let n = square(&mut doc, Rect::new(x, y, x + 20.0, y + 20.0), rgb(i));
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
}
|
||||
let mut stops: Vec<GradientStop> = (0..5u16).map(|i| GradientStop::new(f32::from(i) / 5.0, rgb(i + 50))).collect();
|
||||
let out = named[1].1;
|
||||
stops.push(GradientStop { swatch: Some("Out".into()), tint: 0.5, ..GradientStop::new(0.9, out.tinted(0.5)) });
|
||||
stops.push(GradientStop { swatch: Some("Glaze".into()), tint: 0.7, ..GradientStop::new(1.0, glaze.tinted(0.7)) });
|
||||
let mut fills = vec![gradient_paint(Gradient::new(GradientKind::Linear, stops), None, None)];
|
||||
fills.extend(named.iter().map(|(name, c, spot)| linked(name, *c, if *spot { 1.0 } else { 0.6 })));
|
||||
for (i, fill) in fills.into_iter().enumerate() {
|
||||
let x = (i % 9) as f64 * 30.0 + 10.0;
|
||||
let y = (i / 9) as f64 * 30.0 + 110.0;
|
||||
let n = painted(&mut doc, shapes::rectangle(Rect::new(x, y, x + 20.0, y + 20.0)), vec![AppearanceItem::Fill(FillLayer::new(fill))]);
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
}
|
||||
let first = program(&doc);
|
||||
let again = String::from_utf8(export_ai_editing(&read_back(&doc), &opts()).unwrap().program).unwrap().replace("\r\n", "\n");
|
||||
same_lines(&again.lines().map(String::from).collect::<Vec<_>>(), &first.lines().map(String::from).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
/// With a working RGB space other than sRGB, an RGB fill, an RGB gradient stop and an RGB spot
|
||||
/// color in a stop are written with their working RGB values, the values the reader reads back, so
|
||||
/// the reopened file is written with the same RGB values and CMYK equivalents.
|
||||
#[test]
|
||||
fn rgb_values_are_written_again_unchanged_in_a_wide_gamut_working_space() {
|
||||
const CHILD: &str = "VECTORCRAFT_TEST_AI_EDITING_WIDE_GAMUT";
|
||||
if std::env::var_os(CHILD).is_none() {
|
||||
// Color settings are process-wide: exercise this profile in a separate test process.
|
||||
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args(["--exact", "ai_editing::tests::rgb_values_are_written_again_unchanged_in_a_wide_gamut_working_space", "--nocapture"])
|
||||
.env(CHILD, "1")
|
||||
.output()
|
||||
.unwrap();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"),
|
||||
"isolated color profile test failed:\n{stdout}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
return;
|
||||
}
|
||||
use vectorcraft_color::cms;
|
||||
cms::set_active(&cms::ColorSettings { rgb: cms::WIDE_GAMUT_RGB.into(), ..Default::default() }).unwrap();
|
||||
assert!(!cms::active().rgb_is_srgb());
|
||||
let mut doc = Document::new(300.0, 200.0);
|
||||
let glaze = Color::rgb(0.812_345_6, 0.234_567_8, 0.456_789_1);
|
||||
let mut swatches: Vec<Swatch> = doc.swatches.iter().cloned().collect();
|
||||
swatches.push(Swatch { name: "Glaze".into(), paint: solid(glaze), global: true, spot: true });
|
||||
doc.swatches = swatches.into();
|
||||
let layer = doc.layers[0].id;
|
||||
let n = square(&mut doc, Rect::new(10.0, 10.0, 30.0, 30.0), Color::rgb(0.8, 0.4, 0.2));
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
let stops = vec![
|
||||
GradientStop::new(0.0, Color::rgb(0.1, 0.7, 0.3)),
|
||||
GradientStop { swatch: Some("Glaze".into()), tint: 0.7, ..GradientStop::new(1.0, glaze.tinted(0.7)) },
|
||||
];
|
||||
let fill = gradient_paint(Gradient::new(GradientKind::Linear, stops), None, None);
|
||||
let n = painted(&mut doc, shapes::rectangle(Rect::new(40.0, 10.0, 60.0, 30.0)), vec![AppearanceItem::Fill(FillLayer::new(fill))]);
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
let first = program(&doc);
|
||||
assert!(first.contains("0.8 0.4 0.2 Xa"), "the fill's working RGB values are written");
|
||||
let again = String::from_utf8(export_ai_editing(&read_back(&doc), &opts()).unwrap().program).unwrap().replace("\r\n", "\n");
|
||||
same_lines(&again.lines().map(String::from).collect::<Vec<_>>(), &first.lines().map(String::from).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
/// A gradient stop of a Lab spot color writes the RGB values of the Lab values as written (to four
|
||||
/// decimals, clamped to the range a reader reads), the same Lab values its CMYK equivalent and the
|
||||
/// palette's definition come from: for Lab values of many decimals and for Lab values out of range.
|
||||
#[test]
|
||||
fn a_lab_spot_stop_writes_rgb_values_of_its_lab_values_as_written() {
|
||||
let mut doc = Document::new(300.0, 200.0);
|
||||
let v = |i: u16, s: f32| (f32::from(i) * s + 0.123_456_7).fract();
|
||||
let mut spots: Vec<(String, Color)> = vec![("Out".into(), Color::lab(104.567_89, -140.123_46, 131.987_65))];
|
||||
spots.push(("Rim".into(), Color::lab(55.555_55, 131.234_56, -12.345_67)));
|
||||
for i in 0..8u16 {
|
||||
spots
|
||||
.push((format!("Moss {i}"), Color::lab(v(i, 0.137_911_3) * 100.0, v(i, 0.271_828_2) * 200.0 - 100.0, v(i, 0.314_159_3) * 200.0 - 100.0)));
|
||||
}
|
||||
let mut swatches: Vec<Swatch> = doc.swatches.iter().cloned().collect();
|
||||
swatches.extend(spots.iter().map(|(name, c)| Swatch { name: name.clone(), paint: solid(*c), global: true, spot: true }));
|
||||
doc.swatches = swatches.into();
|
||||
let layer = doc.layers[0].id;
|
||||
for (i, (name, c)) in spots.iter().enumerate() {
|
||||
let stops = vec![
|
||||
GradientStop::new(0.0, Color::rgb(0.1, 0.2, 0.3)),
|
||||
GradientStop { swatch: Some(name.clone()), tint: 0.5, ..GradientStop::new(1.0, c.tinted(0.5)) },
|
||||
];
|
||||
let fill = gradient_paint(Gradient::new(GradientKind::Linear, stops), None, None);
|
||||
let x = i as f64 * 25.0 + 10.0;
|
||||
let n = painted(&mut doc, shapes::rectangle(Rect::new(x, 10.0, x + 20.0, 30.0)), vec![AppearanceItem::Fill(FillLayer::new(fill))]);
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
}
|
||||
let program = program(&doc);
|
||||
for (name, c) in &spots {
|
||||
let mut want = String::new();
|
||||
crate::ps::push_nums(&mut want, &crate::ai_ops::as_written(*c).to_rgb().map(|v| f64::from(crate::ai_ops::unit(v))));
|
||||
want.push_str(&format!("({name}) 0.5 1 4 "));
|
||||
assert!(program.lines().any(|l| l.contains(&want)), "no stop of {name} writes {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_data_is_zstandard_compressed_with_the_header_as_metadata() {
|
||||
let doc = sample();
|
||||
@@ -5872,25 +6000,6 @@ fn outlines(doc: &Document) -> Vec<String> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Fails with the lines where `got` and `want` differ in more than their numbers' last written
|
||||
/// decimal (a color read back from four decimals gives its other model's values again from them).
|
||||
fn near_lines(got: &str, want: &str) {
|
||||
let near = |a: &str, b: &str| {
|
||||
let (a, b): (Vec<&str>, Vec<&str>) = (a.split(' ').collect(), b.split(' ').collect());
|
||||
a.len() == b.len()
|
||||
&& a.iter().zip(&b).all(|(x, y)| match (x.trim_start_matches("%_").parse::<f64>(), y.trim_start_matches("%_").parse::<f64>()) {
|
||||
(Ok(x), Ok(y)) => (x - y).abs() <= 2e-3,
|
||||
_ => x == y,
|
||||
})
|
||||
};
|
||||
let (g, w): (Vec<&str>, Vec<&str>) = (got.lines().collect(), want.lines().collect());
|
||||
let differ: Vec<String> = (0..g.len().max(w.len()))
|
||||
.filter(|&i| !matches!((g.get(i), w.get(i)), (Some(a), Some(b)) if near(a, b)))
|
||||
.map(|i| format!("line {i}:\n got {:?}\n want {:?}", g.get(i), w.get(i)))
|
||||
.collect();
|
||||
assert!(differ.is_empty(), "{}", differ.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objects_with_live_effects_and_graphic_styles_are_read_back() {
|
||||
let doc = styles_document();
|
||||
@@ -6212,25 +6321,7 @@ fn art_styles_write_the_same_bytes_again() {
|
||||
assert_eq!(private_data(&doc, &opts()).unwrap().0, private_data(&doc, &opts()).unwrap().0);
|
||||
// What is read back is written as it was.
|
||||
let again = export_ai_editing(&read_back(&doc), &opts()).unwrap().program;
|
||||
near_lines(&rgb_without_cmyk(&shown(&again)), &rgb_without_cmyk(&shown(&first.program)));
|
||||
}
|
||||
|
||||
/// `program` with the CMYK equivalents of plain RGB colors blanked out: the first four numbers of
|
||||
/// `c m y k r g b Xa` (or `XA`) and of an RGB gradient stop's `c m y k r g b 2 ... Bs`. Named and
|
||||
/// spot RGB colors (`... (name) tint Xk`, `Xx` and spot stops) are not blanked; the document this
|
||||
/// is used on has none. The Generic CMYK separation of a color at the edge of the press gamut can
|
||||
/// land on one of two results that differ by up to 0.01 when the RGB changes in its last float
|
||||
/// bits, as it does between the first save and the save of the reopened file, and the platform's
|
||||
/// float math decides which. The RGB values stay compared.
|
||||
fn rgb_without_cmyk(program: &str) -> String {
|
||||
let blank = |line: &str| {
|
||||
let (prefix, rest) = line.strip_prefix("%_").map_or(("", line), |r| ("%_", r));
|
||||
let t: Vec<&str> = rest.split(' ').collect();
|
||||
let color = t.len() == 8 && matches!(t[7], "Xa" | "XA");
|
||||
let stop = t.len() > 8 && t[7] == "2" && t.last() == Some(&"Bs");
|
||||
if color || stop { format!("{prefix}_ _ _ _ {}", t[4..].join(" ")) } else { line.to_string() }
|
||||
};
|
||||
program.lines().map(blank).collect::<Vec<_>>().join("\n")
|
||||
same_lines(&shown(&again).lines().map(String::from).collect::<Vec<_>>(), &shown(&first.program).lines().map(String::from).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
/// `n` on the first layer of `doc`.
|
||||
|
||||
@@ -1357,8 +1357,9 @@ impl<'a> Writer<'a> {
|
||||
self.plain_color(*color, stroke);
|
||||
}
|
||||
|
||||
/// A color that names no swatch: `g` (gray), `k` (CMYK), else `c m y k r g b Xa` (RGB, and
|
||||
/// Lab as RGB), with its CMYK equivalent.
|
||||
/// A color that names no swatch: `g` (gray), `k` (CMYK), else `c m y k r g b Xa` (RGB in the
|
||||
/// working RGB space, and Lab as display RGB), with its CMYK equivalent (an RGB color's from its
|
||||
/// values as written, a Lab color's from its Lab values).
|
||||
fn plain_color(&mut self, color: Color, stroke: bool) {
|
||||
let s = &mut self.body;
|
||||
let op = |fill: &'static str, line: &'static str| if stroke { line } else { fill };
|
||||
@@ -1373,19 +1374,22 @@ impl<'a> Writer<'a> {
|
||||
let _ = writeln!(s, "{}", op("k", "K"));
|
||||
}
|
||||
Color::Rgb { .. } | Color::Lab { .. } => {
|
||||
let cmyk = self.cms.to_cmyk(&color, self.cms.settings().intent).map(unit);
|
||||
let source = if matches!(color, Color::Rgb { .. }) { ai_ops::as_written(color) } else { color };
|
||||
let cmyk = self.cms.to_cmyk(&source, self.cms.settings().intent).map(unit);
|
||||
ps::push_nums(s, &cmyk.map(f64::from));
|
||||
ps::push_nums(s, &color.to_rgb().map(|v| f64::from(unit(v))));
|
||||
ps::push_nums(s, &ai_ops::rgb_values(color).map(f64::from));
|
||||
let _ = writeln!(s, "{}", op("Xa", "XA"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A spot (`spot`) or global process color `name` of full color `full` at `tint`, written to
|
||||
/// `s`: its CMYK equivalent, its RGB or Lab values, the name, how much lighter than the full
|
||||
/// color, and for any but a CMYK spot color (`x`) the model (0 CMYK, 1 RGB, 2 Lab) before
|
||||
/// `Xx` (spot) or `Xk` (global process).
|
||||
/// `s`: its CMYK equivalent (from its RGB or Lab values as written), its RGB or Lab values
|
||||
/// (clamped as [`ai_ops::as_written`] clamps them), the name, how much lighter than the full
|
||||
/// color, and for any but a CMYK spot color (`x`) the model (0 CMYK, 1 RGB, 2 Lab) before `Xx`
|
||||
/// (spot) or `Xk` (global process).
|
||||
fn named(&self, s: &mut String, name: &str, full: Color, tint: f32, spot: bool, stroke: bool) {
|
||||
let full = ai_ops::as_written(full);
|
||||
let cmyk = self.cms.to_cmyk(&self.doc.linked_color(full, spot), self.cms.settings().intent).map(unit);
|
||||
ps::push_nums(s, &cmyk.map(f64::from));
|
||||
let model = match full {
|
||||
@@ -1481,7 +1485,8 @@ impl<'a> Writer<'a> {
|
||||
true
|
||||
}
|
||||
|
||||
/// The spot color swatches, with their full colors' CMYK equivalents, for the header.
|
||||
/// The spot color swatches, with their full colors' CMYK equivalents (as the palette writes
|
||||
/// them), for the header.
|
||||
pub(super) fn spot_swatches(&self) -> Vec<(String, [f32; 4])> {
|
||||
let intent = self.cms.settings().intent;
|
||||
self.doc
|
||||
@@ -1489,7 +1494,7 @@ impl<'a> Writer<'a> {
|
||||
.filter(|sw| sw.spot)
|
||||
.filter_map(|sw| match sw.paint {
|
||||
Paint::Solid { color, swatch: None, .. } => {
|
||||
Some((sw.name.clone(), self.cms.to_cmyk(&self.doc.linked_color(color, true), intent).map(unit)))
|
||||
Some((sw.name.clone(), self.cms.to_cmyk(&self.doc.linked_color(ai_ops::as_written(color), true), intent).map(unit)))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ use vectorcraft_effects::stroke;
|
||||
use vectorcraft_geom::{Affine, BezPath, Rect, Shape, Vec2};
|
||||
|
||||
use super::{MAX_NEST, Writer, Xy};
|
||||
use crate::ai_ops::{average, unit};
|
||||
use crate::ai_ops::{self, average, unit};
|
||||
use crate::ps;
|
||||
|
||||
pub(in crate::ai_editing) const GRADIENTS: &str =
|
||||
@@ -371,7 +371,11 @@ impl Writer<'_> {
|
||||
/// A stop's color and its style: gray `lightness 0`, CMYK `c m y k 1`, RGB (and Lab, as RGB)
|
||||
/// `c m y k r g b 2` with its CMYK equivalent; a spot color its full color's CMYK equivalent,
|
||||
/// then its RGB values when it isn't CMYK, its name and how much lighter than the full color,
|
||||
/// then `3` (CMYK), or `1 4` (RGB, and Lab as RGB).
|
||||
/// then `3` (CMYK), or `1 4` (RGB, and Lab as RGB). RGB values are an RGB color's own (in the
|
||||
/// working RGB space) and a Lab color's display RGB ([`ai_ops::rgb_values`]). An RGB color's CMYK
|
||||
/// equivalent is converted from its values as written ([`ai_ops::as_written`]), a Lab color's
|
||||
/// from its Lab values, and a spot color's CMYK equivalent and RGB values both come from its full
|
||||
/// color as the palette writes it.
|
||||
fn stop_color(&mut self, s: &mut String, stop: &GradientStop) {
|
||||
let doc = self.doc;
|
||||
let intent = self.cms.settings().intent;
|
||||
@@ -380,11 +384,12 @@ impl Writer<'_> {
|
||||
&& let Paint::Solid { color: full, swatch: None, .. } = sw.paint
|
||||
{
|
||||
if sw.spot {
|
||||
let full = ai_ops::as_written(full);
|
||||
let cmyk = self.cms.to_cmyk(&doc.linked_color(full, true), intent).map(unit);
|
||||
ps::push_nums(s, &cmyk.map(f64::from));
|
||||
let rgb = !matches!(full, Color::Cmyk { .. });
|
||||
if rgb {
|
||||
ps::push_nums(s, &full.to_rgb().map(|v| f64::from(unit(v))));
|
||||
ps::push_nums(s, &ai_ops::rgb_values(full).map(f64::from));
|
||||
}
|
||||
s.push_str(&ps::bytes(name.as_bytes()));
|
||||
s.push(' ');
|
||||
@@ -406,9 +411,10 @@ impl Writer<'_> {
|
||||
s.push_str("1 ");
|
||||
}
|
||||
color @ (Color::Rgb { .. } | Color::Lab { .. }) => {
|
||||
let cmyk = self.cms.to_cmyk(&color, intent).map(unit);
|
||||
let source = if matches!(color, Color::Rgb { .. }) { ai_ops::as_written(color) } else { color };
|
||||
let cmyk = self.cms.to_cmyk(&source, intent).map(unit);
|
||||
ps::push_nums(s, &cmyk.map(f64::from));
|
||||
ps::push_nums(s, &color.to_rgb().map(|v| f64::from(unit(v))));
|
||||
ps::push_nums(s, &ai_ops::rgb_values(color).map(f64::from));
|
||||
s.push_str("2 ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,27 @@ pub(crate) fn unit(v: f32) -> f32 {
|
||||
if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 }
|
||||
}
|
||||
|
||||
/// `color` as a reader reads it back from the values written for it ([`ps::written`]): RGB values
|
||||
/// clamped to 0..1, L to 0..100 and a and b to -128..127, each to four decimals; other colors
|
||||
/// unchanged.
|
||||
pub(crate) fn as_written(color: Color) -> Color {
|
||||
let w = |v: f32| ps::written(f64::from(v)) as f32;
|
||||
match color {
|
||||
Color::Rgb { r, g, b } => Color::rgb(w(unit(r)), w(unit(g)), w(unit(b))),
|
||||
Color::Lab { l, a, b } => Color::lab(w(l.clamp(0.0, 100.0)), w(a.clamp(-128.0, 127.0)), w(b.clamp(-128.0, 127.0))),
|
||||
c => c,
|
||||
}
|
||||
}
|
||||
|
||||
/// The RGB values written for `color`, clamped to 0..1: an RGB color's own values (in the working
|
||||
/// RGB space, as a reader reads them back), any other color's display RGB.
|
||||
pub(crate) fn rgb_values(color: Color) -> [f32; 3] {
|
||||
match color {
|
||||
Color::Rgb { r, g, b } => [r, g, b].map(unit),
|
||||
c => c.to_rgb().map(unit),
|
||||
}
|
||||
}
|
||||
|
||||
/// The average colour of a gradient's stops (in RGB).
|
||||
pub(crate) fn average(g: &Gradient) -> Color {
|
||||
let n = g.stops.len().max(1) as f32;
|
||||
|
||||
@@ -755,6 +755,28 @@ fn a_gradient_fill_and_its_stroke_are_one_object() {
|
||||
assert_eq!(n.appearance.stroke_paint().color(), Some(Color::rgb(1.0, 0.0, 0.0)));
|
||||
}
|
||||
|
||||
/// An RGB gradient stop's CMYK equivalent is the one of its RGB values as written (to four
|
||||
/// decimals), so the reopened file writes the same stops.
|
||||
#[test]
|
||||
fn rgb_stops_are_written_again_unchanged() {
|
||||
let mut doc = Document::new(100.0, 100.0);
|
||||
let rgb = |i: u16| {
|
||||
let v = |s: f32| (f32::from(i) * s + 0.123_456_7).fract();
|
||||
Color::rgb(v(0.137_911_3), v(0.271_828_2), v(0.314_159_3))
|
||||
};
|
||||
let stops = (0..12u16).map(|i| GradientStop::new(f32::from(i) / 11.0, rgb(i))).collect();
|
||||
let paint = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, stops))));
|
||||
let n = shape(&mut doc, shapes::rectangle(Rect::new(10.0, 10.0, 50.0, 50.0)), paint, Paint::None);
|
||||
let layer = doc.layers[0].id;
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
let stops = |bytes: Vec<u8>| String::from_utf8(bytes).unwrap().lines().filter(|l| l.ends_with("%_Bs")).map(String::from).collect::<Vec<_>>();
|
||||
let first = write(&doc).bytes;
|
||||
let again = write(&crate::import(&first).unwrap().document).bytes;
|
||||
let (first, again) = (stops(first), stops(again));
|
||||
assert_eq!(first.len(), 12, "{first:?}");
|
||||
assert_eq!(again, first);
|
||||
}
|
||||
|
||||
/// A gradient from white to #1b2a6b, the colors of #2687's circle, placed at `geom` (None: fitted
|
||||
/// to the object).
|
||||
fn white_to_navy(kind: GradientKind, geom: Option<vectorcraft_color::GradientGeom>) -> Paint {
|
||||
|
||||
@@ -633,7 +633,8 @@ impl<'a> Writer<'a> {
|
||||
// Grey is ink coverage; the format's grey is lightness.
|
||||
Color::Gray { k } => Ink::Gray(unit(1.0 - k)),
|
||||
Color::Cmyk { c, m, y, k } => Ink::Cmyk([c, m, y, k].map(unit)),
|
||||
Color::Rgb { r, g, b } => Ink::Rgb(self.cms.to_cmyk(color, intent).map(unit), [r, g, b].map(unit)),
|
||||
// The CMYK equivalent of the RGB values as written.
|
||||
Color::Rgb { r, g, b } => Ink::Rgb(self.cms.to_cmyk(&ai_ops::as_written(*color), intent).map(unit), [r, g, b].map(unit)),
|
||||
Color::Lab { .. } => {
|
||||
let rgb = color.to_rgb().map(unit);
|
||||
Ink::Rgb(self.cms.to_cmyk(color, intent).map(unit), rgb)
|
||||
|
||||
@@ -36,6 +36,11 @@ pub(crate) fn push_num(s: &mut String, v: f64) {
|
||||
push_num_places(s, v, 4);
|
||||
}
|
||||
|
||||
/// The value [`num`]`(v)` stands for, as a reader parses it back.
|
||||
pub(crate) fn written(v: f64) -> f64 {
|
||||
num(v).parse().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Append `v` to `s` with at most `places` decimals (up to 9), no trailing zeros (0 when not
|
||||
/// finite).
|
||||
pub(crate) fn push_num_places(s: &mut String, v: f64, places: u32) {
|
||||
|
||||
Reference in New Issue
Block a user