-
Notifications
You must be signed in to change notification settings - Fork 105
Description
Description
On macOS, rendering certain SVGs causes a crash with NSInvalidArgumentException ("illegal angle argument") when NSBezierPath.appendArc(withCenter:radius:startAngle:endAngle:clockwise:) receives NaN or inf values.
The root cause is in the SVG arc endpoint-to-center parameterization (as per W3C SVG spec) inside SVGPathReader.swift. Certain arc parameters produce degenerate geometry during the mathematical conversion, resulting in non-finite values that propagate to NSBezierPath.
On iOS this goes unnoticed because UIBezierPath silently tolerates invalid parameters. On macOS, NSBezierPath raises an exception.
How to reproduce
Any SVG containing arc commands (A/a) where:
- The radii are very small relative to the distance between endpoints
- Start and end points coincide or nearly coincide
- The denominator
rx²·y1'² + ry²·x1'²approaches zero
When rendered on macOS via SVGView, the app crashes.
Root cause
In SVGPathReader.swift, the A() function computes:
let denominator = rx * rx * y1_ * y1_ + ry * ry * x1_ * x1_
// If denominator ≈ 0, the division below produces NaN/inf:
let underroot = (rx * rx * ry * ry - rx * rx * y1_ * y1_ - ry * ry * x1_ * x1_) / denominatorThis cascades through cx_, cy_, cx, cy, angles t1 and delta, eventually reaching NSBezierPath.appendArc(...) with non-finite angles.
Additionally, in MBezierPath+Extension_macOS.swift, the addArc(withCenter:...) method converts radians to degrees without validating that the converted values are finite.
Proposed fix
Add guard clauses that validate intermediate values are finite. When an arc is degenerate, skip it by moving the current point to the endpoint without drawing (moveTo), rather than drawing a straight line which would introduce visual artifacts. This also keeps the path's current point in the correct position for subsequent commands.
SVGPathReader.swift — function A()
let denominator = rx * rx * y1_ * y1_ + ry * ry * x1_ * x1_
guard denominator.isFinite, denominator > .leastNonzeroMagnitude else {
bezierPath.move(to: CGPoint(x: x, y: y))
setPoint(CGPoint(x: x, y: y))
return
}
// ... after computing cx, cy:
guard areFinite(rx, ry, cx_, cy_, cx, cy) else {
bezierPath.move(to: CGPoint(x: x, y: y))
setPoint(CGPoint(x: x, y: y))
return
}
// ... when E() fails:
if E(...) {
setPoint(CGPoint(x: CGFloat(x), y: CGFloat(y)))
} else {
bezierPath.move(to: CGPoint(x: x, y: y))
setPoint(CGPoint(x: x, y: y))
}SVGPathReader.swift — function E()
Change return type to Bool and add validation:
@discardableResult
func E(...) -> Bool {
guard areFinite(x, y, w, h, startAngle, arcAngle, rotation), w > 0, h > 0 else { return false }
let end = extent + CGFloat(arcAngle)
guard end.isFinite else { return false }
guard areFinite(cx, cy) else { return false }
// ...
return true
}MBezierPath+Extension_macOS.swift — function addArc()
func addArc(withCenter:...) {
guard withCenter.x.isFinite, withCenter.y.isFinite,
radius.isFinite, radius > 0,
startAngle.isFinite, endAngle.isFinite else { return }
let startAngleRadian = startAngle * (180.0 / .pi)
let endAngleRadian = endAngle * (180.0 / .pi)
guard startAngleRadian.isFinite, endAngleRadian.isFinite else { return }
// ...
}Helper
func areFinite(_ values: CGFloat...) -> Bool {
values.allSatisfy { $0.isFinite }
}Environment
- macOS 15+ / Xcode 26
- SVGView 1.0.6
- Crash does not occur on iOS (
UIBezierPathtolerates invalid parameters)