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();