diff --git a/application/Espo/Classes/TemplateHelpers/TableTag.php b/application/Espo/Classes/TemplateHelpers/TableTag.php
index af87f167d4..8e0694b89a 100644
--- a/application/Espo/Classes/TemplateHelpers/TableTag.php
+++ b/application/Espo/Classes/TemplateHelpers/TableTag.php
@@ -51,8 +51,10 @@ class TableTag implements Helper
$content = $function !== null ? $function() : '';
+ $style = "border: {$border}; border-spacing: 0; border-collapse: collapse;";
+
return Result::createSafeString(
- "
" .
+ ""
);
diff --git a/application/Espo/Modules/Crm/Tools/Campaign/MailMergeGenerator.php b/application/Espo/Modules/Crm/Tools/Campaign/MailMergeGenerator.php
index 08e38030e3..20c7150752 100644
--- a/application/Espo/Modules/Crm/Tools/Campaign/MailMergeGenerator.php
+++ b/application/Espo/Modules/Crm/Tools/Campaign/MailMergeGenerator.php
@@ -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);
diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php
index acad56120b..8dbbc1de80 100644
--- a/application/Espo/Resources/defaults/config.php
+++ b/application/Espo/Resources/defaults/config.php
@@ -204,7 +204,7 @@ return [
'auth2FAMethodList' => ['Totp'],
'personNameFormat' => 'firstLast',
'newNotificationCountInTitle' => false,
- 'pdfEngine' => 'Tcpdf',
+ 'pdfEngine' => 'Dompdf',
'smsProvider' => null,
'defaultFileStorage' => 'EspoUploadDir',
'ldapUserNameAttribute' => 'sAMAccountName',
diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json
index 0bf5de9f78..9bbe095e2e 100644
--- a/application/Espo/Resources/i18n/en_US/Settings.json
+++ b/application/Espo/Resources/i18n/en_US/Settings.json
@@ -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": {
diff --git a/application/Espo/Resources/layouts/Settings/settings.json b/application/Espo/Resources/layouts/Settings/settings.json
index 861fb49382..92eadd23ad 100644
--- a/application/Espo/Resources/layouts/Settings/settings.json
+++ b/application/Espo/Resources/layouts/Settings/settings.json
@@ -18,7 +18,8 @@
"rows": [
[{"name": "followCreatedEntities"}, {"name": "emailAddressIsOptedOutByDefault"}],
[{"name": "aclAllowDeleteCreated"}, {"name": "cleanupDeletedRecords"}],
- [{"name": "exportDisabled"}, {"name": "b2cMode"}]
+ [{"name": "exportDisabled"}, {"name": "b2cMode"}],
+ [{"name": "pdfEngine"}, false]
]
},
{
diff --git a/application/Espo/Resources/metadata/app/pdfEngines.json b/application/Espo/Resources/metadata/app/pdfEngines.json
index f1a79e39a1..7cbec0ece9 100644
--- a/application/Espo/Resources/metadata/app/pdfEngines.json
+++ b/application/Espo/Resources/metadata/app/pdfEngines.json
@@ -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"
+ ]
}
}
diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json
index 8a99cfe850..e780223242 100644
--- a/application/Espo/Resources/metadata/entityDefs/Settings.json
+++ b/application/Espo/Resources/metadata/entityDefs/Settings.json
@@ -852,6 +852,10 @@
"oidcLogoutUrl": {
"type": "varchar",
"tooltip": true
+ },
+ "pdfEngine": {
+ "type": "enum",
+ "view": "views/settings/fields/pdf-engine"
}
}
}
diff --git a/application/Espo/Resources/metadata/entityDefs/Template.json b/application/Espo/Resources/metadata/entityDefs/Template.json
index e8cfa7cf0e..8857a0eabb 100644
--- a/application/Espo/Resources/metadata/entityDefs/Template.json
+++ b/application/Espo/Resources/metadata/entityDefs/Template.json
@@ -53,7 +53,7 @@
},
"headerPosition": {
"type": "float",
- "default": 10
+ "default": 0
},
"teams": {
"type": "linkMultiple"
diff --git a/application/Espo/Tools/Pdf/Builder.php b/application/Espo/Tools/Pdf/Builder.php
index 8cb7797e34..ecdb5c9b19 100644
--- a/application/Espo/Tools/Pdf/Builder.php
+++ b/application/Espo/Tools/Pdf/Builder.php
@@ -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(
diff --git a/application/Espo/Tools/Pdf/Dompdf/Contents.php b/application/Espo/Tools/Pdf/Dompdf/Contents.php
new file mode 100644
index 0000000000..cffcec9de8
--- /dev/null
+++ b/application/Espo/Tools/Pdf/Dompdf/Contents.php
@@ -0,0 +1,73 @@
+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());
+ }
+}
diff --git a/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php
new file mode 100644
index 0000000000..a549a27f4a
--- /dev/null
+++ b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php
@@ -0,0 +1,76 @@
+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;
+ }
+}
diff --git a/application/Espo/Tools/Pdf/Dompdf/EntityPrinter.php b/application/Espo/Tools/Pdf/Dompdf/EntityPrinter.php
new file mode 100644
index 0000000000..09fcde4d28
--- /dev/null
+++ b/application/Espo/Tools/Pdf/Dompdf/EntityPrinter.php
@@ -0,0 +1,62 @@
+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" . $headerFooterHtml . $mainHtml . "";
+
+ $pdf->loadHtml($html);
+ $pdf->render();
+
+ return new DompdfContents($pdf);
+ }
+}
diff --git a/application/Espo/Tools/Pdf/Dompdf/HtmlComposer.php b/application/Espo/Tools/Pdf/Dompdf/HtmlComposer.php
new file mode 100644
index 0000000000..0dcff4841d
--- /dev/null
+++ b/application/Espo/Tools/Pdf/Dompdf/HtmlComposer.php
@@ -0,0 +1,304 @@
+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 = "" . htmlspecialchars($title) . "";
+ }
+
+ $html = "
+
+ {$titleHtml}
+
+
+ ";
+
+ 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 .= "";
+ }
+
+ if ($template->hasFooter()) {
+ $htmlFooter = $renderer->renderTemplate($template->getFooter());
+
+ $htmlFooter = $this->replaceHeadTags($htmlFooter);
+
+ $html .= "";
+ }
+
+ 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 "{$html}";
+ }
+
+ private function replaceTags(string $html): string
+ {
+ // @todo Convert barcode tags.
+
+ $html = str_replace('
', '', $html);
+ $html = preg_replace('/src="\@([A-Za-z0-9\+\/]*={0,2})"/', 'src="data:image/jpeg;base64,$1"', $html);
+ $html = str_replace('?entryPoint=attachment&', '?entryPoint=attachment&', $html ?? '');
+
+ $html = preg_replace_callback(
+ '//',
+ 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}', '', $html);
+
+ return $this->replaceTags($html);
+ }
+
+ /**
+ *
+ * @param array $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 "
";
+ }
+
+ 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 "
";
+ }
+
+ 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;
+ }
+}
diff --git a/application/Espo/Tools/Pdf/Dompdf/ImageSourceProvider.php b/application/Espo/Tools/Pdf/Dompdf/ImageSourceProvider.php
new file mode 100644
index 0000000000..842712fbc9
--- /dev/null
+++ b/application/Espo/Tools/Pdf/Dompdf/ImageSourceProvider.php
@@ -0,0 +1,72 @@
+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);
+ }
+}
diff --git a/application/Espo/Tools/Pdf/MassService.php b/application/Espo/Tools/Pdf/MassService.php
index 0bcc32fb6b..58c0f37342 100644
--- a/application/Espo/Tools/Pdf/MassService.php
+++ b/application/Espo/Tools/Pdf/MassService.php
@@ -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());
diff --git a/application/Espo/Tools/Pdf/PrinterController.php b/application/Espo/Tools/Pdf/PrinterController.php
index 06ecbb7b49..3015d1c25d 100644
--- a/application/Espo/Tools/Pdf/PrinterController.php
+++ b/application/Espo/Tools/Pdf/PrinterController.php
@@ -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 $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 $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
+ */
+ private function getCollectionPrinterClassName(): ?string
+ {
+ /** @var ?class-string */
+ return $this->metadata
+ ->get(['app', 'pdfEngines', $this->engine, 'implementationClassNameMap', 'collection']) ?? null;
+ }
+
}
diff --git a/application/Espo/Tools/Pdf/Template.php b/application/Espo/Tools/Pdf/Template.php
index 8e7e0095f6..2500bb2b5a 100644
--- a/application/Espo/Tools/Pdf/Template.php
+++ b/application/Espo/Tools/Pdf/Template.php
@@ -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;
diff --git a/application/Espo/Tools/Pdf/ZipContents.php b/application/Espo/Tools/Pdf/ZipContents.php
new file mode 100644
index 0000000000..2daa6c663e
--- /dev/null
+++ b/application/Espo/Tools/Pdf/ZipContents.php
@@ -0,0 +1,61 @@
+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();
+ }
+}
diff --git a/application/Espo/Tools/Pdf/Zipper.php b/application/Espo/Tools/Pdf/Zipper.php
new file mode 100644
index 0000000000..fed83ec29d
--- /dev/null
+++ b/application/Espo/Tools/Pdf/Zipper.php
@@ -0,0 +1,93 @@
+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;
+ }
+}
diff --git a/client/src/views/settings/fields/pdf-engine.js b/client/src/views/settings/fields/pdf-engine.js
new file mode 100644
index 0000000000..8b58a2ad3a
--- /dev/null
+++ b/client/src/views/settings/fields/pdf-engine.js
@@ -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 = [''];
+ }
+ },
+ });
+});
diff --git a/composer.json b/composer.json
index 97f3590ae3..cd8458eaeb 100644
--- a/composer.json
+++ b/composer.json
@@ -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",
diff --git a/composer.lock b/composer.lock
index c458920a17..1cee22cf39 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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",