Skip to content

Commit c04a34e

Browse files
committed
fix(svg): also drop single-stop translucent gradient overlays
isAlphaOnlyOverlay required two or more stops, so a one-stop translucent same-colour gradient — a flat alpha fill, which paint() expands to an opaque flat fill — still blotted the art beneath it. Treat a single translucent stop as an overlay too. Also document that nested clip-paths take the innermost shape (no intersection): this is exact for the Noto set (no glyph nests a different clip) and any residual overflow stays bounded by the inline viewBox clip.
1 parent 5669dc9 commit c04a34e

3 files changed

Lines changed: 25 additions & 3 deletions

File tree

src/main/java/com/demcha/compose/document/svg/SvgGradients.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,11 @@ static boolean isAlphaOnlyOverlay(Element gradient, Map<String, Element> all) {
169169
stops = stopElements(target);
170170
}
171171
}
172-
if (stops.size() < 2) {
172+
if (stops.isEmpty()) {
173173
return false;
174174
}
175+
// A single translucent stop is a flat alpha fill — still an overlay we
176+
// cannot composite, so it is caught here too (not only two-stop fades).
175177
Integer rgb = null;
176178
boolean translucent = false;
177179
for (Element stop : stops) {

src/main/java/com/demcha/compose/document/svg/SvgIconReader.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,11 @@ private static void walk(Element element, double[] transform, Paint inherited, S
151151
paint = stylize(element, inherited, gradients);
152152
matrix = compose(transform, element.getAttribute("transform"));
153153
// A clip-path on this element (or group) clips it and its descendants
154-
// to the referenced shape; the innermost clip wins. Resolved in icon
155-
// space with the same matrix/box as the geometry it bounds.
154+
// to the referenced shape, resolved in icon space with the same
155+
// matrix/box as the geometry it bounds. Nested clips are not
156+
// intersected — the innermost wins; this is exact for the Noto set
157+
// (no glyph nests a different clip inside another) and any residual
158+
// overflow is still bounded by the inline viewBox clip at render.
156159
SvgPath ownClip = resolveClip(element, matrix, box, ids);
157160
if (ownClip != null) {
158161
activeClip = ownClip;

src/test/java/com/demcha/compose/document/svg/SvgIconTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,23 @@ void monochromeTranslucentGradientOverlayIsDropped() {
452452
assertThat(icon.layers().get(0).fill().color()).isEqualTo(new java.awt.Color(0, 255, 0));
453453
}
454454

455+
@Test
456+
void singleStopTranslucentGradientOverlayIsDropped() {
457+
// A one-stop translucent gradient is a flat alpha fill — still an overlay
458+
// we cannot composite, so it is dropped too (not only two-stop fades).
459+
SvgIcon icon = SvgIcon.parse("""
460+
<svg viewBox="0 0 10 10">
461+
<radialGradient id="g">
462+
<stop offset="0" style="stop-color:#000000;stop-opacity:0.2"/>
463+
</radialGradient>
464+
<path d="M0 0 H10 V10 Z" fill="#00ff00"/>
465+
<path d="M0 0 H10 V10 Z" fill="url(#g)"/>
466+
</svg>
467+
""");
468+
assertThat(icon.layers()).hasSize(1);
469+
assertThat(icon.layers().get(0).fill().color()).isEqualTo(new java.awt.Color(0, 255, 0));
470+
}
471+
455472
@Test
456473
void multiColourTranslucentGradientStillRenders() {
457474
// A real colour transition (red→blue) is structural even with a

0 commit comments

Comments
 (0)