@anaralabs/lector 3.14.11 → 3.14.13

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
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, memo, forwardRef, useMemo, cloneElement, useEffect, useRef, useState, useCallback, useContext, useLayoutEffect, createRef, useSyncExternalStore, useImperativeHandle } from 'react';
3
+ import React, { createContext, memo, forwardRef, useMemo, cloneElement, useEffect, useRef, useLayoutEffect, useContext, useState, useCallback, useSyncExternalStore, createRef, useImperativeHandle } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { useFloating, autoUpdate, offset, flip, shift, useDismiss, useInteractions, inline } from '@floating-ui/react';
6
6
  import { useStore, createStore } from 'zustand';
@@ -9,7 +9,7 @@ import { useShallow } from 'zustand/react/shallow';
9
9
  import { useDebounce } from 'use-debounce';
10
10
  import { v4 } from 'uuid';
11
11
  import { Slot } from '@radix-ui/react-slot';
12
- import { useVirtualizer, elementScroll, debounce } from '@tanstack/react-virtual';
12
+ import { useVirtualizer, elementScroll, observeElementRect } from '@tanstack/react-virtual';
13
13
 
14
14
  // src/lib/clamp.ts
15
15
  var clamp = (value, minimum, maximum) => {
@@ -219,6 +219,19 @@ function createDarkModeColorMap(colors) {
219
219
  return map;
220
220
  }
221
221
 
222
+ // src/lib/viewport-zoom.ts
223
+ var viewportZoomHandlers = /* @__PURE__ */ new WeakMap();
224
+ function applyViewportZoom(viewport, zoom) {
225
+ if (viewport) viewportZoomHandlers.get(viewport)?.(zoom);
226
+ }
227
+ function registerViewportZoom(viewport, apply) {
228
+ viewportZoomHandlers.set(viewport, apply);
229
+ return () => {
230
+ if (viewportZoomHandlers.get(viewport) === apply)
231
+ viewportZoomHandlers.delete(viewport);
232
+ };
233
+ }
234
+
222
235
  // src/lib/zoom.ts
223
236
  var USE_LAYOUT_ZOOM = typeof navigator !== "undefined" && /AppleWebKit/i.test(navigator.userAgent) && !/Chrome|Chromium|Android/i.test(navigator.userAgent);
224
237
  var getFitWidthZoom = (containerWidth, viewports, zoomOptions) => {
@@ -271,6 +284,7 @@ var PDFStore = createZustandContext(
271
284
  minZoom,
272
285
  maxZoom
273
286
  );
287
+ applyViewportZoom(state.viewportRef.current, newZoom);
274
288
  return newZoom === state.zoom && isZoomFitWidth === state.isZoomFitWidth ? state : { zoom: newZoom, isZoomFitWidth };
275
289
  });
276
290
  },
@@ -285,7 +299,8 @@ var PDFStore = createZustandContext(
285
299
  get().updateZoom(clampedZoom, true);
286
300
  return clampedZoom;
287
301
  },
288
- currentPage: 1,
302
+ currentPage: initialState.initialPage ?? 1,
303
+ initialPage: initialState.initialPage ?? 1,
289
304
  setCurrentPage: (val) => {
290
305
  set(
291
306
  (state) => state.currentPage === val ? state : { currentPage: val }
@@ -310,9 +325,18 @@ var PDFStore = createZustandContext(
310
325
  );
311
326
  },
312
327
  pageProxies: initialState.pageProxies,
328
+ pagesLoaded: initialState.pageProxies.length === initialState.pdfDocumentProxy.numPages,
329
+ loadPdfPageProxies: () => initialState.pageResources ? initialState.pageResources.loadAll() : Promise.resolve(get().pageProxies),
330
+ isPageLoaded: (pageNumber) => Boolean(
331
+ initialState.pageResources?.get(pageNumber) ?? get().pageProxies[pageNumber - 1]
332
+ ),
333
+ loadPdfPageProxy: (pageNumber) => initialState.pageResources ? initialState.pageResources.load(pageNumber) : Promise.resolve().then(() => get().getPdfPageProxy(pageNumber)),
313
334
  getPdfPageProxy: (pageNumber) => {
314
- const proxy = get().pageProxies[pageNumber - 1];
315
- if (!proxy) throw new Error(`Page ${pageNumber} does not exist`);
335
+ const proxy = initialState.pageResources?.get(pageNumber) ?? get().pageProxies[pageNumber - 1];
336
+ if (!proxy)
337
+ throw new Error(
338
+ initialState.pageResources && Number.isInteger(pageNumber) && pageNumber >= 1 && pageNumber <= initialState.pdfDocumentProxy.numPages ? `Page ${pageNumber} is not loaded. Await loadPdfPageProxy(${pageNumber}) first.` : `Page ${pageNumber} does not exist`
339
+ );
316
340
  return proxy;
317
341
  },
318
342
  textContent: [],
@@ -665,23 +689,44 @@ var AnnotationTooltip = ({
665
689
  };
666
690
  var createAnnotationStore = () => createStore((set) => ({
667
691
  annotations: [],
668
- addAnnotation: (annotation) => set((state) => ({
669
- annotations: [...state.annotations, annotation]
670
- })),
671
- updateAnnotation: (id, updates) => set((state) => ({
672
- annotations: state.annotations.map(
673
- (annotation) => annotation.id === id ? {
692
+ addAnnotation: (annotation) => set((state) => {
693
+ const annotations = [...state.annotations, annotation];
694
+ updatePageIndex(state.annotations, annotations, /* @__PURE__ */ new Map(), [
695
+ annotation
696
+ ]);
697
+ return { annotations };
698
+ }),
699
+ updateAnnotation: (id, updates) => set((state) => {
700
+ const changes = /* @__PURE__ */ new Map();
701
+ const keys = Object.keys(updates);
702
+ const annotations = state.annotations.map((annotation) => {
703
+ if (annotation.id !== id || keys.every((key) => Object.is(annotation[key], updates[key])))
704
+ return annotation;
705
+ const updated = changes.get(annotation) ?? {
674
706
  ...annotation,
675
707
  ...updates
676
- } : annotation
677
- )
678
- })),
679
- deleteAnnotation: (id) => set((state) => ({
680
- annotations: state.annotations.filter(
681
- (annotation) => annotation.id !== id
682
- )
683
- })),
684
- setAnnotations: (annotations) => set({ annotations })
708
+ };
709
+ changes.set(annotation, updated);
710
+ return updated;
711
+ });
712
+ if (!changes.size) return state;
713
+ updatePageIndex(state.annotations, annotations, changes);
714
+ return { annotations };
715
+ }),
716
+ deleteAnnotation: (id) => set((state) => {
717
+ const changes = /* @__PURE__ */ new Map();
718
+ const annotations = state.annotations.filter((annotation) => {
719
+ if (annotation.id !== id) return true;
720
+ changes.set(annotation, null);
721
+ return false;
722
+ });
723
+ if (!changes.size) return state;
724
+ updatePageIndex(state.annotations, annotations, changes);
725
+ return { annotations };
726
+ }),
727
+ setAnnotations: (annotations) => set(
728
+ (state) => state.annotations === annotations ? state : { annotations }
729
+ )
685
730
  }));
686
731
  var AnnotationsStore = createZustandContext(
687
732
  () => createAnnotationStore()
@@ -713,9 +758,7 @@ function annotationsByPage(annotations) {
713
758
  if (index) return index;
714
759
  index = /* @__PURE__ */ new Map();
715
760
  for (const annotation of annotations) {
716
- const pages = /* @__PURE__ */ new Set([annotation.pageNumber]);
717
- for (const rect of annotation.highlights) pages.add(rect.pageNumber);
718
- for (const rect of annotation.underlines ?? []) pages.add(rect.pageNumber);
761
+ const pages = annotationPages(annotation);
719
762
  for (const page of pages) {
720
763
  let values = index.get(page);
721
764
  if (!values) {
@@ -728,6 +771,45 @@ function annotationsByPage(annotations) {
728
771
  indexes.set(annotations, index);
729
772
  return index;
730
773
  }
774
+ function annotationPages(annotation) {
775
+ const pages = /* @__PURE__ */ new Set([annotation.pageNumber]);
776
+ for (const rect of annotation.highlights) pages.add(rect.pageNumber);
777
+ for (const rect of annotation.underlines ?? []) pages.add(rect.pageNumber);
778
+ return pages;
779
+ }
780
+ function isOnPage(annotation, page) {
781
+ return annotation.pageNumber === page || annotation.highlights.some((rect) => rect.pageNumber === page) || annotation.underlines?.some((rect) => rect.pageNumber === page);
782
+ }
783
+ function updatePageIndex(previous, next, changes, appended = []) {
784
+ const previousIndex = indexes.get(previous);
785
+ if (!previousIndex) return;
786
+ const index = new Map(previousIndex);
787
+ const affected = /* @__PURE__ */ new Set();
788
+ const movedTo = /* @__PURE__ */ new Set();
789
+ for (const [before, after] of changes) {
790
+ const oldPages = annotationPages(before);
791
+ for (const page of oldPages) affected.add(page);
792
+ if (after)
793
+ for (const page of annotationPages(after)) {
794
+ affected.add(page);
795
+ if (!oldPages.has(page)) movedTo.add(page);
796
+ }
797
+ }
798
+ for (const page of affected) {
799
+ const values = movedTo.has(page) ? next.filter((annotation) => isOnPage(annotation, page)) : (previousIndex.get(page) ?? []).flatMap((annotation) => {
800
+ if (!changes.has(annotation)) return [annotation];
801
+ const updated = changes.get(annotation);
802
+ return updated && isOnPage(updated, page) ? [updated] : [];
803
+ });
804
+ if (values.length) index.set(page, values);
805
+ else index.delete(page);
806
+ }
807
+ for (const annotation of appended)
808
+ for (const page of annotationPages(annotation)) {
809
+ index.set(page, [...index.get(page) ?? [], annotation]);
810
+ }
811
+ indexes.set(next, index);
812
+ }
731
813
  function usePageAnnotations(pageNumber) {
732
814
  return useAnnotations(
733
815
  useShallow(
@@ -1035,7 +1117,7 @@ var usePdfJump = () => {
1035
1117
  }
1036
1118
  let scrollOffset;
1037
1119
  if (align === "center") {
1038
- const viewportHeight = virtualizer.scrollElement?.clientHeight || 0;
1120
+ const viewportHeight = virtualizer.scrollRect?.height ?? 0;
1039
1121
  const rectCenter = pageStart + rectTop + rectHeight / 2;
1040
1122
  scrollOffset = rectCenter - viewportHeight / 2;
1041
1123
  } else {
@@ -2108,6 +2190,69 @@ function applyContextRecolor(ctx, map, options) {
2108
2190
  function removeContextRecolor(ctx) {
2109
2191
  ctx[RECOLOR_CLEANUP]?.();
2110
2192
  }
2193
+
2194
+ // src/lib/viewport-invalidation.ts
2195
+ var viewportInvalidationRegistry = /* @__PURE__ */ new WeakMap();
2196
+ var notifyCallbacks = (entry) => {
2197
+ entry.rafId = null;
2198
+ entry.callbacks.forEach((callback) => {
2199
+ callback();
2200
+ });
2201
+ };
2202
+ var scheduleInvalidation = (entry) => {
2203
+ if (entry.rafId !== null) {
2204
+ return;
2205
+ }
2206
+ entry.rafId = requestAnimationFrame(() => {
2207
+ notifyCallbacks(entry);
2208
+ });
2209
+ };
2210
+ var subscribeToViewportInvalidation = (viewport, callback) => {
2211
+ let entry = viewportInvalidationRegistry.get(viewport);
2212
+ if (!entry) {
2213
+ const callbacks = /* @__PURE__ */ new Set();
2214
+ const schedule = () => {
2215
+ const currentEntry = viewportInvalidationRegistry.get(viewport);
2216
+ if (!currentEntry) {
2217
+ return;
2218
+ }
2219
+ scheduleInvalidation(currentEntry);
2220
+ };
2221
+ entry = {
2222
+ callbacks,
2223
+ scrollListener: schedule,
2224
+ windowResizeListener: schedule,
2225
+ resizeObserver: typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(schedule),
2226
+ rafId: null
2227
+ };
2228
+ viewport.addEventListener("scroll", entry.scrollListener, {
2229
+ passive: true
2230
+ });
2231
+ window.addEventListener("resize", entry.windowResizeListener, {
2232
+ passive: true
2233
+ });
2234
+ entry.resizeObserver?.observe(viewport);
2235
+ viewportInvalidationRegistry.set(viewport, entry);
2236
+ }
2237
+ entry.callbacks.add(callback);
2238
+ return () => {
2239
+ const currentEntry = viewportInvalidationRegistry.get(viewport);
2240
+ if (!currentEntry) {
2241
+ return;
2242
+ }
2243
+ currentEntry.callbacks.delete(callback);
2244
+ if (currentEntry.callbacks.size > 0) {
2245
+ return;
2246
+ }
2247
+ if (currentEntry.rafId !== null) {
2248
+ cancelAnimationFrame(currentEntry.rafId);
2249
+ }
2250
+ viewport.removeEventListener("scroll", currentEntry.scrollListener);
2251
+ window.removeEventListener("resize", currentEntry.windowResizeListener);
2252
+ currentEntry.resizeObserver?.disconnect();
2253
+ viewportInvalidationRegistry.delete(viewport);
2254
+ };
2255
+ };
2111
2256
  var listeners = /* @__PURE__ */ new Set();
2112
2257
  var media;
2113
2258
  var getSnapshot = () => Math.min(window.devicePixelRatio || 1, 3);
@@ -2369,7 +2514,53 @@ var useCanvasLayer = ({ background }) => {
2369
2514
  releaseBuffer();
2370
2515
  throw error;
2371
2516
  }
2517
+ let continuation = null;
2518
+ let idleTimer = null;
2519
+ let unsubscribeViewport = null;
2520
+ let priorityStopped = false;
2521
+ const clearPriorityWait = () => {
2522
+ if (idleTimer !== null) clearTimeout(idleTimer);
2523
+ idleTimer = null;
2524
+ unsubscribeViewport?.();
2525
+ unsubscribeViewport = null;
2526
+ };
2527
+ const resumeWhenVisible = () => {
2528
+ if (priorityStopped || !continuation) return;
2529
+ const { virtualizer, viewportRef } = store.getState();
2530
+ const item = virtualizer?.getVirtualItems().find((candidate) => candidate.index === pageNumber - 1);
2531
+ const offset3 = virtualizer?.scrollOffset;
2532
+ const height = virtualizer?.scrollRect?.height;
2533
+ if (virtualizer?.isScrolling && item && offset3 != null && height && (item.end < offset3 || item.start > offset3 + height)) {
2534
+ if (!unsubscribeViewport && viewportRef.current) {
2535
+ unsubscribeViewport = subscribeToViewportInvalidation(
2536
+ viewportRef.current,
2537
+ resumeWhenVisible
2538
+ );
2539
+ }
2540
+ if (idleTimer === null) {
2541
+ idleTimer = setTimeout(() => {
2542
+ idleTimer = null;
2543
+ resumeWhenVisible();
2544
+ }, 160);
2545
+ }
2546
+ return;
2547
+ }
2548
+ clearPriorityWait();
2549
+ const proceed = continuation;
2550
+ continuation = null;
2551
+ proceed();
2552
+ };
2553
+ const stopPriority = () => {
2554
+ priorityStopped = true;
2555
+ continuation = null;
2556
+ clearPriorityWait();
2557
+ };
2558
+ renderingTask.onContinue = (proceed) => {
2559
+ continuation = proceed;
2560
+ resumeWhenVisible();
2561
+ };
2372
2562
  renderingTask.promise.then(() => {
2563
+ stopPriority();
2373
2564
  if (cancelled) {
2374
2565
  restoreRecolor?.();
2375
2566
  releaseBuffer();
@@ -2414,6 +2605,7 @@ var useCanvasLayer = ({ background }) => {
2414
2605
  releaseBuffer();
2415
2606
  }
2416
2607
  }).catch((error) => {
2608
+ stopPriority();
2417
2609
  restoreRecolor?.();
2418
2610
  releaseBuffer();
2419
2611
  if (cancelled) return;
@@ -2423,6 +2615,7 @@ var useCanvasLayer = ({ background }) => {
2423
2615
  });
2424
2616
  return () => {
2425
2617
  cancelled = true;
2618
+ stopPriority();
2426
2619
  void renderingTask.cancel();
2427
2620
  };
2428
2621
  }, [
@@ -2454,71 +2647,6 @@ var useCanvasLayer = ({ background }) => {
2454
2647
  canvasRef
2455
2648
  };
2456
2649
  };
2457
-
2458
- // src/lib/viewport-invalidation.ts
2459
- var viewportInvalidationRegistry = /* @__PURE__ */ new WeakMap();
2460
- var notifyCallbacks = (entry) => {
2461
- entry.rafId = null;
2462
- entry.callbacks.forEach((callback) => {
2463
- callback();
2464
- });
2465
- };
2466
- var scheduleInvalidation = (entry) => {
2467
- if (entry.rafId !== null) {
2468
- return;
2469
- }
2470
- entry.rafId = requestAnimationFrame(() => {
2471
- notifyCallbacks(entry);
2472
- });
2473
- };
2474
- var subscribeToViewportInvalidation = (viewport, callback) => {
2475
- let entry = viewportInvalidationRegistry.get(viewport);
2476
- if (!entry) {
2477
- const callbacks = /* @__PURE__ */ new Set();
2478
- const schedule = () => {
2479
- const currentEntry = viewportInvalidationRegistry.get(viewport);
2480
- if (!currentEntry) {
2481
- return;
2482
- }
2483
- scheduleInvalidation(currentEntry);
2484
- };
2485
- entry = {
2486
- callbacks,
2487
- scrollListener: schedule,
2488
- windowResizeListener: schedule,
2489
- resizeObserver: typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(schedule),
2490
- rafId: null
2491
- };
2492
- viewport.addEventListener("scroll", entry.scrollListener, {
2493
- passive: true
2494
- });
2495
- window.addEventListener("resize", entry.windowResizeListener, {
2496
- passive: true
2497
- });
2498
- entry.resizeObserver?.observe(viewport);
2499
- viewportInvalidationRegistry.set(viewport, entry);
2500
- }
2501
- entry.callbacks.add(callback);
2502
- return () => {
2503
- const currentEntry = viewportInvalidationRegistry.get(viewport);
2504
- if (!currentEntry) {
2505
- return;
2506
- }
2507
- currentEntry.callbacks.delete(callback);
2508
- if (currentEntry.callbacks.size > 0) {
2509
- return;
2510
- }
2511
- if (currentEntry.rafId !== null) {
2512
- cancelAnimationFrame(currentEntry.rafId);
2513
- }
2514
- viewport.removeEventListener("scroll", currentEntry.scrollListener);
2515
- window.removeEventListener("resize", currentEntry.windowResizeListener);
2516
- currentEntry.resizeObserver?.disconnect();
2517
- viewportInvalidationRegistry.delete(viewport);
2518
- };
2519
- };
2520
-
2521
- // src/hooks/layers/useDetailCanvasLayer.tsx
2522
2650
  var DETAIL_RENDER_IDLE_MS = 80;
2523
2651
  var SCROLL_POLL_MS = 160;
2524
2652
  var ZOOM_DEBOUNCE_MS = 200;
@@ -2894,9 +3022,7 @@ var CanvasLayer = memo(
2894
3022
  );
2895
3023
  CanvasLayer.displayName = "CanvasLayer";
2896
3024
 
2897
- // src/hooks/useSelectionDimensions.tsx
2898
- var MERGE_THRESHOLD = 2;
2899
- var LAYER_ATTRIBUTION_TOLERANCE_PX = 4;
3025
+ // src/lib/selection-rects.ts
2900
3026
  var getTextNodeClientRects = (range) => {
2901
3027
  const root = range.commonAncestorContainer;
2902
3028
  const ownerDoc = root.ownerDocument ?? document;
@@ -2948,6 +3074,39 @@ var getTextNodeClientRects = (range) => {
2948
3074
  }
2949
3075
  return results;
2950
3076
  };
3077
+ var connected = (a, b) => {
3078
+ const overlap = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
3079
+ const gap = Math.max(a.left, b.left) - Math.min(a.left + a.width, b.left + b.width);
3080
+ return overlap >= Math.min(a.height, b.height) / 2 && gap <= Math.min(a.height, b.height) * 0.5;
3081
+ };
3082
+ var mergeTextRuns = (rects) => {
3083
+ const groups = [];
3084
+ for (const rect of rects) {
3085
+ const group = [rect];
3086
+ for (let i = groups.length - 1; i >= 0; i--) {
3087
+ if (groups[i].some((member) => connected(member, rect))) {
3088
+ group.push(...groups[i]);
3089
+ groups.splice(i, 1);
3090
+ }
3091
+ }
3092
+ groups.push(group);
3093
+ }
3094
+ return groups.map((group) => {
3095
+ const left = Math.min(...group.map((rect) => rect.left));
3096
+ const top = Math.min(...group.map((rect) => rect.top));
3097
+ return {
3098
+ ...group[0],
3099
+ left,
3100
+ top,
3101
+ width: Math.max(...group.map((rect) => rect.left + rect.width)) - left,
3102
+ height: Math.max(...group.map((rect) => rect.top + rect.height)) - top
3103
+ };
3104
+ });
3105
+ };
3106
+
3107
+ // src/hooks/useSelectionDimensions.tsx
3108
+ var MERGE_THRESHOLD = 2;
3109
+ var LAYER_ATTRIBUTION_TOLERANCE_PX = 4;
2951
3110
  var mapSelectionRectsToLayers = (range) => {
2952
3111
  const clientRects = getTextNodeClientRects(range).filter(
2953
3112
  ({ rect }) => rect.width > 2 && rect.height > 2
@@ -2990,122 +3149,6 @@ var mapSelectionRectsToLayers = (range) => {
2990
3149
  }
2991
3150
  return result;
2992
3151
  };
2993
- var shouldMergeRects = (rect1, rect2) => {
2994
- const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height);
2995
- const horizontallyConnected = Math.abs(rect1.left + rect1.width - rect2.left) <= MERGE_THRESHOLD || Math.abs(rect2.left + rect2.width - rect1.left) <= MERGE_THRESHOLD || rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
2996
- return verticalOverlap && horizontallyConnected;
2997
- };
2998
- var consolidateHighlightRects = (rects) => {
2999
- if (rects.length <= 1) return rects;
3000
- const consolidated = [];
3001
- const sorted = [...rects].sort((a, b) => {
3002
- const pageCompare = a.pageNumber - b.pageNumber;
3003
- if (pageCompare !== 0) return pageCompare;
3004
- const topDiff = a.top - b.top;
3005
- return Math.abs(topDiff) < 2 ? a.left - b.left : topDiff;
3006
- });
3007
- let current = sorted[0];
3008
- if (!current) return rects;
3009
- for (let i = 1; i < sorted.length; i++) {
3010
- const next = sorted[i];
3011
- if (!next) continue;
3012
- const samePageAndLine = current.pageNumber === next.pageNumber && Math.abs(current.top - next.top) < Math.max(current.height, next.height) * 0.5;
3013
- const horizontallyConnected = samePageAndLine && // Adjacent (touching or very close)
3014
- (Math.abs(current.left + current.width - next.left) <= MERGE_THRESHOLD || Math.abs(next.left + next.width - current.left) <= MERGE_THRESHOLD || // Overlapping
3015
- current.left < next.left + next.width && next.left < current.left + current.width || // Very close (small gap)
3016
- Math.abs(current.left + current.width - next.left) <= current.height * 0.2);
3017
- if (horizontallyConnected) {
3018
- const newLeft = Math.min(current.left, next.left);
3019
- const newRight = Math.max(
3020
- current.left + current.width,
3021
- next.left + next.width
3022
- );
3023
- const newTop = Math.min(current.top, next.top);
3024
- const newBottom = Math.max(
3025
- current.top + current.height,
3026
- next.top + next.height
3027
- );
3028
- current = {
3029
- left: newLeft,
3030
- top: newTop,
3031
- width: newRight - newLeft,
3032
- height: newBottom - newTop,
3033
- pageNumber: current.pageNumber
3034
- };
3035
- } else {
3036
- consolidated.push(current);
3037
- current = next;
3038
- }
3039
- }
3040
- if (current) {
3041
- consolidated.push(current);
3042
- }
3043
- return consolidated;
3044
- };
3045
- var consolidateRects = (rects) => {
3046
- if (rects.length <= 1) return rects;
3047
- const result = [];
3048
- const visited = /* @__PURE__ */ new Set();
3049
- for (let i = 0; i < rects.length; i++) {
3050
- if (visited.has(i)) continue;
3051
- const currentRect = rects[i];
3052
- if (!currentRect) continue;
3053
- const currentGroup = [currentRect];
3054
- visited.add(i);
3055
- let foundNew = true;
3056
- while (foundNew) {
3057
- foundNew = false;
3058
- for (let j = 0; j < rects.length; j++) {
3059
- if (visited.has(j)) continue;
3060
- const candidateRect = rects[j];
3061
- if (!candidateRect) continue;
3062
- const shouldMergeWithGroup = currentGroup.some(
3063
- (groupRect) => doRectsOverlap(groupRect, candidateRect)
3064
- );
3065
- if (shouldMergeWithGroup) {
3066
- currentGroup.push(candidateRect);
3067
- visited.add(j);
3068
- foundNew = true;
3069
- }
3070
- }
3071
- }
3072
- result.push(mergeRectGroup(currentGroup));
3073
- }
3074
- return result;
3075
- };
3076
- var doRectsOverlap = (rect1, rect2) => {
3077
- const horizontalOverlap = rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
3078
- const verticalOverlap = rect1.top < rect2.top + rect2.height && rect2.top < rect1.top + rect1.height;
3079
- const closeEnough = shouldMergeRects(rect1, rect2);
3080
- return horizontalOverlap && verticalOverlap || closeEnough;
3081
- };
3082
- var mergeRectGroup = (rects) => {
3083
- if (rects.length === 1) {
3084
- const rect = rects[0];
3085
- if (!rect) throw new Error("Invalid rect in group");
3086
- return rect;
3087
- }
3088
- const firstRect = rects[0];
3089
- if (!firstRect) throw new Error("Invalid first rect in group");
3090
- let minLeft = firstRect.left;
3091
- let minTop = firstRect.top;
3092
- let maxRight = firstRect.left + firstRect.width;
3093
- let maxBottom = firstRect.top + firstRect.height;
3094
- rects.forEach((rect) => {
3095
- if (!rect) return;
3096
- minLeft = Math.min(minLeft, rect.left);
3097
- minTop = Math.min(minTop, rect.top);
3098
- maxRight = Math.max(maxRight, rect.left + rect.width);
3099
- maxBottom = Math.max(maxBottom, rect.top + rect.height);
3100
- });
3101
- return {
3102
- left: minLeft,
3103
- top: minTop,
3104
- width: maxRight - minLeft,
3105
- height: maxBottom - minTop,
3106
- pageNumber: firstRect.pageNumber
3107
- };
3108
- };
3109
3152
  var useSelectionDimensions = () => {
3110
3153
  const store = PDFStore.useContext();
3111
3154
  const getAnnotationDimension = () => {
@@ -3178,14 +3221,7 @@ var useSelectionDimensions = () => {
3178
3221
  }
3179
3222
  });
3180
3223
  textLayerMapHighlight.forEach((rects) => {
3181
- if (rects.length > 0) {
3182
- if (rects.length === 1) {
3183
- highlightRects.push(...rects);
3184
- } else {
3185
- const consolidated = consolidateHighlightRects(rects);
3186
- highlightRects.push(...consolidated);
3187
- }
3188
- }
3224
+ highlightRects.push(...mergeTextRuns(rects));
3189
3225
  });
3190
3226
  textLayerMapUnderline.forEach((rects) => {
3191
3227
  if (rects.length > 0) {
@@ -3332,7 +3368,7 @@ var useSelectionDimensions = () => {
3332
3368
  });
3333
3369
  textLayerMap.forEach((rects) => {
3334
3370
  if (rects.length > 0) {
3335
- const consolidated = consolidateRects(rects);
3371
+ const consolidated = mergeTextRuns(rects);
3336
3372
  highlights.push(...consolidated);
3337
3373
  }
3338
3374
  });
@@ -3777,13 +3813,110 @@ var HighlightLayer = forwardRef(({ asChild, className, style, ...props }, ref) =
3777
3813
  }) });
3778
3814
  });
3779
3815
  HighlightLayer.displayName = "HighlightLayer";
3816
+
3817
+ // src/lib/selection-background.ts
3818
+ var SVG_NS = "http://www.w3.org/2000/svg";
3819
+ var createSelectionBackground = () => {
3820
+ const overlays = /* @__PURE__ */ new Map();
3821
+ let style;
3822
+ const clear = (layer) => {
3823
+ overlays.get(layer)?.remove();
3824
+ overlays.delete(layer);
3825
+ layer.removeAttribute("data-lector-selection-active");
3826
+ if (overlays.size === 0) {
3827
+ style?.remove();
3828
+ style = void 0;
3829
+ }
3830
+ };
3831
+ const update = (selection, layers) => {
3832
+ const registered = new Set(layers);
3833
+ const byLayer = /* @__PURE__ */ new Map();
3834
+ if (selection && !selection.isCollapsed) {
3835
+ for (let i = 0; i < selection.rangeCount; i++) {
3836
+ for (const { rect, element } of getTextNodeClientRects(
3837
+ selection.getRangeAt(i)
3838
+ )) {
3839
+ const layer = element?.closest(".textLayer");
3840
+ if (!layer || !element || !registered.has(layer) || rect.width <= 0 || rect.height <= 0)
3841
+ continue;
3842
+ let entry = byLayer.get(layer);
3843
+ if (!entry) {
3844
+ entry = { rects: [], element };
3845
+ byLayer.set(layer, entry);
3846
+ }
3847
+ entry.rects.push(rect);
3848
+ }
3849
+ }
3850
+ }
3851
+ for (const layer of registered) {
3852
+ const entry = byLayer.get(layer);
3853
+ if (!entry) {
3854
+ clear(layer);
3855
+ continue;
3856
+ }
3857
+ layer.removeAttribute("data-lector-selection-active");
3858
+ const color = getComputedStyle(
3859
+ entry.element,
3860
+ "::selection"
3861
+ ).backgroundColor;
3862
+ if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") {
3863
+ clear(layer);
3864
+ continue;
3865
+ }
3866
+ const bounds = layer.getBoundingClientRect();
3867
+ if (!bounds.width || !bounds.height || !layer.clientWidth || !layer.clientHeight) {
3868
+ clear(layer);
3869
+ continue;
3870
+ }
3871
+ const scaleX = bounds.width / layer.clientWidth;
3872
+ const scaleY = bounds.height / layer.clientHeight;
3873
+ const rects = mergeTextRuns(
3874
+ entry.rects.map((rect) => ({
3875
+ left: (rect.left - bounds.left) / scaleX,
3876
+ top: (rect.top - bounds.top) / scaleY,
3877
+ width: rect.width / scaleX,
3878
+ height: rect.height / scaleY
3879
+ }))
3880
+ );
3881
+ let overlay = overlays.get(layer);
3882
+ if (!overlay) {
3883
+ overlay = document.createElementNS(SVG_NS, "svg");
3884
+ overlay.dataset.lectorSelection = "";
3885
+ overlay.setAttribute("aria-hidden", "true");
3886
+ overlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;user-select:none;transform:none;z-index:0;overflow:visible";
3887
+ overlay.append(document.createElementNS(SVG_NS, "path"));
3888
+ layer.append(overlay);
3889
+ overlays.set(layer, overlay);
3890
+ }
3891
+ const path = overlay.firstElementChild;
3892
+ path.setAttribute(
3893
+ "d",
3894
+ rects.map(
3895
+ (rect) => `M${rect.left} ${rect.top}h${rect.width}v${rect.height}h${-rect.width}Z`
3896
+ ).join(" ")
3897
+ );
3898
+ path.setAttribute("fill", color);
3899
+ if (!style) {
3900
+ style = document.createElement("style");
3901
+ style.textContent = ".textLayer[data-lector-selection-active] ::selection { background-color: transparent !important; }";
3902
+ document.head.append(style);
3903
+ }
3904
+ layer.setAttribute("data-lector-selection-active", "");
3905
+ }
3906
+ };
3907
+ return { update, clear };
3908
+ };
3909
+
3910
+ // src/hooks/layers/useTextLayer.tsx
3780
3911
  var createTextSelectionManager = () => {
3781
3912
  const textLayers = /* @__PURE__ */ new Map();
3913
+ const selectionBackground = createSelectionBackground();
3782
3914
  let selectionChangeAbortController = null;
3783
3915
  let isPointerDown = false;
3784
3916
  let prevRange = null;
3785
3917
  let isFirefox;
3786
3918
  const removeGlobalSelectionListener = (textLayerDiv) => {
3919
+ selectionBackground.clear(textLayerDiv);
3787
3920
  textLayers.delete(textLayerDiv);
3788
3921
  if (textLayers.size === 0) {
3789
3922
  selectionChangeAbortController?.abort();
@@ -3840,6 +3973,7 @@ var createTextSelectionManager = () => {
3840
3973
  "selectionchange",
3841
3974
  () => {
3842
3975
  const selection = document.getSelection();
3976
+ selectionBackground.update(selection, textLayers.keys());
3843
3977
  if (!selection || selection.rangeCount === 0) {
3844
3978
  textLayers.forEach(reset);
3845
3979
  return;
@@ -4305,6 +4439,37 @@ var Outline = ({
4305
4439
  });
4306
4440
  }) });
4307
4441
  };
4442
+ function PageLoadingBoundary({
4443
+ pageNumber,
4444
+ children,
4445
+ fallback = null
4446
+ }) {
4447
+ const loaded = usePdf((state) => state.isPageLoaded(pageNumber));
4448
+ const loadPage = usePdf((state) => state.loadPdfPageProxy);
4449
+ const [failure, setFailure] = useState(null);
4450
+ const [attempt, setAttempt] = useState(0);
4451
+ useEffect(() => {
4452
+ if (loaded) return;
4453
+ let disposed = false;
4454
+ setFailure(null);
4455
+ void loadPage(pageNumber).catch((error) => {
4456
+ if (!disposed) setFailure({ pageNumber, error });
4457
+ });
4458
+ return () => {
4459
+ disposed = true;
4460
+ };
4461
+ }, [pageNumber, loaded, loadPage, attempt]);
4462
+ if (loaded) return children;
4463
+ if (failure?.pageNumber === pageNumber)
4464
+ return /* @__PURE__ */ jsxs("div", { role: "alert", children: [
4465
+ "Unable to load page ",
4466
+ pageNumber,
4467
+ ".",
4468
+ " ",
4469
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setAttempt((value) => value + 1), children: "Retry" })
4470
+ ] });
4471
+ return fallback;
4472
+ }
4308
4473
  var Page = memo(
4309
4474
  ({
4310
4475
  children,
@@ -4312,14 +4477,16 @@ var Page = memo(
4312
4477
  style,
4313
4478
  ...props
4314
4479
  }) => {
4315
- const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
4480
+ const loaded = usePdf((state) => state.isPageLoaded(pageNumber));
4481
+ const getPage = usePdf((state) => state.getPdfPageProxy);
4482
+ const proxy = loaded ? getPage(pageNumber) : void 0;
4316
4483
  const viewport = usePdf((state) => state.viewports[pageNumber - 1]);
4317
4484
  const colorScheme = usePdf((state) => state.colorScheme);
4318
4485
  const darkModeColors = usePdf((state) => state.darkModeColors);
4319
4486
  const pageBackground = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors)("#ffffff") : "white";
4320
- const width = viewport?.width ?? (pdfPageProxy.view[2] ?? 0) - (pdfPageProxy.view[0] ?? 0);
4321
- const height = viewport?.height ?? (pdfPageProxy.view[3] ?? 0) - (pdfPageProxy.view[1] ?? 0);
4322
- return /* @__PURE__ */ jsx(PDFPageNumberContext.Provider, { value: pdfPageProxy.pageNumber, children: /* @__PURE__ */ jsx(
4487
+ const width = viewport?.width ?? (proxy?.view[2] ?? 0) - (proxy?.view[0] ?? 0);
4488
+ const height = viewport?.height ?? (proxy?.view[3] ?? 0) - (proxy?.view[1] ?? 0);
4489
+ return /* @__PURE__ */ jsx(PDFPageNumberContext.Provider, { value: pageNumber, children: /* @__PURE__ */ jsx(
4323
4490
  Primitive.div,
4324
4491
  {
4325
4492
  style: {
@@ -4337,8 +4504,9 @@ var Page = memo(
4337
4504
  height,
4338
4505
  ...style
4339
4506
  },
4507
+ "aria-busy": !loaded || void 0,
4340
4508
  ...props,
4341
- children
4509
+ children: /* @__PURE__ */ jsx(PageLoadingBoundary, { pageNumber, children })
4342
4510
  }
4343
4511
  )
4344
4512
  }
@@ -4459,6 +4627,28 @@ var addEventListenerOptions = {
4459
4627
  };
4460
4628
  var useObserveElement = () => {
4461
4629
  const store = PDFStore.useContext();
4630
+ const observeElementRect$1 = (instance, cb) => {
4631
+ let physicalRect;
4632
+ const publish = () => {
4633
+ if (!physicalRect) return;
4634
+ const zoom = store.getState().zoom;
4635
+ cb({
4636
+ width: physicalRect.width / zoom,
4637
+ height: physicalRect.height / zoom
4638
+ });
4639
+ };
4640
+ const stopObserving = observeElementRect(instance, (rect) => {
4641
+ physicalRect = rect;
4642
+ publish();
4643
+ });
4644
+ const unsubscribe = store.subscribe((state, previous) => {
4645
+ if (state.zoom !== previous.zoom) publish();
4646
+ });
4647
+ return () => {
4648
+ stopObserving?.();
4649
+ unsubscribe();
4650
+ };
4651
+ };
4462
4652
  const observeElementOffset = (instance, cb) => {
4463
4653
  const element = instance.scrollElement;
4464
4654
  if (!element) {
@@ -4468,33 +4658,46 @@ var useObserveElement = () => {
4468
4658
  if (!targetWindow) {
4469
4659
  return;
4470
4660
  }
4471
- let offset3 = 0;
4472
- const fallback = instance.options.useScrollendEvent && supportsScrollend ? () => void 0 : debounce(
4473
- targetWindow,
4474
- () => {
4475
- cb(offset3, false);
4476
- },
4477
- instance.options.isScrollingResetDelay
4478
- );
4479
- const createHandler = (isScrolling) => () => {
4661
+ let isScrolling = false;
4662
+ let idleTimer;
4663
+ const needsIdleFallback = !instance.options.useScrollendEvent || !supportsScrollend;
4664
+ const publish = (scrolling) => {
4665
+ isScrolling = scrolling;
4480
4666
  const { horizontal, isRtl } = instance.options;
4481
- offset3 = horizontal ? element.scrollLeft * (isRtl && -1 || 1) : element.scrollTop;
4482
- const zoom = store.getState().zoom;
4483
- offset3 = offset3 / zoom;
4484
- fallback();
4485
- cb(offset3, isScrolling);
4667
+ const physicalOffset = horizontal ? element.scrollLeft * (isRtl ? -1 : 1) : element.scrollTop;
4668
+ cb(physicalOffset / store.getState().zoom, isScrolling);
4669
+ };
4670
+ const clearIdleTimer = () => {
4671
+ if (idleTimer !== void 0) targetWindow.clearTimeout(idleTimer);
4672
+ idleTimer = void 0;
4673
+ };
4674
+ const createHandler = (scrolling) => () => {
4675
+ clearIdleTimer();
4676
+ publish(scrolling);
4677
+ if (scrolling && needsIdleFallback) {
4678
+ idleTimer = targetWindow.setTimeout(() => {
4679
+ idleTimer = void 0;
4680
+ publish(false);
4681
+ }, instance.options.isScrollingResetDelay);
4682
+ }
4486
4683
  };
4487
4684
  const handler = createHandler(true);
4488
4685
  const endHandler = createHandler(false);
4489
4686
  endHandler();
4687
+ const unsubscribe = store.subscribe((state, previous) => {
4688
+ if (state.zoom !== previous.zoom) publish(isScrolling);
4689
+ });
4490
4690
  element.addEventListener("scroll", handler, addEventListenerOptions);
4491
4691
  element.addEventListener("scrollend", endHandler, addEventListenerOptions);
4492
4692
  return () => {
4693
+ unsubscribe();
4694
+ clearIdleTimer();
4493
4695
  element.removeEventListener("scroll", handler);
4494
4696
  element.removeEventListener("scrollend", endHandler);
4495
4697
  };
4496
4698
  };
4497
4699
  return {
4700
+ observeElementRect: observeElementRect$1,
4498
4701
  observeElementOffset
4499
4702
  };
4500
4703
  };
@@ -4504,7 +4707,11 @@ var useScrollFn = () => {
4504
4707
  (_offset, canSmooth, instance) => {
4505
4708
  const zoom = store.getState().zoom;
4506
4709
  const offset3 = _offset * zoom;
4507
- elementScroll(offset3, canSmooth, instance);
4710
+ elementScroll(
4711
+ offset3,
4712
+ { ...canSmooth, adjustments: (canSmooth.adjustments ?? 0) * zoom },
4713
+ instance
4714
+ );
4508
4715
  },
4509
4716
  [store]
4510
4717
  );
@@ -4622,7 +4829,7 @@ function _toPrimitive(input, hint) {
4622
4829
  if (typeof input !== "object" || input === null) return input;
4623
4830
  var prim = input[Symbol.toPrimitive];
4624
4831
  if (prim !== void 0) {
4625
- var res = prim.call(input, hint || "default");
4832
+ var res = prim.call(input, hint);
4626
4833
  if (typeof res !== "object") return res;
4627
4834
  throw new TypeError("@@toPrimitive must return a primitive value.");
4628
4835
  }
@@ -6479,6 +6686,8 @@ var useViewportContainer = ({
6479
6686
  });
6480
6687
  const zoomRafRef = useRef(null);
6481
6688
  const lastPushedZoomRef = useRef(null);
6689
+ const initializedZoomRef = useRef(false);
6690
+ const appliedZoomRef = useRef(1);
6482
6691
  const updateTransform = useCallback(
6483
6692
  (zoomUpdate) => {
6484
6693
  if (!elementRef.current || !containerRef.current || !elementWrapperRef.current) {
@@ -6508,17 +6717,36 @@ var useViewportContainer = ({
6508
6717
  elementWrapperRef.current.style.height = `${naturalHeight * zoom2}px`;
6509
6718
  containerRef.current.scrollTop = translateY;
6510
6719
  containerRef.current.scrollLeft = translateX;
6511
- if (zoomUpdate && zoomRafRef.current === null) {
6512
- zoomRafRef.current = requestAnimationFrame(() => {
6513
- zoomRafRef.current = null;
6514
- const z = transformations.current.zoom;
6515
- lastPushedZoomRef.current = z;
6516
- updateZoom(() => z);
6517
- });
6720
+ appliedZoomRef.current = zoom2;
6721
+ if (zoomUpdate) {
6722
+ lastPushedZoomRef.current = zoom2;
6723
+ updateZoom(() => zoom2);
6518
6724
  }
6519
6725
  },
6520
6726
  [containerRef, elementRef, elementWrapperRef, updateZoom, store]
6521
6727
  );
6728
+ useLayoutEffect(() => {
6729
+ const viewport = containerRef.current;
6730
+ if (!viewport) return;
6731
+ return registerViewportZoom(viewport, (nextZoom) => {
6732
+ if (initializedZoomRef.current && appliedZoomRef.current === nextZoom && zoomRafRef.current === null)
6733
+ return;
6734
+ if (zoomRafRef.current !== null) {
6735
+ cancelAnimationFrame(zoomRafRef.current);
6736
+ zoomRafRef.current = null;
6737
+ }
6738
+ lastPushedZoomRef.current = null;
6739
+ const previousZoom = initializedZoomRef.current ? appliedZoomRef.current : store.getState().zoom;
6740
+ const ratio = previousZoom > 0 && Number.isFinite(previousZoom) ? nextZoom / previousZoom : 1;
6741
+ transformations.current = {
6742
+ zoom: nextZoom,
6743
+ translateX: viewport.scrollLeft * ratio,
6744
+ translateY: viewport.scrollTop * ratio
6745
+ };
6746
+ initializedZoomRef.current = true;
6747
+ updateTransform();
6748
+ });
6749
+ }, [containerRef, store, updateTransform]);
6522
6750
  useEffect(() => {
6523
6751
  if (USE_LAYOUT_ZOOM && gestureTransformAppliedRef.current && !isPinching)
6524
6752
  updateTransform();
@@ -6531,6 +6759,16 @@ var useViewportContainer = ({
6531
6759
  };
6532
6760
  }, []);
6533
6761
  useEffect(() => {
6762
+ if (!initializedZoomRef.current && containerRef.current) {
6763
+ initializedZoomRef.current = true;
6764
+ transformations.current = {
6765
+ zoom,
6766
+ translateX: containerRef.current.scrollLeft,
6767
+ translateY: containerRef.current.scrollTop
6768
+ };
6769
+ updateTransform();
6770
+ return;
6771
+ }
6534
6772
  if (transformations.current.zoom === zoom || !containerRef.current) {
6535
6773
  return;
6536
6774
  }
@@ -6543,7 +6781,7 @@ var useViewportContainer = ({
6543
6781
  zoomRafRef.current = null;
6544
6782
  }
6545
6783
  lastPushedZoomRef.current = null;
6546
- const prevZoom = transformations.current.zoom;
6784
+ const prevZoom = appliedZoomRef.current;
6547
6785
  if (!prevZoom || !Number.isFinite(prevZoom)) {
6548
6786
  transformations.current = {
6549
6787
  translateX: containerRef.current.scrollLeft,
@@ -6683,11 +6921,26 @@ var useViewportContainer = ({
6683
6921
  translateY: newTranslateY
6684
6922
  };
6685
6923
  newMemo.lastZoom = newZoom;
6686
- updateTransform(true);
6924
+ if (zoomRafRef.current === null) {
6925
+ zoomRafRef.current = requestAnimationFrame(() => {
6926
+ zoomRafRef.current = null;
6927
+ updateTransform(true);
6928
+ });
6929
+ }
6687
6930
  return newMemo;
6688
6931
  },
6689
6932
  onPinchStart: () => setIsPinching(true),
6690
- onPinchEnd: () => setIsPinching(false)
6933
+ onPinchEnd: () => {
6934
+ const pendingFrame = zoomRafRef.current !== null;
6935
+ if (zoomRafRef.current !== null) {
6936
+ cancelAnimationFrame(zoomRafRef.current);
6937
+ zoomRafRef.current = null;
6938
+ }
6939
+ setIsPinching(false);
6940
+ if (pendingFrame || gestureTransformAppliedRef.current) {
6941
+ updateTransform(pendingFrame);
6942
+ }
6943
+ }
6691
6944
  },
6692
6945
  {
6693
6946
  target: containerRef
@@ -6740,6 +6993,7 @@ var Pages = ({
6740
6993
  const [tempItems, setTempItems] = useState([]);
6741
6994
  const viewports = usePdf((state) => state.viewports);
6742
6995
  const numPages = usePdf((state) => state.pdfDocumentProxy.numPages);
6996
+ const initialPage = usePdf((state) => state.initialPage);
6743
6997
  const isPinching = usePdf((state) => state.isPinching);
6744
6998
  const elementWrapperRef = useRef(null);
6745
6999
  const elementRef = useRef(null);
@@ -6751,7 +7005,7 @@ var Pages = ({
6751
7005
  });
6752
7006
  const setVirtualizer = usePdf((state) => state.setVirtualizer);
6753
7007
  const { scrollToFn } = useScrollFn();
6754
- const { observeElementOffset } = useObserveElement();
7008
+ const { observeElementOffset, observeElementRect } = useObserveElement();
6755
7009
  const viewportsRef = useRef(viewports);
6756
7010
  viewportsRef.current = viewports;
6757
7011
  const estimateSize = useCallback(
@@ -6763,15 +7017,22 @@ var Pages = ({
6763
7017
  []
6764
7018
  // Stable — reads from ref
6765
7019
  );
7020
+ const [startingOffset] = useState(
7021
+ () => initialOffset ?? viewports.slice(0, initialPage - 1).reduce(
7022
+ (offset3, viewport) => offset3 + viewport.height + EXTRA_HEIGHT + gap,
7023
+ 0
7024
+ )
7025
+ );
6766
7026
  const virtualizer = useVirtualizer({
6767
7027
  count: numPages || 0,
6768
7028
  getScrollElement: () => containerRef.current,
6769
7029
  estimateSize,
6770
7030
  observeElementOffset,
7031
+ observeElementRect,
6771
7032
  overscan: virtualizerOptions?.overscan ?? 0,
6772
7033
  scrollToFn,
6773
7034
  gap,
6774
- initialOffset,
7035
+ initialOffset: startingOffset,
6775
7036
  // Never trust scrollend alone to reset isScrolling: a single missed
6776
7037
  // event (a documented browser flake — TanStack flipped this default to
6777
7038
  // false in later 3.x) would pin isScrolling=true forever, and every
@@ -6779,6 +7040,21 @@ var Pages = ({
6779
7040
  // false keeps the isScrollingResetDelay debounce active as a fallback.
6780
7041
  useScrollendEvent: false
6781
7042
  });
7043
+ const previousViewports = useRef(viewports);
7044
+ useLayoutEffect(() => {
7045
+ if (isPinching) return;
7046
+ const previous = previousViewports.current;
7047
+ previousViewports.current = viewports;
7048
+ if (previous === viewports) return;
7049
+ let resized = false;
7050
+ for (let index = 0; index < viewports.length; index++) {
7051
+ if (previous[index]?.height !== viewports[index]?.height) {
7052
+ virtualizer.resizeItem(index, viewports[index].height + EXTRA_HEIGHT);
7053
+ resized = true;
7054
+ }
7055
+ }
7056
+ if (resized) setTempItems([]);
7057
+ }, [viewports, virtualizer, isPinching]);
6782
7058
  useEffect(() => {
6783
7059
  if (onOffsetChange && virtualizer.scrollOffset)
6784
7060
  onOffsetChange(virtualizer.scrollOffset);
@@ -6925,6 +7201,164 @@ async function mapConcurrent(items, concurrency, map, isCancelled = () => false)
6925
7201
  return results;
6926
7202
  }
6927
7203
 
7204
+ // src/lib/page-resources.ts
7205
+ var PageResources = class {
7206
+ constructor(document2, firstPage, rotation, onError, concurrency = 16) {
7207
+ this.document = document2;
7208
+ this.rotation = rotation;
7209
+ this.onError = onError;
7210
+ this.concurrency = concurrency;
7211
+ this.pages.set(firstPage.pageNumber, firstPage);
7212
+ const viewport = this.viewportFor(firstPage);
7213
+ this.viewports = Array.from({ length: document2.numPages }, () => viewport);
7214
+ if (document2.numPages === 1) this.completePages = [firstPage];
7215
+ }
7216
+ pages = /* @__PURE__ */ new Map();
7217
+ requests = /* @__PURE__ */ new Map();
7218
+ queue = [];
7219
+ active = 0;
7220
+ disposed = false;
7221
+ listeners = /* @__PURE__ */ new Set();
7222
+ notification;
7223
+ viewports;
7224
+ completePages;
7225
+ viewportUpdates = /* @__PURE__ */ new Map();
7226
+ viewportFor(page) {
7227
+ return page.getViewport({
7228
+ scale: 1,
7229
+ rotation: this.rotation + (page.rotate || 0)
7230
+ });
7231
+ }
7232
+ get(pageNumber) {
7233
+ return this.pages.get(pageNumber);
7234
+ }
7235
+ getSnapshot = () => ({
7236
+ viewports: this.viewports,
7237
+ pageProxies: this.completePages
7238
+ });
7239
+ subscribe = (listener) => {
7240
+ this.listeners.add(listener);
7241
+ return () => {
7242
+ this.listeners.delete(listener);
7243
+ };
7244
+ };
7245
+ /** Visible pages move ahead of the background queue; in-flight requests are shared. */
7246
+ load = (pageNumber) => {
7247
+ if (this.disposed)
7248
+ return Promise.reject(new Error("Document page loading was cancelled"));
7249
+ if (!Number.isInteger(pageNumber) || pageNumber < 1 || pageNumber > this.document.numPages)
7250
+ return Promise.reject(
7251
+ new RangeError(`Page ${pageNumber} does not exist`)
7252
+ );
7253
+ const page = this.pages.get(pageNumber);
7254
+ if (page) return Promise.resolve(page);
7255
+ const existing = this.requests.get(pageNumber);
7256
+ if (existing) {
7257
+ const queued = this.queue.indexOf(pageNumber);
7258
+ if (queued > 0) {
7259
+ this.queue.splice(queued, 1);
7260
+ this.queue.unshift(pageNumber);
7261
+ }
7262
+ return existing.promise;
7263
+ }
7264
+ const request = this.createRequest(pageNumber);
7265
+ this.queue.unshift(pageNumber);
7266
+ this.pump();
7267
+ return request.promise;
7268
+ };
7269
+ createRequest(pageNumber) {
7270
+ let resolve;
7271
+ let reject;
7272
+ const promise = new Promise((res, rej) => {
7273
+ resolve = res;
7274
+ reject = rej;
7275
+ });
7276
+ const request = { promise, resolve, reject };
7277
+ this.requests.set(pageNumber, request);
7278
+ return request;
7279
+ }
7280
+ /** Start after the reader mounts, so unrelated page metadata cannot delay its first render. */
7281
+ start = () => {
7282
+ if (this.disposed) return;
7283
+ for (let pageNumber = 1; pageNumber <= this.document.numPages; pageNumber++) {
7284
+ if (this.pages.has(pageNumber) || this.requests.has(pageNumber)) continue;
7285
+ const request = this.createRequest(pageNumber);
7286
+ void request.promise.catch(() => {
7287
+ });
7288
+ this.queue.push(pageNumber);
7289
+ }
7290
+ this.pump();
7291
+ };
7292
+ loadAll = async () => {
7293
+ if (this.disposed) throw new Error("Document page loading was cancelled");
7294
+ if (this.completePages) return this.completePages;
7295
+ this.start();
7296
+ await Promise.all(
7297
+ Array.from({ length: this.document.numPages }, (_, index) => {
7298
+ const pageNumber = index + 1;
7299
+ return this.pages.get(pageNumber) ?? this.requests.get(pageNumber).promise;
7300
+ })
7301
+ );
7302
+ return this.completePages;
7303
+ };
7304
+ pump() {
7305
+ while (!this.disposed && this.active < this.concurrency && this.queue.length) {
7306
+ const pageNumber = this.queue.shift();
7307
+ const request = this.requests.get(pageNumber);
7308
+ this.active++;
7309
+ void Promise.resolve().then(() => {
7310
+ if (this.disposed)
7311
+ throw new Error("Document page loading was cancelled");
7312
+ return this.document.getPage(pageNumber);
7313
+ }).then((page) => {
7314
+ if (this.disposed) return;
7315
+ const viewport = this.viewportFor(page);
7316
+ this.pages.set(pageNumber, page);
7317
+ this.viewportUpdates.set(pageNumber - 1, viewport);
7318
+ if (this.pages.size === this.document.numPages) {
7319
+ this.completePages = Array.from(
7320
+ { length: this.document.numPages },
7321
+ (_, index) => this.pages.get(index + 1)
7322
+ );
7323
+ }
7324
+ this.requests.delete(pageNumber);
7325
+ request.resolve(page);
7326
+ this.notify();
7327
+ }).catch((error) => {
7328
+ if (!this.disposed) {
7329
+ this.requests.delete(pageNumber);
7330
+ request.reject(error);
7331
+ this.onError(error);
7332
+ }
7333
+ }).finally(() => {
7334
+ this.active--;
7335
+ this.pump();
7336
+ });
7337
+ }
7338
+ }
7339
+ notify() {
7340
+ if (this.notification !== void 0) return;
7341
+ this.notification = setTimeout(() => {
7342
+ this.notification = void 0;
7343
+ if (this.disposed) return;
7344
+ this.viewports = this.viewports.slice();
7345
+ for (const [index, viewport] of this.viewportUpdates)
7346
+ this.viewports[index] = viewport;
7347
+ this.viewportUpdates.clear();
7348
+ for (const listener of this.listeners) listener();
7349
+ }, 0);
7350
+ }
7351
+ dispose = () => {
7352
+ this.disposed = true;
7353
+ clearTimeout(this.notification);
7354
+ this.listeners.clear();
7355
+ this.queue = [];
7356
+ for (const request of this.requests.values())
7357
+ request.reject(new Error("Document page loading was cancelled"));
7358
+ this.requests.clear();
7359
+ };
7360
+ };
7361
+
6928
7362
  // src/lib/pdfjs.ts
6929
7363
  var pdfJsPromise = null;
6930
7364
  var loadPdfJs = () => {
@@ -7050,6 +7484,8 @@ var usePDFDocumentContext = ({
7050
7484
  onError,
7051
7485
  source,
7052
7486
  initialRotation = 0,
7487
+ progressive = false,
7488
+ initialPage = 1,
7053
7489
  isZoomFitWidth,
7054
7490
  zoom = 1,
7055
7491
  zoomOptions,
@@ -7072,7 +7508,32 @@ var usePDFDocumentContext = ({
7072
7508
  darkModeColorsRef.current = darkModeColors;
7073
7509
  useEffect(() => {
7074
7510
  let isDisposed = false;
7511
+ let pageResources;
7075
7512
  const generateViewports = async (pdf) => {
7513
+ const firstPageNumber = Number.isFinite(initialPage) ? Math.max(1, Math.min(pdf.numPages, Math.trunc(initialPage))) : 1;
7514
+ if (progressive) {
7515
+ const firstPage = await pdf.getPage(firstPageNumber);
7516
+ if (isDisposed) return;
7517
+ pageResources = new PageResources(pdf, firstPage, rotation, (error) => {
7518
+ if (isDisposed) return;
7519
+ console.error("Error generating PDF viewports", error);
7520
+ onErrorRef.current?.({ error, phase: "viewport-generation", source });
7521
+ });
7522
+ setInitialState({
7523
+ isZoomFitWidth,
7524
+ viewports: pageResources.getSnapshot().viewports,
7525
+ pageProxies: pageResources.getSnapshot().pageProxies ?? [],
7526
+ pdfDocumentProxy: pdf,
7527
+ zoom,
7528
+ zoomOptions,
7529
+ pageResources,
7530
+ initialPage: firstPageNumber,
7531
+ colorScheme: colorSchemeRef.current,
7532
+ darkModeColors: darkModeColorsRef.current,
7533
+ renderColorMapRef
7534
+ });
7535
+ return;
7536
+ }
7076
7537
  const pageProxies = [];
7077
7538
  const viewports = await mapConcurrent(
7078
7539
  Array.from({ length: pdf.numPages }, (_, index) => index),
@@ -7098,6 +7559,7 @@ var usePDFDocumentContext = ({
7098
7559
  pdfDocumentProxy: pdf,
7099
7560
  zoom,
7100
7561
  zoomOptions,
7562
+ initialPage: firstPageNumber,
7101
7563
  colorScheme: colorSchemeRef.current,
7102
7564
  darkModeColors: darkModeColorsRef.current,
7103
7565
  renderColorMapRef
@@ -7160,6 +7622,7 @@ var usePDFDocumentContext = ({
7160
7622
  });
7161
7623
  return () => {
7162
7624
  isDisposed = true;
7625
+ pageResources?.dispose();
7163
7626
  void loadingTask?.destroy();
7164
7627
  };
7165
7628
  };
@@ -7201,6 +7664,28 @@ var ColorSchemeSync = ({
7201
7664
  ]);
7202
7665
  return null;
7203
7666
  };
7667
+ var PageResourcesSync = ({ resources }) => {
7668
+ const store = PDFStore.useContext();
7669
+ useEffect(() => {
7670
+ if (!resources) return;
7671
+ const update = () => {
7672
+ const snapshot = resources.getSnapshot();
7673
+ store.setState((state) => ({
7674
+ viewports: snapshot.viewports,
7675
+ pageProxies: snapshot.pageProxies ?? state.pageProxies,
7676
+ pagesLoaded: Boolean(snapshot.pageProxies)
7677
+ }));
7678
+ };
7679
+ const unsubscribe = resources.subscribe(update);
7680
+ update();
7681
+ const timer = setTimeout(resources.start, 0);
7682
+ return () => {
7683
+ clearTimeout(timer);
7684
+ unsubscribe();
7685
+ };
7686
+ }, [resources, store]);
7687
+ return null;
7688
+ };
7204
7689
  var Root = forwardRef(
7205
7690
  ({
7206
7691
  children,
@@ -7212,6 +7697,8 @@ var Root = forwardRef(
7212
7697
  zoom,
7213
7698
  zoomOptions,
7214
7699
  documentOptions,
7700
+ progressive,
7701
+ initialPage,
7215
7702
  colorScheme,
7216
7703
  darkModeColors,
7217
7704
  ...props
@@ -7224,6 +7711,8 @@ var Root = forwardRef(
7224
7711
  zoom,
7225
7712
  zoomOptions,
7226
7713
  documentOptions,
7714
+ progressive,
7715
+ initialPage,
7227
7716
  colorScheme,
7228
7717
  darkModeColors
7229
7718
  });
@@ -7239,6 +7728,7 @@ var Root = forwardRef(
7239
7728
  };
7240
7729
  }, [documentId]);
7241
7730
  return /* @__PURE__ */ jsx(Primitive.div, { ref, ...props, children: initialState ? /* @__PURE__ */ jsxs(PDFStore.Provider, { initialValue: initialState, children: [
7731
+ /* @__PURE__ */ jsx(PageResourcesSync, { resources: initialState.pageResources }),
7242
7732
  /* @__PURE__ */ jsx(
7243
7733
  ColorSchemeSync,
7244
7734
  {
@@ -7333,7 +7823,10 @@ var Search = ({
7333
7823
  }) => {
7334
7824
  const [result, setResult] = useState(null);
7335
7825
  const [attempt, setAttempt] = useState(0);
7826
+ const document2 = usePdf((state) => state.pdfDocumentProxy);
7336
7827
  const proxies = usePdf((state) => state.pageProxies);
7828
+ const pagesLoaded = usePdf((state) => state.pagesLoaded);
7829
+ const loadPages = usePdf((state) => state.loadPdfPageProxies);
7337
7830
  const setTextContent = usePdf((state) => state.setTextContent);
7338
7831
  const retry = useCallback(() => {
7339
7832
  setResult(null);
@@ -7342,22 +7835,27 @@ var Search = ({
7342
7835
  useEffect(() => {
7343
7836
  let disposed = false;
7344
7837
  setResult(null);
7345
- const task = acquireDocumentText(proxies);
7346
- void task.promise.then((text) => {
7347
- if (disposed) return;
7838
+ let task;
7839
+ void loadPages().then((pages) => {
7840
+ if (disposed || !pagesLoaded) return;
7841
+ task = acquireDocumentText(pages);
7842
+ return task.promise;
7843
+ }).then((text) => {
7844
+ if (disposed || !text) return;
7348
7845
  setTextContent(text);
7349
- setResult({ pages: proxies, status: "ready" });
7846
+ setResult({ document: document2, pages: proxies, status: "ready" });
7350
7847
  }).catch((error) => {
7351
7848
  if (disposed) return;
7352
7849
  console.error("Error extracting PDF text", error);
7353
- setResult({ pages: proxies, status: "error", error });
7850
+ setResult({ document: document2, pages: proxies, status: "error", error });
7354
7851
  });
7355
7852
  return () => {
7356
7853
  disposed = true;
7357
- task.release();
7854
+ task?.release();
7358
7855
  };
7359
- }, [proxies, setTextContent, attempt]);
7360
- if (!result || result.pages !== proxies) return loading;
7856
+ }, [document2, proxies, pagesLoaded, loadPages, setTextContent, attempt]);
7857
+ if (!result || result.document !== document2 || result.pages !== proxies)
7858
+ return loading;
7361
7859
  if (result.status === "error") {
7362
7860
  if (errorFallback) return errorFallback({ error: result.error, retry });
7363
7861
  return /* @__PURE__ */ jsxs("div", { role: "alert", children: [
@@ -7457,7 +7955,7 @@ var useThumbnail = (pageNumber, config = {}) => {
7457
7955
  isVisible
7458
7956
  };
7459
7957
  };
7460
- var Thumbnail = ({
7958
+ var LoadedThumbnail = ({
7461
7959
  pageNumber = 1,
7462
7960
  eager = false,
7463
7961
  ...props
@@ -7466,30 +7964,65 @@ var Thumbnail = ({
7466
7964
  isFirstPage: eager || pageNumber < 5
7467
7965
  });
7468
7966
  const { jumpToPage } = usePdfJump();
7967
+ const isCurrentPage = usePdf((state) => state.currentPage === pageNumber);
7968
+ const spacePressed = useRef(false);
7469
7969
  return /* @__PURE__ */ jsx("div", { ref: containerRef, style: { minHeight: "150px", minWidth: "10px" }, children: isVisible && /* @__PURE__ */ jsx(
7470
7970
  Primitive.canvas,
7471
7971
  {
7972
+ "aria-label": `Page ${pageNumber}`,
7973
+ "aria-current": isCurrentPage ? "page" : void 0,
7472
7974
  ...props,
7473
7975
  role: "button",
7474
7976
  tabIndex: 0,
7475
- onClick: (e) => {
7476
- if (props.onClick) {
7477
- props.onClick(e);
7478
- }
7479
- jumpToPage(pageNumber, { behavior: "auto" });
7977
+ onClick: (event) => {
7978
+ props.onClick?.(event);
7979
+ if (!event.defaultPrevented)
7980
+ jumpToPage(pageNumber, { behavior: "auto" });
7480
7981
  },
7481
- onKeyDown: (e) => {
7482
- if (props.onKeyDown) {
7483
- props.onKeyDown(e);
7982
+ onKeyDown: (event) => {
7983
+ props.onKeyDown?.(event);
7984
+ if (event.defaultPrevented) return;
7985
+ if (event.key === "Enter") {
7986
+ event.preventDefault();
7987
+ jumpToPage(pageNumber, { behavior: "auto" });
7988
+ } else if (event.key === " ") {
7989
+ event.preventDefault();
7990
+ spacePressed.current = true;
7484
7991
  }
7485
- if (e.key === "Enter") {
7992
+ },
7993
+ onKeyUp: (event) => {
7994
+ props.onKeyUp?.(event);
7995
+ if (event.key !== " ") return;
7996
+ const activate = spacePressed.current && !event.defaultPrevented;
7997
+ spacePressed.current = false;
7998
+ if (activate) {
7999
+ event.preventDefault();
7486
8000
  jumpToPage(pageNumber, { behavior: "auto" });
7487
8001
  }
7488
8002
  },
8003
+ onBlur: (event) => {
8004
+ spacePressed.current = false;
8005
+ props.onBlur?.(event);
8006
+ },
7489
8007
  ref: canvasRef
7490
8008
  }
7491
8009
  ) });
7492
8010
  };
8011
+ var Thumbnail = (props) => /* @__PURE__ */ jsx(
8012
+ PageLoadingBoundary,
8013
+ {
8014
+ pageNumber: props.pageNumber ?? 1,
8015
+ fallback: /* @__PURE__ */ jsx(
8016
+ "div",
8017
+ {
8018
+ "aria-label": `Loading page ${props.pageNumber ?? 1}`,
8019
+ "aria-busy": "true",
8020
+ style: { minHeight: 150 }
8021
+ }
8022
+ ),
8023
+ children: /* @__PURE__ */ jsx(LoadedThumbnail, { ...props })
8024
+ }
8025
+ );
7493
8026
  function VirtualizedThumbnails({
7494
8027
  children,
7495
8028
  count,
@@ -7502,6 +8035,9 @@ function VirtualizedThumbnails({
7502
8035
  const containerRef = useRef(null);
7503
8036
  useImperativeHandle(ref, () => containerRef.current);
7504
8037
  const [focusPage, setFocusPage] = useState(null);
8038
+ const focusPageLoaded = usePdf(
8039
+ (state) => focusPage === null ? false : state.isPageLoaded(focusPage)
8040
+ );
7505
8041
  const itemHeight = Number.isFinite(virtualize.itemHeight) ? Math.max(1, virtualize.itemHeight) : 150;
7506
8042
  const virtualizer = useVirtualizer({
7507
8043
  count,
@@ -7519,7 +8055,7 @@ function VirtualizedThumbnails({
7519
8055
  canvas.focus({ preventScroll: true });
7520
8056
  setFocusPage(null);
7521
8057
  }
7522
- }, [focusPage, items]);
8058
+ }, [focusPage, focusPageLoaded, items]);
7523
8059
  return /* @__PURE__ */ jsx(
7524
8060
  Primitive.div,
7525
8061
  {
@@ -7652,12 +8188,42 @@ var CurrentZoom = ({ ...props }) => {
7652
8188
  // src/lib/search.ts
7653
8189
  var normalizedPages = /* @__PURE__ */ new WeakMap();
7654
8190
  function normalize(page) {
7655
- let cached = normalizedPages.get(page);
7656
- if (!cached || cached.text !== page.text) {
7657
- cached = { text: page.text, lower: page.text.toLowerCase() };
7658
- normalizedPages.set(page, cached);
7659
- }
7660
- return cached.lower;
8191
+ const cached = normalizedPages.get(page);
8192
+ if (cached?.text === page.text) return cached;
8193
+ const lower = page.text.toLowerCase();
8194
+ const result = { text: page.text, lower };
8195
+ if (lower.length !== page.text.length) {
8196
+ result.starts = new Uint32Array(lower.length);
8197
+ result.ends = new Uint32Array(lower.length);
8198
+ let original = 0;
8199
+ let normalized = 0;
8200
+ for (const character of page.text) {
8201
+ const length = character.toLowerCase().length;
8202
+ for (let i = 0; i < length; i++) {
8203
+ result.starts[normalized + i] = original + (length === character.length ? i : 0);
8204
+ result.ends[normalized + i] = original + (length === character.length ? i + 1 : character.length);
8205
+ }
8206
+ original += character.length;
8207
+ normalized += length;
8208
+ }
8209
+ }
8210
+ normalizedPages.set(page, result);
8211
+ return result;
8212
+ }
8213
+ function createMatch(page, normalized, start, length, searchText, textSize, score) {
8214
+ const end = Math.min(normalized.lower.length, start + length);
8215
+ const matchIndex = normalized.starts?.[start] ?? start;
8216
+ const matchEnd = normalized.ends?.[end - 1] ?? end;
8217
+ const matchLength = matchEnd - matchIndex;
8218
+ return {
8219
+ pageNumber: page.pageNumber,
8220
+ text: page.text.slice(matchIndex, matchEnd + textSize),
8221
+ score,
8222
+ matchIndex,
8223
+ isExactMatch: score === 1,
8224
+ searchText,
8225
+ ...matchLength !== searchText.length ? { matchLength } : {}
8226
+ };
7661
8227
  }
7662
8228
  function createBoundedDistance(query, maxDistance) {
7663
8229
  const length = query.length;
@@ -7720,7 +8286,8 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
7720
8286
  let steps = 0;
7721
8287
  let deadline = cooperative ? performance.now() + budget : 0;
7722
8288
  for (const page of pages) {
7723
- const lower = normalize(page);
8289
+ const normalized = normalize(page);
8290
+ const lower = normalized.lower;
7724
8291
  const exactStarts = [];
7725
8292
  let index = 0;
7726
8293
  while (true) {
@@ -7733,16 +8300,19 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
7733
8300
  exactCount++;
7734
8301
  if (maxDistance > 0) exactStarts.push(matchIndex);
7735
8302
  if (exactMatches.length < limit) {
7736
- exactMatches.push({
7737
- pageNumber: page.pageNumber,
7738
- text: page.text.substr(matchIndex, searchText.length + textSize),
7739
- score: 1,
7740
- matchIndex,
7741
- isExactMatch: true,
7742
- searchText
7743
- });
8303
+ exactMatches.push(
8304
+ createMatch(
8305
+ page,
8306
+ normalized,
8307
+ matchIndex,
8308
+ query.length,
8309
+ searchText,
8310
+ textSize,
8311
+ 1
8312
+ )
8313
+ );
7744
8314
  }
7745
- index = matchIndex + searchText.length;
8315
+ index = matchIndex + query.length;
7746
8316
  }
7747
8317
  if (!(maxDistance > 0)) continue;
7748
8318
  index = 0;
@@ -7752,11 +8322,11 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
7752
8322
  yield;
7753
8323
  deadline = performance.now() + budget;
7754
8324
  }
7755
- while (exactIndex < exactStarts.length && exactStarts[exactIndex] + searchText.length <= index)
8325
+ while (exactIndex < exactStarts.length && exactStarts[exactIndex] + query.length <= index)
7756
8326
  exactIndex++;
7757
8327
  const exactStart = exactStarts[exactIndex];
7758
8328
  if (exactStart !== void 0 && index >= exactStart) {
7759
- index = exactStart + searchText.length;
8329
+ index = exactStart + query.length;
7760
8330
  continue;
7761
8331
  }
7762
8332
  const edits = distance(lower, index);
@@ -7766,14 +8336,15 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
7766
8336
  if (fuzzyMatches.length < limit || score > (fuzzyMatches[limit - 1]?.score ?? Infinity)) {
7767
8337
  insertMatch(
7768
8338
  fuzzyMatches,
7769
- {
7770
- pageNumber: page.pageNumber,
7771
- text: page.text.substr(index, query.length + textSize),
7772
- score,
7773
- matchIndex: index,
7774
- isExactMatch: false,
7775
- searchText
7776
- },
8339
+ createMatch(
8340
+ page,
8341
+ normalized,
8342
+ index,
8343
+ query.length,
8344
+ searchText,
8345
+ textSize,
8346
+ score
8347
+ ),
7777
8348
  limit
7778
8349
  );
7779
8350
  }
@@ -7879,32 +8450,37 @@ var useSearch = () => {
7879
8450
 
7880
8451
  // src/hooks/search/useSearchPosition.tsx
7881
8452
  async function calculateHighlightRects(pageProxy, textMatch) {
7882
- const matchLength = textMatch.searchText ? textMatch.searchText.length : textMatch.text.length;
7883
- if (matchLength === 0 || !Number.isFinite(textMatch.matchIndex) || textMatch.matchIndex < 0)
8453
+ const matchLength = textMatch.matchLength ?? (textMatch.searchText ? textMatch.searchText.length : textMatch.text.length);
8454
+ if (!Number.isFinite(matchLength) || matchLength <= 0 || !Number.isFinite(textMatch.matchIndex) || textMatch.matchIndex < 0)
7884
8455
  return [];
7885
8456
  const matchEnd = textMatch.matchIndex + matchLength;
7886
8457
  const reader = pageProxy.streamTextContent().getReader();
7887
8458
  const matchRects = [];
7888
8459
  const viewport = pageProxy.getViewport({ scale: 1 });
8460
+ const styles = {};
7889
8461
  let offset3 = 0;
7890
8462
  let foundEnd = false;
7891
8463
  try {
7892
8464
  read: while (true) {
7893
8465
  const { value, done } = await reader.read();
7894
8466
  if (done) break;
8467
+ Object.assign(styles, value.styles);
7895
8468
  for (const item of value.items) {
7896
8469
  if (!("str" in item) || item.str.length === 0) continue;
7897
8470
  const length = item.str.length;
7898
8471
  const start = Math.max(0, textMatch.matchIndex - offset3);
7899
8472
  const end = Math.min(length, matchEnd - offset3);
7900
- if (end > start) {
7901
- matchRects.push({
7902
- pageNumber: textMatch.pageNumber,
7903
- left: item.transform[4] + start * (item.width / length),
7904
- top: viewport.height - (item.transform[5] + item.height),
7905
- width: (end - start) * (item.width / length),
7906
- height: item.height
7907
- });
8473
+ if (end > start && item.str.slice(start, end).trim()) {
8474
+ matchRects.push(
8475
+ getItemRect(
8476
+ item,
8477
+ styles[item.fontName],
8478
+ viewport,
8479
+ start,
8480
+ end,
8481
+ textMatch.pageNumber
8482
+ )
8483
+ );
7908
8484
  }
7909
8485
  offset3 += length;
7910
8486
  if (offset3 >= matchEnd) {
@@ -7921,6 +8497,61 @@ async function calculateHighlightRects(pageProxy, textMatch) {
7921
8497
  }
7922
8498
  return mergeAdjacentRects(matchRects);
7923
8499
  }
8500
+ var measurementContext;
8501
+ function getTextFractions(item, style, start, end) {
8502
+ const length = item.str.length;
8503
+ let from = start / length;
8504
+ let to = end / length;
8505
+ if ((start > 0 || end < length) && style?.fontFamily && !style.vertical && typeof document !== "undefined") {
8506
+ if (measurementContext === void 0) {
8507
+ const canvas = document.createElement("canvas");
8508
+ canvas.width = canvas.height = 0;
8509
+ measurementContext = canvas.getContext("2d");
8510
+ }
8511
+ if (measurementContext) {
8512
+ measurementContext.font = `100px ${style.fontFamily}`;
8513
+ const width = measurementContext.measureText(item.str).width;
8514
+ if (width > 0) {
8515
+ from = start === 0 ? 0 : measurementContext.measureText(item.str.slice(0, start)).width / width;
8516
+ to = end === length ? 1 : measurementContext.measureText(item.str.slice(0, end)).width / width;
8517
+ }
8518
+ }
8519
+ }
8520
+ return item.dir === "rtl" ? [1 - to, 1 - from] : [from, to];
8521
+ }
8522
+ function getItemRect(item, style, viewport, start, end, pageNumber) {
8523
+ const angle = Math.atan2(item.transform[1], item.transform[0]) - (style?.vertical ? Math.PI / 2 : 0);
8524
+ const cosine = Math.cos(angle);
8525
+ const sine = Math.sin(angle);
8526
+ const height = Math.hypot(item.transform[2], item.transform[3]);
8527
+ const ascent = (style?.ascent ?? (style?.descent ? 1 + style.descent : 1)) * height;
8528
+ const width = style?.vertical ? item.height : item.width;
8529
+ const [startFraction, endFraction] = getTextFractions(
8530
+ item,
8531
+ style,
8532
+ start,
8533
+ end
8534
+ );
8535
+ const from = startFraction * width;
8536
+ const to = endFraction * width;
8537
+ let left = Infinity;
8538
+ let top = Infinity;
8539
+ let right = -Infinity;
8540
+ let bottom = -Infinity;
8541
+ for (const along of [from, to]) {
8542
+ for (const above of [ascent, ascent - height]) {
8543
+ const [x, y] = viewport.convertToViewportPoint(
8544
+ item.transform[4] + along * cosine - above * sine,
8545
+ item.transform[5] + along * sine + above * cosine
8546
+ );
8547
+ left = Math.min(left, x);
8548
+ top = Math.min(top, y);
8549
+ right = Math.max(right, x);
8550
+ bottom = Math.max(bottom, y);
8551
+ }
8552
+ }
8553
+ return { pageNumber, left, top, width: right - left, height: bottom - top };
8554
+ }
7924
8555
  function mergeAdjacentRects(rects) {
7925
8556
  if (rects.length <= 1) return rects;
7926
8557
  const merged = [];
@@ -7929,10 +8560,13 @@ function mergeAdjacentRects(rects) {
7929
8560
  for (let i = 1; i < rects.length; i++) {
7930
8561
  const next = rects[i];
7931
8562
  if (!next) continue;
7932
- if (Math.abs(current.top - next.top) < 2 && Math.abs(current.height - next.height) < 2) {
8563
+ if (Math.abs(current.top - next.top) < 2 && Math.abs(current.height - next.height) < 2 && Math.max(current.left, next.left) <= Math.min(current.left + current.width, next.left + next.width) + 2) {
7933
8564
  current = {
7934
8565
  ...current,
7935
- width: next.left + next.width - current.left
8566
+ left: Math.min(current.left, next.left),
8567
+ top: Math.min(current.top, next.top),
8568
+ width: Math.max(current.left + current.width, next.left + next.width) - Math.min(current.left, next.left),
8569
+ height: Math.max(current.top + current.height, next.top + next.height) - Math.min(current.top, next.top)
7936
8570
  };
7937
8571
  } else {
7938
8572
  merged.push(current);