Skip to content

feat(svg): stroke fidelity + colour/unit hardening for the beta SVG reader#180

Merged
DemchaAV merged 1 commit into
developfrom
feat/svg-beta-hardening
Jun 13, 2026
Merged

feat(svg): stroke fidelity + colour/unit hardening for the beta SVG reader#180
DemchaAV merged 1 commit into
developfrom
feat/svg-beta-hardening

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Hardening pass that makes the beta SVG surface render real-world exporter output faithfully — including the GraphCompose brand logos at any size.

Stroke fidelity

  • stroke-linecap / stroke-linejoin → native PDF J / j operators via new DocumentLineCap / DocumentLineJoin (also on PathBuilder.lineCap() / lineJoin()). Emitted only when non-default, so default-styled output is byte-identical (negative test guards it).
  • stroke-dasharray honoured (odd lists doubled, all-zero = solid).
  • Bug fix: SvgIcon#node(width) now scales stroke width and dash lengths by width/sourceWidth — they live in SVG user units. Icons drawn below source size previously rendered over-thick outlines (visible on the brand mark at 48 pt).

Colour & unit grammar (new SvgColors / SvgStyles)

  • 147 CSS named colours, rgb() / rgba() with numbers or percentages, #rgb / #rgba / #rrggbb / #rrggbbaa hex, absolute units (px/pt/pc/in/mm/cm) on stroke widths.
  • Unknown colours and relative units (em, %) fail with the supported set listed — never a wrong render.

Loud skips (no more silent content loss)

  • The reader logs one deduplicated warning per dropped element kind (text, image, use, masks, clips, filters) instead of silently.
  • DocxSemanticBackend warns once per geometry-only node kind it drops; LayerStackNode / CanvasLayerNode now recurse so their semantic child text survives the Word export.

Verification

  • ./mvnw verify -pl .BUILD SUCCESS, +22 tests (SvgStylesTest, PdfPathStrokeStyleTest J/j operator assertions, DocxGeometryDropTest, plus SVG-level cases in SvgIconTest / PathBuilderTest).
  • All four assets/*.svg brand logos render 1:1 at any size; the three marks now share a uniform relative stroke thickness (the scaling fix) with rounded caps/joins.
  • VectorPathExample gains a butt/round/square caps & joins demo; vector-path.pdf preview refreshed.

Senior-reviewed (six-lane) before commit; one consistency fix applied (CanvasLayerNode recursion). Independent of any other branch — clean off develop.

Next planned SVG feature: ShapeOutline.path(...) clipper.

…eader

- stroke-linecap/linejoin → native PDF J/j operators (new DocumentLineCap/
  DocumentLineJoin, PathBuilder.lineCap()/lineJoin()); emitted only when
  non-default so default-styled output stays byte-identical
- stroke-dasharray honoured; 147 CSS named colours, rgb()/rgba() with
  numbers or percentages, #rgb/#rgba/#rrggbb/#rrggbbaa hex, absolute length
  units (px/pt/pc/in/mm/cm) on stroke widths — all in new SvgColors/SvgStyles
- fix: SvgIcon#node(width) now scales stroke width AND dash lengths by
  width/sourceWidth (they live in user units) — icons drawn below source
  size no longer render over-thick outlines
- loud skips: one deduplicated warn-log per dropped kind in the reader
  (text/image/use/...) and in DocxSemanticBackend (geometry-only nodes);
  LayerStackNode/CanvasLayerNode now recurse so child text survives DOCX
- unknown colours / relative units fail with the supported set listed
- VectorPathExample gains a caps & joins demo; +22 tests
int colon = entry.indexOf(':');
String hex = entry.substring(colon + 1);
map.put(entry.substring(0, colon), DocumentColor.rgb(
Integer.parseInt(hex.substring(0, 2), 16),
String hex = entry.substring(colon + 1);
map.put(entry.substring(0, colon), DocumentColor.rgb(
Integer.parseInt(hex.substring(0, 2), 16),
Integer.parseInt(hex.substring(2, 4), 16),
map.put(entry.substring(0, colon), DocumentColor.rgb(
Integer.parseInt(hex.substring(0, 2), 16),
Integer.parseInt(hex.substring(2, 4), 16),
Integer.parseInt(hex.substring(4, 6), 16)));
return null;
}
DocumentColor color = DocumentColor.rgb(
Integer.parseInt(expanded.substring(0, 2), 16),
}
DocumentColor color = DocumentColor.rgb(
Integer.parseInt(expanded.substring(0, 2), 16),
Integer.parseInt(expanded.substring(2, 4), 16),
Integer.parseInt(expanded.substring(2, 4), 16),
Integer.parseInt(expanded.substring(4, 6), 16));
if (expanded.length() == 8) {
color = color.withOpacity(Integer.parseInt(expanded.substring(6, 8), 16) / 255.0);
private static int channel(String value) {
String t = value.trim();
if (t.endsWith("%")) {
double pct = Math.max(0, Math.min(100, Double.parseDouble(t.substring(0, t.length() - 1))));
// pct/100*255 keeps 50% exact (127.5 → 128); pct*2.55 drifts to 127.
return (int) Math.round(pct / 100.0 * 255.0);
}
return Math.max(0, Math.min(255, (int) Math.round(Double.parseDouble(t))));
private static double alpha(String value) {
String t = value.trim();
double a = t.endsWith("%")
? Double.parseDouble(t.substring(0, t.length() - 1)) / 100.0
String t = value.trim();
double a = t.endsWith("%")
? Double.parseDouble(t.substring(0, t.length() - 1)) / 100.0
: Double.parseDouble(t);
@DemchaAV DemchaAV merged commit 41e2db0 into develop Jun 13, 2026
11 checks passed
@DemchaAV DemchaAV deleted the feat/svg-beta-hardening branch June 13, 2026 02:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants