fix: Image-Load robust + Schutz gegen Speichern ohne Bild
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
Diagnose: bei alten Berichten ohne composite_path wurde das bgImage beim Öffnen nicht geladen (fabric.Image.fromURL mit crossOrigin bei Blob-URL hing sich auf). Danach hat jeder Save das Composite ohne Bild gespeichert → PDF-Seite leer. Fix 1 — renderImage: - Native Image() statt fabric.Image.fromURL - Kein crossOrigin bei Blob-URLs (überflüssig, verursachte Hänger) - Timeout 10s mit console.warn - onerror mit Debug-Info (mime, bufSize) - applyTool() nach Einfügen damit Tool-Lock greift Fix 2 — savePageAnnotations: - Wenn KEIN Bild im Canvas ist (weder bgImage=true noch type=image), wird der Save ABGEBROCHEN statt ein leeres Composite zu speichern. - Dadurch kann ein alter Bericht nicht versehentlich mit einem leeren Composite überschrieben werden wenn der Bild-Load hakt. - Toast-Warnung für den User, console.warn für Debug. Fix 3 — finalize-Handler: - Native confirm() vor dem Finalisieren - Bessere Fehler-Diagnose: lese Response als text(), parse JSON, logge bei JSON-Parse-Fehler den Raw-Body - toast() statt alert() für Fehler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> [deploy]
This commit is contained in:
parent
01fbd72310
commit
9c7ef73061
1 changed files with 80 additions and 38 deletions
118
js/editor.js
118
js/editor.js
|
|
@ -270,35 +270,52 @@
|
||||||
const blob = new Blob([arrayBuffer], { type: mime });
|
const blob = new Blob([arrayBuffer], { type: mime });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Native Image-Load + fabric.Image(element) — kein CORS auf Blob-URLs,
|
||||||
|
// bessere Fehler-Diagnose, Timeout-Fallback
|
||||||
return new Promise((res) => {
|
return new Promise((res) => {
|
||||||
fabric.Image.fromURL(url, (fabricImg) => {
|
const htmlImg = new Image();
|
||||||
// Wenn schon ein Bild-Objekt im Canvas ist (nach re-render),
|
const tid = setTimeout(() => {
|
||||||
// altes entfernen bevor neues rein kommt
|
console.warn('[renderImage] Timeout beim Bild-Laden nach 10s — URL:', url);
|
||||||
const existing = fabricCanvas.getObjects().find(o => o.bgImage === true);
|
|
||||||
if (existing) fabricCanvas.remove(existing);
|
|
||||||
|
|
||||||
// Markiere dieses Objekt als "Hintergrundbild" (bleibt aber editierbar)
|
|
||||||
fabricImg.bgImage = true;
|
|
||||||
|
|
||||||
// Auf Canvas-Größe skalieren (contain)
|
|
||||||
const imgRatio = Math.min(canvasW / fabricImg.width, canvasH / fabricImg.height);
|
|
||||||
fabricImg.scale(imgRatio);
|
|
||||||
fabricImg.set({
|
|
||||||
left: (canvasW - fabricImg.width * imgRatio) / 2,
|
|
||||||
top: (canvasH - fabricImg.height * imgRatio) / 2,
|
|
||||||
angle: currentPageRotation,
|
|
||||||
selectable: true,
|
|
||||||
hasControls: true,
|
|
||||||
hasBorders: true,
|
|
||||||
lockRotation: false,
|
|
||||||
});
|
|
||||||
// Ziehbares Hintergrundbild ganz nach hinten
|
|
||||||
fabricCanvas.add(fabricImg);
|
|
||||||
fabricImg.sendToBack();
|
|
||||||
fabricCanvas.requestRenderAll();
|
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
res();
|
res();
|
||||||
}, { crossOrigin: 'anonymous' });
|
}, 10000);
|
||||||
|
|
||||||
|
htmlImg.onload = () => {
|
||||||
|
clearTimeout(tid);
|
||||||
|
try {
|
||||||
|
const fabricImg = new fabric.Image(htmlImg);
|
||||||
|
const existing = fabricCanvas.getObjects().find(o => o.bgImage === true);
|
||||||
|
if (existing) fabricCanvas.remove(existing);
|
||||||
|
|
||||||
|
fabricImg.bgImage = true;
|
||||||
|
const imgRatio = Math.min(canvasW / fabricImg.width, canvasH / fabricImg.height);
|
||||||
|
fabricImg.scale(imgRatio);
|
||||||
|
fabricImg.set({
|
||||||
|
left: (canvasW - fabricImg.width * imgRatio) / 2,
|
||||||
|
top: (canvasH - fabricImg.height * imgRatio) / 2,
|
||||||
|
angle: currentPageRotation,
|
||||||
|
selectable: true,
|
||||||
|
hasControls: true,
|
||||||
|
hasBorders: true,
|
||||||
|
lockRotation: false,
|
||||||
|
});
|
||||||
|
fabricCanvas.add(fabricImg);
|
||||||
|
fabricImg.sendToBack();
|
||||||
|
fabricCanvas.requestRenderAll();
|
||||||
|
if (typeof applyTool === 'function') applyTool();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[renderImage] Fabric-Wrap fehlgeschlagen:', e);
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
res();
|
||||||
|
};
|
||||||
|
htmlImg.onerror = (err) => {
|
||||||
|
clearTimeout(tid);
|
||||||
|
console.error('[renderImage] htmlImg onerror:', err, 'mime:', mime, 'bufSize:', arrayBuffer.byteLength);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
res();
|
||||||
|
};
|
||||||
|
htmlImg.src = url;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -713,15 +730,25 @@
|
||||||
/* ---------- Speichern ---------- */
|
/* ---------- Speichern ---------- */
|
||||||
async function savePageAnnotations(showMessage = true) {
|
async function savePageAnnotations(showMessage = true) {
|
||||||
if (!currentPageId) return;
|
if (!currentPageId) return;
|
||||||
|
|
||||||
|
// Sicherheit: wenn kein bgImage im Canvas ist, NICHT speichern — sonst
|
||||||
|
// überschreiben wir einen funktionierenden Bericht mit einem leeren
|
||||||
|
// Composite
|
||||||
|
const hasBg = fabricCanvas.getObjects().some(o => o.bgImage === true || o.type === 'image');
|
||||||
|
if (!hasBg) {
|
||||||
|
console.warn('[savePageAnnotations] Kein Bild im Canvas — skip, um Datenverlust zu vermeiden');
|
||||||
|
if (showMessage) toast('Speichern übersprungen (kein Bild geladen)', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('token', cfg.token);
|
fd.append('token', cfg.token);
|
||||||
fd.append('pageid', currentPageId);
|
fd.append('pageid', currentPageId);
|
||||||
// Fabric-JSON MIT Hintergrundbild (bgImage-Objekte werden serialisiert)
|
|
||||||
fd.append('fabric_json', JSON.stringify(fabricCanvas.toJSON(['bgImage'])));
|
fd.append('fabric_json', JSON.stringify(fabricCanvas.toJSON(['bgImage'])));
|
||||||
fd.append('note', document.getElementById('page-note').value || '');
|
fd.append('note', document.getElementById('page-note').value || '');
|
||||||
fd.append('rotation', currentPageRotation);
|
fd.append('rotation', currentPageRotation);
|
||||||
|
|
||||||
// Phase 6: Composite-PNG der gesamten Seite generieren und mitschicken
|
// Phase 6: Composite-PNG
|
||||||
try {
|
try {
|
||||||
// Selektion aufheben damit keine Controls gerendert werden
|
// Selektion aufheben damit keine Controls gerendert werden
|
||||||
const active = fabricCanvas.getActiveObject();
|
const active = fabricCanvas.getActiveObject();
|
||||||
|
|
@ -821,17 +848,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('btn-finalize').addEventListener('click', async () => {
|
document.getElementById('btn-finalize').addEventListener('click', async () => {
|
||||||
|
if (!confirm('Bericht jetzt finalisieren und PDF erzeugen?')) return;
|
||||||
|
toast('Speichere aktuelle Seite…');
|
||||||
await savePageAnnotations(false);
|
await savePageAnnotations(false);
|
||||||
const fd = new FormData();
|
toast('PDF wird erzeugt…');
|
||||||
fd.append('token', cfg.token);
|
try {
|
||||||
fd.append('berichtid', cfg.berichtid);
|
const fd = new FormData();
|
||||||
const r = await fetch(cfg.urls.generate_pdf, { method: 'POST', body: fd });
|
fd.append('token', cfg.token);
|
||||||
const data = await r.json();
|
fd.append('berichtid', cfg.berichtid);
|
||||||
if (data.success) {
|
const r = await fetch(cfg.urls.generate_pdf, { method: 'POST', body: fd });
|
||||||
toast('PDF erstellt: ' + data.filename);
|
const txt = await r.text();
|
||||||
setTimeout(() => location.reload(), 1500);
|
let data = {};
|
||||||
} else {
|
try { data = JSON.parse(txt); } catch (e) {
|
||||||
alert('Fehler: ' + (data.error || 'unbekannt'));
|
console.error('generate_pdf lieferte kein JSON:', txt);
|
||||||
|
toast('Server-Fehler (kein JSON)', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.success) {
|
||||||
|
toast('✓ PDF erstellt: ' + data.filename);
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
} else {
|
||||||
|
console.error('generate_pdf failed:', data);
|
||||||
|
toast('Fehler: ' + (data.error || 'unbekannt'), 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('finalize exception:', e);
|
||||||
|
toast('Netzwerk-Fehler: ' + e.message, 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue