t commited on
Commit
7d3748e
·
1 Parent(s): 6fb2610

fix: switch to bullseye base to resolve wkhtmltox dependencies

Browse files
Files changed (1) hide show
  1. templates/pdfjs_viewer.html +179 -43
templates/pdfjs_viewer.html CHANGED
@@ -2,11 +2,11 @@
2
  <html lang="en" dir="ltr">
3
  <head>
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
6
  <title>{{ pdf_title }}</title>
7
  <!-- Bootstrap Icons -->
8
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
9
- <!-- Bootstrap CSS (only for Toast styling, scoped to avoid breaking layout) -->
10
  <style>
11
  /* Mozilla PDF.js Viewer Visual Replication */
12
  :root {
@@ -27,6 +27,7 @@
27
  * {
28
  box-sizing: border-box;
29
  user-select: none;
 
30
  }
31
 
32
  body {
@@ -40,6 +41,8 @@
40
  display: flex;
41
  flex-direction: column;
42
  color: var(--text-color);
 
 
43
  }
44
 
45
  #outerContainer {
@@ -67,7 +70,7 @@
67
  flex: 1;
68
  overflow-y: auto;
69
  padding: 10px;
70
- position: relative; /* Ensure offsetTop works reliably */
71
  }
72
 
73
  #sidebarContent::-webkit-scrollbar { width: 10px; }
@@ -97,7 +100,7 @@
97
 
98
  .thumbnail-canvas {
99
  display: block;
100
- width: 100px; /* Fixed width for thumbnails */
101
  height: auto;
102
  }
103
 
@@ -195,6 +198,9 @@
195
  right: 0; bottom: 0; left: 0;
196
  background-color: var(--body-bg);
197
  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=');
 
 
 
198
  }
199
 
200
  #viewerContainer::-webkit-scrollbar { width: 14px; height: 14px; }
@@ -208,6 +214,9 @@
208
  align-items: center;
209
  padding: 20px 0;
210
  position: relative;
 
 
 
211
  }
212
 
213
  .page {
@@ -246,7 +255,7 @@
246
  }
247
 
248
  .toast {
249
- background-color: #2b3035; /* Bootstrap dark toast bg */
250
  color: #fff;
251
  border-radius: 0.375rem;
252
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
@@ -466,6 +475,124 @@
466
  toast: document.getElementById('cacheToast')
467
  };
468
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  // ==========================================
470
  // INDEXED DB CACHE
471
  // ==========================================
@@ -500,10 +627,7 @@
500
 
501
  if (!record) return null;
502
 
503
- // Unpack if wrapped (new format) or use raw (legacy)
504
  const data = record.data || record;
505
-
506
- // If it's the new format, update timestamp (async fire-and-forget)
507
  if (record.data) {
508
  this.touch(url, record);
509
  }
@@ -530,7 +654,6 @@
530
  const tx = db.transaction(this.storeName, 'readwrite');
531
  const store = tx.objectStore(this.storeName);
532
 
533
- // Calculate usage and collect entries for eviction
534
  let currentSize = 0;
535
  const entries = [];
536
 
@@ -539,11 +662,10 @@
539
  req.onsuccess = (e) => {
540
  const cursor = e.target.result;
541
  if (cursor) {
542
- // Skip if it's the current file (we are overwriting anyway)
543
  if (cursor.key !== url) {
544
  const val = cursor.value;
545
  const itemSize = (val.data || val).byteLength || 0;
546
- const itemTime = val.timestamp || 0; // Legacy = 0 (evict first)
547
  currentSize += itemSize;
548
  entries.push({ key: cursor.key, size: itemSize, timestamp: itemTime });
549
  }
@@ -555,11 +677,8 @@
555
  req.onerror = () => resolve();
556
  });
557
 
558
- // Evict if needed
559
  if (currentSize + newSize > this.MAX_TOTAL_SIZE) {
560
- // Sort oldest first
561
  entries.sort((a, b) => a.timestamp - b.timestamp);
562
-
563
  while (entries.length > 0 && (currentSize + newSize > this.MAX_TOTAL_SIZE)) {
564
  const entry = entries.shift();
565
  store.delete(entry.key);
@@ -568,7 +687,6 @@
568
  }
569
  }
570
 
571
- // Save new record
572
  const record = { data: data, timestamp: Date.now() };
573
  store.put(record, url);
574
 
@@ -592,7 +710,7 @@
592
  elements.toast.classList.remove('show');
593
  }
594
 
595
- window.hideToast = hideToast; // Expose for close button
596
 
597
  // ==========================================
598
  // LOADING
@@ -609,10 +727,9 @@
609
  let isCached = !!data;
610
 
611
  if (isCached) {
612
- showToast(); // Show toast if cached
613
  }
614
 
615
- // Config for fonts to prevent warnings
616
  const params = {
617
  cMapUrl: 'https://cdn.jsdelivr.net/npm/pdf.js@3.11.174/dist/cmaps/',
618
  cMapPacked: true,
@@ -635,9 +752,7 @@
635
  state.pdfDoc = await loadingTask.promise;
636
 
637
  if (!isCached) {
638
- // Save to cache for next time
639
  state.pdfDoc.getData().then(pdfData => {
640
- // Limit cache to 100MB per file
641
  if (pdfData.byteLength <= 100 * 1024 * 1024) {
642
  PDFCache.put(config.url, pdfData);
643
  } else {
@@ -664,8 +779,9 @@
664
  elements.viewer.innerHTML = '';
665
  state.pages = [];
666
 
667
- // Get last viewed page for this PDF
668
- const lastPage = PageMemory.getLastPage(config.pdfIdentifier);
 
669
 
670
  for (let i = 1; i <= state.pdfDoc.numPages; i++) {
671
  const pageDiv = document.createElement('div');
@@ -673,6 +789,7 @@
673
  pageDiv.setAttribute('data-page-number', i);
674
  pageDiv.id = `pageContainer${i}`;
675
 
 
676
  pageDiv.style.width = '612px';
677
  pageDiv.style.height = '792px';
678
 
@@ -681,9 +798,13 @@
681
  }
682
 
683
  setupIntersectionObserver();
684
- generateThumbnails();
685
 
686
- // Scroll to last viewed page after a short delay
 
 
 
 
 
687
  if (lastPage > 1) {
688
  setTimeout(() => scrollToPage(lastPage), 100);
689
  }
@@ -716,7 +837,6 @@
716
  if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
717
  const pageNum = parseInt(entry.target.getAttribute('data-page-number'));
718
  updateUIState(pageNum);
719
- // Save current page to localStorage
720
  PageMemory.saveLastPage(config.pdfIdentifier, pageNum);
721
  }
722
  });
@@ -783,7 +903,6 @@
783
  imgContainer.className = 'thumbnail-image-container';
784
  imgContainer.id = `thumb-img-${i}`;
785
 
786
- // Placeholder dimensions until render
787
  imgContainer.style.width = '100px';
788
  imgContainer.style.height = '130px';
789
 
@@ -796,14 +915,11 @@
796
  elements.sidebarContent.appendChild(thumbContainer);
797
  }
798
 
799
- // Render thumbnails lazily when sidebar is open or idle
800
- // For now, render them sequentially to avoid UI freeze
801
  renderThumbnailsQueue();
802
  }
803
 
804
  async function renderThumbnailsQueue() {
805
  for (let i = 1; i <= state.pdfDoc.numPages; i++) {
806
- // Check if we should render (could add visibility check here)
807
  await renderSingleThumbnail(i);
808
  }
809
  }
@@ -811,7 +927,7 @@
811
  async function renderSingleThumbnail(i) {
812
  try {
813
  const page = await state.pdfDoc.getPage(i);
814
- const viewport = page.getViewport({ scale: 0.2 }); // Small scale
815
  const canvas = document.createElement('canvas');
816
  canvas.className = 'thumbnail-canvas';
817
  canvas.height = viewport.height;
@@ -837,8 +953,6 @@
837
  if (num < 1 || num > state.pdfDoc.numPages) return;
838
  const pageDiv = document.getElementById(`pageContainer${num}`);
839
  if (pageDiv) {
840
- // FIXED: Use scrollTop instead of scrollIntoView to prevent layout breaking
841
- // Calculate offset relative to the container
842
  elements.container.scrollTop = pageDiv.offsetTop;
843
  }
844
  }
@@ -853,17 +967,14 @@
853
  if (thumb) {
854
  thumb.classList.add('selected');
855
 
856
- // Manual Scroll Logic for Sidebar (Replaces scrollIntoView)
857
  const thumbTop = thumb.offsetTop;
858
  const thumbHeight = thumb.clientHeight;
859
  const containerHeight = elements.sidebarContent.clientHeight;
860
  const containerScroll = elements.sidebarContent.scrollTop;
861
 
862
- // If above visible area
863
  if (thumbTop < containerScroll) {
864
  elements.sidebarContent.scrollTop = thumbTop - 10;
865
  }
866
- // If below visible area
867
  else if (thumbTop + thumbHeight > containerScroll + containerHeight) {
868
  elements.sidebarContent.scrollTop = thumbTop + thumbHeight - containerHeight + 10;
869
  }
@@ -895,18 +1006,41 @@
895
  });
896
 
897
  if (elements.zoomIn) elements.zoomIn.addEventListener('click', () => {
898
- let current = parseFloat(elements.scaleSelect.value) || 1.0;
899
- const newScale = (current + 0.25).toString();
900
- elements.scaleSelect.value = newScale;
901
- config.scale += 0.25;
 
 
 
 
 
 
 
902
  config.currentScaleValue = config.scale.toString();
 
 
 
903
  initViewer();
904
  });
905
 
906
  if (elements.zoomOut) elements.zoomOut.addEventListener('click', () => {
907
- if (config.scale <= 0.5) return;
908
- config.scale -= 0.25;
 
 
 
 
 
 
 
 
 
 
909
  config.currentScaleValue = config.scale.toString();
 
 
 
910
  initViewer();
911
  });
912
 
@@ -923,13 +1057,15 @@
923
  link.click();
924
  });
925
 
 
 
 
926
  // Start loading
927
  if (typeof pdfjsLib !== 'undefined') {
928
  loadPDF();
929
  } else {
930
- // Wait for script load if needed (though position at end of body usually avoids this)
931
  window.addEventListener('load', loadPDF);
932
  }
933
  </script>
934
  </body>
935
- </html>
 
2
  <html lang="en" dir="ltr">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
6
  <title>{{ pdf_title }}</title>
7
  <!-- Bootstrap Icons -->
8
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
9
+ <!-- Inline Styles -->
10
  <style>
11
  /* Mozilla PDF.js Viewer Visual Replication */
12
  :root {
 
27
  * {
28
  box-sizing: border-box;
29
  user-select: none;
30
+ -webkit-user-select: none;
31
  }
32
 
33
  body {
 
41
  display: flex;
42
  flex-direction: column;
43
  color: var(--text-color);
44
+ /* Prevent native pull-to-refresh and other touch behaviors */
45
+ overscroll-behavior: none;
46
  }
47
 
48
  #outerContainer {
 
70
  flex: 1;
71
  overflow-y: auto;
72
  padding: 10px;
73
+ position: relative;
74
  }
75
 
76
  #sidebarContent::-webkit-scrollbar { width: 10px; }
 
100
 
101
  .thumbnail-canvas {
102
  display: block;
103
+ width: 100px;
104
  height: auto;
105
  }
106
 
 
198
  right: 0; bottom: 0; left: 0;
199
  background-color: var(--body-bg);
200
  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=');
201
+ /* Important for touch handling */
202
+ touch-action: pan-x pan-y;
203
+ -webkit-overflow-scrolling: touch;
204
  }
205
 
206
  #viewerContainer::-webkit-scrollbar { width: 14px; height: 14px; }
 
214
  align-items: center;
215
  padding: 20px 0;
216
  position: relative;
217
+ /* Enhance hardware acceleration for smoother pinch */
218
+ will-change: transform;
219
+ transform-origin: top center;
220
  }
221
 
222
  .page {
 
255
  }
256
 
257
  .toast {
258
+ background-color: #2b3035;
259
  color: #fff;
260
  border-radius: 0.375rem;
261
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
 
475
  toast: document.getElementById('cacheToast')
476
  };
477
 
478
+ // ==========================================
479
+ // PINCH TO ZOOM MODULE
480
+ // ==========================================
481
+ const PinchZoom = {
482
+ state: {
483
+ active: false,
484
+ startDist: 0,
485
+ startScale: 1,
486
+ lastRatio: 1
487
+ },
488
+
489
+ init() {
490
+ // Attach listeners to the container to capture gestures anywhere on the viewer
491
+ elements.container.addEventListener('touchstart', this.onStart.bind(this), { passive: false });
492
+ elements.container.addEventListener('touchmove', this.onMove.bind(this), { passive: false });
493
+ elements.container.addEventListener('touchend', this.onEnd.bind(this));
494
+ },
495
+
496
+ getDistance(e) {
497
+ return Math.hypot(
498
+ e.touches[0].clientX - e.touches[1].clientX,
499
+ e.touches[0].clientY - e.touches[1].clientY
500
+ );
501
+ },
502
+
503
+ getCenter(e) {
504
+ return {
505
+ x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
506
+ y: (e.touches[0].clientY + e.touches[1].clientY) / 2
507
+ };
508
+ },
509
+
510
+ onStart(e) {
511
+ if (e.touches.length === 2) {
512
+ e.preventDefault(); // Prevent native browser zoom/pan
513
+ this.state.active = true;
514
+ this.state.startDist = this.getDistance(e);
515
+ this.state.startScale = config.scale;
516
+ this.state.lastRatio = 1;
517
+
518
+ // Calculate transform origin based on finger position relative to viewer
519
+ const center = this.getCenter(e);
520
+ const rect = elements.viewer.getBoundingClientRect();
521
+
522
+ // The viewer might be scrolled, getBoundingClientRect handles that
523
+ const originX = center.x - rect.left;
524
+ const originY = center.y - rect.top;
525
+
526
+ // Set transform origin so we zoom into the space between fingers
527
+ elements.viewer.style.transformOrigin = `${originX}px ${originY}px`;
528
+ elements.viewer.style.transition = 'none';
529
+ }
530
+ },
531
+
532
+ onMove(e) {
533
+ if (this.state.active && e.touches.length === 2) {
534
+ e.preventDefault();
535
+ const dist = this.getDistance(e);
536
+ const ratio = dist / this.state.startDist;
537
+ this.state.lastRatio = ratio;
538
+
539
+ // Apply temporary visual scale using CSS transform
540
+ // This is performant (60fps) compared to re-rendering canvas
541
+ elements.viewer.style.transform = `scale(${ratio})`;
542
+ }
543
+ },
544
+
545
+ onEnd(e) {
546
+ if (this.state.active && e.touches.length < 2) {
547
+ this.state.active = false;
548
+
549
+ // Calculate new final scale
550
+ let newScale = this.state.startScale * this.state.lastRatio;
551
+
552
+ // Clamp scale to reasonable limits (0.25x to 5.0x)
553
+ newScale = Math.max(0.25, Math.min(newScale, 5.0));
554
+
555
+ // Reset CSS transform
556
+ elements.viewer.style.transform = 'none';
557
+ elements.viewer.style.transformOrigin = 'center top';
558
+
559
+ // Apply new scale to config
560
+ config.scale = newScale;
561
+ config.currentScaleValue = newScale.toString();
562
+
563
+ // Update UI Dropdown
564
+ this.updateScaleDropdown(newScale);
565
+
566
+ // Re-render PDF at new resolution (crisp text)
567
+ initViewer();
568
+ }
569
+ },
570
+
571
+ updateScaleDropdown(scaleVal) {
572
+ // Check if value exists in dropdown
573
+ const rounded = Math.round(scaleVal * 100) / 100;
574
+ let exists = false;
575
+
576
+ // Try to find exact match
577
+ for(let option of elements.scaleSelect.options) {
578
+ if (Math.abs(parseFloat(option.value) - scaleVal) < 0.05) {
579
+ elements.scaleSelect.value = option.value;
580
+ exists = true;
581
+ break;
582
+ }
583
+ }
584
+
585
+ // If no match, add custom option
586
+ if (!exists) {
587
+ const opt = document.createElement('option');
588
+ opt.value = scaleVal;
589
+ opt.textContent = `${Math.round(scaleVal * 100)}%`;
590
+ elements.scaleSelect.appendChild(opt);
591
+ elements.scaleSelect.value = scaleVal;
592
+ }
593
+ }
594
+ };
595
+
596
  // ==========================================
597
  // INDEXED DB CACHE
598
  // ==========================================
 
627
 
628
  if (!record) return null;
629
 
 
630
  const data = record.data || record;
 
 
631
  if (record.data) {
632
  this.touch(url, record);
633
  }
 
654
  const tx = db.transaction(this.storeName, 'readwrite');
655
  const store = tx.objectStore(this.storeName);
656
 
 
657
  let currentSize = 0;
658
  const entries = [];
659
 
 
662
  req.onsuccess = (e) => {
663
  const cursor = e.target.result;
664
  if (cursor) {
 
665
  if (cursor.key !== url) {
666
  const val = cursor.value;
667
  const itemSize = (val.data || val).byteLength || 0;
668
+ const itemTime = val.timestamp || 0;
669
  currentSize += itemSize;
670
  entries.push({ key: cursor.key, size: itemSize, timestamp: itemTime });
671
  }
 
677
  req.onerror = () => resolve();
678
  });
679
 
 
680
  if (currentSize + newSize > this.MAX_TOTAL_SIZE) {
 
681
  entries.sort((a, b) => a.timestamp - b.timestamp);
 
682
  while (entries.length > 0 && (currentSize + newSize > this.MAX_TOTAL_SIZE)) {
683
  const entry = entries.shift();
684
  store.delete(entry.key);
 
687
  }
688
  }
689
 
 
690
  const record = { data: data, timestamp: Date.now() };
691
  store.put(record, url);
692
 
 
710
  elements.toast.classList.remove('show');
711
  }
712
 
713
+ window.hideToast = hideToast;
714
 
715
  // ==========================================
716
  // LOADING
 
727
  let isCached = !!data;
728
 
729
  if (isCached) {
730
+ showToast();
731
  }
732
 
 
733
  const params = {
734
  cMapUrl: 'https://cdn.jsdelivr.net/npm/pdf.js@3.11.174/dist/cmaps/',
735
  cMapPacked: true,
 
752
  state.pdfDoc = await loadingTask.promise;
753
 
754
  if (!isCached) {
 
755
  state.pdfDoc.getData().then(pdfData => {
 
756
  if (pdfData.byteLength <= 100 * 1024 * 1024) {
757
  PDFCache.put(config.url, pdfData);
758
  } else {
 
779
  elements.viewer.innerHTML = '';
780
  state.pages = [];
781
 
782
+ // Get last viewed page for this PDF to restore scroll position
783
+ // If just loaded, use storage. If zoomed, use current state.pageNum
784
+ const lastPage = state.pageNum > 1 ? state.pageNum : PageMemory.getLastPage(config.pdfIdentifier);
785
 
786
  for (let i = 1; i <= state.pdfDoc.numPages; i++) {
787
  const pageDiv = document.createElement('div');
 
789
  pageDiv.setAttribute('data-page-number', i);
790
  pageDiv.id = `pageContainer${i}`;
791
 
792
+ // Initial size guess (letter) until rendered
793
  pageDiv.style.width = '612px';
794
  pageDiv.style.height = '792px';
795
 
 
798
  }
799
 
800
  setupIntersectionObserver();
 
801
 
802
+ if (!state.thumbnailsRendered) {
803
+ generateThumbnails();
804
+ state.thumbnailsRendered = true;
805
+ }
806
+
807
+ // Restore scroll position
808
  if (lastPage > 1) {
809
  setTimeout(() => scrollToPage(lastPage), 100);
810
  }
 
837
  if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
838
  const pageNum = parseInt(entry.target.getAttribute('data-page-number'));
839
  updateUIState(pageNum);
 
840
  PageMemory.saveLastPage(config.pdfIdentifier, pageNum);
841
  }
842
  });
 
903
  imgContainer.className = 'thumbnail-image-container';
904
  imgContainer.id = `thumb-img-${i}`;
905
 
 
906
  imgContainer.style.width = '100px';
907
  imgContainer.style.height = '130px';
908
 
 
915
  elements.sidebarContent.appendChild(thumbContainer);
916
  }
917
 
 
 
918
  renderThumbnailsQueue();
919
  }
920
 
921
  async function renderThumbnailsQueue() {
922
  for (let i = 1; i <= state.pdfDoc.numPages; i++) {
 
923
  await renderSingleThumbnail(i);
924
  }
925
  }
 
927
  async function renderSingleThumbnail(i) {
928
  try {
929
  const page = await state.pdfDoc.getPage(i);
930
+ const viewport = page.getViewport({ scale: 0.2 });
931
  const canvas = document.createElement('canvas');
932
  canvas.className = 'thumbnail-canvas';
933
  canvas.height = viewport.height;
 
953
  if (num < 1 || num > state.pdfDoc.numPages) return;
954
  const pageDiv = document.getElementById(`pageContainer${num}`);
955
  if (pageDiv) {
 
 
956
  elements.container.scrollTop = pageDiv.offsetTop;
957
  }
958
  }
 
967
  if (thumb) {
968
  thumb.classList.add('selected');
969
 
 
970
  const thumbTop = thumb.offsetTop;
971
  const thumbHeight = thumb.clientHeight;
972
  const containerHeight = elements.sidebarContent.clientHeight;
973
  const containerScroll = elements.sidebarContent.scrollTop;
974
 
 
975
  if (thumbTop < containerScroll) {
976
  elements.sidebarContent.scrollTop = thumbTop - 10;
977
  }
 
978
  else if (thumbTop + thumbHeight > containerScroll + containerHeight) {
979
  elements.sidebarContent.scrollTop = thumbTop + thumbHeight - containerHeight + 10;
980
  }
 
1006
  });
1007
 
1008
  if (elements.zoomIn) elements.zoomIn.addEventListener('click', () => {
1009
+ let current = parseFloat(elements.scaleSelect.value);
1010
+ if(isNaN(current)) current = config.scale;
1011
+ const newScale = (Math.floor(current * 4) + 1) / 4; // Step to next 0.25 increment
1012
+
1013
+ // Check if current was page-fit or similar
1014
+ if(isNaN(parseFloat(elements.scaleSelect.value))) {
1015
+ config.scale = config.scale + 0.25;
1016
+ } else {
1017
+ config.scale = newScale;
1018
+ }
1019
+
1020
  config.currentScaleValue = config.scale.toString();
1021
+
1022
+ // Sync Dropdown
1023
+ PinchZoom.updateScaleDropdown(config.scale);
1024
  initViewer();
1025
  });
1026
 
1027
  if (elements.zoomOut) elements.zoomOut.addEventListener('click', () => {
1028
+ if (config.scale <= 0.25) return;
1029
+
1030
+ let current = parseFloat(elements.scaleSelect.value);
1031
+ if(isNaN(current)) current = config.scale;
1032
+ const newScale = (Math.ceil(current * 4) - 1) / 4; // Step to prev 0.25 increment
1033
+
1034
+ if(isNaN(parseFloat(elements.scaleSelect.value))) {
1035
+ config.scale = Math.max(0.25, config.scale - 0.25);
1036
+ } else {
1037
+ config.scale = Math.max(0.25, newScale);
1038
+ }
1039
+
1040
  config.currentScaleValue = config.scale.toString();
1041
+
1042
+ // Sync Dropdown
1043
+ PinchZoom.updateScaleDropdown(config.scale);
1044
  initViewer();
1045
  });
1046
 
 
1057
  link.click();
1058
  });
1059
 
1060
+ // Initialize Pinch Zoom
1061
+ PinchZoom.init();
1062
+
1063
  // Start loading
1064
  if (typeof pdfjsLib !== 'undefined') {
1065
  loadPDF();
1066
  } else {
 
1067
  window.addEventListener('load', loadPDF);
1068
  }
1069
  </script>
1070
  </body>
1071
+ </html>