Skip to content

Commit 83ca902

Browse files
authored
fix(pptx): take the face from the decoration for standard-14 names (#450)
* docs: bring the release-facing surfaces up to 2.1 The three pages a visitor reaches before any code still described an earlier release. README's stable-release banner and the security policy's supported- versions table both name the current line; the table had stayed on 1.9.x across two majors while the policy above it already said fixes go to the latest minor. The showcase site never mentioned PowerPoint at all, though its gallery has been serving decks since the twin examples landed. It also told visitors to call BusinessTheme.modern(), which ships only in the unpublished examples module — the README has said that type is gone since 2.0, so the two public faces disagreed. Preset counts were three short in one place and claimed fourteen entries over a list of five in another, and the test-count claim is replaced with the gate it stands for so it cannot go stale again. * fix(pptx): take the face from the decoration for standard-14 names A span naming a standard-14 style variant travelled to PowerPoint as a bold or italic run flag, taken from the font name string. Those names are family aliases: FontLibrary resolves Helvetica-Bold to the Helvetica family and Font.fontType picks the real face from the decoration, so a span that names one without a decoration is measured with the regular metrics. The viewer honoured the flag and drew a face roughly 6% wider than the frame the engine had sized, so text overran its slot — a chip label crossed its card border, and two words of a rich-text heading closed the gap between their separately placed frames until they read as one word. The PDF backend was unaffected because it selects the face the same way it measures it. Run flags now follow the decoration for the nine standard-14 variants. A binary family keeps its own name and is measured as itself, so its suffix still carries the face.
1 parent 736e599 commit 83ca902

6 files changed

Lines changed: 87 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ shade), while others lose something the format cannot carry — see Known limita
121121

122122
### Fixed
123123

124+
- **PPTX text no longer overruns the frame the engine measured for it.** A span that
125+
named a standard-14 style variant — `Helvetica-Bold`, `Times-Italic` and their
126+
siblings — travelled to PowerPoint as a bold or italic run flag. Those names are
127+
family aliases: the engine resolves each to its regular base and takes the real face
128+
from the span's decoration, so the layout had measured the regular metrics. The viewer
129+
then drew a face about 6% wider than its slot, which pushed a chip label past its card
130+
and closed the gap between two words of a rich-text heading until they read as one.
131+
Run flags now follow the decoration for those families, so a deck renders the same
132+
face the PDF does; a binary family still carries its face in its own name.
124133
- **A large clipped region in a PPTX deck is no longer downscaled.** The raster scale
125134
was capped only from above, so a clip box wider than 2048 pt rasterized *below*
126135
native size — an A0-scale composite landed near 44 DPI and read as visibly blurry,

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
</p>
2020

2121
> **Release status** &mdash;
22-
> 🟢 **Latest stable**: [v2.0.0](https://github.com/DemchaAV/GraphCompose/releases/tag/v2.0.0) &mdash; the **module-first** release: the engine is now a lean `graph-compose-core` with opt-in `render-pdf` / `render-docx` backends, while `graph-compose` stays a drop-in for PDF. **[Migrating to 2.0 &darr;](docs/migration/v2.0.0-modules.md)**
22+
> 🟢 **Latest stable**: [v2.1.0](https://github.com/DemchaAV/GraphCompose/releases/tag/v2.1.0) &mdash; the **PowerPoint** release: `graph-compose-render-pptx` turns the same resolved layout into an editable deck &mdash; one page per slide, geometry-identical to the PDF, text and panels as native shapes. Ships as `@Beta`. **[What each backend supports &darr;](docs/architecture/backend-capability-matrix.md)**
2323
2424
> &nbsp;·&nbsp; ⬆️ **Upgrading from 1.x?** `graph-compose` stays a drop-in for PDF with no code change; see the [2.0 modules migration guide](./docs/migration/v2.0.0-modules.md)
2525
> &nbsp;·&nbsp; See [API stability policy](./docs/api-stability.md) for tier definitions.

SECURITY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ Security fixes are issued for the latest minor release. Older minors do not rece
88

99
| Version | Supported |
1010
|---------|-----------|
11-
| 1.9.x | Yes — actively patched |
12-
| < 1.9 | No — upgrade required (see [CHANGELOG.md](CHANGELOG.md) for per-version migration notes) |
11+
| 2.1.x | Yes — actively patched |
12+
| < 2.1 | No — upgrade required (see [CHANGELOG.md](CHANGELOG.md) for per-version migration notes, and the [2.0 modules migration guide](docs/migration/v2.0.0-modules.md) for the 1.x → 2.x step) |
1313

1414
## Reporting a vulnerability
1515

render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMapping.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,7 @@ public static boolean isBold(TextStyle style) {
9898
if (decoration == TextDecoration.BOLD || decoration == TextDecoration.BOLD_ITALIC) {
9999
return true;
100100
}
101-
return style.fontName() != null
102-
&& style.fontName().name().toLowerCase(Locale.ROOT).contains("bold");
101+
return nameCarriesStyle(style.fontName(), "bold");
103102
}
104103

105104
/** Returns whether the run selects an italic face. */
@@ -108,8 +107,32 @@ public static boolean isItalic(TextStyle style) {
108107
if (decoration == TextDecoration.ITALIC || decoration == TextDecoration.BOLD_ITALIC) {
109108
return true;
110109
}
111-
String name = style.fontName() == null ? "" : style.fontName().name().toLowerCase(Locale.ROOT);
112-
return name.contains("italic") || name.contains("oblique");
110+
return nameCarriesStyle(style.fontName(), "italic")
111+
|| nameCarriesStyle(style.fontName(), "oblique");
112+
}
113+
114+
/**
115+
* Reports whether a font name's own style suffix should become a run flag.
116+
*
117+
* <p>Only binary families carry their face in the name. The standard-14
118+
* PostScript variants — {@code Helvetica-Bold}, {@code Times-Italic},
119+
* {@code Courier-BoldOblique} and their siblings — are <em>family
120+
* aliases</em>: the engine resolves each to its regular base family and
121+
* then picks the real face from the span's {@link TextDecoration}, so a
122+
* span that names one of them without a decoration is measured with the
123+
* regular metrics. Turning the suffix into a run flag would ask the viewer
124+
* for a bold or italic face the layout never measured, and the placed text
125+
* would render wider than the frame the engine sized for it.</p>
126+
*
127+
* @param fontName logical document font, may be {@code null}
128+
* @param token lowercase style token to look for
129+
* @return {@code true} when the name may contribute the flag
130+
*/
131+
private static boolean nameCarriesStyle(FontName fontName, String token) {
132+
if (fontName == null || standardReplacementFor(fontName) != null) {
133+
return false;
134+
}
135+
return fontName.name().toLowerCase(Locale.ROOT).contains(token);
113136
}
114137

115138
static boolean isUnderline(TextStyle style) {

render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMappingTest.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,46 @@ void mapsStandardFamiliesToMetricCompatibleViewerFonts() {
3636
}
3737

3838
@Test
39-
void combinesDecorationWithFaceSuffixes() {
40-
TextStyle boldFace = new TextStyle(FontName.HELVETICA_BOLD, 12,
41-
TextDecoration.DEFAULT, Color.BLACK);
39+
void decorationDrivesTheFace() {
4240
TextStyle decorated = new TextStyle(FontName.HELVETICA, 12,
4341
TextDecoration.BOLD_ITALIC, Color.BLACK);
4442

45-
assertThat(PptxFontMapping.isBold(boldFace)).isTrue();
46-
assertThat(PptxFontMapping.isItalic(boldFace)).isFalse();
4743
assertThat(PptxFontMapping.isBold(decorated)).isTrue();
4844
assertThat(PptxFontMapping.isItalic(decorated)).isTrue();
4945
}
46+
47+
/**
48+
* The standard-14 style variants are family aliases: the engine resolves
49+
* {@code Helvetica-Bold} to the Helvetica family and takes the face from
50+
* the decoration, so a span naming one without a decoration is measured
51+
* with the regular metrics. Claiming the flag here would make the viewer
52+
* draw a wider face than the frame was sized for.
53+
*/
54+
@Test
55+
void standard14FaceSuffixDoesNotBecomeARunFlag() {
56+
TextStyle aliasedBold = new TextStyle(FontName.HELVETICA_BOLD, 12,
57+
TextDecoration.DEFAULT, Color.BLACK);
58+
TextStyle aliasedOblique = new TextStyle(FontName.COURIER_BOLD_OBLIQUE, 12,
59+
TextDecoration.DEFAULT, Color.BLACK);
60+
61+
assertThat(PptxFontMapping.isBold(aliasedBold)).isFalse();
62+
assertThat(PptxFontMapping.isItalic(aliasedBold)).isFalse();
63+
assertThat(PptxFontMapping.isBold(aliasedOblique)).isFalse();
64+
assertThat(PptxFontMapping.isItalic(aliasedOblique)).isFalse();
65+
}
66+
67+
/**
68+
* A binary family keeps its own name and is measured as itself, so its
69+
* suffix is the only signal of the face and must still travel as a flag.
70+
*/
71+
@Test
72+
void binaryFamilySuffixStillCarriesTheFace() {
73+
TextStyle bold = new TextStyle(FontName.of("Acme-Sans-Bold"), 12,
74+
TextDecoration.DEFAULT, Color.BLACK);
75+
TextStyle italic = new TextStyle(FontName.of("Acme-Sans-Italic"), 12,
76+
TextDecoration.DEFAULT, Color.BLACK);
77+
78+
assertThat(PptxFontMapping.isBold(bold)).isTrue();
79+
assertThat(PptxFontMapping.isItalic(italic)).isTrue();
80+
}
5081
}

web/index.html

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@
7676
"featureList": [
7777
"Semantic node tree DSL (ParagraphNode, TableNode, SectionNode, LayerStackNode, CanvasLayerNode)",
7878
"Atomic pagination with row-by-row table splitting",
79-
"14 CV presets and 14 letter pairs out of the box",
80-
"Theme tokens via BusinessTheme.modern() / classic() / executive()",
79+
"16 CV presets and 15 matching cover letters out of the box",
80+
"Theme tokens via BrandTheme.modernProfessional() / executive() / boxedClassic()",
8181
"Markdown-aware bodies (bold, italic) in every paragraph and list block",
82-
"PDFBox-backed render pipeline plus a DOCX backend for editable output",
83-
"819-test verify gate with layout snapshots and pixel-diff visual parity"
82+
"PDFBox-backed render pipeline, an editable PowerPoint backend geometry-identical to the PDF, and a semantic DOCX backend",
83+
"Full-reactor verify gate with layout snapshots and pixel-diff visual parity"
8484
]
8585
},
8686
{
@@ -172,6 +172,7 @@
172172
<h1 id="hero-title">Java PDF layout engine for structured business documents.</h1>
173173
<p class="hero-lead">Describe documents. Render polished PDFs. A semantic layout engine for Java services that need <b>structured, paginated, theme-driven</b> output &mdash; CVs, invoices, proposals, reports.</p>
174174
<p class="hero-text">No drawing API. No pixel arithmetic. You compose <code>ParagraphNode</code>, <code>TableNode</code>, <code>SectionNode</code>; GraphCompose handles measurement, pagination, fonts, and PDFBox rendering.</p>
175+
<p class="hero-text">The same resolved layout also renders to an <b>editable PowerPoint deck</b> &mdash; one page per slide, geometry-identical to the PDF, text and panels as native shapes. Opt in with <code>graph-compose-render-pptx</code> (<code>@Beta</code>).</p>
175176
<div class="hero-actions">
176177
<a class="button button-primary" href="#showcase">Browse Examples</a>
177178
<a class="button button-secondary" href="https://github.com/DemchaAV/GraphCompose">Source on GitHub</a>
@@ -212,12 +213,12 @@ <h3>Atomic pagination</h3>
212213
<p>Tables split row-by-row, rows are atomic, layer stacks atomic. No manual page math.</p>
213214
</div>
214215
<div>
215-
<h3>14 CV presets, 14 letter pairs</h3>
216-
<p>Every preset is one final class with a one-liner <code>create(BusinessTheme)</code> factory.</p>
216+
<h3>16 CV presets, 15 matching letters</h3>
217+
<p>Every preset is one final class with a one-liner <code>create(BrandTheme)</code> factory.</p>
217218
</div>
218219
<div>
219220
<h3>Tested at every layer</h3>
220-
<p>819 green tests, layout snapshots per preset, pixel-diff visual parity gate, public-API leak guards.</p>
221+
<p>A full-reactor verify gate on every push: layout snapshots per preset, pixel-diff visual parity, public-API leak guards.</p>
221222
</div>
222223
</div>
223224
</section>
@@ -286,7 +287,7 @@ <h2 id="showcase-title">50+ generated PDFs you can inspect.</h2>
286287

287288
<h3 id="templates-section">Templates</h3>
288289

289-
<h4>CV (14 presets)</h4>
290+
<h4>CV</h4>
290291
<ul>
291292
<li><a href="showcase/pdf/templates/cv/cv-modern-professional-v2.pdf">Modern Professional CV</a></li>
292293
<li><a href="showcase/pdf/templates/cv/cv-nordic-clean-v2.pdf">Nordic Clean CV</a></li>
@@ -304,7 +305,7 @@ <h4>CV (14 presets)</h4>
304305
<li><a href="showcase/pdf/templates/cv/cv-monogram-sidebar-v2.pdf">Monogram Sidebar CV</a></li>
305306
</ul>
306307

307-
<h4>Cover Letter (14 paired layouts)</h4>
308+
<h4>Cover Letter</h4>
308309
<ul>
309310
<li><a href="showcase/pdf/templates/coverletter/cover-letter-modern-professional-v2.pdf">Cover Letter (canonical)</a></li>
310311
<li><a href="showcase/pdf/templates/coverletter/cover-letter-modern-professional-v2.pdf">Modern Professional letter</a></li>
@@ -359,7 +360,7 @@ <h2 id="architecture-title">Author intent stays separate from PDF rendering.</h2
359360
<ol class="pipeline" aria-label="Document pipeline">
360361
<li><strong>Semantic DSL.</strong> You build a tree of <code>ParagraphNode</code>, <code>TableNode</code>, <code>SectionNode</code>, <code>LayerStackNode</code>, <code>CanvasLayerNode</code>.</li>
361362
<li><strong>Layout pass.</strong> The engine resolves measurements, pagination, decoration, overlays. Output is a <code>LayoutGraph</code> &mdash; geometry only, no draw calls.</li>
362-
<li><strong>Render backend.</strong> The <code>FixedLayoutBackend</code> consumes placed fragments. Default: <code>PdfFixedLayoutBackend</code> via PDFBox. DOCX backend exists for editable output.</li>
363+
<li><strong>Render backend.</strong> The <code>FixedLayoutBackend</code> consumes placed fragments. Default: <code>PdfFixedLayoutBackend</code> via PDFBox. <code>PptxFixedLayoutBackend</code> consumes the same fragments, so a deck is geometry-identical to the PDF by construction. A semantic DOCX backend maps the node tree instead, with much narrower coverage.</li>
363364
</ol>
364365
</section>
365366

@@ -379,7 +380,7 @@ <h3>Snapshot-tested layouts</h3>
379380
</article>
380381
<article class="feature-card">
381382
<h3>Theme tokens, not magic numbers</h3>
382-
<p><code>BusinessTheme.modern()</code> / <code>.classic()</code> / <code>.executive()</code> set palette, typography, table styles. Override one or all.</p>
383+
<p><code>BrandTheme.modernProfessional()</code> / <code>.executive()</code> / <code>.boxedClassic()</code> set palette, typography, table styles. Override one or all. Ships in <code>graph-compose-templates</code>.</p>
383384
</article>
384385
<article class="feature-card">
385386
<h3>Markdown-aware bodies</h3>

0 commit comments

Comments
 (0)