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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,15 @@ Entries land here as they merge.
- **`ChartStyle.paintForSeries` rejects a negative series index** with a
value-naming `IllegalArgumentException` instead of leaking a bare
`IndexOutOfBoundsException` from the palette modulo.
- **A translucent gradient stop is rejected instead of silently rendering
opaque.** Gradients render through PDF axial / radial shadings, which carry
no alpha channel, so `PdfShadingSupport` dropped a stop colour's alpha and a
translucent stop rendered fully opaque with no diagnostic. `DocumentPaint.Stop`
now rejects a colour with alpha below 255 at construction, naming the offending
alpha — flatten the transparency into the stop colour, or apply opacity to the
whole shape. This matches the SVG reader, which already refuses `stop-opacity`,
and reaches the `DocumentPaint.linear(from, to)` sugar too. Opaque gradients are
unaffected.
- **SVG path reader no longer hangs on malformed `d` data.** A `Z`/`z`
close command (which consumes no operands) followed by a stray
non-command token — e.g. `"M0 0 Z5"` — made the scanner loop forever,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,18 +223,35 @@ private static void requireFinite(double value, String what) {
/**
* One gradient colour stop.
*
* <p>The colour must be fully opaque. Gradients render through PDF axial /
* radial shadings, which carry no alpha channel, so a translucent stop
* would silently render opaque. Rather than render wrong, a stop with
* alpha below 255 is rejected at construction — flatten transparency into
* the stop colour, or apply opacity to the whole shape instead. This
* mirrors the SVG reader, which already refuses {@code stop-opacity}.
*
* @param offset position along the gradient axis in [0,1]
* @param color colour at this offset
* @param color fully-opaque colour at this offset
*/
record Stop(double offset, DocumentColor color) {
/**
* Validates the offset and colour.
* Validates the offset and the opaque-colour requirement.
*
* @throws IllegalArgumentException if {@code offset} is outside [0,1]
* or {@code color} is translucent
*/
public Stop {
if (offset < 0 || offset > 1 || Double.isNaN(offset)) {
throw new IllegalArgumentException("stop offset must be in [0,1]: " + offset);
}
Objects.requireNonNull(color, "color");
int alpha = color.color().getAlpha();
if (alpha != 255) {
throw new IllegalArgumentException(
"gradient stop colour must be opaque (alpha=" + alpha + "); shadings "
+ "carry no alpha, so a translucent stop would render opaque — "
+ "flatten transparency into the colour or apply opacity to the shape");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.demcha.compose.document.style;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* Value-level contract of {@link DocumentPaint.Stop}: the offset bounds and the
* opaque-colour requirement. Gradients render through PDF axial / radial
* shadings, which carry no alpha channel, so a translucent stop must fail at
* construction instead of silently rendering opaque — mirroring the SVG
* reader, which already refuses {@code stop-opacity}.
*/
class DocumentPaintTest {

@Test
void opaqueStopIsAccepted() {
assertThatCode(() -> new DocumentPaint.Stop(0.5, DocumentColor.rgb(20, 80, 95)))
.doesNotThrowAnyException();
// rgba with full alpha is still opaque.
assertThatCode(() -> new DocumentPaint.Stop(0.0, DocumentColor.rgba(10, 20, 30, 255)))
.doesNotThrowAnyException();
}

@Test
void translucentStopIsRejected() {
assertThatThrownBy(() -> new DocumentPaint.Stop(0.0, DocumentColor.rgb(20, 80, 95).withOpacity(0.5)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("opaque");
assertThatThrownBy(() -> new DocumentPaint.Stop(1.0, DocumentColor.rgba(10, 20, 30, 0)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("opaque");
}

@Test
void offsetOutsideUnitRangeIsRejected() {
assertThatThrownBy(() -> new DocumentPaint.Stop(-0.1, DocumentColor.BLACK))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("offset");
assertThatThrownBy(() -> new DocumentPaint.Stop(1.5, DocumentColor.BLACK))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("offset");
}

@Test
void linearFactoryRejectsTranslucentEndpoints() {
// The two-colour sugar builds stops, so the opaque guard reaches it too.
assertThatThrownBy(() -> DocumentPaint.linear(
DocumentColor.rgb(20, 80, 95).withOpacity(0.4), DocumentColor.WHITE))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("opaque");
}

@Test
void solidPaintExposesItsColor() {
DocumentColor teal = DocumentColor.rgb(20, 80, 95);
assertThat(DocumentPaint.solid(teal).primaryColor()).isEqualTo(teal);
}
}