mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
PDF export: Add Around is clamped when a file opens, and art too large for f64 gets no effect image
document.rasterEffectsSettings accepts addAround from 0 to 1,000 pt, but a file's raster_effects.addAround opened as it was saved. With addAround 1e300, the area of a raster effect image overflowed f64, the image scale became 0 and the image got the transform [inf, NaN, NaN, inf, x, y]. PDF export left that image out, so the drop shadow was missing. A file's addAround now opens clamped to 0 to 1,000 pt. An effect image whose area or scale is not finite is not made. Export then keeps the object with its effects, and Expand Appearance expands it without an image, as when its PNG can't be encoded. Docs: docs/mcp.md (addAround).
This commit is contained in:
@@ -170,6 +170,8 @@ pub(crate) fn effects_scale(doc: &Document) -> f64 {
|
||||
/// fills' and strokes' raster effects included). With `knockout`, the coverage of that art is knocked out
|
||||
/// of the image, which then only holds what the effects add around it (a shadow to go under the
|
||||
/// vector object). The document's raster effects settings add room around it and finish its pixels.
|
||||
/// `None` when it paints nothing, when the area it covers overflows `f64` or the scale is not
|
||||
/// positive, or when its PNG can't be encoded.
|
||||
pub(crate) fn effect_image(out: &mut Document, whole: &Node, knockout: Option<&Node>, scale: f64) -> Option<Node> {
|
||||
// What it paints (its members' effects included), and how far its fills' and strokes' raster
|
||||
// effects reach beyond that.
|
||||
@@ -177,8 +179,16 @@ pub(crate) fn effect_image(out: &mut Document, whole: &Node, knockout: Option<&N
|
||||
let look = out.raster_effects.clone();
|
||||
let reach = whole.appearance.items.iter().map(|i| effects::outset(i.effects(), b)).fold(0.0, f64::max) + 2.0 + look.add_around;
|
||||
let b = b.inflate(reach, reach);
|
||||
// An area too large for f64 gives a zero scale and an image transform that is not finite.
|
||||
let area = b.width() * b.height();
|
||||
if !area.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// Whole pixels, and no larger than 64 Mpx.
|
||||
let scale = scale.min((64.0e6 / (b.width() * b.height()).max(1.0)).sqrt());
|
||||
let scale = scale.min((64.0e6 / area.max(1.0)).sqrt());
|
||||
if !(scale.is_finite() && scale > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let region = Rect::new(b.x0, b.y0, b.x0 + (b.width() * scale).ceil().max(1.0) / scale, b.y0 + (b.height() * scale).ceil().max(1.0) / scale);
|
||||
// Anti-alias off: hard edges (the effects themselves stay smooth).
|
||||
let anti_alias = if look.anti_alias { AntiAlias::Art } else { AntiAlias::None };
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! raster effect images they make (PDF export, Expand Appearance) and the Rasterize defaults.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::rastersettings::MAX_ADD_AROUND;
|
||||
use vectorcraft_doc::{Background, Document, NodeId, NodeKind, RasterColorModel, RasterEffectsSettings};
|
||||
|
||||
use super::*;
|
||||
@@ -98,6 +99,60 @@ fn add_around_grows_the_effect_image() {
|
||||
assert_eq!((after.width() - before.width(), after.height() - before.height()), (72, 72));
|
||||
}
|
||||
|
||||
/// The transforms of the images in `doc` flattened for output.
|
||||
fn image_transforms(doc: &Document) -> Vec<[f64; 6]> {
|
||||
let mut xfs = vec![];
|
||||
crate::flatten_raster_effects(doc).unwrap().walk(|n| {
|
||||
if let NodeKind::Image(im) = &n.kind {
|
||||
xfs.push(im.xf.as_coeffs());
|
||||
}
|
||||
});
|
||||
xfs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_with_add_around_out_of_range_opens_with_it_in_range() {
|
||||
let mut s = session();
|
||||
shadowed(&mut s);
|
||||
let mut d = (*s.doc().unwrap().doc).clone();
|
||||
d.raster_effects.add_around = 1e300;
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save_file(&d)).unwrap();
|
||||
assert_eq!(back.raster_effects.add_around, MAX_ADD_AROUND);
|
||||
let xfs = image_transforms(&back);
|
||||
assert!(!xfs.is_empty() && xfs.iter().flatten().all(|c| c.is_finite()), "{xfs:?}");
|
||||
// 1,000 pt on every side at 72 ppi.
|
||||
assert_eq!(effect_image(&back).width() - effect_image(&s.doc().unwrap().doc).width(), 2000);
|
||||
d.raster_effects.add_around = -5.0;
|
||||
assert_eq!(vectorcraft_format::load(&vectorcraft_format::save_file(&d)).unwrap().raster_effects.add_around, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_too_large_for_f64_gets_no_effect_image() {
|
||||
let mut s = session();
|
||||
let r = shadowed(&mut s);
|
||||
// Settings out of range in a document made without opening a file.
|
||||
let mut d = (*s.doc().unwrap().doc).clone();
|
||||
d.raster_effects.add_around = 1e300;
|
||||
assert_eq!(image_transforms(&d), Vec::<[f64; 6]>::new());
|
||||
let mut d = (*s.doc().unwrap().doc).clone();
|
||||
let NodeKind::Path { path, live, .. } = &mut d.node_mut(r).unwrap().kind else { panic!("a path") };
|
||||
(*path, *live) = (vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(-1e300, -1e300, 1e300, 1e300)), None);
|
||||
assert_eq!(image_transforms(&d), Vec::<[f64; 6]>::new());
|
||||
// Expand Appearance of it by id, with nothing selected, puts no image into the document (the
|
||||
// rectangle at its own size gets one).
|
||||
let normal = (*s.doc().unwrap().doc).clone();
|
||||
let mut expanded_images = |d: Document| {
|
||||
let st = s.doc_mut().unwrap();
|
||||
(st.doc, st.selection) = (std::sync::Arc::new(d), Default::default());
|
||||
s.execute("effect.expandAppearance", &json!({"ids": [r.0]})).unwrap();
|
||||
let mut images = 0;
|
||||
s.doc().unwrap().doc.walk(|n| images += usize::from(matches!(n.kind, NodeKind::Image(_))));
|
||||
images
|
||||
};
|
||||
assert_eq!(expanded_images(normal), 1);
|
||||
assert_eq!(expanded_images(d), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_color_model_and_anti_alias_reach_the_pixels() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in New Issue
Block a user