修复合作合同 PDF 方框、文字重叠与签字区分页 (#290)
This commit is contained in:
parent
107c883eb2
commit
37d18612a6
@ -3,6 +3,9 @@
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Business\CourierAgreementSignature;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
use Dompdf\FrameReflower\Text;
|
||||
use Illuminate\Support\Carbon;
|
||||
use RuntimeException;
|
||||
|
||||
@ -29,7 +32,16 @@ final class CourierAgreementPdf
|
||||
$pdf = app('dompdf.wrapper');
|
||||
$pdf->setPaper('a4', 'portrait');
|
||||
$pdf->loadHTML($html, 'UTF-8');
|
||||
$bytes = $pdf->output();
|
||||
$this->applyLatinFont($pdf->getDomPDF()->getDom());
|
||||
// Dompdf otherwise treats uninterrupted Chinese text as one word,
|
||||
// moving a whole clause to the next line after its Latin number.
|
||||
$wordBreakPattern = Text::$_wordbreak_pattern;
|
||||
Text::$_wordbreak_pattern = '/([^\S\xA0\x{202F}\x{2007}\n]+|\R|\-+|\xAD+|(?<=[\p{Han}\x{3000}-\x{303F}\x{FF00}-\x{FFEF}])(?![,。、;:?!)】》]))/u';
|
||||
try {
|
||||
$bytes = $pdf->output();
|
||||
} finally {
|
||||
Text::$_wordbreak_pattern = $wordBreakPattern;
|
||||
}
|
||||
|
||||
if (! is_string($bytes) || ! str_starts_with($bytes, '%PDF-')) {
|
||||
throw new RuntimeException('协议 PDF 生成失败。');
|
||||
@ -38,6 +50,38 @@ final class CourierAgreementPdf
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
private function applyLatinFont(DOMDocument $document): void
|
||||
{
|
||||
// DroidSansFallback has no ASCII or typographic quote glyphs. Static
|
||||
// clauses need the Latin font too, or glyph widths and layout diverge.
|
||||
$nodes = (new DOMXPath($document))->query(
|
||||
'//body//text()[not(ancestor::style or ancestor::script or ancestor::*[contains(concat(" ", normalize-space(@class), " "), " latin ")])]'
|
||||
);
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
if (preg_match('/[\x21-\x7E\x{2018}-\x{201F}]/u', $node->nodeValue) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = preg_split('/([\x20-\x7E\x{2018}-\x{201F}]+)/u', $node->nodeValue, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
||||
$fragment = $document->createDocumentFragment();
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$text = $document->createTextNode($part);
|
||||
if (preg_match('/^[\x20-\x7E\x{2018}-\x{201F}]+$/u', $part) === 1) {
|
||||
$span = $document->createElement('span');
|
||||
$span->setAttribute('class', 'latin');
|
||||
$span->appendChild($text);
|
||||
$fragment->appendChild($span);
|
||||
} else {
|
||||
$fragment->appendChild($text);
|
||||
}
|
||||
}
|
||||
|
||||
$node->parentNode->replaceChild($fragment, $node);
|
||||
}
|
||||
}
|
||||
|
||||
private function title(CourierAgreementSignature $signature): string
|
||||
{
|
||||
return match ($signature->document_type) {
|
||||
|
||||
@ -8,13 +8,13 @@
|
||||
body { font-family: agreement-fallback, sans-serif; font-size: 13px; font-weight: 400; line-height: 1.8; color: #171717; }
|
||||
.latin { font-family: "DejaVu Sans", sans-serif; }
|
||||
h1 { text-align: center; font-size: 20px; margin: 0 0 24px; }
|
||||
h2 { font-size: 14px; margin: 18px 0 8px; }
|
||||
h4 { font-size: 14px; margin: 18px 0 8px; }
|
||||
h2 { font-size: 14px; margin: 18px 0 8px; page-break-after: avoid; }
|
||||
h4 { font-size: 14px; margin: 18px 0 8px; page-break-after: avoid; }
|
||||
p { margin: 0 0 12px; overflow-wrap: anywhere; word-break: break-all; }
|
||||
.meta { color: #595959; font-size: 11px; text-align: right; margin-bottom: 20px; }
|
||||
.account { margin: 14px 0 20px 28px; }
|
||||
.account div { margin: 6px 0; }
|
||||
.sign { margin-top: 36px; text-align: right; }
|
||||
.sign { margin-top: 36px; text-align: right; page-break-inside: avoid; }
|
||||
.sign p { margin: 8px 0; }
|
||||
.list { margin-top: 0; }
|
||||
.page { page-break-after: always; }
|
||||
|
||||
@ -8,6 +8,9 @@ use App\Models\Business\CourierAgreementSignature;
|
||||
use App\Models\Business\CourierQuoteChange;
|
||||
use App\Models\Business\CourierQuoteSignature;
|
||||
use App\Models\Business\CourierQuoteVersion;
|
||||
use App\Support\CourierAgreementPdf;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
use Illuminate\Http\Client\Request as ClientRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
@ -360,6 +363,55 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
|
||||
});
|
||||
}
|
||||
|
||||
public function test_cooperation_pdf_preserves_clauses_and_uses_a_supported_font_for_static_numbers(): void
|
||||
{
|
||||
$payload = $this->cooperationContractPayload('cooperation-font');
|
||||
$payload['signature_date'] = '2026-10-10';
|
||||
$signature = new CourierAgreementSignature([
|
||||
'business_no' => 'AGR-FONT-REGRESSION',
|
||||
'template_code' => 'TPL-COOPERATION-CONTRACT-V1',
|
||||
'document_type' => 'cooperation-contract',
|
||||
'payload' => $payload,
|
||||
]);
|
||||
$html = view('business.pdf.courier-agreement', [
|
||||
'signature' => $signature,
|
||||
'type' => $signature->document_type,
|
||||
'title' => '服务协议',
|
||||
'payload' => $payload,
|
||||
'signatureDate' => '2026年10月10日',
|
||||
'fontPath' => str_replace('\\', '/', resource_path('fonts/DroidSansFallback.ttf')),
|
||||
])->render();
|
||||
$original = new DOMDocument();
|
||||
$original->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR);
|
||||
$pdf = app('dompdf.wrapper');
|
||||
$document = null;
|
||||
$wrapper = \Mockery::mock($pdf);
|
||||
$wrapper->shouldReceive('output')->once()->andReturnUsing(function () use ($pdf, &$document): string {
|
||||
$document = clone $pdf->getDomPDF()->getDom();
|
||||
|
||||
return $pdf->output();
|
||||
});
|
||||
$this->app->instance('dompdf.wrapper', $wrapper);
|
||||
|
||||
$wordBreakPattern = \Dompdf\FrameReflower\Text::$_wordbreak_pattern;
|
||||
$bytes = app(CourierAgreementPdf::class)->render($signature);
|
||||
|
||||
$this->assertStringStartsWith('%PDF-', $bytes);
|
||||
$this->assertSame($wordBreakPattern, \Dompdf\FrameReflower\Text::$_wordbreak_pattern);
|
||||
$text = $document->getElementsByTagName('body')->item(0)->textContent;
|
||||
$this->assertSame(
|
||||
preg_replace('/\s+/u', '', $original->getElementsByTagName('body')->item(0)->textContent),
|
||||
preg_replace('/\s+/u', '', $text),
|
||||
);
|
||||
$this->assertStringContainsString('4.6.1、针对甲方客服录入', $text);
|
||||
$this->assertStringContainsString('95%', $text);
|
||||
$this->assertStringContainsString('5000元/次', $text);
|
||||
$xpath = new DOMXPath($document);
|
||||
foreach ($xpath->query('//body//text()[not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " latin ")])]') as $node) {
|
||||
$this->assertDoesNotMatchRegularExpression('/[\x21-\x7E\x{2018}-\x{201F}]/u', $node->nodeValue);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_cooperation_contract_uses_the_persons_real_name_instead_of_the_courier_name(): void
|
||||
{
|
||||
$this->fakeEsign();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user