feat: aggiunto editor immagini basilare.
This commit is contained in:
1 parent
56dcca57aa
commit
e94ef9a38c
2 files changed
+700
-31
No files matched your search
@@ -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
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user