`; filenameSpan.textContent = 'No file'; lastSavedSpan.textContent = 'Never'; currentPdf = null; currentFileName = ''; overlays = {}; actionStack = []; redoStack = []; selectedOverlay = null; drawMode = false; drawBtn.classList.remove('active'); highlightMode = false; highlightBtn.classList.remove('active'); commentMode = false; commentBtn.classList.remove('active'); cropMode = false; cropBtn.classList.remove('active'); eraserMode = false; eraserBtn.classList.remove('active'); eraserCursor.style.display = 'none'; document.body.style.cursor = 'default'; hideTextToolbar(); if (cropOverlay) { cropOverlay.remove(); cropOverlay = null; cropArea = null; } currentZoom = 1.0; zoomLevelSpan.textContent = '100%'; document.querySelectorAll('.pdf-page').forEach(p => { p.style.transform = 'scale(1)'; p.style.transformOrigin = 'top left'; }); } // Render PDF async function renderPDF(arrayBuffer, filename = 'document.pdf') { clearRendered(); currentFileName = filename; filenameSpan.textContent = filename; uploadContainer.style.display = 'none'; pdfContent.style.display = 'block'; try { const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer }); const pdf = await loadingTask.promise; currentPdf = pdf; for (let p = 1; p <= pdf.numPages; p++) { const page = await pdf.getPage(p); const viewport = page.getViewport({ scale: 1.4 }); const pageDiv = document.createElement('div'); pageDiv.className = 'pdf-page'; pageDiv.id = `page${p}`; pageDiv.style.position = 'relative'; pageDiv.style.transform = 'scale(1)'; pageDiv.style.transformOrigin = 'top left'; pageDiv.dataset.page = p; const canvas = document.createElement('canvas'); canvas.width = Math.floor(viewport.width); canvas.height = Math.floor(viewport.height); canvas.style.width = '100%'; canvas.style.height = 'auto'; canvas.dataset.originalW = canvas.width; canvas.dataset.originalH = canvas.height; pageDiv.appendChild(canvas); const overlay = document.createElement('div'); overlay.className = 'overlay'; overlay.style.position = 'absolute'; overlay.style.left = '0'; overlay.style.top = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.pointerEvents = 'auto'; overlay.dataset.page = p; pageDiv.appendChild(overlay); pdfContent.appendChild(pageDiv); const ctx = canvas.getContext('2d'); await page.render({ canvasContext: ctx, viewport }).promise; overlays[p] = []; createThumbnail(p); attachOverlayHandlers(overlay); } showPage(1); lastSavedSpan.textContent = 'Just now'; showStatus('PDF loaded successfully'); applyZoom(currentZoom); } catch (err) { console.error(err); showStatus('Failed to load PDF', true); clearRendered(); uploadContainer.style.display = 'flex'; pdfContent.style.display = 'none'; } } function createThumbnail(page) { const placeholder = pagesList.querySelector('[data-page="placeholder"]'); if (placeholder) placeholder.remove(); const thumb = document.createElement('div'); thumb.className = 'page-thumb'; thumb.dataset.page = page; thumb.innerHTML = `
Page ${page}`; pagesList.appendChild(thumb); thumb.addEventListener('click', () => { document.querySelectorAll('.page-thumb').forEach(t => t.classList.remove('active')); thumb.classList.add('active'); showPage(page); }); if (page === 1) { document.querySelectorAll('.page-thumb').forEach(t => t.classList.remove('active')); thumb.classList.add('active'); } } function showPage(page) { document.querySelectorAll('.pdf-page').forEach(p => p.style.display = 'none'); const target = document.getElementById(`page${page}`); if (target) target.style.display = 'block'; } // Zoom - FIXED: zoom in increases, zoom out decreases function applyZoom(zoom) { currentZoom = Math.min(2.5, Math.max(0.5, zoom)); zoomLevelSpan.textContent = Math.round(currentZoom * 100) + '%'; document.querySelectorAll('.pdf-page').forEach(p => { p.style.transform = `scale(${currentZoom})`; p.style.transformOrigin = 'top left'; // Adjust width to prevent overflow p.style.width = `${100 / currentZoom}%`; }); } // FIXED: zoom in = increase, zoom out = decrease zoomInBtn.addEventListener('click', () => applyZoom(currentZoom + 0.1)); zoomOutBtn.addEventListener('click', () => applyZoom(currentZoom - 0.1)); zoomResetBtn.addEventListener('click', () => applyZoom(1.0)); // Overlay handlers function attachOverlayHandlers(overlay) { overlay.addEventListener('pointerdown', (e) => { if (eraserMode) { handleEraser(e, overlay); return; } if (drawMode && e.target === overlay) { startDrawing(e, overlay); return; } let target = e.target; let editable = null; while (target && target !== overlay) { if (target.classList && target.classList.contains('editable')) { editable = target; break; } target = target.parentElement; } if (editable) { selectOverlay(editable); e.stopPropagation(); if (!eraserMode) makeDraggable(editable, overlay); } else { selectOverlay(null); } }); } function selectOverlay(el) { if (selectedOverlay) selectedOverlay.classList.remove('selected'); selectedOverlay = el; if (el) { el.classList.add('selected'); if (el.contentEditable === 'true') showTextToolbarFor(el); else hideTextToolbar(); } else { hideTextToolbar(); } } function makeDraggable(el, container) { let drag = false, startX, startY, origLeft, origTop; const onDown = (e) => { if (e.pointerType === 'mouse' && e.button !== 0) return; if (eraserMode) return; drag = true; const rect = container.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); startX = e.clientX; startY = e.clientY; origLeft = elRect.left - rect.left; origTop = elRect.top - rect.top; el.setPointerCapture?.(e.pointerId); e.preventDefault(); e.stopPropagation(); }; const onMove = (e) => { if (!drag) return; const dx = e.clientX - startX, dy = e.clientY - startY; el.style.left = Math.max(0, origLeft + dx) + 'px'; el.style.top = Math.max(0, origTop + dy) + 'px'; }; const onUp = (e) => { if (drag) { drag = false; el.releasePointerCapture?.(e.pointerId); } }; el.addEventListener('pointerdown', onDown); window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp); } // Drawing function startDrawing(e, overlay) { const page = getCurrentPage(); if (!page) return; let canvas = overlay.querySelector('.drawing-canvas'); if (!canvas) { canvas = document.createElement('canvas'); canvas.className = 'drawing-canvas'; canvas.style.position = 'absolute'; canvas.style.left = '0'; canvas.style.top = '0'; canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.zIndex = '15'; canvas.style.pointerEvents = 'none'; const base = document.querySelector(`#page${page} canvas`); canvas.width = base.width; canvas.height = base.height; overlay.appendChild(canvas); overlays[page].push(canvas); } currentDrawingCanvas = canvas; currentDrawingCtx = canvas.getContext('2d'); currentDrawingCtx.lineJoin = 'round'; currentDrawingCtx.lineCap = 'round'; currentDrawingCtx.lineWidth = 3; currentDrawingCtx.strokeStyle = '#0b1e33'; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const startX = (e.clientX - rect.left) * scaleX; const startY = (e.clientY - rect.top) * scaleY; const oldData = canvas.toDataURL(); currentDrawingCtx.beginPath(); currentDrawingCtx.moveTo(startX, startY); isDrawing = true; const onMove = (ev) => { if (!isDrawing) return; const x = (ev.clientX - rect.left) * scaleX; const y = (ev.clientY - rect.top) * scaleY; currentDrawingCtx.lineTo(x, y); currentDrawingCtx.stroke(); }; const onUp = () => { if (!isDrawing) return; isDrawing = false; currentDrawingCtx.closePath(); const newData = canvas.toDataURL(); pushAction({ type: 'drawing', page, oldCanvasData: oldData, newCanvasData: newData }); window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp); }; window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp); } // Eraser - IMPROVED: properly removes text, images, highlights, comments, signatures, and drawings function handleEraser(e, overlay) { const page = Number(overlay.dataset.page); // 1. First check if we're clicking on a drawing canvas (erase drawing) const canvas = overlay.querySelector('.drawing-canvas'); if (canvas) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const x = (e.clientX - rect.left) * scaleX; const y = (e.clientY - rect.top) * scaleY; const ctx = canvas.getContext('2d'); const oldData = canvas.toDataURL(); ctx.save(); ctx.globalCompositeOperation = 'destination-out'; ctx.beginPath(); ctx.arc(x, y, 20 * scaleX, 0, Math.PI * 2); ctx.fill(); ctx.closePath(); ctx.restore(); const newData = canvas.toDataURL(); pushAction({ type: 'drawing', page, oldCanvasData: oldData, newCanvasData: newData }); showStatus('Erased drawing'); return; } // 2. Check for editable elements (text, images, highlights, comments, signatures) // Use elementsFromPoint to find the exact element under the cursor const elements = document.elementsFromPoint(e.clientX, e.clientY); let targetEl = null; for (let el of elements) { // Check if this element is an editable element that belongs to this overlay if (el.classList && el.classList.contains('editable') && el.parentElement === overlay) { targetEl = el; break; } } if (targetEl) { // Deselect if it was selected if (selectedOverlay === targetEl) { selectedOverlay = null; hideTextToolbar(); } // Remove the element targetEl.parentElement.removeChild(targetEl); overlays[page] = overlays[page].filter(x => x !== targetEl); pushAction({ type: 'delete', page, el: targetEl }); showStatus('Erased: ' + (targetEl.tagName === 'IMG' ? 'image' : 'element')); } else { showStatus('Click on a drawing or an added element to erase it', true); } } // Add text addTextBtn.addEventListener('click', () => { const page = getCurrentPage(); if (!page) return showStatus('Open a document first', true); const overlay = getOverlay(page); const txt = document.createElement('div'); txt.className = 'editable'; txt.contentEditable = 'true'; txt.innerText = 'Edit text'; txt.style.left = '40px'; txt.style.top = '40px'; txt.style.minWidth = '60px'; txt.style.padding = '6px 10px'; txt.style.background = 'transparent'; txt.style.border = '1px dashed #94a3b8'; txt.style.borderRadius = '4px'; txt.style.fontSize = '18px'; txt.style.color = '#0b1e33'; txt.style.fontFamily = 'Inter'; txt.style.zIndex = '20'; overlay.appendChild(txt); makeDraggable(txt, overlay); overlays[page].push(txt); pushAction({ type: 'add', page, el: txt }); selectOverlay(txt); txt.focus(); showTextToolbarFor(txt); showStatus('Text added – drag to move, double-click to edit'); }); // Add image addImageBtn.addEventListener('click', () => { const page = getCurrentPage(); if (!page) return showStatus('Open a document first', true); imageInput.click(); imageInput.onchange = () => { const f = imageInput.files[0]; if (!f) return; const reader = new FileReader(); reader.onload = (e) => { const overlay = getOverlay(page); const img = document.createElement('img'); img.src = e.target.result; img.className = 'editable'; img.style.left = '30px'; img.style.top = '30px'; img.style.width = '160px'; img.style.maxWidth = '45%'; img.style.height = 'auto'; img.style.borderRadius = '4px'; img.style.zIndex = '20'; img.draggable = false; overlay.appendChild(img); makeDraggable(img, overlay); overlays[page].push(img); pushAction({ type: 'add', page, el: img }); selectOverlay(img); showStatus('Image added'); }; reader.readAsDataURL(f); imageInput.value = ''; }; }); // Sign signBtn.addEventListener('click', () => { const page = getCurrentPage(); if (!page) return showStatus('Open a document first', true); signInput.click(); signInput.onchange = () => { const f = signInput.files[0]; if (!f) return; const reader = new FileReader(); reader.onload = (e) => { const overlay = getOverlay(page); const img = document.createElement('img'); img.src = e.target.result; img.className = 'editable'; img.style.left = '40px'; img.style.top = '60px'; img.style.width = '180px'; img.style.height = 'auto'; img.style.zIndex = '20'; img.draggable = false; overlay.appendChild(img); makeDraggable(img, overlay); overlays[page].push(img); pushAction({ type: 'add', page, el: img }); selectOverlay(img); showStatus('Signature added'); }; reader.readAsDataURL(f); signInput.value = ''; }; }); // Highlight & Comment (rect) function startRectDraw(type) { const page = getCurrentPage(); if (!page) return showStatus('Open a document first', true); const overlay = getOverlay(page); let rect = document.createElement('div'); rect.style.position = 'absolute'; rect.style.border = '2px dashed #f97316'; rect.style.borderRadius = '4px'; rect.style.pointerEvents = 'none'; rect.style.zIndex = '25'; rect.style.display = 'none'; overlay.appendChild(rect); let startX, startY, drawing = false; const onDown = (e) => { if (e.target !== overlay) return; const r = overlay.getBoundingClientRect(); startX = e.clientX - r.left; startY = e.clientY - r.top; rect.style.left = startX + 'px'; rect.style.top = startY + 'px'; rect.style.width = '0px'; rect.style.height = '0px'; rect.style.display = 'block'; drawing = true; const onMove = (ev) => { if (!drawing) return; const cx = ev.clientX - r.left, cy = ev.clientY - r.top; const left = Math.min(startX, cx), top = Math.min(startY, cy); const w = Math.abs(cx - startX), h = Math.abs(cy - startY); rect.style.left = left + 'px'; rect.style.top = top + 'px'; rect.style.width = w + 'px'; rect.style.height = h + 'px'; }; const onUp = () => { drawing = false; const w = parseFloat(rect.style.width), h = parseFloat(rect.style.height); if (w > 10 && h > 10) { const el = document.createElement('div'); el.className = 'editable'; el.style.left = rect.style.left; el.style.top = rect.style.top; el.style.width = w + 'px'; el.style.height = h + 'px'; el.style.zIndex = '20'; el.style.borderRadius = '4px'; if (type === 'highlight') { el.style.backgroundColor = 'rgba(251,191,36,0.4)'; } else { el.style.backgroundColor = 'rgba(96,165,250,0.25)'; el.style.border = '1px solid #60a5fa'; el.contentEditable = 'true'; el.innerText = 'Comment...'; el.style.padding = '6px'; el.style.fontSize = '14px'; el.style.overflow = 'hidden'; } overlay.appendChild(el); makeDraggable(el, overlay); overlays[page].push(el); pushAction({ type: 'add', page, el }); if (type === 'comment') { selectOverlay(el); el.focus(); } showStatus(type === 'highlight' ? 'Highlight added' : 'Comment added'); } rect.remove(); window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp); overlay.removeEventListener('pointerdown', onDown); highlightMode = false; commentMode = false; highlightBtn.classList.remove('active'); commentBtn.classList.remove('active'); }; window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp); }; overlay.addEventListener('pointerdown', onDown); } highlightBtn.addEventListener('click', () => { highlightMode = !highlightMode; highlightBtn.classList.toggle('active', highlightMode); if (highlightMode) { commentMode = false; commentBtn.classList.remove('active'); startRectDraw('highlight'); } }); commentBtn.addEventListener('click', () => { commentMode = !commentMode; commentBtn.classList.toggle('active', commentMode); if (commentMode) { highlightMode = false; highlightBtn.classList.remove('active'); startRectDraw('comment'); } }); // Draw toggle drawBtn.addEventListener('click', () => { drawMode = !drawMode; drawBtn.classList.toggle('active', drawMode); if (drawMode) { eraserMode = false; eraserBtn.classList.remove('active'); eraserCursor.style.display = 'none'; document.body.style.cursor = 'default'; } showStatus(drawMode ? 'Draw mode: click and drag on page' : 'Draw mode off'); }); // Eraser toggle eraserBtn.addEventListener('click', () => { eraserMode = !eraserMode; eraserBtn.classList.toggle('active', eraserMode); if (eraserMode) { drawMode = false; drawBtn.classList.remove('active'); eraserCursor.style.display = 'block'; document.body.style.cursor = 'none'; document.addEventListener('mousemove', moveEraser); showStatus('Eraser: click on any added element (text, image, highlight, comment, signature) or drawing to erase it'); } else { eraserCursor.style.display = 'none'; document.body.style.cursor = 'default'; document.removeEventListener('mousemove', moveEraser); } }); function moveEraser(e) { eraserCursor.style.left = (e.clientX - 15) + 'px'; eraserCursor.style.top = (e.clientY - 15) + 'px'; } // Crop cropBtn.addEventListener('click', () => { const page = getCurrentPage(); if (!page) return showStatus('Open a document first', true); cropMode = !cropMode; cropBtn.classList.toggle('active', cropMode); if (cropMode) startCrop(page); else exitCrop(); }); function startCrop(page) { const pageDiv = document.getElementById(`page${page}`); if (!pageDiv) return; exitCrop(); cropOverlay = document.createElement('div'); cropOverlay.className = 'crop-overlay'; cropOverlay.style.display = 'block'; cropArea = document.createElement('div'); cropArea.className = 'crop-area'; cropArea.style.left = '60px'; cropArea.style.top = '60px'; cropArea.style.width = '200px'; cropArea.style.height = '160px'; ['crop-handle-tl','crop-handle-tr','crop-handle-bl','crop-handle-br'].forEach(cls => { const h = document.createElement('div'); h.className = 'crop-handle ' + cls; cropArea.appendChild(h); }); cropOverlay.appendChild(cropArea); pageDiv.appendChild(cropOverlay); const ctrl = document.createElement('div'); ctrl.style.position = 'absolute'; ctrl.style.bottom = '20px'; ctrl.style.left = '50%'; ctrl.style.transform = 'translateX(-50%)'; ctrl.style.display = 'flex'; ctrl.style.gap = '12px'; ctrl.style.zIndex = '50'; const applyBtn = document.createElement('button'); applyBtn.className = 'btn btn-primary'; applyBtn.innerHTML = '
Apply'; applyBtn.onclick = () => applyCrop(page); const cancelBtn = document.createElement('button'); cancelBtn.className = 'btn btn-outline'; cancelBtn.innerHTML = 'Cancel'; cancelBtn.onclick = exitCrop; ctrl.appendChild(applyBtn); ctrl.appendChild(cancelBtn); cropOverlay.appendChild(ctrl); let dragging = false, resizing = false, dir = ''; let sx, sy, sl, st, sw, sh; cropArea.addEventListener('pointerdown', (e) => { if (e.target === cropArea) { dragging = true; sx = e.clientX; sy = e.clientY; sl = parseFloat(cropArea.style.left); st = parseFloat(cropArea.style.top); } else if (e.target.classList.contains('crop-handle')) { resizing = true; dir = e.target.className.split(' ')[1].split('-')[2]; sx = e.clientX; sy = e.clientY; sl = parseFloat(cropArea.style.left); st = parseFloat(cropArea.style.top); sw = parseFloat(cropArea.style.width); sh = parseFloat(cropArea.style.height); } e.preventDefault(); }); window.addEventListener('pointermove', (e) => { if (!dragging && !resizing) return; const dx = e.clientX - sx, dy = e.clientY - sy; if (dragging) { cropArea.style.left = Math.max(0, sl + dx) + 'px'; cropArea.style.top = Math.max(0, st + dy) + 'px'; } else if (resizing) { let nw = sw, nh = sh, nl = sl, nt = st; if (dir === 'tl') { nl = Math.max(0, sl + dx); nt = Math.max(0, st + dy); nw = Math.max(40, sw - dx); nh = Math.max(40, sh - dy); } else if (dir === 'tr') { nt = Math.max(0, st + dy); nw = Math.max(40, sw + dx); nh = Math.max(40, sh - dy); } else if (dir === 'bl') { nl = Math.max(0, sl + dx); nw = Math.max(40, sw - dx); nh = Math.max(40, sh + dy); } else if (dir === 'br') { nw = Math.max(40, sw + dx); nh = Math.max(40, sh + dy); } cropArea.style.left = nl + 'px'; cropArea.style.top = nt + 'px'; cropArea.style.width = nw + 'px'; cropArea.style.height = nh + 'px'; } }); window.addEventListener('pointerup', () => { dragging = false; resizing = false; }); } function exitCrop() { cropMode = false; cropBtn.classList.remove('active'); if (cropOverlay) { cropOverlay.remove(); cropOverlay = null; cropArea = null; } } function applyCrop(page) { if (!cropArea) return; const pageDiv = document.getElementById(`page${page}`); const canvas = pageDiv?.querySelector('canvas'); if (!canvas) return; const oldCanvas = canvas; const oldOverlays = [...overlays[page] || []]; const cropRect = cropArea.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect(); const scaleX = canvas.width / canvasRect.width; const scaleY = canvas.height / canvasRect.height; const cx = (cropRect.left - canvasRect.left) * scaleX; const cy = (cropRect.top - canvasRect.top) * scaleY; const cw = cropRect.width * scaleX; const ch = cropRect.height * scaleY; const newCanvas = document.createElement('canvas'); newCanvas.width = cw; newCanvas.height = ch; newCanvas.style.width = '100%'; newCanvas.style.height = 'auto'; const ctx = newCanvas.getContext('2d'); ctx.drawImage(canvas, cx, cy, cw, ch, 0, 0, cw, ch); canvas.replaceWith(newCanvas); const overlay = pageDiv.querySelector('.overlay'); overlay.innerHTML = ''; overlays[page] = []; pushAction({ type: 'crop', page, oldCanvas, newCanvas, oldOverlays }); exitCrop(); showStatus('Cropped successfully'); } // Rotate rotateBtn.addEventListener('click', () => { const page = getCurrentPage(); if (!page) return showStatus('Open a document first', true); const pageDiv = document.getElementById(`page${page}`); const canvas = pageDiv.querySelector('canvas'); if (!canvas) return; const old = canvas; const nc = document.createElement('canvas'); nc.width = old.height; nc.height = old.width; const nctx = nc.getContext('2d'); nctx.translate(nc.width/2, nc.height/2); nctx.rotate(Math.PI/2); nctx.drawImage(old, -old.width/2, -old.height/2); nc.style.width = old.style.width; nc.style.height = old.style.height; old.replaceWith(nc); pushAction({ type: 'replaceCanvas', page, oldCanvas: old, newCanvas: nc }); showStatus('Rotated 90°'); }); // Delete deleteBtn.addEventListener('click', () => { if (!selectedOverlay) return showStatus('Select an element first', true); const page = getCurrentPage(); if (!page) return; const el = selectedOverlay; selectedOverlay = null; if (el.parentElement) { el.parentElement.removeChild(el); overlays[page] = overlays[page].filter(x => x !== el); pushAction({ type: 'delete', page, el }); showStatus('Deleted'); } }); // Undo / Redo undoBtn.addEventListener('click', () => { if (!actionStack.length) return showStatus('Nothing to undo'); const a = actionStack.pop(); if (a.type === 'add' || a.type === 'delete') { if (a.type === 'add' && a.el.parentElement) { a.el.parentElement.removeChild(a.el); overlays[a.page] = overlays[a.page].filter(x => x !== a.el); } else if (a.type === 'delete') { const ov = getOverlay(a.page); if (ov) { ov.appendChild(a.el); overlays[a.page].push(a.el); } } redoStack.push(a); } else if (a.type === 'replaceCanvas') { const pd = document.getElementById(`page${a.page}`); if (pd) { const cur = pd.querySelector('canvas'); if (cur) cur.replaceWith(a.oldCanvas); } redoStack.push(a); } else if (a.type === 'drawing') { const pd = document.getElementById(`page${a.page}`); const canvas = pd?.querySelector('.drawing-canvas'); if (canvas) { const img = new Image(); img.onload = () => { const ctx = canvas.getContext('2d'); ctx.clearRect(0,0,canvas.width,canvas.height); ctx.drawImage(img,0,0); }; img.src = a.oldCanvasData; } redoStack.push(a); } else if (a.type === 'crop') { const pd = document.getElementById(`page${a.page}`); if (pd) { const cur = pd.querySelector('canvas'); if (cur) cur.replaceWith(a.oldCanvas); const ov = pd.querySelector('.overlay'); if (ov) { ov.innerHTML = ''; a.oldOverlays?.forEach(el => ov.appendChild(el)); overlays[a.page] = a.oldOverlays || []; } } redoStack.push(a); } }); redoBtn.addEventListener('click', () => { if (!redoStack.length) return showStatus('Nothing to redo'); const a = redoStack.pop(); if (a.type === 'add' || a.type === 'delete') { if (a.type === 'add') { const ov = getOverlay(a.page); if (ov) { ov.appendChild(a.el); overlays[a.page].push(a.el); } } else if (a.type === 'delete' && a.el.parentElement) { a.el.parentElement.removeChild(a.el); overlays[a.page] = overlays[a.page].filter(x => x !== a.el); } actionStack.push(a); } else if (a.type === 'replaceCanvas') { const pd = document.getElementById(`page${a.page}`); if (pd) { const cur = pd.querySelector('canvas'); if (cur) cur.replaceWith(a.newCanvas); } actionStack.push(a); } else if (a.type === 'drawing') { const pd = document.getElementById(`page${a.page}`); const canvas = pd?.querySelector('.drawing-canvas'); if (canvas) { const img = new Image(); img.onload = () => { const ctx = canvas.getContext('2d'); ctx.clearRect(0,0,canvas.width,canvas.height); ctx.drawImage(img,0,0); }; img.src = a.newCanvasData; } actionStack.push(a); } else if (a.type === 'crop') { const pd = document.getElementById(`page${a.page}`); if (pd) { const cur = pd.querySelector('canvas'); if (cur) cur.replaceWith(a.newCanvas); const ov = pd.querySelector('.overlay'); if (ov) { ov.innerHTML = ''; overlays[a.page] = []; } } actionStack.push(a); } }); // Text toolbar function showTextToolbarFor(el) { textToolbar.style.display = 'flex'; const rect = el.getBoundingClientRect(); const tw = 480; let left = rect.left + rect.width/2 - tw/2; left = Math.max(10, left); textToolbar.style.left = left + 'px'; textToolbar.style.top = Math.max(10, rect.top - 52) + 'px'; const fs = parseFloat(window.getComputedStyle(el).fontSize) || 18; textSize.value = Math.round(fs); textColor.value = rgbToHex(window.getComputedStyle(el).color); textFont.value = (window.getComputedStyle(el).fontFamily || 'Inter').split(',')[0].replace(/["']/g,''); textBold.classList.toggle('active', window.getComputedStyle(el).fontWeight >= 600); textItalic.classList.toggle('active', window.getComputedStyle(el).fontStyle === 'italic'); } function hideTextToolbar() { textToolbar.style.display = 'none'; } closeTextToolbar.addEventListener('click', hideTextToolbar); applyTextProps.addEventListener('click', () => { if (!selectedOverlay || selectedOverlay.contentEditable !== 'true') return; const el = selectedOverlay; el.style.fontFamily = textFont.value; el.style.fontSize = (parseInt(textSize.value,10) || 18) + 'px'; el.style.color = textColor.value; el.style.fontWeight = textBold.classList.contains('active') ? '700' : '400'; el.style.fontStyle = textItalic.classList.contains('active') ? 'italic' : 'normal'; showStatus('Text style applied'); }); function rgbToHex(rgb) { const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return '#000000'; return '#' + m.slice(1,4).map(v => parseInt(v).toString(16).padStart(2,'0')).join(''); } // Upload modal uploadBtn.addEventListener('click', () => { uploadModal.style.display = 'flex'; }); cancelUpload.addEventListener('click', () => { uploadModal.style.display = 'none'; fileInput.value = ''; confirmUpload.disabled = true; }); browseBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', () => confirmUpload.disabled = !(fileInput.files.length > 0)); confirmUpload.addEventListener('click', () => { const f = fileInput.files[0]; if (!f) return showStatus('Select a PDF', true); if (f.type !== 'application/pdf') return showStatus('Not a PDF', true); const reader = new FileReader(); reader.onload = (e) => renderPDF(e.target.result, f.name); reader.readAsArrayBuffer(f); uploadModal.style.display = 'none'; fileInput.value = ''; confirmUpload.disabled = true; }); fileDropArea.addEventListener('dragover', e => e.preventDefault()); fileDropArea.addEventListener('drop', e => { e.preventDefault(); const f = e.dataTransfer.files[0]; if (!f || f.type !== 'application/pdf') return showStatus('Drop a PDF file', true); const reader = new FileReader(); reader.onload = (ev) => renderPDF(ev.target.result, f.name); reader.readAsArrayBuffer(f); uploadModal.style.display = 'none'; }); // Reset resetBtn.addEventListener('click', () => { if (!confirm('Reset editor?')) return; clearRendered(); uploadContainer.style.display = 'flex'; pdfContent.style.display = 'none'; }); // Save / Download / Export / Print / Share document.getElementById('saveBtn').addEventListener('click', function() { this.innerHTML = '
Saving'; setTimeout(() => { this.innerHTML = '
Saved'; lastSavedSpan.textContent = new Date().toLocaleTimeString(); setTimeout(() => this.innerHTML = '
Save', 1500); showStatus('Saved'); }, 500); }); document.getElementById('downloadBtn').addEventListener('click', async function() { if (!currentPdf) return showStatus('No document', true); this.innerHTML = '
Preparing'; try { const { jsPDF } = window.jspdf; const pdf = new jsPDF({ unit: 'mm', format: 'a4' }); for (let i = 1; i <= currentPdf.numPages; i++) { const page = await currentPdf.getPage(i); const viewport = page.getViewport({ scale: 1.0 }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; const ctx = canvas.getContext('2d'); await page.render({ canvasContext: ctx, viewport }).promise; const imgData = canvas.toDataURL('image/png'); if (i > 1) pdf.addPage(); const a4w = 210, a4h = 297; const imgProps = pdf.getImageProperties(imgData); const ratio = Math.min(a4w / imgProps.width, a4h / imgProps.height); pdf.addImage(imgData, 'PNG', 0, 0, imgProps.width * ratio, imgProps.height * ratio); } pdf.save((currentFileName || 'document').replace('.pdf','_edited.pdf')); showStatus('Downloaded'); } catch (e) { console.error(e); showStatus('Download failed', true); } this.innerHTML = '
Download'; }); exportBtn.addEventListener('click', async function() { if (!currentPdf) return showStatus('No document', true); this.innerHTML = '
Exporting'; try { const images = await compositeAllPages(); const { jsPDF } = window.jspdf; const pdf = new jsPDF({ unit: 'mm', format: 'a4' }); for (let i = 0; i < images.length; i++) { const img = new Image(); img.src = images[i]; await new Promise(r => { img.onload = r; img.onerror = r; }); const a4w = 210, a4h = 297; const pxToMm = (px) => px * 25.4 / 96; const iw = pxToMm(img.width), ih = pxToMm(img.height); let dw = a4w, dh = (ih * a4w) / iw; if (dh > a4h) { dh = a4h; dw = (iw * a4h) / ih; } const x = (a4w - dw)/2, y = (a4h - dh)/2; if (i > 0) pdf.addPage(); pdf.addImage(images[i], 'PNG', x, y, dw, dh); } pdf.save((currentFileName || 'document').replace('.pdf','_export.pdf')); showStatus('Exported'); } catch (e) { console.error(e); showStatus('Export failed', true); } this.innerHTML = '
Export'; }); async function compositeAllPages() { const pages = document.querySelectorAll('.pdf-page'); const images = []; for (const pg of pages) { const base = pg.querySelector('canvas'); if (!base) continue; const comp = document.createElement('canvas'); comp.width = base.width; comp.height = base.height; const ctx = comp.getContext('2d'); ctx.drawImage(base, 0, 0); const overlay = pg.querySelector('.overlay'); if (overlay) { const rect = base.getBoundingClientRect(); const sx = base.width / rect.width, sy = base.height / rect.height; for (const child of overlay.children) { if (child.tagName === 'CANVAS') { ctx.drawImage(child, 0, 0, child.width, child.height, parseFloat(child.style.left||0)*sx, parseFloat(child.style.top||0)*sy, child.getBoundingClientRect().width * sx, child.getBoundingClientRect().height * sy); } else if (child.tagName === 'IMG') { const img = new Image(); img.src = child.src; await new Promise(r => { img.onload = () => { try { ctx.drawImage(img, parseFloat(child.style.left||0)*sx, parseFloat(child.style.top||0)*sy, child.getBoundingClientRect().width*sx, child.getBoundingClientRect().height*sy); } catch(e){} r(); }; img.onerror = r; }); } else if (child.classList?.contains('editable')) { const style = window.getComputedStyle(child); const l = parseFloat(child.style.left||0)*sx, t = parseFloat(child.style.top||0)*sy; const w = child.getBoundingClientRect().width*sx, h = child.getBoundingClientRect().height*sy; if (style.backgroundColor && style.backgroundColor !== 'transparent' && style.backgroundColor !== 'rgba(0,0,0,0)') { ctx.fillStyle = style.backgroundColor; ctx.fillRect(l, t, w, h); } const text = child.innerText || ''; if (text.trim()) { const fs = parseFloat(style.fontSize) || 16; ctx.fillStyle = style.color || '#000'; ctx.font = `${fs * sx}px ${style.fontFamily || 'Inter'}`; ctx.fillText(text, l+4, t+fs*sx+2); } } } } images.push(comp.toDataURL('image/png')); } return images; } document.getElementById('shareBtn').addEventListener('click', () => showStatus('Share feature coming soon')); document.getElementById('printBtn').addEventListener('click', async () => { if (!currentPdf) return showStatus('No document', true); const imgs = await compositeAllPages(); const win = window.open('', '_blank'); win.document.write(`
Print${imgs.map(s => `
`).join('')}`); win.document.close(); setTimeout(() => { win.focus(); win.print(); }, 400); }); // Click outside to deselect document.addEventListener('pointerdown', (e) => { if (!e.target.closest('.overlay') && !e.target.closest('#textToolbar') && !e.target.closest('.tool-btn')) { selectOverlay(null); } }); // Keyboard delete window.addEventListener('keydown', (e) => { if ((e.key === 'Delete' || e.key === 'Backspace') && selectedOverlay) { const page = getCurrentPage(); if (!page) return; const el = selectedOverlay; selectedOverlay = null; if (el.parentElement) { el.parentElement.removeChild(el); overlays[page] = overlays[page].filter(x => x !== el); pushAction({ type: 'delete', page, el }); showStatus('Deleted'); } } }); // Init clearRendered(); uploadContainer.style.display = 'flex'; pdfContent.style.display = 'none'; })();