. * * 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 Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ namespace Espo\Tools\Pdf\Dompdf; use chillerlan\QRCode\Common\EccLevel; use chillerlan\QRCode\Output\QRMarkupSVG; use chillerlan\QRCode\Output\QROutputInterface; 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 Picqer\Barcode\BarcodeGeneratorSVG; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; use Picqer\Barcode\Exceptions\UnknownTypeException; use RuntimeException; 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 = "" . htmlspecialchars($title) . ""; } $templateStyle = $template->getStyle() ?? ''; /** @noinspection HtmlRequiredTitleElement */ return " {$titleHtml} "; } public function composeHeaderFooter(Template $template, Entity $entity, Params $params, Data $data): string { $html = ""; $renderer = $this->templateRendererFactory ->create() ->setApplyAcl($params->applyAcl()) ->setEntity($entity) ->setSkipInlineAttachmentHandling() ->setData($data->getAdditionalTemplateData()); 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() ->setData($data->getAdditionalTemplateData()); $bodyTemplate = $template->getBody(); $html = $renderer->renderTemplate($bodyTemplate); $html = $this->replaceTags($html); return "
$html
"; } private function replaceTags(string $html): string { /** @noinspection HtmlUnknownAttribute */ $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 ) ?? ''; return 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 ) ?? ''; } 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'; /** @noinspection SpellCheckingInspection */ $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; /** @noinspection SpellCheckingInspection */ if ($codeType === 'QRcode') { $width = $data['width'] ?? 40; $height = $data['height'] ?? 40; //$color = $data['color'] ?? '#000'; $options = new QROptions(); // @todo Revise and test after updating the lib. /** @phpstan-ignore-next-line property.protected */ $options->outputType = QROutputInterface::MARKUP_SVG; /** @phpstan-ignore-next-line property.protected */ $options->eccLevel = EccLevel::H; $code = (new QRCode($options))->render($value); $css = "width: {$width}mm; height: {$height}mm;"; /** @noinspection HtmlRequiredAltAttribute */ return ""; } if (!$type || $type === 'QRCODE,H') { $this->log->warning("Not supported barcode type $codeType."); return ''; } $width = $data['width'] ?? 60; $height = $data['height'] ?? 30; $color = $data['color'] ?? '#000'; try { $code = (new BarcodeGeneratorSVG())->getBarcode($value, $type, 2, $height, $color); } catch (UnknownTypeException $e) { throw new RuntimeException("Barcode print error.", previous: $e); } $encoded = base64_encode($code); $css = "width: {$width}mm; height: {$height}mm;"; /** @noinspection HtmlRequiredAltAttribute */ 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; } }