Skip to content
Closed
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
20 changes: 10 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '17'
cache: maven

- name: Run guards
Expand All @@ -51,11 +51,11 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '17'
cache: maven

- name: Build and run tests
Expand All @@ -76,11 +76,11 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '17'
cache: maven

- name: Install root artifact
Expand Down Expand Up @@ -114,11 +114,11 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '17'
cache: maven

- name: Run coarse performance smoke benchmark
Expand Down Expand Up @@ -148,11 +148,11 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '17'
cache: maven

- name: Restore benchmark history cache
Expand Down
10 changes: 10 additions & 0 deletions docs/production-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ the session.

`buildPdf(Path)` also uses the streaming path internally.

## Glyph Fallback

When a PDF render uses a Standard 14 font such as Helvetica, some Unicode
glyphs are not encodable through PDFBox's WinAnsi path. GraphCompose now
degrades unsupported glyphs to `?` during both width measurement and PDF text
emission so rendering does not fail mid-document.

If preserving the original glyph matters, register an embedded/custom font
family that contains the character instead of relying on the Standard 14 fonts.

## Debug Output

Use `GraphCompose.document(path).guideLines(true)` or
Expand Down
4 changes: 2 additions & 2 deletions examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<properties>
<graphcompose.version>${project.version}</graphcompose.version>
<logback.version>1.5.18</logback.version>
<maven.compiler.release>21</maven.compiler.release>
<maven.compiler.release>17</maven.compiler.release>

<junit.bom.version>5.12.2</junit.bom.version>
<assertj.version>3.27.3</assertj.version>
Expand Down Expand Up @@ -84,4 +84,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -607,16 +607,19 @@ private static List<DocumentTableCell> dayCells(DayShift dayShift, Theme theme)
if (dayShift == null) {
return List.of(emptyDayCell(theme));
}
return switch (dayShift) {
case DayShift.EmptyDay e ->
List.of(emptyDayCell(theme));
case DayShift.FullStatus(ShiftStatus s) ->
List.of(fullStatusCell(s, theme));
case DayShift.CrossMeal(Shift shift) ->
shiftAcrossDay(shift, theme);
case DayShift.Halves(Half lunch, Half dinner) ->
mergedHalfCells(lunch, dinner, theme);
};
if (dayShift instanceof DayShift.EmptyDay) {
return List.of(emptyDayCell(theme));
}
if (dayShift instanceof DayShift.FullStatus fullStatus) {
return List.of(fullStatusCell(fullStatus.status(), theme));
}
if (dayShift instanceof DayShift.CrossMeal crossMeal) {
return shiftAcrossDay(crossMeal.shift(), theme);
}
if (dayShift instanceof DayShift.Halves halves) {
return mergedHalfCells(halves.lunch(), halves.dinner(), theme);
}
throw new IllegalStateException("Unsupported day shift: " + dayShift);
}

private static List<DocumentTableCell> mergedHalfCells(Half lunch, Half dinner, Theme theme) {
Expand All @@ -633,11 +636,16 @@ private static List<DocumentTableCell> mergedHalfCells(Half lunch, Half dinner,
}

private static List<DocumentTableCell> halfCells(Half half, Theme theme) {
return switch (half) {
case Half.Empty e -> List.of(emptyHalfCell(theme));
case Half.StatusFill(ShiftStatus s) -> List.of(statusHalfCell(s, theme));
case Half.Working(Shift shift) -> shiftHalfCells(shift, theme);
};
if (half instanceof Half.Empty) {
return List.of(emptyHalfCell(theme));
}
if (half instanceof Half.StatusFill statusFill) {
return List.of(statusHalfCell(statusFill.status(), theme));
}
if (half instanceof Half.Working working) {
return shiftHalfCells(working.shift(), theme);
}
throw new IllegalStateException("Unsupported half shift: " + half);
}

private static DocumentTableCell emptyDayCell(Theme theme) {
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<properties>
<!-- Toolchain -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
<maven.compiler.release>17</maven.compiler.release>

<!-- Runtime / library dependencies -->
<assertj.version>3.27.3</assertj.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ private void renderLine(PDPageContentStream stream,
try {
for (ParagraphSpan span : spans) {
if (span instanceof ParagraphTextSpan textSpan) {
String text = sanitize(textSpan.text());
PdfFont font = fonts.getFont(textSpan.textStyle().fontName(), PdfFont.class).orElseThrow();
String text = font.sanitizeForRender(textSpan.textStyle(), sanitize(textSpan.text()));
if (text.isEmpty()) {
cursorX += textSpan.width();
continue;
Expand All @@ -99,7 +100,6 @@ private void renderLine(PDPageContentStream stream,
stream.newLineAtOffset((float) cursorX, (float) baselineY);
inTextBlock = true;
}
PdfFont font = fonts.getFont(textSpan.textStyle().fontName(), PdfFont.class).orElseThrow();
stream.setFont(font.fontType(textSpan.textStyle().decoration()), (float) textSpan.textStyle().size());
stream.setNonStrokingColor(textSpan.textStyle().color());
stream.showText(text);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,16 @@ public void render(PlacedFragment fragment,
if (policy == ClipPolicy.CLIP_BOUNDS) {
stream.addRect(x, y, width, height);
} else { // CLIP_PATH
switch (outline) {
case ShapeOutline.Ellipse ignored -> addEllipsePath(stream, x, y, width, height);
case ShapeOutline.RoundedRectangle r -> addRoundedRectanglePath(
if (outline instanceof ShapeOutline.Ellipse) {
addEllipsePath(stream, x, y, width, height);
} else if (outline instanceof ShapeOutline.RoundedRectangle r) {
addRoundedRectanglePath(
stream, x, y, width, height,
(float) Math.min(r.cornerRadius(), Math.min(width, height) / 2.0f));
case ShapeOutline.Rectangle ignored -> stream.addRect(x, y, width, height);
} else if (outline instanceof ShapeOutline.Rectangle) {
stream.addRect(x, y, width, height);
} else {
throw new IllegalStateException("Unsupported shape outline: " + outline);
}
}
// clip() pushes the current path onto the clipping path; the path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,13 @@ private void renderCellText(PDPageContentStream stream,
stream.setFont(font.fontType(cell.style().textStyle().decoration()), (float) cell.style().textStyle().size());
stream.setNonStrokingColor(cell.style().textStyle().color());
for (ResolvedTextLine line : lines) {
if (line.text().isEmpty()) {
String rendered = font.sanitizeForRender(cell.style().textStyle(), line.text());
if (rendered.isEmpty()) {
continue;
}
stream.beginText();
stream.newLineAtOffset((float) line.x(), (float) line.baselineY());
stream.showText(line.text());
stream.showText(rendered);
stream.endText();
}
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ public static PreparedSplitResult<ListNode> splitList(PreparedNode<ListNode> pre
return new PreparedSplitResult<>(head, tail);
}

PreparedListItemLayout firstItem = layout.items().getFirst();
PreparedListItemLayout firstItem = layout.items().get(0);
PreparedParagraphLayout itemLayout = firstItem.paragraphLayout();
int maxLines = maxLinesThatFit(
itemLayout.visualLines(),
Expand Down Expand Up @@ -796,7 +796,7 @@ private static List<String> wrapParagraph(List<String> logicalLines,
result.add(currentPrefix + chunks.get(index));
currentPrefix = continuationPrefix;
}
currentLine = currentPrefix + chunks.getLast();
currentLine = currentPrefix + chunks.get(chunks.size() - 1);
hasContent = true;
}

Expand Down Expand Up @@ -1451,7 +1451,7 @@ private static int maxLinesThatFit(List<ParagraphLine> lines, double lineGap, do
if (lines.isEmpty()) {
return 0;
}
if (availableHeight + EPS < lines.getFirst().lineHeight()) {
if (availableHeight + EPS < lines.get(0).lineHeight()) {
return 0;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,32 +105,37 @@ public List<LayoutFragment> emitFragments(PreparedNode<ShapeContainerNode> prepa
}
Color awtFill = node.fillColor() == null ? null : node.fillColor().color();
Stroke stroke = toStroke(node.stroke());
LayoutFragment outlineFragment = switch (outline) {
case ShapeOutline.Ellipse ignored -> new LayoutFragment(
LayoutFragment outlineFragment;
if (outline instanceof ShapeOutline.Ellipse) {
outlineFragment = new LayoutFragment(
placement.path(),
0,
padLeft,
padBottom,
width,
height,
new EllipseFragmentPayload(awtFill, stroke, null, null));
case ShapeOutline.Rectangle ignored -> new LayoutFragment(
} else if (outline instanceof ShapeOutline.Rectangle) {
outlineFragment = new LayoutFragment(
placement.path(),
0,
padLeft,
padBottom,
width,
height,
new ShapeFragmentPayload(awtFill, stroke, 0.0, null, null, null));
case ShapeOutline.RoundedRectangle r -> new LayoutFragment(
} else if (outline instanceof ShapeOutline.RoundedRectangle r) {
outlineFragment = new LayoutFragment(
placement.path(),
0,
padLeft,
padBottom,
width,
height,
new ShapeFragmentPayload(awtFill, stroke, r.cornerRadius(), null, null, null));
};
} else {
throw new IllegalStateException("Unsupported shape outline: " + outline);
}

List<LayoutFragment> opening = new ArrayList<>(4);
boolean hasTransform = !node.transform().isIdentity();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,25 @@ private DocumentTextStyle bodyStyle(BusinessTheme theme) {
}

private DocumentNode renderBody(BusinessTheme theme, Spacing spacing) {
return switch (body) {
case ParagraphBlock p -> renderParagraph(p, theme, spacing);
case BulletListBlock b -> renderList(b.items(), ListMarker.bullet(),
"bullet", theme, spacing);
case NumberedListBlock n -> renderList(n.items(), ListMarker.custom("1."),
"numbered", theme, spacing);
case MultiParagraphBlock m -> renderMultiParagraph(m, theme, spacing);
case IndentedBlock i -> renderIndented(i, theme, spacing);
case KeyValueBlock k -> renderKeyValue(k, theme, spacing);
};
if (body instanceof ParagraphBlock p) {
return renderParagraph(p, theme, spacing);
}
if (body instanceof BulletListBlock b) {
return renderList(b.items(), ListMarker.bullet(), "bullet", theme, spacing);
}
if (body instanceof NumberedListBlock n) {
return renderList(n.items(), ListMarker.custom("1."), "numbered", theme, spacing);
}
if (body instanceof MultiParagraphBlock m) {
return renderMultiParagraph(m, theme, spacing);
}
if (body instanceof IndentedBlock i) {
return renderIndented(i, theme, spacing);
}
if (body instanceof KeyValueBlock k) {
return renderKeyValue(k, theme, spacing);
}
throw new IllegalStateException("Unsupported block: " + body);
}

private ParagraphNode renderParagraph(ParagraphBlock block, BusinessTheme theme, Spacing spacing) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,39 +215,33 @@ private void addModuleBody(SectionBuilder section, CvModule module) {
}

private void renderBody(SectionBuilder section, Block body) {
switch (body) {
case ParagraphBlock p -> renderParagraph(section, p.text());
case MultiParagraphBlock m -> {
for (String line : m.paragraphs()) {
WorkEntry entry = parseWorkEntry(line);
if (entry != null) {
renderWorkEntry(section, entry);
} else {
renderParagraph(section, line);
}
if (body instanceof ParagraphBlock p) {
renderParagraph(section, p.text());
} else if (body instanceof MultiParagraphBlock m) {
for (String line : m.paragraphs()) {
WorkEntry entry = parseWorkEntry(line);
if (entry != null) {
renderWorkEntry(section, entry);
} else {
renderParagraph(section, line);
}
}
case BulletListBlock b -> renderBulletList(section, b.items());
case NumberedListBlock n -> {
for (String item : n.items()) {
renderParagraph(section, item);
}
}
case IndentedBlock i -> {
for (IndentedBlock.Item item : i.items()) {
String inline = (item.title().isBlank() ? "" : item.title())
+ (item.title().isBlank() || item.body().isBlank() ? "" : " - ")
+ (item.body().isBlank() ? "" : item.body());
renderParagraph(section, inline);
}
} else if (body instanceof BulletListBlock b) {
renderBulletList(section, b.items());
} else if (body instanceof NumberedListBlock n) {
for (String item : n.items()) {
renderParagraph(section, item);
}
case KeyValueBlock kv -> {
for (KeyValueBlock.Entry entry : kv.entries()) {
renderKeyValueEntry(section, entry);
}
} else if (body instanceof IndentedBlock i) {
for (IndentedBlock.Item item : i.items()) {
String inline = (item.title().isBlank() ? "" : item.title())
+ (item.title().isBlank() || item.body().isBlank() ? "" : " - ")
+ (item.body().isBlank() ? "" : item.body());
renderParagraph(section, inline);
}
default -> {
// ignore other block kinds
} else if (body instanceof KeyValueBlock kv) {
for (KeyValueBlock.Entry entry : kv.entries()) {
renderKeyValueEntry(section, entry);
}
}
}
Expand Down
Loading