From 40fb738ccf6961667f2922c5c7c2e8922b6b70fc Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Wed, 8 Apr 2026 16:11:41 +0200 Subject: [PATCH] feat: Seitenrotation (Hoch/Quer) per Toolbar-Buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die ⟲/⟳-Buttons rotieren jetzt die SEITE statt Fabric-Objekte: - Image: ctx.rotate beim drawImage, Buffer-Größe getauscht - PDF: pdfjsLib viewport mit rotation-Param - Rotation in llx_bericht_page.rotation persistiert - Beim Seitenwechsel wird die gespeicherte Rotation aus page_meta geladen - Annotationen werden bei Rotation gelöscht (rotieren VOR dem Annotieren) Co-Authored-By: Claude Opus 4.6 (1M context) [deploy] --- ajax/page_meta.php | 3 +- ajax/save_annotations.php | 9 +++-- js/editor.js | 84 ++++++++++++++++++++++++++++----------- 3 files changed, 68 insertions(+), 28 deletions(-) diff --git a/ajax/page_meta.php b/ajax/page_meta.php index e8be0fd..17d0614 100644 --- a/ajax/page_meta.php +++ b/ajax/page_meta.php @@ -7,7 +7,7 @@ global $db; $pageid = (int) ($_GET['pageid'] ?? 0); if (!$pageid) bericht_ajax_fail('pageid fehlt'); -$res = $db->query("SELECT fabric_json, note FROM ".$db->prefix()."bericht_page WHERE rowid = ".((int) $pageid)); +$res = $db->query("SELECT fabric_json, note, rotation FROM ".$db->prefix()."bericht_page WHERE rowid = ".((int) $pageid)); if (!$res) bericht_ajax_fail($db->lasterror()); $row = $db->fetch_object($res); if (!$row) bericht_ajax_fail('Page nicht gefunden', 404); @@ -15,4 +15,5 @@ if (!$row) bericht_ajax_fail('Page nicht gefunden', 404); bericht_ajax_ok(array( 'fabric_json' => $row->fabric_json, 'note' => $row->note, + 'rotation' => (int) $row->rotation, )); diff --git a/ajax/save_annotations.php b/ajax/save_annotations.php index 25d93a1..8fa4653 100644 --- a/ajax/save_annotations.php +++ b/ajax/save_annotations.php @@ -10,8 +10,10 @@ if (!$user->hasRight('bericht', 'write')) bericht_ajax_fail('Permission denied', $pageid = (int) ($_POST['pageid'] ?? 0); if (!$pageid) bericht_ajax_fail('pageid fehlt'); -$fabric = (string) ($_POST['fabric_json'] ?? ''); -$note = (string) ($_POST['note'] ?? ''); +$fabric = (string) ($_POST['fabric_json'] ?? ''); +$note = (string) ($_POST['note'] ?? ''); +$rotation = (int) ($_POST['rotation'] ?? 0); +$rotation = (($rotation % 360) + 360) % 360; // Page laden $res = $db->query("SELECT rowid FROM ".$db->prefix()."bericht_page WHERE rowid = ".((int) $pageid)); @@ -19,7 +21,8 @@ if (!$res || !$db->fetch_object($res)) bericht_ajax_fail('Page nicht gefunden', $sql = "UPDATE ".$db->prefix()."bericht_page SET " ."fabric_json = ".($fabric !== '' ? "'".$db->escape($fabric)."'" : "NULL")."," - ."note = ".($note !== '' ? "'".$db->escape($note)."'" : "NULL") + ."note = ".($note !== '' ? "'".$db->escape($note)."'" : "NULL")."," + ."rotation = ".((int) $rotation) ." WHERE rowid = ".((int) $pageid); if (!$db->query($sql)) bericht_ajax_fail('DB-Fehler: '.$db->lasterror()); diff --git a/js/editor.js b/js/editor.js index 892d884..8833f8f 100644 --- a/js/editor.js +++ b/js/editor.js @@ -33,6 +33,9 @@ let currentPageId = null; let currentPageEl = null; + let currentPageRotation = 0; // 0 / 90 / 180 / 270 + let currentPageBuffer = null; // ArrayBuffer der aktuellen Quelle + let currentPageMime = ''; let fabricCanvas = null; const pdfCanvas = document.getElementById('pdf-canvas'); let currentTool = 'select'; @@ -75,19 +78,27 @@ const ct = resp.headers.get('Content-Type') || ''; const buf = await resp.arrayBuffer(); + currentPageBuffer = buf; + currentPageMime = ct; + currentPageRotation = 0; // wird gleich aus loadPageMeta überschrieben falls gespeichert + fabricCanvas.clear(); document.getElementById('page-note').value = ''; - if (ct.includes('pdf')) { - await renderPdf(buf); - } else if (ct.includes('image')) { - await renderImage(buf, ct); - } + await loadPageMeta(); // setzt currentPageRotation falls vorhanden + await rerenderCurrent(); // rendert mit Rotation + } - // Vorhandene Annotationen laden — kommt über das Thumb-Dataset oder einen Refetch - // (Für Einfachheit: hier ein extra Ajax wäre sauberer; wir nehmen an, der Server - // liefert die Annotationen einmal beim Seitenwechsel mit.) - await loadPageMeta(); + /** + * Rendert die aktuelle Seite (image oder pdf) aus dem Buffer mit currentPageRotation. + */ + async function rerenderCurrent() { + if (!currentPageBuffer) return; + if (currentPageMime.includes('pdf')) { + await renderPdf(currentPageBuffer); + } else if (currentPageMime.includes('image')) { + await renderImage(currentPageBuffer, currentPageMime); + } } /** @@ -105,15 +116,16 @@ async function renderPdf(arrayBuffer) { if (!window.pdfjsLib) { console.error('PDF.js nicht geladen'); return; } - const pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; + // Buffer kopieren — pdf.js konsumiert den ArrayBuffer beim ersten Aufruf + const pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer.slice(0) }).promise; const pageNum = 1; const page = await pdfDoc.getPage(pageNum); - // Ziel-Breite ermitteln und Skalierung berechnen, damit die Seite in den - // Container passt (statt feste scale=1.5) + // Bei Rotation müssen wir die orientierte Breite messen, um auf den + // Container zu passen const target = getTargetCanvasWidth(); - const baseViewport = page.getViewport({ scale: 1 }); + const baseViewport = page.getViewport({ scale: 1, rotation: currentPageRotation }); const scale = target / baseViewport.width; - const viewport = page.getViewport({ scale: scale }); + const viewport = page.getViewport({ scale: scale, rotation: currentPageRotation }); pdfCanvas.width = viewport.width; pdfCanvas.height = viewport.height; const ctx = pdfCanvas.getContext('2d'); @@ -127,15 +139,25 @@ await new Promise((res) => { const img = new Image(); img.onload = () => { - // Immer auf Container-Breite skalieren — auch hochskalieren wenn - // die Quelle kleiner ist (sonst entsteht rechts leerer Platz). const target = getTargetCanvasWidth(); - const ratio = target / img.width; - pdfCanvas.width = Math.round(img.width * ratio); - pdfCanvas.height = Math.round(img.height * ratio); + // Bei 90/270° werden Breite und Höhe getauscht + const rotated = (currentPageRotation === 90 || currentPageRotation === 270); + const srcW = rotated ? img.height : img.width; + const srcH = rotated ? img.width : img.height; + const ratio = target / srcW; + pdfCanvas.width = Math.round(srcW * ratio); + pdfCanvas.height = Math.round(srcH * ratio); + const ctx = pdfCanvas.getContext('2d'); ctx.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height); - ctx.drawImage(img, 0, 0, pdfCanvas.width, pdfCanvas.height); + ctx.save(); + ctx.translate(pdfCanvas.width / 2, pdfCanvas.height / 2); + ctx.rotate(currentPageRotation * Math.PI / 180); + const dw = img.width * ratio; + const dh = img.height * ratio; + ctx.drawImage(img, -dw / 2, -dh / 2, dw, dh); + ctx.restore(); + URL.revokeObjectURL(url); res(); }; @@ -167,16 +189,17 @@ } async function loadPageMeta() { - // Annotationen + Notiz für aktuelle Seite holen try { const r = await fetch(cfg.urls.save_annotations.replace('save_annotations', 'page_meta') + '?pageid=' + currentPageId); - // Falls page_meta.php nicht existiert: still bleiben if (!r.ok) return; const data = await r.json(); if (data.fabric_json) { fabricCanvas.loadFromJSON(data.fabric_json, () => fabricCanvas.renderAll()); } if (data.note) document.getElementById('page-note').value = data.note; + if (typeof data.rotation !== 'undefined' && data.rotation !== null) { + currentPageRotation = parseInt(data.rotation, 10) || 0; + } } catch (e) { /* ok */ } } @@ -214,8 +237,20 @@ fabricCanvas.discardActiveObject(); fabricCanvas.requestRenderAll(); }); - document.getElementById('btn-rotate-left').addEventListener('click', () => rotateCurrent(-90)); - document.getElementById('btn-rotate-right').addEventListener('click', () => rotateCurrent(90)); + // Seitenrotation: rotate-left/right rotieren die SEITE (nicht das Fabric-Objekt) + document.getElementById('btn-rotate-left').addEventListener('click', () => rotatePage(-90)); + document.getElementById('btn-rotate-right').addEventListener('click', () => rotatePage(90)); + } + + async function rotatePage(deg) { + currentPageRotation = ((currentPageRotation + deg) % 360 + 360) % 360; + // Annotationen für die alte Orientierung sind im JSON noch da — wir werfen + // sie vor dem Re-Render weg, damit sie nicht falsch positioniert sind. + // (Bewusste Designentscheidung: rotieren vor dem Annotieren.) + fabricCanvas.clear(); + await rerenderCurrent(); + // Sofort speichern, damit Rotation persistent ist + await savePageAnnotations(false); } function applyTool() { @@ -295,6 +330,7 @@ fd.append('pageid', currentPageId); fd.append('fabric_json', JSON.stringify(fabricCanvas.toJSON())); fd.append('note', document.getElementById('page-note').value || ''); + fd.append('rotation', currentPageRotation); const r = await fetch(cfg.urls.save_annotations, { method: 'POST', body: fd }); const data = await r.json().catch(() => ({})); if (showMessage && data.success) toast('Seite gespeichert');