mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
New Document and Document Setup: values out of range in a saved preset or a file are read in range
file.new takes a width and height up to 163,830 pt, 1 to 1,000 artboards, a spacing of 0 to 100,000 pt and a bleed of 0 to 72 pt a side, but a saved or Recent New Document preset in the preferences was read as it was stored. With a width of -5, file.new failed with "width/height must be positive and within the canvas"; a spacing of -50 overlapped the artboards, a preset with 5,000 artboards made 5,000 artboards, and a bleed of 100 reached the document. A .vectorcraft file's Document Setup bleed, superscript and subscript proportions and small caps size also opened as stored, and document.setup then failed when its own report was passed back to it. These values are now read as the nearer end of the range their setter takes, and an artboard width or height of 0 or less is read as 1 pt. Presets and files with values in range read as before. Docs: docs/mcp.md (Document Setup, New Document).
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