PDF_Crop_Tool / index.html
beta3's picture
Update index.html
5fc0d7f verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF Crop Tool</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Syne:wght@600;700&display=swap" rel="stylesheet">
<style>
:root {
--color-bg: #fafafa;
--color-surface: #ffffff;
--color-text: #0f172a;
--color-text-muted: #64748b;
--color-primary: #1e293b;
--color-accent: #334155;
--color-border: #e2e8f0;
--color-success: #059669;
--color-error: #dc2626;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'DM Sans', sans-serif;
background: var(--color-bg);
color: var(--color-text);
line-height: 1.7;
overflow-x: hidden;
position: relative;
min-height: 100vh;
}
.geometric-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.shape {
position: absolute;
opacity: 0.08;
}
.shape-1 {
width: 500px;
height: 500px;
border: 3px solid var(--color-primary);
border-radius: 50%;
top: -150px;
right: -150px;
animation: float 20s ease-in-out infinite;
}
.shape-2 {
width: 400px;
height: 400px;
border: 3px solid var(--color-accent);
transform: rotate(45deg);
bottom: -100px;
left: -100px;
animation: float 25s ease-in-out infinite reverse;
}
.shape-3 {
width: 300px;
height: 300px;
border: 2px solid var(--color-primary);
border-radius: 30%;
top: 40%;
left: 5%;
animation: float 30s ease-in-out infinite;
}
.shape-4 {
width: 250px;
height: 250px;
background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
opacity: 0.05;
border-radius: 50%;
top: 15%;
right: 15%;
animation: pulse 15s ease-in-out infinite;
}
.shape-5 {
width: 350px;
height: 350px;
border: 2px solid var(--color-accent);
border-radius: 40%;
top: 60%;
right: 30%;
animation: float 35s ease-in-out infinite;
}
.shape-6 {
width: 200px;
height: 200px;
border: 3px solid var(--color-primary);
transform: rotate(30deg);
top: 25%;
left: 40%;
animation: float 28s ease-in-out infinite reverse;
}
.shape-7 {
width: 180px;
height: 180px;
background: linear-gradient(45deg, var(--color-accent), var(--color-primary));
opacity: 0.04;
border-radius: 50%;
bottom: 20%;
right: 40%;
animation: pulse 20s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
25% { transform: translate(30px, -30px) rotate(8deg); }
50% { transform: translate(-30px, 30px) rotate(-8deg); }
75% { transform: translate(30px, 30px) rotate(8deg); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.04; }
50% { transform: scale(1.15); opacity: 0.08; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 60px 24px;
position: relative;
z-index: 1;
}
header {
text-align: center;
margin-bottom: 80px;
animation: fadeInUp 0.8s ease-out;
}
h1 {
font-family: 'Syne', sans-serif;
font-size: 52px;
font-weight: 700;
margin-bottom: 16px;
letter-spacing: -0.02em;
line-height: 1.1;
}
.subtitle {
color: var(--color-text-muted);
font-size: 18px;
font-weight: 400;
max-width: 500px;
margin: 0 auto;
}
.upload-section {
background: var(--color-surface);
padding: 80px 48px;
border-radius: 24px;
border: 1px solid var(--color-border);
margin-bottom: 40px;
text-align: center;
position: relative;
overflow: hidden;
animation: fadeInUp 0.8s ease-out 0.2s both;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.upload-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--color-border), transparent);
}
.upload-section:hover {
transform: translateY(-4px);
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.08);
}
.upload-section h2 {
font-family: 'Syne', sans-serif;
font-size: 24px;
font-weight: 600;
margin-bottom: 32px;
color: var(--color-text);
}
.file-input-wrapper {
position: relative;
display: inline-block;
}
input[type="file"] { display: none; }
.upload-btn {
background: var(--color-primary);
color: white;
padding: 16px 40px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.upload-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255,255,255,0.1);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.upload-btn:hover::before { width: 300px; height: 300px; }
.upload-btn:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(30, 41, 59, 0.3);
}
.upload-hint {
margin-top: 24px;
color: var(--color-text-muted);
font-size: 14px;
}
.canvas-section {
background: var(--color-surface);
padding: 48px;
border-radius: 24px;
border: 1px solid var(--color-border);
margin-bottom: 40px;
display: none;
animation: fadeInUp 0.8s ease-out;
}
.instructions {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
padding: 24px;
border-radius: 16px;
margin-bottom: 24px;
font-size: 15px;
color: var(--color-text-muted);
border-left: 3px solid var(--color-primary);
line-height: 1.6;
}
.instructions strong {
color: var(--color-text);
font-weight: 600;
}
/* ─── Zoom controls ─── */
.zoom-bar {
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
margin-bottom: 16px;
flex-wrap: wrap;
}
.zoom-label {
font-size: 13px;
color: var(--color-text-muted);
font-weight: 500;
}
.zoom-btn {
width: 36px;
height: 36px;
border: 2px solid var(--color-border);
background: var(--color-surface);
border-radius: 10px;
cursor: pointer;
font-size: 18px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
color: var(--color-text);
}
.zoom-btn:hover {
border-color: var(--color-accent);
background: var(--color-bg);
transform: translateY(-1px);
}
.zoom-value {
font-size: 14px;
font-weight: 600;
color: var(--color-text);
min-width: 52px;
text-align: center;
background: var(--color-bg);
border: 2px solid var(--color-border);
border-radius: 10px;
padding: 6px 10px;
}
.zoom-reset-btn {
font-size: 13px;
padding: 6px 14px;
border: 2px solid var(--color-border);
background: var(--color-surface);
border-radius: 10px;
cursor: pointer;
color: var(--color-text-muted);
font-weight: 500;
transition: all 0.2s;
font-family: 'DM Sans', sans-serif;
}
.zoom-reset-btn:hover {
border-color: var(--color-accent);
color: var(--color-text);
}
/* ─── Scrollable canvas viewport ─── */
.canvas-viewport {
width: 100%;
max-height: 600px;
overflow: auto;
border-radius: 16px;
border: 1px solid var(--color-border);
background: #e8ecf0;
margin: 0 auto 8px;
/* Custom scrollbar */
scrollbar-width: thin;
scrollbar-color: #94a3b8 transparent;
}
.canvas-viewport::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.canvas-viewport::-webkit-scrollbar-track {
background: transparent;
}
.canvas-viewport::-webkit-scrollbar-thumb {
background: #94a3b8;
border-radius: 4px;
}
.canvas-viewport::-webkit-scrollbar-corner {
background: transparent;
}
/* Inner padding so canvas doesn't kiss the edges */
.canvas-inner {
display: inline-block;
padding: 24px;
min-width: 100%;
text-align: center;
}
.canvas-wrapper {
position: relative;
display: inline-block;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.15);
}
canvas {
display: block;
cursor: crosshair;
}
.selection-box {
position: absolute;
border: 2px solid var(--color-primary);
background: rgba(30, 41, 59, 0.08);
pointer-events: none;
backdrop-filter: blur(2px);
}
/* Scroll hint badge */
.scroll-hint {
text-align: center;
font-size: 12px;
color: var(--color-text-muted);
margin-bottom: 20px;
display: none;
gap: 6px;
align-items: center;
justify-content: center;
}
.scroll-hint svg {
opacity: 0.5;
}
.button-group {
display: flex;
gap: 16px;
justify-content: center;
margin-top: 32px;
flex-wrap: wrap;
}
.btn {
padding: 14px 32px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: 'DM Sans', sans-serif;
}
.btn-primary {
background: var(--color-success);
color: white;
box-shadow: 0 4px 16px rgba(5, 150, 105, 0.2);
}
.btn-primary:hover:not(:disabled) {
background: #047857;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(5, 150, 105, 0.3);
}
.btn-primary:disabled {
background: #cbd5e1;
cursor: not-allowed;
box-shadow: none;
}
.btn-secondary {
background: transparent;
color: var(--color-text);
border: 2px solid var(--color-border);
}
.btn-secondary:hover {
background: var(--color-bg);
border-color: var(--color-accent);
transform: translateY(-2px);
}
.btn-reset {
background: var(--color-error);
color: white;
box-shadow: 0 4px 16px rgba(220, 38, 38, 0.2);
}
.btn-reset:hover {
background: #b91c1c;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(220, 38, 38, 0.3);
}
.status {
text-align: center;
padding: 16px 24px;
border-radius: 12px;
margin-top: 32px;
font-size: 15px;
display: none;
animation: fadeInUp 0.4s ease-out;
}
.status.success {
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
color: var(--color-success);
border: 1px solid #6ee7b7;
display: block;
}
.status.processing {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
color: #92400e;
border: 1px solid #fcd34d;
display: block;
}
.status.error {
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
color: var(--color-error);
border: 1px solid #fca5a5;
display: block;
}
.loader {
border: 3px solid rgba(30, 41, 59, 0.1);
border-top: 3px solid var(--color-primary);
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 0.8s linear infinite;
display: inline-block;
margin-right: 12px;
vertical-align: middle;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* ─── Modal ─── */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(8px);
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease-out;
}
.modal-overlay.active { display: flex; }
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
background: var(--color-surface);
padding: 48px;
border-radius: 24px;
text-align: center;
max-width: 450px;
width: 90%;
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.2);
animation: scaleIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.modal-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
}
.processing-spinner {
width: 80px; height: 80px;
border: 3px solid var(--color-border);
border-top: 3px solid var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.success-icon {
width: 80px; height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
display: flex;
align-items: center;
justify-content: center;
animation: successPop 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.success-icon::after {
content: 'βœ“';
font-size: 48px;
color: var(--color-success);
font-weight: 700;
}
@keyframes successPop {
0% { transform: scale(0); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.modal-title {
font-family: 'Syne', sans-serif;
font-size: 28px;
font-weight: 700;
margin-bottom: 12px;
color: var(--color-text);
}
.modal-text {
color: var(--color-text-muted);
font-size: 15px;
margin-bottom: 32px;
line-height: 1.6;
}
.progress-bar {
width: 100%;
height: 4px;
background: var(--color-border);
border-radius: 2px;
overflow: hidden;
margin-top: 24px;
}
.progress-fill {
height: 100%;
background: var(--color-primary);
border-radius: 2px;
animation: progressFlow 2s ease-in-out infinite;
}
@keyframes progressFlow {
0% { width: 0%; opacity: 1; }
50% { width: 100%; opacity: 1; }
100% { width: 100%; opacity: 0; }
}
.modal-btn {
background: var(--color-primary);
color: white;
padding: 16px 48px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: 'DM Sans', sans-serif;
width: 100%;
margin-bottom: 12px;
}
.modal-btn:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(30, 41, 59, 0.3);
}
.modal-btn-secondary {
background: transparent;
color: var(--color-text-muted);
border: 2px solid var(--color-border);
margin-top: 8px;
}
.modal-btn-secondary:hover {
background: var(--color-bg);
border-color: var(--color-accent);
}
@media (max-width: 768px) {
h1 { font-size: 36px; }
.container { padding: 40px 20px; }
.upload-section { padding: 60px 32px; }
.canvas-section { padding: 32px 16px; }
.button-group { flex-direction: column; }
.canvas-viewport { max-height: 450px; }
}
</style>
</head>
<body>
<div class="geometric-bg">
<div class="shape shape-1"></div>
<div class="shape shape-2"></div>
<div class="shape shape-3"></div>
<div class="shape shape-4"></div>
<div class="shape shape-5"></div>
<div class="shape shape-6"></div>
<div class="shape shape-7"></div>
</div>
<div class="container">
<header>
<h1>PDF Crop Tool</h1>
<p class="subtitle">Crop the same area across all PDF pages</p>
</header>
<div class="upload-section" id="uploadSection">
<h2>Upload Document</h2>
<div class="file-input-wrapper">
<input type="file" id="pdfInput" accept=".pdf">
<button class="upload-btn" onclick="document.getElementById('pdfInput').click()">
Select File
</button>
</div>
<p class="upload-hint">Supported formats: PDF</p>
</div>
<div class="canvas-section" id="canvasSection">
<div class="instructions">
<strong>Instructions:</strong> Click and drag on the first page to select the area you want to crop.
Use the zoom controls or scroll/pan to navigate large documents.
The selection will be applied to all pages.
</div>
<!-- Zoom controls -->
<div class="zoom-bar">
<span class="zoom-label">Zoom</span>
<button class="zoom-btn" id="zoomOutBtn" title="Zoom out">βˆ’</button>
<span class="zoom-value" id="zoomValue">100%</span>
<button class="zoom-btn" id="zoomInBtn" title="Zoom in">+</button>
<button class="zoom-reset-btn" id="zoomResetBtn">Fit to view</button>
</div>
<!-- Scroll hint (shown only when content overflows) -->
<div class="scroll-hint" id="scrollHint">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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"/>
</svg>
Scroll inside the preview area to navigate the document
</div>
<!-- Scrollable viewport -->
<div class="canvas-viewport" id="canvasViewport">
<div class="canvas-inner">
<div class="canvas-wrapper" id="canvasWrapper">
<canvas id="pdfCanvas"></canvas>
<div class="selection-box" id="selectionBox"></div>
</div>
</div>
</div>
<div class="button-group">
<button class="btn btn-secondary" id="resetBtn">Clear Selection</button>
<button class="btn btn-primary" id="cropBtn" disabled>Process PDF</button>
<button class="btn btn-reset" id="startOverBtn">Start Over</button>
</div>
<div class="status" id="status"></div>
</div>
</div>
<div class="modal-overlay" id="modalOverlay">
<div class="modal-content">
<div class="modal-icon" id="modalIcon">
<div class="processing-spinner"></div>
</div>
<h2 class="modal-title" id="modalTitle">Processing PDF</h2>
<p class="modal-text" id="modalText">Please wait while we process your document...</p>
<div class="progress-bar" id="progressBar">
<div class="progress-fill"></div>
</div>
<div id="modalButtons" style="display: none;">
<button class="modal-btn" id="downloadBtn">Download PDF</button>
<button class="modal-btn modal-btn-secondary" id="newDocBtn">Process New Document</button>
</div>
</div>
</div>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
let pdfDoc = null;
let pdfBytes = null;
let pdfArrayBuffer = null;
let processedPdfBlob = null;
let canvas = document.getElementById('pdfCanvas');
let ctx = canvas.getContext('2d');
let isSelecting = false;
let startX, startY, endX, endY;
let selection = null;
// Zoom state
let currentScale = 1.5; // pdf.js render scale
let baseScale = 1.5; // "fit-to-view" scale computed on load
const ZOOM_STEP = 0.25;
const MIN_SCALE = 0.5;
const MAX_SCALE = 5;
// ─── Viewport dimensions used for fit-to-view ───────────────────────
const VIEWPORT_MAX_WIDTH = 1000; // matches canvas-section max inner width
const VIEWPORT_MAX_HEIGHT = 600; // matches max-height of canvas-viewport
// ─── Event listeners ─────────────────────────────────────────────────
document.getElementById('pdfInput').addEventListener('change', handleFileSelect);
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
document.getElementById('resetBtn').addEventListener('click', resetSelection);
document.getElementById('cropBtn').addEventListener('click', processPDF);
document.getElementById('downloadBtn').addEventListener('click', downloadPDF);
document.getElementById('startOverBtn').addEventListener('click', startOver);
document.getElementById('newDocBtn').addEventListener('click', startOver);
document.getElementById('zoomInBtn').addEventListener('click', () => zoom(currentScale + ZOOM_STEP));
document.getElementById('zoomOutBtn').addEventListener('click', () => zoom(currentScale - ZOOM_STEP));
document.getElementById('zoomResetBtn').addEventListener('click', () => zoom(baseScale));
// ─── File load ────────────────────────────────────────────────────────
async function handleFileSelect(e) {
const file = e.target.files[0];
if (!file) return;
try {
const originalBuffer = await file.arrayBuffer();
pdfBytes = new Uint8Array(originalBuffer.slice(0));
pdfArrayBuffer = originalBuffer.slice(0);
const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
pdfDoc = await loadingTask.promise;
// Compute a sensible initial scale so the PDF fits in the viewport
const page = await pdfDoc.getPage(1);
const rawVP = page.getViewport({ scale: 1 });
const scaleW = VIEWPORT_MAX_WIDTH / rawVP.width;
const scaleH = VIEWPORT_MAX_HEIGHT / rawVP.height;
baseScale = Math.min(scaleW, scaleH, 1.5); // never zoom in beyond 1.5Γ— on load
currentScale = baseScale;
await renderFirstPage();
document.getElementById('uploadSection').style.display = 'none';
document.getElementById('canvasSection').style.display = 'block';
checkScrollHint();
showStatus('PDF loaded successfully. Select the area to crop.', 'success');
} catch (error) {
showStatus('Error loading PDF: ' + error.message, 'error');
}
}
async function renderFirstPage() {
const page = await pdfDoc.getPage(1);
const viewport = page.getViewport({ scale: currentScale });
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
updateZoomUI();
// Reset selection overlay
resetSelection();
}
// ─── Zoom ─────────────────────────────────────────────────────────────
async function zoom(newScale) {
newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale));
if (Math.abs(newScale - currentScale) < 0.001) return;
// Preserve scroll position ratio
const vp = document.getElementById('canvasViewport');
const ratioX = (vp.scrollLeft + vp.clientWidth / 2) / vp.scrollWidth;
const ratioY = (vp.scrollTop + vp.clientHeight / 2) / vp.scrollHeight;
currentScale = newScale;
await renderFirstPage();
// Restore scroll position
requestAnimationFrame(() => {
vp.scrollLeft = ratioX * vp.scrollWidth - vp.clientWidth / 2;
vp.scrollTop = ratioY * vp.scrollHeight - vp.clientHeight / 2;
});
checkScrollHint();
}
function updateZoomUI() {
document.getElementById('zoomValue').textContent =
Math.round((currentScale / baseScale) * 100) + '%';
}
function checkScrollHint() {
const vp = document.getElementById('canvasViewport');
const hint = document.getElementById('scrollHint');
const overflows = canvas.width > vp.clientWidth ||
canvas.height > vp.clientHeight;
hint.style.display = overflows ? 'flex' : 'none';
}
// ─── Mouse selection ─────────────────────────────────────────────────
function getCanvasPos(e) {
const rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
function handleMouseDown(e) {
const pos = getCanvasPos(e);
startX = pos.x;
startY = pos.y;
endX = pos.x;
endY = pos.y;
isSelecting = true;
}
function handleMouseMove(e) {
if (!isSelecting) return;
const pos = getCanvasPos(e);
endX = pos.x;
endY = pos.y;
updateSelectionBox();
}
function handleMouseUp(e) {
if (!isSelecting) return;
isSelecting = false;
const pos = getCanvasPos(e);
endX = pos.x;
endY = pos.y;
const w = Math.abs(endX - startX);
const h = Math.abs(endY - startY);
if (w > 5 && h > 5) {
selection = {
x: Math.min(startX, endX),
y: Math.min(startY, endY),
width: w,
height: h
};
updateSelectionBox();
document.getElementById('cropBtn').disabled = false;
}
}
function updateSelectionBox() {
const box = document.getElementById('selectionBox');
const x = Math.min(startX, endX);
const y = Math.min(startY, endY);
box.style.left = x + 'px';
box.style.top = y + 'px';
box.style.width = Math.abs(endX - startX) + 'px';
box.style.height = Math.abs(endY - startY) + 'px';
box.style.display = 'block';
}
function resetSelection() {
selection = null;
document.getElementById('selectionBox').style.display = 'none';
document.getElementById('cropBtn').disabled = true;
}
// ─── PDF processing ───────────────────────────────────────────────────
async function processPDF() {
if (!selection || !pdfArrayBuffer) return;
try {
showModal();
document.getElementById('cropBtn').disabled = true;
const page = await pdfDoc.getPage(1);
const viewport = page.getViewport({ scale: currentScale });
// Map canvas pixels β†’ PDF units (using the page's native size)
const scaleX = page.view[2] / viewport.width;
const scaleY = page.view[3] / viewport.height;
const cropBox = {
x: selection.x * scaleX,
y: (viewport.height - selection.y - selection.height) * scaleY,
width: selection.width * scaleX,
height: selection.height * scaleY
};
const pdfLibDoc = await PDFLib.PDFDocument.load(pdfArrayBuffer);
const newPdf = await PDFLib.PDFDocument.create();
const numPages = pdfLibDoc.getPageCount();
for (let i = 0; i < numPages; i++) {
const [croppedPage] = await newPdf.copyPages(pdfLibDoc, [i]);
croppedPage.setCropBox (cropBox.x, cropBox.y, cropBox.width, cropBox.height);
croppedPage.setMediaBox(cropBox.x, cropBox.y, cropBox.width, cropBox.height);
newPdf.addPage(croppedPage);
}
const pdfBytesOutput = await newPdf.save();
processedPdfBlob = new Blob([pdfBytesOutput], { type: 'application/pdf' });
showSuccess();
document.getElementById('cropBtn').disabled = false;
} catch (error) {
closeModal();
showStatus('Error processing PDF: ' + error.message, 'error');
document.getElementById('cropBtn').disabled = false;
}
}
// ─── Modal helpers ────────────────────────────────────────────────────
function showModal() {
document.getElementById('modalOverlay').classList.add('active');
document.getElementById('modalIcon').innerHTML = '<div class="processing-spinner"></div>';
document.getElementById('modalTitle').textContent = 'Processing PDF';
document.getElementById('modalText').textContent = 'Please wait while we process your document...';
document.getElementById('progressBar').style.display = 'block';
document.getElementById('modalButtons').style.display = 'none';
}
function showSuccess() {
document.getElementById('modalIcon').innerHTML = '<div class="success-icon"></div>';
document.getElementById('modalTitle').textContent = 'PDF Processed';
document.getElementById('modalText').textContent = 'Your document has been processed successfully and is ready to download.';
document.getElementById('progressBar').style.display = 'none';
document.getElementById('modalButtons').style.display = 'block';
}
function downloadPDF() {
if (!processedPdfBlob) return;
const url = URL.createObjectURL(processedPdfBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'cropped_document.pdf';
a.click();
URL.revokeObjectURL(url);
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('active');
}
// ─── Start over ───────────────────────────────────────────────────────
function startOver() {
pdfDoc = pdfBytes = pdfArrayBuffer = processedPdfBlob = selection = null;
currentScale = baseScale = 1.5;
document.getElementById('pdfInput').value = '';
document.getElementById('selectionBox').style.display = 'none';
document.getElementById('cropBtn').disabled = true;
document.getElementById('status').style.display = 'none';
document.getElementById('scrollHint').style.display = 'none';
document.getElementById('zoomValue').textContent = '100%';
ctx.clearRect(0, 0, canvas.width, canvas.height);
document.getElementById('canvasSection').style.display = 'none';
document.getElementById('uploadSection').style.display = 'block';
closeModal();
}
// ─── Status bar ───────────────────────────────────────────────────────
function showStatus(message, type) {
const status = document.getElementById('status');
status.className = 'status ' + type;
status.innerHTML = type === 'processing'
? '<span class="loader"></span>' + message
: message;
}
</script>
</body>
</html>