From 3200366a583688dd3fd32a728cb73b987152c086 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 13 Jun 2026 14:27:41 +0100 Subject: [PATCH] fix(svg): gradient number errors read in the reader's house style SvgGradients coordinate/radius/stop parsing now goes through one number() helper that throws ' must be a number, got ' with the cause chained, matching the rest of the beta SVG reader instead of leaking the raw JDK NumberFormatException. Adds a negative test + CHANGELOG entry. --- CHANGELOG.md | 7 +++++ .../compose/document/svg/SvgGradients.java | 27 ++++++++++++++----- .../compose/document/svg/SvgIconTest.java | 19 +++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07796361..45c7194e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -223,6 +223,13 @@ Entries land here as they merge. `ListMarker.defaultForDepth(int)` and `ListMarker.normalizeItemText(String, boolean)` (`@since 1.8.0`) — and the fixed-layout pipeline and the DOCX export both call them. +- **SVG gradient number errors read in the reader's house style.** A + non-numeric gradient coordinate, radius, or stop value (e.g. `x1="abc"`, + `r="x%"`, `offset="?"`) leaked the raw JDK `NumberFormatException` + ("For input string: …") as the reason. `SvgGradients` now parses through one + shared helper that throws `" must be a number, got '…'"` with the + cause chained — matching the rest of the beta SVG reader, where the + per-element wrapper already names the referencing element. ### Documentation diff --git a/src/main/java/com/demcha/compose/document/svg/SvgGradients.java b/src/main/java/com/demcha/compose/document/svg/SvgGradients.java index ddc8640a..b5d04844 100644 --- a/src/main/java/com/demcha/compose/document/svg/SvgGradients.java +++ b/src/main/java/com/demcha/compose/document/svg/SvgGradients.java @@ -216,10 +216,10 @@ private static double coordinate(String value, double defaultFraction, return userSpace ? defaultFraction * viewportSize : defaultFraction; } if (v.endsWith("%")) { - double fraction = Double.parseDouble(v.substring(0, v.length() - 1)) / 100.0; + double fraction = number(v.substring(0, v.length() - 1), "gradient coordinate") / 100.0; return userSpace ? fraction * viewportSize : fraction; } - return Double.parseDouble(v); + return number(v, "gradient coordinate"); } private static double length(Element gradient, String attr, double defaultFraction, @@ -231,10 +231,10 @@ private static double length(Element gradient, String attr, double defaultFracti return userSpace ? defaultFraction * diagonal : defaultFraction; } if (v.endsWith("%")) { - double fraction = Double.parseDouble(v.substring(0, v.length() - 1)) / 100.0; + double fraction = number(v.substring(0, v.length() - 1), "gradient radius") / 100.0; return userSpace ? fraction * diagonal : fraction; } - return Double.parseDouble(v); + return number(v, "gradient radius"); } /** @@ -321,9 +321,24 @@ private static double fraction(String value, double defaultValue) { } String v = value.trim(); if (v.endsWith("%")) { - return Double.parseDouble(v.substring(0, v.length() - 1)) / 100.0; + return number(v.substring(0, v.length() - 1), "gradient stop value") / 100.0; + } + return number(v, "gradient stop value"); + } + + /** + * Parses a gradient numeric value, naming what failed and the offending + * input instead of leaking the raw {@link NumberFormatException} message + * ("For input string: …"). The cause is chained, and the referencing + * element is added by the reader's per-element error wrapper. + */ + private static double number(String value, String what) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + what + " must be a number, got '" + value + "'", e); } - return Double.parseDouble(v); } private static String attrOrStyle(Element element, String property) { diff --git a/src/test/java/com/demcha/compose/document/svg/SvgIconTest.java b/src/test/java/com/demcha/compose/document/svg/SvgIconTest.java index 87620be6..562b8fc8 100644 --- a/src/test/java/com/demcha/compose/document/svg/SvgIconTest.java +++ b/src/test/java/com/demcha/compose/document/svg/SvgIconTest.java @@ -364,6 +364,25 @@ void gradientCornersWithoutAPdfAnalogueFailLoudly() { .hasMessageContaining("focal"); } + @Test + void malformedGradientNumberSaysWhatExpectedANumber() { + // A non-numeric gradient coordinate must read in the reader's house + // style (named + reason) instead of leaking a bare JDK parse message. + assertThatThrownBy(() -> SvgIcon.parse(""" + + + + + + + + + """)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must be a number") + .hasMessageContaining("abc"); + } + @Test void nodeFormPackagesLayersAtTheRequestedWidth() { SvgIcon icon = SvgIcon.parse("""