feat: Phase 1.4 + 1.5 — Multi-Image Grids und Bildgröße komplett
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
DB-Klasse: - BerichtPage::getImages() liest llx_bericht_page_image - BerichtPage::setSlotImage() / clearImages() - BerichtPage::slotCountForLayout() - BerichtPage::slotRects() berechnet Slot-Positionen für 1/2/2v/4/6 - create()/update()/fetchAllForBericht() inkludieren layout/scale/align Endpoints (alle im Bericht-Modul): - ajax/save_page_options.php — speichert layout, image_scale, image_align - ajax/create_grid_page.php — erstellt Multi-Image-Seite mit gewähltem Layout - ajax/set_slot_image.php — setzt einzelnes Bild eines Slots - ajax/page_meta.php liefert layout/scale/align mit - ajax/page_image.php rendert Composite-PNG (GD) für Multi-Image-Seiten UI: - Layout-Dropdown in 3. Toolbar-Zeile (Single/Grid 2/2v/4/6) - Bildgröße-Dropdown (100/70/50/30%) — single-only - Position-Dropdown (Anpassen/Zentriert/Ecken) — single-only - 'Als Grid hinzufügen'-Buttons in der Anhänge-Liste (▭▭ ▯▯ ▦ ▦▦) - Auto-Sync der single-only-Felder beim Layout-Wechsel Rendering: - bericht_render_page_to_pdf() in lib/bericht.lib.php — zentrale Render-Funktion für Single + Grid + PDF-Quelle + image_scale + align - bericht_align_position() für die 6 Align-Modi - generate_pdf + preview_pdf nutzen die gemeinsame Funktion (DRY) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> [deploy]
This commit is contained in:
parent
0fbfb1bf27
commit
06cd70d4a3
12 changed files with 597 additions and 85 deletions
49
ajax/create_grid_page.php
Normal file
49
ajax/create_grid_page.php
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
/* Erstellt eine neue Multi-Image-Seite mit gewähltem Layout
|
||||||
|
* und setzt mehrere Bilder gleich rein.
|
||||||
|
* POST: berichtid, layout, relpaths[] (JSON-Array), token
|
||||||
|
*/
|
||||||
|
require_once __DIR__.'/_inc.php';
|
||||||
|
|
||||||
|
global $db, $user;
|
||||||
|
if (!$user->hasRight('bericht', 'write')) bericht_ajax_fail('Permission denied', 403);
|
||||||
|
|
||||||
|
$berichtid = (int) ($_POST['berichtid'] ?? 0);
|
||||||
|
$layout = (string) ($_POST['layout'] ?? 'grid_4');
|
||||||
|
$relpaths_raw = (string) ($_POST['relpaths'] ?? '[]');
|
||||||
|
$relpaths = json_decode($relpaths_raw, true);
|
||||||
|
if (!is_array($relpaths) || empty($relpaths)) bericht_ajax_fail('Keine Bilder ausgewählt');
|
||||||
|
|
||||||
|
$valid_layouts = array('grid_2', 'grid_2v', 'grid_4', 'grid_6');
|
||||||
|
if (!in_array($layout, $valid_layouts, true)) $layout = 'grid_4';
|
||||||
|
|
||||||
|
$bericht = new Bericht($db);
|
||||||
|
if ($bericht->fetch($berichtid) <= 0) bericht_ajax_fail('Bericht nicht gefunden', 404);
|
||||||
|
|
||||||
|
// Höchste page_order ermitteln
|
||||||
|
$res = $db->query("SELECT COALESCE(MAX(page_order),0) AS m FROM ".$db->prefix()."bericht_page WHERE fk_bericht = ".((int) $berichtid));
|
||||||
|
$next_order = ($res && ($o = $db->fetch_object($res))) ? ((int) $o->m) + 1 : 1;
|
||||||
|
|
||||||
|
// Neue Page anlegen — dummy source_path, da Bilder über page_image kommen
|
||||||
|
$page = new BerichtPage($db);
|
||||||
|
$page->fk_bericht = $berichtid;
|
||||||
|
$page->page_order = $next_order;
|
||||||
|
$page->source_type = 'grid';
|
||||||
|
$page->source_path = ''; // bei grid leer
|
||||||
|
$page->layout = $layout;
|
||||||
|
$page->image_scale = 1.0;
|
||||||
|
$page->image_align = 'fit';
|
||||||
|
if ($page->create() <= 0) bericht_ajax_fail('Page-Insert fehlgeschlagen');
|
||||||
|
|
||||||
|
// Bilder in Slots verteilen
|
||||||
|
$slot_count = BerichtPage::slotCountForLayout($layout);
|
||||||
|
$slot = 0;
|
||||||
|
foreach ($relpaths as $rel) {
|
||||||
|
if ($slot >= $slot_count) break;
|
||||||
|
$full = bericht_resolve_data_path($rel);
|
||||||
|
if (!$full || !file_exists($full)) continue;
|
||||||
|
$page->setSlotImage($slot, $rel, 0);
|
||||||
|
$slot++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bericht_ajax_ok(array('pageid' => $page->id, 'created_slots' => $slot));
|
||||||
|
|
@ -78,45 +78,7 @@ if (!empty($bericht->template_odt)) {
|
||||||
|
|
||||||
// --- Seiten ---
|
// --- Seiten ---
|
||||||
foreach ($pages as $page) {
|
foreach ($pages as $page) {
|
||||||
$full = bericht_resolve_data_path($page->source_path);
|
bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded);
|
||||||
if (!$full || !file_exists($full)) continue;
|
|
||||||
|
|
||||||
if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
|
|
||||||
// Bild als Seite im konfigurierten Format/Orientation
|
|
||||||
$pdf->AddPage($ori, $fmt);
|
|
||||||
list($iw, $ih) = @getimagesize($full);
|
|
||||||
if ($iw && $ih) {
|
|
||||||
$pageW = $pdf->getPageWidth();
|
|
||||||
$pageH = $pdf->getPageHeight();
|
|
||||||
$margin = 10;
|
|
||||||
$maxW = $pageW - 2 * $margin;
|
|
||||||
$maxH = $pageH - 2 * $margin - 10; // 10mm für Notiz unten
|
|
||||||
$ratio = min($maxW / $iw, $maxH / $ih);
|
|
||||||
$w = $iw * $ratio; $h = $ih * $ratio;
|
|
||||||
$x = ($pageW - $w) / 2; $y = $margin;
|
|
||||||
$pdf->Image($full, $x, $y, $w, $h);
|
|
||||||
bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);
|
|
||||||
}
|
|
||||||
} elseif ($fpdi_loaded && preg_match('/\.pdf$/i', $full)) {
|
|
||||||
try {
|
|
||||||
$pdf->setSourceFile($full);
|
|
||||||
$tpl = $pdf->importPage($page->source_page ?: 1);
|
|
||||||
$size = $pdf->getTemplateSize($tpl);
|
|
||||||
$pdf->AddPage($size['orientation'] ?? 'P', array($size['width'], $size['height']));
|
|
||||||
$pdf->useTemplate($tpl);
|
|
||||||
// Annotationen über volle Seitenfläche
|
|
||||||
bericht_burn_annotations($pdf, $page->fabric_json, 0, 0, $size['width'], $size['height']);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
// Seite überspringen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notiz unten
|
|
||||||
if (!empty($page->note)) {
|
|
||||||
$pdf->SetY(-20);
|
|
||||||
$pdf->SetFont('helvetica', 'I', 9);
|
|
||||||
$pdf->MultiCell(0, 5, $page->note, 0, 'L');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Speichern ---
|
// --- Speichern ---
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,29 @@ if (!$user->hasRight('bericht', 'read')) accessforbidden();
|
||||||
$pageid = GETPOSTINT('pageid');
|
$pageid = GETPOSTINT('pageid');
|
||||||
if (!$pageid) { http_response_code(400); exit; }
|
if (!$pageid) { http_response_code(400); exit; }
|
||||||
|
|
||||||
$res = $db->query("SELECT source_type, source_path FROM ".$db->prefix()."bericht_page WHERE rowid = ".((int) $pageid));
|
$res = $db->query("SELECT source_type, source_path, COALESCE(layout,'single') AS layout FROM ".$db->prefix()."bericht_page WHERE rowid = ".((int) $pageid));
|
||||||
if (!$res) { http_response_code(500); exit; }
|
if (!$res) { http_response_code(500); exit; }
|
||||||
$row = $db->fetch_object($res);
|
$row = $db->fetch_object($res);
|
||||||
if (!$row) { http_response_code(404); exit; }
|
if (!$row) { http_response_code(404); exit; }
|
||||||
|
|
||||||
|
// Bei Multi-Image-Seiten: serverseitig ein Composite-Bild rendern (PNG)
|
||||||
|
if ($row->layout !== 'single') {
|
||||||
|
$imgs_q = $db->query("SELECT slot, source_path FROM ".$db->prefix()."bericht_page_image WHERE fk_page = ".((int) $pageid)." ORDER BY slot");
|
||||||
|
if (!$imgs_q) { http_response_code(500); exit; }
|
||||||
|
$imgs = array();
|
||||||
|
while ($r = $db->fetch_object($imgs_q)) {
|
||||||
|
$imgs[(int) $r->slot] = $r->source_path;
|
||||||
|
}
|
||||||
|
if (empty($imgs)) { http_response_code(404); exit; }
|
||||||
|
|
||||||
|
$composite = bericht_render_grid_png($row->layout, $imgs);
|
||||||
|
if (!$composite) { http_response_code(500); exit; }
|
||||||
|
header('Content-Type: image/png');
|
||||||
|
header('Cache-Control: no-store');
|
||||||
|
echo $composite;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$full = bericht_resolve_data_path($row->source_path);
|
$full = bericht_resolve_data_path($row->source_path);
|
||||||
if (!$full || !file_exists($full)) { http_response_code(404); exit; }
|
if (!$full || !file_exists($full)) { http_response_code(404); exit; }
|
||||||
|
|
||||||
|
|
@ -36,3 +54,75 @@ header('Content-Type: '.$mime);
|
||||||
header('Content-Length: '.filesize($full));
|
header('Content-Length: '.filesize($full));
|
||||||
header('Cache-Control: private, max-age=300');
|
header('Cache-Control: private, max-age=300');
|
||||||
readfile($full);
|
readfile($full);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein Composite-PNG für eine Multi-Image-Seite (Editor-Vorschau).
|
||||||
|
*/
|
||||||
|
function bericht_render_grid_png($layout, $imgs)
|
||||||
|
{
|
||||||
|
// A4 Hochformat als Grundfläche, 150 DPI
|
||||||
|
$W = 1240; // ~A4 @ 150dpi
|
||||||
|
$H = 1754;
|
||||||
|
$margin = 40;
|
||||||
|
$gap = 16;
|
||||||
|
|
||||||
|
$canvas = imagecreatetruecolor($W, $H);
|
||||||
|
imagefill($canvas, 0, 0, imagecolorallocate($canvas, 255, 255, 255));
|
||||||
|
|
||||||
|
// Slot-Berechnung (gleiche Logik wie BerichtPage::slotRects, aber in Pixel)
|
||||||
|
$iw = $W - 2 * $margin;
|
||||||
|
$ih = $H - 2 * $margin - 60; // 60px für Notiz-Bereich
|
||||||
|
|
||||||
|
$rects = array();
|
||||||
|
if ($layout === 'grid_2') {
|
||||||
|
$h = ($ih - $gap) / 2;
|
||||||
|
for ($i = 0; $i < 2; $i++) $rects[] = array($margin, $margin + $i * ($h + $gap), $iw, $h);
|
||||||
|
} elseif ($layout === 'grid_2v') {
|
||||||
|
$w = ($iw - $gap) / 2;
|
||||||
|
for ($i = 0; $i < 2; $i++) $rects[] = array($margin + $i * ($w + $gap), $margin, $w, $ih);
|
||||||
|
} elseif ($layout === 'grid_4') {
|
||||||
|
$w = ($iw - $gap) / 2;
|
||||||
|
$h = ($ih - $gap) / 2;
|
||||||
|
for ($r = 0; $r < 2; $r++) for ($c = 0; $c < 2; $c++) {
|
||||||
|
$rects[] = array($margin + $c * ($w + $gap), $margin + $r * ($h + $gap), $w, $h);
|
||||||
|
}
|
||||||
|
} elseif ($layout === 'grid_6') {
|
||||||
|
$w = ($iw - 2 * $gap) / 3;
|
||||||
|
$h = ($ih - $gap) / 2;
|
||||||
|
for ($r = 0; $r < 2; $r++) for ($c = 0; $c < 3; $c++) {
|
||||||
|
$rects[] = array($margin + $c * ($w + $gap), $margin + $r * ($h + $gap), $w, $h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rects as $i => $r) {
|
||||||
|
list($rx, $ry, $rw, $rh) = $r;
|
||||||
|
// Slot-Hintergrund + Border
|
||||||
|
$border = imagecolorallocate($canvas, 200, 200, 200);
|
||||||
|
imagerectangle($canvas, $rx, $ry, $rx + $rw, $ry + $rh, $border);
|
||||||
|
|
||||||
|
if (!isset($imgs[$i])) continue;
|
||||||
|
$full = bericht_resolve_data_path($imgs[$i]);
|
||||||
|
if (!$full || !file_exists($full)) continue;
|
||||||
|
|
||||||
|
$info = @getimagesize($full);
|
||||||
|
if (!$info) continue;
|
||||||
|
$src = null;
|
||||||
|
if ($info[2] === IMAGETYPE_JPEG) $src = @imagecreatefromjpeg($full);
|
||||||
|
elseif ($info[2] === IMAGETYPE_PNG) $src = @imagecreatefrompng($full);
|
||||||
|
if (!$src) continue;
|
||||||
|
|
||||||
|
$sw = $info[0]; $sh = $info[1];
|
||||||
|
$ratio = min($rw / $sw, $rh / $sh);
|
||||||
|
$dw = (int) ($sw * $ratio);
|
||||||
|
$dh = (int) ($sh * $ratio);
|
||||||
|
$dx = (int) ($rx + ($rw - $dw) / 2);
|
||||||
|
$dy = (int) ($ry + ($rh - $dh) / 2);
|
||||||
|
imagecopyresampled($canvas, $src, $dx, $dy, 0, 0, $dw, $dh, $sw, $sh);
|
||||||
|
imagedestroy($src);
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
imagepng($canvas, null, 6);
|
||||||
|
imagedestroy($canvas);
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ 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, rotation FROM ".$db->prefix()."bericht_page WHERE rowid = ".((int) $pageid));
|
$res = $db->query("SELECT fabric_json, note, rotation, COALESCE(layout,'single') AS layout,"
|
||||||
|
." COALESCE(image_scale,1.0) AS image_scale, COALESCE(image_align,'fit') AS image_align"
|
||||||
|
." 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);
|
||||||
|
|
@ -16,4 +18,7 @@ bericht_ajax_ok(array(
|
||||||
'fabric_json' => $row->fabric_json,
|
'fabric_json' => $row->fabric_json,
|
||||||
'note' => $row->note,
|
'note' => $row->note,
|
||||||
'rotation' => (int) $row->rotation,
|
'rotation' => (int) $row->rotation,
|
||||||
|
'layout' => $row->layout,
|
||||||
|
'image_scale' => (float) $row->image_scale,
|
||||||
|
'image_align' => $row->image_align,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -86,46 +86,9 @@ if (!empty($bericht->template_odt)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seiten — Logik aus generate_pdf.php duplizieren (vereinfacht: nur Bilder + PDF + Annotationen)
|
// Seiten via gemeinsamer Render-Funktion
|
||||||
foreach ($pages as $page) {
|
foreach ($pages as $page) {
|
||||||
$full = bericht_resolve_data_path($page->source_path);
|
bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded);
|
||||||
if (!$full || !file_exists($full)) continue;
|
|
||||||
|
|
||||||
if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
|
|
||||||
$pdf->AddPage($ori, $fmt);
|
|
||||||
list($iw, $ih) = @getimagesize($full);
|
|
||||||
if ($iw && $ih) {
|
|
||||||
$pageW = $pdf->getPageWidth();
|
|
||||||
$pageH = $pdf->getPageHeight();
|
|
||||||
$margin = 10;
|
|
||||||
$maxW = $pageW - 2 * $margin;
|
|
||||||
$maxH = $pageH - 2 * $margin - 10;
|
|
||||||
$ratio = min($maxW / $iw, $maxH / $ih);
|
|
||||||
$w = $iw * $ratio; $h = $ih * $ratio;
|
|
||||||
$x = ($pageW - $w) / 2; $y = $margin;
|
|
||||||
$pdf->Image($full, $x, $y, $w, $h);
|
|
||||||
if (function_exists('bericht_burn_annotations')) {
|
|
||||||
bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($fpdi_loaded && preg_match('/\.pdf$/i', $full)) {
|
|
||||||
try {
|
|
||||||
$pdf->setSourceFile($full);
|
|
||||||
$tpl = $pdf->importPage($page->source_page ?: 1);
|
|
||||||
$size = $pdf->getTemplateSize($tpl);
|
|
||||||
$pdf->AddPage($size['orientation'] ?? 'P', array($size['width'], $size['height']));
|
|
||||||
$pdf->useTemplate($tpl);
|
|
||||||
if (function_exists('bericht_burn_annotations')) {
|
|
||||||
bericht_burn_annotations($pdf, $page->fabric_json, 0, 0, $size['width'], $size['height']);
|
|
||||||
}
|
|
||||||
} catch (Throwable $e) { /* skip */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($page->note)) {
|
|
||||||
$pdf->SetY(-20);
|
|
||||||
$pdf->SetFont('helvetica', 'I', 9);
|
|
||||||
$pdf->MultiCell(0, 5, $page->note, 0, 'L');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direkt im Browser anzeigen — kein File schreiben, kein ECM
|
// Direkt im Browser anzeigen — kein File schreiben, kein ECM
|
||||||
|
|
|
||||||
32
ajax/save_page_options.php
Normal file
32
ajax/save_page_options.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
/* Speichert Layout / image_scale / image_align einer Seite.
|
||||||
|
* POST: pageid, layout, image_scale, image_align, token
|
||||||
|
*/
|
||||||
|
require_once __DIR__.'/_inc.php';
|
||||||
|
|
||||||
|
global $db, $user;
|
||||||
|
if (!$user->hasRight('bericht', 'write')) bericht_ajax_fail('Permission denied', 403);
|
||||||
|
|
||||||
|
$pageid = (int) ($_POST['pageid'] ?? 0);
|
||||||
|
if (!$pageid) bericht_ajax_fail('pageid fehlt');
|
||||||
|
|
||||||
|
$layout = $_POST['layout'] ?? 'single';
|
||||||
|
$scale = (float) ($_POST['image_scale'] ?? 1.0);
|
||||||
|
$align = $_POST['image_align'] ?? 'fit';
|
||||||
|
|
||||||
|
$valid_layouts = array('single', 'grid_2', 'grid_2v', 'grid_4', 'grid_6');
|
||||||
|
if (!in_array($layout, $valid_layouts, true)) $layout = 'single';
|
||||||
|
|
||||||
|
$valid_align = array('fit', 'center', 'topleft', 'topright', 'bottomleft', 'bottomright');
|
||||||
|
if (!in_array($align, $valid_align, true)) $align = 'fit';
|
||||||
|
|
||||||
|
$scale = max(0.2, min(1.0, $scale));
|
||||||
|
|
||||||
|
$sql = "UPDATE ".$db->prefix()."bericht_page SET "
|
||||||
|
."layout = '".$db->escape($layout)."',"
|
||||||
|
."image_scale = ".((float) $scale).","
|
||||||
|
."image_align = '".$db->escape($align)."'"
|
||||||
|
." WHERE rowid = ".((int) $pageid);
|
||||||
|
if (!$db->query($sql)) bericht_ajax_fail($db->lasterror());
|
||||||
|
|
||||||
|
bericht_ajax_ok(array('layout' => $layout, 'image_scale' => $scale, 'image_align' => $align));
|
||||||
32
ajax/set_slot_image.php
Normal file
32
ajax/set_slot_image.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
/* Setzt das Bild eines Slots in einer Multi-Image-Seite.
|
||||||
|
* POST: pageid, slot, relpath, token
|
||||||
|
*/
|
||||||
|
require_once __DIR__.'/_inc.php';
|
||||||
|
|
||||||
|
global $db, $user;
|
||||||
|
if (!$user->hasRight('bericht', 'write')) bericht_ajax_fail('Permission denied', 403);
|
||||||
|
|
||||||
|
$pageid = (int) ($_POST['pageid'] ?? 0);
|
||||||
|
$slot = (int) ($_POST['slot'] ?? 0);
|
||||||
|
$relpath = (string) ($_POST['relpath'] ?? '');
|
||||||
|
|
||||||
|
if (!$pageid || $slot < 0 || empty($relpath)) bericht_ajax_fail('Parameter fehlen');
|
||||||
|
|
||||||
|
$full = bericht_resolve_data_path($relpath);
|
||||||
|
if (!$full || !file_exists($full)) bericht_ajax_fail('Datei nicht gefunden', 404);
|
||||||
|
|
||||||
|
// Page laden
|
||||||
|
$page_obj = null;
|
||||||
|
$pages_res = $db->query("SELECT rowid, fk_bericht, layout FROM ".$db->prefix()."bericht_page WHERE rowid = ".((int) $pageid));
|
||||||
|
if (!$pages_res || !($row = $db->fetch_object($pages_res))) bericht_ajax_fail('Seite nicht gefunden', 404);
|
||||||
|
|
||||||
|
$p = new BerichtPage($db);
|
||||||
|
$p->id = $row->rowid;
|
||||||
|
$p->fk_bericht = $row->fk_bericht;
|
||||||
|
$p->layout = $row->layout;
|
||||||
|
|
||||||
|
if ($p->setSlotImage($slot, $relpath, 0) > 0) {
|
||||||
|
bericht_ajax_ok();
|
||||||
|
}
|
||||||
|
bericht_ajax_fail('DB-Fehler');
|
||||||
|
|
@ -262,6 +262,9 @@ if (!$bericht) {
|
||||||
'preview_pdf' => dol_buildpath('/bericht/ajax/preview_pdf.php', 1),
|
'preview_pdf' => dol_buildpath('/bericht/ajax/preview_pdf.php', 1),
|
||||||
'delete_attachment'=> dol_buildpath('/bericht/ajax/delete_attachment.php', 1),
|
'delete_attachment'=> dol_buildpath('/bericht/ajax/delete_attachment.php', 1),
|
||||||
'save_meta' => dol_buildpath('/bericht/ajax/save_meta.php', 1),
|
'save_meta' => dol_buildpath('/bericht/ajax/save_meta.php', 1),
|
||||||
|
'save_page_options'=> dol_buildpath('/bericht/ajax/save_page_options.php', 1),
|
||||||
|
'create_grid_page' => dol_buildpath('/bericht/ajax/create_grid_page.php', 1),
|
||||||
|
'set_slot_image' => dol_buildpath('/bericht/ajax/set_slot_image.php', 1),
|
||||||
),
|
),
|
||||||
'lang' => array(
|
'lang' => array(
|
||||||
'undo' => $langs->trans("BerichtUndo"),
|
'undo' => $langs->trans("BerichtUndo"),
|
||||||
|
|
@ -350,7 +353,14 @@ if (!$bericht) {
|
||||||
}
|
}
|
||||||
print '</div>';
|
print '</div>';
|
||||||
}
|
}
|
||||||
print '<button type="button" id="btn-add-selected" class="butAction">'.$langs->trans("BerichtAddSelectedToReport").'</button>';
|
print '<button type="button" id="btn-add-selected" class="butAction" title="Jedes ausgewählte Bild als eigene Seite">'.$langs->trans("BerichtAddSelectedToReport").'</button>';
|
||||||
|
print '<div class="bericht-grid-buttons" style="margin-top:6px;">';
|
||||||
|
print '<span class="opacitymedium small">Mehrere Bilder als Grid:</span><br>';
|
||||||
|
print '<button type="button" class="btn-add-grid" data-layout="grid_2" title="2 Bilder (oben/unten)">▭▭</button> ';
|
||||||
|
print '<button type="button" class="btn-add-grid" data-layout="grid_2v" title="2 Bilder (links/rechts)">▯▯</button> ';
|
||||||
|
print '<button type="button" class="btn-add-grid" data-layout="grid_4" title="2x2 Grid">▦</button> ';
|
||||||
|
print '<button type="button" class="btn-add-grid" data-layout="grid_6" title="3x2 Grid">▦▦</button>';
|
||||||
|
print '</div>';
|
||||||
}
|
}
|
||||||
print '<hr>';
|
print '<hr>';
|
||||||
print '<div class="bericht-upload">';
|
print '<div class="bericht-upload">';
|
||||||
|
|
@ -400,6 +410,30 @@ if (!$bericht) {
|
||||||
print '<span class="sep"></span>';
|
print '<span class="sep"></span>';
|
||||||
print '<button type="button" id="btn-rotate-left" title="'.$langs->trans("BerichtRotateLeft").' (Seite 90° nach links drehen)">⟲</button>';
|
print '<button type="button" id="btn-rotate-left" title="'.$langs->trans("BerichtRotateLeft").' (Seite 90° nach links drehen)">⟲</button>';
|
||||||
print '<button type="button" id="btn-rotate-right" title="'.$langs->trans("BerichtRotateRight").' (Seite 90° nach rechts drehen)">⟳</button>';
|
print '<button type="button" id="btn-rotate-right" title="'.$langs->trans("BerichtRotateRight").' (Seite 90° nach rechts drehen)">⟳</button>';
|
||||||
|
|
||||||
|
// ====== Dritte Zeile: Layout & Bildgröße der aktuellen Seite ======
|
||||||
|
print '<div class="row-break"></div>';
|
||||||
|
print '<label title="Seiten-Layout: Single = 1 Bild, Grid = mehrere Bilder">Layout: <select id="page-layout">'
|
||||||
|
.'<option value="single">Single (1 Bild)</option>'
|
||||||
|
.'<option value="grid_2">2 Bilder (oben/unten)</option>'
|
||||||
|
.'<option value="grid_2v">2 Bilder (links/rechts)</option>'
|
||||||
|
.'<option value="grid_4">4 Bilder (2x2)</option>'
|
||||||
|
.'<option value="grid_6">6 Bilder (3x2)</option>'
|
||||||
|
.'</select></label>';
|
||||||
|
print '<label class="single-only" title="Wie groß das Bild auf der Seite gedruckt wird (nur Single)">Größe: <select id="page-imgscale">'
|
||||||
|
.'<option value="1.0">Vollseite (100%)</option>'
|
||||||
|
.'<option value="0.7">Groß (70%)</option>'
|
||||||
|
.'<option value="0.5">Mittel (50%)</option>'
|
||||||
|
.'<option value="0.3">Klein (30%)</option>'
|
||||||
|
.'</select></label>';
|
||||||
|
print '<label class="single-only" title="Position auf der Seite (nur bei kleineren Bildern)">Position: <select id="page-imgalign">'
|
||||||
|
.'<option value="fit">Anpassen</option>'
|
||||||
|
.'<option value="center">Zentriert</option>'
|
||||||
|
.'<option value="topleft">Oben links</option>'
|
||||||
|
.'<option value="topright">Oben rechts</option>'
|
||||||
|
.'<option value="bottomleft">Unten links</option>'
|
||||||
|
.'<option value="bottomright">Unten rechts</option>'
|
||||||
|
.'</select></label>';
|
||||||
print '</div>';
|
print '</div>';
|
||||||
print '<div class="bericht-canvas-wrap">';
|
print '<div class="bericht-canvas-wrap">';
|
||||||
print '<canvas id="pdf-canvas"></canvas>';
|
print '<canvas id="pdf-canvas"></canvas>';
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,16 @@ class Bericht extends CommonObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eine Seite eines Berichts.
|
* Eine Seite eines Berichts.
|
||||||
|
*
|
||||||
|
* Layout-Modi:
|
||||||
|
* single = ein Bild/PDF direkt auf source_path
|
||||||
|
* grid_2 = 2 Bilder nebeneinander (über llx_bericht_page_image)
|
||||||
|
* grid_4 = 2x2 Grid
|
||||||
|
* grid_6 = 3x2 Grid
|
||||||
|
*
|
||||||
|
* Bei single werden image_scale + image_align angewendet:
|
||||||
|
* image_scale: 1.0 / 0.7 / 0.5 / 0.3
|
||||||
|
* image_align: fit / center / topleft / topright / bottomleft / bottomright
|
||||||
*/
|
*/
|
||||||
class BerichtPage
|
class BerichtPage
|
||||||
{
|
{
|
||||||
|
|
@ -261,16 +271,124 @@ class BerichtPage
|
||||||
public $rotation;
|
public $rotation;
|
||||||
public $fabric_json;
|
public $fabric_json;
|
||||||
public $note;
|
public $note;
|
||||||
|
public $layout = 'single';
|
||||||
|
public $image_scale = 1.0;
|
||||||
|
public $image_align = 'fit';
|
||||||
|
|
||||||
public function __construct(DoliDB $db)
|
public function __construct(DoliDB $db)
|
||||||
{
|
{
|
||||||
$this->db = $db;
|
$this->db = $db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert die Bilder einer Multi-Image-Seite (layout != single).
|
||||||
|
* @return array [['rowid'=>..., 'slot'=>..., 'source_path'=>..., 'rotation'=>...], ...]
|
||||||
|
*/
|
||||||
|
public function getImages()
|
||||||
|
{
|
||||||
|
$list = array();
|
||||||
|
if ($this->layout === 'single') return $list;
|
||||||
|
$sql = "SELECT rowid, slot, source_path, rotation FROM ".$this->db->prefix()."bericht_page_image"
|
||||||
|
." WHERE fk_page = ".((int) $this->id)
|
||||||
|
." ORDER BY slot ASC";
|
||||||
|
$res = $this->db->query($sql);
|
||||||
|
if (!$res) return $list;
|
||||||
|
while ($obj = $this->db->fetch_object($res)) {
|
||||||
|
$list[] = array(
|
||||||
|
'rowid' => (int) $obj->rowid,
|
||||||
|
'slot' => (int) $obj->slot,
|
||||||
|
'source_path' => $obj->source_path,
|
||||||
|
'rotation' => (int) $obj->rotation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt das Bild eines Slots (überschreibt vorhandenes).
|
||||||
|
*/
|
||||||
|
public function setSlotImage($slot, $source_path, $rotation = 0)
|
||||||
|
{
|
||||||
|
$this->db->query("DELETE FROM ".$this->db->prefix()."bericht_page_image"
|
||||||
|
." WHERE fk_page = ".((int) $this->id)." AND slot = ".((int) $slot));
|
||||||
|
$sql = "INSERT INTO ".$this->db->prefix()."bericht_page_image (fk_page, slot, source_path, rotation)"
|
||||||
|
." VALUES (".((int) $this->id).", ".((int) $slot).", '".$this->db->escape($source_path)."', ".((int) $rotation).")";
|
||||||
|
return $this->db->query($sql) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearImages()
|
||||||
|
{
|
||||||
|
return $this->db->query("DELETE FROM ".$this->db->prefix()."bericht_page_image WHERE fk_page = ".((int) $this->id)) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anzahl Slots eines Layouts.
|
||||||
|
*/
|
||||||
|
public static function slotCountForLayout($layout)
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'single' => 1,
|
||||||
|
'grid_2' => 2,
|
||||||
|
'grid_2v' => 2,
|
||||||
|
'grid_4' => 4,
|
||||||
|
'grid_6' => 6,
|
||||||
|
)[$layout] ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet die Slot-Rectangles innerhalb einer Seite (mm) für ein Grid-Layout.
|
||||||
|
* @return array [['x'=>..,'y'=>..,'w'=>..,'h'=>..], ...]
|
||||||
|
*/
|
||||||
|
public static function slotRects($layout, $pageW, $pageH, $margin = 10, $gap = 4)
|
||||||
|
{
|
||||||
|
$rects = array();
|
||||||
|
$iw = $pageW - 2 * $margin;
|
||||||
|
$ih = $pageH - 2 * $margin - 10; // 10mm Notiz unten
|
||||||
|
|
||||||
|
if ($layout === 'grid_2') {
|
||||||
|
// 2 Bilder horizontal: 1 oben, 1 unten
|
||||||
|
$h = ($ih - $gap) / 2;
|
||||||
|
for ($i = 0; $i < 2; $i++) {
|
||||||
|
$rects[] = array('x' => $margin, 'y' => $margin + $i * ($h + $gap), 'w' => $iw, 'h' => $h);
|
||||||
|
}
|
||||||
|
} elseif ($layout === 'grid_2v') {
|
||||||
|
// 2 Bilder vertikal nebeneinander
|
||||||
|
$w = ($iw - $gap) / 2;
|
||||||
|
for ($i = 0; $i < 2; $i++) {
|
||||||
|
$rects[] = array('x' => $margin + $i * ($w + $gap), 'y' => $margin, 'w' => $w, 'h' => $ih);
|
||||||
|
}
|
||||||
|
} elseif ($layout === 'grid_4') {
|
||||||
|
$w = ($iw - $gap) / 2;
|
||||||
|
$h = ($ih - $gap) / 2;
|
||||||
|
for ($r = 0; $r < 2; $r++) {
|
||||||
|
for ($c = 0; $c < 2; $c++) {
|
||||||
|
$rects[] = array(
|
||||||
|
'x' => $margin + $c * ($w + $gap),
|
||||||
|
'y' => $margin + $r * ($h + $gap),
|
||||||
|
'w' => $w, 'h' => $h
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($layout === 'grid_6') {
|
||||||
|
$w = ($iw - 2 * $gap) / 3;
|
||||||
|
$h = ($ih - $gap) / 2;
|
||||||
|
for ($r = 0; $r < 2; $r++) {
|
||||||
|
for ($c = 0; $c < 3; $c++) {
|
||||||
|
$rects[] = array(
|
||||||
|
'x' => $margin + $c * ($w + $gap),
|
||||||
|
'y' => $margin + $r * ($h + $gap),
|
||||||
|
'w' => $w, 'h' => $h
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $rects;
|
||||||
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
$sql = "INSERT INTO ".$this->db->prefix()."bericht_page ("
|
$sql = "INSERT INTO ".$this->db->prefix()."bericht_page ("
|
||||||
."fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note"
|
."fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note, layout, image_scale, image_align"
|
||||||
.") VALUES ("
|
.") VALUES ("
|
||||||
.((int) $this->fk_bericht).","
|
.((int) $this->fk_bericht).","
|
||||||
.((int) $this->page_order).","
|
.((int) $this->page_order).","
|
||||||
|
|
@ -279,7 +397,10 @@ class BerichtPage
|
||||||
.($this->source_page !== null ? (int) $this->source_page : "NULL").","
|
.($this->source_page !== null ? (int) $this->source_page : "NULL").","
|
||||||
.((int) ($this->rotation ?? 0)).","
|
.((int) ($this->rotation ?? 0)).","
|
||||||
.($this->fabric_json !== null ? "'".$this->db->escape($this->fabric_json)."'" : "NULL").","
|
.($this->fabric_json !== null ? "'".$this->db->escape($this->fabric_json)."'" : "NULL").","
|
||||||
.($this->note ? "'".$this->db->escape($this->note)."'" : "NULL")
|
.($this->note ? "'".$this->db->escape($this->note)."'" : "NULL").","
|
||||||
|
."'".$this->db->escape($this->layout ?: 'single')."',"
|
||||||
|
.((float) ($this->image_scale ?: 1.0)).","
|
||||||
|
."'".$this->db->escape($this->image_align ?: 'fit')."'"
|
||||||
.")";
|
.")";
|
||||||
$res = $this->db->query($sql);
|
$res = $this->db->query($sql);
|
||||||
if (!$res) return -1;
|
if (!$res) return -1;
|
||||||
|
|
@ -293,7 +414,10 @@ class BerichtPage
|
||||||
."page_order = ".((int) $this->page_order).","
|
."page_order = ".((int) $this->page_order).","
|
||||||
."rotation = ".((int) ($this->rotation ?? 0)).","
|
."rotation = ".((int) ($this->rotation ?? 0)).","
|
||||||
."fabric_json = ".($this->fabric_json !== null ? "'".$this->db->escape($this->fabric_json)."'" : "NULL").","
|
."fabric_json = ".($this->fabric_json !== null ? "'".$this->db->escape($this->fabric_json)."'" : "NULL").","
|
||||||
."note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL")
|
."note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL").","
|
||||||
|
."layout = '".$this->db->escape($this->layout ?: 'single')."',"
|
||||||
|
."image_scale = ".((float) ($this->image_scale ?: 1.0)).","
|
||||||
|
."image_align = '".$this->db->escape($this->image_align ?: 'fit')."'"
|
||||||
." WHERE rowid = ".((int) $this->id);
|
." WHERE rowid = ".((int) $this->id);
|
||||||
return $this->db->query($sql) ? 1 : -1;
|
return $this->db->query($sql) ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
@ -306,7 +430,10 @@ class BerichtPage
|
||||||
public static function fetchAllForBericht(DoliDB $db, $fk_bericht)
|
public static function fetchAllForBericht(DoliDB $db, $fk_bericht)
|
||||||
{
|
{
|
||||||
$list = array();
|
$list = array();
|
||||||
$sql = "SELECT rowid, fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note"
|
$sql = "SELECT rowid, fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note,"
|
||||||
|
." COALESCE(layout, 'single') AS layout,"
|
||||||
|
." COALESCE(image_scale, 1.0) AS image_scale,"
|
||||||
|
." COALESCE(image_align, 'fit') AS image_align"
|
||||||
." FROM ".$db->prefix()."bericht_page"
|
." FROM ".$db->prefix()."bericht_page"
|
||||||
." WHERE fk_bericht = ".((int) $fk_bericht)
|
." WHERE fk_bericht = ".((int) $fk_bericht)
|
||||||
." ORDER BY page_order ASC, rowid ASC";
|
." ORDER BY page_order ASC, rowid ASC";
|
||||||
|
|
@ -323,6 +450,9 @@ class BerichtPage
|
||||||
$p->rotation = (int) $obj->rotation;
|
$p->rotation = (int) $obj->rotation;
|
||||||
$p->fabric_json = $obj->fabric_json;
|
$p->fabric_json = $obj->fabric_json;
|
||||||
$p->note = $obj->note;
|
$p->note = $obj->note;
|
||||||
|
$p->layout = $obj->layout;
|
||||||
|
$p->image_scale = (float) $obj->image_scale;
|
||||||
|
$p->image_align = $obj->image_align;
|
||||||
$list[] = $p;
|
$list[] = $p;
|
||||||
}
|
}
|
||||||
return $list;
|
return $list;
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,23 @@
|
||||||
|
|
||||||
.bericht-upload { margin-top: 10px; }
|
.bericht-upload { margin-top: 10px; }
|
||||||
|
|
||||||
|
.bericht-grid-buttons {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid var(--colorboxbordertitle1, #444);
|
||||||
|
}
|
||||||
|
.bericht-grid-buttons button {
|
||||||
|
background: var(--colorbacktitle1, #2a2a30);
|
||||||
|
color: var(--colortext, #ddd);
|
||||||
|
border: 1px solid var(--colorboxbordertitle1, #555);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.bericht-grid-buttons button:hover { background: var(--colorbackhmenu1, #337ab7); color: #fff; }
|
||||||
|
|
||||||
/* Mittlere Spalte: Editor-Canvas — bewusst dunkler Hintergrund (auch im Light-Mode),
|
/* Mittlere Spalte: Editor-Canvas — bewusst dunkler Hintergrund (auch im Light-Mode),
|
||||||
* weil ein Canvas auf grauer Fläche besser sichtbar ist.
|
* weil ein Canvas auf grauer Fläche besser sichtbar ist.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
77
js/editor.js
77
js/editor.js
|
|
@ -36,6 +36,9 @@
|
||||||
let currentPageRotation = 0; // 0 / 90 / 180 / 270
|
let currentPageRotation = 0; // 0 / 90 / 180 / 270
|
||||||
let currentPageBuffer = null; // ArrayBuffer der aktuellen Quelle
|
let currentPageBuffer = null; // ArrayBuffer der aktuellen Quelle
|
||||||
let currentPageMime = '';
|
let currentPageMime = '';
|
||||||
|
let currentPageLayout = 'single';
|
||||||
|
let currentPageScale = 1.0;
|
||||||
|
let currentPageAlign = 'fit';
|
||||||
let currentZoom = 1.0; // 1.0 = 100% (Container-Fit), 0.5..3.0
|
let currentZoom = 1.0; // 1.0 = 100% (Container-Fit), 0.5..3.0
|
||||||
let fabricCanvas = null;
|
let fabricCanvas = null;
|
||||||
const pdfCanvas = document.getElementById('pdf-canvas');
|
const pdfCanvas = document.getElementById('pdf-canvas');
|
||||||
|
|
@ -268,6 +271,20 @@
|
||||||
if (typeof data.rotation !== 'undefined' && data.rotation !== null) {
|
if (typeof data.rotation !== 'undefined' && data.rotation !== null) {
|
||||||
currentPageRotation = parseInt(data.rotation, 10) || 0;
|
currentPageRotation = parseInt(data.rotation, 10) || 0;
|
||||||
}
|
}
|
||||||
|
if (data.layout) currentPageLayout = data.layout;
|
||||||
|
if (data.image_scale) currentPageScale = parseFloat(data.image_scale);
|
||||||
|
if (data.image_align) currentPageAlign = data.image_align;
|
||||||
|
// Toolbar-Selects synchronisieren
|
||||||
|
const lEl = document.getElementById('page-layout');
|
||||||
|
const sEl = document.getElementById('page-imgscale');
|
||||||
|
const aEl = document.getElementById('page-imgalign');
|
||||||
|
if (lEl) lEl.value = currentPageLayout;
|
||||||
|
if (sEl) sEl.value = currentPageScale.toString();
|
||||||
|
if (aEl) aEl.value = currentPageAlign;
|
||||||
|
// Single-only Felder ein/ausblenden
|
||||||
|
document.querySelectorAll('.single-only').forEach(el => {
|
||||||
|
el.style.display = (currentPageLayout === 'single') ? '' : 'none';
|
||||||
|
});
|
||||||
} catch (e) { /* ok */ }
|
} catch (e) { /* ok */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,6 +331,36 @@
|
||||||
document.getElementById('btn-zoom-out').addEventListener('click', () => setZoom(currentZoom - 0.25));
|
document.getElementById('btn-zoom-out').addEventListener('click', () => setZoom(currentZoom - 0.25));
|
||||||
document.getElementById('btn-zoom-reset').addEventListener('click', () => setZoom(1.0));
|
document.getElementById('btn-zoom-reset').addEventListener('click', () => setZoom(1.0));
|
||||||
|
|
||||||
|
// Layout / Bildgröße / Align — pro Seite
|
||||||
|
const layoutEl = document.getElementById('page-layout');
|
||||||
|
const scaleEl = document.getElementById('page-imgscale');
|
||||||
|
const alignEl = document.getElementById('page-imgalign');
|
||||||
|
async function savePageOptions() {
|
||||||
|
if (!currentPageId) return;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('token', cfg.token);
|
||||||
|
fd.append('pageid', currentPageId);
|
||||||
|
fd.append('layout', layoutEl.value);
|
||||||
|
fd.append('image_scale', scaleEl.value);
|
||||||
|
fd.append('image_align', alignEl.value);
|
||||||
|
await fetch(cfg.urls.save_page_options, { method: 'POST', body: fd });
|
||||||
|
}
|
||||||
|
if (layoutEl) layoutEl.addEventListener('change', async () => {
|
||||||
|
currentPageLayout = layoutEl.value;
|
||||||
|
document.querySelectorAll('.single-only').forEach(el => {
|
||||||
|
el.style.display = (currentPageLayout === 'single') ? '' : 'none';
|
||||||
|
});
|
||||||
|
await savePageOptions();
|
||||||
|
});
|
||||||
|
if (scaleEl) scaleEl.addEventListener('change', async () => {
|
||||||
|
currentPageScale = parseFloat(scaleEl.value);
|
||||||
|
await savePageOptions();
|
||||||
|
});
|
||||||
|
if (alignEl) alignEl.addEventListener('change', async () => {
|
||||||
|
currentPageAlign = alignEl.value;
|
||||||
|
await savePageOptions();
|
||||||
|
});
|
||||||
|
|
||||||
// Schrift-Optionen für Text-Tool / selektierte Texte
|
// Schrift-Optionen für Text-Tool / selektierte Texte
|
||||||
const fontFamily = document.getElementById('tool-fontfamily');
|
const fontFamily = document.getElementById('tool-fontfamily');
|
||||||
const fontSize = document.getElementById('tool-fontsize');
|
const fontSize = document.getElementById('tool-fontsize');
|
||||||
|
|
@ -648,6 +695,36 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grid-Buttons: ausgewählte Bilder als eine Multi-Image-Seite hinzufügen
|
||||||
|
document.querySelectorAll('.btn-add-grid').forEach(b => {
|
||||||
|
b.addEventListener('click', async () => {
|
||||||
|
const layout = b.dataset.layout;
|
||||||
|
const checks = document.querySelectorAll('.att-check:checked');
|
||||||
|
if (!checks.length) {
|
||||||
|
alert('Bitte zuerst Bilder ankreuzen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const slotCount = { grid_2: 2, grid_2v: 2, grid_4: 4, grid_6: 6 }[layout] || 4;
|
||||||
|
const relpaths = Array.from(checks)
|
||||||
|
.filter(c => c.dataset.mime.startsWith('image'))
|
||||||
|
.slice(0, slotCount)
|
||||||
|
.map(c => c.dataset.relpath);
|
||||||
|
if (!relpaths.length) {
|
||||||
|
alert('Bitte mindestens ein Bild ankreuzen (PDFs sind in Grids nicht unterstützt)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('token', cfg.token);
|
||||||
|
fd.append('berichtid', cfg.berichtid);
|
||||||
|
fd.append('layout', layout);
|
||||||
|
fd.append('relpaths', JSON.stringify(relpaths));
|
||||||
|
const r = await fetch(cfg.urls.create_grid_page, { method: 'POST', body: fd });
|
||||||
|
const data = await r.json().catch(() => ({}));
|
||||||
|
if (data.success) location.reload();
|
||||||
|
else alert('Fehler: ' + (data.error || 'unbekannt'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Lösch-Buttons in der Anhänge-Liste
|
// Lösch-Buttons in der Anhänge-Liste
|
||||||
document.querySelectorAll('.att-delete').forEach(btn => {
|
document.querySelectorAll('.att-delete').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,127 @@ function bericht_render_cover_for_preview($template_path, $bericht, $parent, $te
|
||||||
return bericht_render_cover_internal($template_path, $bericht, $parent, $tempdir);
|
return bericht_render_cover_internal($template_path, $bericht, $parent, $tempdir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert eine BerichtPage in das aktuelle TCPDF-Objekt.
|
||||||
|
* Behandelt: single (mit image_scale/align), grid_2, grid_2v, grid_4, grid_6, PDF-Quelle.
|
||||||
|
*
|
||||||
|
* @param TCPDF $pdf
|
||||||
|
* @param BerichtPage $page
|
||||||
|
* @param string $ori 'P' oder 'L'
|
||||||
|
* @param string $fmt A4/A3/A5/Letter
|
||||||
|
* @param bool $fpdi_loaded
|
||||||
|
*/
|
||||||
|
function bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded)
|
||||||
|
{
|
||||||
|
$layout = $page->layout ?: 'single';
|
||||||
|
|
||||||
|
/* ----- Multi-Image Grid ----- */
|
||||||
|
if ($layout !== 'single') {
|
||||||
|
$imgs = $page->getImages();
|
||||||
|
if (empty($imgs)) return;
|
||||||
|
|
||||||
|
$pdf->AddPage($ori, $fmt);
|
||||||
|
$pageW = $pdf->getPageWidth();
|
||||||
|
$pageH = $pdf->getPageHeight();
|
||||||
|
$rects = BerichtPage::slotRects($layout, $pageW, $pageH);
|
||||||
|
|
||||||
|
foreach ($imgs as $img) {
|
||||||
|
$slot = $img['slot'];
|
||||||
|
if (!isset($rects[$slot])) continue;
|
||||||
|
$r = $rects[$slot];
|
||||||
|
$full = bericht_resolve_data_path($img['source_path']);
|
||||||
|
if (!$full || !file_exists($full)) continue;
|
||||||
|
|
||||||
|
list($iw, $ih) = @getimagesize($full);
|
||||||
|
if (!$iw || !$ih) continue;
|
||||||
|
$ratio = min($r['w'] / $iw, $r['h'] / $ih);
|
||||||
|
$w = $iw * $ratio; $h = $ih * $ratio;
|
||||||
|
$x = $r['x'] + ($r['w'] - $w) / 2;
|
||||||
|
$y = $r['y'] + ($r['h'] - $h) / 2;
|
||||||
|
$pdf->Image($full, $x, $y, $w, $h);
|
||||||
|
}
|
||||||
|
// Annotationen über volle Seite
|
||||||
|
bericht_burn_annotations($pdf, $page->fabric_json, 0, 0, $pageW, $pageH);
|
||||||
|
|
||||||
|
// Notiz unten
|
||||||
|
if (!empty($page->note)) {
|
||||||
|
$pdf->SetY(-20);
|
||||||
|
$pdf->SetFont('helvetica', 'I', 9);
|
||||||
|
$pdf->MultiCell(0, 5, $page->note, 0, 'L');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----- Single ----- */
|
||||||
|
$full = bericht_resolve_data_path($page->source_path);
|
||||||
|
if (!$full || !file_exists($full)) return;
|
||||||
|
|
||||||
|
if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
|
||||||
|
$pdf->AddPage($ori, $fmt);
|
||||||
|
list($iw, $ih) = @getimagesize($full);
|
||||||
|
if (!$iw || !$ih) return;
|
||||||
|
|
||||||
|
$pageW = $pdf->getPageWidth();
|
||||||
|
$pageH = $pdf->getPageHeight();
|
||||||
|
$margin = 10;
|
||||||
|
$maxW = ($pageW - 2 * $margin);
|
||||||
|
$maxH = ($pageH - 2 * $margin - 10); // Notiz unten
|
||||||
|
|
||||||
|
// image_scale: Anteil der maximalen Größe
|
||||||
|
$scale = max(0.2, min(1.0, (float) ($page->image_scale ?: 1.0)));
|
||||||
|
$availW = $maxW * $scale;
|
||||||
|
$availH = $maxH * $scale;
|
||||||
|
$ratio = min($availW / $iw, $availH / $ih);
|
||||||
|
$w = $iw * $ratio; $h = $ih * $ratio;
|
||||||
|
|
||||||
|
// image_align bestimmt die Position
|
||||||
|
list($x, $y) = bericht_align_position($page->image_align ?: 'fit', $w, $h, $pageW, $pageH, $margin);
|
||||||
|
|
||||||
|
$pdf->Image($full, $x, $y, $w, $h);
|
||||||
|
bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);
|
||||||
|
} elseif ($fpdi_loaded && preg_match('/\.pdf$/i', $full)) {
|
||||||
|
try {
|
||||||
|
$pdf->setSourceFile($full);
|
||||||
|
$tpl = $pdf->importPage($page->source_page ?: 1);
|
||||||
|
$size = $pdf->getTemplateSize($tpl);
|
||||||
|
$pdf->AddPage($size['orientation'] ?? 'P', array($size['width'], $size['height']));
|
||||||
|
$pdf->useTemplate($tpl);
|
||||||
|
bericht_burn_annotations($pdf, $page->fabric_json, 0, 0, $size['width'], $size['height']);
|
||||||
|
} catch (Throwable $e) { /* skip */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($page->note)) {
|
||||||
|
$pdf->SetY(-20);
|
||||||
|
$pdf->SetFont('helvetica', 'I', 9);
|
||||||
|
$pdf->MultiCell(0, 5, $page->note, 0, 'L');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet die x/y-Position für ein Bild auf einer Seite anhand des image_align-Werts.
|
||||||
|
*/
|
||||||
|
function bericht_align_position($align, $w, $h, $pageW, $pageH, $margin = 10)
|
||||||
|
{
|
||||||
|
$usableW = $pageW - 2 * $margin;
|
||||||
|
$usableH = $pageH - 2 * $margin - 10; // Notiz unten
|
||||||
|
|
||||||
|
switch ($align) {
|
||||||
|
case 'topleft':
|
||||||
|
return array($margin, $margin);
|
||||||
|
case 'topright':
|
||||||
|
return array($pageW - $margin - $w, $margin);
|
||||||
|
case 'bottomleft':
|
||||||
|
return array($margin, $margin + $usableH - $h);
|
||||||
|
case 'bottomright':
|
||||||
|
return array($pageW - $margin - $w, $margin + $usableH - $h);
|
||||||
|
case 'center':
|
||||||
|
return array(($pageW - $w) / 2, $margin + ($usableH - $h) / 2);
|
||||||
|
case 'fit':
|
||||||
|
default:
|
||||||
|
return array(($pageW - $w) / 2, $margin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bericht_render_cover_internal($template_path, $bericht, $parent, $tempdir)
|
function bericht_render_cover_internal($template_path, $bericht, $parent, $tempdir)
|
||||||
{
|
{
|
||||||
global $user, $langs;
|
global $user, $langs;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue