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:
BFlat
2026-10-17 17:10:33 +07:00
committed by GitHub
7 changed files with 131 additions and 7 deletions
+22
View File
@@ -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)]
+3 -3
View File
@@ -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)"))),
}
}
+35 -3
View File
@@ -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.
+22
View File
@@ -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();
+42
View File
@@ -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();
+2
View File
@@ -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
View File
@@ -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}}}