From 6339690e58ae4bc8aae8f94d1d2dd2f0dcde5312 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 15 Jan 2023 16:57:45 +0200 Subject: [PATCH] dompdf engine --- .../Espo/Classes/TemplateHelpers/TableTag.php | 4 +- .../Crm/Tools/Campaign/MailMergeGenerator.php | 19 +- .../Espo/Resources/defaults/config.php | 2 +- .../Espo/Resources/i18n/en_US/Settings.json | 3 +- .../Resources/layouts/Settings/settings.json | 3 +- .../Resources/metadata/app/pdfEngines.json | 15 + .../metadata/entityDefs/Settings.json | 4 + .../metadata/entityDefs/Template.json | 2 +- application/Espo/Tools/Pdf/Builder.php | 18 +- .../Espo/Tools/Pdf/Dompdf/Contents.php | 73 +++++ .../Tools/Pdf/Dompdf/DompdfInitializer.php | 76 +++++ .../Espo/Tools/Pdf/Dompdf/EntityPrinter.php | 62 ++++ .../Espo/Tools/Pdf/Dompdf/HtmlComposer.php | 304 ++++++++++++++++++ .../Tools/Pdf/Dompdf/ImageSourceProvider.php | 72 +++++ application/Espo/Tools/Pdf/MassService.php | 10 +- .../Espo/Tools/Pdf/PrinterController.php | 94 +++--- application/Espo/Tools/Pdf/Template.php | 5 + application/Espo/Tools/Pdf/ZipContents.php | 61 ++++ application/Espo/Tools/Pdf/Zipper.php | 93 ++++++ .../src/views/settings/fields/pdf-engine.js | 41 +++ composer.json | 3 +- composer.lock | 276 +++++++++++++++- 22 files changed, 1175 insertions(+), 65 deletions(-) create mode 100644 application/Espo/Tools/Pdf/Dompdf/Contents.php create mode 100644 application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php create mode 100644 application/Espo/Tools/Pdf/Dompdf/EntityPrinter.php create mode 100644 application/Espo/Tools/Pdf/Dompdf/HtmlComposer.php create mode 100644 application/Espo/Tools/Pdf/Dompdf/ImageSourceProvider.php create mode 100644 application/Espo/Tools/Pdf/ZipContents.php create mode 100644 application/Espo/Tools/Pdf/Zipper.php create mode 100644 client/src/views/settings/fields/pdf-engine.js 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( - "" . + "
" . $content . "
" ); 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 .= "
{$htmlHeader}
"; + } + + 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",