dompdf engine

This commit is contained in:
Yuri Kuznetsov
2023-01-15 16:57:45 +02:00
parent c5ae2800b5
commit 6339690e58
22 changed files with 1175 additions and 65 deletions
@@ -51,8 +51,10 @@ class TableTag implements Helper
$content = $function !== null ? $function() : '';
$style = "border: {$border}; border-spacing: 0; border-collapse: collapse;";
return Result::createSafeString(
"<table border=\"{$border}\" cellpadding=\"{$cellpadding}\" {$attributesPart}>" .
"<table style=\"{$style}\" border=\"{$border}\" cellpadding=\"{$cellpadding}\" {$attributesPart}>" .
$content .
"</table>"
);
@@ -30,6 +30,7 @@
namespace Espo\Modules\Crm\Tools\Campaign;
use Espo\Core\Exceptions\Error;
use Espo\Core\Field\LinkParent;
use Espo\Core\FileStorage\Manager as FileStorageManager;
use Espo\Core\Record\ServiceContainer;
use Espo\Core\Utils\Config;
@@ -45,6 +46,7 @@ use Espo\Tools\Pdf\Data\DataLoaderManager;
use Espo\Tools\Pdf\IdDataMap;
use Espo\Tools\Pdf\Params;
use Espo\Tools\Pdf\TemplateWrapper;
use Espo\Tools\Pdf\ZipContents;
class MailMergeGenerator
{
@@ -127,21 +129,26 @@ class MailMergeGenerator
$contents = $printer->printCollection($collection, $params, $idDataMap);
$filename = Util::sanitizeFileName($name) . '.pdf';
$type = $contents instanceof ZipContents ?
'application/zip' :
'application/pdf';
$filename = $contents instanceof ZipContents ?
Util::sanitizeFileName($name) . '.zip' :
Util::sanitizeFileName($name) . '.pdf';
/** @var Attachment $attachment */
$attachment = $this->entityManager->getNewEntity(Attachment::ENTITY_TYPE);
$attachment->set([
'relatedType' => Campaign::ENTITY_TYPE,
'relatedId' => $campaignId,
]);
$relatedLink = $campaignId ?
LinkParent::create(Campaign::ENTITY_TYPE, $campaignId) : null;
$attachment
->setRelated($relatedLink)
->setSize($contents->getStream()->getSize())
->setRole(self::ATTACHMENT_MAIL_MERGE_ROLE)
->setName($filename)
->setType('application/pdf');
->setType($type);
$this->entityManager->saveEntity($attachment);
@@ -204,7 +204,7 @@ return [
'auth2FAMethodList' => ['Totp'],
'personNameFormat' => 'firstLast',
'newNotificationCountInTitle' => false,
'pdfEngine' => 'Tcpdf',
'pdfEngine' => 'Dompdf',
'smsProvider' => null,
'defaultFileStorage' => 'EspoUploadDir',
'ldapUserNameAttribute' => 'sAMAccountName',
@@ -158,7 +158,8 @@
"oidcFallback": "OIDC Fallback Login",
"oidcAllowRegularUserFallback": "OIDC Allow fallback login for regular users",
"oidcAllowAdminUser": "OIDC Allow OIDC login for admin users",
"oidcLogoutUrl": "OIDC Logout URL"
"oidcLogoutUrl": "OIDC Logout URL",
"pdfEngine": "PDF Engine"
},
"options": {
"authenticationMethod": {
@@ -18,7 +18,8 @@
"rows": [
[{"name": "followCreatedEntities"}, {"name": "emailAddressIsOptedOutByDefault"}],
[{"name": "aclAllowDeleteCreated"}, {"name": "cleanupDeletedRecords"}],
[{"name": "exportDisabled"}, {"name": "b2cMode"}]
[{"name": "exportDisabled"}, {"name": "b2cMode"}],
[{"name": "pdfEngine"}, false]
]
},
{
@@ -34,5 +34,20 @@
"symbol",
"times"
]
},
"Dompdf": {
"implementationClassNameMap": {
"entity": "Espo\\Tools\\Pdf\\Dompdf\\EntityPrinter"
},
"fontFaceList": [
"Courier",
"Helvetica",
"Times",
"Symbol",
"ZapfDingbats",
"DejaVu Sans",
"DejaVu Serif",
"DejaVu Sans Mono"
]
}
}
@@ -852,6 +852,10 @@
"oidcLogoutUrl": {
"type": "varchar",
"tooltip": true
},
"pdfEngine": {
"type": "enum",
"view": "views/settings/fields/pdf-engine"
}
}
}
@@ -53,7 +53,7 @@
},
"headerPosition": {
"type": "float",
"default": 10
"default": 0
},
"teams": {
"type": "linkMultiple"
+5 -13
View File
@@ -29,23 +29,15 @@
namespace Espo\Tools\Pdf;
use Espo\Core\{
Exceptions\Error,
InjectableFactory,
};
use Espo\Core\InjectableFactory;
use RuntimeException;
class Builder
{
private ?Template $template = null;
private ?string $engine = null;
private InjectableFactory $injectableFactory;
public function __construct(InjectableFactory $injectableFactory)
{
$this->injectableFactory = $injectableFactory;
}
public function __construct(private InjectableFactory $injectableFactory) {}
public function setTemplate(Template $template): self
{
@@ -64,11 +56,11 @@ class Builder
public function build(): PrinterController
{
if (!$this->engine) {
throw new Error('Engine is not set.');
throw new RuntimeException('Engine is not set.');
}
if (!$this->template) {
throw new Error('Template is not set.');
throw new RuntimeException('Template is not set.');
}
return $this->injectableFactory->createWith(
@@ -0,0 +1,73 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Pdf\Dompdf;
use Espo\Tools\Pdf\Contents as ContentsInterface;
use GuzzleHttp\Psr7\Stream;
use Psr\Http\Message\StreamInterface;
use Dompdf\Dompdf;
use RuntimeException;
class Contents implements ContentsInterface
{
private ?string $string = null;
public function __construct(private Dompdf $pdf) {}
public function getStream(): StreamInterface
{
$resource = fopen('php://temp', 'r+');
if ($resource === false) {
throw new RuntimeException("Could not open temp.");
}
fwrite($resource, $this->getString());
rewind($resource);
return new Stream($resource);
}
public function getString(): string
{
if ($this->string === null) {
$this->string = $this->pdf->output();
}
return $this->string ?? '';
}
public function getLength(): int
{
return strlen($this->getString());
}
}
@@ -0,0 +1,76 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Pdf\Dompdf;
use Dompdf\Dompdf;
use Dompdf\Options;
use Espo\Core\Utils\Config;
use Espo\ORM\Entity;
use Espo\Tools\Pdf\Template;
class DompdfInitializer
{
private string $defaultFontFace = 'Courier';
public function __construct(
private Config $config
) {}
public function initialize(Template $template, ?Entity $entity = null): Dompdf
{
$options = new Options();
$options->setDefaultFont($this->getFontFace($template));
$pdf = new Dompdf($options);
$size = $template->getPageFormat() === Template::PAGE_FORMAT_CUSTOM ?
[$template->getPageWidth(), $template->getPageHeight()] :
$template->getPageFormat();
$orientation = $template->getPageOrientation() === Template::PAGE_ORIENTATION_PORTRAIT ?
'portrait' :
'landscape';
$pdf->setPaper($size, $orientation);
return $pdf;
}
private function getFontFace(Template $template): string
{
return
$template->getFontFace() ??
$this->config->get('pdfFontFace') ??
$this->defaultFontFace;
}
}
@@ -0,0 +1,62 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Pdf\Dompdf;
use Espo\ORM\Entity;
use Espo\Tools\Pdf\Contents;
use Espo\Tools\Pdf\Data;
use Espo\Tools\Pdf\Dompdf\Contents as DompdfContents;
use Espo\Tools\Pdf\EntityPrinter as EntityPrinterInterface;
use Espo\Tools\Pdf\Params;
use Espo\Tools\Pdf\Template;
class EntityPrinter implements EntityPrinterInterface
{
public function __construct(
private DompdfInitializer $dompdfInitializer,
private HtmlComposer $htmlComposer
) {}
public function print(Template $template, Entity $entity, Params $params, Data $data): Contents
{
$pdf = $this->dompdfInitializer->initialize($template, $entity);
$headHtml = $this->htmlComposer->composeHead($template, $entity);
$headerFooterHtml = $this->htmlComposer->composeHeaderFooter($template, $entity, $params, $data);
$mainHtml = $this->htmlComposer->composeMain($template, $entity, $params, $data);
$html = $headHtml . "\n<body>" . $headerFooterHtml . $mainHtml . "</body>";
$pdf->loadHtml($html);
$pdf->render();
return new DompdfContents($pdf);
}
}
@@ -0,0 +1,304 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Pdf\Dompdf;
use Espo\Core\Htmlizer\TemplateRendererFactory;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use Espo\ORM\Entity;
use Espo\Tools\Pdf\Data;
use Espo\Tools\Pdf\Params;
use Espo\Tools\Pdf\Template;
use TCPDF2DBarcode;
use TCPDFBarcode;
class HtmlComposer
{
public function __construct(
private Config $config,
private TemplateRendererFactory $templateRendererFactory,
private ImageSourceProvider $imageSourceProvider,
private Log $log
) {}
public function composeHead(Template $template, Entity $entity): string
{
$topMargin = $template->getTopMargin();
$rightMargin = $template->getRightMargin();
$bottomMargin = $template->getBottomMargin();
$leftMargin = $template->getLeftMargin();
$fontSize = $this->config->get('pdfFontSize') ?? 12;
$headerPosition = $template->getHeaderPosition();
$footerPosition = $template->getFooterPosition();
$titleHtml = '';
if ($template->hasTitle()) {
$title = $this->replacePlaceholders($template->getTitle(), $entity);
$titleHtml = "<title>" . htmlspecialchars($title) . "</title>";
}
$html = "
<head>
{$titleHtml}
</head>
<style>
@page {
margin: {$topMargin}mm {$rightMargin}mm {$bottomMargin}mm {$leftMargin}mm;
}
body {
font-size: {$fontSize}pt;
}
> header {
position: fixed;
margin-top: -{$topMargin}mm;
margin-left: -{$rightMargin}mm;
margin-right: -{$leftMargin}mm;
top: {$headerPosition}mm;
left: {$leftMargin}mm;
right: {$rightMargin}mm;
}
> footer {
position: fixed;
margin-bottom: -{$bottomMargin}mm;
margin-left: -{$leftMargin}mm;
margin-right: -{$rightMargin}mm;
height: {$footerPosition}mm;
bottom: 0;
left: {$leftMargin}mm;
right: {$rightMargin}mm;
}
> header .page-number:after,
> footer .page-number:after {
content: counter(page);
}
</style>
";
return $html;
}
public function composeHeaderFooter(Template $template, Entity $entity, Params $params, Data $data): string
{
$html = "";
$renderer = $this->templateRendererFactory
->create()
->setApplyAcl($params->applyAcl())
->setEntity($entity)
->setSkipInlineAttachmentHandling(true)
->setData($data->getAdditionalTemplateData());
// @todo Apply pagination tags.
if ($template->hasHeader()) {
$htmlHeader = $renderer->renderTemplate($template->getHeader());
$htmlHeader = $this->replaceHeadTags($htmlHeader);
$html .= "<header>{$htmlHeader}</header>";
}
if ($template->hasFooter()) {
$htmlFooter = $renderer->renderTemplate($template->getFooter());
$htmlFooter = $this->replaceHeadTags($htmlFooter);
$html .= "<footer>{$htmlFooter}</footer>";
}
return $html;
}
public function composeMain(
Template $template,
Entity $entity,
Params $params,
Data $data
): string {
$renderer = $this->templateRendererFactory
->create()
->setApplyAcl($params->applyAcl())
->setEntity($entity)
->setSkipInlineAttachmentHandling(true)
->setData($data->getAdditionalTemplateData());
$bodyTemplate = $template->getBody();
$html = $renderer->renderTemplate($bodyTemplate);
$html = $this->replaceTags($html);
return "<main>{$html}</main>";
}
private function replaceTags(string $html): string
{
// @todo Convert barcode tags.
$html = str_replace('<br pagebreak="true">', '<div style="page-break-after: always;"></div>', $html);
$html = preg_replace('/src="\@([A-Za-z0-9\+\/]*={0,2})"/', 'src="data:image/jpeg;base64,$1"', $html);
$html = str_replace('?entryPoint=attachment&amp;', '?entryPoint=attachment&', $html ?? '');
$html = preg_replace_callback(
'/<barcodeimage data="([^"]+)"\/>/',
function ($matches) {
$dataString = $matches[1];
$data = json_decode(urldecode($dataString), true);
return $this->composeBarcode($data);
},
$html
) ?? '';
$html = preg_replace_callback(
"/src=\"\?entryPoint=attachment\&id=([A-Za-z0-9]*)\"/",
function ($matches) {
$id = $matches[1];
if (!$id) {
return '';
}
$src = $this->imageSourceProvider->get($id);
if (!$src) {
return '';
}
return "src=\"{$src}\"";
},
$html
) ?? '';
return $html;
}
private function replaceHeadTags(string $html): string
{
$html = str_replace('{pageNumber}', '<span class="page-number"></span>', $html);
return $this->replaceTags($html);
}
/**
*
* @param array<string, mixed> $data
* @return string
*/
private function composeBarcode(array $data): string
{
$value = $data['value'] ?? null;
if ($value === null) {
return '';
}
$codeType = $data['type'] ?? 'CODE128';
$typeMap = [
"CODE128" => 'C128',
"CODE128A" => 'C128A',
"CODE128B" => 'C128B',
"CODE128C" => 'C128C',
"EAN13" => 'EAN13',
"EAN8" => 'EAN8',
"EAN5" => 'EAN5',
"EAN2" => 'EAN2',
"UPC" => 'UPCA',
"UPCE" => 'UPCE',
"ITF14" => 'I25',
"pharmacode" => 'PHARMA',
"QRcode" => 'QRCODE,H',
];
$type = $typeMap[$codeType] ?? null;
if ($codeType === 'QRcode') {
$width = $data['width'] ?? 40;
$height = $data['height'] ?? 40;
$color = $data['color'] ?? [0, 0, 0];
$barcode = new TCPDF2DBarcode($value, $type);
$code = $barcode->getBarcodeSVGcode($width, $height, $color);
$encoded = base64_encode($code);
$css = "width: {$width}mm; height: {$height}mm;";
return "<img src=\"data:image/svg+xml;base64,{$encoded}\" style=\"{$css}\">";
}
if (!$type) {
$this->log->warning("Not supported barcode type {$codeType}.");
return '';
}
$width = $data['width'] ?? 60;
$height = $data['height'] ?? 30;
$color = $data['color'] ?? [0, 0, 0];
$barcode = new TCPDFBarcode($value, $type);
$code = $barcode->getBarcodeSVGcode($width, $height, $color);
$encoded = base64_encode($code);
$css = "width: {$width}mm; height: {$height}mm;";
return "<img src=\"data:image/svg+xml;base64,{$encoded}\" style=\"{$css}\">";
}
private function replacePlaceholders(string $string, Entity $entity): string
{
$newString = $string;
$attributeList = ['name'];
foreach ($attributeList as $attribute) {
$value = (string) ($entity->get($attribute) ?? '');
$newString = str_replace('{$' . $attribute . '}', $value, $newString);
}
return $newString;
}
}
@@ -0,0 +1,72 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Pdf\Dompdf;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\FileStorage\Manager as FileStorageManager;
use Espo\Entities\Attachment;
use Espo\ORM\EntityManager;
use Espo\Tools\Attachment\Checker;
class ImageSourceProvider
{
public function __construct(
private EntityManager $entityManager,
private Checker $checker,
private FileStorageManager $fileStorageManager,
) {}
public function get(string $id): ?string
{
/** @var Attachment $attachment */
$attachment = $this->entityManager->getEntityById(Attachment::ENTITY_TYPE, $id);
if (!$attachment) {
return null;
}
try {
$this->checker->checkTypeImage($attachment);
}
catch (Forbidden) {
return null;
}
$type = $attachment->getType();
if (!$type) {
return null;
}
$contents = $this->fileStorageManager->getContents($attachment);
return 'data:image/' . $type . ';base64,' . base64_encode($contents);
}
}
+8 -2
View File
@@ -179,14 +179,20 @@ class MassService
$entityTypeTranslated = $this->defaultLanguage->translateLabel($entityType, 'scopeNamesPlural');
$filename = Util::sanitizeFileName($entityTypeTranslated) . '.pdf';
$type = $contents instanceof ZipContents ?
'application/zip' :
'application/pdf';
$filename = $contents instanceof ZipContents ?
Util::sanitizeFileName($entityTypeTranslated) . '.zip' :
Util::sanitizeFileName($entityTypeTranslated) . '.pdf';
/** @var Attachment $attachment */
$attachment = $this->entityManager->getNewEntity(Attachment::ENTITY_TYPE);
$attachment
->setName($filename)
->setType('application/pdf')
->setType($type)
->setRole(self::ATTACHMENT_MASS_PDF_ROLE)
->setSize($contents->getStream()->getSize());
@@ -29,40 +29,24 @@
namespace Espo\Tools\Pdf;
use Espo\Core\{
Exceptions\Error,
Utils\Metadata,
InjectableFactory,
};
use Espo\{
ORM\Entity,
ORM\Collection,
};
use Espo\Core\Exceptions\Error;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Collection;
use Espo\ORM\Entity;
class PrinterController
{
private Metadata $metadata;
private InjectableFactory $injectableFactory;
private Template $template;
private string $engine;
public function __construct(
Metadata $metadata,
InjectableFactory $injectableFactory,
Template $template,
string $engine
) {
$this->metadata = $metadata;
$this->injectableFactory = $injectableFactory;
$this->template = $template;
$this->engine = $engine;
}
private Metadata $metadata,
private InjectableFactory $injectableFactory,
private Template $template,
private string $engine
) {}
/**
* @throws Error
*/
public function printEntity(Entity $entity, ?Params $params, ?Data $data = null): Contents
{
$params = $params ?? new Params();
@@ -75,12 +59,34 @@ class PrinterController
* @param Collection<Entity> $collection
* @throws Error
*/
public function printCollection(Collection $collection, ?Params $params, ?IdDataMap $IdDataMap = null): Contents
{
$params = $params ?? new Params();
$IdDataMap = $IdDataMap ?? new IdDataMap();
public function printCollection(
Collection $collection,
?Params $params,
?IdDataMap $idDataMap = null
): Contents {
return $this->createCollectionPrinter()->print($this->template, $collection, $params, $IdDataMap);
$params = $params ?? new Params();
$idDataMap = $idDataMap ?? new IdDataMap();
if ($this->hasCollectionPrinter()) {
return $this->createCollectionPrinter()->print($this->template, $collection, $params, $idDataMap);
}
$printer = $this->createEntityPrinter();
$zipper = new Zipper();
foreach ($collection as $entity) {
$data = $idDataMap->get($entity->getId()) ?? new Data();
$itemContents = $printer->print($this->template, $entity, $params, $data);
$zipper->add($itemContents, $entity->getId());
}
$zipper->archive();
return new ZipContents($zipper->getFilePath());
}
/**
@@ -104,9 +110,7 @@ class PrinterController
*/
private function createCollectionPrinter(): CollectionPrinter
{
/** @var ?class-string<CollectionPrinter> $className */
$className = $this->metadata
->get(['app', 'pdfEngines', $this->engine, 'implementationClassNameMap', 'collection']) ?? null;
$className = $this->getCollectionPrinterClassName();
if (!$className) {
throw new Error("Unknown PDF engine '{$this->engine}', type 'collection'.");
@@ -114,4 +118,20 @@ class PrinterController
return $this->injectableFactory->create($className);
}
private function hasCollectionPrinter(): bool
{
return (bool) $this->getCollectionPrinterClassName();
}
/**
* @return ?class-string<CollectionPrinter>
*/
private function getCollectionPrinterClassName(): ?string
{
/** @var ?class-string<CollectionPrinter> */
return $this->metadata
->get(['app', 'pdfEngines', $this->engine, 'implementationClassNameMap', 'collection']) ?? null;
}
}
+5
View File
@@ -31,6 +31,11 @@ namespace Espo\Tools\Pdf;
interface Template
{
public const PAGE_FORMAT_CUSTOM = 'Custom';
public const PAGE_ORIENTATION_PORTRAIT = 'Portrait';
public const PAGE_ORIENTATION_LANDSCAPE = 'Landscape';
public function getFontFace(): ?string;
public function getBottomMargin(): float;
@@ -0,0 +1,61 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Pdf;
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7\Stream;
use RuntimeException;
class ZipContents implements Contents
{
public function __construct(private string $filePath) {}
public function getStream(): StreamInterface
{
$resource = fopen($this->filePath, 'r+');
if ($resource === false) {
throw new RuntimeException("Could not open {$this->filePath}.");
}
return new Stream($resource);
}
public function getString(): string
{
return $this->getStream()->getContents();
}
public function getLength(): int
{
return (int) $this->getStream()->getSize();
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Pdf;
use Espo\Core\Utils\Util;
use LogicException;
use RuntimeException;
use ZipArchive;
class Zipper
{
private ?string $filePath = null;
/** @var array{string, string}[] */
private array $itemList = [];
public function __construct() {}
public function add(Contents $contents, string $name): void
{
$tempPath = tempnam(sys_get_temp_dir(), 'espo-pdf-zip-item');
if ($tempPath === false) {
throw new RuntimeException("Could not create a temp file.");
}
$fp = fopen($tempPath, 'w');
if ($fp === false) {
throw new RuntimeException("Could not open a temp file {$tempPath}.");
}
fwrite($fp, $contents->getString());
fclose($fp);
$this->itemList[] = [$tempPath, Util::sanitizeFileName($name) . '.pdf'];
}
public function archive(): void
{
$tempPath = tempnam(sys_get_temp_dir(), 'espo-pdf-zip');
if ($tempPath === false) {
throw new RuntimeException("Could not create a temp file.");
}
$archive = new ZipArchive();
$archive->open($tempPath, ZipArchive::CREATE);
foreach ($this->itemList as $item) {
$archive->addFile($item[0], $item[1]);
}
$archive->close();
$this->filePath = $tempPath;
}
public function getFilePath(): string
{
if (!$this->filePath) {
throw new LogicException();
}
return $this->filePath;
}
}
@@ -0,0 +1,41 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
define('views/settings/fields/pdf-engine', ['views/fields/enum'], function (Dep) {
return Dep.extend({
setupOptions: function () {
this.params.options = Object.keys(this.getMetadata().get(['app', 'pdfEngines']));
if (this.params.options.length === 0) {
this.params.options = [''];
}
},
});
});
+2 -1
View File
@@ -46,7 +46,8 @@
"league/flysystem-async-aws-s3": "^2.0",
"johngrogg/ics-parser": "^3.0",
"phpseclib/phpseclib": "^3.0",
"openspout/openspout": "^4.9"
"openspout/openspout": "^4.9",
"dompdf/dompdf": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^9",
Generated
+275 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6221dcfbe6738d65122c1a60dcf4c1b2",
"content-hash": "ffda69f674ecffbd773206c0e88aeb52",
"packages": [
{
"name": "async-aws/core",
@@ -620,6 +620,68 @@
],
"time": "2020-05-29T18:28:51+00:00"
},
{
"name": "dompdf/dompdf",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
"reference": "c5310df0e22c758c85ea5288175fc6cd777bc085"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/c5310df0e22c758c85ea5288175fc6cd777bc085",
"reference": "c5310df0e22c758c85ea5288175fc6cd777bc085",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-mbstring": "*",
"masterminds/html5": "^2.0",
"phenx/php-font-lib": ">=0.5.4 <1.0.0",
"phenx/php-svg-lib": ">=0.3.3 <1.0.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"ext-json": "*",
"ext-zip": "*",
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^7.5 || ^8 || ^9",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-gmagick": "Improves image processing performance",
"ext-imagick": "Improves image processing performance",
"ext-zlib": "Needed for pdf stream compression"
},
"type": "library",
"autoload": {
"psr-4": {
"Dompdf\\": "src/"
},
"classmap": [
"lib/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "The Dompdf Community",
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
}
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"support": {
"issues": "https://github.com/dompdf/dompdf/issues",
"source": "https://github.com/dompdf/dompdf/tree/v2.0.1"
},
"time": "2022-09-22T13:43:41+00:00"
},
{
"name": "dragonmantank/cron-expression",
"version": "v3.0.2",
@@ -2090,6 +2152,75 @@
},
"time": "2021-01-23T16:37:31+00:00"
},
{
"name": "masterminds/html5",
"version": "2.7.6",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
"reference": "897eb517a343a2281f11bc5556d6548db7d93947"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947",
"reference": "897eb517a343a2281f11bc5556d6548db7d93947",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-dom": "*",
"ext-libxml": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Masterminds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Butcher",
"email": "technosophos@gmail.com"
},
{
"name": "Matt Farina",
"email": "matt@mattfarina.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "An HTML5 parser and serializer.",
"homepage": "http://masterminds.github.io/html5-php",
"keywords": [
"HTML5",
"dom",
"html",
"parser",
"querypath",
"serializer",
"xml"
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.7.6"
},
"time": "2022-08-18T16:18:26+00:00"
},
{
"name": "michelf/php-markdown",
"version": "1.9.0",
@@ -2725,6 +2856,96 @@
},
"time": "2020-10-15T08:29:30+00:00"
},
{
"name": "phenx/php-font-lib",
"version": "0.5.4",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
"reference": "dd448ad1ce34c63d09baccd05415e361300c35b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/dd448ad1ce34c63d09baccd05415e361300c35b4",
"reference": "dd448ad1ce34c63d09baccd05415e361300c35b4",
"shasum": ""
},
"require": {
"ext-mbstring": "*"
},
"require-dev": {
"symfony/phpunit-bridge": "^3 || ^4 || ^5"
},
"type": "library",
"autoload": {
"psr-4": {
"FontLib\\": "src/FontLib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0"
],
"authors": [
{
"name": "Fabien Ménager",
"email": "fabien.menager@gmail.com"
}
],
"description": "A library to read, parse, export and make subsets of different types of font files.",
"homepage": "https://github.com/PhenX/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
"source": "https://github.com/dompdf/php-font-lib/tree/0.5.4"
},
"time": "2021-12-17T19:44:54+00:00"
},
{
"name": "phenx/php-svg-lib",
"version": "0.5.0",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-svg-lib.git",
"reference": "76876c6cf3080bcb6f249d7d59705108166a6685"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/76876c6cf3080bcb6f249d7d59705108166a6685",
"reference": "76876c6cf3080bcb6f249d7d59705108166a6685",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabberworm/php-css-parser": "^8.4"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0"
],
"authors": [
{
"name": "Fabien Ménager",
"email": "fabien.menager@gmail.com"
}
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/PhenX/php-svg-lib",
"support": {
"issues": "https://github.com/dompdf/php-svg-lib/issues",
"source": "https://github.com/dompdf/php-svg-lib/tree/0.5.0"
},
"time": "2022-09-06T12:16:56+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.16.0",
@@ -3953,6 +4174,59 @@
},
"time": "2019-06-21T08:51:04+00:00"
},
{
"name": "sabberworm/php-css-parser",
"version": "8.4.0",
"source": {
"type": "git",
"url": "https://github.com/sabberworm/PHP-CSS-Parser.git",
"reference": "e41d2140031d533348b2192a83f02d8dd8a71d30"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/e41d2140031d533348b2192a83f02d8dd8a71d30",
"reference": "e41d2140031d533348b2192a83f02d8dd8a71d30",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=5.6.20"
},
"require-dev": {
"codacy/coverage": "^1.4",
"phpunit/phpunit": "^4.8.36"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
},
"type": "library",
"autoload": {
"psr-4": {
"Sabberworm\\CSS\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
],
"support": {
"issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues",
"source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.4.0"
},
"time": "2021-12-11T13:40:54+00:00"
},
{
"name": "slim/psr7",
"version": "1.4",