Document Raster Effects Settings: a file's resolution outside 1 to 2400 ppi opens in range

document.rasterEffectsSettings accepts a resolution from 1 to 2,400
ppi, but a file's raster_effects_ppi opened as it was saved. With 0,
-5 or 5000 in the file, object.rasterize without a ppi param failed
with "ppi must be between 1 and 2400", and document.rasterEffectsSettings
and document.info reported the stored value. With 0 or -5, EPS and
PDF export wrote a freeform gradient as a 1 x 1 px image, and EMF
export wrote every gradient as one. A saved New Document preset in the
preferences kept such a value too.

A file's or a saved preset's resolution outside 1 to 2,400 ppi is now
read as the nearer end of that range. Documents with a resolution in
range open as before.

Docs: docs/mcp.md (resolution, rasterEffectsPpi).
This commit is contained in:
dotCooCoo
2026-10-17 13:20:23 +07:00
committed by bflatastic
parent 5eea7e4f66
commit 1e3886c053
6 changed files with 70 additions and 3 deletions
+7
View File
@@ -103,6 +103,11 @@ impl PreviewMode {
}
}
/// A saved preset's raster effects resolution, within 1 to 2400 ppi (72 when it isn't finite).
fn de_ppi<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<f64, D::Error> {
Ok(Some(f64::deserialize(d)?).filter(|v| v.is_finite()).unwrap_or(72.0).clamp(1.0, 2400.0))
}
/// Everything New Document sets: a preset (built-in, saved or recent) or `file.new`'s params.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
@@ -125,6 +130,8 @@ pub struct DocSettings {
pub bleed: [f64; 4],
pub background: Background,
pub color_mode: ColorMode,
/// 1 to 2400 ppi (a saved preset's value outside that range is read as the nearer end).
#[serde(deserialize_with = "de_ppi")]
pub raster_effects_ppi: f64,
pub preview_mode: PreviewMode,
}
+11
View File
@@ -225,6 +225,17 @@ fn saved_presets_live_in_the_preferences() {
assert!(s.execute("file.newPresets.save", &json!({"name": " "})).is_err());
}
#[test]
fn a_saved_preset_with_the_resolution_out_of_range_reads_in_range() {
let mut s = Session::new();
let saved = json!({"newDocPresets": [{"name": "Zero", "rasterEffectsPpi": 0}, {"name": "Huge", "rasterEffectsPpi": 5000}]});
s.apply_prefs(serde_json::from_value(saved).unwrap());
let ppi: Vec<Option<f64>> = presets(&mut s, "Saved").iter().map(|p| p["rasterEffectsPpi"].as_f64()).collect();
assert_eq!(ppi, [Some(1.0), Some(2400.0)]);
s.execute("file.new", &json!({"preset": "Zero"})).unwrap();
assert_eq!(s.doc().unwrap().doc.raster_effects_ppi, 1.0);
}
#[test]
fn rearrange_keeps_its_grid() {
let mut s = new_doc(json!({"width": 100, "height": 50, "artboards": 3}));
+45
View File
@@ -213,3 +213,48 @@ fn raster_settings_round_trip_and_old_files_load() {
let back = vectorcraft_format::load(&bytes).unwrap();
assert_eq!((back.raster_effects_ppi, back.raster_effects), (72.0, RasterEffectsSettings::default()));
}
/// Save the active document with the raster effects resolution `ppi` and open the file.
fn reopen_with_ppi(s: &mut Session, ppi: f64) {
let mut d = (*s.doc().unwrap().doc).clone();
d.raster_effects_ppi = ppi;
let data = vectorcraft_format::base64_encode(&vectorcraft_format::save_file(&d));
s.execute("document.open", &json!({"name": "ppi.vectorcraft", "dataBase64": data})).unwrap();
}
#[test]
fn a_file_with_the_resolution_out_of_range_opens_with_it_in_range() {
for (saved, opened, pixels) in [(0.0, 1.0, 1), (-5.0, 1.0, 1), (5000.0, 2400.0, 334), (f64::MAX, 2400.0, 334)] {
let mut s = session();
let r = id(&s.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 10, "height": 10})).unwrap());
s.execute("paint.setStroke", &json!({"none": true, "ids": [r.0]})).unwrap();
reopen_with_ppi(&mut s, saved);
assert_eq!(s.doc().unwrap().doc.raster_effects_ppi, opened, "saved as {saved}");
assert_eq!(s.execute(C, &json!({})).unwrap()["resolution"], opened);
assert_eq!(s.execute("document.info", &json!({})).unwrap()["document"]["rasterEffectsPpi"], opened);
// Rasterize without a ppi param renders the 10 pt square at it.
s.execute("select.set", &json!({"ids": [r.0]})).unwrap();
let out = s.execute("object.rasterize", &json!({})).unwrap();
assert_eq!((out["width"].as_u64(), out["height"].as_u64()), (Some(pixels), Some(pixels)), "saved as {saved}");
}
}
#[test]
fn a_gradient_exports_at_the_resolution_the_file_opens_with() {
for saved in [0.0, -5.0] {
let mut s = session();
let r = id(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 720, "height": 720})).unwrap());
s.execute("paint.setFill", &json!({"gradient": {"kind": "linear"}, "ids": [r.0]})).unwrap();
reopen_with_ppi(&mut s, saved);
// EMF writes the gradient as an image at the raster effects resolution: 720 pt at 1 ppi.
let region = vectorcraft_geom::Rect::new(0.0, 0.0, 720.0, 720.0);
let out = vectorcraft_metafile::export(&s.doc().unwrap().doc, region, vectorcraft_metafile::Kind::Emf).unwrap();
let mut sizes = vec![];
vectorcraft_metafile::import(&out.bytes).unwrap().document.walk(|n| {
if let NodeKind::Image(im) = &n.kind {
sizes.push((im.width, im.height));
}
});
assert_eq!(sizes, [(10, 10)], "saved as {saved}");
}
}
+2
View File
@@ -555,6 +555,8 @@ pub fn load_file(bytes: &[u8]) -> Result<NativeFile, FormatError> {
doc.raster_effects.add_around = doc.raster_effects.add_around.clamp(0.0, vectorcraft_doc::rastersettings::MAX_ADD_AROUND);
let mut depth_notes = vec![];
doc.limit_depth_noting(&mut depth_notes);
// Untrusted: the raster effects resolution, within 1 to 2400 ppi (72 when it isn't finite).
doc.raster_effects_ppi = Some(doc.raster_effects_ppi).filter(|v| v.is_finite()).unwrap_or(72.0).clamp(1.0, 2400.0);
let profiles = f.profiles.into_iter().filter_map(|(name, icc)| Some((name, bytes_of(&icc)?))).collect();
Ok(NativeFile { doc, depth_notes, info: FileInfo { version: f.version, legacy: f.format == LEGACY_EXTENSION }, profiles })
}
+1 -1
View File
@@ -225,7 +225,7 @@ fn approx_equal_documents_compare_equal() {
fn bug_f64_bit_exact_roundtrip() {
check_native_roundtrip_exact(&rich_doc()).unwrap();
let mut d = Document::new(126.86291501015239, 214.69463130731182);
d.raster_effects_ppi = 0.1 + 0.2;
d.raster_effects_ppi = (0.1 + 0.2) * 1000.0;
check_native_roundtrip_exact(&d).unwrap();
}
+4 -2
View File
@@ -2220,7 +2220,8 @@ art). Pass any of these keys to change them in one undo step; `document.setUnits
`file.new` takes everything New Document sets: `preset` (a name from `file.newPresets`), `name`, `width`/`height`
(points, or lengths such as `"210 mm"`), `units`, `orientation`, `artboards` with `artboardLayout {layout, columns,
spacing, rightToLeft}`, `bleed`, `backgroundContents`, `colorMode`, `rasterEffectsPpi` and `previewMode`.
spacing, rightToLeft}`, `bleed`, `backgroundContents`, `colorMode`, `rasterEffectsPpi` (1 to 2,400; a saved preset's
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
@@ -2342,7 +2343,8 @@ written unless the whole batch succeeds puts the saves and exports last, or runs
## Document Raster Effects Settings
`document.rasterEffectsSettings` holds how raster effects (shadows, glows, blurs, feathers) become images when PDF
export or Expand Appearance renders them: `resolution` (ppi, also New Document's `rasterEffectsPpi`), `colorModel`
export or Expand Appearance renders them: `resolution` (1 to 2,400 ppi, also New Document's `rasterEffectsPpi`; a
file's value outside that range opens as the nearer end), `colorModel`
(the document's `rgb`/`cmyk`, `grayscale` or `bitmap`), `background` (`white` makes the images opaque), `antiAlias`
(off: hard edges), `clippingMask` (the white stays under the art only), `addAround` (0 to 1,000 points of room
around the art; a file's value outside that range opens as the nearer end) and `preserveSpotColors` (stored). They are