feat: Seitenrotation (Hoch/Quer) per Toolbar-Buttons
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
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) <noreply@anthropic.com> [deploy]
This commit is contained in:
parent
9edbe95282
commit
40fb738ccf
3 changed files with 68 additions and 28 deletions
|
|
@ -7,7 +7,7 @@ global $db;
|
||||||
$pageid = (int) ($_GET['pageid'] ?? 0);
|
$pageid = (int) ($_GET['pageid'] ?? 0);
|
||||||
if (!$pageid) bericht_ajax_fail('pageid fehlt');
|
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());
|
if (!$res) bericht_ajax_fail($db->lasterror());
|
||||||
$row = $db->fetch_object($res);
|
$row = $db->fetch_object($res);
|
||||||
if (!$row) bericht_ajax_fail('Page nicht gefunden', 404);
|
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(
|
bericht_ajax_ok(array(
|
||||||
'fabric_json' => $row->fabric_json,
|
'fabric_json' => $row->fabric_json,
|
||||||
'note' => $row->note,
|
'note' => $row->note,
|
||||||
|
'rotation' => (int) $row->rotation,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ if (!$pageid) bericht_ajax_fail('pageid fehlt');
|
||||||
|
|
||||||
$fabric = (string) ($_POST['fabric_json'] ?? '');
|
$fabric = (string) ($_POST['fabric_json'] ?? '');
|
||||||
$note = (string) ($_POST['note'] ?? '');
|
$note = (string) ($_POST['note'] ?? '');
|
||||||
|
$rotation = (int) ($_POST['rotation'] ?? 0);
|
||||||
|
$rotation = (($rotation % 360) + 360) % 360;
|
||||||
|
|
||||||
// Page laden
|
// Page laden
|
||||||
$res = $db->query("SELECT rowid FROM ".$db->prefix()."bericht_page WHERE rowid = ".((int) $pageid));
|
$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 "
|
$sql = "UPDATE ".$db->prefix()."bericht_page SET "
|
||||||
."fabric_json = ".($fabric !== '' ? "'".$db->escape($fabric)."'" : "NULL").","
|
."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);
|
." WHERE rowid = ".((int) $pageid);
|
||||||
if (!$db->query($sql)) bericht_ajax_fail('DB-Fehler: '.$db->lasterror());
|
if (!$db->query($sql)) bericht_ajax_fail('DB-Fehler: '.$db->lasterror());
|
||||||
|
|
||||||
|
|
|
||||||
82
js/editor.js
82
js/editor.js
|
|
@ -33,6 +33,9 @@
|
||||||
|
|
||||||
let currentPageId = null;
|
let currentPageId = null;
|
||||||
let currentPageEl = null;
|
let currentPageEl = null;
|
||||||
|
let currentPageRotation = 0; // 0 / 90 / 180 / 270
|
||||||
|
let currentPageBuffer = null; // ArrayBuffer der aktuellen Quelle
|
||||||
|
let currentPageMime = '';
|
||||||
let fabricCanvas = null;
|
let fabricCanvas = null;
|
||||||
const pdfCanvas = document.getElementById('pdf-canvas');
|
const pdfCanvas = document.getElementById('pdf-canvas');
|
||||||
let currentTool = 'select';
|
let currentTool = 'select';
|
||||||
|
|
@ -75,19 +78,27 @@
|
||||||
const ct = resp.headers.get('Content-Type') || '';
|
const ct = resp.headers.get('Content-Type') || '';
|
||||||
const buf = await resp.arrayBuffer();
|
const buf = await resp.arrayBuffer();
|
||||||
|
|
||||||
|
currentPageBuffer = buf;
|
||||||
|
currentPageMime = ct;
|
||||||
|
currentPageRotation = 0; // wird gleich aus loadPageMeta überschrieben falls gespeichert
|
||||||
|
|
||||||
fabricCanvas.clear();
|
fabricCanvas.clear();
|
||||||
document.getElementById('page-note').value = '';
|
document.getElementById('page-note').value = '';
|
||||||
|
|
||||||
if (ct.includes('pdf')) {
|
await loadPageMeta(); // setzt currentPageRotation falls vorhanden
|
||||||
await renderPdf(buf);
|
await rerenderCurrent(); // rendert mit Rotation
|
||||||
} else if (ct.includes('image')) {
|
|
||||||
await renderImage(buf, ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
* Rendert die aktuelle Seite (image oder pdf) aus dem Buffer mit currentPageRotation.
|
||||||
// liefert die Annotationen einmal beim Seitenwechsel mit.)
|
*/
|
||||||
await loadPageMeta();
|
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) {
|
async function renderPdf(arrayBuffer) {
|
||||||
if (!window.pdfjsLib) { console.error('PDF.js nicht geladen'); return; }
|
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 pageNum = 1;
|
||||||
const page = await pdfDoc.getPage(pageNum);
|
const page = await pdfDoc.getPage(pageNum);
|
||||||
// Ziel-Breite ermitteln und Skalierung berechnen, damit die Seite in den
|
// Bei Rotation müssen wir die orientierte Breite messen, um auf den
|
||||||
// Container passt (statt feste scale=1.5)
|
// Container zu passen
|
||||||
const target = getTargetCanvasWidth();
|
const target = getTargetCanvasWidth();
|
||||||
const baseViewport = page.getViewport({ scale: 1 });
|
const baseViewport = page.getViewport({ scale: 1, rotation: currentPageRotation });
|
||||||
const scale = target / baseViewport.width;
|
const scale = target / baseViewport.width;
|
||||||
const viewport = page.getViewport({ scale: scale });
|
const viewport = page.getViewport({ scale: scale, rotation: currentPageRotation });
|
||||||
pdfCanvas.width = viewport.width;
|
pdfCanvas.width = viewport.width;
|
||||||
pdfCanvas.height = viewport.height;
|
pdfCanvas.height = viewport.height;
|
||||||
const ctx = pdfCanvas.getContext('2d');
|
const ctx = pdfCanvas.getContext('2d');
|
||||||
|
|
@ -127,15 +139,25 @@
|
||||||
await new Promise((res) => {
|
await new Promise((res) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
// Immer auf Container-Breite skalieren — auch hochskalieren wenn
|
|
||||||
// die Quelle kleiner ist (sonst entsteht rechts leerer Platz).
|
|
||||||
const target = getTargetCanvasWidth();
|
const target = getTargetCanvasWidth();
|
||||||
const ratio = target / img.width;
|
// Bei 90/270° werden Breite und Höhe getauscht
|
||||||
pdfCanvas.width = Math.round(img.width * ratio);
|
const rotated = (currentPageRotation === 90 || currentPageRotation === 270);
|
||||||
pdfCanvas.height = Math.round(img.height * ratio);
|
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');
|
const ctx = pdfCanvas.getContext('2d');
|
||||||
ctx.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
|
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);
|
URL.revokeObjectURL(url);
|
||||||
res();
|
res();
|
||||||
};
|
};
|
||||||
|
|
@ -167,16 +189,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPageMeta() {
|
async function loadPageMeta() {
|
||||||
// Annotationen + Notiz für aktuelle Seite holen
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch(cfg.urls.save_annotations.replace('save_annotations', 'page_meta') + '?pageid=' + currentPageId);
|
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;
|
if (!r.ok) return;
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (data.fabric_json) {
|
if (data.fabric_json) {
|
||||||
fabricCanvas.loadFromJSON(data.fabric_json, () => fabricCanvas.renderAll());
|
fabricCanvas.loadFromJSON(data.fabric_json, () => fabricCanvas.renderAll());
|
||||||
}
|
}
|
||||||
if (data.note) document.getElementById('page-note').value = data.note;
|
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 */ }
|
} catch (e) { /* ok */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,8 +237,20 @@
|
||||||
fabricCanvas.discardActiveObject();
|
fabricCanvas.discardActiveObject();
|
||||||
fabricCanvas.requestRenderAll();
|
fabricCanvas.requestRenderAll();
|
||||||
});
|
});
|
||||||
document.getElementById('btn-rotate-left').addEventListener('click', () => rotateCurrent(-90));
|
// Seitenrotation: rotate-left/right rotieren die SEITE (nicht das Fabric-Objekt)
|
||||||
document.getElementById('btn-rotate-right').addEventListener('click', () => rotateCurrent(90));
|
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() {
|
function applyTool() {
|
||||||
|
|
@ -295,6 +330,7 @@
|
||||||
fd.append('pageid', currentPageId);
|
fd.append('pageid', currentPageId);
|
||||||
fd.append('fabric_json', JSON.stringify(fabricCanvas.toJSON()));
|
fd.append('fabric_json', JSON.stringify(fabricCanvas.toJSON()));
|
||||||
fd.append('note', document.getElementById('page-note').value || '');
|
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 r = await fetch(cfg.urls.save_annotations, { method: 'POST', body: fd });
|
||||||
const data = await r.json().catch(() => ({}));
|
const data = await r.json().catch(() => ({}));
|
||||||
if (showMessage && data.success) toast('Seite gespeichert');
|
if (showMessage && data.success) toast('Seite gespeichert');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue