From dffc01dc51efd6b2b159727f8bafe09670b3f420 Mon Sep 17 00:00:00 2001 From: limeng <1131177064@qq.com> Date: Sat, 7 Nov 2026 18:41:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8A=A5=E4=BB=B7?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E5=8D=95=20PDF=20=E6=96=B9=E6=A1=86=E4=B9=B1?= =?UTF-8?q?=E7=A0=81=20(#993)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/pdf/courier-quote.blade.php | 8 +- tests/Unit/CourierQuotePdfWhitespaceTest.php | 99 +++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/CourierQuotePdfWhitespaceTest.php diff --git a/resources/views/business/pdf/courier-quote.blade.php b/resources/views/business/pdf/courier-quote.blade.php index 82bd66ec..58071637 100644 --- a/resources/views/business/pdf/courier-quote.blade.php +++ b/resources/views/business/pdf/courier-quote.blade.php @@ -43,7 +43,9 @@ @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 @@

报价确认单

-

{!! $mixedText($version->carrier_name) !!} · {!! $mixedText($version->quote_type) !!}

+

{!! $mixedText($version->carrier_name) !!} · {!! $mixedText($version->quote_type) !!}

不可覆盖版本:{!! $mixedText($version->version_code) !!}
@@ -140,7 +142,7 @@

签署确认

报价版本:{!! $mixedText($version->version_code) !!} 快递公司:{!! $mixedText($version->carrier_name) !!} 执行日期:{!! $mixedText($version->effective_date?->toDateString()) !!}
乙方(快递方)签章:{!! $mixedText($version->carrier_name) !!}
签署日期:{!! $mixedText($signatureDate) !!}
-
生成时间:{!! $mixedText($generatedAt->format('Y-m-d H:i:s')) !!} · 版本 {!! $mixedText($version->version_code) !!}
+
生成时间:{!! $mixedText($generatedAt->format('Y-m-d H:i:s')) !!} · 版本 {!! $mixedText($version->version_code) !!}
diff --git a/tests/Unit/CourierQuotePdfWhitespaceTest.php b/tests/Unit/CourierQuotePdfWhitespaceTest.php new file mode 100644 index 00000000..d8b93cf4 --- /dev/null +++ b/tests/Unit/CourierQuotePdfWhitespaceTest.php @@ -0,0 +1,99 @@ + 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']); + } +}