235 lines
8.3 KiB
PHP
235 lines
8.3 KiB
PHP
<?php
|
||
|
||
namespace app\support;
|
||
|
||
class ReportPdfGenerator
|
||
{
|
||
public function generate(array $payload): string
|
||
{
|
||
$heroImage = $this->normalizeHeroImage($payload['hero_image'] ?? null);
|
||
$content = $this->buildContentStream($payload, $heroImage);
|
||
$resources = '<< /Font << /F1 5 0 R >>';
|
||
if ($heroImage) {
|
||
$resources .= ' /XObject << /Im1 7 0 R >>';
|
||
}
|
||
$resources .= ' >>';
|
||
|
||
$objects = [
|
||
1 => '<< /Type /Catalog /Pages 2 0 R >>',
|
||
2 => '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
|
||
3 => sprintf('<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources %s /Contents 4 0 R >>', $resources),
|
||
4 => sprintf("<< /Length %d >>\nstream\n%s\nendstream", strlen($content), $content),
|
||
5 => '<< /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [6 0 R] >>',
|
||
6 => '<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 4 >> /DW 1000 >>',
|
||
];
|
||
if ($heroImage) {
|
||
$objects[7] = sprintf(
|
||
"<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>\nstream\n%s\nendstream",
|
||
$heroImage['width'],
|
||
$heroImage['height'],
|
||
strlen($heroImage['data']),
|
||
$heroImage['data']
|
||
);
|
||
}
|
||
|
||
return $this->renderPdf($objects);
|
||
}
|
||
|
||
private function buildContentStream(array $payload, ?array $heroImage): string
|
||
{
|
||
$title = $this->normalizeText((string)($payload['report_title'] ?? '鉴定报告'));
|
||
$serviceProviderText = $this->normalizeText((string)($payload['service_provider_text'] ?? '-'));
|
||
$institutionName = $this->normalizeText((string)($payload['institution_name'] ?? '-'));
|
||
$reportNo = $this->normalizeText((string)($payload['report_no'] ?? '-'));
|
||
$publishTime = $this->normalizeText((string)($payload['publish_time'] ?? '-'));
|
||
$productName = $this->normalizeText((string)($payload['product_name'] ?? '-'));
|
||
$verifyInfo = $this->normalizeText((string)($payload['verify_info'] ?? '-'));
|
||
$riskNotice = $this->normalizeText((string)($payload['risk_notice_text'] ?? '-'));
|
||
$productItems = is_array($payload['product_items'] ?? null) ? $payload['product_items'] : [];
|
||
$heroImageLabels = is_array($payload['hero_image_labels'] ?? null) ? $payload['hero_image_labels'] : [];
|
||
|
||
$blocks = [];
|
||
$y = 790;
|
||
|
||
$blocks[] = $this->textBlock($title, 52, $y, 20);
|
||
$y -= 32;
|
||
$blocks[] = $this->textBlock('正式报告凭证,请以报告页防伪查询结果为准。', 52, $y, 10);
|
||
$y -= 30;
|
||
|
||
foreach ([
|
||
sprintf('报告编号:%s', $reportNo),
|
||
sprintf('检测机构:%s', $institutionName),
|
||
sprintf('出具时间:%s', $publishTime),
|
||
sprintf('服务类型:%s', $serviceProviderText),
|
||
] as $line) {
|
||
$blocks[] = $this->textBlock($line, 52, $y, 12);
|
||
$y -= 22;
|
||
}
|
||
|
||
if ($heroImage) {
|
||
$maxWidth = 230;
|
||
$maxHeight = 150;
|
||
$scale = min($maxWidth / $heroImage['width'], $maxHeight / $heroImage['height'], 1);
|
||
$drawWidth = (int)round($heroImage['width'] * $scale);
|
||
$drawHeight = (int)round($heroImage['height'] * $scale);
|
||
$y -= $drawHeight + 6;
|
||
$blocks[] = $this->imageBlock('Im1', 52, $y, $drawWidth, $drawHeight);
|
||
$y -= 24;
|
||
} elseif ($heroImageLabels) {
|
||
foreach ($this->wrapText('鉴定图片:' . implode('、', array_slice($heroImageLabels, 0, 3)), 34) as $line) {
|
||
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
||
$y -= 18;
|
||
}
|
||
$y -= 8;
|
||
}
|
||
|
||
$y -= 8;
|
||
$blocks[] = $this->textBlock('产品信息', 52, $y, 15);
|
||
$y -= 24;
|
||
$blocks[] = $this->textBlock(sprintf('产品名称:%s', $productName), 52, $y, 12);
|
||
$y -= 22;
|
||
|
||
foreach ($productItems as $item) {
|
||
if (!is_array($item)) {
|
||
continue;
|
||
}
|
||
$label = $this->normalizeText((string)($item['label'] ?? ''));
|
||
if ($label === '') {
|
||
continue;
|
||
}
|
||
$value = $this->normalizeText((string)($item['value'] ?? '-'));
|
||
$remark = $this->normalizeText((string)($item['remark'] ?? ''));
|
||
foreach ($this->wrapText(sprintf('%s:%s', $label, $value !== '' ? $value : '-'), 34) as $line) {
|
||
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
||
$y -= 18;
|
||
}
|
||
if ($remark !== '') {
|
||
foreach ($this->wrapText('说明:' . $remark, 34) as $line) {
|
||
$blocks[] = $this->textBlock($line, 72, $y, 10);
|
||
$y -= 16;
|
||
}
|
||
}
|
||
$y -= 2;
|
||
}
|
||
|
||
$y -= 6;
|
||
$blocks[] = $this->textBlock(sprintf('验真信息:%s', $verifyInfo), 52, $y, 11);
|
||
$y -= 22;
|
||
|
||
$y -= 8;
|
||
foreach ($this->wrapText('风险说明:' . $riskNotice, 30) as $line) {
|
||
$blocks[] = $this->textBlock($line, 52, $y, 10);
|
||
$y -= 17;
|
||
}
|
||
|
||
if ($y > 48) {
|
||
$blocks[] = $this->textBlock('安心检验鉴定平台', 52, 42, 9);
|
||
}
|
||
|
||
return implode("\n", array_filter($blocks));
|
||
}
|
||
|
||
private function normalizeHeroImage(mixed $image): ?array
|
||
{
|
||
if (!is_array($image)) {
|
||
return null;
|
||
}
|
||
|
||
$data = (string)($image['data'] ?? '');
|
||
$width = (int)($image['width'] ?? 0);
|
||
$height = (int)($image['height'] ?? 0);
|
||
if ($data === '' || $width <= 0 || $height <= 0) {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'data' => $data,
|
||
'width' => $width,
|
||
'height' => $height,
|
||
];
|
||
}
|
||
|
||
private function wrapText(string $text, int $maxUnits): array
|
||
{
|
||
$normalized = $this->normalizeText($text);
|
||
if ($normalized === '') {
|
||
return ['-'];
|
||
}
|
||
|
||
$chars = preg_split('//u', $normalized, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||
$lines = [];
|
||
$current = '';
|
||
$width = 0.0;
|
||
|
||
foreach ($chars as $char) {
|
||
$charWidth = strlen($char) === 1 && ord($char) < 128 ? 0.5 : 1.0;
|
||
if ($current !== '' && $width + $charWidth > $maxUnits) {
|
||
$lines[] = $current;
|
||
$current = '';
|
||
$width = 0.0;
|
||
}
|
||
$current .= $char;
|
||
$width += $charWidth;
|
||
}
|
||
|
||
if ($current !== '') {
|
||
$lines[] = $current;
|
||
}
|
||
|
||
return $lines ?: ['-'];
|
||
}
|
||
|
||
private function textBlock(string $text, int $x, int $y, int $fontSize): string
|
||
{
|
||
if ($text === '') {
|
||
return '';
|
||
}
|
||
|
||
return sprintf(
|
||
"BT\n/F1 %d Tf\n1 0 0 1 %d %d Tm\n<%s> Tj\nET",
|
||
$fontSize,
|
||
$x,
|
||
$y,
|
||
strtoupper(bin2hex(mb_convert_encoding($text, 'UCS-2BE', 'UTF-8')))
|
||
);
|
||
}
|
||
|
||
private function imageBlock(string $name, int $x, int $y, int $width, int $height): string
|
||
{
|
||
return sprintf("q\n%d 0 0 %d %d %d cm\n/%s Do\nQ", $width, $height, $x, $y, $name);
|
||
}
|
||
|
||
private function normalizeText(string $text): string
|
||
{
|
||
$text = trim(str_replace(["\r\n", "\r", "\n", "\t"], [' ', ' ', ' ', ' '], $text));
|
||
return preg_replace('/\s+/u', ' ', $text) ?: '';
|
||
}
|
||
|
||
private function renderPdf(array $objects): string
|
||
{
|
||
$pdf = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
|
||
$offsets = [];
|
||
|
||
foreach ($objects as $id => $body) {
|
||
$offsets[$id] = strlen($pdf);
|
||
$pdf .= sprintf("%d 0 obj\n%s\nendobj\n", $id, $body);
|
||
}
|
||
|
||
$xrefPosition = strlen($pdf);
|
||
$pdf .= sprintf("xref\n0 %d\n", count($objects) + 1);
|
||
$pdf .= "0000000000 65535 f \n";
|
||
|
||
foreach ($objects as $id => $_body) {
|
||
$pdf .= sprintf("%010d 00000 n \n", $offsets[$id]);
|
||
}
|
||
|
||
$pdf .= sprintf(
|
||
"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF",
|
||
count($objects) + 1,
|
||
$xrefPosition
|
||
);
|
||
|
||
return $pdf;
|
||
}
|
||
}
|