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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ follow semantic versioning; release dates are ISO 8601.
identical everywhere. Slides are not editable as text; the default stays
the editable vector mode.

- The fixed PPTX backend completes the fragment matrix: images (STRETCH /
CONTAIN / COVER via the picture source crop, dimensions decoded from the
real bitmap), barcodes (the PDF handler's identical ZXing raster), polygons
and free paths as freeform geometry, native DrawingML gradient fills and
gradient strokes (radial centres and explicit linear axes approximate, with
a one-time note), and transform markers as rotated, centre-pivot-scaled
group shapes — every fragment between the markers is created inside the
group, PPTX's replacement for PDF's graphics-state matrix. Clip regions
render unclipped with a one-time warning (DrawingML has no graphics-state
clipping); anchor and bookmark markers record their destinations for the
navigation pass.

### Fixed

- `DocumentSession.toImages` / `toImage` rasterized documents that use binary
Expand Down
22 changes: 11 additions & 11 deletions docs/architecture/backend-capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,18 @@ Payload records live in `core` under
| Rectangle shape — fill, stroke, per-corner radii, side borders (`ShapeFragmentPayload`) | ✅ `PdfShapeFragmentRenderHandler` | ⚠️ `PptxShapeFragmentRenderHandler` (distinct per-corner radii render with the top-left radius on all corners — single-adjust `roundRect` preset — until custom geometry lands; uniform radii and side borders exact) | ❌ |
| Ellipse (`EllipseFragmentPayload`) | ✅ `PdfEllipseFragmentRenderHandler` | ✅ `PptxEllipseFragmentRenderHandler` | ❌ |
| Line — dash pattern, line cap (`LineFragmentPayload`) | ✅ `PdfLineFragmentRenderHandler` | ⚠️ `PptxLineFragmentRenderHandler` (numeric dash arrays map to the generic dashed preset; solid lines and caps exact) | ❌ |
| Polygon (`PolygonFragmentPayload`) | ✅ `PdfPolygonFragmentRenderHandler` | 🚧 | ❌ |
| Free path — segments, dash, cap, join (`PathFragmentPayload`) | ✅ `PdfPathFragmentRenderHandler` + `PdfPathPainter` | 🚧 | ❌ |
| Linear gradient fill (`DocumentPaint`) | ✅ `PdfShadingSupport` | 🚧 | ❌ |
| Radial gradient fill (`DocumentPaint`) | ✅ `PdfShadingSupport` | 🚧 (approximation expected — DrawingML cannot express radius-to-farthest-corner exactly) | ❌ |
| Gradient strokes | ✅ `PdfPathPainter` (pattern stroking colour) | 🚧 | ❌ |
| Image — STRETCH / CONTAIN / COVER fit (`ImageFragmentPayload`) | ✅ `PdfImageFragmentRenderHandler` | 🚧 | ✅ semantic images (`DocxSemanticBackend`) |
| Barcode / QR (`BarcodeFragmentPayload`) | ✅ `PdfBarcodeFragmentRenderHandler` (ZXing raster) | 🚧 | ❌ |
| Polygon (`PolygonFragmentPayload`) | ✅ `PdfPolygonFragmentRenderHandler` | ✅ `PptxPolygonFragmentRenderHandler` + `PptxInlineGeometry` | ❌ |
| Free path — segments, dash, cap, join (`PathFragmentPayload`) | ✅ `PdfPathFragmentRenderHandler` + `PdfPathPainter` | ⚠️ `PptxPathFragmentRenderHandler` + `PptxInlineGeometry` (numeric dash arrays map to the dashed preset) | ❌ |
| Linear gradient fill (`DocumentPaint`) | ✅ `PdfShadingSupport` | ✅ `PptxGradientFill` (native `gradFill`; explicit-axis endpoints approximate to the angle) | ❌ |
| Radial gradient fill (`DocumentPaint`) | ✅ `PdfShadingSupport` | ⚠️ `PptxGradientFill` (`circle` path shade — DrawingML cannot express radius-to-farthest-corner exactly) | ❌ |
| Gradient strokes | ✅ `PdfPathPainter` (pattern stroking colour) | ✅ `PptxGradientFill` (native `ln`/`gradFill`) | ❌ |
| Image — STRETCH / CONTAIN / COVER fit (`ImageFragmentPayload`) | ✅ `PdfImageFragmentRenderHandler` | ✅ `PptxImageFragmentRenderHandler` (COVER via the picture source crop) | ✅ semantic images (`DocxSemanticBackend`) |
| Barcode / QR (`BarcodeFragmentPayload`) | ✅ `PdfBarcodeFragmentRenderHandler` (ZXing raster) | ✅ `PptxBarcodeFragmentRenderHandler` (identical ZXing raster) | ❌ |
| Table rows — resolved cells, row/col spans, two-pass fill/border paint (`TableRowFragmentPayload`) | ✅ `PdfTableRowFragmentRenderHandler` + row grouping in `PdfFixedLayoutBackend` | ✅ `PptxTableRowFragmentRenderHandler` + row grouping in `PptxFixedLayoutBackend` (positioned rectangles, edge lines, and text frames — never native PPTX tables, which re-lay-out content) | ✅ semantic tables (`DocxSemanticBackend`) |
| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | 🚧 (rect crops for pictures; other content degrades with a one-time warning — PPTX has no graphics-state clipping) | ⚠️ inline fallback + one-time capability warning |
| Transform open/close — rotate/scale about fragment centre (`TransformBegin/EndPayload`) | ✅ `PdfTransformBegin/EndRenderHandler` | 🚧 (group shape with `xfrm`) | ⚠️ inline fallback + one-time capability warning |
| Anchor markers (`AnchorMarkerPayload`) | ✅ `PdfAnchorMarkerRenderHandler` + `PdfInternalLinkWriter` | 🚧 | ❌ |
| Bookmark markers (`BookmarkMarkerPayload`) | ✅ `PdfBookmarkMarkerRenderHandler` + `PdfBookmarkOutlineWriter` | 🚧 (PPTX has no document outline; mapped to slide names where 1:1) | ❌ |
| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ⚠️ `PptxShapeClipBegin/EndRenderHandler` (children render unclipped with a one-time warning — DrawingML has no graphics-state clipping; use the PDF backend or raster-slide mode for exact clips) | ⚠️ inline fallback + one-time capability warning |
| Transform open/close — rotate/scale about fragment centre (`TransformBegin/EndPayload`) | ✅ `PdfTransformBegin/EndRenderHandler` | ✅ `PptxTransformBegin/EndRenderHandler` (group shape; rotation and centre-pivot scaling via the exterior/interior frame ratio) | ⚠️ inline fallback + one-time capability warning |
| Anchor markers (`AnchorMarkerPayload`) | ✅ `PdfAnchorMarkerRenderHandler` + `PdfInternalLinkWriter` | ⚠️ `PptxAnchorMarkerRenderHandler` (destinations recorded; slide-jump emission pending) | ❌ |
| Bookmark markers (`BookmarkMarkerPayload`) | ✅ `PdfBookmarkMarkerRenderHandler` + `PdfBookmarkOutlineWriter` | ⚠️ `PptxBookmarkMarkerRenderHandler` (records collected; slide-name mapping pending — PPTX has no document outline) | ❌ |
| Alpha / opacity | ✅ `PdfAlphaSupport` (`PDExtendedGraphicsState`) | 🚧 (native fill alpha) | ❌ |

## Navigation and interactivity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
import com.demcha.compose.document.backend.fixed.FixedLayoutRenderContext;
import com.demcha.compose.document.backend.fixed.FixedLayoutRenderer;
import com.demcha.compose.document.backend.fixed.SectionUnit;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxAnchorMarkerRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxBarcodeFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxBookmarkMarkerRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxEllipseFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxImageFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxPathFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxPolygonFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxShapeClipBeginRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxShapeClipEndRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxTransformBeginRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxTransformEndRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxLineFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxParagraphFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.handlers.PptxShapeFragmentRenderHandler;
Expand Down Expand Up @@ -49,11 +59,13 @@
* throws {@link UnsupportedNodeCapabilityException}). Custom handlers replace
* built-in defaults per payload type via {@link Builder#addHandler}.</p>
*
* <p>The backend currently renders paragraphs (including rich runs, chips and
* inline graphics), table rows, and vector shape, line, and ellipse fragments;
* the remaining payload types arrive incrementally — see
* <p>The backend currently renders every fragment payload the layout compiler
* emits — paragraphs (rich runs, chips, inline graphics), table rows, vector
* shapes, lines, ellipses, polygons, free paths (with gradient fills and
* strokes), images, barcodes, transform groups, and navigation markers; clip
* regions degrade with a one-time warning — see
* {@code docs/architecture/backend-capability-matrix.md} for the live
* per-capability status. Multi-section rendering and render-to-images are not
* per-capability status and documented deviations. Multi-section rendering and render-to-images are not
* implemented yet and throw {@link UnsupportedOperationException}.</p>
*
* <p><b>Thread-safety:</b> immutable and reusable across renders; each render
Expand Down Expand Up @@ -107,7 +119,17 @@ private static List<PptxFragmentRenderHandler<?>> defaultHandlers() {
new PptxLineFragmentRenderHandler(),
new PptxEllipseFragmentRenderHandler(),
new PptxParagraphFragmentRenderHandler(),
new PptxTableRowFragmentRenderHandler());
new PptxTableRowFragmentRenderHandler(),
new PptxImageFragmentRenderHandler(),
new PptxBarcodeFragmentRenderHandler(),
new PptxPolygonFragmentRenderHandler(),
new PptxPathFragmentRenderHandler(),
new PptxTransformBeginRenderHandler(),
new PptxTransformEndRenderHandler(),
new PptxShapeClipBeginRenderHandler(),
new PptxShapeClipEndRenderHandler(),
new PptxAnchorMarkerRenderHandler(),
new PptxBookmarkMarkerRenderHandler());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFFontInfo;
import org.apache.poi.xslf.usermodel.XSLFPictureData;
import org.apache.poi.xslf.usermodel.XSLFGroupShape;
import org.apache.poi.xslf.usermodel.XSLFShapeContainer;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -37,8 +39,8 @@
* <p>The environment owns the slide surfaces for one document render pass plus
* the navigation bookkeeping that resolves after all fragments have been
* painted: bookmark records, anchor destinations, and fragment-level link
* rectangles. Handlers draw through {@link #slide(int)} and never create
* slides themselves.</p>
* rectangles. Handlers draw through {@link #surface(int)} - the innermost open
* transform group or the page's slide - and never create slides themselves.</p>
*
* <p>Navigation records are collected in slide top-down space (rectangles are
* top-left anchored, in points). Their emission — slide-jump hyperlinks and
Expand All @@ -62,10 +64,12 @@ public final class PptxRenderEnvironment {
private final Map<FontName, String> customFontFamilies;
private final Map<FontName, PptxViewerMetrics.ViewerFontMetrics> viewerFontMetrics;
private final Map<String, XSLFPictureData> pictureDataCache = new LinkedHashMap<>();
private final Map<String, java.awt.Dimension> pictureSizeCache = new LinkedHashMap<>();
private final List<BookmarkRecord> bookmarkRecords = new ArrayList<>();
private final Map<String, AnchorDestination> anchorDestinations = new LinkedHashMap<>();
private final List<FragmentLink> fragmentLinks = new ArrayList<>();
private final Set<String> substitutionWarned = new LinkedHashSet<>();
private final java.util.Deque<XSLFGroupShape> groupStack = new java.util.ArrayDeque<>();

PptxRenderEnvironment(XMLSlideShow show,
PptxRenderSession session,
Expand Down Expand Up @@ -134,6 +138,45 @@ public XSLFSlide slide(int pageIndex) {
return session.slide(pageIndex);
}

/**
* Returns the container new shapes must be created on: the innermost open
* transform group when one is active, otherwise the page's slide. The
* layout compiler guarantees begin/end markers stay balanced on one page,
* so the stack never leaks across slides.
*
* @param pageIndex zero-based page index
* @return active drawing container for that page
*/
public XSLFShapeContainer surface(int pageIndex) {
XSLFGroupShape group = groupStack.peek();
return group != null ? group : session.slide(pageIndex);
}

/**
* Opens a transform group as the active drawing container. Called by the
* transform-begin handler; every push is popped by the matching end
* marker.
*
* @param group group shape covering the transformed composite
*/
public void pushGroup(XSLFGroupShape group) {
groupStack.push(group);
}

/**
* Closes the innermost transform group.
*
* @return the group that was active
*/
public XSLFGroupShape popGroup() {
XSLFGroupShape group = groupStack.poll();
if (group == null) {
throw new IllegalStateException(
"Transform end marker without a matching begin: the group stack is empty.");
}
return group;
}

/**
* Returns the page height in points — the constant every vertical
* coordinate flips against when moving from the engine's bottom-left page
Expand Down Expand Up @@ -227,6 +270,28 @@ public double viewerAscent(TextStyle style, double fallbackAscent) {
style, font, viewerFontMetrics.get(style.fontName()));
}

/**
* Returns the image's intrinsic pixel size, decoded once per fingerprint.
* POI's header sniffing mis-reports some images (notably tiny PNGs), which
* would break the CONTAIN/COVER aspect math the PDF backend computes from
* the real bitmap — so both backends read the same decoded dimensions.
*
* @param imageData engine image payload
* @return decoded pixel dimensions, or {@code null} when undecodable
*/
public java.awt.Dimension pictureSize(ImageData imageData) {
return pictureSizeCache.computeIfAbsent(imageData.getFingerprint(), ignored -> {
try {
java.awt.image.BufferedImage decoded = javax.imageio.ImageIO.read(
new ByteArrayInputStream(imageData.getBytes()));
return decoded == null ? null
: new java.awt.Dimension(decoded.getWidth(), decoded.getHeight());
} catch (IOException exception) {
return null;
}
});
}

/**
* Resolves picture data once per distinct image fingerprint.
*
Expand Down Expand Up @@ -279,7 +344,14 @@ private static boolean embedFontFamily(XMLSlideShow show,
return true;
}

void registerBookmark(PlacedFragment fragment, DocumentBookmarkOptions bookmarkOptions) {
/**
* Records a bookmark for the navigation pass.
*
* @param fragment placed fragment carrying the bookmark
* @param bookmarkOptions resolved bookmark metadata
*/
@Internal
public void registerBookmark(PlacedFragment fragment, DocumentBookmarkOptions bookmarkOptions) {
bookmarkRecords.add(new BookmarkRecord(
bookmarkOptions.title(),
bookmarkOptions.level(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.demcha.compose.document.backend.fixed.pptx.handlers;

import com.demcha.compose.document.backend.fixed.pptx.PptxFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pptx.PptxRenderEnvironment;
import com.demcha.compose.document.layout.PlacedFragment;
import com.demcha.compose.document.layout.payloads.AnchorMarkerPayload;

/**
* Registers a named anchor destination; nothing is drawn. Slide-jump
* hyperlinks resolve against these records when navigation emission lands.
*
* @since 2.1.0
*/
public final class PptxAnchorMarkerRenderHandler
implements PptxFragmentRenderHandler<AnchorMarkerPayload> {

/**
* Creates the anchor-marker handler.
*/
public PptxAnchorMarkerRenderHandler() {
}

@Override
public Class<AnchorMarkerPayload> payloadType() {
return AnchorMarkerPayload.class;
}

@Override
public void render(PlacedFragment fragment,
AnchorMarkerPayload payload,
PptxRenderEnvironment environment) {
environment.registerAnchor(fragment, payload.anchor());
}
}
Loading
Loading