diff --git a/packages/api/src/files/documents/html.spec.ts b/packages/api/src/files/documents/html.spec.ts index 5b39cc0695..a4bc924025 100644 --- a/packages/api/src/files/documents/html.spec.ts +++ b/packages/api/src/files/documents/html.spec.ts @@ -466,23 +466,33 @@ describe('Office HTML producers', () => { * scroll horizontally and slides spill outside the viewport. * The bootstrap wraps each rendered slide in `.lc-slide-wrap` * and applies `transform: scale(panel_width / 960)` so the - * panel always fits — manual e2e feedback on PR #12934. */ + * panel always fits — manual e2e feedback on PR #12934. + * + * The wrap is applied ONCE, after `previewer.preview` resolves + * (and the post-render container is visible). Wrapping during + * streaming via MutationObserver caused pptx-preview to throw + * — its internal pipeline holds references to the appended + * slides and broke when we moved them under a parent wrap. */ const pptx = await buildPptx([{ title: 'A' }, { title: 'B' }]); const html = await _internal.pptxToHtmlViaCdn(pptx); - /* The wrapper class — used by the CSS rules that own the - * sized container. */ + /* The wrapper class used by the CSS rules. */ expect(html).toContain('lc-slide-wrap'); - /* The bootstrap script's wrap function name + the scale - * computation derived from `clientWidth / SLIDE_W`. */ - expect(html).toContain('wrapAndFit'); + /* The wrap function + the scale computation derived from + * `clientWidth / SLIDE_W`. */ + expect(html).toContain('wrapSlides'); expect(html).toContain('SLIDE_W'); expect(html).toContain('available / SLIDE_W'); - /* MutationObserver watches for streaming slide insertions; - * ResizeObserver re-fits on panel resize. */ - expect(html).toContain('MutationObserver'); + /* Container is hidden during render and revealed by the + * `finalize` step so the unscaled flash never reaches the + * user. */ + expect(html).toContain("container.style.visibility = 'hidden'"); + expect(html).toContain("container.style.visibility = 'visible'"); + /* ResizeObserver re-fits on panel resize. (No + * MutationObserver — streaming wraps broke pptx-preview.) */ expect(html).toContain('ResizeObserver'); - /* Native dimensions cached on the slide so re-fitting on - * resize doesn't measure the already-transformed box. */ + expect(html).not.toContain('MutationObserver'); + /* Native dimensions cached on the slide dataset so re-fitting + * on resize never measures an already-transformed box. */ expect(html).toContain('lcNativeW'); }); }); diff --git a/packages/api/src/files/documents/html.ts b/packages/api/src/files/documents/html.ts index bd6d6ece88..96bc424d92 100644 --- a/packages/api/src/files/documents/html.ts +++ b/packages/api/src/files/documents/html.ts @@ -1153,99 +1153,91 @@ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); fon /* pptx-preview renders into a fixed pixel size; we keep it at the * native 16:9 default so its internal layout calculations are * right, then post-process to scale each slide to the iframe's - * width via CSS transform. The wrap+transform approach (vs. - * shrinking the init dimensions) preserves font metrics, image - * resolution, and chart positioning that pptx-preview computed - * against the 960×540 canvas. */ + * width via CSS transform. */ var SLIDE_W = 960, SLIDE_H = 540; + /* Hide the container while pptx-preview renders so the user + * doesnt see the unscaled 960px-wide flash. We reveal once + * wrap+scale has settled. */ + container.style.visibility = 'hidden'; var previewer = pptxPreview.init(container, { width: SLIDE_W, height: SLIDE_H }); - /* Wrap a single slide element in a sized container and apply the - * scale transform. Native size is cached on the slide's dataset - * so subsequent fits don't measure the already-transformed box. */ - function wrapAndFit(slide, scale) { - if (slide.parentNode && slide.parentNode.classList && slide.parentNode.classList.contains('lc-slide-wrap')) { - return slide.parentNode; - } - var nativeW = parseFloat(slide.dataset.lcNativeW); - var nativeH = parseFloat(slide.dataset.lcNativeH); - if (!nativeW || !nativeH) { - nativeW = slide.offsetWidth || SLIDE_W; - nativeH = slide.offsetHeight || SLIDE_H; - slide.dataset.lcNativeW = String(nativeW); - slide.dataset.lcNativeH = String(nativeH); - } - var wrap = document.createElement('div'); - wrap.className = 'lc-slide-wrap'; - slide.parentNode.insertBefore(wrap, slide); - wrap.appendChild(slide); - wrap.style.width = (nativeW * scale) + 'px'; - wrap.style.height = (nativeH * scale) + 'px'; - slide.style.transform = 'scale(' + scale + ')'; - return wrap; - } - function computeScale() { - /* clientWidth excludes vertical scrollbar but includes our - * 16px horizontal padding (set via box-sizing:border-box). The - * cap at 1.0 means slides smaller than the panel render at 1:1 - * rather than upscaling and getting blurry. */ var available = (container.clientWidth || window.innerWidth) - 32; if (available <= 0) { available = 600; } return Math.min(1, available / SLIDE_W); } - function fitAll() { + /* Wrap each rendered slide and apply the scale. Called ONCE, + * after pptx-preview is done — wrapping during streaming would + * move slides out from under the librarys references and break + * its internal state. */ + function wrapSlides() { + var scale = computeScale(); + var children = Array.prototype.slice.call(container.children); + for (var i = 0; i < children.length; i++) { + var slide = children[i]; + if (!slide.classList || slide.classList.contains('lc-slide-wrap') || slide.classList.contains('lc-pptx-loading')) { + continue; + } + var nativeW = slide.offsetWidth || SLIDE_W; + var nativeH = slide.offsetHeight || SLIDE_H; + if (slide.dataset) { + slide.dataset.lcNativeW = String(nativeW); + slide.dataset.lcNativeH = String(nativeH); + } + var wrap = document.createElement('div'); + wrap.className = 'lc-slide-wrap'; + wrap.style.width = (nativeW * scale) + 'px'; + wrap.style.height = (nativeH * scale) + 'px'; + slide.style.transformOrigin = 'top left'; + slide.style.transform = 'scale(' + scale + ')'; + container.insertBefore(wrap, slide); + wrap.appendChild(slide); + } + } + + /* Re-fit existing wraps on iframe resize (panel drag, window + * resize). Reads the cached native dimensions from the slides + * dataset so we never re-measure an already-scaled box. */ + function refit() { var scale = computeScale(); - /* Re-fit any already-wrapped slides (window resize). */ var wraps = container.querySelectorAll('.lc-slide-wrap'); for (var i = 0; i < wraps.length; i++) { var wrap = wraps[i]; var inner = wrap.firstElementChild; - if (!inner) { continue; } + if (!inner || !inner.dataset) { continue; } var nativeW = parseFloat(inner.dataset.lcNativeW) || SLIDE_W; var nativeH = parseFloat(inner.dataset.lcNativeH) || SLIDE_H; wrap.style.width = (nativeW * scale) + 'px'; wrap.style.height = (nativeH * scale) + 'px'; inner.style.transform = 'scale(' + scale + ')'; } - /* Wrap any newly-appended slides (pptx-preview emits them - * incrementally as parsing completes). */ - var children = container.children; - for (var j = 0; j < children.length; j++) { - var el = children[j]; - if (el.classList && (el.classList.contains('lc-slide-wrap') || el.classList.contains('lc-pptx-loading'))) { - continue; - } - wrapAndFit(el, scale); + } + + function finalize() { + wrapSlides(); + container.style.visibility = 'visible'; + /* Resize observer set up AFTER the wrap so it doesnt fire + * during the wrap pass. */ + if (typeof ResizeObserver !== 'undefined') { + new ResizeObserver(refit).observe(document.body); + } else { + window.addEventListener('resize', refit); } - } - - /* Watch the container for slide insertions so streaming renders - * get scaled as they arrive (vs. waiting for the whole preview - * promise to settle). */ - if (typeof MutationObserver !== 'undefined') { - new MutationObserver(fitAll).observe(container, { childList: true }); - } - - /* Re-fit on iframe resize (panel drag, window resize). */ - if (typeof ResizeObserver !== 'undefined') { - new ResizeObserver(fitAll).observe(container); - } else { - window.addEventListener('resize', fitAll); + markSuccess(); } var result = previewer.preview(bytes.buffer); if (result && typeof result.then === 'function') { - result.then(function () { fitAll(); markSuccess(); }).catch(function (err) { showFallback(err && err.message); }); + result.then(finalize).catch(function (err) { showFallback(err && err.message); }); } - /* Final safety net: if 8 seconds in the renderer hasn't appended - * anything visible to the container, assume it failed silently. */ + /* Safety net: if 8 seconds in the preview promise hasnt resolved, + * accept whatever pptx-preview managed to render and apply the + * wrap+scale to it. Empty container → fallback. */ setTimeout(function () { if (settled) { return; } if (container && container.children.length > 0) { - fitAll(); - markSuccess(); + finalize(); return; } showFallback('renderer-timeout');