diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a7cddcfe..4c11b5502 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,29 @@
All notable changes to GraphCompose are documented here. Versions
follow semantic versioning; release dates are ISO 8601.
+## v1.9.1 — Planned
+
+Table columns now contain long inline-code content instead of letting it spill
+over the next column.
+
+### Bug fixes
+
+- **Long inline-code chips no longer overflow their column.** A long
+ `inlineCode(...)` / `inlineHighlight(...)` token with no spaces — a package
+ coordinate, fully-qualified class name or URL — now breaks *within* its
+ paragraph or table cell instead of drawing over the neighbouring content. It
+ breaks at `.` `:` `/` `-` seams and char-splits only when a segment is still
+ too wide, keeping the rounded chip fill intact on every wrapped fragment.
+ Applies to all three paragraph wrap paths (plain, inline, markdown); text that
+ already fits is laid out exactly as before.
+
+- **`auto()` table columns grow to fit composed cell content.** A composed cell
+ (`DocumentTableCell.node(...)`) in an `auto()` column now contributes its
+ intrinsic content width, so the column sizes to the content (e.g. an
+ inline-code chip) instead of collapsing toward zero. `fixed(...)` columns are
+ unchanged. As with plain-text auto columns, a table too narrow for the summed
+ intrinsic width of its auto columns reports `exceeds available width`.
+
## v1.9.0 — 2026-06-29
In-document navigation. Rendered PDFs can now declare named **anchors** and
diff --git a/assets/readme/examples/inline-code-column-wrap.pdf b/assets/readme/examples/inline-code-column-wrap.pdf
new file mode 100644
index 000000000..c4f1ee395
Binary files /dev/null and b/assets/readme/examples/inline-code-column-wrap.pdf differ
diff --git a/examples/README.md b/examples/README.md
index e87e62dbf..a732b8aad 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -75,6 +75,7 @@ are with the canonical DSL, then jump to its detailed section below.
| [Section presets](#section-presets) | `pageBackground`, `band`, `softPanel`, `accentLeft / Right / Top / Bottom`, per-corner `DocumentCornerRadius` | [PDF](../assets/readme/examples/section-presets.pdf) · [Source](src/main/java/com/demcha/examples/features/text/SectionPresetsExample.java) |
| [Nested lists](#nested-lists-v16) | `ListBuilder.addItem(label, Consumer)` — depth cascade, per-depth markers, mixed flat / nested authoring | [PDF](../assets/readme/examples/nested-list-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/lists/NestedListExample.java) |
| [Composed table cells](#composed-table-cells-v16) | `DocumentTableCell.node(DocumentNode)` — paragraphs, lists, sub-tables inside cells with two-pass measurement | [PDF](../assets/readme/examples/composed-table-cell-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/ComposedTableCellExample.java) |
+| [Inline-code column wrap](#inline-code-column-wrap) | A long `inlineCode(...)` coordinate breaks at its `. : / -` seams inside a narrow **fixed** column and an **auto** column grows to fit it on one line | [PDF](../assets/readme/examples/inline-code-column-wrap.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/InlineCodeColumnWrapExample.java) |
| [Canvas layer (free placement)](#canvas-layer-v16) | `CanvasLayerNode` — pixel-precise `(x, y)` placement of children inside a fixed bounding box, with `ClipPolicy` clipping | [PDF](../assets/readme/examples/canvas-layer-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/canvas/CanvasLayerExample.java) |
| [Transforms](#transforms) | `rotate`, `scale`, and per-layer `zIndex` swap | [PDF](../assets/readme/examples/transforms.pdf) · [Source](src/main/java/com/demcha/examples/features/transforms/TransformsExample.java) |
| [Block alignment](#block-alignment) | `addAligned(align, node)` / `addSvgIcon(icon, w, align)` — seat any fixed-size node left / centre / right across the content width | [PDF](../assets/readme/examples/block-align.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/BlockAlignExample.java) |
@@ -577,6 +578,24 @@ table.columns(...)
[📄 View PDF](../assets/readme/examples/table-advanced.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/tables/TableAdvancedExample.java)
+### Inline-code column wrap
+
+A composed cell holding one long `inlineCode(...)` token — a Maven coordinate,
+fully-qualified class name or URL — stays inside its column. In a narrow
+**fixed** column the chip breaks at its `. : / -` seams (char-splitting only
+when a segment is still too wide), with the rounded fill intact on every
+fragment; in an **auto** column the column grows to fit the coordinate on one
+line instead of collapsing.
+
+```java
+DocumentTableCell.node(document.dsl().paragraph()
+ .inlineCode("org.junit.jupiter:junit-jupiter:5.10.2").build())
+// fixed(104) column -> wraps at seams; auto() column -> grows to one line
+```
+
+[📄 View PDF](../assets/readme/examples/inline-code-column-wrap.pdf) ·
+[📜 Full source](src/main/java/com/demcha/examples/features/tables/InlineCodeColumnWrapExample.java)
+
### Custom Business Theme
Build a `BusinessTheme` from raw `DocumentPalette` / `SpacingScale` /
diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
index 2a19aa455..3a57b1132 100644
--- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
+++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
@@ -24,6 +24,7 @@
import com.demcha.examples.features.streaming.HttpStreamingExample;
import com.demcha.examples.features.svg.SvgIconGalleryExample;
import com.demcha.examples.features.tables.ComposedTableCellExample;
+import com.demcha.examples.features.tables.InlineCodeColumnWrapExample;
import com.demcha.examples.features.tables.TableAdvancedExample;
import com.demcha.examples.features.text.EmojiGalleryExample;
import com.demcha.examples.features.text.EmojiShortcodeExample;
@@ -151,6 +152,7 @@ public static void main(String[] args) throws Exception {
// v1.6 layout primitives
System.out.println("Generated: " + NestedListExample.generate());
System.out.println("Generated: " + ComposedTableCellExample.generate());
+ System.out.println("Generated: " + InlineCodeColumnWrapExample.generate());
System.out.println("Generated: " + CanvasLayerExample.generate());
// v1.5 visual primitives
diff --git a/examples/src/main/java/com/demcha/examples/features/tables/InlineCodeColumnWrapExample.java b/examples/src/main/java/com/demcha/examples/features/tables/InlineCodeColumnWrapExample.java
new file mode 100644
index 000000000..37df23ee9
--- /dev/null
+++ b/examples/src/main/java/com/demcha/examples/features/tables/InlineCodeColumnWrapExample.java
@@ -0,0 +1,121 @@
+package com.demcha.examples.features.tables;
+
+import com.demcha.compose.GraphCompose;
+import com.demcha.compose.document.api.DocumentPageSize;
+import com.demcha.compose.document.api.DocumentSession;
+import com.demcha.compose.document.node.TableNode;
+import com.demcha.compose.document.style.DocumentColor;
+import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentStroke;
+import com.demcha.compose.document.style.DocumentTextDecoration;
+import com.demcha.compose.document.style.DocumentTextStyle;
+import com.demcha.compose.document.table.DocumentTableCell;
+import com.demcha.compose.document.table.DocumentTableColumn;
+import com.demcha.compose.document.table.DocumentTableStyle;
+import com.demcha.compose.font.FontName;
+import com.demcha.examples.support.ExampleOutputPaths;
+
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Runnable showcase for long inline-code coordinates inside table columns.
+ *
+ *
A composed cell holding a single long {@code inlineCode(...)} token — a
+ * Maven coordinate, fully-qualified class name or URL — no longer overflows its
+ * column. In a narrow fixed column the chip breaks inside the
+ * cell, preferring its {@code . : / -} seams and char-splitting only as a last
+ * resort, with the rounded fill intact on every fragment. In an auto
+ * column the column grows to fit the coordinate on one line instead of
+ * collapsing.
+ *
+ * @author Artem Demchyshyn
+ */
+public final class InlineCodeColumnWrapExample {
+
+ private static final DocumentColor INK = DocumentColor.rgb(34, 38, 50);
+ private static final DocumentColor MUTED = DocumentColor.rgb(102, 106, 118);
+ private static final DocumentColor RULE = DocumentColor.rgb(180, 188, 200);
+ private static final DocumentColor HEADER_FILL = DocumentColor.rgb(20, 60, 75);
+ private static final DocumentColor ROW_TINT = DocumentColor.rgb(244, 247, 252);
+
+ private static final List COORDINATES = List.of(
+ "org.junit.jupiter:junit-jupiter:5.10.2",
+ "io.github.demchaav:graph-compose:1.9.0",
+ "https://repo1.maven.org/maven2/");
+
+ private InlineCodeColumnWrapExample() {
+ }
+
+ public static Path generate() throws Exception {
+ Path outputFile = ExampleOutputPaths.prepare("features/tables", "inline-code-column-wrap.pdf");
+
+ DocumentTextStyle title = DocumentTextStyle.builder()
+ .fontName(FontName.HELVETICA_BOLD).size(20).color(INK)
+ .decoration(DocumentTextDecoration.BOLD).build();
+ DocumentTextStyle caption = DocumentTextStyle.builder()
+ .fontName(FontName.HELVETICA_OBLIQUE).size(10).color(MUTED).build();
+ DocumentTextStyle body = DocumentTextStyle.builder()
+ .fontName(FontName.HELVETICA).size(10.5).color(INK).build();
+ DocumentTextStyle headerText = DocumentTextStyle.builder()
+ .fontName(FontName.HELVETICA_BOLD).size(11).color(DocumentColor.WHITE)
+ .decoration(DocumentTextDecoration.BOLD).build();
+
+ DocumentTableStyle headerStyle = DocumentTableStyle.builder()
+ .fillColor(HEADER_FILL).stroke(DocumentStroke.of(RULE, 0.6))
+ .padding(DocumentInsets.of(8)).textStyle(headerText).build();
+ DocumentTableStyle bodyCellStyle = DocumentTableStyle.builder()
+ .stroke(DocumentStroke.of(RULE, 0.6)).padding(DocumentInsets.of(8)).textStyle(body).build();
+ DocumentTableStyle tintedCellStyle = DocumentTableStyle.builder()
+ .fillColor(ROW_TINT).stroke(DocumentStroke.of(RULE, 0.6))
+ .padding(DocumentInsets.of(8)).textStyle(body).build();
+
+ try (DocumentSession document = GraphCompose.document(outputFile)
+ .pageSize(DocumentPageSize.A4)
+ .margin(36, 36, 36, 36)
+ .create()) {
+
+ document.pageFlow()
+ .name("CodeWrapShowcase")
+ .spacing(8)
+ .addParagraph("Long inline-code coordinates stay inside the column", title)
+ .addParagraph(
+ "A composed cell with one long inlineCode(...) token used to spill over the "
+ + "next column. It now breaks at its . : / - seams inside a narrow FIXED "
+ + "column (char-splitting only when a segment is still too wide), and an "
+ + "AUTO column grows to fit it on one line.", caption)
+ .build();
+
+ List> rows = new java.util.ArrayList<>();
+ rows.add(List.of(
+ DocumentTableCell.text("Fixed 104pt").withStyle(headerStyle),
+ DocumentTableCell.text("Auto").withStyle(headerStyle)));
+ for (int i = 0; i < COORDINATES.size(); i++) {
+ String coordinate = COORDINATES.get(i);
+ DocumentTableStyle cellStyle = i % 2 == 0 ? bodyCellStyle : tintedCellStyle;
+ rows.add(List.of(
+ DocumentTableCell.node(document.dsl().paragraph().inlineCode(coordinate).build())
+ .withStyle(cellStyle),
+ DocumentTableCell.node(document.dsl().paragraph().inlineCode(coordinate).build())
+ .withStyle(cellStyle)));
+ }
+
+ document.add(new TableNode(
+ "CodeWrapTable",
+ List.of(DocumentTableColumn.fixed(104),
+ DocumentTableColumn.auto()),
+ rows,
+ bodyCellStyle,
+ null,
+ DocumentInsets.zero(),
+ DocumentInsets.zero()));
+
+ document.buildPdf();
+ }
+ return outputFile;
+ }
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("Generated: " + generate());
+ }
+}
diff --git a/src/main/java/com/demcha/compose/document/layout/TableLayoutSupport.java b/src/main/java/com/demcha/compose/document/layout/TableLayoutSupport.java
index 1529d0f55..93be462fb 100644
--- a/src/main/java/com/demcha/compose/document/layout/TableLayoutSupport.java
+++ b/src/main/java/com/demcha/compose/document/layout/TableLayoutSupport.java
@@ -47,12 +47,13 @@ static ResolvedTableLayoutWithContents resolveTableLayout(TableNode node,
List> logicalRows = buildLogicalRows(node, columnCount);
List normalizedSpecs = normalizeSpecs(node, columnCount);
TableCellLayoutStyle[][] stylesGrid = buildStylesGrid(node, logicalRows, columnCount);
- double[] naturalWidths = resolveNaturalColumnWidths(node, normalizedSpecs, logicalRows, stylesGrid, columnCount, measurement);
+ double innerAvailableWidth = Math.max(0.0, availableWidth - node.padding().horizontal());
+ double[] naturalWidths = resolveNaturalColumnWidths(node, normalizedSpecs, logicalRows, stylesGrid,
+ columnCount, measurement, prepareContext, innerAvailableWidth);
double naturalWidth = sum(naturalWidths);
double[] finalWidths = resolveFinalColumnWidths(node, normalizedSpecs, naturalWidths, naturalWidth);
double finalWidth = sum(finalWidths);
- double innerAvailableWidth = Math.max(0.0, availableWidth - node.padding().horizontal());
if (finalWidth > innerAvailableWidth + EPS) {
throw new IllegalStateException("Table '" + displayName(node) + "' width " + finalWidth
+ " exceeds available width " + innerAvailableWidth + ".");
@@ -488,7 +489,9 @@ private static double[] resolveNaturalColumnWidths(TableNode node,
List> logicalRows,
TableCellLayoutStyle[][] stylesGrid,
int columnCount,
- TextMeasurementSystem measurement) {
+ TextMeasurementSystem measurement,
+ PrepareContext prepareContext,
+ double innerAvailableWidth) {
double[] widths = new double[columnCount];
double[] singleCellRequired = new double[columnCount];
@@ -498,8 +501,12 @@ private static double[] resolveNaturalColumnWidths(TableNode node,
continue;
}
int col = logical.startColumn();
- singleCellRequired[col] = Math.max(singleCellRequired[col],
- cellNaturalWidth(logical.sanitizedLines(), stylesGrid[rowIndex][col], measurement));
+ double required = composedAutoCellNaturalWidth(logical, normalizedSpecs.get(col),
+ stylesGrid[rowIndex][col], prepareContext, innerAvailableWidth);
+ if (required < 0.0) {
+ required = cellNaturalWidth(logical.sanitizedLines(), stylesGrid[rowIndex][col], measurement);
+ }
+ singleCellRequired[col] = Math.max(singleCellRequired[col], required);
}
}
@@ -596,6 +603,35 @@ private static double[] resolveFinalColumnWidths(TableNode node,
return finalWidths;
}
+ /**
+ * Natural width contribution of a composed cell that sits in an AUTO column:
+ * the child's intrinsic content width plus the cell padding, so the AUTO
+ * column grows to fit composed content (e.g. an inline-code chip) instead of
+ * being sized to zero by its plain-text siblings. The child is measured
+ * against the table's inner width, so the result is already clamped to what
+ * the table can give — it never forces the table past its available width.
+ *
+ * Returns a negative sentinel when this is not a composed cell in an AUTO
+ * column, so the caller falls back to the plain-text {@link #cellNaturalWidth}
+ * formula. Composed cells in FIXED columns keep contributing nothing: the
+ * column width is fixed and an over-wide child wraps/breaks inside it.
+ */
+ private static double composedAutoCellNaturalWidth(LogicalCell logical,
+ TableColumnLayout spec,
+ TableCellLayoutStyle style,
+ PrepareContext prepareContext,
+ double innerAvailableWidth) {
+ DocumentTableCell src = logical.source();
+ if (src == null || !src.hasComposedContent() || !spec.isAuto() || prepareContext == null) {
+ return -1.0;
+ }
+ Padding padding = style.padding() == null ? Padding.zero() : style.padding();
+ double childInner = Math.max(0.0, innerAvailableWidth - padding.horizontal());
+ double natural = prepareContext.prepare(src.content(), BoxConstraints.natural(childInner))
+ .measureResult().width();
+ return natural + padding.horizontal();
+ }
+
private static double cellNaturalWidth(List sanitizedLines,
TableCellLayoutStyle style,
TextMeasurementSystem measurement) {
diff --git a/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java b/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java
index f18ec44da..583939518 100644
--- a/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java
+++ b/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java
@@ -827,32 +827,38 @@ private static List wrapParagraph(List logicalLines,
}
double nextTokenWidth = measurement.textWidth(style, nextToken);
- if (!hasContent || currentWidth + nextTokenWidth <= maxWidth + EPS) {
+ if (currentWidth + nextTokenWidth <= maxWidth + EPS) {
currentLine.append(nextToken);
currentWidth += nextTokenWidth;
hasContent = true;
continue;
}
- result.add(trimTrailingSpaces(currentLine.toString()));
- currentPrefix = continuationPrefix;
- currentLine.setLength(0);
- currentLine.append(continuationPrefix);
- currentWidth = measurement.textWidth(style, continuationPrefix);
- hasContent = false;
-
- double availableWidth = availableWidthForPrefix(maxWidth, currentPrefix, style, measurement);
+ // Does not fit. If the line already has content, flush it and retry
+ // the token on a fresh line before resorting to a break.
String strippedToken = nextToken.stripLeading();
double strippedTokenWidth = measurement.textWidth(style, strippedToken);
- if (currentWidth + strippedTokenWidth <= maxWidth + EPS) {
+ if (hasContent) {
+ result.add(trimTrailingSpaces(currentLine.toString()));
+ currentPrefix = continuationPrefix;
currentLine.setLength(0);
- currentLine.append(currentPrefix).append(strippedToken);
- currentWidth += strippedTokenWidth;
- hasContent = true;
- continue;
+ currentLine.append(continuationPrefix);
+ currentWidth = measurement.textWidth(style, continuationPrefix);
+ hasContent = false;
+
+ if (currentWidth + strippedTokenWidth <= maxWidth + EPS) {
+ currentLine.setLength(0);
+ currentLine.append(currentPrefix).append(strippedToken);
+ currentWidth += strippedTokenWidth;
+ hasContent = true;
+ continue;
+ }
}
- List chunks = splitLongToken(strippedToken, style, availableWidth, measurement);
+ // Over-wide on a fresh (or already empty) line: break it at soft seams,
+ // char-splitting as a last resort.
+ double availableWidth = availableWidthForPrefix(maxWidth, currentPrefix, style, measurement);
+ List chunks = breakLongToken(strippedToken, style, availableWidth, measurement);
if (chunks.isEmpty()) {
continue;
}
@@ -938,62 +944,81 @@ private static List wrapInlineParagraph(List runs,
}
double tokenWidth = sanitizedToken.wrapWidth();
- if (currentLine.isEmpty() || currentWidth + tokenWidth <= maxWidth + EPS) {
+ if (currentWidth + tokenWidth <= maxWidth + EPS) {
currentLine.add(sanitizedToken);
currentWidth += tokenWidth;
continue;
}
+ // Does not fit. If the line already has content, flush it and retry
+ // the token on a fresh line before resorting to a break.
if (!currentLine.isEmpty()) {
result.add(toInlineParagraphLine(currentLine, defaultMetrics, measurement));
- }
- currentLine = new ArrayList<>();
- if (!continuationPrefix.isEmpty()) {
- currentLine.add(InlineTextToken.of(continuationPrefix, defaultStyle, null, measurement));
- }
- currentWidth = inlineLineWidth(currentLine);
+ currentLine = new ArrayList<>();
+ if (!continuationPrefix.isEmpty()) {
+ currentLine.add(InlineTextToken.of(continuationPrefix, defaultStyle, null, measurement));
+ }
+ currentWidth = inlineLineWidth(currentLine);
- sanitizedToken = trimLeadingIfInlineLineStart(token, currentLine, measurement);
- if (sanitizedToken == null) {
- continue;
- }
- tokenWidth = sanitizedToken.wrapWidth();
- if (currentWidth + tokenWidth <= maxWidth + EPS) {
- currentLine.add(sanitizedToken);
- currentWidth += tokenWidth;
- continue;
+ sanitizedToken = trimLeadingIfInlineLineStart(token, currentLine, measurement);
+ if (sanitizedToken == null) {
+ continue;
+ }
+ tokenWidth = sanitizedToken.wrapWidth();
+ if (currentWidth + tokenWidth <= maxWidth + EPS) {
+ currentLine.add(sanitizedToken);
+ currentWidth += tokenWidth;
+ continue;
+ }
}
- if (!(sanitizedToken instanceof InlineTextToken textToken)
- || textToken.highlightGroup() != null) {
- // Atomic runs that exceed the available width are emitted on their
- // own line; further breaking is not possible (image / shape / SVG,
- // and a single highlight-chip word — a chip wraps between its words
- // but a lone over-wide chip word is not char-split).
+ // Still over-wide on a fresh (or already empty) line. Text tokens —
+ // plain OR highlight chip — are broken within the column; a graphic
+ // (image / shape / SVG) has no break point and is emitted as-is.
+ if (!(sanitizedToken instanceof InlineTextToken textToken)) {
currentLine.add(sanitizedToken);
- currentWidth += tokenWidth;
+ currentWidth += sanitizedToken.wrapWidth();
continue;
}
- List chunks = splitLongToken(
+ boolean chip = textToken.highlightGroup() != null;
+ // A chip fragment paints leadPad + glyphs + trailPad, but the break
+ // budgets glyphs only, so reserve the run's padding here to keep the
+ // coalesced fill inside the column. Interior fragments under-fill by
+ // at most that padding — cosmetic, and only on an over-wide chip.
+ double reserve = chip ? textToken.leadPad() + textToken.trailPad() : 0.0;
+ List pieces = breakLongToken(
textToken.text(),
textToken.textStyle(),
- Math.max(1.0, maxWidth - currentWidth),
+ Math.max(1.0, maxWidth - currentWidth - reserve),
measurement);
- for (int chunkIndex = 0; chunkIndex < chunks.size(); chunkIndex++) {
- String chunk = chunks.get(chunkIndex);
- if (chunk.isEmpty()) {
+ for (int pieceIndex = 0; pieceIndex < pieces.size(); pieceIndex++) {
+ String piece = pieces.get(pieceIndex);
+ if (piece.isEmpty()) {
continue;
}
- InlineTextToken chunkToken = InlineTextToken.of(
- chunk,
- textToken.textStyle(),
- textToken.linkTarget(),
- measurement);
+ // Chip pieces keep the run's group + background so the coalescer
+ // paints one fill per fragment; the run's outer pad sits on the
+ // first/last piece only, leaving the break seams open.
+ InlineTextToken chunkToken = chip
+ ? InlineTextToken.ofHighlight(
+ piece,
+ textToken.textStyle(),
+ textToken.linkTarget(),
+ textToken.background(),
+ textToken.highlightGroup(),
+ pieceIndex == 0 ? textToken.leadPad() : 0.0,
+ pieceIndex == pieces.size() - 1 ? textToken.trailPad() : 0.0,
+ measurement)
+ : InlineTextToken.of(
+ piece,
+ textToken.textStyle(),
+ textToken.linkTarget(),
+ measurement);
currentLine.add(chunkToken);
- currentWidth += chunkToken.width();
+ currentWidth += chunkToken.wrapWidth();
- if (chunkIndex < chunks.size() - 1) {
+ if (pieceIndex < pieces.size() - 1) {
result.add(toInlineParagraphLine(currentLine, defaultMetrics, measurement));
currentLine = new ArrayList<>();
if (!continuationPrefix.isEmpty()) {
@@ -1064,33 +1089,35 @@ private static List wrapMarkdownParagraph(List logicalLin
}
double tokenWidth = measurement.textWidth(sanitizedToken.textStyle(), sanitizedToken.text());
- if (currentLine.isEmpty() || currentWidth + tokenWidth <= maxWidth + EPS) {
+ if (currentWidth + tokenWidth <= maxWidth + EPS) {
currentLine.add(sanitizedToken);
currentWidth += tokenWidth;
continue;
}
+ // Does not fit. If the line already has content, flush it and retry
+ // the token on a fresh line before resorting to a break.
if (!currentLine.isEmpty()) {
result.add(toParagraphLine(currentLine, metrics, measurement));
- }
- currentLine = new ArrayList<>();
- if (!continuationPrefix.isEmpty()) {
- currentLine.add(new TextDataBody(continuationPrefix, style));
- }
- currentWidth = lineWidth(currentLine, measurement);
+ currentLine = new ArrayList<>();
+ if (!continuationPrefix.isEmpty()) {
+ currentLine.add(new TextDataBody(continuationPrefix, style));
+ }
+ currentWidth = lineWidth(currentLine, measurement);
- sanitizedToken = trimLeadingIfLineStart(token, currentLine);
- if (sanitizedToken == null || sanitizedToken.text().isEmpty()) {
- continue;
- }
- tokenWidth = measurement.textWidth(sanitizedToken.textStyle(), sanitizedToken.text());
- if (currentWidth + tokenWidth <= maxWidth + EPS) {
- currentLine.add(sanitizedToken);
- currentWidth += tokenWidth;
- continue;
+ sanitizedToken = trimLeadingIfLineStart(token, currentLine);
+ if (sanitizedToken == null || sanitizedToken.text().isEmpty()) {
+ continue;
+ }
+ tokenWidth = measurement.textWidth(sanitizedToken.textStyle(), sanitizedToken.text());
+ if (currentWidth + tokenWidth <= maxWidth + EPS) {
+ currentLine.add(sanitizedToken);
+ currentWidth += tokenWidth;
+ continue;
+ }
}
- List chunks = splitLongToken(
+ List chunks = breakLongToken(
sanitizedToken.text(),
sanitizedToken.textStyle(),
Math.max(1.0, maxWidth - currentWidth),
@@ -1604,6 +1631,84 @@ private static double lineWidth(List bodies,
return width;
}
+ /**
+ * Characters after which a long, otherwise-unbreakable token may wrap. These
+ * are the joiners inside package coordinates, fully-qualified class names and
+ * URLs ({@code org.junit.jupiter:junit-jupiter:5.10.2}, {@code a/b/c}), so an
+ * over-wide code token breaks at a readable seam instead of mid-identifier.
+ */
+ private static final String SOFT_BREAK_AFTER_CHARS = ".:/-";
+
+ /**
+ * Breaks a single over-wide token into line-pieces that each fit
+ * {@code maxWidth}. Prefers breaking at {@link #SOFT_BREAK_AFTER_CHARS soft
+ * seams} (after {@code . : / -}) so coordinates and URLs split naturally, and
+ * falls back to character-level {@link #splitLongToken splitting} for any lone
+ * segment still too wide (e.g. a long dot-less identifier). The returned pieces
+ * have the same shape the wrap loops already consume from {@code splitLongToken}
+ * — for a token with no soft seams the two are identical.
+ */
+ private static List breakLongToken(String token,
+ TextStyle style,
+ double maxWidth,
+ TextMeasurementSystem measurement) {
+ if (token == null || token.isEmpty()) {
+ return List.of();
+ }
+ List segments = softBreakSegments(token);
+ List pieces = new ArrayList<>();
+ StringBuilder current = new StringBuilder();
+ double currentWidth = 0.0;
+ for (String segment : segments) {
+ double segmentWidth = measurement.textWidth(style, segment);
+ if (current.length() > 0 && currentWidth + segmentWidth > maxWidth + EPS) {
+ pieces.add(current.toString());
+ current.setLength(0);
+ currentWidth = 0.0;
+ }
+ if (current.length() == 0 && segmentWidth > maxWidth + EPS) {
+ // A single seam-delimited segment is itself too wide: char-split it.
+ // Every char-chunk but the last is a finished piece; the tail seeds
+ // the current piece so a following short segment can still join it.
+ List chars = splitLongToken(segment, style, maxWidth, measurement);
+ for (int index = 0; index < chars.size() - 1; index++) {
+ pieces.add(chars.get(index));
+ }
+ String tail = chars.get(chars.size() - 1);
+ current.append(tail);
+ currentWidth = measurement.textWidth(style, tail);
+ } else {
+ current.append(segment);
+ currentWidth += segmentWidth;
+ }
+ }
+ if (current.length() > 0) {
+ pieces.add(current.toString());
+ }
+ return List.copyOf(pieces);
+ }
+
+ /**
+ * Splits a token after each {@link #SOFT_BREAK_AFTER_CHARS soft-break
+ * character}, keeping the delimiter with the preceding segment
+ * ({@code org.junit.jupiter:} then {@code junit-jupiter:} then {@code 5.10.2}).
+ * A token with no such characters yields a single segment (itself).
+ */
+ private static List softBreakSegments(String token) {
+ List segments = new ArrayList<>();
+ int start = 0;
+ for (int index = 0; index < token.length(); index++) {
+ if (SOFT_BREAK_AFTER_CHARS.indexOf(token.charAt(index)) >= 0) {
+ segments.add(token.substring(start, index + 1));
+ start = index + 1;
+ }
+ }
+ if (start < token.length()) {
+ segments.add(token.substring(start));
+ }
+ return segments;
+ }
+
private static List splitLongToken(String token,
TextStyle style,
double maxWidth,
diff --git a/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRenderTest.java b/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRenderTest.java
index a504b94c2..f96287b68 100644
--- a/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRenderTest.java
+++ b/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRenderTest.java
@@ -150,18 +150,33 @@ void multiWordChipStaysOneSpanAndCollapsesNewlines() throws Exception {
}
@Test
- void overWideAtomicChipRendersWithoutThrowing() throws Exception {
- // A chip wider than the column is emitted on its own line (atomic);
- // it must still render the text on one page without throwing.
+ void overWideChipBreaksWithinColumnWithoutThrowing() throws Exception {
+ // A single-word chip wider than the column breaks within it (at soft seams,
+ // char-split as a last resort): every visual fragment stays inside the inner
+ // width, keeps its rounded fill, and the coordinate survives end-to-end.
+ double innerWidth = 90 - 20;
+ List lines;
byte[] pdf;
try (DocumentSession session = GraphCompose.document().pageSize(90, 140).margin(10, 10, 10, 10).create()) {
session.dsl().pageFlow().name("Flow")
.addParagraph(p -> p.inlineCode("io.github.demchaav.a.very.long.package.name")).build();
+ lines = paragraphLines(session.layoutGraph());
pdf = session.toPdfBytes();
}
+ List chipLines = lines.stream()
+ .filter(l -> l.spans().stream().anyMatch(s -> s instanceof ParagraphTextSpan ts && ts.background() != null))
+ .toList();
+ assertThat(chipLines).as("the over-wide chip breaks to >= 2 fragments").hasSizeGreaterThanOrEqualTo(2);
+ for (ParagraphLine line : chipLines) {
+ ParagraphTextSpan fragment = chipSpan(line);
+ assertThat(fragment.width()).as("each fragment stays inside the inner width")
+ .isLessThanOrEqualTo(innerWidth + 0.5);
+ assertThat(fragment.background()).as("each fragment keeps its fill").isNotNull();
+ }
try (PDDocument document = Loader.loadPDF(pdf)) {
assertThat(document.getNumberOfPages()).isEqualTo(1);
- assertThat(new PDFTextStripper().getText(document)).doesNotContain("?");
+ String text = new PDFTextStripper().getText(document).replaceAll("\\s+", "");
+ assertThat(text).contains("io.github.demchaav.a.very.long.package.name").doesNotContain("?");
}
}
@@ -336,6 +351,111 @@ void wrappedLinkedChipEmitsAClickableRectPerFragment() throws Exception {
}
}
+ @Test
+ void overWideCodeChipBreaksAtSoftSeams() throws Exception {
+ // A coordinate breaks at its . : / - joiners, not mid-identifier: at least
+ // one non-final fragment ends with a soft-break character.
+ List lines;
+ try (DocumentSession session = GraphCompose.document().pageSize(120, 200).margin(10, 10, 10, 10).create()) {
+ session.dsl().pageFlow().name("Flow")
+ .addParagraph(p -> p.inlineCode("org.junit.jupiter:junit-jupiter:5.10.2")).build();
+ lines = paragraphLines(session.layoutGraph());
+ }
+ List chipLines = lines.stream()
+ .filter(l -> l.spans().stream().anyMatch(s -> s instanceof ParagraphTextSpan ts && ts.background() != null))
+ .toList();
+ assertThat(chipLines).hasSizeGreaterThanOrEqualTo(2);
+ boolean seamBreak = chipLines.subList(0, chipLines.size() - 1).stream()
+ .map(l -> chipSpan(l).text())
+ .anyMatch(t -> !t.isEmpty() && ".:/-".indexOf(t.charAt(t.length() - 1)) >= 0);
+ assertThat(seamBreak).as("the coordinate breaks at a . : / - seam, not mid-identifier").isTrue();
+ }
+
+ @Test
+ void overWidePlainTokenBreaksWithinColumn() throws Exception {
+ // The plain wrap path (no inline runs, no markdown): a lone over-wide first
+ // token char-splits within the column instead of overflowing it.
+ double innerWidth = 90 - 20;
+ String token = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
+ List lines;
+ try (DocumentSession session = GraphCompose.document().pageSize(90, 160).margin(10, 10, 10, 10).create()) {
+ session.dsl().pageFlow().name("Flow").addParagraph(p -> p.text(token)).build();
+ lines = paragraphLines(session.layoutGraph());
+ }
+ assertThat(lines).as("the over-wide plain token breaks to >= 2 lines").hasSizeGreaterThanOrEqualTo(2);
+ for (ParagraphLine line : lines) {
+ assertThat(line.width()).as("each line stays inside the inner width").isLessThanOrEqualTo(innerWidth + 0.5);
+ }
+ String joined = lines.stream().flatMap(l -> l.spans().stream())
+ .filter(ParagraphTextSpan.class::isInstance).map(s -> ((ParagraphTextSpan) s).text())
+ .reduce("", String::concat).replaceAll("\\s+", "");
+ assertThat(joined).isEqualTo(token);
+ }
+
+ @Test
+ void overWideTokenInMarkdownBreaksWithinColumn() throws Exception {
+ // The markdown wrap path (markdown enabled + markdown syntax present): a lone
+ // over-wide token still breaks within the column.
+ double innerWidth = 90 - 20;
+ String token = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
+ List lines;
+ try (DocumentSession session = GraphCompose.document().pageSize(90, 160).margin(10, 10, 10, 10).create()) {
+ session.markdown(true);
+ session.dsl().pageFlow().name("Flow").addParagraph(p -> p.text("**b** " + token)).build();
+ lines = paragraphLines(session.layoutGraph());
+ }
+ assertThat(lines).hasSizeGreaterThanOrEqualTo(2);
+ for (ParagraphLine line : lines) {
+ assertThat(line.width()).as("each line stays inside the inner width").isLessThanOrEqualTo(innerWidth + 0.5);
+ }
+ String joined = lines.stream().flatMap(l -> l.spans().stream())
+ .filter(ParagraphTextSpan.class::isInstance).map(s -> ((ParagraphTextSpan) s).text())
+ .reduce("", String::concat).replaceAll("\\s+", "");
+ assertThat(joined).as("the long token survives the break").contains(token);
+ }
+
+ @Test
+ void nonFirstOverWideChipBreaksWithinColumn() throws Exception {
+ // The over-wide chip is NOT the first token: leading prose is flushed, then
+ // the chip breaks on the following lines (it still fits none of them whole).
+ double innerWidth = 120 - 20;
+ List lines;
+ try (DocumentSession session = GraphCompose.document().pageSize(120, 200).margin(10, 10, 10, 10).create()) {
+ session.dsl().pageFlow().name("Flow")
+ .addParagraph(p -> p.inlineText("Use ").inlineCode("io.github.demchaav.a.very.long.package.name"))
+ .build();
+ lines = paragraphLines(session.layoutGraph());
+ }
+ List chipLines = lines.stream()
+ .filter(l -> l.spans().stream().anyMatch(s -> s instanceof ParagraphTextSpan ts && ts.background() != null))
+ .toList();
+ assertThat(chipLines).as("the trailing chip breaks to >= 2 fragments").hasSizeGreaterThanOrEqualTo(2);
+ for (ParagraphLine line : chipLines) {
+ assertThat(chipSpan(line).width()).isLessThanOrEqualTo(innerWidth + 0.5);
+ }
+ }
+
+ @Test
+ void overWideLinkedChipEmitsAClickableRectPerFragment() throws Exception {
+ // Char-split (single-word) linked chip: each visual fragment is independently
+ // clickable, mirroring the multi-word wrapped-chip guarantee.
+ byte[] pdf;
+ try (DocumentSession session = GraphCompose.document().pageSize(120, 200).margin(10, 10, 10, 10).create()) {
+ session.dsl().pageFlow().name("Flow")
+ .addParagraph(p -> p.inlineHighlight(
+ "io.github.demchaav.a.very.long.package.name", MONO, FILL, 3.0, PAD,
+ new DocumentLinkOptions("https://example.com")))
+ .build();
+ pdf = session.toPdfBytes();
+ }
+ try (PDDocument document = Loader.loadPDF(pdf)) {
+ long links = document.getPage(0).getAnnotations().stream()
+ .filter(PDAnnotationLink.class::isInstance).count();
+ assertThat(links).as("a char-split linked chip is clickable on each fragment")
+ .isGreaterThanOrEqualTo(2);
+ }
+ }
+
private static byte[] render(Consumer body) throws Exception {
try (DocumentSession session = GraphCompose.document().pageSize(320, 140).margin(16, 16, 16, 16).create()) {
session.dsl().pageFlow().name("Flow").addParagraph(body::accept).build();
diff --git a/src/test/java/com/demcha/compose/document/table/TableAutoColumnComposedWidthTest.java b/src/test/java/com/demcha/compose/document/table/TableAutoColumnComposedWidthTest.java
new file mode 100644
index 000000000..66afd93c5
--- /dev/null
+++ b/src/test/java/com/demcha/compose/document/table/TableAutoColumnComposedWidthTest.java
@@ -0,0 +1,74 @@
+package com.demcha.compose.document.table;
+
+import com.demcha.compose.GraphCompose;
+import com.demcha.compose.document.api.DocumentSession;
+import com.demcha.compose.document.layout.PlacedFragment;
+import com.demcha.compose.document.layout.payloads.ParagraphFragmentPayload;
+import com.demcha.compose.document.layout.payloads.ParagraphTextSpan;
+import com.demcha.compose.document.node.TableNode;
+import com.demcha.compose.document.style.DocumentInsets;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * An AUTO column sizes to fit a composed cell's intrinsic content (e.g. an
+ * inline-code chip) instead of collapsing to zero because the composed cell
+ * contributed no natural width. A FIXED column keeps its declared width, so an
+ * over-wide chip breaks inside it (handled by the paragraph wrap engine).
+ */
+class TableAutoColumnComposedWidthTest {
+
+ private static final String CODE = "io.github.demchaav";
+
+ /** Number of visual chip fragments (rounded-fill spans) rendered by the composed cell. */
+ private static long chipFragmentCount(DocumentSession session) {
+ return session.layoutGraph().fragments().stream()
+ .map(PlacedFragment::payload)
+ .filter(ParagraphFragmentPayload.class::isInstance)
+ .map(ParagraphFragmentPayload.class::cast)
+ .flatMap(payload -> payload.lines().stream())
+ .filter(line -> line.spans().stream()
+ .anyMatch(s -> s instanceof ParagraphTextSpan ts && ts.background() != null))
+ .count();
+ }
+
+ private static TableNode chipTable(DocumentSession session, DocumentTableColumn column) {
+ return new TableNode(
+ "ChipTable",
+ List.of(column),
+ List.of(List.of(DocumentTableCell.node(session.dsl().paragraph().inlineCode(CODE).build()))),
+ DocumentTableStyle.empty(),
+ null,
+ DocumentInsets.zero(),
+ DocumentInsets.zero());
+ }
+
+ @Test
+ void autoColumnGrowsToFitComposedChipOnOneFragment() throws Exception {
+ try (DocumentSession session = GraphCompose.document()
+ .pageSize(300, 160)
+ .margin(DocumentInsets.of(10))
+ .create()) {
+ session.add(chipTable(session, DocumentTableColumn.auto()));
+ // The AUTO column sizes to the chip's natural width (the page is wide
+ // enough), so the chip renders on ONE fragment instead of collapsing to
+ // zero width and breaking character by character.
+ assertThat(chipFragmentCount(session)).isEqualTo(1);
+ }
+ }
+
+ @Test
+ void fixedNarrowColumnStillBreaksTheComposedChip() throws Exception {
+ try (DocumentSession session = GraphCompose.document()
+ .pageSize(300, 160)
+ .margin(DocumentInsets.of(10))
+ .create()) {
+ session.add(chipTable(session, DocumentTableColumn.fixed(40)));
+ // A FIXED column is never grown; the over-wide chip breaks inside it.
+ assertThat(chipFragmentCount(session)).isGreaterThanOrEqualTo(2);
+ }
+ }
+}
diff --git a/src/test/java/com/demcha/compose/document/table/TableCellInlineCodeWrapTest.java b/src/test/java/com/demcha/compose/document/table/TableCellInlineCodeWrapTest.java
new file mode 100644
index 000000000..c286f8d3a
--- /dev/null
+++ b/src/test/java/com/demcha/compose/document/table/TableCellInlineCodeWrapTest.java
@@ -0,0 +1,110 @@
+package com.demcha.compose.document.table;
+
+import com.demcha.compose.GraphCompose;
+import com.demcha.compose.document.api.DocumentSession;
+import com.demcha.compose.document.layout.LayoutGraph;
+import com.demcha.compose.document.layout.PlacedFragment;
+import com.demcha.compose.document.layout.payloads.ParagraphFragmentPayload;
+import com.demcha.compose.document.layout.payloads.ParagraphLine;
+import com.demcha.compose.document.layout.payloads.ParagraphTextSpan;
+import com.demcha.compose.document.node.TableNode;
+import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.testing.layout.LayoutSnapshotAssertions;
+import com.demcha.testing.VisualTestOutputs;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.text.PDFTextStripper;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Regression for the reported "long inline-code chip overflows its table column":
+ * a composed cell holding a single over-wide {@code inlineCode(...)} coordinate in a
+ * narrow fixed column must break the chip inside the cell (fill intact per
+ * fragment), never draw past the cell box, and never throw.
+ */
+class TableCellInlineCodeWrapTest {
+
+ /** Fixed column widths: a deliberately narrow middle column holds the chip. */
+ private static final double MIDDLE_CELL_WIDTH = 60.0;
+ private static final String COORDINATE = "org.junit.jupiter:junit-jupiter:5.10.2";
+
+ private static TableNode narrowCodeTable(DocumentSession session) {
+ DocumentTableCell left = DocumentTableCell.text("L");
+ DocumentTableCell mid = DocumentTableCell.node(
+ session.dsl().paragraph().inlineCode(COORDINATE).build());
+ DocumentTableCell right = DocumentTableCell.text("R");
+ return new TableNode(
+ "CodeTable",
+ List.of(DocumentTableColumn.fixed(40),
+ DocumentTableColumn.fixed(MIDDLE_CELL_WIDTH),
+ DocumentTableColumn.fixed(40)),
+ List.of(List.of(left, mid, right)),
+ DocumentTableStyle.empty(),
+ 140.0,
+ DocumentInsets.zero(),
+ DocumentInsets.zero());
+ }
+
+ @Test
+ void narrowCellInlineCodeBreaksWithinCell() throws Exception {
+ byte[] pdf;
+ List chipLines;
+ try (DocumentSession session = GraphCompose.document()
+ .pageSize(220, 200)
+ .margin(DocumentInsets.of(10))
+ .create()) {
+ session.add(narrowCodeTable(session));
+
+ LayoutGraph graph = session.layoutGraph();
+ chipLines = graph.fragments().stream()
+ .map(PlacedFragment::payload)
+ .filter(ParagraphFragmentPayload.class::isInstance)
+ .map(ParagraphFragmentPayload.class::cast)
+ .flatMap(payload -> payload.lines().stream())
+ .filter(l -> l.spans().stream()
+ .anyMatch(s -> s instanceof ParagraphTextSpan ts && ts.background() != null))
+ .toList();
+ pdf = session.toPdfBytes();
+ }
+
+ assertThat(chipLines).as("the over-wide chip breaks to >= 2 fragments in the cell")
+ .hasSizeGreaterThanOrEqualTo(2);
+ for (ParagraphLine line : chipLines) {
+ ParagraphTextSpan fragment = (ParagraphTextSpan) line.spans().stream()
+ .filter(s -> s instanceof ParagraphTextSpan ts && ts.background() != null)
+ .findFirst().orElseThrow();
+ assertThat(fragment.width()).as("no chip fragment draws past the cell's inner width")
+ .isLessThanOrEqualTo(MIDDLE_CELL_WIDTH + 0.5);
+ assertThat(fragment.background()).as("each fragment keeps its fill").isNotNull();
+ }
+
+ // Human-inspectable artifact: the chip wraps inside the middle column.
+ Path output = VisualTestOutputs.preparePdf("inline_code_wrap_narrow_cell", "tables");
+ Files.write(output, pdf);
+ assertThat(new String(pdf, 0, 8, StandardCharsets.US_ASCII)).startsWith("%PDF-");
+
+ try (PDDocument document = Loader.loadPDF(pdf)) {
+ assertThat(document.getNumberOfPages()).isEqualTo(1);
+ String text = new PDFTextStripper().getText(document).replaceAll("\\s+", "");
+ assertThat(text).contains(COORDINATE).doesNotContain("?");
+ }
+ }
+
+ @Test
+ void narrowCellInlineCodeLayoutSnapshot() throws Exception {
+ try (DocumentSession session = GraphCompose.document()
+ .pageSize(220, 200)
+ .margin(DocumentInsets.of(10))
+ .create()) {
+ session.add(narrowCodeTable(session));
+ LayoutSnapshotAssertions.assertMatches(session, "tables/inline_code_wrap_narrow_cell");
+ }
+ }
+}
diff --git a/src/test/resources/layout-snapshots/tables/inline_code_wrap_narrow_cell.json b/src/test/resources/layout-snapshots/tables/inline_code_wrap_narrow_cell.json
new file mode 100644
index 000000000..a12bf8cb6
--- /dev/null
+++ b/src/test/resources/layout-snapshots/tables/inline_code_wrap_narrow_cell.json
@@ -0,0 +1,47 @@
+{
+ "formatVersion" : "2.0",
+ "canvas" : {
+ "pageWidth" : 220.0,
+ "pageHeight" : 200.0,
+ "innerWidth" : 200.0,
+ "innerHeight" : 180.0,
+ "margin" : {
+ "top" : 10.0,
+ "right" : 10.0,
+ "bottom" : 10.0,
+ "left" : 10.0
+ }
+ },
+ "totalPages" : 1,
+ "nodes" : [ {
+ "path" : "CodeTable[0]",
+ "entityName" : "CodeTable",
+ "entityKind" : "TableNode",
+ "parentPath" : null,
+ "childIndex" : 0,
+ "depth" : 1,
+ "layer" : 1,
+ "computedX" : 10.0,
+ "computedY" : 71.96,
+ "placementX" : 10.0,
+ "placementY" : 71.96,
+ "placementWidth" : 140.0,
+ "placementHeight" : 118.04,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 140.0,
+ "contentHeight" : 118.04,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ } ]
+}