diff --git a/packages/api/src/files/documents/html.spec.ts b/packages/api/src/files/documents/html.spec.ts
index eca519f9a6..a5d1d54f77 100644
--- a/packages/api/src/files/documents/html.spec.ts
+++ b/packages/api/src/files/documents/html.spec.ts
@@ -419,14 +419,23 @@ describe('Office HTML producers', () => {
expect(html).toContain('id="lc-doc-data"');
expect(html).toContain('pptxPreview.init');
expect(html).toContain('cdn.jsdelivr.net/npm/pptx-preview@');
- // Slide-list signature must NOT appear (those are
).
- expect(html).not.toContain('class="lc-pptx-list"');
+ /* The slide-list `` is now embedded
+ * INSIDE the CDN doc as a runtime fallback for the silent-
+ * empty-render case (pptx-preview's compatibility issues with
+ * pptxgenjs decks). It lives in the hidden `#lc-fallback`
+ * block; the bootstrap reveals it when `hasRenderedContent()`
+ * detects empty wrappers. Manual e2e on PR #12934. */
+ expect(html).toContain('class="lc-pptx-list"');
+ expect(html).toContain('id="lc-fallback"');
});
describe('CDN-rendered path', () => {
test('embeds the binary as base64 that round-trips to the original bytes', async () => {
const original = await buildPptx([{ title: 'Test' }]);
- const html = await _internal.pptxToHtmlViaCdn(original);
+ const html = await _internal.pptxToHtmlViaCdn(
+ original,
+ '- fb
',
+ );
const match = html.match(
/
-
Preview unavailable. Please download the file to view it.
-
+ High-fidelity renderer unavailable or returned no slide content. Showing the simplified slide-list view below.
+ ${slideListFallbackBody}
+
Diagnostic details
@@ -1162,8 +1182,10 @@ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); fon
if (loading) { loading.remove(); }
var container = document.getElementById('lc-render');
if (container) {
- // Wipe any partial render the library left behind so the
- // fallback message isn't competing with a half-rendered slide.
+ /* Hide the render slot (which may contain pptx-previews
+ * empty/black wrappers) so the slide-list fallback owns the
+ * viewport. */
+ container.hidden = true;
container.innerHTML = '';
}
var fallback = document.getElementById('lc-fallback');
@@ -1300,8 +1322,32 @@ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); fon
}
}
+ /* Detect the silent-failure case: pptx-preview created the slide
+ * wrappers but didnt populate them with content. Common with
+ * pptxgenjs-generated PPTXs — pptx-previews parser handles
+ * Microsoft PowerPoint XML but chokes on subtle convention
+ * differences and produces empty 960x540 wrappers. Check if every
+ * wrapped slide has zero children; if so, the render failed and
+ * the slide-list fallback should take over. Manual e2e on PR
+ * #12934. */
+ function hasRenderedContent() {
+ var wraps = container.querySelectorAll('.lc-slide-wrap');
+ if (wraps.length === 0) { return false; }
+ for (var i = 0; i < wraps.length; i++) {
+ var inner = wraps[i].firstElementChild;
+ if (inner && inner.children.length > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
function finalize() {
wrapSlides();
+ if (!hasRenderedContent()) {
+ showFallback('renderer-produced-empty-wrappers (pptx-preview compatibility issue with this PPTX format — falling back to slide-list view)');
+ return;
+ }
container.style.visibility = 'visible';
/* Resize observer set up AFTER the wrap so it doesnt fire
* during the wrap pass. */
@@ -1337,8 +1383,21 @@ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); fon
`;
}
-async function pptxToHtmlViaCdn(buffer: Buffer): Promise {
- return buildPptxCdnDocument(buffer.toString('base64'));
+/**
+ * Render the slide-list body (just the `` HTML) for a buffer.
+ * Used by `pptxToHtml` to embed as a fallback inside the CDN doc and
+ * by `pptxToSlideListHtmlInternal` for the standalone path. */
+async function renderPptxSlidesBodyForBuffer(buffer: Buffer): Promise {
+ const rawSlides = await extractPptxSlideXml(buffer);
+ const slides: PptxSlide[] = rawSlides.map(({ number, xml }) => {
+ const { title, body } = extractSlideText(xml);
+ return { number, title, body };
+ });
+ return renderPptxSlidesBody(slides);
+}
+
+async function pptxToHtmlViaCdn(buffer: Buffer, slideListFallbackBody: string): Promise {
+ return buildPptxCdnDocument(buffer.toString('base64'), slideListFallbackBody);
}
/**
@@ -1370,13 +1429,25 @@ export async function pptxToHtml(buffer: Buffer): Promise {
if (lo) {
return lo;
}
- if (isOfficePreviewCdnDisabled()) {
+ if (isOfficePreviewCdnDisabled() || buffer.length > MAX_PPTX_CDN_BINARY_BYTES) {
return pptxToSlideListHtmlInternal(buffer);
}
- if (buffer.length <= MAX_PPTX_CDN_BINARY_BYTES) {
- return pptxToHtmlViaCdn(buffer);
+ /* Render the slide-list first so we can embed it inside the CDN doc
+ * as a runtime fallback. pptx-preview is unreliable on PPTXs not
+ * generated by Microsoft PowerPoint (e.g. pptxgenjs decks) — it
+ * silently produces empty wrappers. The iframe bootstrap detects
+ * the empty-render case and reveals this slide-list fallback so the
+ * user always gets readable content. Manual e2e on PR #12934. */
+ const slideListBody = await renderPptxSlidesBodyForBuffer(buffer);
+ const cdnDoc = await pptxToHtmlViaCdn(buffer, slideListBody);
+ /* Combined size budget: if base64 binary + slide-list fallback +
+ * wrapper would exceed the cache cap, drop CDN entirely and ship
+ * the slide-list standalone. Same pattern as the DOCX dispatcher's
+ * size budget. */
+ if (Buffer.byteLength(cdnDoc, 'utf-8') > OFFICE_HTML_OUTPUT_CAP) {
+ return pptxToSlideListHtmlInternal(buffer);
}
- return pptxToSlideListHtmlInternal(buffer);
+ return cdnDoc;
}
/**