EPS: arc and arcn with huge angles no longer hang the import
arc raised its second angle by 2π in a loop until it was no longer below the first, and arcn lowered it the same way. For an angle of 1e18 degrees that loop takes about 3e15 steps: vectorcraft-cli info was still reading an EPS file with "newpath 100 100 50 1e18 0 arc stroke" when "timeout 20" stopped it. For 1e308 the loop never ends: from 2^56 radians (about 4.1e18 degrees) up, adding 2π leaves a double unchanged. The import fuzz test eps_hostile_programs_never_panic builds programs from tokens that include arc, arcn, 1e308 and 1e15. arc now raises the second angle by multiples of 360 until it is at least the first, and arcn lowers it until it is at most the first, as the PostScript Language Reference defines them. The code works out both sweeps from the remainders of the two angles after whole turns, and it places the first endpoint at the remainder of the first angle. Ordinary angles give the same arcs as before, apart from rounding in the last digits. One case changes: when raising (arc) or lowering (arcn) the second angle by whole turns makes it equal to the first, as in 60 -300 arc or -60 300 arcn, rounding sometimes made the old loop add another turn and draw a full circle. Those arcs are now empty, as the PostScript Language Reference defines them. Tests: 1e18 0 arc, 1e308 0 arc, 0 1e18 arcn and 0 1e308 arcn draw the arc between the angles' remainders and leave the current point at the second angle. The test reads each program on a thread of its own and fails when no result arrives within 20 seconds.
This commit is contained in:
parent
b19731b307
commit
2d95ac1694
|
|
@ -339,6 +339,12 @@ fn turn(a: f64, b: f64) -> f64 {
|
|||
if d > std::f64::consts::PI { d - std::f64::consts::TAU } else { d }
|
||||
}
|
||||
|
||||
/// The angle from `a` counterclockwise to `b` (degrees, 0 to 360), worked out from what is left
|
||||
/// of each after whole turns.
|
||||
fn ccw_turn(a: f64, b: f64) -> f64 {
|
||||
(b % 360.0 - a % 360.0).rem_euclid(360.0)
|
||||
}
|
||||
|
||||
impl Interp<'_> {
|
||||
/// User space → document space.
|
||||
pub fn xf(&self) -> Affine {
|
||||
|
|
@ -436,18 +442,19 @@ impl Interp<'_> {
|
|||
|
||||
/// `arc` / `arcn`: from angle `a1` to `a2` (degrees, counter-clockwise or clockwise).
|
||||
fn arc(&mut self, c: Point, r: f64, a1: f64, a2: f64, ccw: bool) -> Res {
|
||||
let (a1, mut a2) = (a1.to_radians(), a2.to_radians());
|
||||
let tau = std::f64::consts::TAU;
|
||||
if ccw {
|
||||
while a2 < a1 {
|
||||
a2 += tau;
|
||||
}
|
||||
// `arc` raises `a2` by multiples of 360 until it is at least `a1`, and `arcn` lowers it
|
||||
// until it is at most `a1` (PLRM 3rd ed., `arc` and `arcn` in chapter 8). Those sweeps,
|
||||
// and the first endpoint, are worked out from what is left of each angle after whole turns.
|
||||
let sweep = if ccw && a2 < a1 {
|
||||
ccw_turn(a1, a2)
|
||||
} else if !ccw && a2 > a1 {
|
||||
-ccw_turn(a2, a1)
|
||||
} else {
|
||||
while a2 > a1 {
|
||||
a2 -= tau;
|
||||
}
|
||||
}
|
||||
let sweep = (a2 - a1).clamp(-tau * 4.0, tau * 4.0);
|
||||
a2 - a1
|
||||
};
|
||||
let tau = std::f64::consts::TAU;
|
||||
let sweep = sweep.to_radians().clamp(-tau * 4.0, tau * 4.0);
|
||||
let a1 = (a1 % 360.0).to_radians();
|
||||
let start = c + Vec2::new(r * a1.cos(), r * a1.sin());
|
||||
self.append_user(std::iter::once(PathEl::MoveTo(start)))?;
|
||||
let arc = kurbo::Arc::new(c, Vec2::new(r, r), a1, sweep, 0.0);
|
||||
|
|
|
|||
|
|
@ -640,6 +640,42 @@ fn hostile_executable_files_and_type_loops_are_refused() {
|
|||
blue("currentfile cvx exec currentfile flushfile 0 0 1 setrgbcolor");
|
||||
}
|
||||
|
||||
/// Reads `body` as [`read`] does, on a thread of its own, and fails the test when reading takes
|
||||
/// more than 20 s.
|
||||
fn read_soon(body: &str) -> crate::Imported {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let ps = eps(body);
|
||||
std::thread::spawn(move || tx.send(import(&ps)).ok());
|
||||
rx.recv_timeout(std::time::Duration::from_secs(20)).unwrap_or_else(|_| panic!("reading {body} did not finish")).unwrap()
|
||||
}
|
||||
|
||||
/// `arc` raises its second angle by multiples of 360 until it is at least the first, and `arcn`
|
||||
/// lowers it until it is at most the first (PLRM 3rd ed., `arc` and `arcn` in chapter 8), however
|
||||
/// large the angles: the arc runs between the angles' remainders after whole turns and ends at
|
||||
/// the second angle, the new current point.
|
||||
#[test]
|
||||
fn arcs_with_huge_angles_end_at_their_second_angle() {
|
||||
// `at(deg)` is the point at `deg` degrees on the circle of radius 10 around (50, 50), in
|
||||
// document space.
|
||||
let at = |deg: f64| Point::new(50.0 + 10.0 * deg.to_radians().cos(), 50.0 - 10.0 * deg.to_radians().sin());
|
||||
// Each program is listed with the remainders of its first and second angles after whole turns.
|
||||
for (body, first, second) in [
|
||||
("50 50 10 1e18 0 arc", 280.0, 0.0),
|
||||
("50 50 10 1e308 0 arc", 296.0, 0.0),
|
||||
("50 50 10 0 1e18 arcn", 0.0, 280.0),
|
||||
("50 50 10 0 1e308 arcn", 0.0, 296.0),
|
||||
] {
|
||||
let r = read_soon(&format!("newpath {body} currentpoint stroke 1 1 rectfill"));
|
||||
let o = objects(&r.document);
|
||||
assert_eq!(o.len(), 2, "{body}: {:?}", r.warnings);
|
||||
// The arc lies within one quarter of the circle, so its endpoints span its bounds.
|
||||
let (a, b) = (at(first), at(second));
|
||||
assert!(near(bounds(&o[0]), Rect::from_points(a, b)), "{body}: {:?}", bounds(&o[0]));
|
||||
// The second object is the square drawn at the current point.
|
||||
assert!(near(bounds(&o[1]), Rect::new(b.x, b.y - 1.0, b.x + 1.0, b.y)), "{body}: {:?}", bounds(&o[1]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cshow_runs_its_procedure_for_each_character() {
|
||||
check("/n 0 def { pop pop pop /n n 1 add def } (abc) cshow n 3 eq");
|
||||
|
|
|
|||
Loading…
Reference in New Issue