beta3 commited on
Commit
5fc0d7f
Β·
verified Β·
1 Parent(s): 4769cb1

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +334 -194
index.html CHANGED
@@ -137,14 +137,8 @@
137
  }
138
 
139
  @keyframes fadeInUp {
140
- from {
141
- opacity: 0;
142
- transform: translateY(30px);
143
- }
144
- to {
145
- opacity: 1;
146
- transform: translateY(0);
147
- }
148
  }
149
 
150
  .container {
@@ -219,9 +213,7 @@
219
  display: inline-block;
220
  }
221
 
222
- input[type="file"] {
223
- display: none;
224
- }
225
 
226
  .upload-btn {
227
  background: var(--color-primary);
@@ -245,15 +237,12 @@
245
  width: 0;
246
  height: 0;
247
  border-radius: 50%;
248
- background: rgba(255, 255, 255, 0.1);
249
  transform: translate(-50%, -50%);
250
  transition: width 0.6s, height 0.6s;
251
  }
252
 
253
- .upload-btn:hover::before {
254
- width: 300px;
255
- height: 300px;
256
- }
257
 
258
  .upload-btn:hover {
259
  transform: translateY(-2px);
@@ -276,13 +265,136 @@
276
  animation: fadeInUp 0.8s ease-out;
277
  }
278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  .canvas-wrapper {
280
  position: relative;
281
  display: inline-block;
282
- margin: 32px auto;
283
- border-radius: 12px;
284
  overflow: hidden;
285
- box-shadow: 0 8px 32px rgba(15, 23, 42, 0.1);
286
  }
287
 
288
  canvas {
@@ -298,20 +410,20 @@
298
  backdrop-filter: blur(2px);
299
  }
300
 
301
- .instructions {
302
- background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
303
- padding: 24px;
304
- border-radius: 16px;
305
- margin-bottom: 32px;
306
- font-size: 15px;
307
  color: var(--color-text-muted);
308
- border-left: 3px solid var(--color-primary);
309
- line-height: 1.6;
 
 
 
310
  }
311
 
312
- .instructions strong {
313
- color: var(--color-text);
314
- font-weight: 600;
315
  }
316
 
317
  .button-group {
@@ -423,12 +535,10 @@
423
  100% { transform: rotate(360deg); }
424
  }
425
 
 
426
  .modal-overlay {
427
  position: fixed;
428
- top: 0;
429
- left: 0;
430
- right: 0;
431
- bottom: 0;
432
  background: rgba(15, 23, 42, 0.6);
433
  backdrop-filter: blur(8px);
434
  z-index: 1000;
@@ -438,9 +548,7 @@
438
  animation: fadeIn 0.3s ease-out;
439
  }
440
 
441
- .modal-overlay.active {
442
- display: flex;
443
- }
444
 
445
  @keyframes fadeIn {
446
  from { opacity: 0; }
@@ -456,30 +564,21 @@
456
  width: 90%;
457
  box-shadow: 0 24px 80px rgba(15, 23, 42, 0.2);
458
  animation: scaleIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
459
- position: relative;
460
  }
461
 
462
  @keyframes scaleIn {
463
- from {
464
- opacity: 0;
465
- transform: scale(0.9);
466
- }
467
- to {
468
- opacity: 1;
469
- transform: scale(1);
470
- }
471
  }
472
 
473
  .modal-icon {
474
  width: 80px;
475
  height: 80px;
476
  margin: 0 auto 24px;
477
- position: relative;
478
  }
479
 
480
  .processing-spinner {
481
- width: 80px;
482
- height: 80px;
483
  border: 3px solid var(--color-border);
484
  border-top: 3px solid var(--color-primary);
485
  border-radius: 50%;
@@ -487,8 +586,7 @@
487
  }
488
 
489
  .success-icon {
490
- width: 80px;
491
- height: 80px;
492
  border-radius: 50%;
493
  background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
494
  display: flex;
@@ -505,17 +603,9 @@
505
  }
506
 
507
  @keyframes successPop {
508
- 0% {
509
- transform: scale(0);
510
- opacity: 0;
511
- }
512
- 50% {
513
- transform: scale(1.1);
514
- }
515
- 100% {
516
- transform: scale(1);
517
- opacity: 1;
518
- }
519
  }
520
 
521
  .modal-title {
@@ -550,18 +640,9 @@
550
  }
551
 
552
  @keyframes progressFlow {
553
- 0% {
554
- width: 0%;
555
- opacity: 1;
556
- }
557
- 50% {
558
- width: 100%;
559
- opacity: 1;
560
- }
561
- 100% {
562
- width: 100%;
563
- opacity: 0;
564
- }
565
  }
566
 
567
  .modal-btn {
@@ -597,25 +678,12 @@
597
  }
598
 
599
  @media (max-width: 768px) {
600
- h1 {
601
- font-size: 36px;
602
- }
603
-
604
- .container {
605
- padding: 40px 20px;
606
- }
607
-
608
- .upload-section {
609
- padding: 60px 32px;
610
- }
611
-
612
- .canvas-section {
613
- padding: 32px 24px;
614
- }
615
-
616
- .button-group {
617
- flex-direction: column;
618
- }
619
  }
620
  </style>
621
  </head>
@@ -649,13 +717,35 @@
649
 
650
  <div class="canvas-section" id="canvasSection">
651
  <div class="instructions">
652
- <strong>Instructions:</strong> Click and drag on the first page to select the area you want to crop. This area will be applied to all pages of the document.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  </div>
654
-
655
- <div style="text-align: center;">
656
- <div class="canvas-wrapper" id="canvasWrapper">
657
- <canvas id="pdfCanvas"></canvas>
658
- <div class="selection-box" id="selectionBox"></div>
 
 
 
659
  </div>
660
  </div>
661
 
@@ -699,6 +789,18 @@
699
  let startX, startY, endX, endY;
700
  let selection = null;
701
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  document.getElementById('pdfInput').addEventListener('change', handleFileSelect);
703
  canvas.addEventListener('mousedown', handleMouseDown);
704
  canvas.addEventListener('mousemove', handleMouseMove);
@@ -708,25 +810,35 @@
708
  document.getElementById('downloadBtn').addEventListener('click', downloadPDF);
709
  document.getElementById('startOverBtn').addEventListener('click', startOver);
710
  document.getElementById('newDocBtn').addEventListener('click', startOver);
 
 
 
711
 
 
712
  async function handleFileSelect(e) {
713
  const file = e.target.files[0];
714
  if (!file) return;
715
-
716
  try {
717
  const originalBuffer = await file.arrayBuffer();
718
-
719
  pdfBytes = new Uint8Array(originalBuffer.slice(0));
720
-
721
  pdfArrayBuffer = originalBuffer.slice(0);
722
-
723
- const loadingTask = pdfjsLib.getDocument({data: pdfBytes});
724
  pdfDoc = await loadingTask.promise;
725
-
 
 
 
 
 
 
 
 
726
  await renderFirstPage();
727
-
728
  document.getElementById('uploadSection').style.display = 'none';
729
  document.getElementById('canvasSection').style.display = 'block';
 
730
  showStatus('PDF loaded successfully. Select the area to crop.', 'success');
731
  } catch (error) {
732
  showStatus('Error loading PDF: ' + error.message, 'error');
@@ -735,49 +847,96 @@
735
 
736
  async function renderFirstPage() {
737
  const page = await pdfDoc.getPage(1);
738
- const viewport = page.getViewport({ scale: 1.5 });
739
-
740
- canvas.width = viewport.width;
741
  canvas.height = viewport.height;
742
-
743
- await page.render({
744
- canvasContext: ctx,
745
- viewport: viewport
746
- }).promise;
 
747
  }
748
 
749
- function handleMouseDown(e) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
  const rect = canvas.getBoundingClientRect();
751
- startX = e.clientX - rect.left;
752
- startY = e.clientY - rect.top;
 
 
 
 
 
 
 
 
 
 
753
  isSelecting = true;
754
  }
755
 
756
  function handleMouseMove(e) {
757
  if (!isSelecting) return;
758
-
759
- const rect = canvas.getBoundingClientRect();
760
- endX = e.clientX - rect.left;
761
- endY = e.clientY - rect.top;
762
-
763
  updateSelectionBox();
764
  }
765
 
766
  function handleMouseUp(e) {
767
  if (!isSelecting) return;
768
-
769
  isSelecting = false;
770
- const rect = canvas.getBoundingClientRect();
771
- endX = e.clientX - rect.left;
772
- endY = e.clientY - rect.top;
773
-
774
- const x = Math.min(startX, endX);
775
- const y = Math.min(startY, endY);
776
- const width = Math.abs(endX - startX);
777
- const height = Math.abs(endY - startY);
778
-
779
- if (width > 5 && height > 5) {
780
- selection = { x, y, width, height };
 
 
 
781
  updateSelectionBox();
782
  document.getElementById('cropBtn').disabled = false;
783
  }
@@ -787,13 +946,10 @@
787
  const box = document.getElementById('selectionBox');
788
  const x = Math.min(startX, endX);
789
  const y = Math.min(startY, endY);
790
- const width = Math.abs(endX - startX);
791
- const height = Math.abs(endY - startY);
792
-
793
- box.style.left = x + 'px';
794
- box.style.top = y + 'px';
795
- box.style.width = width + 'px';
796
- box.style.height = height + 'px';
797
  box.style.display = 'block';
798
  }
799
 
@@ -803,54 +959,41 @@
803
  document.getElementById('cropBtn').disabled = true;
804
  }
805
 
 
806
  async function processPDF() {
807
  if (!selection || !pdfArrayBuffer) return;
808
-
809
  try {
810
  showModal();
811
  document.getElementById('cropBtn').disabled = true;
812
 
813
- const page = await pdfDoc.getPage(1);
814
- const viewport = page.getViewport({ scale: 1.5 });
815
-
 
816
  const scaleX = page.view[2] / viewport.width;
817
  const scaleY = page.view[3] / viewport.height;
818
-
819
  const cropBox = {
820
- x: selection.x * scaleX,
821
- y: (viewport.height - selection.y - selection.height) * scaleY,
822
- width: selection.width * scaleX,
823
  height: selection.height * scaleY
824
  };
825
 
826
  const pdfLibDoc = await PDFLib.PDFDocument.load(pdfArrayBuffer);
827
- const newPdf = await PDFLib.PDFDocument.create();
828
-
829
- const numPages = pdfLibDoc.getPageCount();
830
-
831
  for (let i = 0; i < numPages; i++) {
832
  const [croppedPage] = await newPdf.copyPages(pdfLibDoc, [i]);
833
-
834
- croppedPage.setCropBox(
835
- cropBox.x,
836
- cropBox.y,
837
- cropBox.width,
838
- cropBox.height
839
- );
840
-
841
- croppedPage.setMediaBox(
842
- cropBox.x,
843
- cropBox.y,
844
- cropBox.width,
845
- cropBox.height
846
- );
847
-
848
  newPdf.addPage(croppedPage);
849
  }
850
-
851
  const pdfBytesOutput = await newPdf.save();
852
  processedPdfBlob = new Blob([pdfBytesOutput], { type: 'application/pdf' });
853
-
854
  showSuccess();
855
  document.getElementById('cropBtn').disabled = false;
856
  } catch (error) {
@@ -860,29 +1003,29 @@
860
  }
861
  }
862
 
 
863
  function showModal() {
864
  document.getElementById('modalOverlay').classList.add('active');
865
  document.getElementById('modalIcon').innerHTML = '<div class="processing-spinner"></div>';
866
  document.getElementById('modalTitle').textContent = 'Processing PDF';
867
- document.getElementById('modalText').textContent = 'Please wait while we process your document...';
868
- document.getElementById('progressBar').style.display = 'block';
869
  document.getElementById('modalButtons').style.display = 'none';
870
  }
871
 
872
  function showSuccess() {
873
  document.getElementById('modalIcon').innerHTML = '<div class="success-icon"></div>';
874
  document.getElementById('modalTitle').textContent = 'PDF Processed';
875
- document.getElementById('modalText').textContent = 'Your document has been processed successfully and is ready to download.';
876
- document.getElementById('progressBar').style.display = 'none';
877
  document.getElementById('modalButtons').style.display = 'block';
878
  }
879
 
880
  function downloadPDF() {
881
  if (!processedPdfBlob) return;
882
-
883
  const url = URL.createObjectURL(processedPdfBlob);
884
- const a = document.createElement('a');
885
- a.href = url;
886
  a.download = 'cropped_document.pdf';
887
  a.click();
888
  URL.revokeObjectURL(url);
@@ -892,35 +1035,32 @@
892
  document.getElementById('modalOverlay').classList.remove('active');
893
  }
894
 
 
895
  function startOver() {
896
- pdfDoc = null;
897
- pdfBytes = null;
898
- pdfArrayBuffer = null;
899
- processedPdfBlob = null;
900
- selection = null;
901
-
902
  document.getElementById('pdfInput').value = '';
903
  document.getElementById('selectionBox').style.display = 'none';
904
  document.getElementById('cropBtn').disabled = true;
905
  document.getElementById('status').style.display = 'none';
906
-
 
 
907
  ctx.clearRect(0, 0, canvas.width, canvas.height);
908
-
909
- document.getElementById('canvasSection').style.display = 'none';
910
- document.getElementById('uploadSection').style.display = 'block';
911
-
912
  closeModal();
913
  }
914
 
 
915
  function showStatus(message, type) {
916
  const status = document.getElementById('status');
917
  status.className = 'status ' + type;
918
-
919
- if (type === 'processing') {
920
- status.innerHTML = '<span class="loader"></span>' + message;
921
- } else {
922
- status.textContent = message;
923
- }
924
  }
925
  </script>
926
  </body>
 
137
  }
138
 
139
  @keyframes fadeInUp {
140
+ from { opacity: 0; transform: translateY(30px); }
141
+ to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
142
  }
143
 
144
  .container {
 
213
  display: inline-block;
214
  }
215
 
216
+ input[type="file"] { display: none; }
 
 
217
 
218
  .upload-btn {
219
  background: var(--color-primary);
 
237
  width: 0;
238
  height: 0;
239
  border-radius: 50%;
240
+ background: rgba(255,255,255,0.1);
241
  transform: translate(-50%, -50%);
242
  transition: width 0.6s, height 0.6s;
243
  }
244
 
245
+ .upload-btn:hover::before { width: 300px; height: 300px; }
 
 
 
246
 
247
  .upload-btn:hover {
248
  transform: translateY(-2px);
 
265
  animation: fadeInUp 0.8s ease-out;
266
  }
267
 
268
+ .instructions {
269
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
270
+ padding: 24px;
271
+ border-radius: 16px;
272
+ margin-bottom: 24px;
273
+ font-size: 15px;
274
+ color: var(--color-text-muted);
275
+ border-left: 3px solid var(--color-primary);
276
+ line-height: 1.6;
277
+ }
278
+
279
+ .instructions strong {
280
+ color: var(--color-text);
281
+ font-weight: 600;
282
+ }
283
+
284
+ /* ─── Zoom controls ─── */
285
+ .zoom-bar {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 12px;
289
+ justify-content: center;
290
+ margin-bottom: 16px;
291
+ flex-wrap: wrap;
292
+ }
293
+
294
+ .zoom-label {
295
+ font-size: 13px;
296
+ color: var(--color-text-muted);
297
+ font-weight: 500;
298
+ }
299
+
300
+ .zoom-btn {
301
+ width: 36px;
302
+ height: 36px;
303
+ border: 2px solid var(--color-border);
304
+ background: var(--color-surface);
305
+ border-radius: 10px;
306
+ cursor: pointer;
307
+ font-size: 18px;
308
+ line-height: 1;
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ transition: all 0.2s;
313
+ color: var(--color-text);
314
+ }
315
+
316
+ .zoom-btn:hover {
317
+ border-color: var(--color-accent);
318
+ background: var(--color-bg);
319
+ transform: translateY(-1px);
320
+ }
321
+
322
+ .zoom-value {
323
+ font-size: 14px;
324
+ font-weight: 600;
325
+ color: var(--color-text);
326
+ min-width: 52px;
327
+ text-align: center;
328
+ background: var(--color-bg);
329
+ border: 2px solid var(--color-border);
330
+ border-radius: 10px;
331
+ padding: 6px 10px;
332
+ }
333
+
334
+ .zoom-reset-btn {
335
+ font-size: 13px;
336
+ padding: 6px 14px;
337
+ border: 2px solid var(--color-border);
338
+ background: var(--color-surface);
339
+ border-radius: 10px;
340
+ cursor: pointer;
341
+ color: var(--color-text-muted);
342
+ font-weight: 500;
343
+ transition: all 0.2s;
344
+ font-family: 'DM Sans', sans-serif;
345
+ }
346
+
347
+ .zoom-reset-btn:hover {
348
+ border-color: var(--color-accent);
349
+ color: var(--color-text);
350
+ }
351
+
352
+ /* ─── Scrollable canvas viewport ─── */
353
+ .canvas-viewport {
354
+ width: 100%;
355
+ max-height: 600px;
356
+ overflow: auto;
357
+ border-radius: 16px;
358
+ border: 1px solid var(--color-border);
359
+ background: #e8ecf0;
360
+ margin: 0 auto 8px;
361
+ /* Custom scrollbar */
362
+ scrollbar-width: thin;
363
+ scrollbar-color: #94a3b8 transparent;
364
+ }
365
+
366
+ .canvas-viewport::-webkit-scrollbar {
367
+ width: 8px;
368
+ height: 8px;
369
+ }
370
+
371
+ .canvas-viewport::-webkit-scrollbar-track {
372
+ background: transparent;
373
+ }
374
+
375
+ .canvas-viewport::-webkit-scrollbar-thumb {
376
+ background: #94a3b8;
377
+ border-radius: 4px;
378
+ }
379
+
380
+ .canvas-viewport::-webkit-scrollbar-corner {
381
+ background: transparent;
382
+ }
383
+
384
+ /* Inner padding so canvas doesn't kiss the edges */
385
+ .canvas-inner {
386
+ display: inline-block;
387
+ padding: 24px;
388
+ min-width: 100%;
389
+ text-align: center;
390
+ }
391
+
392
  .canvas-wrapper {
393
  position: relative;
394
  display: inline-block;
395
+ border-radius: 8px;
 
396
  overflow: hidden;
397
+ box-shadow: 0 8px 32px rgba(15, 23, 42, 0.15);
398
  }
399
 
400
  canvas {
 
410
  backdrop-filter: blur(2px);
411
  }
412
 
413
+ /* Scroll hint badge */
414
+ .scroll-hint {
415
+ text-align: center;
416
+ font-size: 12px;
 
 
417
  color: var(--color-text-muted);
418
+ margin-bottom: 20px;
419
+ display: none;
420
+ gap: 6px;
421
+ align-items: center;
422
+ justify-content: center;
423
  }
424
 
425
+ .scroll-hint svg {
426
+ opacity: 0.5;
 
427
  }
428
 
429
  .button-group {
 
535
  100% { transform: rotate(360deg); }
536
  }
537
 
538
+ /* ─── Modal ─── */
539
  .modal-overlay {
540
  position: fixed;
541
+ top: 0; left: 0; right: 0; bottom: 0;
 
 
 
542
  background: rgba(15, 23, 42, 0.6);
543
  backdrop-filter: blur(8px);
544
  z-index: 1000;
 
548
  animation: fadeIn 0.3s ease-out;
549
  }
550
 
551
+ .modal-overlay.active { display: flex; }
 
 
552
 
553
  @keyframes fadeIn {
554
  from { opacity: 0; }
 
564
  width: 90%;
565
  box-shadow: 0 24px 80px rgba(15, 23, 42, 0.2);
566
  animation: scaleIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
 
567
  }
568
 
569
  @keyframes scaleIn {
570
+ from { opacity: 0; transform: scale(0.9); }
571
+ to { opacity: 1; transform: scale(1); }
 
 
 
 
 
 
572
  }
573
 
574
  .modal-icon {
575
  width: 80px;
576
  height: 80px;
577
  margin: 0 auto 24px;
 
578
  }
579
 
580
  .processing-spinner {
581
+ width: 80px; height: 80px;
 
582
  border: 3px solid var(--color-border);
583
  border-top: 3px solid var(--color-primary);
584
  border-radius: 50%;
 
586
  }
587
 
588
  .success-icon {
589
+ width: 80px; height: 80px;
 
590
  border-radius: 50%;
591
  background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
592
  display: flex;
 
603
  }
604
 
605
  @keyframes successPop {
606
+ 0% { transform: scale(0); opacity: 0; }
607
+ 50% { transform: scale(1.1); }
608
+ 100% { transform: scale(1); opacity: 1; }
 
 
 
 
 
 
 
 
609
  }
610
 
611
  .modal-title {
 
640
  }
641
 
642
  @keyframes progressFlow {
643
+ 0% { width: 0%; opacity: 1; }
644
+ 50% { width: 100%; opacity: 1; }
645
+ 100% { width: 100%; opacity: 0; }
 
 
 
 
 
 
 
 
 
646
  }
647
 
648
  .modal-btn {
 
678
  }
679
 
680
  @media (max-width: 768px) {
681
+ h1 { font-size: 36px; }
682
+ .container { padding: 40px 20px; }
683
+ .upload-section { padding: 60px 32px; }
684
+ .canvas-section { padding: 32px 16px; }
685
+ .button-group { flex-direction: column; }
686
+ .canvas-viewport { max-height: 450px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
687
  }
688
  </style>
689
  </head>
 
717
 
718
  <div class="canvas-section" id="canvasSection">
719
  <div class="instructions">
720
+ <strong>Instructions:</strong> Click and drag on the first page to select the area you want to crop.
721
+ Use the zoom controls or scroll/pan to navigate large documents.
722
+ The selection will be applied to all pages.
723
+ </div>
724
+
725
+ <!-- Zoom controls -->
726
+ <div class="zoom-bar">
727
+ <span class="zoom-label">Zoom</span>
728
+ <button class="zoom-btn" id="zoomOutBtn" title="Zoom out">βˆ’</button>
729
+ <span class="zoom-value" id="zoomValue">100%</span>
730
+ <button class="zoom-btn" id="zoomInBtn" title="Zoom in">+</button>
731
+ <button class="zoom-reset-btn" id="zoomResetBtn">Fit to view</button>
732
+ </div>
733
+
734
+ <!-- Scroll hint (shown only when content overflows) -->
735
+ <div class="scroll-hint" id="scrollHint">
736
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
737
+ <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
738
+ </svg>
739
+ Scroll inside the preview area to navigate the document
740
  </div>
741
+
742
+ <!-- Scrollable viewport -->
743
+ <div class="canvas-viewport" id="canvasViewport">
744
+ <div class="canvas-inner">
745
+ <div class="canvas-wrapper" id="canvasWrapper">
746
+ <canvas id="pdfCanvas"></canvas>
747
+ <div class="selection-box" id="selectionBox"></div>
748
+ </div>
749
  </div>
750
  </div>
751
 
 
789
  let startX, startY, endX, endY;
790
  let selection = null;
791
 
792
+ // Zoom state
793
+ let currentScale = 1.5; // pdf.js render scale
794
+ let baseScale = 1.5; // "fit-to-view" scale computed on load
795
+ const ZOOM_STEP = 0.25;
796
+ const MIN_SCALE = 0.5;
797
+ const MAX_SCALE = 5;
798
+
799
+ // ─── Viewport dimensions used for fit-to-view ───────────────────────
800
+ const VIEWPORT_MAX_WIDTH = 1000; // matches canvas-section max inner width
801
+ const VIEWPORT_MAX_HEIGHT = 600; // matches max-height of canvas-viewport
802
+
803
+ // ─── Event listeners ─────────────────────────────────────────────────
804
  document.getElementById('pdfInput').addEventListener('change', handleFileSelect);
805
  canvas.addEventListener('mousedown', handleMouseDown);
806
  canvas.addEventListener('mousemove', handleMouseMove);
 
810
  document.getElementById('downloadBtn').addEventListener('click', downloadPDF);
811
  document.getElementById('startOverBtn').addEventListener('click', startOver);
812
  document.getElementById('newDocBtn').addEventListener('click', startOver);
813
+ document.getElementById('zoomInBtn').addEventListener('click', () => zoom(currentScale + ZOOM_STEP));
814
+ document.getElementById('zoomOutBtn').addEventListener('click', () => zoom(currentScale - ZOOM_STEP));
815
+ document.getElementById('zoomResetBtn').addEventListener('click', () => zoom(baseScale));
816
 
817
+ // ─── File load ────────────────────────────────────────────────────────
818
  async function handleFileSelect(e) {
819
  const file = e.target.files[0];
820
  if (!file) return;
 
821
  try {
822
  const originalBuffer = await file.arrayBuffer();
 
823
  pdfBytes = new Uint8Array(originalBuffer.slice(0));
 
824
  pdfArrayBuffer = originalBuffer.slice(0);
825
+
826
+ const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
827
  pdfDoc = await loadingTask.promise;
828
+
829
+ // Compute a sensible initial scale so the PDF fits in the viewport
830
+ const page = await pdfDoc.getPage(1);
831
+ const rawVP = page.getViewport({ scale: 1 });
832
+ const scaleW = VIEWPORT_MAX_WIDTH / rawVP.width;
833
+ const scaleH = VIEWPORT_MAX_HEIGHT / rawVP.height;
834
+ baseScale = Math.min(scaleW, scaleH, 1.5); // never zoom in beyond 1.5Γ— on load
835
+ currentScale = baseScale;
836
+
837
  await renderFirstPage();
838
+
839
  document.getElementById('uploadSection').style.display = 'none';
840
  document.getElementById('canvasSection').style.display = 'block';
841
+ checkScrollHint();
842
  showStatus('PDF loaded successfully. Select the area to crop.', 'success');
843
  } catch (error) {
844
  showStatus('Error loading PDF: ' + error.message, 'error');
 
847
 
848
  async function renderFirstPage() {
849
  const page = await pdfDoc.getPage(1);
850
+ const viewport = page.getViewport({ scale: currentScale });
851
+
852
+ canvas.width = viewport.width;
853
  canvas.height = viewport.height;
854
+
855
+ await page.render({ canvasContext: ctx, viewport }).promise;
856
+ updateZoomUI();
857
+
858
+ // Reset selection overlay
859
+ resetSelection();
860
  }
861
 
862
+ // ─── Zoom ─────────────────────────────────────────────────────────────
863
+ async function zoom(newScale) {
864
+ newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale));
865
+ if (Math.abs(newScale - currentScale) < 0.001) return;
866
+
867
+ // Preserve scroll position ratio
868
+ const vp = document.getElementById('canvasViewport');
869
+ const ratioX = (vp.scrollLeft + vp.clientWidth / 2) / vp.scrollWidth;
870
+ const ratioY = (vp.scrollTop + vp.clientHeight / 2) / vp.scrollHeight;
871
+
872
+ currentScale = newScale;
873
+ await renderFirstPage();
874
+
875
+ // Restore scroll position
876
+ requestAnimationFrame(() => {
877
+ vp.scrollLeft = ratioX * vp.scrollWidth - vp.clientWidth / 2;
878
+ vp.scrollTop = ratioY * vp.scrollHeight - vp.clientHeight / 2;
879
+ });
880
+
881
+ checkScrollHint();
882
+ }
883
+
884
+ function updateZoomUI() {
885
+ document.getElementById('zoomValue').textContent =
886
+ Math.round((currentScale / baseScale) * 100) + '%';
887
+ }
888
+
889
+ function checkScrollHint() {
890
+ const vp = document.getElementById('canvasViewport');
891
+ const hint = document.getElementById('scrollHint');
892
+ const overflows = canvas.width > vp.clientWidth ||
893
+ canvas.height > vp.clientHeight;
894
+ hint.style.display = overflows ? 'flex' : 'none';
895
+ }
896
+
897
+ // ─── Mouse selection ─────────────────────────────────────────────────
898
+ function getCanvasPos(e) {
899
  const rect = canvas.getBoundingClientRect();
900
+ return {
901
+ x: e.clientX - rect.left,
902
+ y: e.clientY - rect.top
903
+ };
904
+ }
905
+
906
+ function handleMouseDown(e) {
907
+ const pos = getCanvasPos(e);
908
+ startX = pos.x;
909
+ startY = pos.y;
910
+ endX = pos.x;
911
+ endY = pos.y;
912
  isSelecting = true;
913
  }
914
 
915
  function handleMouseMove(e) {
916
  if (!isSelecting) return;
917
+ const pos = getCanvasPos(e);
918
+ endX = pos.x;
919
+ endY = pos.y;
 
 
920
  updateSelectionBox();
921
  }
922
 
923
  function handleMouseUp(e) {
924
  if (!isSelecting) return;
 
925
  isSelecting = false;
926
+ const pos = getCanvasPos(e);
927
+ endX = pos.x;
928
+ endY = pos.y;
929
+
930
+ const w = Math.abs(endX - startX);
931
+ const h = Math.abs(endY - startY);
932
+
933
+ if (w > 5 && h > 5) {
934
+ selection = {
935
+ x: Math.min(startX, endX),
936
+ y: Math.min(startY, endY),
937
+ width: w,
938
+ height: h
939
+ };
940
  updateSelectionBox();
941
  document.getElementById('cropBtn').disabled = false;
942
  }
 
946
  const box = document.getElementById('selectionBox');
947
  const x = Math.min(startX, endX);
948
  const y = Math.min(startY, endY);
949
+ box.style.left = x + 'px';
950
+ box.style.top = y + 'px';
951
+ box.style.width = Math.abs(endX - startX) + 'px';
952
+ box.style.height = Math.abs(endY - startY) + 'px';
 
 
 
953
  box.style.display = 'block';
954
  }
955
 
 
959
  document.getElementById('cropBtn').disabled = true;
960
  }
961
 
962
+ // ─── PDF processing ───────────────────────────────────────────────────
963
  async function processPDF() {
964
  if (!selection || !pdfArrayBuffer) return;
 
965
  try {
966
  showModal();
967
  document.getElementById('cropBtn').disabled = true;
968
 
969
+ const page = await pdfDoc.getPage(1);
970
+ const viewport = page.getViewport({ scale: currentScale });
971
+
972
+ // Map canvas pixels β†’ PDF units (using the page's native size)
973
  const scaleX = page.view[2] / viewport.width;
974
  const scaleY = page.view[3] / viewport.height;
975
+
976
  const cropBox = {
977
+ x: selection.x * scaleX,
978
+ y: (viewport.height - selection.y - selection.height) * scaleY,
979
+ width: selection.width * scaleX,
980
  height: selection.height * scaleY
981
  };
982
 
983
  const pdfLibDoc = await PDFLib.PDFDocument.load(pdfArrayBuffer);
984
+ const newPdf = await PDFLib.PDFDocument.create();
985
+ const numPages = pdfLibDoc.getPageCount();
986
+
 
987
  for (let i = 0; i < numPages; i++) {
988
  const [croppedPage] = await newPdf.copyPages(pdfLibDoc, [i]);
989
+ croppedPage.setCropBox (cropBox.x, cropBox.y, cropBox.width, cropBox.height);
990
+ croppedPage.setMediaBox(cropBox.x, cropBox.y, cropBox.width, cropBox.height);
 
 
 
 
 
 
 
 
 
 
 
 
 
991
  newPdf.addPage(croppedPage);
992
  }
993
+
994
  const pdfBytesOutput = await newPdf.save();
995
  processedPdfBlob = new Blob([pdfBytesOutput], { type: 'application/pdf' });
996
+
997
  showSuccess();
998
  document.getElementById('cropBtn').disabled = false;
999
  } catch (error) {
 
1003
  }
1004
  }
1005
 
1006
+ // ─── Modal helpers ────────────────────────────────────────────────────
1007
  function showModal() {
1008
  document.getElementById('modalOverlay').classList.add('active');
1009
  document.getElementById('modalIcon').innerHTML = '<div class="processing-spinner"></div>';
1010
  document.getElementById('modalTitle').textContent = 'Processing PDF';
1011
+ document.getElementById('modalText').textContent = 'Please wait while we process your document...';
1012
+ document.getElementById('progressBar').style.display = 'block';
1013
  document.getElementById('modalButtons').style.display = 'none';
1014
  }
1015
 
1016
  function showSuccess() {
1017
  document.getElementById('modalIcon').innerHTML = '<div class="success-icon"></div>';
1018
  document.getElementById('modalTitle').textContent = 'PDF Processed';
1019
+ document.getElementById('modalText').textContent = 'Your document has been processed successfully and is ready to download.';
1020
+ document.getElementById('progressBar').style.display = 'none';
1021
  document.getElementById('modalButtons').style.display = 'block';
1022
  }
1023
 
1024
  function downloadPDF() {
1025
  if (!processedPdfBlob) return;
 
1026
  const url = URL.createObjectURL(processedPdfBlob);
1027
+ const a = document.createElement('a');
1028
+ a.href = url;
1029
  a.download = 'cropped_document.pdf';
1030
  a.click();
1031
  URL.revokeObjectURL(url);
 
1035
  document.getElementById('modalOverlay').classList.remove('active');
1036
  }
1037
 
1038
+ // ─── Start over ───────────────────────────────────────────────────────
1039
  function startOver() {
1040
+ pdfDoc = pdfBytes = pdfArrayBuffer = processedPdfBlob = selection = null;
1041
+ currentScale = baseScale = 1.5;
1042
+
 
 
 
1043
  document.getElementById('pdfInput').value = '';
1044
  document.getElementById('selectionBox').style.display = 'none';
1045
  document.getElementById('cropBtn').disabled = true;
1046
  document.getElementById('status').style.display = 'none';
1047
+ document.getElementById('scrollHint').style.display = 'none';
1048
+ document.getElementById('zoomValue').textContent = '100%';
1049
+
1050
  ctx.clearRect(0, 0, canvas.width, canvas.height);
1051
+
1052
+ document.getElementById('canvasSection').style.display = 'none';
1053
+ document.getElementById('uploadSection').style.display = 'block';
 
1054
  closeModal();
1055
  }
1056
 
1057
+ // ─── Status bar ───────────────────────────────────────────────────────
1058
  function showStatus(message, type) {
1059
  const status = document.getElementById('status');
1060
  status.className = 'status ' + type;
1061
+ status.innerHTML = type === 'processing'
1062
+ ? '<span class="loader"></span>' + message
1063
+ : message;
 
 
 
1064
  }
1065
  </script>
1066
  </body>