PDF Editor - WordPress

PDF Editor Ready

Upload a PDF file to start editing or create a new document

Upload PDF File
`; pagesList.appendChild(thumb); thumb.addEventListener('click', () => { pagesList.querySelectorAll('.page-thumb').forEach(t => t.classList.remove('active')); thumb.classList.add('active'); showPage(pageNumber); }); if (pageNumber === 1) { pagesList.querySelectorAll('.page-thumb').forEach(t => t.classList.remove('active')); thumb.classList.add('active'); } } function showPage(pageNumber) { document.querySelectorAll('.pdf-page').forEach(pg => pg.style.display = 'none'); const target = document.getElementById(`page${pageNumber}`); if (target) target.style.display = 'block'; } function clearRenderedPDF() { pdfContent.innerHTML = ''; pagesList.innerHTML = ''; const ph = document.createElement('div'); ph.className = 'page-thumb active'; ph.dataset.page = 'placeholder'; ph.innerHTML = `
No file
`; pagesList.appendChild(ph); filenameSpan.textContent = 'No file'; lastSavedSpan.textContent = 'Never'; currentPdf = null; currentFileName = ''; overlays = {}; actionStack = []; redoStack = []; selectedOverlay = null; drawMode = false; highlightMode = false; commentMode = false; cropMode = false; eraserMode = false; currentDrawingCanvas = null; currentDrawingCtx = null; isDrawing = false; hideTextToolbar(); hideEraserCursor(); exitCropMode(); } function getCurrentVisiblePage() { const pages = document.querySelectorAll('.pdf-page'); for (let p of pages) { if (p.style.display !== 'none') { return Number(p.dataset.page); } } const any = document.querySelector('.pdf-page'); return any ? Number(any.dataset.page) : null; } /* ------------------------ Overlay interactions - FIXED VERSION ------------------------ */ function attachOverlayHandlers(overlayContainer) { overlayContainer.addEventListener('pointerdown', (e) => { // if eraser mode active, handle erasing first if (eraserMode) { handleEraserPointerDown(e, overlayContainer); return; } // if draw mode active and we're clicking on the overlay background if (drawMode && e.target === overlayContainer) { startDrawing(e, overlayContainer); return; } const target = e.target; if (target === overlayContainer) { selectOverlay(null); return; } // Check if we're clicking on an editable element (image, text, etc.) let el = target; while (el && el !== overlayContainer && el !== document.body && !el.classList.contains('editable') && el.tagName !== 'CANVAS' && el.tagName !== 'IMG' && el.tagName !== 'DIV') { el = el.parentElement; } if (el && el !== overlayContainer) { // Only select the element, don't remove it selectOverlay(el); e.stopPropagation(); // If it's a draggable element, start dragging if (el.classList.contains('editable')) { makeDraggable(el, overlayContainer); } } }); } function selectOverlay(el) { if (selectedOverlay && selectedOverlay.classList) { selectedOverlay.classList.remove('selected'); } selectedOverlay = el; if (el && el.classList) { el.classList.add('selected'); // if the element is text, show text toolbar near it (for quick edits) if (el.contentEditable === 'true') { showTextToolbarFor(el); } else { hideTextToolbar(); } } else { hideTextToolbar(); } } // draggable with pointer events - IMPROVED VERSION function makeDraggable(el, container) { el.style.position = 'absolute'; el.style.touchAction = 'none'; let dragging = false; let startX = 0, startY = 0; let origLeft = 0, origTop = 0; const onDown = (ev) => { // Only handle left mouse button or touch if (ev.pointerType === 'mouse' && ev.button !== 0) return; // Prevent eraser from interfering if (eraserMode) return; dragging = true; const contRect = container.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); startX = ev.clientX; startY = ev.clientY; origLeft = elRect.left - contRect.left; origTop = elRect.top - contRect.top; try { el.setPointerCapture(ev.pointerId); } catch(e){} ev.preventDefault(); ev.stopPropagation(); }; const onMove = (ev) => { if (!dragging) return; const dx = ev.clientX - startX; const dy = ev.clientY - startY; el.style.left = Math.max(0, origLeft + dx) + 'px'; el.style.top = Math.max(0, origTop + dy) + 'px'; }; const onUp = (ev) => { if (!dragging) return; dragging = false; try { el.releasePointerCapture(ev.pointerId); } catch(e){} }; el.addEventListener('pointerdown', onDown); window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp); return () => { el.removeEventListener('pointerdown', onDown); window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp); }; } function pushAction(action) { actionStack.push(action); redoStack = []; } function undo() { if (!actionStack.length) return; const a = actionStack.pop(); if (a.type === 'add') { if (a.el && a.el.parentElement) a.el.parentElement.removeChild(a.el); overlays[a.page] = overlays[a.page] ? overlays[a.page].filter(x => x !== a.el) : []; redoStack.push(a); } else if (a.type === 'delete') { const container = document.querySelector(`#page${a.page} .overlay`); if (container && a.el) { container.appendChild(a.el); overlays[a.page] = overlays[a.page] || []; overlays[a.page].push(a.el); } redoStack.push(a); } else if (a.type === 'replaceCanvas') { const pageDiv = document.getElementById(`page${a.page}`); if (pageDiv && a.oldCanvas) { const currentCanvas = pageDiv.querySelector('canvas'); if (currentCanvas) currentCanvas.remove(); pageDiv.insertBefore(a.oldCanvas, pageDiv.querySelector('.overlay')); } redoStack.push(a); } else if (a.type === 'drawing') { const pageDiv = document.getElementById(`page${a.page}`); if (pageDiv && a.oldCanvasData) { const canvas = pageDiv.querySelector('.drawing-canvas'); if (canvas) { const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { 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 pageDiv = document.getElementById(`page${a.page}`); if (pageDiv && a.oldCanvas) { const currentCanvas = pageDiv.querySelector('canvas'); if (currentCanvas) currentCanvas.remove(); pageDiv.insertBefore(a.oldCanvas, pageDiv.querySelector('.overlay')); // Restore overlays if (a.oldOverlays) { const overlay = pageDiv.querySelector('.overlay'); overlay.innerHTML = ''; a.oldOverlays.forEach(el => overlay.appendChild(el)); overlays[a.page] = a.oldOverlays; } } redoStack.push(a); } } function redo() { if (!redoStack.length) return; const a = redoStack.pop(); if (a.type === 'add') { const container = document.querySelector(`#page${a.page} .overlay`); if (container && a.el) { container.appendChild(a.el); overlays[a.page] = overlays[a.page] || []; overlays[a.page].push(a.el); } actionStack.push(a); } else if (a.type === 'delete') { if (a.el && a.el.parentElement) a.el.parentElement.removeChild(a.el); overlays[a.page] = overlays[a.page] ? overlays[a.page].filter(x => x !== a.el) : []; actionStack.push(a); } else if (a.type === 'replaceCanvas') { const pageDiv = document.getElementById(`page${a.page}`); if (pageDiv && a.newCanvas) { const currentCanvas = pageDiv.querySelector('canvas'); if (currentCanvas) currentCanvas.remove(); pageDiv.insertBefore(a.newCanvas, pageDiv.querySelector('.overlay')); } actionStack.push(a); } else if (a.type === 'drawing') { const pageDiv = document.getElementById(`page${a.page}`); if (pageDiv && a.newCanvasData) { const canvas = pageDiv.querySelector('.drawing-canvas'); if (canvas) { const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { 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 pageDiv = document.getElementById(`page${a.page}`); if (pageDiv && a.newCanvas) { const currentCanvas = pageDiv.querySelector('canvas'); if (currentCanvas) currentCanvas.remove(); pageDiv.insertBefore(a.newCanvas, pageDiv.querySelector('.overlay')); // Clear overlays const overlay = pageDiv.querySelector('.overlay'); overlay.innerHTML = ''; overlays[a.page] = []; } actionStack.push(a); } } undoBtn.addEventListener('click', () => undo()); redoBtn.addEventListener('click', () => redo()); /* --------------------------- Add Text (with toolbar UI) --------------------------- */ addTextBtn.addEventListener('click', () => { const pageNum = getCurrentVisiblePage(); if (!pageNum) return showStatus('Open a document first.', true); const overlay = document.querySelector(`#page${pageNum} .overlay`); 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 = '80px'; txt.style.minHeight = '24px'; txt.style.background = 'transparent'; txt.style.padding = '6px'; txt.style.border = '1px dashed rgba(0,0,0,0.15)'; txt.style.zIndex = 1000; txt.style.whiteSpace = 'pre-wrap'; txt.style.fontSize = '16px'; txt.style.color = '#000'; txt.style.fontFamily = 'Segoe UI'; overlay.appendChild(txt); makeDraggable(txt, overlay); overlays[pageNum] = overlays[pageNum] || []; overlays[pageNum].push(txt); pushAction({type:'add', page: pageNum, el: txt}); selectOverlay(txt); txt.focus(); // show toolbar near the added element showTextToolbarFor(txt); showStatus('Text added. Click and drag to move, or double-click to edit.'); }); // Show/hide/apply text toolbar function showTextToolbarFor(el) { if (!el) return; textToolbar.style.display = 'flex'; textToolbar.setAttribute('aria-hidden', 'false'); // set toolbar controls to current element style const rect = el.getBoundingClientRect(); const toolbarWidth = 420; let left = rect.left + (rect.width/2) - toolbarWidth/2; left = Math.max(8, left); textToolbar.style.left = left + 'px'; textToolbar.style.top = Math.max(8, (rect.top - 48)) + 'px'; // parse style const fs = parseFloat(window.getComputedStyle(el).fontSize) || 16; textSize.value = Math.round(fs); textColor.value = rgbToHex(window.getComputedStyle(el).color || '#000000'); textFont.value = (window.getComputedStyle(el).fontFamily || 'Segoe UI').split(',')[0].replace(/["']/g,''); textBold.classList.toggle('active', window.getComputedStyle(el).fontWeight >= 600 || window.getComputedStyle(el).fontWeight === 'bold'); textItalic.classList.toggle('active', window.getComputedStyle(el).fontStyle === 'italic'); } function hideTextToolbar() { textToolbar.style.display = 'none'; textToolbar.setAttribute('aria-hidden', 'true'); } closeTextToolbar.addEventListener('click', () => hideTextToolbar()); applyTextProps.addEventListener('click', () => { if (!selectedOverlay) return; applyPropertiesToTextElement(selectedOverlay); showStatus('Text properties applied'); }); textBold.addEventListener('click', () => textBold.classList.toggle('active')); textItalic.addEventListener('click', () => textItalic.classList.toggle('active')); function applyPropertiesToTextElement(el) { if (!el || el.contentEditable !== 'true') return; el.style.fontFamily = textFont.value; el.style.fontSize = (parseInt(textSize.value,10) || 16) + 'px'; el.style.color = textColor.value; el.style.fontWeight = textBold.classList.contains('active') ? '700' : '400'; el.style.fontStyle = textItalic.classList.contains('active') ? 'italic' : 'normal'; } function rgbToHex(rgb) { // rgb like "rgb(0, 0, 0)" or "rgba(0,0,0,1)" const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return '#000000'; const r = parseInt(m[1]).toString(16).padStart(2,'0'); const g = parseInt(m[2]).toString(16).padStart(2,'0'); const b = parseInt(m[3]).toString(16).padStart(2,'0'); return `#${r}${g}${b}`; } /* --------------------------- Add Image - FIXED VERSION --------------------------- */ addImageBtn.addEventListener('click', () => { const pageNum = getCurrentVisiblePage(); if (!pageNum) 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 = document.querySelector(`#page${pageNum} .overlay`); const img = document.createElement('img'); img.src = e.target.result; img.className = 'editable'; img.style.left = '20px'; img.style.top = '20px'; img.style.width = '180px'; img.style.height = 'auto'; img.style.maxWidth = '45%'; img.style.maxHeight = '45%'; img.style.zIndex = 1000; img.style.objectFit = 'contain'; img.draggable = false; // Prevent native drag behavior overlay.appendChild(img); makeDraggable(img, overlay); overlays[pageNum] = overlays[pageNum] || []; overlays[pageNum].push(img); pushAction({type:'add', page: pageNum, el: img}); selectOverlay(img); showStatus('Image added. Click and drag to move.'); }; reader.readAsDataURL(f); imageInput.value = ''; }; }); /* --------------------------- Draw (freehand) --------------------------- */ drawBtn.addEventListener('click', () => { drawMode = !drawMode; // disable eraser if draw mode is toggled on if (drawMode) { eraserMode = false; eraserBtn.classList.remove('active'); hideEraserCursor(); } drawBtn.classList.toggle('active', drawMode); if (!drawMode) { // Stop drawing if we're turning it off stopDrawing(); } showStatus(drawMode ? 'Drawing mode activated. Click and drag to draw.' : 'Drawing mode deactivated.'); }); function startDrawing(e, overlayContainer) { if (!drawMode) return; const pageNum = getCurrentVisiblePage(); if (!pageNum) return; // Get or create drawing canvas let drawingCanvas = overlayContainer.querySelector('.drawing-canvas'); if (!drawingCanvas) { drawingCanvas = document.createElement('canvas'); drawingCanvas.className = 'drawing-canvas'; drawingCanvas.style.position = 'absolute'; drawingCanvas.style.left = '0'; drawingCanvas.style.top = '0'; drawingCanvas.style.width = '100%'; drawingCanvas.style.height = '100%'; drawingCanvas.style.zIndex = '500'; drawingCanvas.style.pointerEvents = 'none'; // Set canvas dimensions to match the base canvas const baseCanvas = document.querySelector(`#page${pageNum} canvas`); drawingCanvas.width = baseCanvas.width; drawingCanvas.height = baseCanvas.height; overlayContainer.appendChild(drawingCanvas); overlays[pageNum] = overlays[pageNum] || []; overlays[pageNum].push(drawingCanvas); } currentDrawingCanvas = drawingCanvas; currentDrawingCtx = drawingCanvas.getContext('2d'); // Set drawing style currentDrawingCtx.lineJoin = 'round'; currentDrawingCtx.lineCap = 'round'; currentDrawingCtx.lineWidth = 4; currentDrawingCtx.strokeStyle = '#000000'; const rect = drawingCanvas.getBoundingClientRect(); const scaleX = drawingCanvas.width / rect.width; const scaleY = drawingCanvas.height / rect.height; const startX = (e.clientX - rect.left) * scaleX; const startY = (e.clientY - rect.top) * scaleY; // Save initial state for undo const oldCanvasData = drawingCanvas.toDataURL(); currentDrawingCtx.beginPath(); currentDrawingCtx.moveTo(startX, startY); isDrawing = true; function drawMove(moveEvent) { if (!isDrawing) return; const moveX = (moveEvent.clientX - rect.left) * scaleX; const moveY = (moveEvent.clientY - rect.top) * scaleY; currentDrawingCtx.lineTo(moveX, moveY); currentDrawingCtx.stroke(); } function drawEnd() { if (!isDrawing) return; isDrawing = false; currentDrawingCtx.closePath(); // Save action for undo const newCanvasData = drawingCanvas.toDataURL(); pushAction({ type: 'drawing', page: pageNum, oldCanvasData: oldCanvasData, newCanvasData: newCanvasData }); window.removeEventListener('pointermove', drawMove); window.removeEventListener('pointerup', drawEnd); } window.addEventListener('pointermove', drawMove); window.addEventListener('pointerup', drawEnd); } function stopDrawing() { isDrawing = false; if (currentDrawingCtx) { currentDrawingCtx.closePath(); } } /* --------------------------- Highlight & Comment (rect draw) --------------------------- */ highlightBtn.addEventListener('click', () => { highlightMode = !highlightMode; highlightBtn.classList.toggle('active', highlightMode); if (highlightMode) { // Disable other modes commentMode = false; commentBtn.classList.remove('active'); startRectDraw('highlight'); showStatus('Highlight mode activated. Click and drag to create a highlight area.'); } else { stopRectDraw(); } }); commentBtn.addEventListener('click', () => { commentMode = !commentMode; commentBtn.classList.toggle('active', commentMode); if (commentMode) { // Disable other modes highlightMode = false; highlightBtn.classList.remove('active'); startRectDraw('comment'); showStatus('Comment mode activated. Click and drag to create a comment area.'); } else { stopRectDraw(); } }); let rectDrawActive = false; let rectStartX, rectStartY, rectCurrentX, rectCurrentY; let rectPreview; function startRectDraw(type) { const pageNum = getCurrentVisiblePage(); if (!pageNum) return; const overlay = document.querySelector(`#page${pageNum} .overlay`); if (!overlay) return; // Create preview rectangle rectPreview = document.createElement('div'); rectPreview.style.position = 'absolute'; rectPreview.style.border = '2px dashed #3498db'; rectPreview.style.backgroundColor = type === 'highlight' ? 'rgba(255,255,0,0.3)' : 'rgba(173,216,230,0.3)'; rectPreview.style.pointerEvents = 'none'; rectPreview.style.zIndex = '1000'; rectPreview.style.display = 'none'; overlay.appendChild(rectPreview); rectDrawActive = true; function handleMouseDown(e) { if (!rectDrawActive) return; const rect = overlay.getBoundingClientRect(); rectStartX = e.clientX - rect.left; rectStartY = e.clientY - rect.top; rectPreview.style.left = rectStartX + 'px'; rectPreview.style.top = rectStartY + 'px'; rectPreview.style.width = '0px'; rectPreview.style.height = '0px'; rectPreview.style.display = 'block'; function handleMouseMove(moveE) { if (!rectDrawActive) return; rectCurrentX = moveE.clientX - rect.left; rectCurrentY = moveE.clientY - rect.top; const left = Math.min(rectStartX, rectCurrentX); const top = Math.min(rectStartY, rectCurrentY); const width = Math.abs(rectCurrentX - rectStartX); const height = Math.abs(rectCurrentY - rectStartY); rectPreview.style.left = left + 'px'; rectPreview.style.top = top + 'px'; rectPreview.style.width = width + 'px'; rectPreview.style.height = height + 'px'; } function handleMouseUp() { if (!rectDrawActive) return; const left = parseFloat(rectPreview.style.left); const top = parseFloat(rectPreview.style.top); const width = parseFloat(rectPreview.style.width); const height = parseFloat(rectPreview.style.height); if (width > 10 && height > 10) { createRectElement(type, pageNum, left, top, width, height); } // Clean up rectPreview.remove(); window.removeEventListener('pointermove', handleMouseMove); window.removeEventListener('pointerup', handleMouseUp); overlay.removeEventListener('pointerdown', handleMouseDown); rectDrawActive = false; // Reset modes highlightMode = false; commentMode = false; highlightBtn.classList.remove('active'); commentBtn.classList.remove('active'); } window.addEventListener('pointermove', handleMouseMove); window.addEventListener('pointerup', handleMouseUp); } overlay.addEventListener('pointerdown', handleMouseDown); } function stopRectDraw() { rectDrawActive = false; if (rectPreview && rectPreview.parentElement) { rectPreview.remove(); } } function createRectElement(type, pageNum, left, top, width, height) { const overlay = document.querySelector(`#page${pageNum} .overlay`); if (!overlay) return; const element = document.createElement('div'); element.className = 'editable'; element.style.left = left + 'px'; element.style.top = top + 'px'; element.style.width = width + 'px'; element.style.height = height + 'px'; element.style.zIndex = '800'; if (type === 'highlight') { element.style.backgroundColor = 'rgba(255,255,0,0.4)'; } else { // Comment element.style.backgroundColor = 'rgba(173,216,230,0.7)'; element.style.border = '1px solid #87CEEB'; element.contentEditable = 'true'; element.innerText = 'Type your comment here...'; element.style.padding = '5px'; element.style.fontSize = '12px'; element.style.overflow = 'hidden'; } overlay.appendChild(element); makeDraggable(element, overlay); overlays[pageNum] = overlays[pageNum] || []; overlays[pageNum].push(element); pushAction({type:'add', page: pageNum, el: element}); if (type === 'comment') { selectOverlay(element); element.focus(); } } /* --------------------------- Sign (upload) - FIXED VERSION --------------------------- */ signBtn.addEventListener('click', () => { const pageNum = getCurrentVisiblePage(); if (!pageNum) 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 = document.querySelector(`#page${pageNum} .overlay`); const img = document.createElement('img'); img.src = e.target.result; img.className = 'editable'; img.style.left = '20px'; img.style.top = '20px'; img.style.width = '200px'; img.style.height = 'auto'; img.style.zIndex = 1000; img.style.objectFit = 'contain'; img.draggable = false; // Prevent native drag behavior overlay.appendChild(img); makeDraggable(img, overlay); overlays[pageNum] = overlays[pageNum] || []; overlays[pageNum].push(img); pushAction({type:'add', page: pageNum, el: img}); selectOverlay(img); showStatus('Signature added. Click and drag to move.'); }; reader.readAsDataURL(f); signInput.value = ''; }; }); /* --------------------------- Rotate (actual canvas rotation) --------------------------- */ rotateBtn.addEventListener('click', () => { const pageNum = getCurrentVisiblePage(); if (!pageNum) return showStatus('Open a document first.', true); const pageDiv = document.getElementById(`page${pageNum}`); const canvas = pageDiv.querySelector('canvas'); if (!canvas) return; const oldCanvas = canvas; const newCanvas = document.createElement('canvas'); newCanvas.width = oldCanvas.height; newCanvas.height = oldCanvas.width; const nctx = newCanvas.getContext('2d'); nctx.translate(newCanvas.width / 2, newCanvas.height / 2); nctx.rotate(Math.PI / 2); nctx.drawImage(oldCanvas, -oldCanvas.width / 2, -oldCanvas.height / 2); newCanvas.style.width = oldCanvas.style.width; newCanvas.style.height = oldCanvas.style.height; oldCanvas.replaceWith(newCanvas); pushAction({ type: 'replaceCanvas', page: pageNum, oldCanvas: oldCanvas, newCanvas: newCanvas }); showStatus('Page rotated 90 degrees'); }); /* --------------------------- Crop - FIXED VERSION --------------------------- */ cropBtn.addEventListener('click', () => { const pageNum = getCurrentVisiblePage(); if (!pageNum) return showStatus('Open a document first.', true); cropMode = !cropMode; cropBtn.classList.toggle('active', cropMode); if (cropMode) { startCropMode(pageNum); showStatus('Crop mode activated. Drag to select area, then click Apply.'); } else { exitCropMode(); } }); function startCropMode(pageNum) { const pageDiv = document.getElementById(`page${pageNum}`); if (!pageDiv) return; // Create crop overlay cropOverlay = document.createElement('div'); cropOverlay.className = 'crop-overlay'; cropOverlay.style.display = 'block'; // Create crop area cropArea = document.createElement('div'); cropArea.className = 'crop-area'; cropArea.style.left = '50px'; cropArea.style.top = '50px'; cropArea.style.width = '200px'; cropArea.style.height = '150px'; // Add resize handles const handles = [ {className: 'crop-handle crop-handle-tl', cursor: 'nw-resize'}, {className: 'crop-handle crop-handle-tr', cursor: 'ne-resize'}, {className: 'crop-handle crop-handle-bl', cursor: 'sw-resize'}, {className: 'crop-handle crop-handle-br', cursor: 'se-resize'} ]; handles.forEach(handle => { const handleEl = document.createElement('div'); handleEl.className = handle.className; handleEl.style.cursor = handle.cursor; cropArea.appendChild(handleEl); }); cropOverlay.appendChild(cropArea); pageDiv.appendChild(cropOverlay); // Add crop controls const cropControls = document.createElement('div'); cropControls.style.position = 'absolute'; cropControls.style.bottom = '20px'; cropControls.style.left = '50%'; cropControls.style.transform = 'translateX(-50%)'; cropControls.style.display = 'flex'; cropControls.style.gap = '10px'; cropControls.style.zIndex = '4000'; const applyBtn = document.createElement('button'); applyBtn.className = 'btn btn-primary'; applyBtn.innerHTML = ' Apply Crop'; applyBtn.addEventListener('click', () => applyCrop(pageNum)); const cancelBtn = document.createElement('button'); cancelBtn.className = 'btn btn-outline'; cancelBtn.innerHTML = ' Cancel'; cancelBtn.addEventListener('click', exitCropMode); cropControls.appendChild(applyBtn); cropControls.appendChild(cancelBtn); cropOverlay.appendChild(cropControls); // Make crop area draggable and resizable makeCropAreaInteractive(cropArea, cropOverlay); } function makeCropAreaInteractive(cropArea, container) { let isDragging = false; let isResizing = false; let startX, startY; let startLeft, startTop, startWidth, startHeight; let resizeDirection = ''; cropArea.addEventListener('pointerdown', (e) => { if (e.target === cropArea) { // Dragging the entire crop area isDragging = true; startX = e.clientX; startY = e.clientY; startLeft = parseFloat(cropArea.style.left); startTop = parseFloat(cropArea.style.top); } else if (e.target.classList.contains('crop-handle')) { // Resizing isResizing = true; startX = e.clientX; startY = e.clientY; startLeft = parseFloat(cropArea.style.left); startTop = parseFloat(cropArea.style.top); startWidth = parseFloat(cropArea.style.width); startHeight = parseFloat(cropArea.style.height); // Determine resize direction based on handle class if (e.target.classList.contains('crop-handle-tl')) resizeDirection = 'tl'; else if (e.target.classList.contains('crop-handle-tr')) resizeDirection = 'tr'; else if (e.target.classList.contains('crop-handle-bl')) resizeDirection = 'bl'; else if (e.target.classList.contains('crop-handle-br')) resizeDirection = 'br'; } e.preventDefault(); e.stopPropagation(); }); function onPointerMove(e) { if (isDragging) { const dx = e.clientX - startX; const dy = e.clientY - startY; const containerRect = container.getBoundingClientRect(); const newLeft = Math.max(0, Math.min(containerRect.width - parseFloat(cropArea.style.width), startLeft + dx)); const newTop = Math.max(0, Math.min(containerRect.height - parseFloat(cropArea.style.height), startTop + dy)); cropArea.style.left = newLeft + 'px'; cropArea.style.top = newTop + 'px'; } else if (isResizing) { const dx = e.clientX - startX; const dy = e.clientY - startY; const containerRect = container.getBoundingClientRect(); if (resizeDirection === 'tl') { const newLeft = Math.max(0, startLeft + dx); const newTop = Math.max(0, startTop + dy); const newWidth = Math.max(50, startWidth - dx); const newHeight = Math.max(50, startHeight - dy); cropArea.style.left = newLeft + 'px'; cropArea.style.top = newTop + 'px'; cropArea.style.width = newWidth + 'px'; cropArea.style.height = newHeight + 'px'; } else if (resizeDirection === 'tr') { const newTop = Math.max(0, startTop + dy); const newWidth = Math.max(50, startWidth + dx); const newHeight = Math.max(50, startHeight - dy); cropArea.style.top = newTop + 'px'; cropArea.style.width = newWidth + 'px'; cropArea.style.height = newHeight + 'px'; } else if (resizeDirection === 'bl') { const newLeft = Math.max(0, startLeft + dx); const newWidth = Math.max(50, startWidth - dx); const newHeight = Math.max(50, startHeight + dy); cropArea.style.left = newLeft + 'px'; cropArea.style.width = newWidth + 'px'; cropArea.style.height = newHeight + 'px'; } else if (resizeDirection === 'br') { const newWidth = Math.max(50, startWidth + dx); const newHeight = Math.max(50, startHeight + dy); cropArea.style.width = newWidth + 'px'; cropArea.style.height = newHeight + 'px'; } } } function onPointerUp() { isDragging = false; isResizing = false; resizeDirection = ''; window.removeEventListener('pointermove', onPointerMove); window.removeEventListener('pointerup', onPointerUp); } window.addEventListener('pointermove', onPointerMove); window.addEventListener('pointerup', onPointerUp); } function applyCrop(pageNum) { const pageDiv = document.getElementById(`page${pageNum}`); if (!pageDiv || !cropArea) return; const canvas = pageDiv.querySelector('canvas'); if (!canvas) return; // Save current state for undo const oldCanvas = canvas; const oldOverlays = [...(overlays[pageNum] || [])]; // Get crop coordinates and dimensions const cropRect = cropArea.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect(); // Calculate scale factors const scaleX = canvas.width / canvasRect.width; const scaleY = canvas.height / canvasRect.height; // Calculate crop area in canvas coordinates const cropX = (cropRect.left - canvasRect.left) * scaleX; const cropY = (cropRect.top - canvasRect.top) * scaleY; const cropWidth = cropRect.width * scaleX; const cropHeight = cropRect.height * scaleY; // Create new canvas with cropped dimensions const newCanvas = document.createElement('canvas'); newCanvas.width = cropWidth; newCanvas.height = cropHeight; newCanvas.style.width = '100%'; newCanvas.style.height = 'auto'; // Draw cropped portion to new canvas const ctx = newCanvas.getContext('2d'); ctx.drawImage(canvas, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight); // Replace old canvas with new one canvas.replaceWith(newCanvas); // Clear overlays for this page const overlay = pageDiv.querySelector('.overlay'); overlay.innerHTML = ''; overlays[pageNum] = []; // Save action for undo pushAction({ type: 'crop', page: pageNum, oldCanvas: oldCanvas, newCanvas: newCanvas, oldOverlays: oldOverlays }); exitCropMode(); showStatus('Page cropped successfully'); } function exitCropMode() { cropMode = false; cropBtn.classList.remove('active'); if (cropOverlay && cropOverlay.parentElement) { cropOverlay.parentElement.removeChild(cropOverlay); } cropOverlay = null; cropArea = null; } /* --------------------------- Delete selected overlay --------------------------- */ deleteBtn.addEventListener('click', () => { if (!selectedOverlay) return showStatus('Select an overlay to delete (click the overlay).', true); const pageNum = getCurrentVisiblePage(); if (!pageNum) return; const el = selectedOverlay; selectedOverlay = null; if (el && el.parentElement) { el.parentElement.removeChild(el); overlays[pageNum] = overlays[pageNum] ? overlays[pageNum].filter(x => x !== el) : []; pushAction({type:'delete', page: pageNum, el: el}); showStatus('Element deleted'); } }); /* --------------------------- Keyboard delete --------------------------- */ window.addEventListener('keydown', (e) => { if (e.key === 'Delete' || e.key === 'Backspace') { if (selectedOverlay) { const pageNum = getCurrentVisiblePage(); if (!pageNum) return; const el = selectedOverlay; selectedOverlay = null; if (el && el.parentElement) { el.parentElement.removeChild(el); overlays[pageNum] = overlays[pageNum] ? overlays[pageNum].filter(x => x !== el) : []; pushAction({type:'delete', page: pageNum, el: el}); showStatus('Element deleted'); } } } }); /* --------------------------- Reset / Save / Download / Share --------------------------- */ resetBtn.addEventListener('click', () => { if (!confirm('Reset the editor? This will remove the currently loaded document from view.')) return; clearRenderedPDF(); uploadContainer.style.display = 'flex'; pdfContent.style.display = 'none'; resetFileInput(); showStatus('Editor reset'); }); document.getElementById('saveBtn').addEventListener('click', function() { this.innerHTML = ' Saving...'; setTimeout(() => { this.innerHTML = ' Saved'; lastSavedSpan.textContent = new Date().toLocaleTimeString(); setTimeout(() => this.innerHTML = ' Save', 1500); showStatus('Document saved'); }, 900); }); document.getElementById('downloadBtn').addEventListener('click', async function() { if (!currentPdf) return showStatus('No document loaded to download.', true); this.innerHTML = ' Preparing...'; try { // Create a new PDF with the original content and overlays const { jsPDF } = window.jspdf; const pdf = new jsPDF({ unit: 'mm', format: 'a4' }); // For each page, add the original PDF page for (let i = 1; i <= currentPdf.numPages; i++) { if (i > 1) pdf.addPage(); // Get the page const page = await currentPdf.getPage(i); const viewport = page.getViewport({ scale: 1.0 }); // Create a canvas to render the page const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; const ctx = canvas.getContext('2d'); // Render the page to the canvas await page.render({ canvasContext: ctx, viewport }).promise; // Convert canvas to image const imgData = canvas.toDataURL('image/png'); // Add image to PDF (scaled to A4) const a4w = 210; // A4 width in mm const a4h = 297; // A4 height in mm const imgProps = pdf.getImageProperties(imgData); const imgWidth = imgProps.width; const imgHeight = imgProps.height; const ratio = Math.min(a4w / imgWidth, a4h / imgHeight); pdf.addImage(imgData, 'PNG', 0, 0, imgWidth * ratio, imgHeight * ratio); } // Save the PDF pdf.save(currentFileName.replace('.pdf', '_edited.pdf')); showStatus('Document downloaded successfully'); } catch (err) { console.error(err); showStatus('Download failed. See console for details.', true); } finally { this.innerHTML = ' Download'; } }); document.getElementById('shareBtn').addEventListener('click', () => alert('Share options would appear here in a real implementation.')); /* --------------------------- Print (composite and open print window) --------------------------- */ document.getElementById('printBtn').addEventListener('click', async () => { if (!currentPdf) return showStatus('No document to print.', true); try { const images = await compositeAllPagesAsImages(); const printWindow = window.open('', '_blank'); const html = []; html.push('Print PDF'); html.push(''); html.push(''); for (const src of images) { html.push(``); } html.push(''); printWindow.document.open(); printWindow.document.write(html.join('')); printWindow.document.close(); setTimeout(()=>{ printWindow.focus(); printWindow.print(); }, 600); showStatus('Print dialog opened'); } catch (err) { console.error(err); showStatus('Printing failed.', true); } }); /* --------------------------- Export to PDF (html2canvas + jsPDF) --------------------------- */ exportBtn.addEventListener('click', async () => { if (!currentPdf) return showStatus('No document to export.', true); exportBtn.innerHTML = ' Exporting...'; try { // create images for each page const images = await compositeAllPagesAsImages(); // instantiate jsPDF const { jsPDF } = window.jspdf; // A4 size in pts: 595.28 x 841.89 (points) -> use px conversion; we'll use mm as easier: 210 x 297 mm const pdf = new jsPDF({ unit: 'mm', format: 'a4' }); for (let i = 0; i < images.length; i++) { const imgData = images[i]; // create an image object to get dimensions const img = new Image(); img.src = imgData; await new Promise((res) => { img.onload = res; img.onerror = res; }); // compute width/height maintaining aspect ratio to fit A4 (210 x 297 mm) const a4w = 210, a4h = 297; // convert image pixel dims to mm roughly: assume 96dpi -> px * 25.4/96 const pxToMm = (px) => px * 25.4 / 96; const iw = pxToMm(img.width); const ih = pxToMm(img.height); let drawW = a4w, drawH = (ih * a4w) / iw; if (drawH > a4h) { drawH = a4h; drawW = (iw * a4h) / ih; } const x = (a4w - drawW) / 2; const y = (a4h - drawH) / 2; if (i > 0) pdf.addPage(); pdf.addImage(imgData, 'PNG', x, y, drawW, drawH); } pdf.save((currentFileName || 'document').replace(/\s+/g,'_') + '_export.pdf'); showStatus('Document exported successfully'); } catch (err) { console.error(err); showStatus('Export failed. See console.', true); } finally { exportBtn.innerHTML = ' Export'; } }); // helper: create composite images of each page (base canvas + overlays) async function compositeAllPagesAsImages() { const pages = document.querySelectorAll('.pdf-page'); const images = []; for (const pg of pages) { const pageNum = Number(pg.dataset.page); const baseCanvas = pg.querySelector('canvas'); if (!baseCanvas) continue; // composite const composite = document.createElement('canvas'); composite.width = baseCanvas.width; composite.height = baseCanvas.height; const ctx = composite.getContext('2d'); ctx.drawImage(baseCanvas, 0, 0); const overlay = pg.querySelector('.overlay'); if (overlay) { const baseRect = baseCanvas.getBoundingClientRect(); const scaleX = baseCanvas.width / baseRect.width; const scaleY = baseCanvas.height / baseRect.height; // process overlay children in order const children = Array.from(overlay.children); for (const child of children) { const style = window.getComputedStyle(child); const left = parseFloat(child.style.left || 0); const top = parseFloat(child.style.top || 0); const width = child.getBoundingClientRect().width; const height = child.getBoundingClientRect().height; const dx = Math.round(left * scaleX); const dy = Math.round(top * scaleY); const dw = Math.round(width * scaleX); const dh = Math.round(height * scaleY); if (child.tagName === 'CANVAS') { try { ctx.drawImage(child, 0, 0, child.width, child.height, dx, dy, dw, dh); } catch(e){ console.warn(e); } } else if (child.tagName === 'IMG') { const img = new Image(); img.src = child.src; await new Promise((res) => { img.onload = () => { try { ctx.drawImage(img, dx, dy, dw, dh); } catch(e){} res(); }; img.onerror = res; }); } else { // background const bg = style.backgroundColor; if (bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)') { ctx.fillStyle = bg; ctx.fillRect(dx, dy, dw, dh); } if (style.borderStyle && style.borderStyle !== 'none') { ctx.strokeStyle = style.borderColor || '#000'; ctx.strokeRect(dx, dy, dw, dh); } const text = child.innerText || child.textContent || ''; if (text.trim()) { const fontSize = parseFloat(style.fontSize) || 16; const fontFamily = style.fontFamily || 'Arial'; ctx.fillStyle = style.color || '#000'; // scale font using scaleX ctx.font = `${fontSize * scaleX}px ${fontFamily}`; const words = text.split(/\s+/); let line = ''; const lineHeight = fontSize * scaleX * 1.15; let y = dy + lineHeight; for (let n = 0; n < words.length; n++) { const testLine = line + words[n] + ' '; const metrics = ctx.measureText(testLine); if (metrics.width > dw && n > 0) { ctx.fillText(line, dx + 2, y); line = words[n] + ' '; y += lineHeight; } else { line = testLine; } } if (line) ctx.fillText(line, dx + 2, y); } } } } images.push(composite.toDataURL('image/png')); } return images; } /* --------------------------- Eraser tool - FIXED VERSION --------------------------- */ eraserBtn.addEventListener('click', () => { eraserMode = !eraserMode; // disable draw mode if eraser enabled if (eraserMode) { drawMode = false; drawBtn.classList.remove('active'); stopDrawing(); } eraserBtn.classList.toggle('active', eraserMode); if (eraserMode) { showEraserCursor(); showStatus('Eraser mode activated. Click on elements to remove them.'); } else { hideEraserCursor(); } }); function showEraserCursor() { eraserCursor.style.display = 'block'; document.addEventListener('mousemove', updateEraserCursor); // Set cursor style for the entire document document.body.style.cursor = 'none'; } function hideEraserCursor() { eraserCursor.style.display = 'none'; document.removeEventListener('mousemove', updateEraserCursor); // Restore default cursor document.body.style.cursor = 'default'; } function updateEraserCursor(e) { eraserCursor.style.left = (e.clientX - ERASER_SIZE/2) + 'px'; eraserCursor.style.top = (e.clientY - ERASER_SIZE/2) + 'px'; } function handleEraserPointerDown(e, overlayContainer) { if (!eraserMode) return; const pageNum = Number(overlayContainer.dataset.page); // Check if we're clicking on a drawing canvas first const drawingCanvas = overlayContainer.querySelector('.drawing-canvas'); if (drawingCanvas && isPointOverDrawingCanvas(e, drawingCanvas)) { // Erase on drawing canvas const canvas = drawingCanvas; 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'); // Save initial state for undo const oldCanvasData = canvas.toDataURL(); // Use globalCompositeOperation = 'destination-out' to erase ctx.save(); ctx.globalCompositeOperation = 'destination-out'; ctx.beginPath(); ctx.arc(x, y, ERASER_SIZE * scaleX, 0, Math.PI * 2); ctx.fill(); ctx.closePath(); ctx.restore(); // Handle dragging erase continuous function eraseMove(ev) { if (!eraserMode) return; const moveX = (ev.clientX - rect.left) * scaleX; const moveY = (ev.clientY - rect.top) * scaleY; ctx.save(); ctx.globalCompositeOperation = 'destination-out'; ctx.beginPath(); ctx.arc(moveX, moveY, ERASER_SIZE * scaleX, 0, Math.PI * 2); ctx.fill(); ctx.closePath(); ctx.restore(); } function eraseUp() { // Save action for undo const newCanvasData = canvas.toDataURL(); pushAction({ type: 'drawing', page: pageNum, oldCanvasData: oldCanvasData, newCanvasData: newCanvasData }); window.removeEventListener('pointermove', eraseMove); window.removeEventListener('pointerup', eraseUp); } window.addEventListener('pointermove', eraseMove); window.addEventListener('pointerup', eraseUp); e.stopPropagation(); return; } // If not a drawing canvas, try to find and remove an editable element const elements = document.elementsFromPoint(e.clientX, e.clientY); const editableElement = elements.find(el => el.classList && el.classList.contains('editable') && el.parentElement === overlayContainer ); // Check if we're currently dragging - if so, don't remove the element const isCurrentlyDragging = document.querySelector('.editable.selected') !== null; if (editableElement && !isCurrentlyDragging) { if (editableElement.parentElement) { // If we're removing the selected overlay, clear selection if (selectedOverlay === editableElement) { selectedOverlay = null; hideTextToolbar(); } editableElement.parentElement.removeChild(editableElement); overlays[pageNum] = overlays[pageNum] ? overlays[pageNum].filter(x => x !== editableElement) : []; pushAction({ type: 'delete', page: pageNum, el: editableElement }); showStatus('Element erased'); } e.stopPropagation(); } } // Helper function to check if point is over drawing canvas function isPointOverDrawingCanvas(e, canvas) { const rect = canvas.getBoundingClientRect(); return e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom; } /* --------------------------- click-out dismiss toolbar --------------------------- */ document.addEventListener('click', (e) => { if (!e.target.closest('.overlay') && !e.target.closest('.page-thumb') && !e.target.closest('.tool-btn') && !e.target.closest('#textToolbar')) { selectOverlay(null); } }); /* --------------------------- Initialize --------------------------- */ clearRenderedPDF(); uploadContainer.style.display = 'flex'; pdfContent.style.display = 'none'; });
zaheerpashamd

Share
Published by
zaheerpashamd

Recent Posts

Protect PDF

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

2 months ago

PNG to PDF Converter

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

2 months ago

PDF to PNG Converter

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

2 months ago

Advanced Image Resize Tool

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

3 months ago

Geometry Calculator

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

3 months ago

Algebra Calculator

Algebra Calculator Algebra Calculator Solve equations, simplify expressions, and perform algebraic operations Enter Expression or…

3 months ago