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
16 changes: 16 additions & 0 deletions render-docx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ The semantic DOCX export backend for GraphCompose, backed by Apache POI. It carr
Add it (at compile scope) only when you export `.docx`. It is **not** included by
`graph-compose`, `graph-compose-core`, or `graph-compose-bundle` — DOCX is opt-in.

**It is not sufficient on its own.** Opening a `DocumentSession` resolves a
`FontMetricsProvider` so text can be measured, and `graph-compose-render-pdf` is the
only artifact that publishes one. A classpath of `graph-compose-core` +
`graph-compose-render-docx` fails at `create()` with `MissingBackendException` before
any export happens. Add the PDF backend alongside it — or depend on `graph-compose`,
which is core + render-pdf already:

```xml
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-render-pdf</artifactId>
<version>2.0.0</version>
<scope>runtime</scope>
</dependency>
```

## Usage

```java
Expand Down
47 changes: 46 additions & 1 deletion scripts/cut-release.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,42 @@ function Update-ModuleReadmeInstallVersion($readmePath, $newVersion) {
}
}

function Update-ReleaseSmokeDefaultVersion($repoRoot, $newVersion) {
# The consumer-smoke harness hard-codes the version it tests when none is
# passed. Left behind, a post-release run that accepts the prefilled default
# re-verifies the PREVIOUS release and reports green — the one failure mode a
# release gate must not have. Each pattern is anchored to its own construct so
# a bump cannot smear across unrelated version strings in the same file.
$targets = @(
@{ Path = 'scripts/release-smoke/run.sh'; Pattern = '(?<=^GC_VERSION=")[\w\.\-]+(?=")'; Label = 'run.sh default' },
@{ Path = 'scripts/release-smoke/run.sh'; Pattern = '(?<=tests gc\.version=)[\w\.\-]+'; Label = 'run.sh usage' },
@{ Path = 'scripts/release-smoke/run.ps1'; Pattern = "(?<=\[string\]\`$Version = ')[\w\.\-]+(?=')"; Label = 'run.ps1 default' },
@{ Path = 'scripts/release-smoke/run.ps1'; Pattern = '(?<=# isolated, tests )[\w\.\-]+'; Label = 'run.ps1 usage' },
@{ Path = '.github/workflows/release-smoke.yml'; Pattern = "(?<=^\s{8}default: ')[\w\.\-]+(?=')"; Label = 'workflow input default' },
@{ Path = 'scripts/release-smoke/README.md'; Pattern = '(?<=defaults to the current published release \(`)[\w\.\-]+(?=`\))'; Label = 'README prose' }
)

foreach ($target in $targets) {
$path = Join-Path $repoRoot $target.Path
if (-not (Test-Path $path)) {
Note "skip (no file): $($target.Path)"
continue
}
$content = Get-Content $path -Raw
$updated = [regex]::Replace($content, $target.Pattern, $newVersion, 'Multiline')
if ($content -eq $updated) {
Note "no change: $($target.Path) [$($target.Label)] (already $newVersion?)"
continue
}
if ($DryRun) {
Write-Host " [DRY RUN] Bump $($target.Path) [$($target.Label)] -> $newVersion" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($path, $updated)
Note "bumped release-smoke $($target.Label): $($target.Path) -> $newVersion"
}
}
}

function Update-IndexHtmlVersion($indexHtmlPath, $newVersion) {
if (-not (Test-Path $indexHtmlPath)) {
Note "skip (no file): $indexHtmlPath"
Expand Down Expand Up @@ -815,6 +851,9 @@ try {
Update-ModuleReadmeInstallVersion (Join-Path $repoRoot $moduleReadme) $Version
}
Update-IndexHtmlVersion (Join-Path $repoRoot 'web/index.html') $Version
# The smoke harness's default version must follow the release, or the
# post-release run silently re-verifies the previous one.
Update-ReleaseSmokeDefaultVersion $repoRoot $Version
} else {
Note "pre-release: skipped README / module-README / web install-snippet bumps (stay on last stable)"
}
Expand Down Expand Up @@ -941,7 +980,13 @@ try {
'benchmarks/pom.xml',
'README.md',
'CHANGELOG.md',
'web/index.html'
'web/index.html',
# Bumped by Update-ReleaseSmokeDefaultVersion so the post-release smoke run
# defaults to the version just published, not the previous one.
'scripts/release-smoke/run.sh',
'scripts/release-smoke/run.ps1',
'scripts/release-smoke/README.md',
'.github/workflows/release-smoke.yml'
)
# qa + coverage exist only in the 2.0 aggregator layout; add them to the commit
# only when present so the script stays layout-agnostic (the 1.x single-artifact
Expand Down
7 changes: 7 additions & 0 deletions scripts/release-smoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ build never touches them. Run them explicitly with the harness below.
| `s4-templates` | `graph-compose-templates` | a built-in template composes and renders through the PDF stack |
| `s5-testing` | `graph-compose` + `graph-compose-testing` | the consumer testing helper (`LayoutSnapshotAssertions`) resolves and round-trips a layout snapshot |
| `s6-bundle` | `graph-compose-bundle` | the batteries-included aggregate renders a templated document, exposes the bundled fonts (`DefaultFonts.bundledFontNames()`), and makes the colour-emoji set resolvable (`GraphComposeEmoji.isAvailable()`) |
| `s7-core-render-pptx` | `graph-compose-core` + `graph-compose-render-pptx` | the PPTX backend is discovered by format, and `toPptxBytes()` produces a real OPC package — one slide part, 16:9 slide dimensions in EMU, editable text runs, and **no** full-slide picture in vector mode. **Requires 2.1.0+**: the fixed-layout backend and `DocumentPageSize.SLIDE_16_9` do not exist in 2.0.0, so this scenario cannot pass against an earlier version |
| `s8-core-render-docx` | `graph-compose-core` + `graph-compose-render-docx` + `graph-compose-render-pdf` | the semantic Word exporter is on the consumer's compile classpath (it is named directly, not discovered through the ServiceLoader) and `export(new DocxSemanticBackend())` produces a `word/document.xml` carrying the text. The PDF backend is in the set because it is **required**: opening a session resolves a `FontMetricsProvider` and render-pdf is the only artifact that publishes one, while render-docx declares it at test scope only — so core + render-docx alone cannot construct a session |

Both new scenarios inspect the emitted OPC package with `java.util.zip` rather than
Apache POI. The point is to prove the *published* artifacts work for a consumer who
installed nothing else, so a scenario must not pull a parsing library of its own to make
its assertions pass.

The "must pull core + render-pdf" (wrapper) and "must pull the documented
aggregate" (bundle) assertions are proven positively: `s1` / `s6` can only
Expand Down
2 changes: 1 addition & 1 deletion scripts/release-smoke/run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ $repoRoot = (Resolve-Path (Join-Path $here '..\..')).Path
$mvnw = Join-Path $repoRoot 'mvnw.cmd'
$settings = Join-Path $here 'settings.xml'

$scenarios = @('s1-graph-compose', 's2-core-only', 's3-core-render-pdf', 's4-templates', 's5-testing', 's6-bundle')
$scenarios = @('s1-graph-compose', 's2-core-only', 's3-core-render-pdf', 's4-templates', 's5-testing', 's6-bundle', 's7-core-render-pptx', 's8-core-render-docx')
$repo = Join-Path $repoRoot 'target\release-smoke-m2\repo'
New-Item -ItemType Directory -Force -Path $repo | Out-Null

Expand Down
2 changes: 1 addition & 1 deletion scripts/release-smoke/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ REPO_ROOT="$(cd "$HERE/../.." && pwd)"
MVNW="$REPO_ROOT/mvnw"
SETTINGS="$HERE/settings.xml"

SCENARIOS=(s1-graph-compose s2-core-only s3-core-render-pdf s4-templates s5-testing s6-bundle)
SCENARIOS=(s1-graph-compose s2-core-only s3-core-render-pdf s4-templates s5-testing s6-bundle s7-core-render-pptx s8-core-render-docx)
REPO="$REPO_ROOT/target/release-smoke-m2/repo"

# Default version under test: the currently published release. Release smoke must
Expand Down
64 changes: 64 additions & 0 deletions scripts/release-smoke/s7-core-render-pptx/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- Standalone consumer smoke project: no <parent>, never part of the
GraphCompose reactor. The lean core plus the PPTX backend — the
coordinate a PowerPoint consumer actually installs. -->
<groupId>com.demcha.smoke</groupId>
<artifactId>graph-compose-smoke-core-render-pptx</artifactId>
<version>1.0.0</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<gc.version>2.0.0</gc.version>
<junit.version>5.11.4</junit.version>
<assertj.version>3.27.3</assertj.version>
<surefire.version>3.5.2</surefire.version>
</properties>

<dependencies>
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-core</artifactId>
<version>${gc.version}</version>
</dependency>
<!-- Declared at runtime scope on purpose: the consumer compiles against
core only and discovers the backend through META-INF/services. The
PDF stack arrives transitively — the backend reuses its font
measurement and the clip raster pass — which this scenario asserts
rather than hides. -->
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-render-pptx</artifactId>
<version>${gc.version}</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.demcha.smoke;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.backend.fixed.BackendProviders;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

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

/**
* Scenario 7 — {@code graph-compose-core} + {@code graph-compose-render-pptx}
* resolved from Maven Central produces a real, editable PowerPoint deck.
*
* <p>The deck is inspected with {@link ZipInputStream} rather than Apache POI:
* the point is to prove the <em>published</em> artifacts work for a consumer who
* installed nothing else, so the scenario must not pull a parsing library of its
* own to make its assertions pass.</p>
*
* <p>An empty file with a valid ZIP header would satisfy a naive smoke test, so
* the assertions go further: the slide part must exist, carry real text runs, and
* consist of native shapes rather than one full-slide picture — the failure mode
* that would mean the backend silently fell back to rasterising everything.</p>
*/
class CoreRenderPptxTest {

/** EMU per PostScript point, the unit OOXML stores slide dimensions in. */
private static final int EMU_PER_POINT = 12700;

@Test
void coreWithRenderPptxProducesAnEditableDeck() throws Exception {
byte[] deck;
try (DocumentSession document = GraphCompose.document()
.pageSize(DocumentPageSize.SLIDE_16_9)
.margin(48f, 48f, 48f, 48f)
.create()) {
document.add(document.dsl().paragraph()
.text("graph-compose-core + graph-compose-render-pptx renders via the SPI.")
.build());
deck = document.toPptxBytes();
}

assertThat(deck).isNotEmpty();
assertThat(new String(deck, 0, 2, StandardCharsets.US_ASCII))
.describedAs("a .pptx is an OPC package, so it must start with the ZIP signature")
.isEqualTo("PK");

Map<String, byte[]> parts = unzip(deck);
assertThat(parts).containsKey("[Content_Types].xml");
assertThat(parts)
.describedAs("one resolved page must become one slide part")
.containsKey("ppt/slides/slide1.xml");
assertThat(parts.keySet().stream().filter(name -> name.startsWith("ppt/slides/slide")))
.hasSize(1);

String presentation = text(parts.get("ppt/presentation.xml"));
assertThat(presentation)
.describedAs("SLIDE_16_9 is 960x540 pt, which OOXML stores in EMU")
.contains("cx=\"" + (960 * EMU_PER_POINT) + "\"")
.contains("cy=\"" + (540 * EMU_PER_POINT) + "\"");

String slide = text(parts.get("ppt/slides/slide1.xml"));
assertThat(slide)
.describedAs("the paragraph must land as an editable text run, not as pixels")
.contains("<a:t>");
assertThat(countOccurrences(slide, "<p:sp>"))
.describedAs("the slide must be built from native shapes")
.isPositive();
// POI writes both elements without attributes, verified against a generated
// deck that does contain a picture — so neither assertion is vacuous, and
// "<p:sp>" cannot accidentally match "<p:spTree" or "<p:spPr>".
assertThat(countOccurrences(slide, "<p:pic>"))
.describedAs("a text-only document must produce no pictures at all in vector mode")
.isZero();
}

@Test
void thePptxProviderIsDiscoveredByFormat() {
assertThat(BackendProviders.fixedLayout("pptx").format()).isEqualTo("pptx");
}

private static Map<String, byte[]> unzip(byte[] archive) throws Exception {
Map<String, byte[]> parts = new HashMap<>();
try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(archive))) {
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
zip.transferTo(bytes);
parts.put(entry.getName(), bytes.toByteArray());
}
}
return parts;
}

private static String text(byte[] part) {
assertThat(part).isNotNull();
return new String(part, StandardCharsets.UTF_8);
}

private static int countOccurrences(String haystack, String needle) {
int count = 0;
for (int at = haystack.indexOf(needle); at >= 0; at = haystack.indexOf(needle, at + needle.length())) {
count++;
}
return count;
}
}
72 changes: 72 additions & 0 deletions scripts/release-smoke/s8-core-render-docx/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- Standalone consumer smoke project: no <parent>, never part of the
GraphCompose reactor. The lean core plus the semantic Word exporter. -->
<groupId>com.demcha.smoke</groupId>
<artifactId>graph-compose-smoke-core-render-docx</artifactId>
<version>1.0.0</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<gc.version>2.0.0</gc.version>
<junit.version>5.11.4</junit.version>
<assertj.version>3.27.3</assertj.version>
<surefire.version>3.5.2</surefire.version>
</properties>

<dependencies>
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-core</artifactId>
<version>${gc.version}</version>
</dependency>
<!-- Compile scope, unlike the fixed-layout backends: the DOCX exporter is
named directly by the caller (session.export(new DocxSemanticBackend()))
rather than discovered through the ServiceLoader. -->
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-render-docx</artifactId>
<version>${gc.version}</version>
</dependency>
<!-- Required, and not transitive: opening a DocumentSession resolves a
FontMetricsProvider, and render-pdf is the only artifact that publishes
one. render-docx declares it at test scope for its own suite, so a
consumer installing core + render-docx alone cannot construct a session
at all. This scenario therefore pins the combination that actually
works — see render-docx/README.md. -->
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-render-pdf</artifactId>
<version>${gc.version}</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
</plugin>
</plugins>
</build>
</project>
Loading
Loading