@extend-ai/react-pptx 0.1.2 → 0.2.1

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
@@ -9242,13 +9242,94 @@ async function renderWmfToDataUrl(buffer, maxWidth, maxHeight, optionsOrScale) {
9242
9242
  }
9243
9243
  }
9244
9244
 
9245
+ // src/zoom.ts
9246
+ var MIN_VIEWER_ZOOM = 10;
9247
+ var MAX_VIEWER_ZOOM = 400;
9248
+ function normalizeViewerZoomLevel(level) {
9249
+ if (typeof level !== "number") return level;
9250
+ if (Number.isNaN(level)) return 100;
9251
+ return Math.max(MIN_VIEWER_ZOOM, Math.min(MAX_VIEWER_ZOOM, level));
9252
+ }
9253
+ function resolveViewerZoom(level, viewportWidth, viewportHeight, contentWidth, contentHeight) {
9254
+ const normalized = normalizeViewerZoomLevel(level);
9255
+ if (typeof normalized === "number") return normalized;
9256
+ const safeContentWidth = Number.isFinite(contentWidth) && contentWidth > 0 ? contentWidth : 1;
9257
+ const safeContentHeight = Number.isFinite(contentHeight) && contentHeight > 0 ? contentHeight : 1;
9258
+ const fitWidth = (Number.isFinite(viewportWidth) && viewportWidth > 0 ? viewportWidth : safeContentWidth) / safeContentWidth * 100;
9259
+ const fitHeight = (Number.isFinite(viewportHeight) && viewportHeight > 0 ? viewportHeight : safeContentHeight) / safeContentHeight * 100;
9260
+ const resolved = normalized === "fit-width" ? fitWidth : normalized === "fit-page" ? Math.min(fitWidth, fitHeight) : Math.min(fitWidth, 100);
9261
+ return Math.max(MIN_VIEWER_ZOOM, Math.min(MAX_VIEWER_ZOOM, resolved));
9262
+ }
9263
+
9245
9264
  // src/normalized-viewer.ts
9246
9265
  var EMU_PER_CSS_PIXEL = 9525;
9247
9266
  var DEFAULT_TEXT_HORIZONTAL_INSET_EMU = 91440;
9248
9267
  var DEFAULT_TEXT_VERTICAL_INSET_EMU = 45720;
9249
9268
  var POWERPOINT_SINGLE_LINE_HEIGHT = 1.15;
9269
+ var PresentationAssetCache = class {
9270
+ references = 0;
9271
+ objectUrls = /* @__PURE__ */ new Set();
9272
+ assetUrls = /* @__PURE__ */ new Map();
9273
+ metafileUrls = /* @__PURE__ */ new Map();
9274
+ acquire() {
9275
+ this.references += 1;
9276
+ return this;
9277
+ }
9278
+ metafileUrl(assetId, contentType, data) {
9279
+ const cached = this.metafileUrls.get(assetId);
9280
+ if (cached) return cached;
9281
+ const convert = contentType.includes("wmf") ? renderWmfToDataUrl : renderEmfToDataUrl;
9282
+ const ownedBytes = data.slice();
9283
+ const pending = convert(ownedBytes.buffer, 2048, 2048, {
9284
+ dpiScale: 2,
9285
+ maxCanvasDimension: 4096,
9286
+ maxRecords: 2e5,
9287
+ fontFamilyMap: METAFILE_FONT_MAP
9288
+ }).then((url) => url || void 0);
9289
+ this.metafileUrls.set(assetId, pending);
9290
+ return pending;
9291
+ }
9292
+ assetUrl(asset) {
9293
+ if (asset.url) return Promise.resolve(asset.url);
9294
+ if (!asset.data) return Promise.resolve(void 0);
9295
+ const contentType = asset.contentType.toLowerCase();
9296
+ if (contentType.includes("emf") || contentType.includes("wmf")) {
9297
+ return this.metafileUrl(asset.id, contentType, asset.data);
9298
+ }
9299
+ const cached = this.assetUrls.get(asset.id);
9300
+ if (cached) return Promise.resolve(cached);
9301
+ const ownedBytes = asset.data.slice();
9302
+ const url = URL.createObjectURL(
9303
+ new Blob([ownedBytes.buffer], { type: asset.contentType })
9304
+ );
9305
+ this.objectUrls.add(url);
9306
+ this.assetUrls.set(asset.id, url);
9307
+ return Promise.resolve(url);
9308
+ }
9309
+ release() {
9310
+ this.references = Math.max(0, this.references - 1);
9311
+ if (this.references > 0) return;
9312
+ for (const url of this.objectUrls) URL.revokeObjectURL(url);
9313
+ this.objectUrls.clear();
9314
+ this.assetUrls.clear();
9315
+ this.metafileUrls.clear();
9316
+ }
9317
+ };
9318
+ var presentationAssetCaches = /* @__PURE__ */ new WeakMap();
9319
+ function acquirePresentationAssetCache(presentation) {
9320
+ let cache = presentationAssetCaches.get(presentation);
9321
+ if (!cache) {
9322
+ cache = new PresentationAssetCache();
9323
+ presentationAssetCaches.set(presentation, cache);
9324
+ }
9325
+ return cache.acquire();
9326
+ }
9250
9327
  var LIST_ITEM_GAP = 24;
9251
9328
  var FALLBACK_VIEWPORT_RECT = { width: 800, height: 600 };
9329
+ function pixelValue(value) {
9330
+ const parsed = Number.parseFloat(value);
9331
+ return Number.isFinite(parsed) ? parsed : 0;
9332
+ }
9252
9333
  function scaleListAnchorOffset(offset, previousSlideHeight, nextSlideHeight) {
9253
9334
  if (previousSlideHeight <= 0) return offset;
9254
9335
  if (offset <= previousSlideHeight) return offset * (nextSlideHeight / previousSlideHeight);
@@ -9788,16 +9869,17 @@ var NormalizedPresentationViewer = class {
9788
9869
  this.container = container;
9789
9870
  this.presentation = presentation;
9790
9871
  this.callbacks = callbacks;
9872
+ this.assetCache = acquirePresentationAssetCache(presentation);
9791
9873
  }
9792
9874
  container;
9793
9875
  presentation;
9794
9876
  callbacks;
9795
9877
  current = 0;
9796
- zoom = 100;
9797
- fit = "contain";
9798
- objectUrls = /* @__PURE__ */ new Set();
9799
- assetUrls = /* @__PURE__ */ new Map();
9800
- metafileUrls = /* @__PURE__ */ new Map();
9878
+ zoomLevel = "automatic";
9879
+ resolvedZoom = 100;
9880
+ assetCache;
9881
+ resizeObserver;
9882
+ activeViewport;
9801
9883
  listCleanup;
9802
9884
  highlight;
9803
9885
  activeMountResources;
@@ -9809,6 +9891,7 @@ var NormalizedPresentationViewer = class {
9809
9891
  renderGeneration = 0;
9810
9892
  navigationGeneration = 0;
9811
9893
  lastNotifiedSlide;
9894
+ lastZoomNotification;
9812
9895
  listState;
9813
9896
  renderState;
9814
9897
  get slideCount() {
@@ -9817,11 +9900,17 @@ var NormalizedPresentationViewer = class {
9817
9900
  get currentSlideIndex() {
9818
9901
  return this.current;
9819
9902
  }
9903
+ get zoom() {
9904
+ return this.zoomLevel;
9905
+ }
9820
9906
  get zoomPercent() {
9821
- return this.zoom;
9907
+ return this.resolvedZoom;
9908
+ }
9909
+ get resolvedZoomPercent() {
9910
+ return this.resolvedZoom;
9822
9911
  }
9823
9912
  get fitMode() {
9824
- return this.fit;
9913
+ return typeof this.zoomLevel === "number" ? "none" : "contain";
9825
9914
  }
9826
9915
  get naturalSlideWidth() {
9827
9916
  const width = this.presentation.size.widthEmu / EMU_PER_CSS_PIXEL;
@@ -9831,9 +9920,57 @@ var NormalizedPresentationViewer = class {
9831
9920
  const height = this.presentation.size.heightEmu / EMU_PER_CSS_PIXEL;
9832
9921
  return Number.isFinite(height) && height > 0 ? height : 1;
9833
9922
  }
9834
- scaleForViewport(viewportWidth) {
9835
- const fitScale = this.fit === "contain" ? Math.min(1, (viewportWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
9836
- return fitScale * (this.zoom / 100);
9923
+ viewportSize(viewport) {
9924
+ const rect = viewport.getBoundingClientRect();
9925
+ let width = viewport.clientWidth || rect.width;
9926
+ let height = viewport.clientHeight || rect.height;
9927
+ if (typeof getComputedStyle === "function") {
9928
+ const viewportStyle = getComputedStyle(viewport);
9929
+ width -= pixelValue(viewportStyle.paddingLeft) + pixelValue(viewportStyle.paddingRight);
9930
+ height -= pixelValue(viewportStyle.paddingTop) + pixelValue(viewportStyle.paddingBottom);
9931
+ if (viewport !== this.container) {
9932
+ const containerStyle = getComputedStyle(this.container);
9933
+ width -= pixelValue(containerStyle.paddingLeft) + pixelValue(containerStyle.paddingRight);
9934
+ height -= pixelValue(containerStyle.paddingTop) + pixelValue(containerStyle.paddingBottom);
9935
+ }
9936
+ }
9937
+ return { width: Math.max(0, width), height: Math.max(0, height) };
9938
+ }
9939
+ resolveZoom(level, viewport = this.activeViewport ?? this.container) {
9940
+ const size = this.viewportSize(viewport);
9941
+ return resolveViewerZoom(
9942
+ level,
9943
+ size.width,
9944
+ size.height,
9945
+ this.naturalSlideWidth,
9946
+ this.naturalSlideHeight
9947
+ );
9948
+ }
9949
+ scaleForViewport(viewport) {
9950
+ return this.resolveZoom(this.zoomLevel, viewport) / 100;
9951
+ }
9952
+ publishZoom(resolvedZoom) {
9953
+ const normalized = Math.max(MIN_VIEWER_ZOOM, Math.min(MAX_VIEWER_ZOOM, resolvedZoom));
9954
+ const levelChanged = this.lastZoomNotification?.level !== this.zoomLevel;
9955
+ const resolutionChanged = this.lastZoomNotification === void 0 || Math.abs(this.lastZoomNotification.resolvedZoom - normalized) > 0.01;
9956
+ this.resolvedZoom = normalized;
9957
+ if (!levelChanged && !resolutionChanged) return;
9958
+ const state = { level: this.zoomLevel, resolvedZoom: normalized };
9959
+ this.lastZoomNotification = state;
9960
+ this.callbacks.onZoomChange?.(state);
9961
+ }
9962
+ observeViewport(viewport) {
9963
+ if (this.activeViewport === viewport && this.resizeObserver) return;
9964
+ this.resizeObserver?.disconnect();
9965
+ this.resizeObserver = void 0;
9966
+ this.activeViewport = viewport;
9967
+ if (typeof ResizeObserver === "undefined") return;
9968
+ this.resizeObserver = new ResizeObserver(() => {
9969
+ if (this.destroyed || typeof this.zoomLevel === "number") return;
9970
+ void this.updateRenderedScale();
9971
+ });
9972
+ this.resizeObserver.observe(viewport);
9973
+ if (viewport !== this.container) this.resizeObserver.observe(this.container);
9837
9974
  }
9838
9975
  isRenderActive(generation) {
9839
9976
  return !this.destroyed && generation === this.renderGeneration;
@@ -9844,6 +9981,9 @@ var NormalizedPresentationViewer = class {
9844
9981
  this.navigationGeneration += 1;
9845
9982
  this.listCleanup?.();
9846
9983
  this.listCleanup = void 0;
9984
+ this.resizeObserver?.disconnect();
9985
+ this.resizeObserver = void 0;
9986
+ this.activeViewport = void 0;
9847
9987
  this.listState = void 0;
9848
9988
  this.clearSearchHighlights();
9849
9989
  return generation;
@@ -9853,36 +9993,8 @@ var NormalizedPresentationViewer = class {
9853
9993
  this.lastNotifiedSlide = index;
9854
9994
  this.callbacks.onSlideChange?.(index);
9855
9995
  }
9856
- metafileUrl(assetId, contentType, data) {
9857
- const cached = this.metafileUrls.get(assetId);
9858
- if (cached) return cached;
9859
- const convert = contentType.includes("wmf") ? renderWmfToDataUrl : renderEmfToDataUrl;
9860
- const ownedBytes = data.slice();
9861
- const pending = convert(ownedBytes.buffer, 2048, 2048, {
9862
- dpiScale: 2,
9863
- maxCanvasDimension: 4096,
9864
- maxRecords: 2e5,
9865
- fontFamilyMap: METAFILE_FONT_MAP
9866
- }).then((url) => url || void 0);
9867
- this.metafileUrls.set(assetId, pending);
9868
- return pending;
9869
- }
9870
9996
  assetUrl(asset) {
9871
- if (asset.url) return Promise.resolve(asset.url);
9872
- if (!asset.data) return Promise.resolve(void 0);
9873
- const contentType = asset.contentType.toLowerCase();
9874
- if (contentType.includes("emf") || contentType.includes("wmf")) {
9875
- return this.metafileUrl(asset.id, contentType, asset.data);
9876
- }
9877
- const cached = this.assetUrls.get(asset.id);
9878
- if (cached) return Promise.resolve(cached);
9879
- const ownedBytes = asset.data.slice();
9880
- const url = URL.createObjectURL(
9881
- new Blob([ownedBytes.buffer], { type: asset.contentType })
9882
- );
9883
- this.objectUrls.add(url);
9884
- this.assetUrls.set(asset.id, url);
9885
- return Promise.resolve(url);
9997
+ return this.assetCache.assetUrl(asset);
9886
9998
  }
9887
9999
  applyAsset(assetId, apply, nodeId) {
9888
10000
  const asset = this.presentation.assets[assetId];
@@ -10429,7 +10541,7 @@ var NormalizedPresentationViewer = class {
10429
10541
  } finally {
10430
10542
  this.activeMountResources = previousResources;
10431
10543
  }
10432
- const effectiveScale = scale ?? this.scaleForViewport(target.clientWidth);
10544
+ const effectiveScale = scale ?? this.scaleForViewport(target);
10433
10545
  const wrapper = document.createElement("div");
10434
10546
  wrapper.dataset.rpvSlideWrapper = String(index);
10435
10547
  wrapper.style.position = "relative";
@@ -10521,6 +10633,7 @@ var NormalizedPresentationViewer = class {
10521
10633
  this.renderState = { mode: "single" };
10522
10634
  const generation = this.beginRender();
10523
10635
  if (generation === void 0) return;
10636
+ this.observeViewport(this.container);
10524
10637
  this.current = Math.max(0, Math.min(this.slideCount - 1, index));
10525
10638
  this.disposeContainerSlides();
10526
10639
  this.container.replaceChildren();
@@ -10530,6 +10643,7 @@ var NormalizedPresentationViewer = class {
10530
10643
  if (!this.isRenderActive(generation) || this.handlesByTarget.get(this.container) !== handle) {
10531
10644
  return;
10532
10645
  }
10646
+ this.publishZoom(this.scaleForViewport(this.container) * 100);
10533
10647
  this.notifySlideChange(this.current);
10534
10648
  }
10535
10649
  createListPlaceholder(index, item, generation, scale) {
@@ -10590,7 +10704,8 @@ var NormalizedPresentationViewer = class {
10590
10704
  Math.min(this.slideCount - 1, options.initialSlideIndex ?? this.current)
10591
10705
  );
10592
10706
  const scroller = options.scrollElement ?? this.container;
10593
- let effectiveScale = this.scaleForViewport(scroller.clientWidth);
10707
+ this.observeViewport(scroller);
10708
+ let effectiveScale = this.scaleForViewport(scroller);
10594
10709
  let slideHeight = this.naturalSlideHeight * effectiveScale;
10595
10710
  let itemStride = slideHeight + LIST_ITEM_GAP;
10596
10711
  if (!windowingEnabled) {
@@ -10605,6 +10720,8 @@ var NormalizedPresentationViewer = class {
10605
10720
  const state2 = { generation, options: normalizedOptions, placeholders: placeholders2 };
10606
10721
  state2.updateScale = () => {
10607
10722
  if (!this.isRenderActive(generation) || this.listState !== state2) return;
10723
+ const previousScale = effectiveScale;
10724
+ const previousScrollLeft = scroller.scrollLeft;
10608
10725
  const previousStride = itemStride;
10609
10726
  const previousSlideHeight = slideHeight;
10610
10727
  const listStart = scroller === this.container ? 0 : this.container.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
@@ -10614,13 +10731,17 @@ var NormalizedPresentationViewer = class {
10614
10731
  Math.min(this.slideCount - 1, Math.floor(listOffset / previousStride))
10615
10732
  );
10616
10733
  const anchorOffset = listOffset - anchorIndex * previousStride;
10617
- effectiveScale = this.scaleForViewport(scroller.clientWidth);
10734
+ effectiveScale = this.scaleForViewport(scroller);
10618
10735
  slideHeight = this.naturalSlideHeight * effectiveScale;
10619
10736
  itemStride = slideHeight + LIST_ITEM_GAP;
10620
10737
  placeholders2.forEach((placeholder) => {
10621
10738
  placeholder.item.style.minHeight = `${slideHeight}px`;
10622
10739
  placeholder.setScale(effectiveScale);
10623
10740
  });
10741
+ if (previousScale > 0 && previousScrollLeft > 0) {
10742
+ scroller.scrollLeft = previousScrollLeft * (effectiveScale / previousScale);
10743
+ }
10744
+ this.publishZoom(effectiveScale * 100);
10624
10745
  if (listOffset < 0) return;
10625
10746
  const nextAnchorOffset = scaleListAnchorOffset(
10626
10747
  anchorOffset,
@@ -10646,6 +10767,7 @@ var NormalizedPresentationViewer = class {
10646
10767
  initialItem.scrollIntoView?.({ behavior: "instant", block: "start" });
10647
10768
  }
10648
10769
  this.current = initialIndex;
10770
+ this.publishZoom(effectiveScale * 100);
10649
10771
  this.notifySlideChange(initialIndex);
10650
10772
  return;
10651
10773
  }
@@ -10789,6 +10911,8 @@ var NormalizedPresentationViewer = class {
10789
10911
  };
10790
10912
  state.updateScale = () => {
10791
10913
  if (!isActive()) return;
10914
+ const previousScale = effectiveScale;
10915
+ const previousScrollLeft = scroller.scrollLeft;
10792
10916
  const previousStride = itemStride;
10793
10917
  const previousSlideHeight = slideHeight;
10794
10918
  const anchorIndex = this.current;
@@ -10798,7 +10922,7 @@ var NormalizedPresentationViewer = class {
10798
10922
  const previousScrollTop = Math.abs(observedIndex - anchorIndex) > 1 && intendedIndex !== void 0 && Math.abs(intendedIndex - anchorIndex) <= 1 ? lastProgrammaticTop : observedScrollTop;
10799
10923
  const previousOffset = previousScrollTop - scrollMargin;
10800
10924
  const anchorOffset = previousOffset - anchorIndex * previousStride;
10801
- effectiveScale = this.scaleForViewport(scroller.clientWidth);
10925
+ effectiveScale = this.scaleForViewport(scroller);
10802
10926
  slideHeight = this.naturalSlideHeight * effectiveScale;
10803
10927
  itemStride = slideHeight + LIST_ITEM_GAP;
10804
10928
  sizer.style.height = `${this.slideCount * itemStride - LIST_ITEM_GAP}px`;
@@ -10807,6 +10931,9 @@ var NormalizedPresentationViewer = class {
10807
10931
  placeholder.item.style.transform = `translateY(${index * itemStride}px)`;
10808
10932
  placeholder.setScale(effectiveScale);
10809
10933
  });
10934
+ if (previousScale > 0 && previousScrollLeft > 0) {
10935
+ scroller.scrollLeft = previousScrollLeft * (effectiveScale / previousScale);
10936
+ }
10810
10937
  const nextAnchorOffset = scaleListAnchorOffset(
10811
10938
  anchorOffset,
10812
10939
  previousSlideHeight,
@@ -10819,6 +10946,7 @@ var NormalizedPresentationViewer = class {
10819
10946
  writeScrollTop(nextScrollTop);
10820
10947
  virtualizer.setOptions({ ...virtualizer.options, overscan });
10821
10948
  virtualizer.measure();
10949
+ this.publishZoom(effectiveScale * 100);
10822
10950
  };
10823
10951
  const teardown = virtualizer._didMount();
10824
10952
  this.listCleanup = teardown;
@@ -10837,6 +10965,7 @@ var NormalizedPresentationViewer = class {
10837
10965
  this.container.scrollLeft = 0;
10838
10966
  }
10839
10967
  this.current = initialIndex;
10968
+ this.publishZoom(effectiveScale * 100);
10840
10969
  this.notifySlideChange(initialIndex);
10841
10970
  }
10842
10971
  async goToSlide(index, scrollOptions) {
@@ -10873,10 +11002,7 @@ var NormalizedPresentationViewer = class {
10873
11002
  if (!changed && this.container.querySelector(`[data-rpv-slide-index="${next}"]`)) return;
10874
11003
  await this.renderSlide(next);
10875
11004
  }
10876
- async setZoom(percent) {
10877
- const next = Math.max(10, Math.min(400, percent));
10878
- if (this.zoom === next) return;
10879
- this.zoom = next;
11005
+ async updateRenderedScale() {
10880
11006
  if (this.renderState?.mode === "continuous") {
10881
11007
  const state = this.listState;
10882
11008
  if (state && this.isRenderActive(state.generation) && state.updateScale) {
@@ -10886,25 +11012,34 @@ var NormalizedPresentationViewer = class {
10886
11012
  }
10887
11013
  } else if (this.renderState) {
10888
11014
  const handle = this.handlesByTarget.get(this.container);
10889
- if (handle) handle.setScale(this.scaleForViewport(this.container.clientWidth));
10890
- else await this.renderSlide(this.current);
11015
+ if (handle) {
11016
+ const viewport = this.activeViewport ?? this.container;
11017
+ const previousScale = this.resolvedZoom / 100;
11018
+ const previousScrollTop = viewport.scrollTop;
11019
+ const previousScrollLeft = viewport.scrollLeft;
11020
+ const nextScale = this.scaleForViewport(viewport);
11021
+ handle.setScale(nextScale);
11022
+ if (previousScale > 0) {
11023
+ viewport.scrollTop = previousScrollTop * (nextScale / previousScale);
11024
+ viewport.scrollLeft = previousScrollLeft * (nextScale / previousScale);
11025
+ }
11026
+ this.publishZoom(nextScale * 100);
11027
+ } else await this.renderSlide(this.current);
11028
+ } else {
11029
+ this.publishZoom(this.resolveZoom(this.zoomLevel));
10891
11030
  }
10892
11031
  }
10893
- async setFitMode(mode) {
10894
- if (this.fit === mode) return;
10895
- this.fit = mode;
10896
- if (this.renderState?.mode === "continuous") {
10897
- const state = this.listState;
10898
- if (state && this.isRenderActive(state.generation) && state.updateScale) {
10899
- state.updateScale();
10900
- } else {
10901
- await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10902
- }
10903
- } else if (this.renderState) {
10904
- const handle = this.handlesByTarget.get(this.container);
10905
- if (handle) handle.setScale(this.scaleForViewport(this.container.clientWidth));
10906
- else await this.renderSlide(this.current);
11032
+ async setZoom(level) {
11033
+ const next = normalizeViewerZoomLevel(level);
11034
+ if (this.zoomLevel === next) {
11035
+ if (typeof next !== "number") await this.updateRenderedScale();
11036
+ return;
10907
11037
  }
11038
+ this.zoomLevel = next;
11039
+ await this.updateRenderedScale();
11040
+ }
11041
+ async setFitMode(mode) {
11042
+ await this.setZoom(mode === "contain" ? "automatic" : 100);
10908
11043
  }
10909
11044
  searchText(query, options) {
10910
11045
  return searchDocument(this.presentation, query, options);
@@ -10980,12 +11115,12 @@ var NormalizedPresentationViewer = class {
10980
11115
  this.navigationGeneration += 1;
10981
11116
  this.listCleanup?.();
10982
11117
  this.listCleanup = void 0;
11118
+ this.resizeObserver?.disconnect();
11119
+ this.resizeObserver = void 0;
11120
+ this.activeViewport = void 0;
10983
11121
  this.listState = void 0;
10984
11122
  this.clearSearchHighlights();
10985
- for (const url of this.objectUrls) URL.revokeObjectURL(url);
10986
- this.objectUrls.clear();
10987
- this.assetUrls.clear();
10988
- this.metafileUrls.clear();
11123
+ this.assetCache.release();
10989
11124
  this.activeMountResources = void 0;
10990
11125
  for (const handle of [...this.mountedHandles]) handle.dispose();
10991
11126
  this.handlesByTarget.clear();
@@ -11145,6 +11280,18 @@ async function parseInWorker(bytes, signal, wasmSource) {
11145
11280
  });
11146
11281
  }
11147
11282
 
11283
+ // src/performance.ts
11284
+ function performanceNow() {
11285
+ return globalThis.performance?.now?.() ?? Date.now();
11286
+ }
11287
+ function performanceMeasurement(phase, startedAt, slideIndex) {
11288
+ return {
11289
+ durationMs: Math.max(0, performanceNow() - startedAt),
11290
+ phase,
11291
+ ...slideIndex === void 0 ? {} : { slideIndex }
11292
+ };
11293
+ }
11294
+
11148
11295
  // src/parse.ts
11149
11296
  var DEFAULT_MAX_INPUT_BYTES = 100 * 1024 * 1024;
11150
11297
  function isParsedPresentation(source) {
@@ -11229,14 +11376,20 @@ async function parsePresentation(source, options = {}) {
11229
11376
  warnings: source.warnings
11230
11377
  };
11231
11378
  }
11379
+ const downloadStartedAt = performanceNow();
11232
11380
  const buffer = await readSource(source, options);
11381
+ if (typeof source === "string" || source instanceof URL) {
11382
+ options.onPerformanceMeasurement?.(performanceMeasurement("download", downloadStartedAt));
11383
+ }
11233
11384
  const format = detectFormat(buffer, options.formatHint);
11234
11385
  const bytes = new Uint8Array(buffer);
11235
11386
  throwIfAborted(options.signal);
11387
+ const parsingStartedAt = performanceNow();
11236
11388
  const nativeDocument = await parseWithWasm(bytes, {
11237
11389
  formatHint: format,
11238
11390
  ...options.signal ? { signal: options.signal } : {}
11239
11391
  });
11392
+ options.onPerformanceMeasurement?.(performanceMeasurement("wasm-parsing", parsingStartedAt));
11240
11393
  throwIfAborted(options.signal);
11241
11394
  if (!nativeDocument) {
11242
11395
  throw new PptxViewerError(
@@ -11309,11 +11462,15 @@ function requestInitKey(value) {
11309
11462
  return String(value);
11310
11463
  }
11311
11464
  }
11312
- async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSlideChange, reportWarning, isCurrent) {
11465
+ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSlideChange, onZoomChange, reportWarning, isCurrent) {
11313
11466
  const fontManager = new PptxFontManager(props.fonts, (warning) => {
11314
11467
  if (isCurrent()) reportWarning(warning);
11315
11468
  });
11469
+ const fontStartedAt = performanceNow();
11316
11470
  await fontManager.prepare(parsed.document);
11471
+ getCurrentProps().onPerformanceMeasurement?.(
11472
+ performanceMeasurement("embedded-font-preparation", fontStartedAt)
11473
+ );
11317
11474
  if (!isCurrent()) {
11318
11475
  fontManager.destroy();
11319
11476
  throw new Error("Viewer adapter creation was superseded.");
@@ -11324,6 +11481,9 @@ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSl
11324
11481
  onSlideChange: (index) => {
11325
11482
  if (active()) onCurrentSlideChange(index);
11326
11483
  },
11484
+ onZoomChange: (state) => {
11485
+ if (active()) onZoomChange(state);
11486
+ },
11327
11487
  onSlideRendered: (index, element) => {
11328
11488
  if (!active()) return;
11329
11489
  fontManager.applyTo(element, index);
@@ -11346,6 +11506,7 @@ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSl
11346
11506
  const normalized = new NormalizedPresentationViewer(target, parsed.document, callbacks);
11347
11507
  return {
11348
11508
  async render(mode, slideIndex) {
11509
+ const startedAt = performanceNow();
11349
11510
  if (mode === "continuous") {
11350
11511
  const virtualization = typeof props.virtualization === "object" ? props.virtualization : {};
11351
11512
  await normalized.renderList({
@@ -11359,10 +11520,13 @@ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSl
11359
11520
  } else {
11360
11521
  await normalized.renderSlide(slideIndex);
11361
11522
  }
11523
+ getCurrentProps().onPerformanceMeasurement?.(
11524
+ performanceMeasurement("slide-dom-svg-rendering", startedAt, slideIndex)
11525
+ );
11362
11526
  },
11363
11527
  goToSlide: (index, options) => normalized.goToSlide(index, options),
11364
- setZoom: (percent) => normalized.setZoom(percent),
11365
- setFitMode: (mode) => normalized.setFitMode(mode),
11528
+ setZoom: (level) => normalized.setZoom(level),
11529
+ resolveZoom: (level) => normalized.resolveZoom(level),
11366
11530
  search: (query, options) => normalized.searchText(query, options),
11367
11531
  highlight: (result, options) => normalized.highlightSearchResult(result, options),
11368
11532
  clearHighlights: () => normalized.clearSearchHighlights(),
@@ -11430,7 +11594,7 @@ var ReactPptxViewer = forwardRef(
11430
11594
  slideIndex: controlledSlide,
11431
11595
  initialSlide = 0,
11432
11596
  zoom: controlledZoom,
11433
- defaultZoom = 100,
11597
+ defaultZoom,
11434
11598
  fitMode = "contain",
11435
11599
  showToolbar = false,
11436
11600
  showThumbnails = false,
@@ -11461,6 +11625,7 @@ var ReactPptxViewer = forwardRef(
11461
11625
  onError,
11462
11626
  onWarning,
11463
11627
  onSlideChange: _onSlideChange,
11628
+ onZoomChange: _onZoomChange,
11464
11629
  onSlideRendered: _onSlideRendered,
11465
11630
  onSlideUnmounted: _onSlideUnmounted,
11466
11631
  onSearchResults,
@@ -11479,7 +11644,12 @@ var ReactPptxViewer = forwardRef(
11479
11644
  const [error, setError] = useState(null);
11480
11645
  const [loading, setLoading] = useState(true);
11481
11646
  const [internalSlide, setInternalSlide] = useState(initialSlide);
11482
- const [internalZoom, setInternalZoom] = useState(defaultZoom);
11647
+ const [internalZoom, setInternalZoom] = useState(
11648
+ () => normalizeViewerZoomLevel(defaultZoom ?? (fitMode === "contain" ? "automatic" : 100))
11649
+ );
11650
+ const [resolvedZoom, setResolvedZoom] = useState(
11651
+ () => typeof defaultZoom === "number" ? normalizeViewerZoomLevel(defaultZoom) : 100
11652
+ );
11483
11653
  const [results, setResults] = useState([]);
11484
11654
  const [runtimeWarnings, setRuntimeWarnings] = useState([]);
11485
11655
  const virtualizationOptions = typeof virtualization === "object" ? virtualization : {};
@@ -11493,14 +11663,33 @@ var ReactPptxViewer = forwardRef(
11493
11663
  const parseFetchInit = parseOptions?.fetchInit;
11494
11664
  const parseFetchInitKey = requestInitKey(parseFetchInit);
11495
11665
  const slide = clampSlide(controlledSlide ?? internalSlide, parsed?.document.slides.length ?? 0);
11496
- const zoom = controlledZoom ?? internalZoom;
11666
+ const zoom = normalizeViewerZoomLevel(controlledZoom ?? internalZoom);
11497
11667
  const latestSlideRef = useRef(slide);
11498
11668
  const latestZoomRef = useRef(zoom);
11499
- const latestFitModeRef = useRef(fitMode);
11669
+ const latestResolvedZoomRef = useRef(resolvedZoom);
11670
+ const latestControlledZoomRef = useRef(controlledZoom);
11671
+ const lastPublishedZoomRef = useRef(void 0);
11500
11672
  const lastViewerSlideRef = useRef(void 0);
11501
11673
  latestSlideRef.current = slide;
11502
11674
  latestZoomRef.current = zoom;
11503
- latestFitModeRef.current = fitMode;
11675
+ latestResolvedZoomRef.current = resolvedZoom;
11676
+ latestControlledZoomRef.current = controlledZoom;
11677
+ const notifyZoomChange = useCallback((state) => {
11678
+ const previous = lastPublishedZoomRef.current;
11679
+ if (previous?.level === state.level && Math.abs(previous.resolvedZoom - state.resolvedZoom) <= 0.01) {
11680
+ return;
11681
+ }
11682
+ lastPublishedZoomRef.current = state;
11683
+ latestPropsRef.current.onZoomChange?.(state);
11684
+ }, []);
11685
+ const publishZoomChange = useCallback(
11686
+ (state) => {
11687
+ setResolvedZoom(state.resolvedZoom);
11688
+ latestResolvedZoomRef.current = state.resolvedZoom;
11689
+ notifyZoomChange(state);
11690
+ },
11691
+ [notifyZoomChange]
11692
+ );
11504
11693
  const thumbnailCount = showThumbnails ? parsed?.document.slides.length ?? 0 : 0;
11505
11694
  const getThumbnailKey = useCallback(
11506
11695
  (index) => parsed?.document.slides[index]?.id ?? index,
@@ -11541,13 +11730,28 @@ var ReactPptxViewer = forwardRef(
11541
11730
  block: "center"
11542
11731
  });
11543
11732
  },
11544
- async setZoom(percent) {
11545
- const next = Math.max(10, Math.min(400, percent));
11733
+ async setZoom(level) {
11734
+ const next = normalizeViewerZoomLevel(level);
11735
+ const adapter = adapterRef.current;
11736
+ if (latestControlledZoomRef.current !== void 0) {
11737
+ notifyZoomChange({
11738
+ level: next,
11739
+ resolvedZoom: adapter?.resolveZoom(next) ?? (typeof next === "number" ? next : latestResolvedZoomRef.current)
11740
+ });
11741
+ return;
11742
+ }
11546
11743
  setInternalZoom(next);
11547
- await adapterRef.current?.setZoom(next);
11744
+ latestZoomRef.current = next;
11745
+ if (adapter) await adapter.setZoom(next);
11746
+ else {
11747
+ publishZoomChange({
11748
+ level: next,
11749
+ resolvedZoom: typeof next === "number" ? next : latestResolvedZoomRef.current
11750
+ });
11751
+ }
11548
11752
  },
11549
11753
  async setFitMode(nextMode) {
11550
- await adapterRef.current?.setFitMode(nextMode);
11754
+ await this.setZoom(nextMode === "contain" ? "automatic" : 100);
11551
11755
  },
11552
11756
  search(query, options) {
11553
11757
  return adapterRef.current?.search(query, options) ?? [];
@@ -11571,9 +11775,10 @@ var ReactPptxViewer = forwardRef(
11571
11775
  },
11572
11776
  getDocument: () => parsed?.document ?? null,
11573
11777
  getSlideIndex: () => latestSlideRef.current,
11574
- getZoom: () => latestZoomRef.current
11778
+ getZoom: () => latestZoomRef.current,
11779
+ getResolvedZoom: () => latestResolvedZoomRef.current
11575
11780
  }),
11576
- [adapterState?.generation, parsed]
11781
+ [adapterState?.generation, notifyZoomChange, parsed, publishZoomChange]
11577
11782
  );
11578
11783
  useImperativeHandle(forwardedRef, () => controller, [controller]);
11579
11784
  useEffect(() => {
@@ -11591,7 +11796,8 @@ var ReactPptxViewer = forwardRef(
11591
11796
  signal: abort.signal,
11592
11797
  ...parseFormatHint ? { formatHint: parseFormatHint } : {},
11593
11798
  ...parseMaxInputBytes !== void 0 ? { maxInputBytes: parseMaxInputBytes } : {},
11594
- ...parseFetchInit ? { fetchInit: parseFetchInit } : {}
11799
+ ...parseFetchInit ? { fetchInit: parseFetchInit } : {},
11800
+ onPerformanceMeasurement: (measurement) => latestPropsRef.current.onPerformanceMeasurement?.(measurement)
11595
11801
  }).then((next) => {
11596
11802
  if (abort.signal.aborted) return;
11597
11803
  setParsed(next);
@@ -11646,6 +11852,7 @@ var ReactPptxViewer = forwardRef(
11646
11852
  setInternalSlide(index);
11647
11853
  latestPropsRef.current.onSlideChange?.(index);
11648
11854
  },
11855
+ publishZoomChange,
11649
11856
  (warning) => {
11650
11857
  if (!isCurrent()) return;
11651
11858
  setRuntimeWarnings(
@@ -11667,11 +11874,6 @@ var ReactPptxViewer = forwardRef(
11667
11874
  adapter.destroy();
11668
11875
  return;
11669
11876
  }
11670
- await adapter.setFitMode(latestFitModeRef.current);
11671
- if (!isCurrent()) {
11672
- adapter.destroy();
11673
- return;
11674
- }
11675
11877
  await adapter.render(mode, latestSlideRef.current);
11676
11878
  if (!isCurrent()) {
11677
11879
  adapter.destroy();
@@ -11709,7 +11911,7 @@ var ReactPptxViewer = forwardRef(
11709
11911
  overscanViewport,
11710
11912
  batchSize,
11711
11913
  scrollElement,
11712
- width
11914
+ publishZoomChange
11713
11915
  ]);
11714
11916
  useEffect(() => {
11715
11917
  if (adapterState && controlledSlide !== void 0 && slide !== lastViewerSlideRef.current) {
@@ -11717,13 +11919,12 @@ var ReactPptxViewer = forwardRef(
11717
11919
  }
11718
11920
  }, [adapterState, controlledSlide, slide]);
11719
11921
  useEffect(() => {
11720
- if (adapterState && controlledZoom !== void 0) {
11721
- void adapterState.adapter.setZoom(zoom);
11722
- }
11723
- }, [adapterState, controlledZoom, zoom]);
11922
+ if (adapterState) void adapterState.adapter.setZoom(zoom);
11923
+ }, [adapterState, zoom]);
11724
11924
  useEffect(() => {
11725
- if (adapterState) void adapterState.adapter.setFitMode(fitMode);
11726
- }, [adapterState, fitMode]);
11925
+ if (controlledZoom !== void 0 || defaultZoom !== void 0) return;
11926
+ setInternalZoom(fitMode === "contain" ? "automatic" : 100);
11927
+ }, [controlledZoom, defaultZoom, fitMode]);
11727
11928
  useEffect(() => {
11728
11929
  const adapter = adapterState?.adapter;
11729
11930
  if (!adapter || searchQuery === void 0 || searchQuery === "") {
@@ -11800,20 +12001,20 @@ var ReactPptxViewer = forwardRef(
11800
12001
  "button",
11801
12002
  {
11802
12003
  type: "button",
11803
- onClick: () => void controller.setZoom(zoom - 10),
12004
+ onClick: () => void controller.setZoom(resolvedZoom - 10),
11804
12005
  "aria-label": "Zoom out",
11805
12006
  children: "\u2212"
11806
12007
  }
11807
12008
  ),
11808
12009
  /* @__PURE__ */ jsxs("span", { className: "rpv-toolbar__count", children: [
11809
- zoom,
12010
+ Math.round(resolvedZoom),
11810
12011
  "%"
11811
12012
  ] }),
11812
12013
  /* @__PURE__ */ jsx(
11813
12014
  "button",
11814
12015
  {
11815
12016
  type: "button",
11816
- onClick: () => void controller.setZoom(zoom + 10),
12017
+ onClick: () => void controller.setZoom(resolvedZoom + 10),
11817
12018
  "aria-label": "Zoom in",
11818
12019
  children: "+"
11819
12020
  }
@@ -12284,6 +12485,404 @@ function usePptxViewerThumbnails(controller, options = {}) {
12284
12485
  );
12285
12486
  }
12286
12487
 
12287
- export { OFFICE_FONT_FALLBACKS, PptxViewerError, ReactPptxViewer, initWasm, parsePresentation, presentationParsingDefaults, resolvePptxFontFamily, setWasmSource, usePptxModel, usePptxPresentation, usePptxViewer, usePptxViewerThumbnails };
12488
+ // src/thumbnail-renderer.ts
12489
+ var EMU_PER_CSS_PIXEL3 = 9525;
12490
+ var DEFAULT_MAX_DIMENSION3 = 160;
12491
+ var RenderQueue = class {
12492
+ constructor(limit) {
12493
+ this.limit = limit;
12494
+ }
12495
+ limit;
12496
+ active = 0;
12497
+ destroyed = false;
12498
+ waiters = [];
12499
+ acquire(signal) {
12500
+ throwIfAborted(signal);
12501
+ if (this.destroyed) return Promise.reject(abortedError());
12502
+ return new Promise((resolve, reject) => {
12503
+ const waiter = { resolve, reject, ...signal ? { signal } : {} };
12504
+ if (signal) {
12505
+ waiter.abort = () => {
12506
+ const index = this.waiters.indexOf(waiter);
12507
+ if (index >= 0) this.waiters.splice(index, 1);
12508
+ reject(abortedError(signal.reason));
12509
+ };
12510
+ signal.addEventListener("abort", waiter.abort, { once: true });
12511
+ }
12512
+ this.waiters.push(waiter);
12513
+ this.drain();
12514
+ });
12515
+ }
12516
+ destroy() {
12517
+ this.destroyed = true;
12518
+ for (const waiter of this.waiters.splice(0)) {
12519
+ if (waiter.abort && waiter.signal) {
12520
+ waiter.signal.removeEventListener("abort", waiter.abort);
12521
+ }
12522
+ waiter.reject(abortedError());
12523
+ }
12524
+ }
12525
+ drain() {
12526
+ while (!this.destroyed && this.active < this.limit && this.waiters.length) {
12527
+ const waiter = this.waiters.shift();
12528
+ if (waiter.abort && waiter.signal) {
12529
+ waiter.signal.removeEventListener("abort", waiter.abort);
12530
+ }
12531
+ if (waiter.signal?.aborted) {
12532
+ waiter.reject(abortedError(waiter.signal.reason));
12533
+ continue;
12534
+ }
12535
+ this.active += 1;
12536
+ let released = false;
12537
+ waiter.resolve(() => {
12538
+ if (released) return;
12539
+ released = true;
12540
+ this.active -= 1;
12541
+ this.drain();
12542
+ });
12543
+ }
12544
+ }
12545
+ };
12546
+ function abortedError(reason) {
12547
+ return new PptxViewerError("aborted", "Thumbnail rendering was cancelled.", { cause: reason });
12548
+ }
12549
+ function finitePositive3(value, fallback) {
12550
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
12551
+ }
12552
+ function outputSize(presentation, options) {
12553
+ const naturalWidth = Math.max(1, presentation.document.size.widthEmu / EMU_PER_CSS_PIXEL3);
12554
+ const naturalHeight = Math.max(1, presentation.document.size.heightEmu / EMU_PER_CSS_PIXEL3);
12555
+ const hasWidth = options.maxWidth !== void 0 && Number.isFinite(options.maxWidth) && options.maxWidth > 0;
12556
+ const hasHeight = options.maxHeight !== void 0 && Number.isFinite(options.maxHeight) && options.maxHeight > 0;
12557
+ const maxWidth = hasWidth ? options.maxWidth : hasHeight ? Number.POSITIVE_INFINITY : DEFAULT_MAX_DIMENSION3;
12558
+ const maxHeight = hasHeight ? options.maxHeight : hasWidth ? Number.POSITIVE_INFINITY : DEFAULT_MAX_DIMENSION3;
12559
+ const scale = Math.min(maxWidth / naturalWidth, maxHeight / naturalHeight);
12560
+ return {
12561
+ height: Math.max(1, Math.round(naturalHeight * scale)),
12562
+ naturalHeight,
12563
+ naturalWidth,
12564
+ width: Math.max(1, Math.round(naturalWidth * scale))
12565
+ };
12566
+ }
12567
+ function blobToDataUrl(blob, signal) {
12568
+ return abortable(
12569
+ new Promise((resolve, reject) => {
12570
+ const reader = new FileReader();
12571
+ reader.onload = () => resolve(String(reader.result));
12572
+ reader.onerror = () => reject(reader.error ?? new Error("Could not read a slide asset."));
12573
+ reader.readAsDataURL(blob);
12574
+ }),
12575
+ signal
12576
+ );
12577
+ }
12578
+ async function inlineResourceUrl(url, signal) {
12579
+ if (!/^(?:blob:|https?:)/i.test(url)) return url;
12580
+ try {
12581
+ const response = await fetch(url, signal ? { signal } : void 0);
12582
+ if (!response.ok) return url;
12583
+ return await blobToDataUrl(await response.blob(), signal);
12584
+ } catch {
12585
+ throwIfAborted(signal);
12586
+ return url;
12587
+ }
12588
+ }
12589
+ async function inlineResources(root, signal) {
12590
+ const cache = /* @__PURE__ */ new Map();
12591
+ const inline = (url) => {
12592
+ let pending = cache.get(url);
12593
+ if (!pending) {
12594
+ pending = inlineResourceUrl(url, signal);
12595
+ cache.set(url, pending);
12596
+ }
12597
+ return pending;
12598
+ };
12599
+ const elements = [root, ...root.querySelectorAll("*")];
12600
+ await Promise.all(
12601
+ elements.flatMap((element) => {
12602
+ const attributeNames = ["src", "poster", ...element.localName === "image" ? ["href"] : []];
12603
+ const attributes = attributeNames.map((name) => ({ name, value: element.getAttribute(name) })).filter((entry) => Boolean(entry.value));
12604
+ const attributeTasks = attributes.map(async ({ name, value }) => {
12605
+ element.setAttribute(name, await inline(value));
12606
+ });
12607
+ const styleTasks = Array.from(
12608
+ { length: element.style.length },
12609
+ (_, index) => element.style.item(index)
12610
+ ).map((property) => ({ property, value: element.style.getPropertyValue(property) })).filter(({ value }) => value.includes("url(")).map(async ({ property, value }) => {
12611
+ const urls = [...value.matchAll(/url\(["']?([^"')]+)["']?\)/g)].map((match) => match[1]);
12612
+ let next = value;
12613
+ for (const url of urls) next = next.replace(url, await inline(url));
12614
+ element.style.setProperty(property, next);
12615
+ });
12616
+ return [...attributeTasks, ...styleTasks];
12617
+ })
12618
+ );
12619
+ }
12620
+ async function serializeSlide(slide, naturalWidth, naturalHeight, width, height, signal) {
12621
+ const clone = slide.cloneNode(true);
12622
+ clone.style.transform = "none";
12623
+ clone.style.transformOrigin = "top left";
12624
+ clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
12625
+ const sourceCanvases = slide.querySelectorAll("canvas");
12626
+ const clonedCanvases = clone.querySelectorAll("canvas");
12627
+ sourceCanvases.forEach((canvas, index) => {
12628
+ const cloned = clonedCanvases[index];
12629
+ if (!cloned) return;
12630
+ try {
12631
+ const image = document.createElement("img");
12632
+ image.src = canvas.toDataURL("image/png");
12633
+ image.setAttribute("style", cloned.getAttribute("style") ?? "");
12634
+ cloned.replaceWith(image);
12635
+ } catch {
12636
+ cloned.remove();
12637
+ }
12638
+ });
12639
+ await inlineResources(clone, signal);
12640
+ const content = new XMLSerializer().serializeToString(clone);
12641
+ 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>`;
12642
+ }
12643
+ async function abortable(promise, signal) {
12644
+ throwIfAborted(signal);
12645
+ if (!signal) return promise;
12646
+ return new Promise((resolve, reject) => {
12647
+ const abort = () => reject(abortedError(signal.reason));
12648
+ signal.addEventListener("abort", abort, { once: true });
12649
+ void promise.then(
12650
+ (value) => {
12651
+ signal.removeEventListener("abort", abort);
12652
+ resolve(value);
12653
+ },
12654
+ (error) => {
12655
+ signal.removeEventListener("abort", abort);
12656
+ reject(error);
12657
+ }
12658
+ );
12659
+ });
12660
+ }
12661
+ async function decodeElementImages(root, signal) {
12662
+ const images = [...root.querySelectorAll("img")];
12663
+ const svgImages = [...root.querySelectorAll("image")].map((image) => image.href.baseVal || image.getAttribute("href") || "").filter(Boolean);
12664
+ await abortable(
12665
+ Promise.all([
12666
+ ...images.map(async (image) => {
12667
+ if (typeof image.decode === "function") await image.decode().catch(() => void 0);
12668
+ }),
12669
+ ...svgImages.map(
12670
+ (source) => new Promise((resolve) => {
12671
+ const image = new Image();
12672
+ image.onload = () => resolve();
12673
+ image.onerror = () => resolve();
12674
+ image.src = source;
12675
+ })
12676
+ )
12677
+ ]).then(() => void 0),
12678
+ signal
12679
+ );
12680
+ }
12681
+ async function decodeSvg(svg, signal) {
12682
+ return abortable(
12683
+ new Promise((resolve, reject) => {
12684
+ const image = new Image();
12685
+ image.onload = () => resolve(image);
12686
+ image.onerror = () => reject(new Error("The rendered slide SVG could not be decoded."));
12687
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
12688
+ }),
12689
+ signal
12690
+ );
12691
+ }
12692
+ function canvasToBlob(canvas, quality) {
12693
+ return new Promise((resolve, reject) => {
12694
+ canvas.toBlob(
12695
+ (blob) => {
12696
+ if (blob) resolve(blob);
12697
+ else reject(new Error("The browser could not encode the thumbnail."));
12698
+ },
12699
+ "image/png",
12700
+ quality
12701
+ );
12702
+ });
12703
+ }
12704
+ var ThumbnailRenderer = class {
12705
+ constructor(presentation, options) {
12706
+ this.presentation = presentation;
12707
+ this.options = options;
12708
+ if (typeof document === "undefined") {
12709
+ throw new PptxViewerError(
12710
+ "unsupported-environment",
12711
+ "Thumbnail rendering requires a browser DOM."
12712
+ );
12713
+ }
12714
+ const concurrency = Math.max(1, Math.floor(finitePositive3(options.concurrency, 2)));
12715
+ this.queue = new RenderQueue(concurrency);
12716
+ this.root = document.createElement("div");
12717
+ this.root.setAttribute("aria-hidden", "true");
12718
+ this.root.style.position = "fixed";
12719
+ this.root.style.left = "-100000px";
12720
+ this.root.style.top = "0";
12721
+ this.root.style.pointerEvents = "none";
12722
+ this.root.style.opacity = "0";
12723
+ document.body.append(this.root);
12724
+ this.fontManager = new PptxFontManager(
12725
+ {
12726
+ loadEmbeddedFonts: false,
12727
+ reportMissingFonts: false,
12728
+ waitForFonts: false,
12729
+ ...options.fonts
12730
+ },
12731
+ (warning) => options.onWarning?.(warning)
12732
+ );
12733
+ this.viewer = new NormalizedPresentationViewer(this.root, presentation.document, {
12734
+ onNodeError: (nodeId, error) => options.onWarning?.({
12735
+ code: "degraded-rendering",
12736
+ severity: "warning",
12737
+ nodeId,
12738
+ feature: "thumbnail-renderer",
12739
+ message: `A slide element could not be rendered: ${error instanceof Error ? error.message : String(error)}`
12740
+ })
12741
+ });
12742
+ const startedAt = performanceNow();
12743
+ this.ready = this.fontManager.prepare(presentation.document).then(() => {
12744
+ this.report(performanceMeasurement("embedded-font-preparation", startedAt));
12745
+ });
12746
+ }
12747
+ presentation;
12748
+ options;
12749
+ ready;
12750
+ fontManager;
12751
+ queue;
12752
+ root;
12753
+ viewer;
12754
+ destroyed = false;
12755
+ async renderSlide(slideIndex, options = {}) {
12756
+ const release = await this.queue.acquire(options.signal);
12757
+ try {
12758
+ this.assertAvailable(options.signal);
12759
+ await abortable(this.ready, options.signal);
12760
+ this.assertAvailable(options.signal);
12761
+ if (!Number.isInteger(slideIndex) || slideIndex < 0 || slideIndex >= this.presentation.document.slides.length) {
12762
+ throw new RangeError(`Slide ${slideIndex} does not exist.`);
12763
+ }
12764
+ const measurements = [];
12765
+ const size = outputSize(this.presentation, options);
12766
+ const target = document.createElement("div");
12767
+ this.root.append(target);
12768
+ const domStartedAt = performanceNow();
12769
+ const handle = this.viewer.renderThumbnailToContainer(slideIndex, target, {
12770
+ width: size.naturalWidth
12771
+ });
12772
+ try {
12773
+ await abortable(handle.ready, options.signal);
12774
+ this.fontManager.applyTo(handle.element, slideIndex);
12775
+ const domDurationMs = performanceNow() - domStartedAt;
12776
+ const decodeStartedAt = performanceNow();
12777
+ await decodeElementImages(handle.element, options.signal);
12778
+ let decodeDurationMs = performanceNow() - decodeStartedAt;
12779
+ const serializationStartedAt = performanceNow();
12780
+ const svg = await serializeSlide(
12781
+ handle.element,
12782
+ size.naturalWidth,
12783
+ size.naturalHeight,
12784
+ size.width,
12785
+ size.height,
12786
+ options.signal
12787
+ );
12788
+ this.record(
12789
+ {
12790
+ durationMs: Math.max(0, domDurationMs + performanceNow() - serializationStartedAt),
12791
+ phase: "slide-dom-svg-rendering",
12792
+ slideIndex
12793
+ },
12794
+ measurements
12795
+ );
12796
+ const output = options.output ?? "blob";
12797
+ if (output === "svg") {
12798
+ this.record(
12799
+ { durationMs: Math.max(0, decodeDurationMs), phase: "image-decoding", slideIndex },
12800
+ measurements
12801
+ );
12802
+ return this.result(svg, output, slideIndex, size, measurements);
12803
+ }
12804
+ const svgDecodeStartedAt = performanceNow();
12805
+ const image = await decodeSvg(svg, options.signal);
12806
+ decodeDurationMs += performanceNow() - svgDecodeStartedAt;
12807
+ this.record(
12808
+ { durationMs: Math.max(0, decodeDurationMs), phase: "image-decoding", slideIndex },
12809
+ measurements
12810
+ );
12811
+ this.assertAvailable(options.signal);
12812
+ const encodeStartedAt = performanceNow();
12813
+ const pixelRatio = finitePositive3(options.pixelRatio, 1);
12814
+ const canvas = document.createElement("canvas");
12815
+ canvas.width = Math.max(1, Math.round(size.width * pixelRatio));
12816
+ canvas.height = Math.max(1, Math.round(size.height * pixelRatio));
12817
+ canvas.style.width = `${size.width}px`;
12818
+ canvas.style.height = `${size.height}px`;
12819
+ const context = canvas.getContext("2d");
12820
+ if (!context) throw new Error("The browser does not provide a 2D canvas renderer.");
12821
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
12822
+ let data;
12823
+ if (output === "canvas") data = canvas;
12824
+ else if (output === "imageBitmap") {
12825
+ if (typeof createImageBitmap !== "function") {
12826
+ throw new PptxViewerError(
12827
+ "unsupported-environment",
12828
+ "ImageBitmap output is not supported by this browser."
12829
+ );
12830
+ }
12831
+ data = await abortable(createImageBitmap(canvas), options.signal);
12832
+ } else data = await abortable(canvasToBlob(canvas, options.quality), options.signal);
12833
+ this.measure("raster-encoding", encodeStartedAt, slideIndex, measurements);
12834
+ return this.result(data, output, slideIndex, size, measurements);
12835
+ } finally {
12836
+ handle.dispose();
12837
+ target.remove();
12838
+ }
12839
+ } finally {
12840
+ release();
12841
+ }
12842
+ }
12843
+ renderSlides(slideIndexes, options = {}) {
12844
+ return Promise.all(slideIndexes.map((slideIndex) => this.renderDynamic(slideIndex, options)));
12845
+ }
12846
+ destroy() {
12847
+ if (this.destroyed) return;
12848
+ this.destroyed = true;
12849
+ this.queue.destroy();
12850
+ this.viewer.destroy();
12851
+ this.fontManager.destroy();
12852
+ this.root.remove();
12853
+ }
12854
+ renderDynamic(slideIndex, options) {
12855
+ return this.renderSlide(slideIndex, options);
12856
+ }
12857
+ assertAvailable(signal) {
12858
+ throwIfAborted(signal);
12859
+ if (this.destroyed) throw abortedError();
12860
+ }
12861
+ measure(phase, startedAt, slideIndex, measurements) {
12862
+ this.record(performanceMeasurement(phase, startedAt, slideIndex), measurements);
12863
+ }
12864
+ record(measurement, measurements) {
12865
+ measurements.push(measurement);
12866
+ this.report(measurement);
12867
+ }
12868
+ report(measurement) {
12869
+ this.options.onPerformanceMeasurement?.(measurement);
12870
+ }
12871
+ result(data, output, slideIndex, size, measurements) {
12872
+ return {
12873
+ data,
12874
+ height: size.height,
12875
+ measurements,
12876
+ output,
12877
+ slideIndex,
12878
+ width: size.width
12879
+ };
12880
+ }
12881
+ };
12882
+ function createPptxThumbnailRenderer(presentation, options = {}) {
12883
+ return new ThumbnailRenderer(presentation, options);
12884
+ }
12885
+
12886
+ export { OFFICE_FONT_FALLBACKS, PptxViewerError, ReactPptxViewer, createPptxThumbnailRenderer, initWasm, parsePresentation, presentationParsingDefaults, resolvePptxFontFamily, setWasmSource, usePptxModel, usePptxPresentation, usePptxViewer, usePptxViewerThumbnails };
12288
12887
  //# sourceMappingURL=index.js.map
12289
12888
  //# sourceMappingURL=index.js.map