Add PDF generation module with HTML2PDF integration.

This commit introduces a new PDF generation module leveraging the HTML2PDF library. The module generates PDFs from HTML templates, supports customization through styles and templates, and integrates with Minio for storage and retrieval. A test route is added for generating and serving PDFs dynamically.
This commit is contained in:
Jepp9350
2025-03-14 14:56:55 +01:00
parent b95215677a
commit 45328a6b72
17 changed files with 1176 additions and 0 deletions
@@ -0,0 +1,23 @@
# HTML2PDF MODULE
### This module is used to convert HTML to PDF using the html2pdf library.
#### Repository: https://github.com/spipu/html2pdf/
### What does it do?
- Converts HTML to PDF using the html2pdf library.
- Automatically stores the generated PDF in the specified directory. (temporary directory)
- Sends the generated PDF to the minio / s3 bucket.
- Downloads the generated PDF to the local container, when the user requests it. (This is to ensure that the user can
download the PDF even if the file is not stored in the local machine)
- Sends the generated PDF to the user as a response to the request.
### Requirements
- PHP 7.2 or higher
- Composer
- HTML2PDF library
- TCPDF library
- The `./tmp` directory must be writable by the web server user
- The `./tmp` directory must be writable by the user running the PHP script
@@ -0,0 +1,247 @@
<?php
namespace html2pdf\helpers;
require_once WD . '/modules/html2pdf/helpers/html2pdf_template_i.php';
require_once WD . '/modules/html2pdf/helpers/html2pdf_template_t.php';
require_once WD . '/modules/html2pdf/helpers/html2pdf_template_styles.php';
abstract class html2pdf_template extends html2pdf_template_styles implements html2pdf_template_i
{
use html2pdf_template_t;
protected array $data = [];
protected array $company = [
'name' => 'Truck Wash',
'address' => '123 Truck Wash Lane',
'zip' => 12345,
'city' => 'Truck City',
// The prefix for the phone number (e.g. 45 for +45)
'phone_prefix' => 45,
// The phone number without the prefix (e.g. 12345678)
'phone' => 12345678,
'email' => 'email@example.com',
'website' => 'www.truckwash.com',
'images' => [
'logo' => 'path/to/logo.png',
'banner' => 'path/to/banner.png',
'signature' => 'path/to/signature.png',
],
];
public function __construct($data = null)
{
// Constructor code here
if ($data) {
$this->data = $data;
}
}
/**
* Add data to the template
* @param array $data Data to add
* @return self
*/
public function addData(array $data): self
{
$this->data = array_merge($this->data, $data);
return $this;
}
/**
* Set the company data
* @param array $company Company data
* @notation The company data is an associative array with the following keys:
* - name: The name of the company
* - address: The address of the company
* - zip: The zip code of the company
* - city: The city of the company
* - phone_prefix: The prefix for the phone number (e.g. 45 for +45)
* - phone: The phone number without the prefix (e.g. 12345678)
* - email: The email address of the company
* - website: The website of the company
* - images: An associative array with the following keys:
* - logo: The path to the logo image (e.g. path/to/logo.png)
* - banner: The path to the banner image (e.g. path/to/banner.png)
* - signature: The path to the signature image (e.g. path/to/signature.png)
* @return self
*/
public function setCompany(array $company): self
{
$this->company = array_merge($this->company, $company);
return $this;
}
/**
* Get the HTML content for the template
* @return string HTML content
*/
abstract public function getHtml(): string;
/**
* Set a key-value pair in the template data
* @param string $key Key
* @return string|null Value, or null if not set
*/
public function getKey(string $key): string|null
{
return $this->data[$key] ?? null;
}
/**
* Get the company logo image path
* @return string Company logo image path (e.g. path/to/logo.png)
*/
public function getCompanyLogo(): string
{
return $this->company['images']['logo'];
}
/**
* Get the issuer company details and logo for the template
* @return string HTML header
*/
public function getIssuerCompany(): string
{
return '
<table>
<tr>
<td style="width: 50%; text-align: left;">
<h1>' . self::getCompanyName() . '</h1>
<p>' . self::getCompanyAddress() . '</p>
<p>' . self::getCompanyZip() . ' ' . self::getCompanyCity() . '</p>
<p>Phone: +' . self::getCompanyPhoneCountryCode() . ' ' . self::getCompanyPhone() . '</p>
<p>Email: ' . self::getCompanyEmail() . '</p>
<p>Website: ' . self::getCompanyWebsite() . '</p>
</td>
<td style="width: 50%; text-align: right;">
' . $this->addImage($this->pathSrc(self::getCompanyBanner()), 'Company Logo', 50, 250, 'right') . '
</td>
</tr>
</table>
';
}
/**
* Get the company name
* @return string Company name
*/
public function getCompanyName(): string
{
return $this->company['name'];
}
/**
* Get the company address
* @return string Company address
*/
public function getCompanyAddress(): string
{
return $this->company['address'];
}
/**
* Get the company zip code
* @return int Company zip code
*/
public function getCompanyZip(): int
{
return $this->company['zip'];
}
/**
* Get the company city
* @return string Company city
*/
public function getCompanyCity(): string
{
return $this->company['city'];
}
/**
* Get the company phone country code
* @return int Company phone country code (e.g. 45 for +45)
*/
public function getCompanyPhoneCountryCode(): int
{
return $this->company['phone_prefix'];
}
/**
* Get the company phone number
* @return int Company phone number
*/
public function getCompanyPhone(): int
{
return $this->company['phone'];
}
/**
* Get the company email
* @return string Company email
*/
public function getCompanyEmail(): string
{
return $this->company['email'];
}
/**
* Get the company website
* @return string Company website
*/
public function getCompanyWebsite(): string
{
return $this->company['website'];
}
/**
* Get the company banner image path
* @return string Company banner image path (e.g. path/to/banner.png)
*/
public function getCompanyBanner(): string
{
return $this->company['images']['banner'];
}
/**
* Get the signature for the template
* @return string HTML signature
*/
public function getSignature(): string
{
return '
<div style="text-align: center; margin-top: 20px;">
' . $this->addImage($this->pathSrc(self::getCompanySignature()), 'Signature', null, 200, 'center') . '
</div>
';
}
/**
* Get the company signature image path
* @return string Company signature image path (e.g. path/to/signature.png)
*/
public function getCompanySignature(): string
{
return $this->company['images']['signature'];
}
/**
* Get the header for the template
* @param string|null $title Title
* @return string HTML header
*/
public function getHeader(string $title = null): string
{
return '
<table>
<tr>
<td style="width: 50%; text-align: left; padding: 10px; max-height: 50px;">
' . $this->addImage($this->pathSrc(self::getCompanyBanner()), self::getCompanyName() . ' Logo', 50, 250, 'left') . '
</td>
<td style="width: 50%; text-align: right; max-height: 50px;">
' . self::getHeaderTitle($title) . '
</td>
</tr>
</table>
';
}
}
@@ -0,0 +1,26 @@
<?php
namespace html2pdf\helpers;
interface html2pdf_template_i
{
/**
* Add an image to the template
* @param string $imagePath
* @param string $altText
* @param int $height
* @param int $width
* @param string $align
*
* @return string HTML string for the image
*/
public function addImage(string $imagePath, string $altText = '', int $height = 100, int $width = 100, string $align = 'center'): string;
/**
* Get a src path for the template
* @notation This method is used to get the path of e.g. images (e.g. truckwash-banner-white-compressed.png)
* @param string $path The path to the file, relative to the modules src directory.
* @return string The full path to the file. (e.g. /var/www/html/modules/html2pdf/src/truckwash-banner-white-compressed.png)
*/
public function pathSrc(string $path): string;
}
@@ -0,0 +1,106 @@
<?php
namespace html2pdf\helpers;
class html2pdf_template_styles
{
/**
* @var array Styles for different elements
*/
private array $styles = [
'title' => 'font-size: 48px; font-weight: bold; color: #000; text-align: center;',
'subtitle' => 'font-size: 24px; font-weight: normal; color: #555; text-align: center;',
'header' => 'font-size: 36px; font-weight: bold; color: #17375e;',
'footer' => 'font-size: 12px; font-weight: normal; color: #777;',
// Add more styles as needed
'default' => 'font-size: 12px; font-weight: normal; color: #000;',
'content' => 'padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem;',
];
/**
* Set the style for an element
* @param string $element Element name / identifier (E.g. 'title', 'subtitle', 'header', etc.)
* @param string $style CSS styles
*/
public function setStyle(string $element, string $style): void
{
$this->styles[$element] = $style;
}
/**
* Get the title for the template
* @param string|null $title Title
* @param string|null $subtitle Subtitle
* @return string HTML title
*/
public function getTitle(string $title = null, string $subtitle = null): string
{
$html = '';
if (!empty($title)) {
$title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
$html .= '<h1 style="' . self::getStyle('title') . '">' . $title . '</h1>';
}
if (!empty($subtitle)) {
$subtitle = htmlspecialchars($subtitle, ENT_QUOTES, 'UTF-8');
$html .= '<h2 style="' . self::getStyle('subtitle') . '">' . $subtitle . '</h2>';
}
return $html;
}
/**
* Get the style for an element
* @param string $element Element name / identifier (E.g. 'title', 'subtitle', 'header', etc.)
* @return string CSS styles
*/
public function getStyle(string $element): string
{
return $this->styles[$element] ?? $this->styles['default'];
}
/**
* Get the header title for the template
* @param string|null $title Title
* @return string HTML header title
*/
public function getHeaderTitle(string $title = null): string
{
$html = '';
if (!empty($title)) {
$title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
$html .= '<span style="' . self::getStyle('header') . '">' . $title . '</span>';
}
return $html;
}
/**
* Get the CSS (default) styles for the template
* @return string CSS styles
*/
public function getDefaultStyles(): string
{
return '
<style>
body {
font-family: Arial, sans-serif;
font-size: 12px;
color: #333;
}
h1 {
font-size: 24px;
margin-bottom: 10px;
}
p {
margin: 5px 0;
}
table {
width: 100%;
border-collapse: collapse;
}
td {
padding: 10px;
vertical-align: top;
}
</style>
';
}
}
@@ -0,0 +1,38 @@
<?php
namespace html2pdf\helpers;
trait html2pdf_template_t
{
/**
* @inheritDoc
*/
public function addImage(string $imagePath, string $altText = '', int|null $height = 100, int|null $width = 100, string $align = 'center'): string
{
// Validate the image path (Local, then URL)
if (!file_exists($imagePath)) {
if (filter_var($imagePath, FILTER_VALIDATE_URL) === false) {
throw new \InvalidArgumentException("Invalid image path: $imagePath");
}
}
// Validate the height and width
if (empty($height)) {
$height = 'auto';
}
if (empty($width)) {
$width = 'auto';
}
// Generate the HTML for the image
$style = "height: {$height}px; width: {$width}px; text-align: {$align};";
return "<img src='{$imagePath}' alt='{$altText}' style='max-height: {$height}px; max-width: {$width}px; {$style}' />";
}
public function pathSrc(string $path): string
{
// Assuming the path is relative to the module directory
return WD . '/modules/html2pdf/src/' . $path;
}
}
@@ -0,0 +1,41 @@
<?php
namespace html2pdf;
require_once WD . '/modules/html2pdf/html2pdf_templates_i.php';
require_once WD . '/modules/html2pdf/helpers/html2pdf_template.php';
/** Template classes */
require_once WD . '/modules/html2pdf/templates/wash_certificate.php';
use Exception;
use html2pdf\helpers\html2pdf_template;
class html2pdf_templates implements html2pdf_templates_i
{
/**
* Get the template with the given name, and optionally pass data to it
* @param string $templateName Template name
* @param array|null $data Data to pass to the template
* @return html2pdf_template Template object
* @throws Exception If the template class does not exist
*/
public static function getTemplate(string $templateName, array $data = null): html2pdf_template
{
$className = self::getTemplateClass($templateName);
if (!class_exists($className)) {
throw new Exception('Template class does not exist: ' . $className);
}
return new $className($data);
}
/**
* Get the template class name based on the template name
* @param string $templateName Template name
* @return string Class name of the template
*/
public static function getTemplateClass(string $templateName): string
{
return 'html2pdf\\templates\\' . $templateName;
}
}
@@ -0,0 +1,8 @@
<?php
namespace html2pdf;
interface html2pdf_templates_i
{
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -0,0 +1,179 @@
<?php
namespace html2pdf\templates;
use html2pdf\helpers\html2pdf_template;
class wash_certificate extends html2pdf_template
{
/**
* @inheritDoc
*/
public function getHtml($data = null): string
{
// self::addImage(self::pathSrc('truckwash-banner-white-compressed.png'), 'Certificate Image', 100, 100, 'center');
/** Add the styles */
$html = self::getDefaultStyles();
/** Add the header */
$html .= self::getHeader();
/** Add the issuer company */
//$html .= self::getIssuerCompany();
/** Open the content div */
$html .= '<div style="' . self::getStyle('content') . '">';
/** Add the title */
$html .= self::getTitle('Wash Certificate');
/** Add the seal number */
$html .= self::getSealNumber();
/** Add the registration numbers, time, date and the carried out by */
$html .= self::getDetails();
/** Add the provider details */
$html .= self::getProviderDetails();
/** Add the regarding this document */
$html .= self::getRegardingThisDocument('DK');
/** Add the regarding this document in English */
$html .= self::getRegardingThisDocument('EN');
/** Add the signature */
$html .= self::getSignature();
/** Close the content div */
$html .= '</div>';
return $html;
}
/**
* Get the seal number for the wash certificate
* @notation This method is used to generate the seal number for the wash certificate.
* @return string HTML table
* @see addData() - This method is used to add data to the template.
*/
public function getSealNumber(): string
{
return '
<table style="width: 80%; border-collapse: collapse; margin-top: 20px; margin-left: 10%; margin-right: 10%; align-content: space-between;">
<tr>
<!-- Seal number -->
<td style="width: 100%; padding: 10px; align-content: center; text-align: center">
<strong>SEAL/PLOM number</strong> <br>' . htmlspecialchars($this->getKey('seal_number'), ENT_QUOTES, 'UTF-8') . '
</td>
</tr>
</table>
';
}
/**
* Get the HTML details for the wash certificate
* @notation This method is used to generate the details of the wash certificate. (E.g. registration numbers, time, date and the carried out by)
* @return string HTML table
* @see addData() - This method is used to add data to the template.
*/
public function getDetails(): string
{
return '
<table style="width: 80%; border-collapse: collapse; margin-left: 10%; margin-right: 10%; align-content: space-between;">
<tr>
<!-- Customer name -->
<td style="width: 33%; padding: 10px; align-content: center; text-align: center">
<strong>Company name</strong> <br>' . htmlspecialchars($this->getKey('customer_name'), ENT_QUOTES, 'UTF-8') . '
</td>
<!-- Reg number 1 -->
<td style="width: 33%; padding: 10px; align-content: center; text-align: center">
<strong>Reg.no.</strong> <br>' . htmlspecialchars($this->getKey('reg_1'), ENT_QUOTES, 'UTF-8') . '
</td>
<!-- Date -->
<td style="width: 33%; padding: 10px; align-content: center; text-align: center">
<strong>Date</strong> <br>' . htmlspecialchars($this->getKey('time'), ENT_QUOTES, 'UTF-8') . ' ' . htmlspecialchars($this->getKey('date'), ENT_QUOTES, 'UTF-8') . '
</td>
</tr>
<tr>
<!-- Type -->
<td style="width: 33%; padding: 10px; align-content: center; text-align: center">
<strong>Type</strong> <br>' . htmlspecialchars($this->getKey('type'), ENT_QUOTES, 'UTF-8') . '
</td>
<!-- Reg number 2 -->
<td style="width: 33%; padding: 10px; align-content: center; text-align: center">
<strong>Trailerno.</strong> <br>' . htmlspecialchars($this->getKey('reg_2'), ENT_QUOTES, 'UTF-8') . '
</td>
<!-- Time -->
<td style="width: 33%; padding: 10px; align-content: center; text-align: center">
<strong>Time</strong> <br>' . htmlspecialchars($this->getKey('time'), ENT_QUOTES, 'UTF-8') . '
</td>
</tr>
</table>
';
}
/**
* Get the HTML provider details for the wash certificate
* @notation This method is used to generate the provider details of the wash certificate. (E.g. department, company name, address, zip, city, phone prefix, phone, email and website)
* @return string HTML table
* @see addData() - This method is used to add data to the template.
*/
public function getProviderDetails(): string
{
return '
<table style="width: 80%; border-collapse: collapse; margin-left: 10%; margin-right: 10%; align-content: space-evenly;">
<tr>
<!-- Service provider -->
<td style="width: 33%; padding: 10px; align-content: center; text-align: center">
<strong>Service Provider</strong>
<br>' . self::getCompanyName() . '
<br>' . self::getCompanyAddress() . '
<br>' . self::getCompanyZip() . ' ' . self::getCompanyCity() . '
</td>
<!-- Contact details -->
<td style="width: 33%; padding: 10px; align-content: center; text-align: center">
<strong>Contact Details</strong>
<br>+' . self::getCompanyPhoneCountryCode() . ' ' . self::getCompanyPhone() . '
<br>' . self::getCompanyEmail() . '
</td>
<!-- Operator -->
<td style="width: 33%; padding: 10px; align-content: center; text-align: center">
<strong>Carried Out By</strong>
<br>' . htmlspecialchars($this->getKey('carried_out_by'), ENT_QUOTES, 'UTF-8') . '
</td>
</tr>
</table>
';
}
/**
* Get the regarding this document
* @notation This method is used to generate the regarding this document.
* @param string $language Language code (e.g. 'DK', 'EN', etc.)
* @return string HTML table
*/
public function getRegardingThisDocument(string $language = 'DK'): string
{
$translations = [
'DK' => [
'Angående dette dokument',
'Der kvitteres herved for udførelse af indvendig vask af denne trailer. Vasken er udført med en godkendt sæbe til formålet samt afskyllet med vand bagefter.',
'Der er udført egenkontrol af vasken, Herved bekræftes det at vasken er udført optimalt.',
'Datablade kan rekvireres hos Copenhagen Truck Wash.',
'Bakterielle prøver bør tages umiddelbart efter vask, dog max 3 timer efter. Prøven skal tages på et tørt sted og ikke på gulvet',
],
'EN' => [
'Regarding this document',
'This is hereby confirmed for the performance of internal washing of this trailer. The wash has been performed with an approved soap for the purpose - and rinsed with water afterwards.',
'Self-control of the wash has been performed, hereby confirming that the wash has been performed optimally.',
'Data sheets can be requested from Copenhagen Truck Wash.',
'Bacterial samples should be taken immediately after washing, but no later than 3 hours after. The sample should be taken in a dry place and not on the floor.',
],
];
return '
<table style="width: 80%; border-collapse: collapse; margin-top: 20px; margin-left: 10%; margin-right: 10%; align-content: space-between;">
<tr>
<!-- Regarding this document -->
<td style="width: 100%; padding: 10px; align-content: center; text-align: left">
<strong>' . htmlspecialchars($translations[$language][0], ENT_QUOTES, 'UTF-8') . '</strong>
<br>' . htmlspecialchars($translations[$language][1], ENT_QUOTES, 'UTF-8') . '
<br>' . htmlspecialchars($translations[$language][2], ENT_QUOTES, 'UTF-8') . '
<br>' . htmlspecialchars($translations[$language][3], ENT_QUOTES, 'UTF-8') . '
<br>' . htmlspecialchars($translations[$language][4], ENT_QUOTES, 'UTF-8') . '
</td>
</tr>
</table>
';
}
}