@epam/pdf-highlighter-kit 0.0.2 → 0.0.4

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.
Files changed (63) hide show
  1. package/README.md +186 -62
  2. package/dist/PDFHighlightViewer.d.ts +31 -24
  3. package/dist/PDFHighlightViewer.d.ts.map +1 -1
  4. package/dist/PDFHighlightViewer.js +332 -292
  5. package/dist/PDFHighlightViewer.js.map +1 -1
  6. package/dist/api.d.ts +7 -12
  7. package/dist/api.d.ts.map +1 -1
  8. package/dist/api.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/interaction-handler.d.ts +1 -1
  12. package/dist/core/interaction-handler.d.ts.map +1 -1
  13. package/dist/core/interaction-handler.js +43 -31
  14. package/dist/core/interaction-handler.js.map +1 -1
  15. package/dist/core/pdf-engine.d.ts.map +1 -1
  16. package/dist/core/pdf-engine.js.map +1 -1
  17. package/dist/core/performance-optimizer.d.ts +3 -3
  18. package/dist/core/performance-optimizer.d.ts.map +1 -1
  19. package/dist/core/performance-optimizer.js +7 -8
  20. package/dist/core/performance-optimizer.js.map +1 -1
  21. package/dist/core/text-segmentation.d.ts +8 -7
  22. package/dist/core/text-segmentation.d.ts.map +1 -1
  23. package/dist/core/text-segmentation.js +101 -133
  24. package/dist/core/text-segmentation.js.map +1 -1
  25. package/dist/core/unified-layer-builder.d.ts +7 -8
  26. package/dist/core/unified-layer-builder.d.ts.map +1 -1
  27. package/dist/core/unified-layer-builder.js +124 -174
  28. package/dist/core/unified-layer-builder.js.map +1 -1
  29. package/dist/core/viewport-manager.d.ts.map +1 -1
  30. package/dist/core/viewport-manager.js.map +1 -1
  31. package/dist/index.d.ts +1 -4
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +0 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/types.d.ts +32 -48
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js.map +1 -1
  38. package/dist/utils/highlight-adapter.d.ts +2 -30
  39. package/dist/utils/highlight-adapter.d.ts.map +1 -1
  40. package/dist/utils/highlight-adapter.js +21 -197
  41. package/dist/utils/highlight-adapter.js.map +1 -1
  42. package/dist/utils/pdf-utils.d.ts.map +1 -1
  43. package/dist/utils/pdf-utils.js.map +1 -1
  44. package/dist/utils/worker-loader-simple.d.ts.map +1 -1
  45. package/dist/utils/worker-loader-simple.js.map +1 -1
  46. package/package.json +2 -2
  47. package/dist/core/style-manager.d.ts +0 -88
  48. package/dist/core/style-manager.d.ts.map +0 -1
  49. package/dist/core/style-manager.js +0 -413
  50. package/dist/core/style-manager.js.map +0 -1
  51. package/dist/vite.config.d.ts +0 -3
  52. package/dist/vite.config.d.ts.map +0 -1
  53. package/dist/vite.config.js +0 -20
  54. package/dist/vite.config.js.map +0 -1
  55. package/dist/vitest.config.d.ts +0 -3
  56. package/dist/vitest.config.d.ts.map +0 -1
  57. package/dist/vitest.config.js +0 -24
  58. package/dist/vitest.config.js.map +0 -1
  59. package/dist/vitest.setup.d.ts +0 -2
  60. package/dist/vitest.setup.d.ts.map +0 -1
  61. package/dist/vitest.setup.js +0 -8
  62. package/dist/vitest.setup.js.map +0 -1
  63. package/styles/pdf-highlight-viewer.css +0 -488
@@ -3,26 +3,30 @@ import { ViewportManager } from './core/viewport-manager';
3
3
  import { UnifiedLayerBuilder } from './core/unified-layer-builder';
4
4
  import { UnifiedInteractionHandler } from './core/interaction-handler';
5
5
  import { PerformanceOptimizer } from './core/performance-optimizer';
6
- import { CategoryStyleManager } from './core/style-manager';
7
- import { adaptHighlightData } from './utils/highlight-adapter';
6
+ import { buildHighlightsIndex } from './utils/highlight-adapter';
8
7
  export class PDFHighlightViewer {
9
8
  constructor() {
10
9
  this.container = null;
11
10
  this.pdfContainer = null;
12
11
  this.pageContainers = new Map();
13
- this.highlightData = {};
12
+ this.highlightsIndex = {
13
+ highlights: [],
14
+ byId: new Map(),
15
+ pages: {},
16
+ occurrences: [],
17
+ };
14
18
  this.currentPage = 1;
15
19
  this.currentScale = 1.5;
16
20
  this.totalPages = 0;
17
21
  this.selectedTermId = null;
18
22
  this.isInitialized = false;
23
+ this.navIndex = -1;
19
24
  this.pageDimensions = new Map();
20
25
  this.defaultPageHeight = 800;
21
26
  this.eventListeners = [];
22
27
  this.scrollListener = null;
23
28
  this.analytics = {
24
29
  totalHighlights: 0,
25
- categoryBreakdown: {},
26
30
  mostViewedPages: [],
27
31
  interactionHeatmap: {},
28
32
  averageTimePerPage: 0,
@@ -58,22 +62,25 @@ export class PDFHighlightViewer {
58
62
  this.emit('selectionCopied', { text: selection, format });
59
63
  }
60
64
  },
61
- createHighlightFromSelection: (category) => {
65
+ createHighlightFromSelection: (style) => {
62
66
  const selectionData = this.textSelection.getSelectionWithContext();
63
67
  if (!selectionData)
64
68
  return null;
65
- // Create new term occurrence from selection
66
- const termId = `selection-${Date.now()}`;
67
- const occurrence = {
68
- termId,
69
- coordinates: [], // TODO: Calculate coordinates from selection
69
+ const id = `selection-${Date.now()}`;
70
+ const highlight = {
71
+ id,
72
+ bboxes: [], // still TODO: compute bboxes from selection
73
+ style,
74
+ tooltipText: selectionData.text,
75
+ metadata: {
76
+ pages: selectionData.pages,
77
+ context: selectionData.context,
78
+ range: selectionData.range,
79
+ },
70
80
  };
71
- // Add to highlights
72
- selectionData.pages.forEach((pageNumber) => {
73
- this.addHighlight(pageNumber, occurrence);
74
- });
75
- this.emit('selectionHighlighted', { text: selectionData.text, category, coordinates: [] });
76
- return occurrence;
81
+ this.addHighlight(highlight);
82
+ this.emit('selectionHighlighted', { text: selectionData.text, termId: id });
83
+ return highlight;
77
84
  },
78
85
  };
79
86
  // =============================================================================
@@ -124,7 +131,6 @@ export class PDFHighlightViewer {
124
131
  maxCacheSize: this.options.maxCachedPages ? this.options.maxCachedPages * 10 : 100,
125
132
  frameBudget: this.options.performanceMode ? 8 : 16,
126
133
  });
127
- this.styleManager = new CategoryStyleManager();
128
134
  // Setup interaction callbacks
129
135
  const interactionCallbacks = {
130
136
  onHighlightHover: (event) => this.emit('highlightHover', event),
@@ -252,31 +258,6 @@ export class PDFHighlightViewer {
252
258
  opacity: 0.3;
253
259
  }
254
260
 
255
- /* Category-specific highlight colors */
256
- .protein-highlight .highlight-background {
257
- background-color: #ff6b6b;
258
- }
259
-
260
- .species-highlight .highlight-background {
261
- background-color: #4ecdc4;
262
- }
263
-
264
- .chemical-highlight .highlight-background {
265
- background-color: #45b7d1;
266
- }
267
-
268
- .disease-highlight .highlight-background {
269
- background-color: #f7b731;
270
- }
271
-
272
- .gene-highlight .highlight-background {
273
- background-color: #5f27cd;
274
- }
275
-
276
- .cell_line-highlight .highlight-background {
277
- background-color: #00d2d3;
278
- }
279
-
280
261
  /* Text segment styling */
281
262
  .text-segment {
282
263
  position: relative;
@@ -339,6 +320,12 @@ export class PDFHighlightViewer {
339
320
  };
340
321
  this.container.addEventListener('scroll', this.scrollListener);
341
322
  }
323
+ getPageScrollTop(pageNumber) {
324
+ const el = this.pageContainers.get(pageNumber);
325
+ if (el)
326
+ return el.offsetTop;
327
+ return this.viewportManager.getScrollPositionForPage(pageNumber);
328
+ }
342
329
  /**
343
330
  * Handle scroll events for virtual viewport management
344
331
  */
@@ -502,6 +489,9 @@ export class PDFHighlightViewer {
502
489
  await this.createPageContainers();
503
490
  // Load initial pages
504
491
  await this.loadInitialPages();
492
+ if (this.options.enableVirtualScrolling === false) {
493
+ await this.renderAllPagesBatched(2);
494
+ }
505
495
  this.emit('pdfLoaded', { totalPages: this.totalPages });
506
496
  }
507
497
  catch (error) {
@@ -575,6 +565,21 @@ export class PDFHighlightViewer {
575
565
  });
576
566
  }, 100);
577
567
  }
568
+ async renderAllPagesBatched(batchSize = 2) {
569
+ for (let i = 1; i <= this.totalPages; i += batchSize) {
570
+ const batch = [];
571
+ for (let j = i; j < i + batchSize && j <= this.totalPages; j++) {
572
+ const pageContainer = this.pageContainers.get(j);
573
+ if (pageContainer && !pageContainer.classList.contains('rendered')) {
574
+ batch.push(this.renderPage(j).catch((error) => {
575
+ console.debug(`Failed to render page ${j}:`, error);
576
+ }));
577
+ }
578
+ }
579
+ await Promise.all(batch);
580
+ await new Promise((r) => setTimeout(r, 0));
581
+ }
582
+ }
578
583
  /**
579
584
  * Render a specific page
580
585
  */
@@ -626,9 +631,8 @@ export class PDFHighlightViewer {
626
631
  setPage(pageNumber) {
627
632
  if (pageNumber < 1 || pageNumber > this.totalPages)
628
633
  return;
629
- const pagePosition = this.viewportManager.getScrollPositionForPage(pageNumber);
630
634
  if (this.container) {
631
- this.container.scrollTop = pagePosition;
635
+ this.container.scrollTop = this.getPageScrollTop(pageNumber);
632
636
  }
633
637
  }
634
638
  getZoom() {
@@ -713,73 +717,84 @@ export class PDFHighlightViewer {
713
717
  // Highlight Management
714
718
  // =============================================================================
715
719
  loadHighlights(data) {
716
- if (Array.isArray(data)) {
717
- this.highlightData = adaptHighlightData(data, {
718
- categoryResolver: (highlight) => highlight.metadata?.category || 'default',
719
- termNameResolver: (highlight) => highlight.metadata?.term || highlight.id,
720
- });
721
- }
722
- else {
723
- this.highlightData = data;
724
- }
720
+ this.highlightsIndex = buildHighlightsIndex(data);
721
+ this.navIndex = -1;
725
722
  this.updateAnalytics();
726
- // Update unified layers for all rendered pages
723
+ // refresh rendered pages
727
724
  this.updateAllUnifiedLayers();
728
- // Update spatial indices
729
725
  this.buildAllSpatialIndices();
730
- this.emit('highlightsLoaded', { data: this.highlightData });
731
- }
732
- addHighlight(pageNumber, highlight) {
733
- // Add to first available category or create 'custom' category
734
- const categoryKey = Object.keys(this.highlightData)[0] || 'custom';
735
- if (!this.highlightData[categoryKey]) {
736
- this.highlightData[categoryKey] = { pages: {}, terms: {} };
737
- }
738
- if (!this.highlightData[categoryKey].pages[pageNumber.toString()]) {
739
- this.highlightData[categoryKey].pages[pageNumber.toString()] = [];
740
- }
741
- this.highlightData[categoryKey].pages[pageNumber.toString()].push(highlight);
742
- // Update page if rendered
743
- this.updatePageUnifiedLayer(pageNumber);
744
- this.buildSpatialIndexForPage(pageNumber);
726
+ this.emit('highlightsLoaded', { count: data.length });
727
+ }
728
+ addHighlight(highlight) {
729
+ const prev = this.highlightsIndex.byId.get(highlight.id);
730
+ const affectedPages = new Set();
731
+ prev?.bboxes?.forEach((b) => affectedPages.add(b.page));
732
+ highlight.bboxes.forEach((b) => affectedPages.add(b.page));
733
+ const nextList = [...this.highlightsIndex.highlights];
734
+ const idx = nextList.findIndex((h) => h.id === highlight.id);
735
+ if (idx >= 0)
736
+ nextList[idx] = highlight;
737
+ else
738
+ nextList.push(highlight);
739
+ this.highlightsIndex = buildHighlightsIndex(nextList);
740
+ this.navIndex = -1;
745
741
  this.updateAnalytics();
746
- this.emit('highlightAdded', { pageNumber, highlight });
742
+ // Refresh only affected pages
743
+ for (const pageNumber of affectedPages) {
744
+ this.refreshHighlightLayerForPage(pageNumber);
745
+ this.updatePageUnifiedLayer(pageNumber);
746
+ this.buildSpatialIndexForPage(pageNumber);
747
+ }
748
+ this.emit('highlightAdded', { highlight, pages: Array.from(affectedPages) });
747
749
  }
748
750
  removeHighlight(termId) {
749
- let found = false;
750
- // Remove from all categories
751
- Object.keys(this.highlightData).forEach((category) => {
752
- Object.keys(this.highlightData[category].pages).forEach((pageNumber) => {
753
- const page = this.highlightData[category].pages[pageNumber];
754
- const initialLength = page.length;
755
- this.highlightData[category].pages[pageNumber] = page.filter((highlight) => highlight.termId !== termId);
756
- if (page.length !== initialLength) {
757
- found = true;
758
- this.updatePageUnifiedLayer(parseInt(pageNumber));
759
- this.buildSpatialIndexForPage(parseInt(pageNumber));
760
- }
761
- });
762
- // Remove from terms
763
- delete this.highlightData[category].terms[termId];
764
- });
765
- if (found) {
766
- this.updateAnalytics();
767
- this.emit('highlightRemoved', { termId });
751
+ const prev = this.highlightsIndex.byId.get(termId);
752
+ if (!prev)
753
+ return;
754
+ const affectedPages = new Set();
755
+ prev.bboxes.forEach((b) => affectedPages.add(b.page));
756
+ const nextList = this.highlightsIndex.highlights.filter((h) => h.id !== termId);
757
+ this.highlightsIndex = buildHighlightsIndex(nextList);
758
+ this.navIndex = -1;
759
+ // If removed highlight was selected — clear selection visuals
760
+ if (this.selectedTermId === termId) {
761
+ this.selectedTermId = null;
762
+ this.clearSelectedTermHighlighting();
768
763
  }
764
+ this.updateAnalytics();
765
+ for (const pageNumber of affectedPages) {
766
+ this.refreshHighlightLayerForPage(pageNumber);
767
+ this.updatePageUnifiedLayer(pageNumber);
768
+ this.buildSpatialIndexForPage(pageNumber);
769
+ }
770
+ this.emit('highlightRemoved', { termId, pages: Array.from(affectedPages) });
769
771
  }
770
- updateHighlightStyle(category, style) {
771
- this.styleManager.updateCategoryStyle(category, style);
772
- this.emit('styleUpdated', { category, style });
773
- }
774
- getHighlightsForPage(pageNumber) {
775
- const highlights = [];
776
- Object.values(this.highlightData).forEach((categoryData) => {
777
- const pageHighlights = categoryData.pages[pageNumber.toString()];
778
- if (pageHighlights) {
779
- highlights.push(...pageHighlights);
780
- }
781
- });
782
- return highlights;
772
+ updateHighlightStyle(termId, stylePatch) {
773
+ const prev = this.highlightsIndex.byId.get(termId);
774
+ if (!prev)
775
+ return;
776
+ const next = {
777
+ ...prev,
778
+ style: {
779
+ ...(prev.style ?? {}),
780
+ ...stylePatch,
781
+ },
782
+ };
783
+ const affectedPages = new Set();
784
+ prev.bboxes.forEach((b) => affectedPages.add(b.page));
785
+ const nextList = [...this.highlightsIndex.highlights];
786
+ const idx = nextList.findIndex((h) => h.id === termId);
787
+ if (idx >= 0)
788
+ nextList[idx] = next;
789
+ this.highlightsIndex = buildHighlightsIndex(nextList);
790
+ this.navIndex = -1;
791
+ this.updateAnalytics();
792
+ for (const pageNumber of affectedPages) {
793
+ this.refreshHighlightLayerForPage(pageNumber);
794
+ this.updatePageUnifiedLayer(pageNumber);
795
+ this.buildSpatialIndexForPage(pageNumber);
796
+ }
797
+ this.emit('styleUpdated', { termId, style: next.style, patch: stylePatch });
783
798
  }
784
799
  /**
785
800
  * Update unified layer for a specific page
@@ -789,9 +804,9 @@ export class PDFHighlightViewer {
789
804
  if (!pageContainer)
790
805
  return;
791
806
  const pageData = this.pdfEngine.getPageData(pageNumber);
792
- if (!pageData || !pageData.textContent)
807
+ if (!pageData?.textContent)
793
808
  return;
794
- this.layerBuilder.updateHighlights(this.highlightData, pageNumber, pageData.textContent);
809
+ this.layerBuilder.updateHighlights(pageContainer, this.highlightsIndex.highlights, pageNumber, pageData.textContent, this.currentScale);
795
810
  }
796
811
  /**
797
812
  * Update all unified layers
@@ -839,7 +854,7 @@ export class PDFHighlightViewer {
839
854
  totalHeight += newDimensions.height;
840
855
  validDimensions++;
841
856
  }
842
- catch (_error) {
857
+ catch {
843
858
  pageContainer.style.height = `${this.defaultPageHeight}px`;
844
859
  }
845
860
  }
@@ -864,60 +879,91 @@ export class PDFHighlightViewer {
864
879
  // =============================================================================
865
880
  // Navigation
866
881
  // =============================================================================
867
- goToHighlight(termId, occurrenceIndex = 0) {
868
- // Find highlight location
869
- for (const [, categoryData] of Object.entries(this.highlightData)) {
870
- for (const [pageNumber, highlights] of Object.entries(categoryData.pages)) {
871
- const highlight = highlights.find((h) => h.termId === termId);
872
- if (highlight && highlight.coordinates[occurrenceIndex]) {
873
- const page = parseInt(pageNumber);
874
- this.setPage(page);
875
- // TODO: Scroll to specific coordinates within page
876
- this.emit('navigationComplete', { termId, pageNumber: page, occurrenceIndex });
877
- return;
878
- }
882
+ getNavOccurrences() {
883
+ const list = [];
884
+ for (const h of this.highlightsIndex.highlights) {
885
+ for (let i = 0; i < h.bboxes.length; i++) {
886
+ const b = h.bboxes[i];
887
+ list.push({ termId: h.id, pageNumber: b.page, occurrenceIndex: i, x1: b.x1, y1: b.y1 });
879
888
  }
880
889
  }
890
+ list.sort((a, b) => a.pageNumber - b.pageNumber || a.y1 - b.y1 || a.x1 - b.x1);
891
+ return list;
881
892
  }
882
- nextHighlight(category) {
883
- // TODO: Implement next highlight navigation
884
- console.log('nextHighlight not yet implemented:', category);
893
+ goToHighlight(termId, occurrenceIndex = 0) {
894
+ const highlight = this.getHighlightById(termId);
895
+ if (!highlight)
896
+ return;
897
+ const bbox = highlight.bboxes[occurrenceIndex];
898
+ if (!bbox)
899
+ return;
900
+ const page = bbox.page;
901
+ this.highlightSelectedTerm(termId);
902
+ this.setPage(page);
903
+ void this.renderPage(page)
904
+ .then(() => {
905
+ const pageContainer = this.pageContainers.get(page);
906
+ if (!pageContainer || !this.container) {
907
+ this.emit('navigationComplete', { termId, pageNumber: page, occurrenceIndex });
908
+ return;
909
+ }
910
+ const pageTop = pageContainer.offsetTop;
911
+ const y = bbox.y1 * this.currentScale;
912
+ this.container.scrollTop = Math.max(0, pageTop + y - 60);
913
+ this.emit('navigationComplete', { termId, pageNumber: page, occurrenceIndex });
914
+ })
915
+ .catch((error) => {
916
+ console.error('goToHighlight render/scroll failed:', error);
917
+ this.emit('navigationError', { termId, pageNumber: page, occurrenceIndex, error });
918
+ });
885
919
  }
886
- previousHighlight(category) {
887
- // TODO: Implement previous highlight navigation
888
- console.log('previousHighlight not yet implemented:', category);
920
+ nextHighlight() {
921
+ const list = this.getNavOccurrences();
922
+ if (list.length === 0)
923
+ return;
924
+ this.navIndex = (this.navIndex + 1) % list.length;
925
+ const next = list[this.navIndex];
926
+ this.goToHighlight(next.termId, next.occurrenceIndex);
927
+ }
928
+ previousHighlight() {
929
+ const list = this.getNavOccurrences();
930
+ if (list.length === 0)
931
+ return;
932
+ this.navIndex = (this.navIndex - 1 + list.length) % list.length;
933
+ const prev = list[this.navIndex];
934
+ this.goToHighlight(prev.termId, prev.occurrenceIndex);
889
935
  }
890
936
  goToCoordinate(pageNumber, x, y) {
891
937
  this.setPage(pageNumber);
892
- // TODO: Scroll to specific coordinates
938
+ if (!this.container)
939
+ return;
940
+ const pageTop = this.getPageScrollTop(pageNumber);
941
+ const targetY = pageTop + y * this.currentScale - this.container.clientHeight * 0.3;
942
+ this.container.scrollTop = Math.max(0, targetY);
893
943
  this.emit('coordinateNavigation', { pageNumber, x, y });
894
944
  }
895
945
  // =============================================================================
896
946
  // Search & Filter
897
947
  // =============================================================================
898
- searchTerms(query) {
899
- const results = [];
900
- Object.values(this.highlightData).forEach((categoryData) => {
901
- Object.values(categoryData.terms).forEach((term) => {
902
- if (term.term.toLowerCase().includes(query.toLowerCase()) ||
903
- term.aliases.some((alias) => alias.toLowerCase().includes(query.toLowerCase()))) {
904
- results.push(term);
948
+ searchHighlights(query) {
949
+ const q = query.trim().toLowerCase();
950
+ if (!q)
951
+ return [];
952
+ return this.highlightsIndex.highlights.filter((h) => {
953
+ if (h.id.toLowerCase().includes(q))
954
+ return true;
955
+ if ((h.tooltipText ?? '').toLowerCase().includes(q))
956
+ return true;
957
+ if (h.metadata) {
958
+ try {
959
+ return JSON.stringify(h.metadata).toLowerCase().includes(q);
905
960
  }
906
- });
961
+ catch {
962
+ // ignore non-serializable metadata
963
+ }
964
+ }
965
+ return false;
907
966
  });
908
- return results;
909
- }
910
- filterByCategory(categories) {
911
- // TODO: Implement category filtering
912
- console.log('filterByCategory not yet implemented:', categories);
913
- }
914
- highlightSearchResults(query) {
915
- // TODO: Implement search result highlighting
916
- console.log('highlightSearchResults not yet implemented:', query);
917
- }
918
- clearSearchResults() {
919
- // TODO: Implement search result clearing
920
- console.log('clearSearchResults not yet implemented');
921
967
  }
922
968
  // =============================================================================
923
969
  // Interaction Modes
@@ -933,14 +979,13 @@ export class PDFHighlightViewer {
933
979
  // =============================================================================
934
980
  getPerformanceMetrics() {
935
981
  const baseMetrics = this.performanceOptimizer.getPerformanceMetrics();
936
- // Add memory usage info
937
- const renderedPages = Array.from(this.pageContainers.entries()).filter(([_, container]) => container.classList.contains('rendered')).length;
938
- const memoryEstimate = renderedPages * 2; // ~2MB per rendered page
982
+ const renderedPages = Array.from(this.pageContainers.values()).filter((container) => container.classList.contains('rendered')).length;
983
+ const memoryEstimate = renderedPages * 2; // ~2MB per rendered page (rough estimate)
939
984
  return {
940
985
  ...baseMetrics,
941
986
  memoryUsage: {
942
987
  pages: renderedPages,
943
- highlights: Object.keys(this.highlightData).length,
988
+ highlights: this.highlightsIndex.highlights.length,
944
989
  cache: this.pageDimensions.size,
945
990
  total: memoryEstimate,
946
991
  },
@@ -1037,6 +1082,18 @@ export class PDFHighlightViewer {
1037
1082
  // =============================================================================
1038
1083
  // Private Helper Methods
1039
1084
  // =============================================================================
1085
+ refreshHighlightLayerForPage(pageNumber) {
1086
+ const pageContainer = this.pageContainers.get(pageNumber);
1087
+ if (!pageContainer || !pageContainer.classList.contains('rendered'))
1088
+ return;
1089
+ const existingHighlightLayer = pageContainer.querySelector('.highlight-layer');
1090
+ if (existingHighlightLayer)
1091
+ existingHighlightLayer.remove();
1092
+ const canvas = pageContainer.querySelector('canvas');
1093
+ if (!canvas)
1094
+ return;
1095
+ this.addHighlightsToPage(pageNumber, canvas.width, canvas.height);
1096
+ }
1040
1097
  /**
1041
1098
  * Add highlights to a rendered page
1042
1099
  */
@@ -1048,12 +1105,8 @@ export class PDFHighlightViewer {
1048
1105
  if (pageContainer.querySelector('.highlight-layer')) {
1049
1106
  return;
1050
1107
  }
1051
- // Get the actual PDF page to get proper dimensions
1052
1108
  try {
1053
- // Simple approach - just scale coordinates by the current zoom level
1054
- // The coordinates in the JSON appear to be at scale 1.0
1055
1109
  const scale = this.currentScale;
1056
- // Create highlight layer
1057
1110
  const highlightLayer = document.createElement('div');
1058
1111
  highlightLayer.className = 'highlight-layer';
1059
1112
  highlightLayer.style.position = 'absolute';
@@ -1061,78 +1114,73 @@ export class PDFHighlightViewer {
1061
1114
  highlightLayer.style.left = '0';
1062
1115
  highlightLayer.style.width = `${canvasWidth}px`;
1063
1116
  highlightLayer.style.height = `${canvasHeight}px`;
1064
- highlightLayer.style.zIndex = '2'; // Above text layer
1117
+ highlightLayer.style.zIndex = '2';
1065
1118
  highlightLayer.style.pointerEvents = 'none';
1066
- // Add highlights from each category
1067
- Object.entries(this.highlightData).forEach(([category, categoryData]) => {
1068
- const pageHighlights = categoryData.pages[pageNumber.toString()];
1069
- if (!pageHighlights)
1070
- return;
1071
- pageHighlights.forEach((highlight) => {
1072
- if (highlight.coordinates && highlight.coordinates.length > 0) {
1073
- highlight.coordinates.forEach((coord) => {
1074
- const highlightDiv = document.createElement('div');
1075
- highlightDiv.className = `highlight ${category}-highlight`;
1076
- highlightDiv.setAttribute('data-term-id', highlight.termId);
1077
- highlightDiv.setAttribute('data-category', category);
1078
- // Scale coordinates by current zoom level
1079
- const left = coord.x1 * scale;
1080
- const top = coord.y1 * scale;
1081
- const width = (coord.x2 - coord.x1) * scale;
1082
- const height = (coord.y2 - coord.y1) * scale;
1083
- highlightDiv.style.position = 'absolute';
1084
- highlightDiv.style.left = `${left}px`;
1085
- highlightDiv.style.top = `${top}px`;
1086
- highlightDiv.style.width = `${width}px`;
1087
- highlightDiv.style.height = `${height}px`;
1088
- highlightDiv.style.backgroundColor = this.getHighlightColor(highlight.termId, category);
1089
- highlightDiv.style.border = `1px solid ${this.getHighlightColor(highlight.termId, category)}`;
1090
- highlightDiv.style.pointerEvents = 'auto';
1091
- highlightDiv.style.cursor = 'pointer';
1092
- highlightDiv.style.boxSizing = 'border-box';
1093
- highlightDiv.style.userSelect = 'none'; // Prevent highlight div from being selected
1094
- highlightDiv.style.mixBlendMode = 'multiply'; // Better color blending
1095
- // Check for overlapping highlights and adjust opacity
1096
- const overlappingCount = this.countOverlappingHighlights(highlightLayer, coord, scale);
1097
- const baseOpacity = Math.max(0.15, 0.3 / Math.max(1, overlappingCount * 0.7));
1098
- highlightDiv.style.opacity = baseOpacity.toString();
1099
- // todo make hover opacities configurable?
1100
- // Add hover effect with dynamic opacity
1101
- const originalOpacity = baseOpacity.toString();
1102
- const hoverOpacity = Math.min(0.6, baseOpacity + 0.2).toString();
1103
- highlightDiv.addEventListener('mouseenter', () => {
1104
- if (this.options.highlightsConfig?.enableMultilineHover) {
1105
- const highlightBoxes = highlightLayer.querySelectorAll(`[data-term-id="${highlight.termId}"]`);
1106
- highlightBoxes.forEach((highlightBox) => {
1107
- highlightBox.style.opacity = hoverOpacity;
1108
- });
1109
- const unhoveredBoxes = highlightLayer.querySelectorAll(`div[data-term-id]:not([data-term-id="${highlight.termId}"])`);
1110
- unhoveredBoxes.forEach((highlightBox) => {
1111
- highlightBox.style.opacity = '0.1';
1112
- });
1113
- }
1114
- else {
1115
- highlightDiv.style.opacity = hoverOpacity;
1116
- }
1117
- });
1118
- highlightDiv.addEventListener('mouseleave', () => {
1119
- if (this.options.highlightsConfig?.enableMultilineHover) {
1120
- const allBoxes = highlightLayer.querySelectorAll(`div[data-term-id]`);
1121
- allBoxes.forEach((highlightBox) => {
1122
- highlightBox.style.opacity = originalOpacity;
1123
- });
1124
- }
1125
- else {
1126
- highlightDiv.style.opacity = originalOpacity;
1127
- }
1119
+ for (const highlight of this.highlightsIndex.highlights) {
1120
+ const style = highlight.style;
1121
+ const backgroundColor = style?.backgroundColor ?? '#666666';
1122
+ const borderColor = style?.borderColor ?? backgroundColor;
1123
+ const borderWidth = style?.borderWidth ?? '1px';
1124
+ for (let bboxIndex = 0; bboxIndex < highlight.bboxes.length; bboxIndex++) {
1125
+ const bbox = highlight.bboxes[bboxIndex];
1126
+ if (bbox.page !== pageNumber)
1127
+ continue;
1128
+ const highlightDiv = document.createElement('div');
1129
+ highlightDiv.className = 'highlight';
1130
+ highlightDiv.setAttribute('data-term-id', highlight.id);
1131
+ highlightDiv.setAttribute('data-page', String(pageNumber));
1132
+ highlightDiv.setAttribute('data-bbox-index', String(bboxIndex));
1133
+ const left = bbox.x1 * scale;
1134
+ const top = bbox.y1 * scale;
1135
+ const width = (bbox.x2 - bbox.x1) * scale;
1136
+ const height = (bbox.y2 - bbox.y1) * scale;
1137
+ highlightDiv.style.position = 'absolute';
1138
+ highlightDiv.style.left = `${left}px`;
1139
+ highlightDiv.style.top = `${top}px`;
1140
+ highlightDiv.style.width = `${width}px`;
1141
+ highlightDiv.style.height = `${height}px`;
1142
+ highlightDiv.style.backgroundColor = backgroundColor;
1143
+ highlightDiv.style.border = `${borderWidth} solid ${borderColor}`;
1144
+ highlightDiv.style.pointerEvents = 'auto';
1145
+ highlightDiv.style.cursor = 'pointer';
1146
+ highlightDiv.style.boxSizing = 'border-box';
1147
+ highlightDiv.style.userSelect = 'none';
1148
+ highlightDiv.style.mixBlendMode = 'multiply';
1149
+ const baseOpacity = typeof style?.opacity === 'number' ? style.opacity : 0.3;
1150
+ const overlappingCount = this.countOverlappingHighlights(highlightLayer, bbox, scale);
1151
+ const effectiveOpacity = Math.max(0.05, baseOpacity / Math.max(1, overlappingCount * 0.7));
1152
+ highlightDiv.style.opacity = effectiveOpacity.toString();
1153
+ highlightDiv.dataset.originalOpacity = effectiveOpacity.toString();
1154
+ const hoverOpacity = typeof style?.hoverOpacity === 'number'
1155
+ ? style.hoverOpacity
1156
+ : Math.min(0.6, effectiveOpacity + 0.2);
1157
+ highlightDiv.addEventListener('mouseenter', () => {
1158
+ if (this.options.highlightsConfig?.enableMultilineHover) {
1159
+ const same = highlightLayer.querySelectorAll(`[data-term-id="${highlight.id}"]`);
1160
+ same.forEach((el) => (el.style.opacity = String(hoverOpacity)));
1161
+ const other = highlightLayer.querySelectorAll(`div[data-term-id]:not([data-term-id="${highlight.id}"])`);
1162
+ other.forEach((el) => (el.style.opacity = '0.1'));
1163
+ }
1164
+ else {
1165
+ highlightDiv.style.opacity = String(hoverOpacity);
1166
+ }
1167
+ });
1168
+ highlightDiv.addEventListener('mouseleave', () => {
1169
+ if (this.options.highlightsConfig?.enableMultilineHover) {
1170
+ const all = highlightLayer.querySelectorAll(`div[data-term-id]`);
1171
+ all.forEach((el) => {
1172
+ const original = el.dataset.originalOpacity ?? '0.3';
1173
+ el.style.opacity = original;
1128
1174
  });
1129
- highlightLayer.appendChild(highlightDiv);
1130
- });
1131
- }
1132
- });
1133
- });
1175
+ }
1176
+ else {
1177
+ highlightDiv.style.opacity = highlightDiv.dataset.originalOpacity ?? '0.3';
1178
+ }
1179
+ });
1180
+ highlightLayer.appendChild(highlightDiv);
1181
+ }
1182
+ }
1134
1183
  pageContainer.appendChild(highlightLayer);
1135
- // Apply selected term highlighting if there's a selected term
1136
1184
  if (this.selectedTermId) {
1137
1185
  this.applySelectionToPage(pageNumber);
1138
1186
  }
@@ -1141,76 +1189,66 @@ export class PDFHighlightViewer {
1141
1189
  console.error(`Failed to add highlights to page ${pageNumber}:`, error);
1142
1190
  }
1143
1191
  }
1192
+ getHighlightById(termId) {
1193
+ const byId = this.highlightsIndex?.byId;
1194
+ if (byId && typeof byId.get === 'function') {
1195
+ const fromMap = byId.get(termId);
1196
+ if (fromMap)
1197
+ return fromMap;
1198
+ }
1199
+ return this.highlightsIndex.highlights.find((h) => h.id === termId);
1200
+ }
1201
+ getHighlightStyle(termId) {
1202
+ return this.getHighlightById(termId)?.style;
1203
+ }
1144
1204
  /**
1145
1205
  * Update highlights colors for specified page
1146
1206
  * */
1147
1207
  updateHighlightsStyles(pageNumber, hoveredIds) {
1148
1208
  const pageContainer = this.pageContainers.get(pageNumber);
1149
- if (!pageContainer) {
1209
+ if (!pageContainer)
1150
1210
  return;
1151
- }
1152
1211
  const highlightLayer = pageContainer.querySelector('.highlight-layer');
1153
- if (!highlightLayer) {
1212
+ if (!highlightLayer)
1154
1213
  return;
1155
- }
1156
- // Find all highlights in this page
1157
1214
  const allHighlights = pageContainer.querySelectorAll('.highlight, .highlight-wrapper');
1158
- allHighlights.forEach((highlight) => {
1159
- const elementTermId = highlight.getAttribute('data-term-id');
1160
- const category = highlight.getAttribute('data-category');
1161
- if (elementTermId && category) {
1162
- highlight.style.backgroundColor = this.getHighlightColor(elementTermId, category);
1163
- highlight.style.border =
1164
- `1px solid ${this.getHighlightColor(elementTermId, category)}`;
1165
- if (this.options.highlightsConfig?.enableMultilineHover &&
1166
- hoveredIds &&
1167
- Array.isArray(hoveredIds)) {
1168
- const baseOpacity = 0.3;
1169
- const originalOpacity = baseOpacity.toString();
1170
- const hoverOpacity = Math.min(0.6, baseOpacity + 0.2).toString();
1171
- const unhoveredOpacity = '0.1';
1172
- if (hoveredIds.includes(elementTermId)) {
1173
- highlight.style.opacity = hoverOpacity;
1174
- }
1175
- else if (hoveredIds.length > 0) {
1176
- highlight.style.opacity = unhoveredOpacity;
1177
- }
1178
- else {
1179
- highlight.style.opacity = originalOpacity;
1180
- }
1215
+ allHighlights.forEach((el) => {
1216
+ const termId = el.getAttribute('data-term-id');
1217
+ if (!termId)
1218
+ return;
1219
+ const style = this.getHighlightStyle(termId);
1220
+ const bg = style?.backgroundColor ?? el.style.backgroundColor ?? '#666666';
1221
+ const borderColor = style?.borderColor ?? bg;
1222
+ const borderWidth = style?.borderWidth ?? '1px';
1223
+ el.style.backgroundColor = bg;
1224
+ el.style.border = `${borderWidth} solid ${borderColor}`;
1225
+ if (this.options.highlightsConfig?.enableMultilineHover &&
1226
+ hoveredIds &&
1227
+ Array.isArray(hoveredIds)) {
1228
+ const baseOpacity = typeof style?.opacity === 'number'
1229
+ ? style.opacity
1230
+ : parseFloat(el.dataset.originalOpacity ?? '0.3');
1231
+ const hoverOpacity = typeof style?.hoverOpacity === 'number'
1232
+ ? style.hoverOpacity
1233
+ : Math.min(0.6, baseOpacity + 0.2);
1234
+ if (hoveredIds.includes(termId)) {
1235
+ el.style.opacity = String(hoverOpacity);
1236
+ }
1237
+ else if (hoveredIds.length > 0) {
1238
+ el.style.opacity = '0.1';
1239
+ }
1240
+ else {
1241
+ el.style.opacity = String(baseOpacity);
1181
1242
  }
1182
1243
  }
1183
1244
  });
1184
1245
  }
1185
- /**
1186
- * Get highlight color
1187
- */
1188
- getHighlightColor(termId, category) {
1189
- if (this.options.highlightsConfig && this.options.highlightsConfig.getHighlightColor) {
1190
- return this.options.highlightsConfig.getHighlightColor(termId);
1191
- }
1192
- return this.getCategoryColor(category);
1193
- }
1194
- /**
1195
- * Get color for highlight category
1196
- */
1197
- getCategoryColor(category) {
1198
- const colors = {
1199
- protein: '#ff6b6b',
1200
- species: '#4ecdc4',
1201
- chemical: '#45b7d1',
1202
- disease: '#f7b731',
1203
- gene: '#5f27cd',
1204
- cell_line: '#00d2d3',
1205
- };
1206
- return colors[category] || '#666666';
1207
- }
1208
1246
  /**
1209
1247
  * Build spatial index for a specific page
1210
1248
  */
1211
1249
  buildSpatialIndexForPage(pageNumber) {
1212
- const highlights = this.getHighlightsForPage(pageNumber);
1213
- this.performanceOptimizer.buildSpatialIndex(highlights, pageNumber);
1250
+ const refs = this.highlightsIndex.pages[String(pageNumber)] ?? [];
1251
+ this.performanceOptimizer.buildSpatialIndex(refs, pageNumber);
1214
1252
  }
1215
1253
  /**
1216
1254
  * Build spatial indices for all pages
@@ -1221,29 +1259,33 @@ export class PDFHighlightViewer {
1221
1259
  }
1222
1260
  }
1223
1261
  /**
1224
- * Get highlight count for a page
1262
+ * Get the number of highlights on a given page.
1263
+ *
1264
+ * This reads from the precomputed `highlightsIndex.pages` map, which stores
1265
+ * arrays of highlight references keyed by the page number as a string.
1266
+ * If there are no references for the requested page, an empty array is used,
1267
+ * and the method returns 0.
1268
+ *
1269
+ * @param pageNumber - The 1-based page number to count highlights for.
1270
+ * @returns The total number of highlight references on the page.
1225
1271
  */
1226
1272
  getHighlightCountForPage(pageNumber) {
1227
- return this.getHighlightsForPage(pageNumber).length;
1273
+ const refs = this.highlightsIndex.pages[String(pageNumber)] ?? [];
1274
+ return refs.length;
1228
1275
  }
1229
1276
  /**
1230
- * Update analytics data
1277
+ * Update analytics information based on the current highlights index.
1278
+ *
1279
+ * This method recalculates the total number of highlights by reading the
1280
+ * length of `this.highlightsIndex.highlights` and updates the
1281
+ * `this.analytics` object with the new `totalHighlights` value, while
1282
+ * preserving all other existing analytics properties.
1231
1283
  */
1232
1284
  updateAnalytics() {
1233
- let totalHighlights = 0;
1234
- const categoryBreakdown = {};
1235
- Object.entries(this.highlightData).forEach(([category, categoryData]) => {
1236
- let categoryCount = 0;
1237
- Object.values(categoryData.pages).forEach((highlights) => {
1238
- categoryCount += highlights.length;
1239
- });
1240
- categoryBreakdown[category] = categoryCount;
1241
- totalHighlights += categoryCount;
1242
- });
1285
+ const totalHighlights = this.highlightsIndex.highlights.length;
1243
1286
  this.analytics = {
1244
1287
  ...this.analytics,
1245
1288
  totalHighlights,
1246
- categoryBreakdown,
1247
1289
  };
1248
1290
  }
1249
1291
  /**
@@ -1535,14 +1577,12 @@ export class PDFHighlightViewer {
1535
1577
  this.pdfEngine.destroy();
1536
1578
  this.interactionHandler.destroy();
1537
1579
  this.performanceOptimizer.destroy();
1538
- this.styleManager.destroy();
1539
1580
  // Clear DOM references
1540
1581
  this.container = null;
1541
1582
  this.pdfContainer = null;
1542
1583
  this.pageContainers.clear();
1543
1584
  // Clear state
1544
1585
  this.eventListeners = [];
1545
- this.highlightData = {};
1546
1586
  this.isInitialized = false;
1547
1587
  this.emit('destroyed');
1548
1588
  }