dolibarr.exportzugferd/class/pdfembedder.class.php
data 6e33cc7096 Version 2.0 - ZUGFeRD PDF-Einbettung
- ZUGFeRD/Factur-X XML-Generierung (EN16931)
- XRechnung 3.0 Unterstützung
- PDF-Einbettung (echtes ZUGFeRD-PDF)
- Option XML nach Einbettung zu löschen
- ODT-Template Unterstützung
- E-Mail Anhang Funktion
- XML-Vorschau

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 15:36:15 +01:00

327 lines
9.8 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* \file class/pdfembedder.class.php
* \ingroup exportzugferd
* \brief Embeds ZUGFeRD/Factur-X XML into PDF
*/
/**
* Class ZugferdPdfEmbedder
* Embeds ZUGFeRD XML into existing PDF files to create PDF/A-3 compliant ZUGFeRD PDFs
*/
class ZugferdPdfEmbedder
{
/**
* @var string Last error message
*/
public $error = '';
/**
* @var string[] Error messages
*/
public $errors = array();
/**
* Embed XML file into existing PDF
*
* @param string $pdfFile Path to the PDF file
* @param string $xmlFile Path to the XML file to embed
* @param string $profile ZUGFeRD profile (MINIMUM, BASIC, EN16931, XRECHNUNG, EXTENDED)
* @return bool True on success, false on error
*/
public function embedXmlInPdf($pdfFile, $xmlFile, $profile = 'EN16931')
{
global $conf;
dol_syslog("ZugferdPdfEmbedder::embedXmlInPdf called - PDF: $pdfFile, XML: $xmlFile", LOG_INFO);
if (!file_exists($pdfFile)) {
$this->error = 'PDF file not found: ' . $pdfFile;
dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR);
return false;
}
if (!file_exists($xmlFile)) {
$this->error = 'XML file not found: ' . $xmlFile;
dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR);
return false;
}
// Load TCPDF first (required by TCPDI)
if (!class_exists('TCPDF')) {
$tcpdfPath = DOL_DOCUMENT_ROOT . '/includes/tecnickcom/tcpdf/tcpdf.php';
if (file_exists($tcpdfPath)) {
require_once $tcpdfPath;
} else {
$this->error = 'TCPDF library not found';
dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR);
return false;
}
}
// Check if TCPDI is available
if (!class_exists('TCPDI')) {
$tcpdiPath = DOL_DOCUMENT_ROOT . '/includes/tcpdi/tcpdi.php';
if (file_exists($tcpdiPath)) {
require_once $tcpdiPath;
} else {
$this->error = 'TCPDI library not found';
dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR);
return false;
}
}
try {
// Read XML content
$xmlContent = file_get_contents($xmlFile);
if ($xmlContent === false) {
$this->error = 'Could not read XML file';
dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR);
return false;
}
dol_syslog("ZugferdPdfEmbedder: Creating TCPDI object", LOG_DEBUG);
// Create new PDF with embedded XML
$pdf = new TCPDI('P', 'mm', 'A4', true, 'UTF-8');
dol_syslog("ZugferdPdfEmbedder: TCPDI object created", LOG_DEBUG);
// Disable header and footer
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
// Set PDF/A-3 mode for ZUGFeRD compliance (only if method exists - TCPDI may not have it)
if (method_exists($pdf, 'setPDFA')) {
$pdf->setPDFA(true);
}
// Set document information
$pdf->SetCreator('Dolibarr ExportZugferd');
$pdf->SetAuthor('Dolibarr ERP/CRM');
$pdf->SetTitle('ZUGFeRD Invoice');
$pdf->SetSubject('Electronic Invoice with embedded ZUGFeRD XML');
dol_syslog("ZugferdPdfEmbedder: Setting source file: $pdfFile", LOG_DEBUG);
// Import existing PDF pages
$pageCount = $pdf->setSourceFile($pdfFile);
dol_syslog("ZugferdPdfEmbedder: Importing $pageCount pages", LOG_DEBUG);
for ($pageNo = 1; $pageNo <= $pageCount; $pageNo++) {
dol_syslog("ZugferdPdfEmbedder: Importing page $pageNo", LOG_DEBUG);
$templateId = $pdf->importPage($pageNo);
$size = $pdf->getTemplateSize($templateId);
// TCPDI returns 'w' and 'h' keys, not 'width' and 'height'
$width = isset($size['w']) ? $size['w'] : (isset($size['width']) ? $size['width'] : 210);
$height = isset($size['h']) ? $size['h'] : (isset($size['height']) ? $size['height'] : 297);
$orientation = isset($size['orientation']) ? $size['orientation'] : ($width > $height ? 'L' : 'P');
dol_syslog("ZugferdPdfEmbedder: Page $pageNo size: {$width}x{$height}, orientation: $orientation", LOG_DEBUG);
// Add page with same size as original
$pdf->AddPage($orientation, array($width, $height));
$pdf->useTemplate($templateId);
}
dol_syslog("ZugferdPdfEmbedder: All pages imported", LOG_DEBUG);
// Determine the attachment filename based on profile
$attachmentName = $this->getAttachmentFilename($profile);
dol_syslog("ZugferdPdfEmbedder: Adding embedded file for $attachmentName", LOG_DEBUG);
// Embed XML using the correct ZUGFeRD method: only in EmbeddedFiles name tree
// NOT as a FileAttachment annotation (which causes double listing)
$this->addEmbeddedFile($pdf, $xmlFile, $attachmentName);
dol_syslog("ZugferdPdfEmbedder: Embedded file added", LOG_DEBUG);
// Create temporary file for output
$tempFile = $pdfFile . '.tmp';
// Output the modified PDF
$pdf->Output($tempFile, 'F');
dol_syslog("ZugferdPdfEmbedder: Temp file created: $tempFile", LOG_DEBUG);
// Replace original with modified version
if (file_exists($tempFile)) {
if (filesize($tempFile) > 0) {
// Replace original
if (!rename($tempFile, $pdfFile)) {
$this->error = 'Could not replace original PDF file';
@unlink($tempFile);
dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR);
return false;
}
dol_syslog("ZugferdPdfEmbedder: Successfully embedded XML into PDF: " . $pdfFile, LOG_INFO);
return true;
} else {
$this->error = 'Generated PDF is empty';
@unlink($tempFile);
dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR);
return false;
}
} else {
$this->error = 'Failed to create temporary PDF file';
dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR);
return false;
}
} catch (Exception $e) {
$this->error = 'PDF embedding failed: ' . $e->getMessage();
dol_syslog("ZugferdPdfEmbedder error: " . $e->getMessage(), LOG_ERR);
return false;
}
}
/**
* Get the standard attachment filename for the profile
*
* @param string $profile ZUGFeRD profile
* @return string Filename
*/
private function getAttachmentFilename($profile)
{
switch (strtoupper($profile)) {
case 'XRECHNUNG':
return 'xrechnung.xml';
case 'MINIMUM':
case 'BASIC':
case 'BASIC WL':
case 'EN16931':
case 'EXTENDED':
default:
return 'factur-x.xml';
}
}
/**
* Embed XML content directly into PDF (alternative method)
*
* @param string $pdfFile Path to the PDF file
* @param string $xmlContent XML content to embed
* @param string $xmlFilename Filename for the embedded XML (e.g., factur-x.xml)
* @param string $profile ZUGFeRD profile
* @return bool True on success, false on error
*/
public function embedXmlContentInPdf($pdfFile, $xmlContent, $xmlFilename = 'factur-x.xml', $profile = 'EN16931')
{
// Create temporary XML file
$tempXmlFile = sys_get_temp_dir() . '/' . $xmlFilename;
if (file_put_contents($tempXmlFile, $xmlContent) === false) {
$this->error = 'Could not create temporary XML file';
return false;
}
$result = $this->embedXmlInPdf($pdfFile, $tempXmlFile, $profile);
// Clean up temporary file
@unlink($tempXmlFile);
return $result;
}
/**
* Check if embedding is possible
*
* @return bool True if libraries are available
*/
public static function isEmbeddingAvailable()
{
// Check for TCPDF
$tcpdfAvailable = class_exists('TCPDF') || file_exists(DOL_DOCUMENT_ROOT . '/includes/tecnickcom/tcpdf/tcpdf.php');
// Check for TCPDI
$tcpdiAvailable = class_exists('TCPDI') || file_exists(DOL_DOCUMENT_ROOT . '/includes/tcpdi/tcpdi.php');
return $tcpdfAvailable && $tcpdiAvailable;
}
/**
* Get ZUGFeRD conformance level string for XMP metadata
*
* @param string $profile Profile name
* @return string Conformance level
*/
private function getConformanceLevel($profile)
{
switch (strtoupper($profile)) {
case 'MINIMUM':
return 'MINIMUM';
case 'BASIC WL':
case 'BASICWL':
return 'BASIC WL';
case 'BASIC':
return 'BASIC';
case 'EN16931':
return 'EN 16931';
case 'EXTENDED':
return 'EXTENDED';
case 'XRECHNUNG':
return 'EN 16931';
default:
return 'EN 16931';
}
}
/**
* Add embedded file to PDF without creating a FileAttachment annotation
* This ensures the file is only listed once in the EmbeddedFiles name tree
*
* @param TCPDI $pdf PDF object
* @param string $filepath Path to the file to embed
* @param string $filename Name for the embedded file
* @return void
*/
private function addEmbeddedFile($pdf, $filepath, $filename)
{
// Access TCPDF's internal embeddedfiles array directly
// This avoids creating a FileAttachment annotation which causes double listing
$reflection = new ReflectionClass($pdf);
// Get the 'n' property (object counter)
$nProp = $reflection->getProperty('n');
$nProp->setAccessible(true);
$n = $nProp->getValue($pdf);
// Get the embeddedfiles array
$efProp = $reflection->getProperty('embeddedfiles');
$efProp->setAccessible(true);
$embeddedfiles = $efProp->getValue($pdf);
// Add the file to the embeddedfiles array
$embeddedfiles[$filename] = array(
'f' => ++$n,
'n' => ++$n,
'file' => $filepath
);
// Update the object counter
$nProp->setValue($pdf, $n);
// Update the embeddedfiles array
$efProp->setValue($pdf, $embeddedfiles);
dol_syslog("ZugferdPdfEmbedder: Added $filename to embeddedfiles array", LOG_DEBUG);
}
}