mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
Merge pull request #2656 from dotCooCoo/fix/saved-bleed-and-spacing-in-range
New Document and Document Setup: values out of range in a saved preset or a file are read in range
This commit is contained in:
@@ -192,6 +192,16 @@ impl Background {
|
||||
|
||||
/// Largest bleed on any side, in points (1 inch).
|
||||
pub const MAX_BLEED: f64 = 72.0;
|
||||
/// Largest superscript or subscript size, and the farthest its baseline moves either way, in
|
||||
/// percent of the font size.
|
||||
pub const MAX_SCRIPT: f64 = 500.0;
|
||||
/// Largest synthesized small capitals size, in percent of the font size.
|
||||
pub const MAX_SMALL_CAPS: f64 = 200.0;
|
||||
|
||||
/// Returns `bleed` with each side kept to 0–[`MAX_BLEED`] points; a side that isn't finite becomes 0.
|
||||
pub fn bleed_in_range(bleed: [f64; 4]) -> [f64; 4] {
|
||||
bleed.map(|b| if b.is_finite() { b.clamp(0.0, MAX_BLEED) } else { 0.0 })
|
||||
}
|
||||
|
||||
/// File → Document Setup.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -274,6 +284,18 @@ impl DocSetup {
|
||||
pub fn flattener(&self) -> &str {
|
||||
self.flattener_preset.as_deref().unwrap_or(DEFAULT_FLATTENER)
|
||||
}
|
||||
/// Moves the bleed and the type proportions into the ranges Document Setup takes, each to the
|
||||
/// nearer end: the bleed by [`bleed_in_range`], superscript and subscript sizes to
|
||||
/// 1–[`MAX_SCRIPT`] and positions to ±[`MAX_SCRIPT`], the small caps size to
|
||||
/// 1–[`MAX_SMALL_CAPS`].
|
||||
pub fn keep_in_range(&mut self) {
|
||||
self.bleed = bleed_in_range(self.bleed);
|
||||
for m in [&mut self.superscript, &mut self.subscript] {
|
||||
m.size = m.size.clamp(1.0, MAX_SCRIPT);
|
||||
m.position = m.position.clamp(-MAX_SCRIPT, MAX_SCRIPT);
|
||||
}
|
||||
self.small_caps_size = self.small_caps_size.clamp(1.0, MAX_SMALL_CAPS);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -121,7 +121,7 @@ fn apply(s: &mut DocSetup, units: &mut vectorcraft_doc::Unit, k: &str, v: &Value
|
||||
"typographersQuotes" => s.typographers_quotes = flag(k, v)?,
|
||||
"superscript" => s.superscript = script_param(k, v, s.superscript)?,
|
||||
"subscript" => s.subscript = script_param(k, v, s.subscript)?,
|
||||
"smallCapsSize" => s.small_caps_size = percent(k, v, 1.0, 200.0)?,
|
||||
"smallCapsSize" => s.small_caps_size = percent(k, v, 1.0, vectorcraft_doc::setup::MAX_SMALL_CAPS)?,
|
||||
"exportText" => s.export_text = v.as_str().and_then(ExportText::parse).ok_or_else(|| bad(C, "exportText must be editable or appearance"))?,
|
||||
"backgroundContents" => s.background = background_param(v, C)?,
|
||||
// Read-only values `document.setup` reports.
|
||||
@@ -223,8 +223,8 @@ fn script_param(k: &str, v: &Value, mut m: ScriptMetrics) -> Result<ScriptMetric
|
||||
let o = v.as_object().ok_or_else(|| bad(C, format!("{k} must be {{size?, position?}}")))?;
|
||||
for (key, x) in o {
|
||||
match key.as_str() {
|
||||
"size" => m.size = percent(&format!("{k}.size"), x, 1.0, 500.0)?,
|
||||
"position" => m.position = percent(&format!("{k}.position"), x, -500.0, 500.0)?,
|
||||
"size" => m.size = percent(&format!("{k}.size"), x, 1.0, vectorcraft_doc::setup::MAX_SCRIPT)?,
|
||||
"position" => m.position = percent(&format!("{k}.position"), x, -vectorcraft_doc::setup::MAX_SCRIPT, vectorcraft_doc::setup::MAX_SCRIPT)?,
|
||||
_ => return Err(bad(C, format!("unknown {k} key `{key}` (size, position)"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,19 +114,29 @@ fn de_ppi<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<f64, D:
|
||||
pub struct DocSettings {
|
||||
/// The preset's name.
|
||||
pub name: String,
|
||||
/// Artboard size in points.
|
||||
/// Artboard size in points, more than 0 and up to `MAX_SIDE` (a saved or Recent preset's
|
||||
/// value outside that range is read as the nearer end, and one of 0 or less is read as 1).
|
||||
#[serde(deserialize_with = "de_side")]
|
||||
pub width: f64,
|
||||
#[serde(deserialize_with = "de_side")]
|
||||
pub height: f64,
|
||||
pub units: Unit,
|
||||
/// 1 to `MAX_ARTBOARDS` (a saved or Recent preset's value outside that range is read as the
|
||||
/// nearer end).
|
||||
#[serde(deserialize_with = "de_artboards")]
|
||||
pub artboards: u32,
|
||||
pub layout: ArtboardLayout,
|
||||
/// Columns of a grid layout.
|
||||
pub columns: u32,
|
||||
/// Space between artboards, points.
|
||||
/// Space between artboards, points: 0 to `MAX_SPACING` (a saved or Recent preset's value
|
||||
/// outside that range is read as the nearer end).
|
||||
#[serde(deserialize_with = "de_spacing")]
|
||||
pub spacing: f64,
|
||||
/// Change to Right-to-Left Layout: the first artboard at the right.
|
||||
pub right_to_left: bool,
|
||||
/// Top, bottom, left, right, points.
|
||||
/// Top, bottom, left, right, points: 0 to `MAX_BLEED` each (a saved or Recent preset's value
|
||||
/// outside that range is read as the nearer end).
|
||||
#[serde(deserialize_with = "de_bleed")]
|
||||
pub bleed: [f64; 4],
|
||||
pub background: Background,
|
||||
pub color_mode: ColorMode,
|
||||
@@ -348,6 +358,28 @@ pub fn color_mode_id(m: ColorMode) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// A saved or Recent preset's artboard side: up to [`MAX_SIDE`] points, and 1 when it is 0 or less.
|
||||
fn de_side<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<f64, D::Error> {
|
||||
let v = f64::deserialize(d)?;
|
||||
Ok(if v > 0.0 { v.min(MAX_SIDE) } else { 1.0 })
|
||||
}
|
||||
|
||||
/// A saved or Recent preset's artboard count, rounded and kept to 1 to [`MAX_ARTBOARDS`] as
|
||||
/// `file.new` reads its `artboards` param.
|
||||
fn de_artboards<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<u32, D::Error> {
|
||||
Ok(f64::deserialize(d)?.round().clamp(1.0, MAX_ARTBOARDS as f64) as u32)
|
||||
}
|
||||
|
||||
/// A saved or Recent preset's artboard spacing, kept to 0 to [`MAX_SPACING`] points.
|
||||
fn de_spacing<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<f64, D::Error> {
|
||||
Ok(f64::deserialize(d)?.clamp(0.0, MAX_SPACING))
|
||||
}
|
||||
|
||||
/// A saved or Recent preset's bleed, each side kept to 0 to `MAX_BLEED` points.
|
||||
fn de_bleed<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<[f64; 4], D::Error> {
|
||||
Ok(vectorcraft_doc::setup::bleed_in_range(<[f64; 4]>::deserialize(d)?))
|
||||
}
|
||||
|
||||
/// Top-left corners for artboards of `sizes` from `origin`: `columns` per row, `spacing` apart,
|
||||
/// each column as wide and each row as tall as its largest artboard; `by_column` fills columns
|
||||
/// top to bottom first, `rtl` puts the first column at the right.
|
||||
|
||||
@@ -213,6 +213,28 @@ fn setup_survives_save_and_load() {
|
||||
assert_eq!(back.setup.grid_colors_name(), "Custom");
|
||||
}
|
||||
|
||||
/// A file whose bleed, superscript, subscript or small caps size lies outside the range
|
||||
/// `document.setup` takes opens with it at the nearer end of that range, and the setup it reports
|
||||
/// can be fed back.
|
||||
#[test]
|
||||
fn a_file_with_setup_values_out_of_range_opens_with_them_in_range() {
|
||||
let mut s = session();
|
||||
let mut d = (*s.doc().unwrap().doc).clone();
|
||||
d.setup.bleed = [-5.0, 100.0, f64::MAX, 9.0];
|
||||
d.setup.superscript = ScriptMetrics { size: 0.0, position: f64::MAX };
|
||||
d.setup.subscript = ScriptMetrics { size: f64::MAX, position: -1000.0 };
|
||||
d.setup.small_caps_size = -5.0;
|
||||
let data = vectorcraft_format::base64_encode(&vectorcraft_format::save_file(&d));
|
||||
s.execute("document.open", &json!({"name": "setup.vectorcraft", "dataBase64": data})).unwrap();
|
||||
let st = setup(&s);
|
||||
assert_eq!(st.bleed, [0.0, 72.0, 72.0, 9.0]);
|
||||
assert_eq!(st.superscript, ScriptMetrics { size: 1.0, position: 500.0 });
|
||||
assert_eq!(st.subscript, ScriptMetrics { size: 500.0, position: -500.0 });
|
||||
assert_eq!(st.small_caps_size, 1.0);
|
||||
let q = s.execute("document.setup", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("document.setup", &q).unwrap(), q);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_text_export_follows_the_export_text_setting() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -194,6 +194,48 @@ fn recent_remembers_the_last_sizes() {
|
||||
assert_eq!(presets(&mut s, "Recent").len(), 12);
|
||||
}
|
||||
|
||||
/// A saved preset or a Recent entry whose size, artboards, spacing or bleed the preferences hold
|
||||
/// outside the range `file.new` takes reads as the nearer end of that range (a size of 0 or less
|
||||
/// reads as 1 pt), so `file.new` makes a document from it.
|
||||
#[test]
|
||||
fn a_saved_preset_with_values_out_of_range_reads_in_range() {
|
||||
let mut s = Session::new();
|
||||
let prefs = json!({
|
||||
"newDocPresets": [
|
||||
{"name": "Low", "width": -5, "height": 0, "artboards": 0, "spacing": -50, "bleed": [-5, -1, 0, 9]},
|
||||
{"name": "High", "width": f64::MAX, "height": 200000, "spacing": f64::MAX, "bleed": [100, f64::MAX, 72, 0]},
|
||||
{"name": "Many", "width": 10, "height": 10, "artboards": 5000, "spacing": 0},
|
||||
],
|
||||
"recentNewDocs": [{"name": "Custom", "width": 100, "height": 50, "artboards": 3, "spacing": -50}],
|
||||
});
|
||||
s.apply_prefs(serde_json::from_value(prefs).unwrap());
|
||||
let read = |p: &Value| {
|
||||
let n = |v: &Value| v.as_f64().unwrap();
|
||||
let bleed: Vec<f64> = p["bleed"].as_array().unwrap().iter().map(n).collect();
|
||||
(n(&p["width"]), n(&p["height"]), n(&p["artboards"]), n(&p["artboardLayout"]["spacing"]), bleed)
|
||||
};
|
||||
let saved: Vec<_> = presets(&mut s, "Saved").iter().map(read).collect();
|
||||
assert_eq!(
|
||||
saved,
|
||||
[
|
||||
(1.0, 1.0, 1.0, 0.0, vec![0.0, 0.0, 0.0, 9.0]),
|
||||
(163830.0, 163830.0, 1.0, 1.0e5, vec![72.0, 72.0, 72.0, 0.0]),
|
||||
(10.0, 10.0, 1000.0, 0.0, vec![0.0; 4]),
|
||||
]
|
||||
);
|
||||
assert_eq!(read(&presets(&mut s, "Recent")[0]), (100.0, 50.0, 3.0, 0.0, vec![0.0; 4]));
|
||||
// file.new makes each one with those values.
|
||||
s.execute("file.new", &json!({"preset": "Low"})).unwrap();
|
||||
assert_eq!((boards(&s), s.doc().unwrap().doc.setup.bleed), (vec![Rect::new(0.0, 0.0, 1.0, 1.0)], [0.0, 0.0, 0.0, 9.0]));
|
||||
s.execute("file.new", &json!({"preset": "High"})).unwrap();
|
||||
assert_eq!((boards(&s), s.doc().unwrap().doc.setup.bleed), (vec![Rect::new(0.0, 0.0, 163830.0, 163830.0)], [72.0, 72.0, 72.0, 0.0]));
|
||||
s.execute("file.new", &json!({"preset": "Many"})).unwrap();
|
||||
assert_eq!(boards(&s).len(), 1000);
|
||||
let custom = presets(&mut s, "Recent").into_iter().find(|p| p["name"] == "Custom").unwrap();
|
||||
s.execute("file.new", &custom).unwrap();
|
||||
assert_eq!(boards(&s).iter().map(|r| r.x0).collect::<Vec<_>>(), [0.0, 100.0, 200.0], "side by side, not overlapping");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_presets_live_in_the_preferences() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -549,6 +549,8 @@ pub fn load_file(bytes: &[u8]) -> Result<NativeFile, FormatError> {
|
||||
// Saved before v3: the assigned profiles move out of `unknown`.
|
||||
doc.migrate_color_profiles();
|
||||
doc.fix_next_id();
|
||||
// Untrusted: the bleed and type proportions, within the ranges Document Setup takes.
|
||||
doc.setup.keep_in_range();
|
||||
// Untrusted: capped, named and pointing at objects the file has.
|
||||
doc.tidy_saved_selections();
|
||||
doc.tidy_graph_designs();
|
||||
|
||||
+5
-1
@@ -2366,6 +2366,8 @@ paper colour), the flattener preset and Discard White Overprint (Overprint Previ
|
||||
it is on), the type options (language and its quotes, Use Typographer's Quotes for typed quotes, superscript, subscript
|
||||
and small caps proportions, SVG text export) and the background contents (white: raster exports are white behind the
|
||||
art). Pass any of these keys to change them in one undo step; `document.setUnits` stays as an alias for `units`.
|
||||
A file whose bleed (0 to 72 pt a side), superscript and subscript sizes (1 to 500%) and positions (-500 to 500%) or
|
||||
small caps size (1 to 200%) lies outside those ranges opens with the nearer end of each range.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"document.setup","params":{"gridColors":"Blue","gridSize":"large","language":"German","bleed":9}}}
|
||||
@@ -2380,7 +2382,9 @@ value outside that range is read as the nearer end) and `previewMode`.
|
||||
`file.newPresets {category?}` lists the categories and presets (Recent: the last sizes used; Saved: the user's presets,
|
||||
kept in the preferences by `file.newPresets.save` and removed by `file.newPresets.delete`). A listed preset can be
|
||||
passed straight back to `file.new`. Print presets and sizes without `units` start in `unitsGeneral`; screen presets
|
||||
(mobile, web, video, social) in Pixels.
|
||||
(mobile, web, video, social) in Pixels. A saved or Recent preset whose size (up to 163,830 pt), `artboards` (1 to
|
||||
1,000), spacing (0 to 100,000 pt) or bleed (0 to 72 pt a side) lies outside those ranges is read with the nearer end of
|
||||
each range, and a size of 0 or less is read as 1 pt.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"file.new","params":{"preset":"A4","orientation":"landscape","artboards":4,"artboardLayout":{"columns":2},"bleed":9}}}
|
||||
|
||||
Reference in New Issue
Block a user