mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Raise undefinedresult for non-finite EPS arithmetic instead of panicking
Fixes #3585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
b9819ebd89
commit
b3df3d2069
@@ -15,7 +15,7 @@ use vectorcraft_doc::{Appearance, AppearanceItem, Dash, Document, FillLayer, Lin
|
||||
use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect, Vec2};
|
||||
|
||||
use super::interp::{Interp, MAX_GSAVE, matrix_obj};
|
||||
use super::obj::{DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err};
|
||||
use super::obj::{DictRef, Key, Obj, Op, PsError, Res, Shared, clamp_between, ps_err};
|
||||
|
||||
/// Most objects one file makes.
|
||||
const MAX_NODES: usize = 1 << 20;
|
||||
@@ -1192,7 +1192,7 @@ impl Interp<'_> {
|
||||
let domain = nums(get("Domain"));
|
||||
let t = x.first().copied().unwrap_or(0.0);
|
||||
let t = match domain.as_slice() {
|
||||
[a, b, ..] => t.clamp(a.min(*b), a.max(*b)),
|
||||
[a, b, ..] => clamp_between(t, *a, *b),
|
||||
_ => t,
|
||||
};
|
||||
match get("FunctionType").and_then(|o| o.as_num()) {
|
||||
|
||||
@@ -567,11 +567,15 @@ impl<'a> Interp<'a> {
|
||||
None => {
|
||||
let (x, y) =
|
||||
(a.as_num().ok_or(PsError::Ps("typecheck", String::new()))?, b.as_num().ok_or(PsError::Ps("typecheck", String::new()))?);
|
||||
Obj::Real(match op {
|
||||
let r = match op {
|
||||
Add => x + y,
|
||||
Sub => x - y,
|
||||
_ => x * y,
|
||||
})
|
||||
};
|
||||
if !r.is_finite() {
|
||||
return ps_err("undefinedresult", "");
|
||||
}
|
||||
Obj::Real(r)
|
||||
}
|
||||
};
|
||||
self.push(r)?;
|
||||
|
||||
@@ -231,11 +231,11 @@ impl Obj {
|
||||
Self::Dict(DictRef::new(d))
|
||||
}
|
||||
|
||||
/// The number this is (integers and reals).
|
||||
/// The number this is (integers and finite reals: a NaN or infinite real is not a number).
|
||||
pub fn as_num(&self) -> Option<f64> {
|
||||
match *self {
|
||||
Self::Int(i) => Some(i as f64),
|
||||
Self::Real(r) => Some(r),
|
||||
Self::Real(r) if r.is_finite() => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -406,6 +406,13 @@ pub(crate) enum PsError {
|
||||
|
||||
pub(crate) type Res<T = ()> = Result<T, PsError>;
|
||||
|
||||
/// `v` limited to the range between `a` and `b` (in either order). Unlike `f64::clamp` this never
|
||||
/// panics: when one bound is NaN the range collapses to the other bound (both NaN gives `v`), and a
|
||||
/// NaN `v` gives the lower bound.
|
||||
pub(crate) fn clamp_between(v: f64, a: f64, b: f64) -> f64 {
|
||||
v.max(a.min(b)).min(a.max(b))
|
||||
}
|
||||
|
||||
/// A PostScript error raised at `at`.
|
||||
pub(crate) fn ps_err<T>(name: &'static str, at: &str) -> Res<T> {
|
||||
Err(PsError::Ps(name, at.to_string()))
|
||||
|
||||
@@ -12,7 +12,7 @@ use vectorcraft_geom::{Affine, Point};
|
||||
|
||||
use super::graphics::Space;
|
||||
use super::interp::{Interp, matrix_of};
|
||||
use super::obj::{DictRef, Key, Obj, PsError, Res, ps_err};
|
||||
use super::obj::{DictRef, Key, Obj, PsError, Res, clamp_between, ps_err};
|
||||
|
||||
/// Most patches (or triangles) one shading makes: past it, it is left out with a warning.
|
||||
const MAX_PATCHES: usize = 4096;
|
||||
@@ -348,7 +348,7 @@ impl Interp<'_> {
|
||||
decode.get(2 * j + 1).or(range.get(2 * j + 1)).copied().unwrap_or(1.0),
|
||||
);
|
||||
let (r0, r1) = (range.get(2 * j).copied().unwrap_or(0.0), range.get(2 * j + 1).copied().unwrap_or(1.0));
|
||||
(d0 + raw * (d1 - d0) / max).clamp(r0.min(r1), r0.max(r1))
|
||||
clamp_between(d0 + raw * (d1 - d0) / max, r0, r1)
|
||||
};
|
||||
// Each input into sample space.
|
||||
let mut pos = Vec::with_capacity(m);
|
||||
@@ -356,9 +356,9 @@ impl Interp<'_> {
|
||||
let (a, b) = (domain.get(2 * i).copied().unwrap_or(0.0), domain.get(2 * i + 1).copied().unwrap_or(1.0));
|
||||
let s = size.get(i).copied().unwrap_or(1.0);
|
||||
let (e0, e1) = (encode.get(2 * i).copied().unwrap_or(0.0), encode.get(2 * i + 1).copied().unwrap_or(s - 1.0));
|
||||
let xi = x.get(i).copied().unwrap_or(a).clamp(a.min(b), a.max(b));
|
||||
let xi = clamp_between(x.get(i).copied().unwrap_or(a), a, b);
|
||||
let e = if b != a { e0 + (xi - a) * (e1 - e0) / (b - a) } else { e0 };
|
||||
pos.push(e.clamp(0.0, s - 1.0));
|
||||
pos.push(clamp_between(e, 0.0, s - 1.0));
|
||||
}
|
||||
if let ([p], Some(s)) = (pos.as_slice(), size.first()) {
|
||||
let (lo, hi) = (p.floor() as usize, (p.ceil() as usize).min(*s as usize - 1));
|
||||
|
||||
@@ -324,6 +324,36 @@ fn shadings_become_gradients() {
|
||||
assert!(near(stops[1].1.to_rgb(), blue.to_rgb()), "{stops:?}");
|
||||
}
|
||||
|
||||
/// #3585: arithmetic that overflows to infinity or NaN is an `undefinedresult` error, so a
|
||||
/// non-finite number never reaches a shading function's `Domain` (where it used to panic).
|
||||
#[test]
|
||||
fn non_finite_arithmetic_is_undefinedresult() {
|
||||
let errs = |body: &str| {
|
||||
let r = read(&format!("0 0 10 10 rectfill {body}"));
|
||||
assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings);
|
||||
r.warnings.iter().any(|w| w.contains("undefinedresult"))
|
||||
};
|
||||
for body in [
|
||||
"1.7e308 1.7e308 add pop",
|
||||
"-1.7e308 1.7e308 sub pop",
|
||||
"1e300 1e300 mul pop",
|
||||
"-1 ln pop",
|
||||
"0 ln pop",
|
||||
"-1 log pop",
|
||||
"-1 sqrt pop",
|
||||
"10 400 exp pop",
|
||||
] {
|
||||
assert!(errs(body), "{body}");
|
||||
}
|
||||
let axial =
|
||||
"<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function << /FunctionType 2 /Domain [n n] /C0 [1 0 0] /C1 [0 0 1] /N 1 >> >>";
|
||||
assert!(errs(&format!("/n 1e300 1e300 mul dup sub def 0 0 100 100 rectclip {axial} shfill")));
|
||||
// A sampled function with a Size below 1 is a rangecheck, not a panic.
|
||||
let sampled = "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function << /FunctionType 0 /Domain [0 1] /Range [0 1 0 1 0 1] /Size [0.5] /BitsPerSample 8 /DataSource (abc) >> >>";
|
||||
let r = read(&format!("0 0 10 10 rectfill gsave 0 0 100 100 rectclip {sampled} shfill grestore"));
|
||||
assert!(!objects(&r.document).is_empty(), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// Tiling patterns and spot inks are found again through a hash of what tells them apart, so a
|
||||
/// program that makes 200,000 patterns and 200,001 inks reads within [`read_soon`]'s 20 seconds.
|
||||
/// Patterns are named Pattern 1, Pattern 2 and so on in the order they are made. A pattern made
|
||||
|
||||
Reference in New Issue
Block a user