hasRight('bericht', 'write')) bericht_ajax_fail('Permission denied', 403); $berichtid = (int) ($_POST['berichtid'] ?? 0); $bericht = new Bericht($db); if ($bericht->fetch($berichtid) <= 0) bericht_ajax_fail('Bericht nicht gefunden', 404); $parent = bericht_fetch_parent($db, $bericht->element_type, $bericht->fk_element); if (!$parent) bericht_ajax_fail('Parent nicht gefunden', 404); $pages = BerichtPage::fetchAllForBericht($db, $bericht->id); if (empty($pages)) bericht_ajax_fail('Bericht enthält keine Seiten'); // TCPDF + FPDI laden $tcpdf_loaded = false; foreach (array( DOL_DOCUMENT_ROOT.'/includes/tecnickcom/tcpdf/tcpdf.php', DOL_DOCUMENT_ROOT.'/includes/tcpdf/tcpdf.php', ) as $p) { if (file_exists($p)) { require_once $p; $tcpdf_loaded = true; break; } } if (!$tcpdf_loaded) bericht_ajax_fail('TCPDF nicht gefunden'); $fpdi_loaded = false; foreach (array( DOL_DOCUMENT_ROOT.'/includes/setasign/vendor/setasign/fpdi/src/Tcpdf/Fpdi.php', DOL_DOCUMENT_ROOT.'/includes/fpdi/src/Tcpdf/Fpdi.php', ) as $p) { if (file_exists($p)) { require_once $p; $fpdi_loaded = true; break; } } // FPDI ist optional — wenn fehlt, können wir keine bestehenden PDFs einbetten, // aber Bilder + Annotationen funktionieren weiterhin. if ($fpdi_loaded) { $pdf = new \setasign\Fpdi\Tcpdf\Fpdi('P', 'mm', 'A4', true, 'UTF-8', false); } else { $pdf = new TCPDF('P', 'mm', 'A4', 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); // --- Deckblatt aus ODT-Template --- $tempdir = DOL_DATA_ROOT.'/bericht/temp/'.$berichtid; if (!is_dir($tempdir)) dol_mkdir($tempdir); if (!empty($bericht->template_odt)) { $template_path = DOL_DATA_ROOT.'/bericht/templates/'.dol_sanitizeFileName($bericht->template_odt); if (file_exists($template_path)) { $cover_pdf = bericht_render_cover($template_path, $bericht, $parent, $tempdir); if ($cover_pdf && file_exists($cover_pdf) && $fpdi_loaded) { $cover_pages = $pdf->setSourceFile($cover_pdf); for ($cp = 1; $cp <= $cover_pages; $cp++) { $tpl = $pdf->importPage($cp); $size = $pdf->getTemplateSize($tpl); $pdf->AddPage($size['orientation'] ?? 'P', array($size['width'], $size['height'])); $pdf->useTemplate($tpl); } } } } // --- Seiten --- foreach ($pages as $page) { $full = bericht_resolve_data_path($page->source_path); if (!$full || !file_exists($full)) continue; if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) { // Bild als A4-Seite $pdf->AddPage('P', 'A4'); list($iw, $ih) = @getimagesize($full); if ($iw && $ih) { $maxW = 190; $maxH = 277; $ratio = min($maxW / $iw, $maxH / $ih); $w = $iw * $ratio; $h = $ih * $ratio; $x = (210 - $w) / 2; $y = 10; $pdf->Image($full, $x, $y, $w, $h); // Annotationen drauf 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 --- $dir_key = bericht_element_to_dir_key($bericht->element_type); $target_dir = $conf->{$dir_key}->multidir_output[$parent->entity].'/'.dol_sanitizeFileName($parent->ref); if (!is_dir($target_dir)) dol_mkdir($target_dir); $filename = 'Bericht_'.dol_sanitizeFileName($bericht->auftragsnummer ?: $bericht->ref).'_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'.pdf'; $target_path = $target_dir.'/'.$filename; $pdf->Output($target_path, 'F'); if (!file_exists($target_path)) bericht_ajax_fail('PDF wurde nicht erzeugt'); // In ECM registrieren (taucht unter Verknüpfte Dokumente auf) require_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php'; $ecmfile = new EcmFiles($db); $ecmfile->filepath = $dir_key.'/'.dol_sanitizeFileName($parent->ref); $ecmfile->filename = $filename; $ecmfile->fullpath_orig = $target_path; $ecmfile->src_object_type = $dir_key; $ecmfile->src_object_id = $parent->id; $ecmfile->label = md5_file($target_path); @$ecmfile->create($user); // Fehler bei bereits existierendem Eintrag ignorieren // Bericht-Status auf Final $bericht->status = Bericht::STATUS_FINAL; $bericht->final_pdf_path = str_replace(DOL_DATA_ROOT.'/', '', $target_path); $bericht->update($user); bericht_ajax_ok(array( 'filename' => $filename, 'path' => $bericht->final_pdf_path, )); /** * Rendert das ODT-Deckblatt mit Platzhaltern und konvertiert es zu PDF. * Nutzt LibreOffice headless. * * @return string|null Pfad zum erzeugten PDF oder null bei Fehler */ function bericht_render_cover($template_path, $bericht, $parent, $tempdir) { global $conf, $user, $langs; $odt_loader = DOL_DOCUMENT_ROOT.'/includes/odtphp/odf.php'; if (!file_exists($odt_loader)) return null; require_once $odt_loader; try { $odf = new Odf($template_path, array('PATH_TO_TMP' => $tempdir)); $vars = array( 'auftragsnummer' => $bericht->auftragsnummer ?: '', 'angebotsnummer' => $parent->array_options['options_angebotsnummer'] ?? '', 'rechnungsnummer' => $parent->ref ?? '', 'kunde_name' => $parent->thirdparty->name ?? '', 'kunde_adresse' => trim(($parent->thirdparty->address ?? '')."\n".($parent->thirdparty->zip ?? '').' '.($parent->thirdparty->town ?? '')), 'datum' => dol_print_date(dol_now(), 'day'), 'beschreibung' => $parent->array_options['options_beschreibung'] ?? '', 'hinweis' => $parent->array_options['options_hinweis'] ?? '', 'bericht_titel' => $bericht->titel ?? '', 'ersteller' => $user->getFullName($langs ?? null) ?: $user->login, ); foreach ($vars as $k => $v) { try { $odf->setVars($k, $v, true, 'UTF-8'); } catch (Throwable $e) {} } $odt_out = $tempdir.'/cover_'.$bericht->id.'.odt'; $odf->saveToDisk($odt_out); // ODT → PDF via LibreOffice $lobin = getDolGlobalString('BERICHT_LIBREOFFICE_BIN', '/usr/bin/libreoffice'); $cmd = escapeshellcmd($lobin) .' --headless --convert-to pdf --outdir '.escapeshellarg($tempdir).' '.escapeshellarg($odt_out).' 2>&1'; @shell_exec($cmd); $pdf_out = preg_replace('/\.odt$/i', '.pdf', $odt_out); return file_exists($pdf_out) ? $pdf_out : null; } catch (Throwable $e) { return null; } } /** * Rendert Fabric.js-Annotationen ins TCPDF-Objekt. * Versteht die wichtigsten Shape-Typen: rect, circle, line, path (freihand), text. * Koordinaten in fabric_json sind in Pixel relativ zum Bild → werden auf mm skaliert. */ function bericht_burn_annotations(TCPDF $pdf, $fabric_json, $x, $y, $w, $h) { if (empty($fabric_json)) return; $data = json_decode($fabric_json, true); if (!is_array($data) || empty($data['objects'])) return; // Skalierung: Fabric arbeitet in Pixel, TCPDF in mm // Wir nehmen an, der Fabric-Canvas hatte die gleiche Pixelgröße wie das gerenderte Bild, // und das Bild belegt im PDF (x,y,w,h). $cw = $data['width'] ?? null; $ch = $data['height'] ?? null; if (!$cw || !$ch) return; $sx = $w / $cw; $sy = $h / $ch; foreach ($data['objects'] as $obj) { $stroke = isset($obj['stroke']) ? bericht_hex_to_rgb($obj['stroke']) : array(255, 0, 0); $sw = ($obj['strokeWidth'] ?? 2) * $sx; $pdf->SetDrawColor($stroke[0], $stroke[1], $stroke[2]); $pdf->SetLineWidth(max(0.2, $sw)); $type = $obj['type'] ?? ''; $ox = ($obj['left'] ?? 0) * $sx + $x; $oy = ($obj['top'] ?? 0) * $sy + $y; $ow = ($obj['width'] ?? 0) * ($obj['scaleX'] ?? 1) * $sx; $oh = ($obj['height'] ?? 0) * ($obj['scaleY'] ?? 1) * $sy; switch ($type) { case 'rect': $pdf->Rect($ox, $oy, $ow, $oh, 'D'); break; case 'circle': case 'ellipse': $rx = ($obj['rx'] ?? ($ow / 2)) * $sx; $ry = ($obj['ry'] ?? ($oh / 2)) * $sy; $pdf->Ellipse($ox + $rx, $oy + $ry, $rx, $ry, 0, 0, 360, 'D'); break; case 'line': $x1 = ($obj['x1'] ?? 0) * $sx + $x + ($obj['left'] ?? 0) * $sx; $y1 = ($obj['y1'] ?? 0) * $sy + $y + ($obj['top'] ?? 0) * $sy; $x2 = ($obj['x2'] ?? 0) * $sx + $x + ($obj['left'] ?? 0) * $sx; $y2 = ($obj['y2'] ?? 0) * $sy + $y + ($obj['top'] ?? 0) * $sy; $pdf->Line($x1, $y1, $x2, $y2); break; case 'path': // Freihand-Pfade vereinfacht als Polyline rendern if (!empty($obj['path']) && is_array($obj['path'])) { $prev = null; foreach ($obj['path'] as $seg) { if (!is_array($seg) || count($seg) < 3) continue; $cmd = $seg[0]; if ($cmd === 'M') { $prev = array($seg[1] * $sx + $x, $seg[2] * $sy + $y); } elseif (($cmd === 'L' || $cmd === 'Q') && $prev) { $px = $seg[1] * $sx + $x; $py = $seg[2] * $sy + $y; $pdf->Line($prev[0], $prev[1], $px, $py); $prev = array($px, $py); } } } break; case 'i-text': case 'text': case 'textbox': $fontsize = max(6, ($obj['fontSize'] ?? 16) * $sx * 2.83); // px → pt $pdf->SetFont('helvetica', '', $fontsize); $pdf->SetTextColor($stroke[0], $stroke[1], $stroke[2]); $pdf->Text($ox, $oy + $fontsize * 0.35, $obj['text'] ?? ''); break; } } } function bericht_hex_to_rgb($hex) { $hex = ltrim($hex, '#'); if (strlen($hex) === 3) { $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; } if (strlen($hex) !== 6) return array(255, 0, 0); return array(hexdec(substr($hex, 0, 2)), hexdec(substr($hex, 2, 2)), hexdec(substr($hex, 4, 2))); }