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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ follow semantic versioning; release dates are ISO 8601.
byte-identical decks across runs by pinning the OPC created/modified
properties and normalizing every zip entry timestamp (zone-independent).

- `DocumentSession.toPptxBytes()` / `writePptx(OutputStream)` /
`buildPptx()` / `buildPptx(Path)` — the PPTX counterparts of the PDF
convenience trio. The backend resolves through the format-keyed provider
(`BackendProviders.fixedLayout("pptx")`), so the core stays free of a PPTX
dependency: with `graph-compose-render-pptx` on the classpath the session's
chrome (metadata, watermark, headers/footers) applies exactly as in the PDF
paths, and without it the render fails with a `MissingBackendException`
naming the artifact to add. Stream and default-output-file contracts match
the PDF trio.

### Fixed

- `DocumentSession.toImages` / `toImage` rasterized documents that use binary
Expand Down Expand Up @@ -142,6 +152,12 @@ follow semantic versioning; release dates are ISO 8601.
differing-page-size rejection, and render-twice byte equality with pinned
zip entry times; a chrome-and-navigation parity demo renders the PDF/PPTX
pair with per-page PNG previews.
- Session-level PPTX convenience tests pin chrome flow-through (metadata and
footer tokens land in the deck), slide-count identity with the resolved
layout graph, the caller-owned-stream contract, file output, and the
default-output-file and empty-session failure modes; the backend-free
missing-format diagnostics were already pinned by the core's
`MissingBackendContractTest`.

## v2.0.0 — 2026-07-13

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/com/demcha/compose/GraphCompose.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ public static DocumentBuilder document() {

/**
* Starts the canonical semantic document composition flow with a default output target
* used by {@link DocumentSession#buildPdf()}.
* used by {@link DocumentSession#buildPdf()} and {@link DocumentSession#buildPptx()}.
*
* @param outputFile default PDF output path for {@link DocumentSession#buildPdf()}
* @param outputFile default output path for the no-arg build methods
* @return builder for creating a semantic document session
*/
public static DocumentBuilder document(Path outputFile) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,18 @@ DocumentOutputOptions snapshot() {
}

/**
* Resolves and configures the fixed-layout backend for the session's
* convenience output methods, translating the attached chrome through the
* registered
* Resolves and configures the fixed-layout backend of the requested format
* for the session's convenience output methods, translating the attached
* chrome through the registered
* {@link com.demcha.compose.document.backend.fixed.FixedLayoutBackendProvider}.
*
* @param debug debug overlay options; never {@code null}
* @param format backend format key, e.g. {@code "pdf"} or {@code "pptx"}
* @param debug debug overlay options; never {@code null}
* @return a configured renderer
* @throws com.demcha.compose.document.exceptions.MissingBackendException
* if no provider for the format is on the classpath
*/
FixedLayoutRenderer toConveniencePdfBackend(DocumentDebugOptions debug) {
return BackendProviders.fixedLayout("pdf").create(snapshot(), debug);
FixedLayoutRenderer toConvenienceBackend(String format, DocumentDebugOptions debug) {
return BackendProviders.fixedLayout(format).create(snapshot(), debug);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
final class DocumentRenderingFacade {
private static final Logger LIFECYCLE_LOG = LoggerFactory.getLogger("com.demcha.compose.document.lifecycle");

/** Provider format keys for the fixed-layout convenience paths. */
private static final String PDF = "pdf";
private static final String PPTX = "pptx";

private final Context context;

DocumentRenderingFacade(Context context) {
Expand Down Expand Up @@ -95,24 +99,50 @@ <R> R export(SemanticBackend<R> backend, Path outputFile) throws Exception {
}

byte[] toPdfBytes() throws Exception {
return renderBytes(PDF);
}

void writePdf(OutputStream output) throws Exception {
writeFixedLayout(PDF, output);
}

void buildPdf(Path outputFile) throws Exception {
buildFixedLayout(PDF, outputFile);
}

byte[] toPptxBytes() throws Exception {
return renderBytes(PPTX);
}

void writePptx(OutputStream output) throws Exception {
writeFixedLayout(PPTX, output);
}

void buildPptx(Path outputFile) throws Exception {
buildFixedLayout(PPTX, outputFile);
}

private byte[] renderBytes(String format) throws Exception {
context.ensureOpen();
long startNanos = System.nanoTime();
LIFECYCLE_LOG.debug("document.pdf.bytes.start sessionId={} revision={} roots={}",
context.sessionId(), context.revision(), context.rootCount());
LIFECYCLE_LOG.debug("document.{}.bytes.start sessionId={} revision={} roots={}",
format, context.sessionId(), context.revision(), context.rootCount());
try {
ByteArrayOutputStream output = new ByteArrayOutputStream();
writePdf(output);
writeFixedLayout(format, output);
byte[] bytes = output.toByteArray();
LIFECYCLE_LOG.debug(
"document.pdf.bytes.end sessionId={} revision={} byteCount={} durationMs={}",
"document.{}.bytes.end sessionId={} revision={} byteCount={} durationMs={}",
format,
context.sessionId(),
context.revision(),
bytes.length,
elapsedMillis(startNanos));
return bytes;
} catch (Exception ex) {
LIFECYCLE_LOG.error(
"document.pdf.bytes.failed sessionId={} revision={} errorType={}",
"document.{}.bytes.failed sessionId={} revision={} errorType={}",
format,
context.sessionId(),
context.revision(),
ex.getClass().getSimpleName(),
Expand All @@ -121,27 +151,29 @@ byte[] toPdfBytes() throws Exception {
}
}

void writePdf(OutputStream output) throws Exception {
private void writeFixedLayout(String format, OutputStream output) throws Exception {
context.ensureOpen();
context.ensureRenderable();
OutputStream target = Objects.requireNonNull(output, "output");
long startNanos = System.nanoTime();
LIFECYCLE_LOG.debug("document.pdf.stream.start sessionId={} revision={} roots={}",
context.sessionId(), context.revision(), context.rootCount());
LIFECYCLE_LOG.debug("document.{}.stream.start sessionId={} revision={} roots={}",
format, context.sessionId(), context.revision(), context.rootCount());
try {
context.conveniencePdfBackend().write(context.layoutGraph(), new FixedLayoutRenderContext(
context.convenienceBackend(format).write(context.layoutGraph(), new FixedLayoutRenderContext(
context.canvas(),
context.customFontFamilies(),
null,
target));
LIFECYCLE_LOG.debug(
"document.pdf.stream.end sessionId={} revision={} durationMs={}",
"document.{}.stream.end sessionId={} revision={} durationMs={}",
format,
context.sessionId(),
context.revision(),
elapsedMillis(startNanos));
} catch (Exception ex) {
LIFECYCLE_LOG.error(
"document.pdf.stream.failed sessionId={} revision={} errorType={}",
"document.{}.stream.failed sessionId={} revision={} errorType={}",
format,
context.sessionId(),
context.revision(),
ex.getClass().getSimpleName(),
Expand All @@ -150,23 +182,25 @@ void writePdf(OutputStream output) throws Exception {
}
}

void buildPdf(Path outputFile) throws Exception {
private void buildFixedLayout(String format, Path outputFile) throws Exception {
context.ensureOpen();
context.ensureRenderable();
Path target = Objects.requireNonNull(outputFile, "outputFile");
long startNanos = System.nanoTime();
LIFECYCLE_LOG.debug("document.pdf.build.start sessionId={} revision={} roots={}",
context.sessionId(), context.revision(), context.rootCount());
LIFECYCLE_LOG.debug("document.{}.build.start sessionId={} revision={} roots={}",
format, context.sessionId(), context.revision(), context.rootCount());
try (OutputStream output = Files.newOutputStream(target)) {
writePdf(output);
writeFixedLayout(format, output);
LIFECYCLE_LOG.debug(
"document.pdf.build.end sessionId={} revision={} durationMs={}",
"document.{}.build.end sessionId={} revision={} durationMs={}",
format,
context.sessionId(),
context.revision(),
elapsedMillis(startNanos));
} catch (Exception ex) {
LIFECYCLE_LOG.error(
"document.pdf.build.failed sessionId={} revision={} errorType={}",
"document.{}.build.failed sessionId={} revision={} errorType={}",
format,
context.sessionId(),
context.revision(),
ex.getClass().getSimpleName(),
Expand All @@ -182,7 +216,7 @@ List<BufferedImage> renderImages(int dpi, boolean transparent, int pageIndex) th
LIFECYCLE_LOG.debug("document.images.start sessionId={} revision={} roots={} dpi={} transparent={} pageIndex={}",
context.sessionId(), context.revision(), context.rootCount(), dpi, transparent, pageIndex);
try {
List<BufferedImage> images = context.conveniencePdfBackend().renderToImages(
List<BufferedImage> images = context.convenienceBackend(PDF).renderToImages(
context.layoutGraph(),
new FixedLayoutRenderContext(context.canvas(), context.customFontFamilies(), null, null),
dpi,
Expand Down Expand Up @@ -224,6 +258,10 @@ interface Context {

DocumentOutputOptions outputOptions();

FixedLayoutRenderer conveniencePdfBackend();
/**
* Resolves the session-chrome-configured fixed-layout backend for the
* given format key ({@code "pdf"}, {@code "pptx"}, ...).
*/
FixedLayoutRenderer convenienceBackend(String format);
}
}
Loading
Loading