From d40587845fd9a74167c56f77b33f57522fb316dc Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Thu, 9 Apr 2026 15:39:42 +0200 Subject: [PATCH] PDF-Header mit Logo+Titel, Footer mit Seitenzahl, Hack-Font beschreibbar [deploy] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue Klasse BerichtPdf / BerichtPdfFpdi (Trait-basiert): * Header: links Bericht-Titel (Bold) + Firmenname, rechts Firmen-Logo (max 40x18mm), Trennlinie. Top-Margin jetzt 30mm für den Header-Bereich. * Footer: zentriert "Seite X / Y" mit TCPDF-Aliases. * berichtInit(): kompiliert Hack-TTFs nach DOL_DATA_ROOT/bericht/tcpdf_fonts/ (beschreibbar) und bindet sie per AddFont an die PDF-Instance. Vorher schlug addTTFfont still fehl weil K_PATH_FONTS read-only war — deshalb kam weder Titel noch Notiz in Hack. - bericht_ensure_hack_font($pdf) zieht den Font-Key jetzt aus der Instance (BerichtPdfTrait), sonst Fallback helvetica. - bericht_write_note_html() wrapped das CKEditor-HTML in damit writeHTMLCell den Hack-Font tatsächlich verwendet. - Composite-Branch: $mT=30 / $mB=16 damit Bilder nicht unter dem Header sitzen. - ajax/generate_pdf, ajax/preview_pdf, api/pdf, api/reports, bericht_batch: alle nutzen jetzt BerichtPdf(Fpdi), setzen SetMargins(10,30,10), setPrintHeader(true) und berichtInit() mit Titel, mysoc->name und Logo. --- ajax/generate_pdf.php | 19 ++++--- ajax/preview_pdf.php | 19 ++++--- api/pdf.php | 20 ++++--- api/reports.php | 20 ++++--- bericht_batch.php | 26 ++++++--- class/berichtpdf.class.php | 108 +++++++++++++++++++++++++++++++++++++ lib/bericht.lib.php | 67 ++++++++--------------- 7 files changed, 200 insertions(+), 79 deletions(-) create mode 100644 class/berichtpdf.class.php diff --git a/ajax/generate_pdf.php b/ajax/generate_pdf.php index 5d5829b..57093ce 100644 --- a/ajax/generate_pdf.php +++ b/ajax/generate_pdf.php @@ -43,18 +43,23 @@ foreach (array( $ori = in_array($bericht->page_orientation, array('P','L'), true) ? $bericht->page_orientation : 'P'; $fmt = in_array($bericht->page_format, array('A4','A3','A5','Letter'), true) ? $bericht->page_format : 'A4'; -if ($fpdi_loaded) { - $pdf = new \setasign\Fpdi\Tcpdf\Fpdi($ori, 'mm', $fmt, true, 'UTF-8', false); +require_once __DIR__.'/../class/berichtpdf.class.php'; +if ($fpdi_loaded && class_exists('BerichtPdfFpdi')) { + $pdf = new BerichtPdfFpdi($ori, 'mm', $fmt, true, 'UTF-8', false); } else { - $pdf = new TCPDF($ori, 'mm', $fmt, true, 'UTF-8', false); + $pdf = new BerichtPdf($ori, 'mm', $fmt, true, 'UTF-8', false); } $pdf->SetCreator('Dolibarr Bericht-Modul'); $pdf->SetAuthor($user->getFullName($langs)); $pdf->SetTitle($bericht->titel ?: $bericht->ref); -$pdf->SetMargins(10, 10, 10); -$pdf->SetAutoPageBreak(true, 10); -$pdf->setPrintHeader(false); -$pdf->setPrintFooter(false); +$logo_path = !empty($mysoc->logo) ? $conf->mycompany->dir_output.'/logos/'.$mysoc->logo : ''; +$pdf->berichtInit($bericht->titel ?: $bericht->ref, $mysoc->name ?? '', $logo_path); +$pdf->SetMargins(10, 30, 10); +$pdf->SetAutoPageBreak(true, 16); +$pdf->setPrintHeader(true); +$pdf->setPrintFooter(true); +$pdf->setHeaderMargin(5); +$pdf->setFooterMargin(10); // --- Deckblatt aus ODT-Template --- $tempdir = DOL_DATA_ROOT.'/bericht/temp/'.$berichtid; diff --git a/ajax/preview_pdf.php b/ajax/preview_pdf.php index 0a2ff76..2e1ace6 100644 --- a/ajax/preview_pdf.php +++ b/ajax/preview_pdf.php @@ -53,18 +53,23 @@ foreach (array( $ori = in_array($bericht->page_orientation, array('P','L'), true) ? $bericht->page_orientation : 'P'; $fmt = in_array($bericht->page_format, array('A4','A3','A5','Letter'), true) ? $bericht->page_format : 'A4'; -if ($fpdi_loaded) { - $pdf = new \setasign\Fpdi\Tcpdf\Fpdi($ori, 'mm', $fmt, true, 'UTF-8', false); +require_once __DIR__.'/../class/berichtpdf.class.php'; +if ($fpdi_loaded && class_exists('BerichtPdfFpdi')) { + $pdf = new BerichtPdfFpdi($ori, 'mm', $fmt, true, 'UTF-8', false); } else { - $pdf = new TCPDF($ori, 'mm', $fmt, true, 'UTF-8', false); + $pdf = new BerichtPdf($ori, 'mm', $fmt, true, 'UTF-8', false); } $pdf->SetCreator('Dolibarr Bericht-Modul (Vorschau)'); $pdf->SetAuthor($user->getFullName($langs)); $pdf->SetTitle(($bericht->titel ?: $bericht->ref).' [Vorschau]'); -$pdf->SetMargins(10, 10, 10); -$pdf->SetAutoPageBreak(true, 10); -$pdf->setPrintHeader(false); -$pdf->setPrintFooter(false); +$logo_path = !empty($mysoc->logo) ? $conf->mycompany->dir_output.'/logos/'.$mysoc->logo : ''; +$pdf->berichtInit($bericht->titel ?: $bericht->ref, $mysoc->name ?? '', $logo_path); +$pdf->SetMargins(10, 30, 10); +$pdf->SetAutoPageBreak(true, 16); +$pdf->setPrintHeader(true); +$pdf->setPrintFooter(true); +$pdf->setHeaderMargin(5); +$pdf->setFooterMargin(10); $tempdir = DOL_DATA_ROOT.'/bericht/temp/'.$berichtid; if (!is_dir($tempdir)) dol_mkdir($tempdir); diff --git a/api/pdf.php b/api/pdf.php index 7795b80..c9c3534 100644 --- a/api/pdf.php +++ b/api/pdf.php @@ -97,17 +97,23 @@ foreach (array( $ori = in_array($bericht->page_orientation, array('P','L'), true) ? $bericht->page_orientation : 'P'; $fmt = in_array($bericht->page_format, array('A4','A3','A5','Letter'), true) ? $bericht->page_format : 'A4'; -$pdf = $fpdi_loaded - ? new \setasign\Fpdi\Tcpdf\Fpdi($ori, 'mm', $fmt, true, 'UTF-8', false) - : new TCPDF($ori, 'mm', $fmt, true, 'UTF-8', false); +require_once __DIR__.'/../class/berichtpdf.class.php'; +$pdf = ($fpdi_loaded && class_exists('BerichtPdfFpdi')) + ? new BerichtPdfFpdi($ori, 'mm', $fmt, true, 'UTF-8', false) + : new BerichtPdf($ori, 'mm', $fmt, true, 'UTF-8', false); +global $mysoc, $conf; $pdf->SetCreator('Dolibarr Bericht (PWA Vorschau)'); $pdf->SetAuthor($user->getFullName($langs ?? null)); $pdf->SetTitle($bericht->titel ?: $bericht->ref); -$pdf->SetMargins(10, 10, 10); -$pdf->SetAutoPageBreak(true, 10); -$pdf->setPrintHeader(false); -$pdf->setPrintFooter(false); +$logo_path = !empty($mysoc->logo) ? $conf->mycompany->dir_output.'/logos/'.$mysoc->logo : ''; +$pdf->berichtInit($bericht->titel ?: $bericht->ref, $mysoc->name ?? '', $logo_path); +$pdf->SetMargins(10, 30, 10); +$pdf->SetAutoPageBreak(true, 16); +$pdf->setPrintHeader(true); +$pdf->setPrintFooter(true); +$pdf->setHeaderMargin(5); +$pdf->setFooterMargin(10); foreach ($pages as $page) { bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded); diff --git a/api/reports.php b/api/reports.php index 9592884..87c5c0d 100644 --- a/api/reports.php +++ b/api/reports.php @@ -143,18 +143,24 @@ if ($action === 'finalize') { $ori = in_array($bericht->page_orientation, array('P','L'), true) ? $bericht->page_orientation : 'P'; $fmt = in_array($bericht->page_format, array('A4','A3','A5','Letter'), true) ? $bericht->page_format : 'A4'; - if ($fpdi_loaded) { - $pdf = new \setasign\Fpdi\Tcpdf\Fpdi($ori, 'mm', $fmt, true, 'UTF-8', false); + require_once __DIR__.'/../class/berichtpdf.class.php'; + if ($fpdi_loaded && class_exists('BerichtPdfFpdi')) { + $pdf = new BerichtPdfFpdi($ori, 'mm', $fmt, true, 'UTF-8', false); } else { - $pdf = new TCPDF($ori, 'mm', $fmt, true, 'UTF-8', false); + $pdf = new BerichtPdf($ori, 'mm', $fmt, true, 'UTF-8', false); } + global $mysoc; $pdf->SetCreator('Dolibarr Bericht-Modul (PWA)'); $pdf->SetAuthor($user->getFullName($langs)); $pdf->SetTitle($bericht->titel ?: $bericht->ref); - $pdf->SetMargins(10, 10, 10); - $pdf->SetAutoPageBreak(true, 10); - $pdf->setPrintHeader(false); - $pdf->setPrintFooter(false); + $logo_path = !empty($mysoc->logo) ? $conf->mycompany->dir_output.'/logos/'.$mysoc->logo : ''; + $pdf->berichtInit($bericht->titel ?: $bericht->ref, $mysoc->name ?? '', $logo_path); + $pdf->SetMargins(10, 30, 10); + $pdf->SetAutoPageBreak(true, 16); + $pdf->setPrintHeader(true); + $pdf->setPrintFooter(true); + $pdf->setHeaderMargin(5); + $pdf->setFooterMargin(10); foreach ($pages as $page) { bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded); diff --git a/bericht_batch.php b/bericht_batch.php index 8d1375f..165bf1e 100644 --- a/bericht_batch.php +++ b/bericht_batch.php @@ -51,23 +51,35 @@ if ($action === 'generate') { ) as $p) { if (file_exists($p)) { require_once $p; $fpdi_loaded = true; break; } } if (!$fpdi_loaded) { http_response_code(500); exit('FPDI wird für Batch-Modus benötigt'); } - $pdf = new \setasign\Fpdi\Tcpdf\Fpdi('P', 'mm', 'A4', true, 'UTF-8', false); + require_once __DIR__.'/class/berichtpdf.class.php'; + $pdf = class_exists('BerichtPdfFpdi') + ? new BerichtPdfFpdi('P', 'mm', 'A4', true, 'UTF-8', false) + : new \setasign\Fpdi\Tcpdf\Fpdi('P', 'mm', 'A4', true, 'UTF-8', false); + global $mysoc; $pdf->SetCreator('Dolibarr Bericht-Modul Batch'); $pdf->SetAuthor($user->getFullName($langs)); $pdf->SetTitle('Bericht-Sammlung '.dol_print_date(dol_now(), '%Y-%m-%d')); - $pdf->setPrintHeader(false); - $pdf->setPrintFooter(false); + $logo_path = !empty($mysoc->logo) ? $conf->mycompany->dir_output.'/logos/'.$mysoc->logo : ''; + if (method_exists($pdf, 'berichtInit')) { + $pdf->berichtInit('Bericht-Sammlung', $mysoc->name ?? '', $logo_path); + } + $pdf->SetMargins(10, 30, 10); + $pdf->SetAutoPageBreak(true, 16); + $pdf->setPrintHeader(true); + $pdf->setPrintFooter(true); + $pdf->setHeaderMargin(5); + $pdf->setFooterMargin(10); // Inhaltsverzeichnis-Seite $pdf->AddPage('P', 'A4'); - $pdf->SetFont(bericht_ensure_hack_font(), 'B', 18); + $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 18); $pdf->Cell(0, 12, 'Bericht-Sammlung', 0, 1, 'C'); - $pdf->SetFont(bericht_ensure_hack_font(), '', 11); + $pdf->SetFont(bericht_ensure_hack_font($pdf), '', 11); $pdf->Cell(0, 8, 'Erstellt: '.dol_print_date(dol_now(), 'dayhour'), 0, 1, 'C'); $pdf->Ln(8); - $pdf->SetFont(bericht_ensure_hack_font(), 'B', 13); + $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 13); $pdf->Cell(0, 8, 'Enthaltene Berichte ('.count($ids).')', 0, 1, 'L'); - $pdf->SetFont(bericht_ensure_hack_font(), '', 10); + $pdf->SetFont(bericht_ensure_hack_font($pdf), '', 10); $berichte = array(); foreach ($ids as $id) { diff --git a/class/berichtpdf.class.php b/class/berichtpdf.class.php new file mode 100644 index 0000000..0adca9e --- /dev/null +++ b/class/berichtpdf.class.php @@ -0,0 +1,108 @@ +bericht_title = (string) $title; + $this->bericht_company = (string) $company; + $this->bericht_logo = (string) $logo_path; + + // Hack-Font registrieren (Eddys Corporate-Font) + if (class_exists('TCPDF_FONTS') && defined('DOL_DATA_ROOT')) { + $fontdir = dirname(__DIR__).'/fonts/'; + $outdir = DOL_DATA_ROOT.'/bericht/tcpdf_fonts/'; + if (!is_dir($outdir)) @mkdir($outdir, 0755, true); + if (is_dir($outdir) && is_writable($outdir)) { + $map = array( + '' => 'Hack-Regular.ttf', + 'B' => 'Hack-Bold.ttf', + 'I' => 'Hack-Italic.ttf', + 'BI' => 'Hack-BoldItalic.ttf', + ); + $reg_key = null; + foreach ($map as $style => $fn) { + $src = $fontdir.$fn; + if (!file_exists($src)) continue; + try { + $k = TCPDF_FONTS::addTTFfont($src, 'TrueTypeUnicode', '', 32, $outdir); + if ($k) { + $ffile = $outdir.$k.'.php'; + if (file_exists($ffile)) { + $this->AddFont($k, '', $ffile); + } + if ($style === '') $reg_key = $k; + } + } catch (Throwable $e) { + error_log('[BerichtPdf] Hack-Font '.$fn.' fehlgeschlagen: '.$e->getMessage()); + } + } + if ($reg_key) $this->hack_font_key = $reg_key; + } else { + error_log('[BerichtPdf] Font-Outdir nicht schreibbar: '.$outdir); + } + } + } + + public function Header() + { + $pageW = $this->getPageWidth(); + $logo_w = 0; + if ($this->bericht_logo && file_exists($this->bericht_logo)) { + $info = @getimagesize($this->bericht_logo); + if ($info && $info[0] && $info[1]) { + $maxW = 40; $maxH = 18; + $ratio = min($maxW / $info[0], $maxH / $info[1]); + $w = $info[0] * $ratio; + $h = $info[1] * $ratio; + $this->Image($this->bericht_logo, $pageW - 10 - $w, 6, $w, $h); + $logo_w = $w; + } + } + $this->SetTextColor(40, 40, 40); + $this->SetFont($this->hack_font_key, 'B', 13); + $this->SetXY(10, 8); + $textW = $pageW - 20 - $logo_w - 4; + $this->Cell($textW, 6, $this->bericht_title, 0, 2, 'L'); + if ($this->bericht_company) { + $this->SetFont($this->hack_font_key, '', 9); + $this->SetX(10); + $this->Cell($textW, 5, $this->bericht_company, 0, 2, 'L'); + } + $this->SetDrawColor(180, 180, 180); + $this->SetLineWidth(0.2); + $this->Line(10, 27, $pageW - 10, 27); + } + + public function Footer() + { + $this->SetY(-12); + $this->SetFont($this->hack_font_key, '', 9); + $this->SetTextColor(100, 100, 100); + $txt = 'Seite '.$this->getAliasNumPage().' / '.$this->getAliasNbPages(); + $this->Cell(0, 8, $txt, 0, 0, 'C'); + } +} + +class BerichtPdf extends TCPDF { use BerichtPdfTrait; } + +if (class_exists('\\setasign\\Fpdi\\Tcpdf\\Fpdi')) { + class BerichtPdfFpdi extends \setasign\Fpdi\Tcpdf\Fpdi { use BerichtPdfTrait; } +} diff --git a/lib/bericht.lib.php b/lib/bericht.lib.php index 3415b04..22ae934 100644 --- a/lib/bericht.lib.php +++ b/lib/bericht.lib.php @@ -276,7 +276,7 @@ function bericht_burn_annotations($pdf, $fabric_json, $x, $y, $w, $h) case 'text': case 'textbox': $fontsize = max(6, ($obj['fontSize'] ?? 16) * $sx * 2.83); - $pdf->SetFont(bericht_ensure_hack_font(), '', $fontsize); + $pdf->SetFont(bericht_ensure_hack_font($pdf), '', $fontsize); $pdf->SetTextColor($stroke[0], $stroke[1], $stroke[2]); $pdf->Text($ox, $oy + $fontsize * 0.35, $obj['text'] ?? ''); break; @@ -312,34 +312,13 @@ function bericht_render_cover_for_preview($template_path, $bericht, $parent, $te * * @return string Font-Key für $pdf->SetFont() */ -function bericht_ensure_hack_font() +function bericht_ensure_hack_font($pdf = null) { - static $cached = null; - if ($cached !== null) return $cached; - - if (!class_exists('TCPDF_FONTS')) { - $cached = 'helvetica'; - return $cached; + // Der Font wird in BerichtPdfTrait::berichtInit registriert; hier nur Key zurückgeben. + if ($pdf && isset($pdf->hack_font_key) && $pdf->hack_font_key) { + return $pdf->hack_font_key; } - $dir = dirname(__DIR__).'/fonts'; - $files = array( - '' => $dir.'/Hack-Regular.ttf', - 'B' => $dir.'/Hack-Bold.ttf', - 'I' => $dir.'/Hack-Italic.ttf', - 'BI' => $dir.'/Hack-BoldItalic.ttf', - ); - try { - $key = null; - foreach ($files as $style => $path) { - if (!file_exists($path)) continue; - $k = TCPDF_FONTS::addTTFfont($path, 'TrueTypeUnicode', '', 32); - if ($style === '' && $k) $key = $k; - } - $cached = $key ?: 'helvetica'; - } catch (Throwable $e) { - $cached = 'helvetica'; - } - return $cached; + return 'helvetica'; } /** @@ -349,14 +328,15 @@ function bericht_ensure_hack_font() function bericht_write_note_html($pdf, $html, $x, $y, $w, $h) { if (empty($html)) return; - // CKEditor liefert

— TCPDF kann das direkt - $pdf->SetFont(bericht_ensure_hack_font(), '', 9); + $font = bericht_ensure_hack_font($pdf); + $pdf->SetFont($font, '', 9); $pdf->SetTextColor(40, 40, 40); - // Border 0, kein Auto-Pagebreak damit die Notiz im reservierten Bereich bleibt $autoPB = $pdf->getAutoPageBreak(); $bMargin = $pdf->getBreakMargin(); $pdf->SetAutoPageBreak(false, 0); - $pdf->writeHTMLCell($w, $h, $x, $y, $html, 0, 1, false, true, 'L', true); + // HTML in Font-Wrapper packen, damit TCPDF den Hack-Font für den ganzen Block nutzt + $wrapped = ''.$html.''; + $pdf->writeHTMLCell($w, $h, $x, $y, $wrapped, 0, 1, false, true, 'L', true); $pdf->SetAutoPageBreak($autoPB, $bMargin); } @@ -369,27 +349,26 @@ function bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded) $pdf->AddPage($ori, $fmt); $pageW = $pdf->getPageWidth(); $pageH = $pdf->getPageHeight(); - $margin = 10; + // Margins: oben Platz für Header (Logo/Titel/Firmenname), unten für Footer + $mL = 10; $mR = 10; $mT = 30; $mB = 16; // Notiz-Bereich reservieren wenn eine Notiz existiert $note_h = 0; if (!empty($page->note)) { - // ca. 1/4 Seite für HTML-Notizen, aber mindestens 20mm, max 80mm $note_h = max(20, min(80, $pageH * 0.25)); } - $maxW = $pageW - 2 * $margin; - $maxH = $pageH - 2 * $margin - $note_h - ($note_h ? 4 : 0); + $maxW = $pageW - $mL - $mR; + $maxH = $pageH - $mT - $mB - $note_h - ($note_h ? 4 : 0); list($iw, $ih) = @getimagesize($full); if ($iw && $ih) { $ratio = min($maxW / $iw, $maxH / $ih); $w = $iw * $ratio; $h = $ih * $ratio; $x = ($pageW - $w) / 2; - $y = $margin; + $y = $mT; $pdf->Image($full, $x, $y, $w, $h); } if (!empty($page->note)) { - // Notiz direkt unter dem Bild rendern — kein SetY(-20), keine auto-Page-Break - $note_y = $margin + ($ih && $iw ? ($ih * min($maxW / $iw, $maxH / $ih)) : $maxH) + 4; - bericht_write_note_html($pdf, $page->note, $margin, $note_y, $maxW, $note_h); + $note_y = $mT + ($ih && $iw ? ($ih * min($maxW / $iw, $maxH / $ih)) : $maxH) + 4; + bericht_write_note_html($pdf, $page->note, $mL, $note_y, $maxW, $note_h); } return; } @@ -402,13 +381,13 @@ function bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded) $pdf->AddPage($ori, $fmt); $pageW = $pdf->getPageWidth(); $pageH = $pdf->getPageHeight(); - $pdf->SetFont(bericht_ensure_hack_font(), 'B', 32); + $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 32); $pdf->SetTextColor(40, 40, 40); $pdf->SetY($pageH / 2 - 20); $pdf->MultiCell(0, 20, $page->title, 0, 'C'); if (!empty($page->note)) { $pdf->Ln(12); - $pdf->SetFont(bericht_ensure_hack_font(), '', 12); + $pdf->SetFont(bericht_ensure_hack_font($pdf), '', 12); $pdf->MultiCell(0, 8, $page->note, 0, 'C'); } return; @@ -424,7 +403,7 @@ function bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded) $margin = 10; $title_h = !empty($page->title) ? 12 : 0; if ($title_h) { - $pdf->SetFont(bericht_ensure_hack_font(), 'B', 16); + $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 16); $pdf->SetY($margin); $pdf->MultiCell(0, $title_h, $page->title, 0, 'C'); } @@ -445,7 +424,7 @@ function bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded) $x0 = $margin + $slot * ($w_each + $gap); // Label - $pdf->SetFont(bericht_ensure_hack_font(), 'B', 11); + $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 11); $pdf->SetXY($x0, $top); $pdf->Cell($w_each, $label_h, $label, 0, 0, 'C'); // Bild darunter @@ -474,7 +453,7 @@ function bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded) // Titel oben $title_h = 0; if (!empty($page->title)) { - $pdf->SetFont(bericht_ensure_hack_font(), 'B', 16); + $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 16); $pdf->SetY(10); $pdf->MultiCell(0, 10, $page->title, 0, 'C'); $title_h = 14;