|
|
@@ -124,9 +124,9 @@ class PdfMerger
|
|
|
try {
|
|
|
switch ($this->mergeTool) {
|
|
|
case 'pdfunite':
|
|
|
- return $this->mergeWithPdfunite($pdfPaths, $outputPath);
|
|
|
+ return $this->mergeWithPdfunite($pdfPaths, $outputPath, null);
|
|
|
case 'qpdf':
|
|
|
- return $this->mergeWithQpdf($pdfPaths, $outputPath);
|
|
|
+ return $this->mergeWithQpdf($pdfPaths, $outputPath, null);
|
|
|
default:
|
|
|
throw new \Exception("不支持的合并工具: {$this->mergeTool}");
|
|
|
}
|
|
|
@@ -140,73 +140,210 @@ class PdfMerger
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前使用的合并工具
|
|
|
+ */
|
|
|
+ public function getMergeTool(): string
|
|
|
+ {
|
|
|
+ return $this->mergeTool;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查环境是否支持PDF合并
|
|
|
+ */
|
|
|
+ public function isSupported(): bool
|
|
|
+ {
|
|
|
+ return in_array($this->mergeTool, ['pdfunite', 'qpdf']);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * 使用pdfunite合并PDF
|
|
|
- * pdfunite file1.pdf file2.pdf ... output.pdf
|
|
|
+ * 【新增】快速合并模式(带进度回调)
|
|
|
+ *
|
|
|
+ * @param array $pdfPaths PDF文件路径数组
|
|
|
+ * @param string $outputPath 输出文件路径
|
|
|
+ * @param callable|null $progressCallback 进度回调函数 (percentage, message) => void
|
|
|
+ * @return bool 合并是否成功
|
|
|
+ * @throws \Exception
|
|
|
*/
|
|
|
- private function mergeWithPdfunite(array $pdfPaths, string $outputPath): bool
|
|
|
+ public function mergeWithProgress(array $pdfPaths, string $outputPath, ?callable $progressCallback = null): bool
|
|
|
+ {
|
|
|
+ // 进度回调:开始
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(0, '开始合并PDF...');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证输入文件
|
|
|
+ foreach ($pdfPaths as $index => $path) {
|
|
|
+ if (!file_exists($path)) {
|
|
|
+ throw new \Exception("PDF文件不存在: {$path}");
|
|
|
+ }
|
|
|
+ // 进度回调:验证文件
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progress = round(($index + 1) / (count($pdfPaths) + 1) * 20, 0); // 前20%用于验证
|
|
|
+ $progressCallback($progress, "验证PDF文件: " . basename($path));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保输出目录存在
|
|
|
+ $outputDir = dirname($outputPath);
|
|
|
+ if (!is_dir($outputDir)) {
|
|
|
+ mkdir($outputDir, 0755, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::info('开始快速合并PDF', [
|
|
|
+ 'tool' => $this->mergeTool,
|
|
|
+ 'input_count' => count($pdfPaths),
|
|
|
+ 'output_path' => $outputPath
|
|
|
+ ]);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 进度回调:开始合并
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(20, "使用 {$this->mergeTool} 开始合并PDF...");
|
|
|
+ }
|
|
|
+
|
|
|
+ switch ($this->mergeTool) {
|
|
|
+ case 'pdfunite':
|
|
|
+ $result = $this->mergeWithPdfunite($pdfPaths, $outputPath, $progressCallback);
|
|
|
+ break;
|
|
|
+ case 'qpdf':
|
|
|
+ $result = $this->mergeWithQpdf($pdfPaths, $outputPath, $progressCallback);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ throw new \Exception("不支持的合并工具: {$this->mergeTool}");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 进度回调:完成
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(100, 'PDF合并完成!');
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('PDF合并失败', [
|
|
|
+ 'tool' => $this->mergeTool,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'trace' => $e->getTraceAsString()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(-1, 'PDF合并失败: ' . $e->getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用pdfunite合并PDF(带进度回调)
|
|
|
+ * 【优化】添加进度反馈
|
|
|
+ */
|
|
|
+ private function mergeWithPdfunite(array $pdfPaths, string $outputPath, ?callable $progressCallback = null): bool
|
|
|
{
|
|
|
$command = 'pdfunite ' . implode(' ', array_map('escapeshellarg', $pdfPaths)) . ' ' . escapeshellarg($outputPath);
|
|
|
|
|
|
Log::debug('执行pdfunite命令', ['command' => $command]);
|
|
|
|
|
|
- $output = Process::run($command);
|
|
|
+ // 【优化】设置超时时间为60秒,避免无限等待
|
|
|
+ $timeout = 60;
|
|
|
+
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(30, '执行pdfunite命令...');
|
|
|
+ }
|
|
|
+
|
|
|
+ $startTime = microtime(true);
|
|
|
+ $output = Process::timeout($timeout)->run($command);
|
|
|
+ $duration = round((microtime(true) - $startTime) * 1000, 2);
|
|
|
+
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(80, '处理PDF合并结果...');
|
|
|
+ }
|
|
|
|
|
|
if ($output->successful()) {
|
|
|
- Log::info('pdfunite合并成功', ['output_path' => $outputPath]);
|
|
|
+ Log::info('pdfunite合并成功', [
|
|
|
+ 'output_path' => $outputPath,
|
|
|
+ 'duration_ms' => $duration,
|
|
|
+ 'file_count' => count($pdfPaths)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(95, "合并完成!耗时 {$duration}ms");
|
|
|
+ }
|
|
|
+
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
Log::error('pdfunite合并失败', [
|
|
|
'exit_code' => $output->exitCode(),
|
|
|
'output' => $output->output(),
|
|
|
- 'error' => $output->errorOutput()
|
|
|
+ 'error' => $output->errorOutput(),
|
|
|
+ 'duration_ms' => $duration,
|
|
|
+ 'timeout_seconds' => $timeout
|
|
|
]);
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 使用qpdf合并PDF
|
|
|
- * qpdf --empty --pages file1.pdf file2.pdf -- -- output.pdf
|
|
|
+ * 使用qpdf合并PDF(带进度回调)
|
|
|
+ * 【修复】qpdf命令格式不正确,缺少页面范围参数
|
|
|
+ * 正确格式:qpdf --empty --pages file1.pdf 1-z,file2.pdf 1-z -- output.pdf
|
|
|
*/
|
|
|
- private function mergeWithQpdf(array $pdfPaths, string $outputPath): bool
|
|
|
+ private function mergeWithQpdf(array $pdfPaths, string $outputPath, ?callable $progressCallback = null): bool
|
|
|
{
|
|
|
- // 构建qpdf命令
|
|
|
- $pagesArg = implode(' ', array_map('escapeshellarg', $pdfPaths));
|
|
|
+ // 【修复】构建正确的qpdf命令
|
|
|
+ // qpdf --empty --pages file1.pdf 1-z,file2.pdf 1-z -- output.pdf
|
|
|
+ // 每个文件都需要指定页面范围(1-z表示从第一页到最后一页)
|
|
|
+
|
|
|
+ $pagesArgs = [];
|
|
|
+ foreach ($pdfPaths as $pdfPath) {
|
|
|
+ $pagesArgs[] = escapeshellarg($pdfPath) . ' 1-z';
|
|
|
+ }
|
|
|
+ $pagesArg = implode(',', $pagesArgs);
|
|
|
+
|
|
|
$command = "qpdf --empty --pages {$pagesArg} -- -- " . escapeshellarg($outputPath);
|
|
|
|
|
|
Log::debug('执行qpdf命令', ['command' => $command]);
|
|
|
|
|
|
- $output = Process::run($command);
|
|
|
+ // 【优化】设置超时时间为60秒,避免无限等待
|
|
|
+ $timeout = 60;
|
|
|
+
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(30, '执行qpdf命令...');
|
|
|
+ }
|
|
|
+
|
|
|
+ $startTime = microtime(true);
|
|
|
+ $output = Process::timeout($timeout)->run($command);
|
|
|
+ $duration = round((microtime(true) - $startTime) * 1000, 2);
|
|
|
+
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(80, '处理PDF合并结果...');
|
|
|
+ }
|
|
|
|
|
|
if ($output->successful()) {
|
|
|
- Log::info('qpdf合并成功', ['output_path' => $outputPath]);
|
|
|
+ Log::info('qpdf合并成功', [
|
|
|
+ 'output_path' => $outputPath,
|
|
|
+ 'duration_ms' => $duration,
|
|
|
+ 'file_count' => count($pdfPaths)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($progressCallback) {
|
|
|
+ $progressCallback(95, "合并完成!耗时 {$duration}ms");
|
|
|
+ }
|
|
|
+
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
Log::error('qpdf合并失败', [
|
|
|
'exit_code' => $output->exitCode(),
|
|
|
'output' => $output->output(),
|
|
|
- 'error' => $output->errorOutput()
|
|
|
+ 'error' => $output->errorOutput(),
|
|
|
+ 'duration_ms' => $duration,
|
|
|
+ 'timeout_seconds' => $timeout,
|
|
|
+ 'corrected_command' => $command // 记录修正后的命令用于调试
|
|
|
]);
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取当前使用的合并工具
|
|
|
- */
|
|
|
- public function getMergeTool(): string
|
|
|
- {
|
|
|
- return $this->mergeTool;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查环境是否支持PDF合并
|
|
|
- */
|
|
|
- public function isSupported(): bool
|
|
|
- {
|
|
|
- return in_array($this->mergeTool, ['pdfunite', 'qpdf']);
|
|
|
- }
|
|
|
}
|