Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,14 @@ Entries land here as they merge.

### Bug fixes

- **A stray non-drawing element no longer breaks a whole SVG icon.** A
visible-painted SVG element that lowers to a moveto-only or moveto+close
path — `d="M12 12"`, a zero-length arc, the stray subpaths real exporters
emit — drew no ink, yet a lone moveto threw at `SvgIcon#node(...)` (an empty
`PathNode`) and a moveto+close rendered blank. `SvgIconReader` now drops a
layer with no drawing segment, so one degenerate element no longer fails the
icon; an icon of only such elements still fails loudly with "no drawable
geometry".
- **Stacked bars anchor at zero even with an explicit positive axis minimum.**
A stacked bar chart with `valueAxis().min(positive)` lifted the baseline
while segment heights stayed measured from zero, so the stack overshot its
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ private static void emitLayer(Element element, String d, Paint paint,
return;
}
SvgPath geometry = SvgPath.parseTransformed(d, matrix, box[0], box[1], box[2], box[3]);
if (!geometry.hasDrawingSegment()) {
// A moveto-only or moveto+close element (e.g. d="M12 12", or a
// zero-length arc that lowers to a lone moveto) draws no ink. Drop
// it so one degenerate element a real-world exporter emitted does
// not fail the whole icon at SvgIcon#node(...).
return;
}

// Gradients resolve here, where the shape's geometry (the
// objectBoundingBox reference) and accumulated affine exist. The flat
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/demcha/compose/document/svg/SvgPath.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,25 @@ public double aspectRatio() {
return sourceWidth / sourceHeight;
}

/**
* Whether this path actually draws ink: it carries at least one
* {@code lineTo} / {@code cubicTo} after the leading {@code moveTo}. A
* moveto-only path, or a moveto followed only by {@code close} (which a
* degenerate SVG element such as {@code d="M12 12"} or a zero-length arc
* lowers to), draws nothing.
*
* @return {@code true} if a drawing segment is present
*/
boolean hasDrawingSegment() {
for (DocumentPathSegment segment : segments) {
if (segment instanceof DocumentPathSegment.LineTo
|| segment instanceof DocumentPathSegment.CubicTo) {
return true;
}
}
return false;
}

// ------------------------------------------------------------------
// Normalization (y-flip into the unit box)
// ------------------------------------------------------------------
Expand Down
29 changes: 29 additions & 0 deletions src/test/java/com/demcha/compose/document/svg/SvgIconTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,35 @@ void invisibleElementsProduceNoLayers() {
assertThat(icon.layers()).hasSize(1);
}

@Test
void degenerateGeometryIsDroppedSoOneStrayElementDoesNotFailTheIcon() {
// Real exporters emit stray moveto-only / moveto+close subpaths that
// draw nothing. They must not break the whole icon: a lone moveto used
// to throw at node() (an empty PathNode), a moveto+close rendered blank.
SvgIcon icon = SvgIcon.parse("""
<svg viewBox="0 0 10 10">
<path d="M5 5" fill="#000"/>
<path d="M5 5 Z" fill="#000"/>
<path d="M0 0 H10 V10 Z" fill="#f00"/>
</svg>
""");

// Only the drawable box survives; both degenerate paths are dropped.
assertThat(icon.layers()).hasSize(1);
assertThat(icon.node(24)).isNotNull(); // threw before the drop guard
}

@Test
void anIconOfOnlyDegenerateGeometryFailsLoudly() {
assertThatThrownBy(() -> SvgIcon.parse("""
<svg viewBox="0 0 10 10">
<path d="M5 5" fill="#000"/>
</svg>
"""))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("no drawable geometry");
}

@Test
void basicShapesLowerToPathGeometry() {
SvgIcon icon = SvgIcon.parse("""
Expand Down