feat: aggiunto editor immagini basilare.

This commit is contained in:
Gabriele Cimaschi committed 2026-09-10 20:23:55 +02:00
1 parent 56dcca57aa
commit e94ef9a38c
2 files changed
+700 -31

No files matched your search

+3 -3
View File
@@ -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 }
}
];
+697 -28
View File
@@ -231,7 +231,77 @@ const TicketDetailView = {
let previewHtml = '';
if (current.type === 'image') {
previewHtml = `<img id="image-lightbox-img" src="${current.src}" style="max-width: 86vw; max-height: 80vh; border-radius: 6px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); object-fit: contain; transition: opacity 0.15s ease;" />`;
previewHtml = `
<div id="image-viewer-container" style="position: relative; width: 84vw; height: 78vh; background: var(--bg-card); border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--border-subtle);">
<div id="image-toolbar" style="min-height: 48px; background: rgba(15, 23, 42, 0.95); border-bottom: 1px solid rgba(255, 255, 255, 0.15); display: flex; align-items: center; justify-content: space-between; padding: 6px 14px; color: #ffffff; font-size: 0.82rem; user-select: none; gap: 8px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 6px; flex-wrap: wrap;">
<div id="img-edit-tools" style="display: flex; align-items: center; gap: 4px; background: rgba(255,255,255,0.08); padding: 3px 6px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15);">
<button id="img-tool-select" class="btn btn-ghost btn-sm" title="Seleziona, Sposta, Ridimensiona o Elimina elementi" style="color:#ffffff; height:26px; padding:0 6px; font-size:0.78rem; background:transparent; border-radius:4px; border:none; cursor:pointer;">🖐️ Seleziona</button>
<button id="img-tool-draw" class="btn btn-ghost btn-sm active" title="Disegna / Penna libera" style="color:#ffffff; height:26px; padding:0 6px; font-size:0.78rem; background:var(--accent-primary); border-radius:4px; border:none; cursor:pointer;">✏️ Penna</button>
<button id="img-tool-text" class="btn btn-ghost btn-sm" title="Aggiungi Testo" style="color:#ffffff; height:26px; padding:0 6px; font-size:0.78rem; background:transparent; border-radius:4px; border:none; cursor:pointer;">🔤 Testo</button>
<button id="img-tool-highlight" class="btn btn-ghost btn-sm" title="Evidenziatore" style="color:#ffffff; height:26px; padding:0 6px; font-size:0.78rem; background:transparent; border-radius:4px; border:none; cursor:pointer;">🖍️ Evidenzia</button>
<button id="img-tool-shape" class="btn btn-ghost btn-sm" title="Inserisci Forme" style="color:#ffffff; height:26px; padding:0 6px; font-size:0.78rem; background:transparent; border-radius:4px; border:none; cursor:pointer;">📐 Forme</button>
<button id="img-tool-signature" class="btn btn-ghost btn-sm" title="Apponi Timbro / Firma Utente" style="color:#ffffff; height:26px; padding:0 6px; font-size:0.78rem; background:transparent; border-radius:4px; border:none; cursor:pointer;">✍️ Firma</button>
</div>
<input type="color" id="img-color-picker" value="#ef4444" title="Colore primario" style="width:24px; height:24px; border:none; border-radius:50%; cursor:pointer; background:none;" />
<div id="img-shape-controls" style="display: none; align-items: center; gap: 4px; padding-left: 4px; border-left: 1px solid rgba(255,255,255,0.2);">
<select id="img-shape-type" style="height: 24px; background: #0f172a; color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; font-size: 0.74rem; padding: 0 4px; cursor: pointer;">
<option value="rectangle">🟦 Rettangolo</option>
<option value="circle">⚪ Cerchio</option>
<option value="line">📏 Linea</option>
<option value="arrow">🏹 Freccia</option>
</select>
<label style="font-size:0.7rem; color:#cbd5e1; display:flex; align-items:center; gap:2px;">
Riemp. <input type="color" id="img-shape-fill-color" value="#3b82f6" style="width:20px; height:20px; border:none; border-radius:50%; cursor:pointer; background:none;" />
</label>
<label style="font-size:0.7rem; color:#cbd5e1; display:flex; align-items:center; gap:2px;">
Bordo <input type="color" id="img-shape-border-color" value="#1e3a8a" style="width:20px; height:20px; border:none; border-radius:50%; cursor:pointer; background:none;" />
</label>
<label style="font-size:0.7rem; color:#cbd5e1; display:flex; align-items:center; gap:2px;">
Spess. <input type="number" id="img-shape-border-width" min="0" max="30" value="3" style="width:36px; height:22px; background:#0f172a; color:#fff; border:1px solid rgba(255,255,255,0.3); border-radius:4px; font-size:0.74rem; text-align:center;" />
</label>
</div>
<!-- Quick Image Corrections Bar -->
<div id="img-adjustments-bar" style="display: flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.08); padding: 2px 6px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15);">
<label style="font-size:0.7rem; color:#cbd5e1; display:flex; align-items:center; gap:3px;" title="Regola Luminosità">
☀️ Lum
<input type="range" id="img-adj-brightness" min="20" max="200" value="100" style="width:55px; cursor:pointer;" />
</label>
<label style="font-size:0.7rem; color:#cbd5e1; display:flex; align-items:center; gap:3px;" title="Regola Contrasto">
🌗 Con
<input type="range" id="img-adj-contrast" min="20" max="200" value="100" style="width:55px; cursor:pointer;" />
</label>
<label style="font-size:0.7rem; color:#cbd5e1; display:flex; align-items:center; gap:3px;" title="Regola Saturazione">
🎨 Sat
<input type="range" id="img-adj-saturate" min="0" max="250" value="100" style="width:55px; cursor:pointer;" />
</label>
<button id="img-btn-rotate" class="btn btn-ghost btn-sm" title="Ruota immagine 90° in senso orario" style="color:#ffffff; height:24px; padding:0 5px; font-size:0.72rem; background:transparent; border:none; cursor:pointer;">🔄 90°</button>
<button id="img-btn-grayscale" class="btn btn-ghost btn-sm" title="Filtro Bianco e Nero / Grayscale" style="color:#ffffff; height:24px; padding:0 5px; font-size:0.72rem; background:transparent; border:none; cursor:pointer;">🔳 B/N</button>
<button id="img-btn-reset-adj" class="btn btn-ghost btn-sm" title="Ripristina filtri immagine originali" style="color:#cbd5e1; height:24px; padding:0 5px; font-size:0.72rem; background:transparent; border:none; cursor:pointer;">⚡ Reset</button>
</div>
<button id="img-tool-clear" class="btn btn-ghost btn-sm" title="Cancella annotazioni inserite sull'immagine" style="color:#f87171; height:26px; padding:0 6px; font-size:0.76rem; background:transparent; border-radius:4px; border:none; cursor:pointer;">🗑️ Pulisci</button>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<button id="img-save-edited" class="btn btn-primary btn-sm" style="height:28px; padding:0 12px; font-size:0.78rem; font-weight:600; display:inline-flex; align-items:center; gap:4px; background:var(--accent-primary); color:#ffffff; border:none; border-radius:6px; cursor:pointer;" title="Scarica l'immagine modificata (Ctrl+Enter)">
💾 Scarica Immagine Modificata
</button>
</div>
</div>
<!-- Canvas Container -->
<div id="image-canvas-wrapper" style="flex: 1; overflow: auto; display: flex; align-items: center; justify-content: center; padding: 16px; background: #1e293b; position: relative;">
<div id="image-canvas-container" style="position: relative; box-shadow: 0 10px 40px rgba(0,0,0,0.6); display: inline-block; border-radius: 4px; overflow: hidden;">
<canvas id="image-base-canvas" style="display: block; border-radius: 4px;"></canvas>
<canvas id="image-draw-canvas" style="position: absolute; top: 0; left: 0; border-radius: 4px; cursor: crosshair; z-index: 10; touch-action: none;"></canvas>
</div>
</div>
</div>
`;
} else if (current.type === 'pdf') {
previewHtml = `
<div id="pdf-viewer-container" style="position: relative; width: 84vw; height: 78vh; background: var(--bg-card); border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--border-subtle);">
@@ -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();