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; } }