canvu-react 0.3.12 → 0.3.14

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/README.md CHANGED
@@ -255,6 +255,11 @@ export function BoardImport() {
255
255
  files: Array.from(files),
256
256
  worldCenter: { x: 0, y: 0 },
257
257
  assetStore,
258
+ pdfScale: 1.15,
259
+ pdfPageConcurrency: 2,
260
+ onItemsReady(nextItems) {
261
+ doc.onItemsChange([...doc.items, ...nextItems]);
262
+ },
258
263
  decorateItem(item) {
259
264
  return {
260
265
  ...item,
@@ -265,7 +270,6 @@ export function BoardImport() {
265
270
 
266
271
  doc.onItemsChange([...doc.items, ...result.items]);
267
272
  }
268
-
269
273
  if (!doc.isHydrated) return null;
270
274
 
271
275
  return (
@@ -300,6 +304,10 @@ export function BoardImport() {
300
304
  }
301
305
  ```
302
306
 
307
+ canvu can now stream PDF pages into the canvas progressively through
308
+ `onItemsReady(...)`, skip PDF thumbnails during ingest, and use a lower initial
309
+ raster scale by default to improve time-to-first-render.
310
+
303
311
  This helper is the same ingestion layer used internally by the native file
304
312
  tool, so external imports do not need to reimplement PDF rasterization, local
305
313
  blob persistence, or `pluginData` attachment.
package/dist/index.cjs CHANGED
@@ -243,10 +243,32 @@ async function runWithConcurrency(items, concurrency, execute) {
243
243
  async function loadPdfToStore(file, store, options) {
244
244
  const scale = options?.scale ?? 1.5;
245
245
  const pageConcurrency = options?.pageConcurrency ?? 2;
246
+ const storeThumbnails = options?.storeThumbnails ?? false;
246
247
  const pdfjs = await getPdfJs();
247
248
  const arrayBuffer = await file.arrayBuffer();
248
249
  const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise;
249
250
  const pageNumbers = normalizePdfPageNumbers(options?.pageNumbers, pdf.numPages);
251
+ const bufferedResults = /* @__PURE__ */ new Map();
252
+ let nextEmitIndex = 0;
253
+ let emitChain = Promise.resolve();
254
+ const queuePageEmission = async (pageResult) => {
255
+ bufferedResults.set(pageResult.pageNumber, pageResult);
256
+ const run = async () => {
257
+ while (nextEmitIndex < pageNumbers.length) {
258
+ const nextPageNumber = pageNumbers[nextEmitIndex];
259
+ if (nextPageNumber == null) break;
260
+ const bufferedResult = bufferedResults.get(nextPageNumber);
261
+ if (!bufferedResult) break;
262
+ bufferedResults.delete(nextPageNumber);
263
+ nextEmitIndex += 1;
264
+ await options?.onPageStored?.(bufferedResult);
265
+ }
266
+ };
267
+ const nextChain = emitChain.then(run, run);
268
+ emitChain = nextChain.catch(() => {
269
+ });
270
+ await nextChain;
271
+ };
250
272
  return await runWithConcurrency(
251
273
  pageNumbers,
252
274
  pageConcurrency,
@@ -256,27 +278,31 @@ async function loadPdfToStore(file, store, options) {
256
278
  const mime = "image/png";
257
279
  const pageBlob = await canvasToBlob(canvas, mime);
258
280
  const blobId = await store.storeOriginal(pageBlob);
259
- const thumbScale = Math.min(1, 256 / Math.max(width, height));
260
- const tw = Math.max(1, Math.round(width * thumbScale));
261
- const th = Math.max(1, Math.round(height * thumbScale));
262
- const thumbCanvas = document.createElement("canvas");
263
- thumbCanvas.width = tw;
264
- thumbCanvas.height = th;
265
- const tCtx = thumbCanvas.getContext("2d");
266
- if (tCtx) {
267
- tCtx.imageSmoothingEnabled = true;
268
- tCtx.imageSmoothingQuality = "high";
269
- tCtx.drawImage(canvas, 0, 0, tw, th);
270
- }
271
- const thumbBlob = await canvasToBlob(thumbCanvas, mime);
272
- const thumbnailBlobId = await store.storeThumbnail(thumbBlob);
273
- return {
281
+ const thumbnailBlobId = storeThumbnails ? await (async () => {
282
+ const thumbScale = Math.min(1, 256 / Math.max(width, height));
283
+ const tw = Math.max(1, Math.round(width * thumbScale));
284
+ const th = Math.max(1, Math.round(height * thumbScale));
285
+ const thumbCanvas = document.createElement("canvas");
286
+ thumbCanvas.width = tw;
287
+ thumbCanvas.height = th;
288
+ const tCtx = thumbCanvas.getContext("2d");
289
+ if (tCtx) {
290
+ tCtx.imageSmoothingEnabled = true;
291
+ tCtx.imageSmoothingQuality = "high";
292
+ tCtx.drawImage(canvas, 0, 0, tw, th);
293
+ }
294
+ const thumbBlob = await canvasToBlob(thumbCanvas, mime);
295
+ return await store.storeThumbnail(thumbBlob);
296
+ })() : "";
297
+ const pageResult = {
274
298
  blobId,
275
299
  thumbnailBlobId,
276
300
  width,
277
301
  height,
278
302
  pageNumber
279
303
  };
304
+ await queuePageEmission(pageResult);
305
+ return pageResult;
280
306
  }
281
307
  );
282
308
  }
@@ -1713,7 +1739,8 @@ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
1713
1739
  var HYDRATION_CACHE_NAME = "canvu-asset-hydration-v1";
1714
1740
  var DEFAULT_PDF_SCALE = 1.15;
1715
1741
  var DEFAULT_PDF_PAGE_CONCURRENCY = 2;
1716
- var hydrationBlobPromises = /* @__PURE__ */ new Map();
1742
+ var hydrationSourcePromises = /* @__PURE__ */ new Map();
1743
+ var pdfSourceBlobPromises = /* @__PURE__ */ new Map();
1717
1744
  function buildImageHydrationKey(assetId) {
1718
1745
  return `image:${assetId}`;
1719
1746
  }
@@ -1761,23 +1788,38 @@ async function fetchBlob(url) {
1761
1788
  return null;
1762
1789
  }
1763
1790
  }
1764
- async function getHydrationBlob(key, preferCachedRasters, loader) {
1791
+ async function getHydrationSource(key, preferCachedRasters, loader) {
1765
1792
  const cached = preferCachedRasters ? await readCachedHydrationBlob(key) : null;
1766
- if (cached) return cached;
1767
- const inFlight = hydrationBlobPromises.get(key);
1793
+ if (cached) {
1794
+ return {
1795
+ blob: cached
1796
+ };
1797
+ }
1798
+ const inFlight = hydrationSourcePromises.get(key);
1768
1799
  if (inFlight) return await inFlight;
1769
1800
  const nextPromise = (async () => {
1770
- const blob = await loader();
1771
- if (blob) {
1772
- await writeCachedHydrationBlob(key, blob);
1801
+ const source = await loader();
1802
+ if (source?.blob) {
1803
+ await writeCachedHydrationBlob(key, source.blob);
1773
1804
  }
1774
- return blob;
1805
+ return source;
1775
1806
  })();
1776
- hydrationBlobPromises.set(key, nextPromise);
1807
+ hydrationSourcePromises.set(key, nextPromise);
1808
+ try {
1809
+ return await nextPromise;
1810
+ } finally {
1811
+ hydrationSourcePromises.delete(key);
1812
+ }
1813
+ }
1814
+ async function getPdfSourceBlob(url) {
1815
+ const inFlight = pdfSourceBlobPromises.get(url);
1816
+ if (inFlight) return await inFlight;
1817
+ const nextPromise = fetchBlob(url);
1818
+ pdfSourceBlobPromises.set(url, nextPromise);
1777
1819
  try {
1778
1820
  return await nextPromise;
1779
1821
  } finally {
1780
- hydrationBlobPromises.delete(key);
1822
+ pdfSourceBlobPromises.delete(url);
1781
1823
  }
1782
1824
  }
1783
1825
  function registerObjectUrl(objectUrls, blob) {
@@ -1831,18 +1873,22 @@ async function hydrateImageAssets(requests, resolvedAssetUrls, objectUrls, prefe
1831
1873
  if (!resolvedAsset?.url) {
1832
1874
  return [assetId, null];
1833
1875
  }
1834
- const blob = await getHydrationBlob(
1876
+ const source = await getHydrationSource(
1835
1877
  buildImageHydrationKey(assetId),
1836
1878
  preferCachedRasters,
1837
- async () => await fetchBlob(resolvedAsset.url)
1879
+ async () => {
1880
+ const blob = await fetchBlob(resolvedAsset.url);
1881
+ if (!blob) return null;
1882
+ return { blob };
1883
+ }
1838
1884
  );
1839
- if (!blob) {
1885
+ if (!source?.blob) {
1840
1886
  return [assetId, null];
1841
1887
  }
1842
1888
  return [
1843
1889
  assetId,
1844
1890
  {
1845
- href: registerObjectUrl(objectUrls, blob)
1891
+ href: registerObjectUrl(objectUrls, source.blob)
1846
1892
  }
1847
1893
  ];
1848
1894
  })
@@ -1857,45 +1903,69 @@ async function hydratePdfAssets(requests, resolvedAssetUrls, objectUrls, options
1857
1903
  if (!resolvedAsset?.url) {
1858
1904
  continue;
1859
1905
  }
1860
- const missingPages = [];
1861
- for (const pageNumber of group.pageNumbers) {
1862
- const cacheKey = buildPdfHydrationKey(group.assetId, pageNumber, group.scale);
1863
- const cachedBlob = await readCachedHydrationBlob(cacheKey);
1864
- if (!cachedBlob) {
1865
- missingPages.push(pageNumber);
1906
+ const pageKeys = group.pageNumbers.map((pageNumber) => ({
1907
+ pageNumber,
1908
+ cacheKey: buildPdfHydrationKey(group.assetId, pageNumber, group.scale)
1909
+ }));
1910
+ const pagesToRender = [];
1911
+ for (const { pageNumber, cacheKey } of pageKeys) {
1912
+ const cachedBlob = options.preferCachedRasters ? await readCachedHydrationBlob(cacheKey) : null;
1913
+ if (cachedBlob) {
1914
+ hydratedPages.set(cacheKey, {
1915
+ href: registerObjectUrl(objectUrls, cachedBlob)
1916
+ });
1866
1917
  continue;
1867
1918
  }
1868
- hydratedPages.set(cacheKey, {
1869
- href: registerObjectUrl(objectUrls, cachedBlob)
1870
- });
1871
- }
1872
- if (missingPages.length === 0) {
1873
- continue;
1919
+ if (!hydrationSourcePromises.has(cacheKey)) {
1920
+ pagesToRender.push({ pageNumber, cacheKey });
1921
+ }
1874
1922
  }
1875
- const pdfBlob = await fetchBlob(resolvedAsset.url);
1876
- if (!pdfBlob) {
1877
- continue;
1923
+ if (pagesToRender.length > 0) {
1924
+ const renderPromise = (async () => {
1925
+ const pdfBlob = await getPdfSourceBlob(resolvedAsset.url);
1926
+ if (!pdfBlob) {
1927
+ return /* @__PURE__ */ new Map();
1928
+ }
1929
+ const renderedPages = await loadPdfToStore(pdfBlob, options.imageStore, {
1930
+ scale: group.scale,
1931
+ pageNumbers: pagesToRender.map(({ pageNumber }) => pageNumber),
1932
+ pageConcurrency: options.pdfPageConcurrency
1933
+ });
1934
+ const renderedByPage = /* @__PURE__ */ new Map();
1935
+ for (const renderedPage of renderedPages) {
1936
+ const pageBlob = await options.imageStore.getOriginal(renderedPage.blobId);
1937
+ renderedByPage.set(
1938
+ renderedPage.pageNumber,
1939
+ pageBlob ? {
1940
+ blob: pageBlob,
1941
+ width: renderedPage.width,
1942
+ height: renderedPage.height
1943
+ } : null
1944
+ );
1945
+ }
1946
+ return renderedByPage;
1947
+ })();
1948
+ for (const { pageNumber, cacheKey } of pagesToRender) {
1949
+ const pagePromise = getHydrationSource(
1950
+ cacheKey,
1951
+ options.preferCachedRasters,
1952
+ async () => (await renderPromise).get(pageNumber) ?? null
1953
+ );
1954
+ hydrationSourcePromises.set(cacheKey, pagePromise);
1955
+ }
1878
1956
  }
1879
- const renderedPages = await loadPdfToStore(pdfBlob, options.imageStore, {
1880
- scale: group.scale,
1881
- pageNumbers: missingPages,
1882
- pageConcurrency: options.pdfPageConcurrency
1883
- });
1884
- for (const renderedPage of renderedPages) {
1885
- const cacheKey = buildPdfHydrationKey(
1886
- group.assetId,
1887
- renderedPage.pageNumber,
1888
- group.scale
1957
+ for (const { cacheKey } of pageKeys) {
1958
+ if (hydratedPages.has(cacheKey)) continue;
1959
+ const source = await getHydrationSource(
1960
+ cacheKey,
1961
+ options.preferCachedRasters,
1962
+ async () => null
1889
1963
  );
1890
- const pageBlob = await options.imageStore.getOriginal(renderedPage.blobId);
1891
- if (!pageBlob) {
1892
- continue;
1893
- }
1894
- await writeCachedHydrationBlob(cacheKey, pageBlob);
1964
+ if (!source?.blob) continue;
1895
1965
  hydratedPages.set(cacheKey, {
1896
- href: registerObjectUrl(objectUrls, pageBlob),
1897
- width: renderedPage.width,
1898
- height: renderedPage.height
1966
+ href: registerObjectUrl(objectUrls, source.blob),
1967
+ width: source.width,
1968
+ height: source.height
1899
1969
  });
1900
1970
  }
1901
1971
  }
@@ -1936,7 +2006,9 @@ async function hydrateSceneItemsWithAssets(items, assetStore, options = {}) {
1936
2006
  objectUrls,
1937
2007
  {
1938
2008
  imageStore,
1939
- pdfPageConcurrency}
2009
+ pdfPageConcurrency,
2010
+ preferCachedRasters
2011
+ }
1940
2012
  );
1941
2013
  return {
1942
2014
  items: items.map((item) => {