@extend-ai/react-pptx 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9247,8 +9247,71 @@ var EMU_PER_CSS_PIXEL = 9525;
9247
9247
  var DEFAULT_TEXT_HORIZONTAL_INSET_EMU = 91440;
9248
9248
  var DEFAULT_TEXT_VERTICAL_INSET_EMU = 45720;
9249
9249
  var POWERPOINT_SINGLE_LINE_HEIGHT = 1.15;
9250
+ var PresentationAssetCache = class {
9251
+ references = 0;
9252
+ objectUrls = /* @__PURE__ */ new Set();
9253
+ assetUrls = /* @__PURE__ */ new Map();
9254
+ metafileUrls = /* @__PURE__ */ new Map();
9255
+ acquire() {
9256
+ this.references += 1;
9257
+ return this;
9258
+ }
9259
+ metafileUrl(assetId, contentType, data) {
9260
+ const cached = this.metafileUrls.get(assetId);
9261
+ if (cached) return cached;
9262
+ const convert = contentType.includes("wmf") ? renderWmfToDataUrl : renderEmfToDataUrl;
9263
+ const ownedBytes = data.slice();
9264
+ const pending = convert(ownedBytes.buffer, 2048, 2048, {
9265
+ dpiScale: 2,
9266
+ maxCanvasDimension: 4096,
9267
+ maxRecords: 2e5,
9268
+ fontFamilyMap: METAFILE_FONT_MAP
9269
+ }).then((url) => url || void 0);
9270
+ this.metafileUrls.set(assetId, pending);
9271
+ return pending;
9272
+ }
9273
+ assetUrl(asset) {
9274
+ if (asset.url) return Promise.resolve(asset.url);
9275
+ if (!asset.data) return Promise.resolve(void 0);
9276
+ const contentType = asset.contentType.toLowerCase();
9277
+ if (contentType.includes("emf") || contentType.includes("wmf")) {
9278
+ return this.metafileUrl(asset.id, contentType, asset.data);
9279
+ }
9280
+ const cached = this.assetUrls.get(asset.id);
9281
+ if (cached) return Promise.resolve(cached);
9282
+ const ownedBytes = asset.data.slice();
9283
+ const url = URL.createObjectURL(
9284
+ new Blob([ownedBytes.buffer], { type: asset.contentType })
9285
+ );
9286
+ this.objectUrls.add(url);
9287
+ this.assetUrls.set(asset.id, url);
9288
+ return Promise.resolve(url);
9289
+ }
9290
+ release() {
9291
+ this.references = Math.max(0, this.references - 1);
9292
+ if (this.references > 0) return;
9293
+ for (const url of this.objectUrls) URL.revokeObjectURL(url);
9294
+ this.objectUrls.clear();
9295
+ this.assetUrls.clear();
9296
+ this.metafileUrls.clear();
9297
+ }
9298
+ };
9299
+ var presentationAssetCaches = /* @__PURE__ */ new WeakMap();
9300
+ function acquirePresentationAssetCache(presentation) {
9301
+ let cache = presentationAssetCaches.get(presentation);
9302
+ if (!cache) {
9303
+ cache = new PresentationAssetCache();
9304
+ presentationAssetCaches.set(presentation, cache);
9305
+ }
9306
+ return cache.acquire();
9307
+ }
9250
9308
  var LIST_ITEM_GAP = 24;
9251
9309
  var FALLBACK_VIEWPORT_RECT = { width: 800, height: 600 };
9310
+ function scaleListAnchorOffset(offset, previousSlideHeight, nextSlideHeight) {
9311
+ if (previousSlideHeight <= 0) return offset;
9312
+ if (offset <= previousSlideHeight) return offset * (nextSlideHeight / previousSlideHeight);
9313
+ return nextSlideHeight + (offset - previousSlideHeight);
9314
+ }
9252
9315
  var METAFILE_FONT_MAP = Object.fromEntries(
9253
9316
  Object.entries(OFFICE_FONT_FALLBACKS).map(([family, fallbacks]) => [
9254
9317
  family,
@@ -9783,6 +9846,7 @@ var NormalizedPresentationViewer = class {
9783
9846
  this.container = container;
9784
9847
  this.presentation = presentation;
9785
9848
  this.callbacks = callbacks;
9849
+ this.assetCache = acquirePresentationAssetCache(presentation);
9786
9850
  }
9787
9851
  container;
9788
9852
  presentation;
@@ -9790,12 +9854,10 @@ var NormalizedPresentationViewer = class {
9790
9854
  current = 0;
9791
9855
  zoom = 100;
9792
9856
  fit = "contain";
9793
- objectUrls = /* @__PURE__ */ new Set();
9794
- assetUrls = /* @__PURE__ */ new Map();
9795
- metafileUrls = /* @__PURE__ */ new Map();
9857
+ assetCache;
9796
9858
  listCleanup;
9797
9859
  highlight;
9798
- pendingResources = /* @__PURE__ */ new Set();
9860
+ activeMountResources;
9799
9861
  mountedHandles = /* @__PURE__ */ new Set();
9800
9862
  handlesByTarget = /* @__PURE__ */ new Map();
9801
9863
  destroyed = false;
@@ -9826,6 +9888,10 @@ var NormalizedPresentationViewer = class {
9826
9888
  const height = this.presentation.size.heightEmu / EMU_PER_CSS_PIXEL;
9827
9889
  return Number.isFinite(height) && height > 0 ? height : 1;
9828
9890
  }
9891
+ scaleForViewport(viewportWidth) {
9892
+ const fitScale = this.fit === "contain" ? Math.min(1, (viewportWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
9893
+ return fitScale * (this.zoom / 100);
9894
+ }
9829
9895
  isRenderActive(generation) {
9830
9896
  return !this.destroyed && generation === this.renderGeneration;
9831
9897
  }
@@ -9844,46 +9910,19 @@ var NormalizedPresentationViewer = class {
9844
9910
  this.lastNotifiedSlide = index;
9845
9911
  this.callbacks.onSlideChange?.(index);
9846
9912
  }
9847
- metafileUrl(assetId, contentType, data) {
9848
- const cached = this.metafileUrls.get(assetId);
9849
- if (cached) return cached;
9850
- const convert = contentType.includes("wmf") ? renderWmfToDataUrl : renderEmfToDataUrl;
9851
- const ownedBytes = data.slice();
9852
- const pending = convert(ownedBytes.buffer, 2048, 2048, {
9853
- dpiScale: 2,
9854
- maxCanvasDimension: 4096,
9855
- maxRecords: 2e5,
9856
- fontFamilyMap: METAFILE_FONT_MAP
9857
- }).then((url) => url || void 0);
9858
- this.metafileUrls.set(assetId, pending);
9859
- return pending;
9860
- }
9861
9913
  assetUrl(asset) {
9862
- if (asset.url) return Promise.resolve(asset.url);
9863
- if (!asset.data) return Promise.resolve(void 0);
9864
- const contentType = asset.contentType.toLowerCase();
9865
- if (contentType.includes("emf") || contentType.includes("wmf")) {
9866
- return this.metafileUrl(asset.id, contentType, asset.data);
9867
- }
9868
- const cached = this.assetUrls.get(asset.id);
9869
- if (cached) return Promise.resolve(cached);
9870
- const ownedBytes = asset.data.slice();
9871
- const url = URL.createObjectURL(
9872
- new Blob([ownedBytes.buffer], { type: asset.contentType })
9873
- );
9874
- this.objectUrls.add(url);
9875
- this.assetUrls.set(asset.id, url);
9876
- return Promise.resolve(url);
9914
+ return this.assetCache.assetUrl(asset);
9877
9915
  }
9878
9916
  applyAsset(assetId, apply, nodeId) {
9879
9917
  const asset = this.presentation.assets[assetId];
9880
9918
  if (!asset) return;
9919
+ const resources = this.activeMountResources;
9881
9920
  const pending = this.assetUrl(asset).then((url) => {
9882
9921
  if (url && !this.destroyed) apply(url);
9883
9922
  }).catch((error) => {
9884
9923
  if (!this.destroyed) this.callbacks.onNodeError?.(nodeId, error);
9885
- }).finally(() => this.pendingResources.delete(pending));
9886
- this.pendingResources.add(pending);
9924
+ });
9925
+ resources?.add(pending);
9887
9926
  }
9888
9927
  /**
9889
9928
  * Applies DrawingML picture recolor effects (`a:biLevel`, `a:grayscl`,
@@ -10039,11 +10078,7 @@ var NormalizedPresentationViewer = class {
10039
10078
  image.setAttribute("preserveAspectRatio", "none");
10040
10079
  image.setAttribute("clip-path", `url(#${clipId})`);
10041
10080
  image.style.opacity = String(Math.max(0, Math.min(1, fillStyle.opacity ?? 1)));
10042
- this.applyAsset(
10043
- fillStyle.assetId,
10044
- (url) => image.setAttribute("href", url),
10045
- node.id
10046
- );
10081
+ this.applyAsset(fillStyle.assetId, (url) => image.setAttribute("href", url), node.id);
10047
10082
  svg.append(image);
10048
10083
  }
10049
10084
  if (node.line) {
@@ -10406,23 +10441,35 @@ var NormalizedPresentationViewer = class {
10406
10441
  element,
10407
10442
  target,
10408
10443
  ready: Promise.resolve(),
10444
+ setScale: () => {
10445
+ },
10409
10446
  dispose: () => element.remove()
10410
10447
  };
10411
10448
  }
10412
10449
  mount(index, target, scale) {
10413
10450
  if (this.destroyed) return this.emptyHandle(target);
10414
10451
  this.handlesByTarget.get(target)?.dispose();
10415
- const slide = this.createSlide(index);
10416
- const fitScale = this.fit === "contain" ? Math.min(1, (target.clientWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
10417
- const effectiveScale = scale ?? fitScale * (this.zoom / 100);
10452
+ const resources = /* @__PURE__ */ new Set();
10453
+ const previousResources = this.activeMountResources;
10454
+ this.activeMountResources = resources;
10455
+ let slide;
10456
+ try {
10457
+ slide = this.createSlide(index);
10458
+ } finally {
10459
+ this.activeMountResources = previousResources;
10460
+ }
10461
+ const effectiveScale = scale ?? this.scaleForViewport(target.clientWidth);
10418
10462
  const wrapper = document.createElement("div");
10419
10463
  wrapper.dataset.rpvSlideWrapper = String(index);
10420
10464
  wrapper.style.position = "relative";
10421
- wrapper.style.width = `${this.naturalSlideWidth * effectiveScale}px`;
10422
- wrapper.style.height = `${this.naturalSlideHeight * effectiveScale}px`;
10423
10465
  wrapper.style.margin = "0 auto";
10424
10466
  slide.style.transformOrigin = "top left";
10425
- slide.style.transform = `scale(${effectiveScale})`;
10467
+ const setScale = (nextScale) => {
10468
+ wrapper.style.width = `${this.naturalSlideWidth * nextScale}px`;
10469
+ wrapper.style.height = `${this.naturalSlideHeight * nextScale}px`;
10470
+ slide.style.transform = `scale(${nextScale})`;
10471
+ };
10472
+ setScale(effectiveScale);
10426
10473
  wrapper.append(slide);
10427
10474
  target.replaceChildren(wrapper);
10428
10475
  this.callbacks.onSlideRendered?.(index, slide);
@@ -10430,7 +10477,8 @@ var NormalizedPresentationViewer = class {
10430
10477
  const handle = {
10431
10478
  element: slide,
10432
10479
  target,
10433
- ready: Promise.all([...this.pendingResources]).then(() => void 0),
10480
+ ready: Promise.all([...resources]).then(() => void 0),
10481
+ setScale,
10434
10482
  dispose: () => {
10435
10483
  if (disposed) return;
10436
10484
  disposed = true;
@@ -10458,6 +10506,32 @@ var NormalizedPresentationViewer = class {
10458
10506
  await Promise.resolve();
10459
10507
  }
10460
10508
  }
10509
+ async yieldNavigationPaint() {
10510
+ if (typeof requestAnimationFrame !== "undefined") {
10511
+ await new Promise((resolve) => {
10512
+ let settled = false;
10513
+ let firstFrame;
10514
+ let secondFrame;
10515
+ const complete = () => {
10516
+ if (settled) return;
10517
+ settled = true;
10518
+ clearTimeout(timeout2);
10519
+ if (typeof cancelAnimationFrame !== "undefined") {
10520
+ if (firstFrame !== void 0) cancelAnimationFrame(firstFrame);
10521
+ if (secondFrame !== void 0) cancelAnimationFrame(secondFrame);
10522
+ }
10523
+ resolve();
10524
+ };
10525
+ const timeout2 = setTimeout(complete, 100);
10526
+ firstFrame = requestAnimationFrame(() => {
10527
+ if (settled) return;
10528
+ secondFrame = requestAnimationFrame(complete);
10529
+ });
10530
+ });
10531
+ } else {
10532
+ await new Promise((resolve) => setTimeout(resolve, 0));
10533
+ }
10534
+ }
10461
10535
  async mountInBatches(placeholders, indices, batchSize, generation) {
10462
10536
  const uniqueIndices = [...new Set(indices)].filter(
10463
10537
  (index) => index >= 0 && index < placeholders.length
@@ -10490,12 +10564,13 @@ var NormalizedPresentationViewer = class {
10490
10564
  createListPlaceholder(index, item, generation, scale) {
10491
10565
  let handle;
10492
10566
  let mountPromise;
10567
+ let effectiveScale = scale;
10493
10568
  const isActive = () => this.isRenderActive(generation) && this.listState?.generation === generation;
10494
10569
  const mount = () => {
10495
10570
  if (!isActive()) return Promise.resolve();
10496
10571
  if (handle) return handle.ready;
10497
10572
  if (mountPromise) return mountPromise;
10498
- const nextHandle = this.mount(index, item, scale);
10573
+ const nextHandle = this.mount(index, item, effectiveScale);
10499
10574
  handle = nextHandle;
10500
10575
  const pending = nextHandle.ready.then(() => {
10501
10576
  if (!isActive() || handle !== nextHandle) {
@@ -10511,11 +10586,16 @@ var NormalizedPresentationViewer = class {
10511
10586
  const unmount = () => {
10512
10587
  const mounted = handle;
10513
10588
  handle = void 0;
10589
+ mountPromise = void 0;
10514
10590
  if (mounted) {
10515
10591
  mounted.dispose();
10516
10592
  }
10517
10593
  };
10518
- return { item, mount, unmount, isMounted: () => Boolean(handle) };
10594
+ const setScale = (nextScale) => {
10595
+ effectiveScale = nextScale;
10596
+ handle?.setScale(nextScale);
10597
+ };
10598
+ return { item, mount, unmount, isMounted: () => Boolean(handle), setScale };
10519
10599
  }
10520
10600
  async renderList(options = {}) {
10521
10601
  const windowingEnabled = options.enabled !== false;
@@ -10539,11 +10619,9 @@ var NormalizedPresentationViewer = class {
10539
10619
  Math.min(this.slideCount - 1, options.initialSlideIndex ?? this.current)
10540
10620
  );
10541
10621
  const scroller = options.scrollElement ?? this.container;
10542
- const viewportWidth = scroller.clientWidth;
10543
- const estimatedFitScale = this.fit === "contain" ? Math.min(1, (viewportWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
10544
- const effectiveScale = estimatedFitScale * (this.zoom / 100);
10545
- const slideHeight = this.naturalSlideHeight * effectiveScale;
10546
- const itemStride = slideHeight + LIST_ITEM_GAP;
10622
+ let effectiveScale = this.scaleForViewport(scroller.clientWidth);
10623
+ let slideHeight = this.naturalSlideHeight * effectiveScale;
10624
+ let itemStride = slideHeight + LIST_ITEM_GAP;
10547
10625
  if (!windowingEnabled) {
10548
10626
  const placeholders2 = this.presentation.slides.map((_, index) => {
10549
10627
  const item = document.createElement("div");
@@ -10551,9 +10629,35 @@ var NormalizedPresentationViewer = class {
10551
10629
  item.style.minHeight = `${slideHeight}px`;
10552
10630
  item.style.margin = `0 auto ${LIST_ITEM_GAP}px`;
10553
10631
  this.container.append(item);
10554
- return this.createListPlaceholder(index, item, generation);
10632
+ return this.createListPlaceholder(index, item, generation, effectiveScale);
10555
10633
  });
10556
10634
  const state2 = { generation, options: normalizedOptions, placeholders: placeholders2 };
10635
+ state2.updateScale = () => {
10636
+ if (!this.isRenderActive(generation) || this.listState !== state2) return;
10637
+ const previousStride = itemStride;
10638
+ const previousSlideHeight = slideHeight;
10639
+ const listStart = scroller === this.container ? 0 : this.container.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
10640
+ const listOffset = scroller.scrollTop - listStart;
10641
+ const anchorIndex = Math.max(
10642
+ 0,
10643
+ Math.min(this.slideCount - 1, Math.floor(listOffset / previousStride))
10644
+ );
10645
+ const anchorOffset = listOffset - anchorIndex * previousStride;
10646
+ effectiveScale = this.scaleForViewport(scroller.clientWidth);
10647
+ slideHeight = this.naturalSlideHeight * effectiveScale;
10648
+ itemStride = slideHeight + LIST_ITEM_GAP;
10649
+ placeholders2.forEach((placeholder) => {
10650
+ placeholder.item.style.minHeight = `${slideHeight}px`;
10651
+ placeholder.setScale(effectiveScale);
10652
+ });
10653
+ if (listOffset < 0) return;
10654
+ const nextAnchorOffset = scaleListAnchorOffset(
10655
+ anchorOffset,
10656
+ previousSlideHeight,
10657
+ slideHeight
10658
+ );
10659
+ scroller.scrollTop = Math.max(0, listStart + anchorIndex * itemStride + nextAnchorOffset);
10660
+ };
10557
10661
  this.listState = state2;
10558
10662
  await this.mountInBatches(
10559
10663
  placeholders2,
@@ -10595,6 +10699,20 @@ var NormalizedPresentationViewer = class {
10595
10699
  const state = { generation, options: normalizedOptions, placeholders };
10596
10700
  this.listState = state;
10597
10701
  const isActive = () => this.isRenderActive(generation) && this.listState === state;
10702
+ const deferredMounts = /* @__PURE__ */ new Map();
10703
+ let virtualWindow = /* @__PURE__ */ new Set();
10704
+ state.deferMount = (index) => {
10705
+ deferredMounts.set(index, (deferredMounts.get(index) ?? 0) + 1);
10706
+ };
10707
+ state.resumeMount = (index) => {
10708
+ const remaining = (deferredMounts.get(index) ?? 0) - 1;
10709
+ if (remaining > 0) {
10710
+ deferredMounts.set(index, remaining);
10711
+ return;
10712
+ }
10713
+ deferredMounts.delete(index);
10714
+ if (virtualWindow.has(index)) void placeholders[index]?.mount();
10715
+ };
10598
10716
  const measureViewportRect = () => {
10599
10717
  const rect = scroller.getBoundingClientRect();
10600
10718
  if (rect.width > 0 && rect.height > 0) return { width: rect.width, height: rect.height };
@@ -10606,16 +10724,25 @@ var NormalizedPresentationViewer = class {
10606
10724
  const initialRect = measureViewportRect();
10607
10725
  const requestedOverscan = options.overscanViewport ?? 1.5;
10608
10726
  const overscanViewports = Number.isFinite(requestedOverscan) && requestedOverscan >= 0 ? requestedOverscan : 1.5;
10609
- const overscan = Math.max(1, Math.ceil(overscanViewports * initialRect.height / itemStride));
10727
+ let overscan = Math.max(1, Math.ceil(overscanViewports * initialRect.height / itemStride));
10610
10728
  const measureScrollMargin = () => sizer.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
10611
10729
  let scrollMargin = measureScrollMargin();
10612
10730
  let needsSettleAlignment = initialIndex > 0;
10613
10731
  let lastProgrammaticTop;
10732
+ const writeScrollTop = (top, behavior) => {
10733
+ lastProgrammaticTop = top;
10734
+ if (typeof scroller.scrollTo === "function") {
10735
+ scroller.scrollTo(behavior ? { top, behavior } : { top });
10736
+ } else {
10737
+ scroller.scrollTop = top;
10738
+ }
10739
+ };
10614
10740
  const reconcile = (instance) => {
10615
10741
  if (!isActive()) return;
10616
10742
  const windowed = new Set(instance.getVirtualItems().map((item) => item.index));
10743
+ virtualWindow = windowed;
10617
10744
  placeholders.forEach((placeholder, index) => {
10618
- if (windowed.has(index)) void placeholder.mount();
10745
+ if (windowed.has(index) && !deferredMounts.has(index)) void placeholder.mount();
10619
10746
  else placeholder.unmount();
10620
10747
  });
10621
10748
  const offset = (instance.scrollOffset ?? 0) - scrollMargin;
@@ -10678,12 +10805,7 @@ var NormalizedPresentationViewer = class {
10678
10805
  },
10679
10806
  scrollToFn: (offset, { adjustments, behavior }) => {
10680
10807
  const top = offset + (adjustments ?? 0);
10681
- lastProgrammaticTop = top;
10682
- if (typeof scroller.scrollTo === "function") {
10683
- scroller.scrollTo(behavior ? { top, behavior } : { top });
10684
- } else {
10685
- scroller.scrollTop = top;
10686
- }
10808
+ writeScrollTop(top, behavior);
10687
10809
  },
10688
10810
  onChange: (instance) => reconcile(instance)
10689
10811
  });
@@ -10694,6 +10816,39 @@ var NormalizedPresentationViewer = class {
10694
10816
  behavior: scrollOptions?.behavior === "smooth" ? "smooth" : "auto"
10695
10817
  });
10696
10818
  };
10819
+ state.updateScale = () => {
10820
+ if (!isActive()) return;
10821
+ const previousStride = itemStride;
10822
+ const previousSlideHeight = slideHeight;
10823
+ const anchorIndex = this.current;
10824
+ const observedScrollTop = virtualizer.scrollOffset ?? scroller.scrollTop;
10825
+ const observedIndex = Math.floor((observedScrollTop - scrollMargin) / previousStride);
10826
+ const intendedIndex = lastProgrammaticTop === void 0 ? void 0 : Math.floor((lastProgrammaticTop - scrollMargin) / previousStride);
10827
+ const previousScrollTop = Math.abs(observedIndex - anchorIndex) > 1 && intendedIndex !== void 0 && Math.abs(intendedIndex - anchorIndex) <= 1 ? lastProgrammaticTop : observedScrollTop;
10828
+ const previousOffset = previousScrollTop - scrollMargin;
10829
+ const anchorOffset = previousOffset - anchorIndex * previousStride;
10830
+ effectiveScale = this.scaleForViewport(scroller.clientWidth);
10831
+ slideHeight = this.naturalSlideHeight * effectiveScale;
10832
+ itemStride = slideHeight + LIST_ITEM_GAP;
10833
+ sizer.style.height = `${this.slideCount * itemStride - LIST_ITEM_GAP}px`;
10834
+ placeholders.forEach((placeholder, index) => {
10835
+ placeholder.item.style.height = `${slideHeight}px`;
10836
+ placeholder.item.style.transform = `translateY(${index * itemStride}px)`;
10837
+ placeholder.setScale(effectiveScale);
10838
+ });
10839
+ const nextAnchorOffset = scaleListAnchorOffset(
10840
+ anchorOffset,
10841
+ previousSlideHeight,
10842
+ slideHeight
10843
+ );
10844
+ const nextScrollTop = previousOffset < 0 ? previousScrollTop : Math.max(0, scrollMargin + anchorIndex * itemStride + nextAnchorOffset);
10845
+ const viewportRect = measureViewportRect();
10846
+ overscan = Math.max(1, Math.ceil(overscanViewports * viewportRect.height / itemStride));
10847
+ virtualizer.scrollOffset = nextScrollTop;
10848
+ writeScrollTop(nextScrollTop);
10849
+ virtualizer.setOptions({ ...virtualizer.options, overscan });
10850
+ virtualizer.measure();
10851
+ };
10697
10852
  const teardown = virtualizer._didMount();
10698
10853
  this.listCleanup = teardown;
10699
10854
  virtualizer._willUpdate();
@@ -10721,16 +10876,27 @@ var NormalizedPresentationViewer = class {
10721
10876
  if (state && this.isRenderActive(state.generation)) {
10722
10877
  const navigation = ++this.navigationGeneration;
10723
10878
  const placeholder = state.placeholders[next];
10724
- await placeholder?.mount();
10725
- if (this.destroyed || navigation !== this.navigationGeneration || !this.isRenderActive(state.generation) || this.listState !== state) {
10726
- return;
10727
- }
10728
- if (changed) {
10729
- if (state.scrollToIndex) state.scrollToIndex(next, scrollOptions);
10730
- else placeholder?.item.scrollIntoView?.(scrollOptions);
10879
+ let mountDeferred = Boolean(state.deferMount && placeholder && !placeholder.isMounted());
10880
+ if (mountDeferred) state.deferMount?.(next);
10881
+ try {
10882
+ if (changed) {
10883
+ if (state.scrollToIndex) state.scrollToIndex(next, scrollOptions);
10884
+ else placeholder?.item.scrollIntoView?.(scrollOptions);
10885
+ this.current = next;
10886
+ this.notifySlideChange(next);
10887
+ }
10888
+ if (mountDeferred) await this.yieldNavigationPaint();
10889
+ if (this.destroyed || navigation !== this.navigationGeneration || !this.isRenderActive(state.generation) || this.listState !== state) {
10890
+ return;
10891
+ }
10892
+ if (mountDeferred) {
10893
+ state.resumeMount?.(next);
10894
+ mountDeferred = false;
10895
+ }
10896
+ await placeholder?.mount();
10897
+ } finally {
10898
+ if (mountDeferred) state.resumeMount?.(next);
10731
10899
  }
10732
- this.current = next;
10733
- if (changed) this.notifySlideChange(next);
10734
10900
  return;
10735
10901
  }
10736
10902
  if (!changed && this.container.querySelector(`[data-rpv-slide-index="${next}"]`)) return;
@@ -10741,15 +10907,33 @@ var NormalizedPresentationViewer = class {
10741
10907
  if (this.zoom === next) return;
10742
10908
  this.zoom = next;
10743
10909
  if (this.renderState?.mode === "continuous") {
10744
- await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10745
- } else if (this.renderState) await this.renderSlide(this.current);
10910
+ const state = this.listState;
10911
+ if (state && this.isRenderActive(state.generation) && state.updateScale) {
10912
+ state.updateScale();
10913
+ } else {
10914
+ await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10915
+ }
10916
+ } else if (this.renderState) {
10917
+ const handle = this.handlesByTarget.get(this.container);
10918
+ if (handle) handle.setScale(this.scaleForViewport(this.container.clientWidth));
10919
+ else await this.renderSlide(this.current);
10920
+ }
10746
10921
  }
10747
10922
  async setFitMode(mode) {
10748
10923
  if (this.fit === mode) return;
10749
10924
  this.fit = mode;
10750
10925
  if (this.renderState?.mode === "continuous") {
10751
- await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10752
- } else if (this.renderState) await this.renderSlide(this.current);
10926
+ const state = this.listState;
10927
+ if (state && this.isRenderActive(state.generation) && state.updateScale) {
10928
+ state.updateScale();
10929
+ } else {
10930
+ await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10931
+ }
10932
+ } else if (this.renderState) {
10933
+ const handle = this.handlesByTarget.get(this.container);
10934
+ if (handle) handle.setScale(this.scaleForViewport(this.container.clientWidth));
10935
+ else await this.renderSlide(this.current);
10936
+ }
10753
10937
  }
10754
10938
  searchText(query, options) {
10755
10939
  return searchDocument(this.presentation, query, options);
@@ -10827,11 +11011,8 @@ var NormalizedPresentationViewer = class {
10827
11011
  this.listCleanup = void 0;
10828
11012
  this.listState = void 0;
10829
11013
  this.clearSearchHighlights();
10830
- for (const url of this.objectUrls) URL.revokeObjectURL(url);
10831
- this.objectUrls.clear();
10832
- this.assetUrls.clear();
10833
- this.metafileUrls.clear();
10834
- this.pendingResources.clear();
11014
+ this.assetCache.release();
11015
+ this.activeMountResources = void 0;
10835
11016
  for (const handle of [...this.mountedHandles]) handle.dispose();
10836
11017
  this.handlesByTarget.clear();
10837
11018
  this.renderState = void 0;
@@ -10990,6 +11171,18 @@ async function parseInWorker(bytes, signal, wasmSource) {
10990
11171
  });
10991
11172
  }
10992
11173
 
11174
+ // src/performance.ts
11175
+ function performanceNow() {
11176
+ return globalThis.performance?.now?.() ?? Date.now();
11177
+ }
11178
+ function performanceMeasurement(phase, startedAt, slideIndex) {
11179
+ return {
11180
+ durationMs: Math.max(0, performanceNow() - startedAt),
11181
+ phase,
11182
+ ...slideIndex === void 0 ? {} : { slideIndex }
11183
+ };
11184
+ }
11185
+
10993
11186
  // src/parse.ts
10994
11187
  var DEFAULT_MAX_INPUT_BYTES = 100 * 1024 * 1024;
10995
11188
  function isParsedPresentation(source) {
@@ -11074,14 +11267,20 @@ async function parsePresentation(source, options = {}) {
11074
11267
  warnings: source.warnings
11075
11268
  };
11076
11269
  }
11270
+ const downloadStartedAt = performanceNow();
11077
11271
  const buffer = await readSource(source, options);
11272
+ if (typeof source === "string" || source instanceof URL) {
11273
+ options.onPerformanceMeasurement?.(performanceMeasurement("download", downloadStartedAt));
11274
+ }
11078
11275
  const format = detectFormat(buffer, options.formatHint);
11079
11276
  const bytes = new Uint8Array(buffer);
11080
11277
  throwIfAborted(options.signal);
11278
+ const parsingStartedAt = performanceNow();
11081
11279
  const nativeDocument = await parseWithWasm(bytes, {
11082
11280
  formatHint: format,
11083
11281
  ...options.signal ? { signal: options.signal } : {}
11084
11282
  });
11283
+ options.onPerformanceMeasurement?.(performanceMeasurement("wasm-parsing", parsingStartedAt));
11085
11284
  throwIfAborted(options.signal);
11086
11285
  if (!nativeDocument) {
11087
11286
  throw new PptxViewerError(
@@ -11099,7 +11298,7 @@ var presentationParsingDefaults = Object.freeze({
11099
11298
  maxInputBytes: DEFAULT_MAX_INPUT_BYTES
11100
11299
  });
11101
11300
  var THUMBNAIL_ROW_ESTIMATE = 123;
11102
- var THUMBNAIL_OVERSCAN = 3;
11301
+ var THUMBNAIL_OVERSCAN = 1;
11103
11302
  var THUMBNAIL_INITIAL_RECT = { height: 780, width: 184 };
11104
11303
  var observeThumbnailFilmstripRect = (instance, callback) => {
11105
11304
  const element = instance.scrollElement;
@@ -11158,7 +11357,11 @@ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSl
11158
11357
  const fontManager = new PptxFontManager(props.fonts, (warning) => {
11159
11358
  if (isCurrent()) reportWarning(warning);
11160
11359
  });
11360
+ const fontStartedAt = performanceNow();
11161
11361
  await fontManager.prepare(parsed.document);
11362
+ getCurrentProps().onPerformanceMeasurement?.(
11363
+ performanceMeasurement("embedded-font-preparation", fontStartedAt)
11364
+ );
11162
11365
  if (!isCurrent()) {
11163
11366
  fontManager.destroy();
11164
11367
  throw new Error("Viewer adapter creation was superseded.");
@@ -11191,6 +11394,7 @@ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSl
11191
11394
  const normalized = new NormalizedPresentationViewer(target, parsed.document, callbacks);
11192
11395
  return {
11193
11396
  async render(mode, slideIndex) {
11397
+ const startedAt = performanceNow();
11194
11398
  if (mode === "continuous") {
11195
11399
  const virtualization = typeof props.virtualization === "object" ? props.virtualization : {};
11196
11400
  await normalized.renderList({
@@ -11204,6 +11408,9 @@ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSl
11204
11408
  } else {
11205
11409
  await normalized.renderSlide(slideIndex);
11206
11410
  }
11411
+ getCurrentProps().onPerformanceMeasurement?.(
11412
+ performanceMeasurement("slide-dom-svg-rendering", startedAt, slideIndex)
11413
+ );
11207
11414
  },
11208
11415
  goToSlide: (index, options) => normalized.goToSlide(index, options),
11209
11416
  setZoom: (percent) => normalized.setZoom(percent),
@@ -11436,7 +11643,8 @@ var ReactPptxViewer = forwardRef(
11436
11643
  signal: abort.signal,
11437
11644
  ...parseFormatHint ? { formatHint: parseFormatHint } : {},
11438
11645
  ...parseMaxInputBytes !== void 0 ? { maxInputBytes: parseMaxInputBytes } : {},
11439
- ...parseFetchInit ? { fetchInit: parseFetchInit } : {}
11646
+ ...parseFetchInit ? { fetchInit: parseFetchInit } : {},
11647
+ onPerformanceMeasurement: (measurement) => latestPropsRef.current.onPerformanceMeasurement?.(measurement)
11440
11648
  }).then((next) => {
11441
11649
  if (abort.signal.aborted) return;
11442
11650
  setParsed(next);
@@ -11820,9 +12028,12 @@ function usePptxViewer() {
11820
12028
  }
11821
12029
  var EMU_PER_CSS_PIXEL2 = 9525;
11822
12030
  var DEFAULT_MAX_DIMENSION2 = 160;
11823
- function normalizeSlideIndexes(indexes, slideCount) {
12031
+ function normalizeSlideIndexes(indexes, slideCount, sort = true) {
11824
12032
  if (!indexes?.length || slideCount <= 0) return [];
11825
- return [...new Set(indexes)].filter((index) => Number.isInteger(index) && index >= 0 && index < slideCount).sort((left, right) => left - right);
12033
+ const normalized = [...new Set(indexes)].filter(
12034
+ (index) => Number.isInteger(index) && index >= 0 && index < slideCount
12035
+ );
12036
+ return sort ? normalized.sort((left, right) => left - right) : normalized;
11826
12037
  }
11827
12038
  function finitePositive2(value) {
11828
12039
  return value !== void 0 && Number.isFinite(value) && value > 0 ? value : void 0;
@@ -11860,6 +12071,7 @@ function usePptxViewerThumbnails(controller, options = {}) {
11860
12071
  const attachedElementsRef = useRef(/* @__PURE__ */ new Map());
11861
12072
  const cleanupsRef = useRef(/* @__PURE__ */ new Map());
11862
12073
  const prefetchedRef = useRef(/* @__PURE__ */ new Map());
12074
+ const prefetchInFlightRef = useRef(/* @__PURE__ */ new Map());
11863
12075
  const renderGenerationRef = useRef(/* @__PURE__ */ new Map());
11864
12076
  const refCallbacksRef = useRef(/* @__PURE__ */ new Map());
11865
12077
  const attachRef = useRef(() => {
@@ -11872,7 +12084,7 @@ function usePptxViewerThumbnails(controller, options = {}) {
11872
12084
  [options.renderWindow?.visibleSlideIndexes, slideCount]
11873
12085
  );
11874
12086
  const prefetchSlideIndexes = useMemo(
11875
- () => normalizeSlideIndexes(options.renderWindow?.prefetchSlideIndexes, slideCount),
12087
+ () => normalizeSlideIndexes(options.renderWindow?.prefetchSlideIndexes, slideCount, false),
11876
12088
  [options.renderWindow?.prefetchSlideIndexes, slideCount]
11877
12089
  );
11878
12090
  const visibleSlideIndexesKey = visibleSlideIndexes.join(",");
@@ -11987,28 +12199,46 @@ function usePptxViewerThumbnails(controller, options = {}) {
11987
12199
  }, [renderThumbnail]);
11988
12200
  const prefetchThumbnail = useCallback(
11989
12201
  async (slideIndex) => {
11990
- if (disabled || !controller?.isReady() || attachedElementsRef.current.has(slideIndex) || prefetchedRef.current.has(slideIndex)) {
11991
- return;
11992
- }
11993
- const generation = (renderGenerationRef.current.get(slideIndex) ?? 0) + 1;
11994
- renderGenerationRef.current.set(slideIndex, generation);
11995
- const element = globalThis.document.createElement("div");
11996
- setThumbnailState(slideIndex, { status: "rendering" });
11997
- try {
11998
- const cleanup = await controller.renderThumbnail(slideIndex, element, {
11999
- width: size.width
12000
- });
12001
- if (!mountedRef.current || renderGenerationRef.current.get(slideIndex) !== generation || attachedElementsRef.current.has(slideIndex) || !requestedPrefetchSlideIndexSetRef.current.has(slideIndex)) {
12002
- cleanup();
12003
- return;
12202
+ while (true) {
12203
+ const existing = prefetchInFlightRef.current.get(slideIndex);
12204
+ if (existing) {
12205
+ await existing;
12206
+ continue;
12004
12207
  }
12005
- prefetchedRef.current.set(slideIndex, { cleanup, element, width: size.width });
12006
- setThumbnailState(slideIndex, { status: "ready" });
12007
- } catch (reason) {
12008
- if (!mountedRef.current || renderGenerationRef.current.get(slideIndex) !== generation) {
12208
+ if (disabled || !controller?.isReady() || attachedElementsRef.current.has(slideIndex) || prefetchedRef.current.has(slideIndex) || !requestedPrefetchSlideIndexSetRef.current.has(slideIndex)) {
12009
12209
  return;
12010
12210
  }
12011
- setThumbnailState(slideIndex, { error: asError(reason), status: "error" });
12211
+ const generation = (renderGenerationRef.current.get(slideIndex) ?? 0) + 1;
12212
+ renderGenerationRef.current.set(slideIndex, generation);
12213
+ const element = globalThis.document.createElement("div");
12214
+ setThumbnailState(slideIndex, { status: "rendering" });
12215
+ const pending = (async () => {
12216
+ try {
12217
+ const cleanup = await controller.renderThumbnail(slideIndex, element, {
12218
+ width: size.width
12219
+ });
12220
+ if (!mountedRef.current || renderGenerationRef.current.get(slideIndex) !== generation || attachedElementsRef.current.has(slideIndex) || !requestedPrefetchSlideIndexSetRef.current.has(slideIndex)) {
12221
+ cleanup();
12222
+ return;
12223
+ }
12224
+ prefetchedRef.current.set(slideIndex, { cleanup, element, width: size.width });
12225
+ setThumbnailState(slideIndex, { status: "ready" });
12226
+ } catch (reason) {
12227
+ if (!mountedRef.current || renderGenerationRef.current.get(slideIndex) !== generation) {
12228
+ return;
12229
+ }
12230
+ setThumbnailState(slideIndex, { error: asError(reason), status: "error" });
12231
+ }
12232
+ })();
12233
+ prefetchInFlightRef.current.set(slideIndex, pending);
12234
+ try {
12235
+ await pending;
12236
+ } finally {
12237
+ if (prefetchInFlightRef.current.get(slideIndex) === pending) {
12238
+ prefetchInFlightRef.current.delete(slideIndex);
12239
+ }
12240
+ }
12241
+ return;
12012
12242
  }
12013
12243
  },
12014
12244
  [controller, disabled, setThumbnailState, size.width]
@@ -12024,14 +12254,17 @@ function usePptxViewerThumbnails(controller, options = {}) {
12024
12254
  cleanupsRef.current.clear();
12025
12255
  for (const prefetched of prefetchedRef.current.values()) prefetched.cleanup();
12026
12256
  prefetchedRef.current.clear();
12257
+ prefetchInFlightRef.current.clear();
12027
12258
  };
12028
12259
  }, []);
12029
12260
  useEffect(
12030
12261
  () => () => {
12262
+ for (const index of prefetchInFlightRef.current.keys()) invalidateThumbnail(index);
12263
+ prefetchInFlightRef.current.clear();
12031
12264
  for (const prefetched of prefetchedRef.current.values()) prefetched.cleanup();
12032
12265
  prefetchedRef.current.clear();
12033
12266
  },
12034
- [controller, size.width]
12267
+ [controller, invalidateThumbnail, size.width]
12035
12268
  );
12036
12269
  useEffect(() => {
12037
12270
  void rerenderAttachedThumbnails();
@@ -12043,6 +12276,13 @@ function usePptxViewerThumbnails(controller, options = {}) {
12043
12276
  if (!attachedElementsRef.current.has(index)) setThumbnailState(index, { status: "idle" });
12044
12277
  }
12045
12278
  }
12279
+ for (const index of prefetchInFlightRef.current.keys()) {
12280
+ if (disabled || attachedElementsRef.current.has(index) || !requestedPrefetchSlideIndexSetRef.current.has(index)) {
12281
+ invalidateThumbnail(index);
12282
+ prefetchInFlightRef.current.delete(index);
12283
+ if (!attachedElementsRef.current.has(index)) setThumbnailState(index, { status: "idle" });
12284
+ }
12285
+ }
12046
12286
  if (disabled) return;
12047
12287
  let cancelled = false;
12048
12288
  void (async () => {
@@ -12057,6 +12297,7 @@ function usePptxViewerThumbnails(controller, options = {}) {
12057
12297
  }, [
12058
12298
  disabled,
12059
12299
  disposePrefetchedThumbnail,
12300
+ invalidateThumbnail,
12060
12301
  prefetchSlideIndexesKey,
12061
12302
  prefetchThumbnail,
12062
12303
  setThumbnailState
@@ -12096,6 +12337,404 @@ function usePptxViewerThumbnails(controller, options = {}) {
12096
12337
  );
12097
12338
  }
12098
12339
 
12099
- export { OFFICE_FONT_FALLBACKS, PptxViewerError, ReactPptxViewer, initWasm, parsePresentation, presentationParsingDefaults, resolvePptxFontFamily, setWasmSource, usePptxModel, usePptxPresentation, usePptxViewer, usePptxViewerThumbnails };
12340
+ // src/thumbnail-renderer.ts
12341
+ var EMU_PER_CSS_PIXEL3 = 9525;
12342
+ var DEFAULT_MAX_DIMENSION3 = 160;
12343
+ var RenderQueue = class {
12344
+ constructor(limit) {
12345
+ this.limit = limit;
12346
+ }
12347
+ limit;
12348
+ active = 0;
12349
+ destroyed = false;
12350
+ waiters = [];
12351
+ acquire(signal) {
12352
+ throwIfAborted(signal);
12353
+ if (this.destroyed) return Promise.reject(abortedError());
12354
+ return new Promise((resolve, reject) => {
12355
+ const waiter = { resolve, reject, ...signal ? { signal } : {} };
12356
+ if (signal) {
12357
+ waiter.abort = () => {
12358
+ const index = this.waiters.indexOf(waiter);
12359
+ if (index >= 0) this.waiters.splice(index, 1);
12360
+ reject(abortedError(signal.reason));
12361
+ };
12362
+ signal.addEventListener("abort", waiter.abort, { once: true });
12363
+ }
12364
+ this.waiters.push(waiter);
12365
+ this.drain();
12366
+ });
12367
+ }
12368
+ destroy() {
12369
+ this.destroyed = true;
12370
+ for (const waiter of this.waiters.splice(0)) {
12371
+ if (waiter.abort && waiter.signal) {
12372
+ waiter.signal.removeEventListener("abort", waiter.abort);
12373
+ }
12374
+ waiter.reject(abortedError());
12375
+ }
12376
+ }
12377
+ drain() {
12378
+ while (!this.destroyed && this.active < this.limit && this.waiters.length) {
12379
+ const waiter = this.waiters.shift();
12380
+ if (waiter.abort && waiter.signal) {
12381
+ waiter.signal.removeEventListener("abort", waiter.abort);
12382
+ }
12383
+ if (waiter.signal?.aborted) {
12384
+ waiter.reject(abortedError(waiter.signal.reason));
12385
+ continue;
12386
+ }
12387
+ this.active += 1;
12388
+ let released = false;
12389
+ waiter.resolve(() => {
12390
+ if (released) return;
12391
+ released = true;
12392
+ this.active -= 1;
12393
+ this.drain();
12394
+ });
12395
+ }
12396
+ }
12397
+ };
12398
+ function abortedError(reason) {
12399
+ return new PptxViewerError("aborted", "Thumbnail rendering was cancelled.", { cause: reason });
12400
+ }
12401
+ function finitePositive3(value, fallback) {
12402
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
12403
+ }
12404
+ function outputSize(presentation, options) {
12405
+ const naturalWidth = Math.max(1, presentation.document.size.widthEmu / EMU_PER_CSS_PIXEL3);
12406
+ const naturalHeight = Math.max(1, presentation.document.size.heightEmu / EMU_PER_CSS_PIXEL3);
12407
+ const hasWidth = options.maxWidth !== void 0 && Number.isFinite(options.maxWidth) && options.maxWidth > 0;
12408
+ const hasHeight = options.maxHeight !== void 0 && Number.isFinite(options.maxHeight) && options.maxHeight > 0;
12409
+ const maxWidth = hasWidth ? options.maxWidth : hasHeight ? Number.POSITIVE_INFINITY : DEFAULT_MAX_DIMENSION3;
12410
+ const maxHeight = hasHeight ? options.maxHeight : hasWidth ? Number.POSITIVE_INFINITY : DEFAULT_MAX_DIMENSION3;
12411
+ const scale = Math.min(maxWidth / naturalWidth, maxHeight / naturalHeight);
12412
+ return {
12413
+ height: Math.max(1, Math.round(naturalHeight * scale)),
12414
+ naturalHeight,
12415
+ naturalWidth,
12416
+ width: Math.max(1, Math.round(naturalWidth * scale))
12417
+ };
12418
+ }
12419
+ function blobToDataUrl(blob, signal) {
12420
+ return abortable(
12421
+ new Promise((resolve, reject) => {
12422
+ const reader = new FileReader();
12423
+ reader.onload = () => resolve(String(reader.result));
12424
+ reader.onerror = () => reject(reader.error ?? new Error("Could not read a slide asset."));
12425
+ reader.readAsDataURL(blob);
12426
+ }),
12427
+ signal
12428
+ );
12429
+ }
12430
+ async function inlineResourceUrl(url, signal) {
12431
+ if (!/^(?:blob:|https?:)/i.test(url)) return url;
12432
+ try {
12433
+ const response = await fetch(url, signal ? { signal } : void 0);
12434
+ if (!response.ok) return url;
12435
+ return await blobToDataUrl(await response.blob(), signal);
12436
+ } catch {
12437
+ throwIfAborted(signal);
12438
+ return url;
12439
+ }
12440
+ }
12441
+ async function inlineResources(root, signal) {
12442
+ const cache = /* @__PURE__ */ new Map();
12443
+ const inline = (url) => {
12444
+ let pending = cache.get(url);
12445
+ if (!pending) {
12446
+ pending = inlineResourceUrl(url, signal);
12447
+ cache.set(url, pending);
12448
+ }
12449
+ return pending;
12450
+ };
12451
+ const elements = [root, ...root.querySelectorAll("*")];
12452
+ await Promise.all(
12453
+ elements.flatMap((element) => {
12454
+ const attributeNames = ["src", "poster", ...element.localName === "image" ? ["href"] : []];
12455
+ const attributes = attributeNames.map((name) => ({ name, value: element.getAttribute(name) })).filter((entry) => Boolean(entry.value));
12456
+ const attributeTasks = attributes.map(async ({ name, value }) => {
12457
+ element.setAttribute(name, await inline(value));
12458
+ });
12459
+ const styleTasks = Array.from(
12460
+ { length: element.style.length },
12461
+ (_, index) => element.style.item(index)
12462
+ ).map((property) => ({ property, value: element.style.getPropertyValue(property) })).filter(({ value }) => value.includes("url(")).map(async ({ property, value }) => {
12463
+ const urls = [...value.matchAll(/url\(["']?([^"')]+)["']?\)/g)].map((match) => match[1]);
12464
+ let next = value;
12465
+ for (const url of urls) next = next.replace(url, await inline(url));
12466
+ element.style.setProperty(property, next);
12467
+ });
12468
+ return [...attributeTasks, ...styleTasks];
12469
+ })
12470
+ );
12471
+ }
12472
+ async function serializeSlide(slide, naturalWidth, naturalHeight, width, height, signal) {
12473
+ const clone = slide.cloneNode(true);
12474
+ clone.style.transform = "none";
12475
+ clone.style.transformOrigin = "top left";
12476
+ clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
12477
+ const sourceCanvases = slide.querySelectorAll("canvas");
12478
+ const clonedCanvases = clone.querySelectorAll("canvas");
12479
+ sourceCanvases.forEach((canvas, index) => {
12480
+ const cloned = clonedCanvases[index];
12481
+ if (!cloned) return;
12482
+ try {
12483
+ const image = document.createElement("img");
12484
+ image.src = canvas.toDataURL("image/png");
12485
+ image.setAttribute("style", cloned.getAttribute("style") ?? "");
12486
+ cloned.replaceWith(image);
12487
+ } catch {
12488
+ cloned.remove();
12489
+ }
12490
+ });
12491
+ await inlineResources(clone, signal);
12492
+ const content = new XMLSerializer().serializeToString(clone);
12493
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${naturalWidth} ${naturalHeight}"><foreignObject width="${naturalWidth}" height="${naturalHeight}">${content}</foreignObject></svg>`;
12494
+ }
12495
+ async function abortable(promise, signal) {
12496
+ throwIfAborted(signal);
12497
+ if (!signal) return promise;
12498
+ return new Promise((resolve, reject) => {
12499
+ const abort = () => reject(abortedError(signal.reason));
12500
+ signal.addEventListener("abort", abort, { once: true });
12501
+ void promise.then(
12502
+ (value) => {
12503
+ signal.removeEventListener("abort", abort);
12504
+ resolve(value);
12505
+ },
12506
+ (error) => {
12507
+ signal.removeEventListener("abort", abort);
12508
+ reject(error);
12509
+ }
12510
+ );
12511
+ });
12512
+ }
12513
+ async function decodeElementImages(root, signal) {
12514
+ const images = [...root.querySelectorAll("img")];
12515
+ const svgImages = [...root.querySelectorAll("image")].map((image) => image.href.baseVal || image.getAttribute("href") || "").filter(Boolean);
12516
+ await abortable(
12517
+ Promise.all([
12518
+ ...images.map(async (image) => {
12519
+ if (typeof image.decode === "function") await image.decode().catch(() => void 0);
12520
+ }),
12521
+ ...svgImages.map(
12522
+ (source) => new Promise((resolve) => {
12523
+ const image = new Image();
12524
+ image.onload = () => resolve();
12525
+ image.onerror = () => resolve();
12526
+ image.src = source;
12527
+ })
12528
+ )
12529
+ ]).then(() => void 0),
12530
+ signal
12531
+ );
12532
+ }
12533
+ async function decodeSvg(svg, signal) {
12534
+ return abortable(
12535
+ new Promise((resolve, reject) => {
12536
+ const image = new Image();
12537
+ image.onload = () => resolve(image);
12538
+ image.onerror = () => reject(new Error("The rendered slide SVG could not be decoded."));
12539
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
12540
+ }),
12541
+ signal
12542
+ );
12543
+ }
12544
+ function canvasToBlob(canvas, quality) {
12545
+ return new Promise((resolve, reject) => {
12546
+ canvas.toBlob(
12547
+ (blob) => {
12548
+ if (blob) resolve(blob);
12549
+ else reject(new Error("The browser could not encode the thumbnail."));
12550
+ },
12551
+ "image/png",
12552
+ quality
12553
+ );
12554
+ });
12555
+ }
12556
+ var ThumbnailRenderer = class {
12557
+ constructor(presentation, options) {
12558
+ this.presentation = presentation;
12559
+ this.options = options;
12560
+ if (typeof document === "undefined") {
12561
+ throw new PptxViewerError(
12562
+ "unsupported-environment",
12563
+ "Thumbnail rendering requires a browser DOM."
12564
+ );
12565
+ }
12566
+ const concurrency = Math.max(1, Math.floor(finitePositive3(options.concurrency, 2)));
12567
+ this.queue = new RenderQueue(concurrency);
12568
+ this.root = document.createElement("div");
12569
+ this.root.setAttribute("aria-hidden", "true");
12570
+ this.root.style.position = "fixed";
12571
+ this.root.style.left = "-100000px";
12572
+ this.root.style.top = "0";
12573
+ this.root.style.pointerEvents = "none";
12574
+ this.root.style.opacity = "0";
12575
+ document.body.append(this.root);
12576
+ this.fontManager = new PptxFontManager(
12577
+ {
12578
+ loadEmbeddedFonts: false,
12579
+ reportMissingFonts: false,
12580
+ waitForFonts: false,
12581
+ ...options.fonts
12582
+ },
12583
+ (warning) => options.onWarning?.(warning)
12584
+ );
12585
+ this.viewer = new NormalizedPresentationViewer(this.root, presentation.document, {
12586
+ onNodeError: (nodeId, error) => options.onWarning?.({
12587
+ code: "degraded-rendering",
12588
+ severity: "warning",
12589
+ nodeId,
12590
+ feature: "thumbnail-renderer",
12591
+ message: `A slide element could not be rendered: ${error instanceof Error ? error.message : String(error)}`
12592
+ })
12593
+ });
12594
+ const startedAt = performanceNow();
12595
+ this.ready = this.fontManager.prepare(presentation.document).then(() => {
12596
+ this.report(performanceMeasurement("embedded-font-preparation", startedAt));
12597
+ });
12598
+ }
12599
+ presentation;
12600
+ options;
12601
+ ready;
12602
+ fontManager;
12603
+ queue;
12604
+ root;
12605
+ viewer;
12606
+ destroyed = false;
12607
+ async renderSlide(slideIndex, options = {}) {
12608
+ const release = await this.queue.acquire(options.signal);
12609
+ try {
12610
+ this.assertAvailable(options.signal);
12611
+ await abortable(this.ready, options.signal);
12612
+ this.assertAvailable(options.signal);
12613
+ if (!Number.isInteger(slideIndex) || slideIndex < 0 || slideIndex >= this.presentation.document.slides.length) {
12614
+ throw new RangeError(`Slide ${slideIndex} does not exist.`);
12615
+ }
12616
+ const measurements = [];
12617
+ const size = outputSize(this.presentation, options);
12618
+ const target = document.createElement("div");
12619
+ this.root.append(target);
12620
+ const domStartedAt = performanceNow();
12621
+ const handle = this.viewer.renderThumbnailToContainer(slideIndex, target, {
12622
+ width: size.naturalWidth
12623
+ });
12624
+ try {
12625
+ await abortable(handle.ready, options.signal);
12626
+ this.fontManager.applyTo(handle.element, slideIndex);
12627
+ const domDurationMs = performanceNow() - domStartedAt;
12628
+ const decodeStartedAt = performanceNow();
12629
+ await decodeElementImages(handle.element, options.signal);
12630
+ let decodeDurationMs = performanceNow() - decodeStartedAt;
12631
+ const serializationStartedAt = performanceNow();
12632
+ const svg = await serializeSlide(
12633
+ handle.element,
12634
+ size.naturalWidth,
12635
+ size.naturalHeight,
12636
+ size.width,
12637
+ size.height,
12638
+ options.signal
12639
+ );
12640
+ this.record(
12641
+ {
12642
+ durationMs: Math.max(0, domDurationMs + performanceNow() - serializationStartedAt),
12643
+ phase: "slide-dom-svg-rendering",
12644
+ slideIndex
12645
+ },
12646
+ measurements
12647
+ );
12648
+ const output = options.output ?? "blob";
12649
+ if (output === "svg") {
12650
+ this.record(
12651
+ { durationMs: Math.max(0, decodeDurationMs), phase: "image-decoding", slideIndex },
12652
+ measurements
12653
+ );
12654
+ return this.result(svg, output, slideIndex, size, measurements);
12655
+ }
12656
+ const svgDecodeStartedAt = performanceNow();
12657
+ const image = await decodeSvg(svg, options.signal);
12658
+ decodeDurationMs += performanceNow() - svgDecodeStartedAt;
12659
+ this.record(
12660
+ { durationMs: Math.max(0, decodeDurationMs), phase: "image-decoding", slideIndex },
12661
+ measurements
12662
+ );
12663
+ this.assertAvailable(options.signal);
12664
+ const encodeStartedAt = performanceNow();
12665
+ const pixelRatio = finitePositive3(options.pixelRatio, 1);
12666
+ const canvas = document.createElement("canvas");
12667
+ canvas.width = Math.max(1, Math.round(size.width * pixelRatio));
12668
+ canvas.height = Math.max(1, Math.round(size.height * pixelRatio));
12669
+ canvas.style.width = `${size.width}px`;
12670
+ canvas.style.height = `${size.height}px`;
12671
+ const context = canvas.getContext("2d");
12672
+ if (!context) throw new Error("The browser does not provide a 2D canvas renderer.");
12673
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
12674
+ let data;
12675
+ if (output === "canvas") data = canvas;
12676
+ else if (output === "imageBitmap") {
12677
+ if (typeof createImageBitmap !== "function") {
12678
+ throw new PptxViewerError(
12679
+ "unsupported-environment",
12680
+ "ImageBitmap output is not supported by this browser."
12681
+ );
12682
+ }
12683
+ data = await abortable(createImageBitmap(canvas), options.signal);
12684
+ } else data = await abortable(canvasToBlob(canvas, options.quality), options.signal);
12685
+ this.measure("raster-encoding", encodeStartedAt, slideIndex, measurements);
12686
+ return this.result(data, output, slideIndex, size, measurements);
12687
+ } finally {
12688
+ handle.dispose();
12689
+ target.remove();
12690
+ }
12691
+ } finally {
12692
+ release();
12693
+ }
12694
+ }
12695
+ renderSlides(slideIndexes, options = {}) {
12696
+ return Promise.all(slideIndexes.map((slideIndex) => this.renderDynamic(slideIndex, options)));
12697
+ }
12698
+ destroy() {
12699
+ if (this.destroyed) return;
12700
+ this.destroyed = true;
12701
+ this.queue.destroy();
12702
+ this.viewer.destroy();
12703
+ this.fontManager.destroy();
12704
+ this.root.remove();
12705
+ }
12706
+ renderDynamic(slideIndex, options) {
12707
+ return this.renderSlide(slideIndex, options);
12708
+ }
12709
+ assertAvailable(signal) {
12710
+ throwIfAborted(signal);
12711
+ if (this.destroyed) throw abortedError();
12712
+ }
12713
+ measure(phase, startedAt, slideIndex, measurements) {
12714
+ this.record(performanceMeasurement(phase, startedAt, slideIndex), measurements);
12715
+ }
12716
+ record(measurement, measurements) {
12717
+ measurements.push(measurement);
12718
+ this.report(measurement);
12719
+ }
12720
+ report(measurement) {
12721
+ this.options.onPerformanceMeasurement?.(measurement);
12722
+ }
12723
+ result(data, output, slideIndex, size, measurements) {
12724
+ return {
12725
+ data,
12726
+ height: size.height,
12727
+ measurements,
12728
+ output,
12729
+ slideIndex,
12730
+ width: size.width
12731
+ };
12732
+ }
12733
+ };
12734
+ function createPptxThumbnailRenderer(presentation, options = {}) {
12735
+ return new ThumbnailRenderer(presentation, options);
12736
+ }
12737
+
12738
+ export { OFFICE_FONT_FALLBACKS, PptxViewerError, ReactPptxViewer, createPptxThumbnailRenderer, initWasm, parsePresentation, presentationParsingDefaults, resolvePptxFontFamily, setWasmSource, usePptxModel, usePptxPresentation, usePptxViewer, usePptxViewerThumbnails };
12100
12739
  //# sourceMappingURL=index.js.map
12101
12740
  //# sourceMappingURL=index.js.map