From e94ef9a38c90363733d1b5f6d4ae407cc6bda7c5 Mon Sep 17 00:00:00 2001 From: Gabriele Cimaschi Date: Thu, 10 Sep 2026 20:23:55 +0200 Subject: [PATCH] feat: aggiunto editor immagini basilare. --- public/js/views/shortcuts.js | 6 +- public/js/views/ticketDetail.js | 725 ++++++++++++++++++++++++++++++-- 2 files changed, 700 insertions(+), 31 deletions(-) diff --git a/public/js/views/shortcuts.js b/public/js/views/shortcuts.js index af89cd9..2459bb4 100644 --- a/public/js/views/shortcuts.js +++ b/public/js/views/shortcuts.js @@ -81,9 +81,9 @@ const ShortcutsView = { }, { id: 'pdf_save_edited', - name: 'Salva Allegato / PDF Modificato', - description: 'Scarica l\'allegato corrente o salva il PDF con le annotazioni apportate', - context: 'Anteprima Allegato / PDF', + name: 'Salva Immagine / Allegato / PDF Modificato', + description: 'Scarica l\'immagine o il PDF con le correzioni e annotazioni apportate', + context: 'Anteprima Allegato / PDF / Immagine', defaultCombo: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true } } ]; diff --git a/public/js/views/ticketDetail.js b/public/js/views/ticketDetail.js index 0deab17..2f2e453 100644 --- a/public/js/views/ticketDetail.js +++ b/public/js/views/ticketDetail.js @@ -231,7 +231,77 @@ const TicketDetailView = { let previewHtml = ''; if (current.type === 'image') { - previewHtml = ``; + previewHtml = ` +
+
+
+
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+
+ `; } else if (current.type === 'pdf') { previewHtml = `
@@ -337,6 +407,8 @@ const TicketDetailView = { if (current.type === 'pdf') { this.initPdfJsViewer(current.src); + } else if (current.type === 'image') { + this.initImageEditor(current.src, current.filename); } overlay.focus(); @@ -390,25 +462,7 @@ const TicketDetailView = { }; // Helper to generate filename with optional timestamp prefix if already downloaded in current session - const getOutputFilename = (originalName) => { - let base = originalName || 'documento.pdf'; - if (!this.downloadedFilenames) this.downloadedFilenames = new Set(); - - if (this.downloadedFilenames.has(base)) { - const now = new Date(); - const yyyy = now.getFullYear(); - const mm = String(now.getMonth() + 1).padStart(2, '0'); - const dd = String(now.getDate()).padStart(2, '0'); - const hh = String(now.getHours()).padStart(2, '0'); - const min = String(now.getMinutes()).padStart(2, '0'); - const ss = String(now.getSeconds()).padStart(2, '0'); - const timestamp = `${yyyy}${mm}${dd}_${hh}${min}${ss}`; - return `${timestamp}_${base}`; - } else { - this.downloadedFilenames.add(base); - return base; - } - }; + const getOutputFilename = (originalName) => this.getOutputFilename(originalName); const savePdfDocument = async () => { if (!window.PDFLib) { @@ -492,21 +546,16 @@ const TicketDetailView = { }; const handleKeydown = (e) => { - // If user is editing text in an input, textarea or contenteditable element, allow standard cursor navigation - const activeTag = document.activeElement ? document.activeElement.tagName.toUpperCase() : ''; - const isInputOrTextarea = activeTag === 'INPUT' || activeTag === 'TEXTAREA' || (document.activeElement && document.activeElement.isContentEditable); - if (isInputOrTextarea) { - return; - } - const currentItem = itemsList[currentIndex]; if (currentItem && App.isShortcutTriggered(e, 'pdf_save_edited')) { e.preventDefault(); e.stopPropagation(); if (currentItem.type === 'pdf') { savePdfDocument(); + } else if (currentItem.type === 'image') { + this.saveImageDocument(currentItem.filename); } else { - // Non-PDF attachment (e.g. image): trigger download/open link + // Non-PDF/image attachment: trigger download/open link const downloadBtn = overlay.querySelector('#lightbox-download-btn'); if (downloadBtn) { downloadBtn.click(); @@ -521,6 +570,13 @@ const TicketDetailView = { return; } + // If user is editing text in an input, textarea or contenteditable element, allow standard cursor navigation for arrow keys etc. + const activeTag = document.activeElement ? document.activeElement.tagName.toUpperCase() : ''; + const isInputOrTextarea = activeTag === 'INPUT' || activeTag === 'TEXTAREA' || (document.activeElement && document.activeElement.isContentEditable); + if (isInputOrTextarea) { + return; + } + if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); @@ -2098,6 +2154,619 @@ const TicketDetailView = { } }, + initImageEditor(url, filename) { + const baseCanvas = document.getElementById('image-base-canvas'); + const drawCanvas = document.getElementById('image-draw-canvas'); + const wrapper = document.getElementById('image-canvas-wrapper'); + if (!baseCanvas || !drawCanvas || !wrapper) return; + + const baseCtx = baseCanvas.getContext('2d'); + const drawCtx = drawCanvas.getContext('2d'); + + const loadedImg = new Image(); + loadedImg.crossOrigin = 'anonymous'; + + let brightness = 100; + let contrast = 100; + let saturate = 100; + let rotation = 0; + let isGrayscale = false; + + let currentTool = 'draw'; + let currentColor = '#ef4444'; + let currentBrushType = 'pencil'; + let currentBrushSize = 3; + let activeSignature = null; + + try { + App.api('/api/config/user-signatures').then(sigs => { + if (sigs && sigs.length > 0) { + activeSignature = sigs.find(s => s.is_default) || sigs[0]; + } + }).catch(() => {}); + } catch (_) {} + + const pageObjects = []; + drawCanvas.pageObjects = pageObjects; + + let selectedObject = null; + let isDraggingObj = false; + let isResizingObj = false; + let resizeHandleIndex = -1; + let dragOffsetX = 0; + let dragOffsetY = 0; + let isDrawingStroke = false; + let currentStrokePoints = []; + + const renderBaseImage = () => { + const w = baseCanvas.width; + const h = baseCanvas.height; + baseCtx.clearRect(0, 0, w, h); + baseCtx.save(); + + let filterStr = `brightness(${brightness}%) contrast(${contrast}%) saturate(${saturate}%)`; + if (isGrayscale) filterStr += ' grayscale(100%)'; + baseCtx.filter = filterStr; + + if (rotation !== 0) { + baseCtx.translate(w / 2, h / 2); + baseCtx.rotate((rotation * Math.PI) / 180); + if (rotation === 90 || rotation === 270) { + baseCtx.translate(-h / 2, -w / 2); + baseCtx.drawImage(loadedImg, 0, 0, h, w); + } else { + baseCtx.translate(-w / 2, -h / 2); + baseCtx.drawImage(loadedImg, 0, 0, w, h); + } + } else { + baseCtx.drawImage(loadedImg, 0, 0, w, h); + } + + baseCtx.restore(); + }; + + const redrawPageObjects = () => { + drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height); + pageObjects.forEach(obj => { + drawCtx.save(); + if (obj.type === 'stroke') { + if (obj.points && obj.points.length > 0) { + drawCtx.beginPath(); + drawCtx.moveTo(obj.points[0].x, obj.points[0].y); + obj.points.forEach(p => drawCtx.lineTo(p.x, p.y)); + drawCtx.strokeStyle = obj.color; + drawCtx.lineWidth = obj.size; + if (obj.brushType === 'highlight') { + drawCtx.globalCompositeOperation = 'multiply'; + drawCtx.globalAlpha = 0.55; + drawCtx.lineCap = 'square'; + } else { + drawCtx.globalCompositeOperation = 'source-over'; + drawCtx.globalAlpha = obj.opacity || 1.0; + drawCtx.lineCap = 'round'; + } + drawCtx.lineJoin = 'round'; + drawCtx.stroke(); + } + } else if (obj.type === 'text') { + drawCtx.font = `bold ${obj.size || 16}px Inter, sans-serif`; + drawCtx.fillStyle = obj.color; + drawCtx.globalAlpha = 1.0; + drawCtx.fillText(obj.text, obj.x, obj.y + (obj.size || 16)); + } else if (obj.type === 'image') { + drawCtx.globalAlpha = 1.0; + if (obj.img) { + drawCtx.drawImage(obj.img, obj.x, obj.y, obj.width, obj.height); + } + } else if (obj.type === 'shape') { + drawCtx.globalAlpha = 1.0; + drawCtx.beginPath(); + if (obj.shapeType === 'circle') { + const rx = obj.width / 2; + const ry = obj.height / 2; + const cx = obj.x + rx; + const cy = obj.y + ry; + drawCtx.ellipse(cx, cy, Math.abs(rx), Math.abs(ry), 0, 0, Math.PI * 2); + if (obj.fillColor && obj.fillColor !== 'transparent') { + drawCtx.fillStyle = obj.fillColor; + drawCtx.fill(); + } + if (obj.borderWidth > 0 && obj.borderColor) { + drawCtx.lineWidth = obj.borderWidth; + drawCtx.strokeStyle = obj.borderColor; + drawCtx.stroke(); + } + } else if (obj.shapeType === 'line' || obj.shapeType === 'arrow') { + const startX = obj.x; + const startY = obj.y; + const endX = obj.x + obj.width; + const endY = obj.y + obj.height; + const bw = obj.borderWidth || 3; + const strokeColor = obj.borderColor || obj.fillColor || '#1e3a8a'; + + if (obj.shapeType === 'arrow') { + const angle = Math.atan2(endY - startY, endX - startX); + const headLength = Math.max(14, bw * 3.5); + const shorten = headLength * 0.6; + const stemEndX = endX - shorten * Math.cos(angle); + const stemEndY = endY - shorten * Math.sin(angle); + + drawCtx.moveTo(startX, startY); + drawCtx.lineTo(stemEndX, stemEndY); + drawCtx.lineWidth = bw; + drawCtx.strokeStyle = strokeColor; + drawCtx.lineCap = 'round'; + drawCtx.stroke(); + + drawCtx.beginPath(); + drawCtx.moveTo(endX, endY); + drawCtx.lineTo( + endX - headLength * Math.cos(angle - Math.PI / 6), + endY - headLength * Math.sin(angle - Math.PI / 6) + ); + drawCtx.lineTo( + endX - headLength * Math.cos(angle + Math.PI / 6), + endY - headLength * Math.sin(angle + Math.PI / 6) + ); + drawCtx.closePath(); + drawCtx.fillStyle = strokeColor; + drawCtx.fill(); + } else { + drawCtx.moveTo(startX, startY); + drawCtx.lineTo(endX, endY); + drawCtx.lineWidth = bw; + drawCtx.strokeStyle = strokeColor; + drawCtx.lineCap = 'round'; + drawCtx.stroke(); + } + } else { + drawCtx.rect(obj.x, obj.y, obj.width, obj.height); + if (obj.fillColor && obj.fillColor !== 'transparent') { + drawCtx.fillStyle = obj.fillColor; + drawCtx.fill(); + } + if (obj.borderWidth > 0 && obj.borderColor) { + drawCtx.lineWidth = obj.borderWidth; + drawCtx.strokeStyle = obj.borderColor; + drawCtx.stroke(); + } + } + } + drawCtx.restore(); + + if (selectedObject === obj) { + drawCtx.save(); + drawCtx.strokeStyle = '#3b82f6'; + drawCtx.lineWidth = 1.5; + + if (obj.type === 'shape' && (obj.shapeType === 'line' || obj.shapeType === 'arrow')) { + drawCtx.setLineDash([3, 3]); + drawCtx.beginPath(); + drawCtx.moveTo(obj.x, obj.y); + drawCtx.lineTo(obj.x + obj.width, obj.y + obj.height); + drawCtx.stroke(); + drawCtx.setLineDash([]); + } else { + drawCtx.setLineDash([4, 4]); + drawCtx.strokeRect(obj.x - 4, obj.y - 4, obj.width + 8, obj.height + 8); + drawCtx.setLineDash([]); + } + + const handles = getObjectHandles(obj); + handles.forEach(h => { + drawCtx.fillStyle = '#ffffff'; + drawCtx.strokeStyle = '#2563eb'; + drawCtx.lineWidth = 1.5; + drawCtx.beginPath(); + drawCtx.arc(h.x, h.y, 5, 0, Math.PI * 2); + drawCtx.fill(); + drawCtx.stroke(); + }); + drawCtx.restore(); + } + }); + }; + + const getObjectHandles = (obj) => { + const x = obj.x; const y = obj.y; const w = obj.width; const h = obj.height; + if (obj.type === 'shape' && (obj.shapeType === 'line' || obj.shapeType === 'arrow')) { + return [ + { x: x, y: y }, + { x: x + w / 2, y: y + h / 2 }, + { x: x + w, y: y + h } + ]; + } + return [ + { x: x, y: y }, { x: x + w / 2, y: y }, { x: x + w, y: y }, + { x: x + w, y: y + h / 2 }, { x: x + w, y: y + h }, + { x: x + w / 2, y: y + h }, { x: x, y: y + h }, { x: x, y: y + h / 2 } + ]; + }; + + const hitTestObject = (x, y) => { + for (let i = pageObjects.length - 1; i >= 0; i--) { + const obj = pageObjects[i]; + if (x >= obj.x - 6 && x <= obj.x + obj.width + 6 && y >= obj.y - 6 && y <= obj.y + obj.height + 6) { + return obj; + } + } + return null; + }; + + const hitTestHandle = (obj, x, y) => { + if (!obj) return -1; + const handles = getObjectHandles(obj); + for (let i = 0; i < handles.length; i++) { + const h = handles[i]; + if (Math.hypot(x - h.x, y - h.y) <= 7) return i; + } + return -1; + }; + + loadedImg.onload = () => { + const containerW = wrapper.clientWidth - 32 || 800; + const containerH = wrapper.clientHeight - 32 || 600; + + let targetW = loadedImg.naturalWidth || loadedImg.width || 800; + let targetH = loadedImg.naturalHeight || loadedImg.height || 600; + + const scale = Math.min(containerW / targetW, containerH / targetH, 1.0); + targetW = Math.max(300, Math.round(targetW * scale)); + targetH = Math.max(200, Math.round(targetH * scale)); + + baseCanvas.width = targetW; + baseCanvas.height = targetH; + drawCanvas.width = targetW; + drawCanvas.height = targetH; + + renderBaseImage(); + redrawPageObjects(); + }; + + loadedImg.src = url; + + // Image Adjustment inputs + const bInput = document.getElementById('img-adj-brightness'); + const cInput = document.getElementById('img-adj-contrast'); + const sInput = document.getElementById('img-adj-saturate'); + const btnRotate = document.getElementById('img-btn-rotate'); + const btnGrayscale = document.getElementById('img-btn-grayscale'); + const btnResetAdj = document.getElementById('img-btn-reset-adj'); + + if (bInput) bInput.oninput = (e) => { brightness = e.target.value; renderBaseImage(); }; + if (cInput) cInput.oninput = (e) => { contrast = e.target.value; renderBaseImage(); }; + if (sInput) sInput.oninput = (e) => { saturate = e.target.value; renderBaseImage(); }; + if (btnRotate) btnRotate.onclick = (e) => { e.stopPropagation(); rotation = (rotation + 90) % 360; renderBaseImage(); }; + if (btnGrayscale) btnGrayscale.onclick = (e) => { + e.stopPropagation(); + isGrayscale = !isGrayscale; + btnGrayscale.style.background = isGrayscale ? 'var(--accent-primary)' : 'transparent'; + renderBaseImage(); + }; + if (btnResetAdj) btnResetAdj.onclick = (e) => { + e.stopPropagation(); + brightness = 100; contrast = 100; saturate = 100; rotation = 0; isGrayscale = false; + if (bInput) bInput.value = 100; + if (cInput) cInput.value = 100; + if (sInput) sInput.value = 100; + if (btnGrayscale) btnGrayscale.style.background = 'transparent'; + renderBaseImage(); + }; + + // Tool switching + const toolSelectBtn = document.getElementById('img-tool-select'); + const toolDrawBtn = document.getElementById('img-tool-draw'); + const toolTextBtn = document.getElementById('img-tool-text'); + const toolHighlightBtn = document.getElementById('img-tool-highlight'); + const toolShapeBtn = document.getElementById('img-tool-shape'); + const toolSigBtn = document.getElementById('img-tool-signature'); + const shapeControls = document.getElementById('img-shape-controls'); + const colorPicker = document.getElementById('img-color-picker'); + const toolClearBtn = document.getElementById('img-tool-clear'); + const saveEditedBtn = document.getElementById('img-save-edited'); + + const setActiveTool = (tool, activeBtn) => { + currentTool = tool; + const allToolBtns = [toolSelectBtn, toolDrawBtn, toolTextBtn, toolHighlightBtn, toolShapeBtn, toolSigBtn]; + allToolBtns.forEach(b => { + if (b) { + b.style.background = 'transparent'; + b.classList.remove('active'); + } + }); + if (activeBtn) { + activeBtn.style.background = 'var(--accent-primary)'; + activeBtn.classList.add('active'); + } + if (shapeControls) { + shapeControls.style.display = tool === 'shape' ? 'flex' : 'none'; + } + drawCanvas.style.pointerEvents = tool === 'select' ? 'none' : 'auto'; + }; + + if (toolSelectBtn) toolSelectBtn.onclick = (e) => { e.stopPropagation(); setActiveTool('select', toolSelectBtn); }; + if (toolDrawBtn) toolDrawBtn.onclick = (e) => { e.stopPropagation(); setActiveTool('draw', toolDrawBtn); }; + if (toolTextBtn) toolTextBtn.onclick = (e) => { e.stopPropagation(); setActiveTool('text', toolTextBtn); }; + if (toolHighlightBtn) toolHighlightBtn.onclick = (e) => { e.stopPropagation(); setActiveTool('highlight', toolHighlightBtn); }; + if (toolShapeBtn) toolShapeBtn.onclick = (e) => { e.stopPropagation(); setActiveTool('shape', toolShapeBtn); }; + if (toolSigBtn) toolSigBtn.onclick = (e) => { e.stopPropagation(); setActiveTool('signature', toolSigBtn); }; + + if (colorPicker) { + colorPicker.onchange = (e) => { + currentColor = e.target.value; + if (selectedObject) { + if (selectedObject.type === 'stroke') selectedObject.color = currentColor; + else if (selectedObject.type === 'text') selectedObject.color = currentColor; + redrawPageObjects(); + } + }; + } + + if (toolClearBtn) { + toolClearBtn.onclick = (e) => { + e.stopPropagation(); + pageObjects.length = 0; + selectedObject = null; + redrawPageObjects(); + Toast.info('Annotazioni immagine pulite'); + }; + } + + if (saveEditedBtn) { + saveEditedBtn.onclick = (e) => { + e.stopPropagation(); + this.saveImageDocument(filename); + }; + } + + // Mouse Events on drawCanvas + drawCanvas.onmousedown = (e) => { + if (e.button !== 0) return; + e.preventDefault(); + e.stopPropagation(); + const rect = drawCanvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + if (selectedObject) { + const hIdx = hitTestHandle(selectedObject, x, y); + if (hIdx !== -1) { + isResizingObj = true; + resizeHandleIndex = hIdx; + return; + } + } + + const hitObj = hitTestObject(x, y); + if (hitObj) { + selectedObject = hitObj; + isDraggingObj = true; + dragOffsetX = x - hitObj.x; + dragOffsetY = y - hitObj.y; + redrawPageObjects(); + return; + } + + if (selectedObject) { + selectedObject = null; + redrawPageObjects(); + } + + if (currentTool === 'select') return; + + if (currentTool === 'signature') { + if (activeSignature) { + const img = new Image(); + img.onload = () => { + const targetWidth = Math.min(220, img.width || 180); + const targetHeight = (img.height / img.width) * targetWidth || 80; + const newObj = { + id: Date.now(), + type: 'image', + img: img, + x: x - targetWidth / 2, + y: y - targetHeight / 2, + width: targetWidth, + height: targetHeight + }; + pageObjects.push(newObj); + selectedObject = newObj; + redrawPageObjects(); + }; + img.src = activeSignature.image_data; + } else { + Toast.warning('Nessuna firma selezionata.'); + } + return; + } + + if (currentTool === 'shape') { + const shapeTypeEl = document.getElementById('img-shape-type'); + const fillColorEl = document.getElementById('img-shape-fill-color'); + const borderColorEl = document.getElementById('img-shape-border-color'); + const borderWidthEl = document.getElementById('img-shape-border-width'); + + const shapeType = shapeTypeEl ? shapeTypeEl.value : 'rectangle'; + const fillColor = fillColorEl ? fillColorEl.value : '#3b82f6'; + const borderColor = borderColorEl ? borderColorEl.value : '#1e3a8a'; + const borderWidth = borderWidthEl ? parseInt(borderWidthEl.value, 10) : 3; + + if (shapeType === 'line' || shapeType === 'arrow') { + const newShapeObj = { + id: Date.now(), + type: 'shape', + shapeType: shapeType, + x: x, y: y, width: 0, height: 0, + fillColor: fillColor, borderColor: borderColor, borderWidth: borderWidth + }; + pageObjects.push(newShapeObj); + selectedObject = newShapeObj; + isResizingObj = true; + resizeHandleIndex = 2; + redrawPageObjects(); + return; + } + + const newShapeObj = { + id: Date.now(), + type: 'shape', + shapeType: shapeType, + startX: x, startY: y, x: x, y: y, width: 0, height: 0, + fillColor: fillColor, borderColor: borderColor, borderWidth: borderWidth + }; + + pageObjects.push(newShapeObj); + selectedObject = newShapeObj; + isResizingObj = true; + resizeHandleIndex = 99; + redrawPageObjects(); + return; + } + + isDrawingStroke = true; + currentStrokePoints = [{ x, y }]; + const opacity = (currentTool === 'highlight' || currentBrushType === 'highlight') ? 0.15 : 1.0; + const size = (currentTool === 'highlight' || currentBrushType === 'highlight') ? (currentBrushSize || 20) : (currentBrushType === 'brush' ? (currentBrushSize || 8) : (currentBrushSize || 3)); + + const newStrokeObj = { + id: Date.now(), + type: 'stroke', + points: currentStrokePoints, + x: x, y: y, width: size, height: size, + color: currentColor, size: size, opacity: opacity, brushType: currentBrushType + }; + pageObjects.push(newStrokeObj); + selectedObject = null; + }; + + drawCanvas.onmousemove = (e) => { + const rect = drawCanvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + if (isResizingObj && selectedObject) { + e.preventDefault(); + const obj = selectedObject; + if (obj.type === 'shape' && resizeHandleIndex === 99) { + const startX = obj.startX !== undefined ? obj.startX : obj.x; + const startY = obj.startY !== undefined ? obj.startY : obj.y; + obj.x = Math.min(startX, x); + obj.y = Math.min(startY, y); + obj.width = Math.abs(x - startX); + obj.height = Math.abs(y - startY); + redrawPageObjects(); + return; + } + + if (obj.type === 'shape' && (obj.shapeType === 'line' || obj.shapeType === 'arrow')) { + if (resizeHandleIndex === 0) { + const endX = obj.x + obj.width; const endY = obj.y + obj.height; + obj.x = x; obj.y = y; obj.width = endX - x; obj.height = endY - y; + } else if (resizeHandleIndex === 2) { + obj.width = x - obj.x; obj.height = y - obj.y; + } + } else if (resizeHandleIndex === 4) { + obj.width = Math.max(10, x - obj.x); + obj.height = Math.max(10, y - obj.y); + } + redrawPageObjects(); + return; + } + + if (isDraggingObj && selectedObject) { + e.preventDefault(); + selectedObject.x = x - dragOffsetX; + selectedObject.y = y - dragOffsetY; + redrawPageObjects(); + return; + } + + if (!isDrawingStroke) return; + e.preventDefault(); + currentStrokePoints.push({ x, y }); + redrawPageObjects(); + }; + + const stopDrawing = () => { + if (isResizingObj && selectedObject && selectedObject.type === 'shape') { + if (resizeHandleIndex === 99) { + if (selectedObject.width < 5 && selectedObject.height < 5) { + const defaultW = 120; const defaultH = 80; + const startX = selectedObject.startX !== undefined ? selectedObject.startX : selectedObject.x; + const startY = selectedObject.startY !== undefined ? selectedObject.startY : selectedObject.y; + selectedObject.x = startX - defaultW / 2; + selectedObject.y = startY - defaultH / 2; + selectedObject.width = defaultW; + selectedObject.height = defaultH; + } + } else if (selectedObject.shapeType === 'line' || selectedObject.shapeType === 'arrow') { + if (Math.hypot(selectedObject.width, selectedObject.height) < 5) { + selectedObject.width = 120; selectedObject.height = 0; + } + } + } + isDrawingStroke = false; + isDraggingObj = false; + isResizingObj = false; + resizeHandleIndex = -1; + redrawPageObjects(); + }; + + drawCanvas.onmouseup = stopDrawing; + drawCanvas.onmouseleave = stopDrawing; + }, + + getOutputFilename(originalName) { + let base = originalName || 'documento.pdf'; + // Clean any existing timestamp prefix to avoid duplication + base = base.replace(/^\d{8}_\d{6}_/, ''); + + const now = new Date(); + const yyyy = now.getFullYear(); + const mm = String(now.getMonth() + 1).padStart(2, '0'); + const dd = String(now.getDate()).padStart(2, '0'); + const hh = String(now.getHours()).padStart(2, '0'); + const min = String(now.getMinutes()).padStart(2, '0'); + const ss = String(now.getSeconds()).padStart(2, '0'); + const timestamp = `${yyyy}${mm}${dd}_${hh}${min}${ss}`; + return `${timestamp}_${base}`; + }, + + saveImageDocument(filename) { + const baseCanvas = document.getElementById('image-base-canvas'); + const drawCanvas = document.getElementById('image-draw-canvas'); + if (!baseCanvas || !drawCanvas) return; + + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = baseCanvas.width; + tempCanvas.height = baseCanvas.height; + const tempCtx = tempCanvas.getContext('2d'); + + tempCtx.drawImage(baseCanvas, 0, 0); + tempCtx.drawImage(drawCanvas, 0, 0); + + let baseName = filename || 'immagine_modificata.png'; + if (!baseName.toLowerCase().match(/\.(png|jpg|jpeg|webp)$/)) { + baseName += '.png'; + } + const outName = this.getOutputFilename(baseName); + + tempCanvas.toBlob((blob) => { + if (!blob) return; + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = outName; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + a.remove(); + URL.revokeObjectURL(url); + }, 200); + Toast.success(`Immagine modificata salvata come ${outName}`); + }, 'image/png'); + }, + showPdfTextModal(onSubmitCallback) { let modal = document.getElementById('pdf-text-modal-overlay'); if (modal) modal.remove();