* * 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 . */ /** * \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); } }