fix: 修复报价确认单 PDF 方框乱码 (#993)

This commit is contained in:
limeng 2026-11-07 18:41:31 +08:00
parent d98faf851a
commit dffc01dc51
2 changed files with 104 additions and 3 deletions

View File

@ -43,7 +43,9 @@
<body>
@php
$mixedText = static function ($value): string {
$parts = preg_split('/([\x20-\x7E]+)/', (string) $value, -1, PREG_SPLIT_DELIM_CAPTURE) ?: [];
// Spreadsheet-pasted whitespace has no glyph in the CJK font.
$text = str_replace(["\r\n", "\r", "\t", "\u{00A0}"], ["\n", "\n", ' ', ' '], (string) $value);
$parts = preg_split('/([\x20-\x7E]+)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE) ?: [];
return implode('', array_map(static function (string $part): string {
$escaped = e($part);
@ -57,7 +59,7 @@
<header class="heading">
<div class="heading__title">
<h1>报价确认单</h1>
<p>{!! $mixedText($version->carrier_name) !!} · {!! $mixedText($version->quote_type) !!}</p>
<p>{!! $mixedText($version->carrier_name) !!} <span class="latin">·</span> {!! $mixedText($version->quote_type) !!}</p>
</div>
<div class="heading__mark">不可覆盖版本:{!! $mixedText($version->version_code) !!}</div>
</header>
@ -140,7 +142,7 @@
<h2>签署确认</h2>
<div class="signature__meta">报价版本:{!! $mixedText($version->version_code) !!} 快递公司:{!! $mixedText($version->carrier_name) !!} 执行日期:{!! $mixedText($version->effective_date?->toDateString()) !!}</div>
<div class="signature__box"><div>乙方(快递方)签章:{!! $mixedText($version->carrier_name) !!}</div><div class="signature__blank"></div><div>签署日期:{!! $mixedText($signatureDate) !!}</div></div>
<div class="document-control">生成时间:{!! $mixedText($generatedAt->format('Y-m-d H:i:s')) !!} · 版本 {!! $mixedText($version->version_code) !!}</div>
<div class="document-control">生成时间:{!! $mixedText($generatedAt->format('Y-m-d H:i:s')) !!} <span class="latin">·</span> 版本 {!! $mixedText($version->version_code) !!}</div>
</section>
</body>
</html>

View File

@ -0,0 +1,99 @@
<?php
namespace Tests\Unit;
use App\Models\Business\CourierQuoteVersion;
use DOMElement;
use Dompdf\FrameDecorator\Text;
use FontLib\Font;
use Illuminate\Foundation\Testing\TestCase;
use Tests\CreatesApplication;
class CourierQuotePdfWhitespaceTest extends TestCase
{
use CreatesApplication;
public function test_pasted_quote_rules_render_without_missing_glyphs_and_keep_separate_lines(): void
{
$html = view('business.pdf.courier-quote', [
'version' => new CourierQuoteVersion([
'version_code' => 'QB-TEST', 'carrier_name' => '示例快递',
'quote_type' => '普通报价', 'effective_date' => '2026-11-01',
]),
'change' => null,
'values' => [['地区', '费用'], ['江苏', '6.50']],
'formulas' => [],
'partyA' => '示例甲方', 'expiryDate' => '长期',
'faceFee' => '0.50', 'bubbleCoefficient' => '1.2',
'billingRules' => "1、\u{00A0}计费重 > 0.5kg,费用6.50元。\t\t\t\r\n2、\u{00A0}轻抛比1:10000。\t\t\t\r3、保留末行。",
'signatureDate' => '2026年11月7日', 'generatedAt' => now(),
'fontPath' => str_replace('\\', '/', resource_path('fonts/DroidSansFallback.ttf')),
])->render();
$pdf = app('dompdf.wrapper');
$frames = [];
$pdf->getDomPDF()->setCallbacks([[
'event' => 'end_frame',
'f' => static function ($frame) use (&$frames): void {
if (! $frame instanceof Text) {
return;
}
$isSummary = false;
for ($node = $frame->get_node()->parentNode; $node; $node = $node->parentNode) {
if ($node instanceof DOMElement && $node->getAttribute('class') === 'summary') {
$isSummary = true;
break;
}
}
$frames[] = [
'text' => $frame->get_text(),
'font' => $frame->get_style()->font_family,
'y' => $frame->get_position()['y'],
'summary' => $isSummary,
];
},
]]);
$pdf->setPaper('a4', 'landscape');
$pdf->loadHTML($html, 'UTF-8');
$this->assertStringStartsWith('%PDF-', $pdf->output());
$this->assertNotEmpty($frames);
$characterMaps = [];
foreach ($frames as $frame) {
if (! isset($characterMaps[$frame['font']])) {
$font = Font::load($frame['font'] . '.ttf');
$font->parse();
$characterMaps[$frame['font']] = $font->getUnicodeCharMap();
$font->close();
}
foreach (mb_str_split($frame['text']) as $character) {
if ($character === "\n" || $character === "\r") {
continue;
}
$codepoint = mb_ord($character);
$this->assertNotEmpty(
$characterMaps[$frame['font']][$codepoint] ?? null,
sprintf('Quote PDF would display a missing glyph for U+%04X.', $codepoint)
);
}
}
$ruleFrames = array_values(array_filter($frames, fn ($frame) => $frame['summary']));
$renderedText = implode('', array_column($ruleFrames, 'text'));
$this->assertStringContainsString('计费重 > 0.5kg,费用6.50元。', $renderedText);
$this->assertStringContainsString('轻抛比1:10000。', $renderedText);
$this->assertStringContainsString('保留末行。', $renderedText);
$lineY = [];
foreach ($ruleFrames as $frame) {
foreach (['1', '2', '3'] as $number) {
if (str_starts_with($frame['text'], $number)) {
$lineY[$number] ??= $frame['y'];
}
}
}
$this->assertCount(3, $lineY);
$this->assertGreaterThan($lineY['1'], $lineY['2']);
$this->assertGreaterThan($lineY['2'], $lineY['3']);
}
}