Professional PDF Editor – Fixed Zoom & Eraser
100%

Ready to Edit

Upload a PDF to add text, images, highlights, drawings, and more.

Upload PDF
No file A4 Never
`; 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'; })();
zaheerpashamd

Share
Published by
zaheerpashamd

Recent Posts

Audio Converter

🎧 aura convert · professional online audio converter aura convert client‑side MP3 · WAV ·…

6 months ago

Protect PDF

Protect PDF - Add Password | Secure & Free Protect PDF Add password protection to…

10 months ago

PNG to PDF Converter

PNG to PDF Converter PNG to PDF Converter Upload your PNG images and convert them…

10 months ago

PDF to PNG Converter

PDF to PNG Converter PDF to PNG Converter Upload your PDF file and convert it…

10 months ago

Advanced Image Resize Tool

Advanced Image Resize Tool - Free Online Image Resizer Home > Tools > Image Resize…

11 months ago

Geometry Calculator

Geometry Calculator Geometry Calculator Calculate area, perimeter, volume, and other properties of geometric shapes Select…

11 months ago