|
@@ -87,20 +87,34 @@ class ExamPdfExportService
|
|
|
|
|
|
|
|
private function buildPdf(string $html): ?string
|
|
private function buildPdf(string $html): ?string
|
|
|
{
|
|
{
|
|
|
- // 使用无头 Chrome 渲染 HTML,保留前端样式并彻底解决大量空白页问题
|
|
|
|
|
$tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_') . '.html';
|
|
$tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_') . '.html';
|
|
|
- $tmpPdf = tempnam(sys_get_temp_dir(), 'exam_pdf_') . '.pdf';
|
|
|
|
|
-
|
|
|
|
|
file_put_contents($tmpHtml, $this->ensureUtf8Html($html));
|
|
file_put_contents($tmpHtml, $this->ensureUtf8Html($html));
|
|
|
|
|
|
|
|
|
|
+ // 先尝试 Chrome
|
|
|
|
|
+ $chromePdf = $this->renderWithChrome($tmpHtml);
|
|
|
|
|
+ if ($chromePdf !== null) {
|
|
|
|
|
+ @unlink($tmpHtml);
|
|
|
|
|
+ return $chromePdf;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Chrome 失败则降级 wkhtmltopdf,尽量保证有输出
|
|
|
|
|
+ $wkPdf = $this->renderWithWkhtml($tmpHtml);
|
|
|
|
|
+ @unlink($tmpHtml);
|
|
|
|
|
+ return $wkPdf;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function renderWithChrome(string $htmlPath): ?string
|
|
|
|
|
+ {
|
|
|
|
|
+ $tmpPdf = tempnam(sys_get_temp_dir(), 'exam_pdf_') . '.pdf';
|
|
|
|
|
+
|
|
|
$chromeBinary = env('PDF_CHROME_BINARY');
|
|
$chromeBinary = env('PDF_CHROME_BINARY');
|
|
|
if (!$chromeBinary) {
|
|
if (!$chromeBinary) {
|
|
|
- // 默认优先 Mac,本地开发;不存在则尝试常见 Linux 路径
|
|
|
|
|
$candidates = [
|
|
$candidates = [
|
|
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
|
|
|
+ '/usr/bin/google-chrome-stable',
|
|
|
|
|
+ '/usr/bin/google-chrome',
|
|
|
'/usr/bin/chromium-browser',
|
|
'/usr/bin/chromium-browser',
|
|
|
'/usr/bin/chromium',
|
|
'/usr/bin/chromium',
|
|
|
- '/usr/bin/google-chrome',
|
|
|
|
|
];
|
|
];
|
|
|
foreach ($candidates as $path) {
|
|
foreach ($candidates as $path) {
|
|
|
if (is_file($path) && is_executable($path)) {
|
|
if (is_file($path) && is_executable($path)) {
|
|
@@ -111,16 +125,17 @@ class ExamPdfExportService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!$chromeBinary) {
|
|
if (!$chromeBinary) {
|
|
|
- Log::error('ExamPdfExportService: 未找到可用的 Chrome/Chromium 可执行文件');
|
|
|
|
|
|
|
+ Log::warning('ExamPdfExportService: 未找到可用的 Chrome/Chromium,可尝试 wkhtmltopdf');
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
$process = new Process([
|
|
$process = new Process([
|
|
|
$chromeBinary,
|
|
$chromeBinary,
|
|
|
'--headless',
|
|
'--headless',
|
|
|
'--disable-gpu',
|
|
'--disable-gpu',
|
|
|
'--no-sandbox',
|
|
'--no-sandbox',
|
|
|
'--disable-setuid-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
|
- '--disable-dev-shm-usage', // 避免容器内 /dev/shm 过小导致崩溃
|
|
|
|
|
|
|
+ '--disable-dev-shm-usage',
|
|
|
'--no-zygote',
|
|
'--no-zygote',
|
|
|
'--single-process',
|
|
'--single-process',
|
|
|
'--disable-features=VizDisplayCompositor',
|
|
'--disable-features=VizDisplayCompositor',
|
|
@@ -128,9 +143,9 @@ class ExamPdfExportService
|
|
|
'--print-to-pdf=' . $tmpPdf,
|
|
'--print-to-pdf=' . $tmpPdf,
|
|
|
'--print-to-pdf-no-header',
|
|
'--print-to-pdf-no-header',
|
|
|
'--allow-file-access-from-files',
|
|
'--allow-file-access-from-files',
|
|
|
- 'file://' . $tmpHtml,
|
|
|
|
|
|
|
+ 'file://' . $htmlPath,
|
|
|
]);
|
|
]);
|
|
|
- $process->setTimeout(30);
|
|
|
|
|
|
|
+ $process->setTimeout(40);
|
|
|
$process->run();
|
|
$process->run();
|
|
|
|
|
|
|
|
if (!$process->isSuccessful() || !file_exists($tmpPdf)) {
|
|
if (!$process->isSuccessful() || !file_exists($tmpPdf)) {
|
|
@@ -140,15 +155,61 @@ class ExamPdfExportService
|
|
|
'error' => $process->getErrorOutput(),
|
|
'error' => $process->getErrorOutput(),
|
|
|
'output' => $process->getOutput(),
|
|
'output' => $process->getOutput(),
|
|
|
]);
|
|
]);
|
|
|
- @unlink($tmpHtml);
|
|
|
|
|
@unlink($tmpPdf);
|
|
@unlink($tmpPdf);
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$pdfBinary = file_get_contents($tmpPdf);
|
|
$pdfBinary = file_get_contents($tmpPdf);
|
|
|
- @unlink($tmpHtml);
|
|
|
|
|
@unlink($tmpPdf);
|
|
@unlink($tmpPdf);
|
|
|
|
|
+ return $pdfBinary ?: null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function renderWithWkhtml(string $htmlPath): ?string
|
|
|
|
|
+ {
|
|
|
|
|
+ $tmpPdf = tempnam(sys_get_temp_dir(), 'exam_pdf_wk_') . '.pdf';
|
|
|
|
|
+
|
|
|
|
|
+ $wkBinary = env('PDF_WKHTML_BINARY');
|
|
|
|
|
+ if (!$wkBinary) {
|
|
|
|
|
+ $candidates = [
|
|
|
|
|
+ '/usr/bin/wkhtmltopdf',
|
|
|
|
|
+ '/usr/local/bin/wkhtmltopdf',
|
|
|
|
|
+ ];
|
|
|
|
|
+ foreach ($candidates as $path) {
|
|
|
|
|
+ if (is_file($path) && is_executable($path)) {
|
|
|
|
|
+ $wkBinary = $path;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!$wkBinary) {
|
|
|
|
|
+ Log::error('ExamPdfExportService: 未找到可用的 Chrome,且 wkhtmltopdf 未安装,无法导出');
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $process = new Process([
|
|
|
|
|
+ $wkBinary,
|
|
|
|
|
+ '--disable-smart-shrinking',
|
|
|
|
|
+ '--encoding', 'utf-8',
|
|
|
|
|
+ $htmlPath,
|
|
|
|
|
+ $tmpPdf,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $process->setTimeout(40);
|
|
|
|
|
+ $process->run();
|
|
|
|
|
|
|
|
|
|
+ if (!$process->isSuccessful() || !file_exists($tmpPdf)) {
|
|
|
|
|
+ Log::error('ExamPdfExportService: wkhtmltopdf 渲染失败', [
|
|
|
|
|
+ 'cmd' => implode(' ', (array) $process->getCommandLine()),
|
|
|
|
|
+ 'exit_code' => $process->getExitCode(),
|
|
|
|
|
+ 'error' => $process->getErrorOutput(),
|
|
|
|
|
+ 'output' => $process->getOutput(),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ @unlink($tmpPdf);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $pdfBinary = file_get_contents($tmpPdf);
|
|
|
|
|
+ @unlink($tmpPdf);
|
|
|
return $pdfBinary ?: null;
|
|
return $pdfBinary ?: null;
|
|
|
}
|
|
}
|
|
|
|
|
|