| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- <?php
- namespace App\Services;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Facades\Process;
- use Illuminate\Support\Str;
- /**
- * PDF合并工具类
- * 支持pdfunite(生产环境)和qpdf(本地开发)
- */
- class PdfMerger
- {
- private string $mergeTool;
- private bool $isProduction;
- public function __construct()
- {
- $this->isProduction = app()->environment('production');
- $this->mergeTool = $this->detectMergeTool();
- }
- /**
- * 检测系统中可用的PDF合并工具
- */
- private function detectMergeTool(): string
- {
- // 生产环境优先使用pdfunite
- if ($this->isProduction) {
- if ($this->commandExists('pdfunite')) {
- return 'pdfunite';
- }
- }
- // 本地开发环境使用qpdf
- if ($this->commandExists('qpdf')) {
- return 'qpdf';
- }
- // 备选:尝试pdfunite
- if ($this->commandExists('pdfunite')) {
- return 'pdfunite';
- }
- throw new \Exception('未找到可用的PDF合并工具(pdfunite或qpdf)');
- }
- /**
- * 检查命令是否存在
- */
- private function commandExists(string $command): bool
- {
- // 【修复】异步环境中PATH可能不完整,尝试绝对路径
- $fullPaths = [
- // 本地开发路径
- "/opt/homebrew/bin/{$command}",
- "/usr/local/bin/{$command}",
- // 系统路径
- "/usr/bin/{$command}",
- "/usr/sbin/{$command}",
- // Docker/Laravel 容器常见路径
- "/bin/{$command}",
- "/sbin/{$command}",
- "/usr/local/sbin/{$command}",
- ];
- Log::debug("检查命令是否存在: {$command}", [
- 'checking_absolute_paths' => $fullPaths
- ]);
- // 首先尝试绝对路径
- foreach ($fullPaths as $path) {
- if (file_exists($path) && is_executable($path)) {
- Log::debug("找到命令(绝对路径): {$command} -> {$path}");
- return true;
- }
- }
- // 如果绝对路径都不行,再尝试which命令
- Log::debug("绝对路径未找到,尝试which命令: {$command}");
- $output = Process::run("which {$command}");
- $result = $output->successful();
- Log::debug("which命令结果", [
- 'command' => $command,
- 'successful' => $result,
- 'output' => $output->output(),
- 'error' => $output->errorOutput()
- ]);
- return $result;
- }
- /**
- * 合并多个PDF文件
- *
- * @param array $pdfPaths PDF文件路径数组
- * @param string $outputPath 输出文件路径
- * @return bool 合并是否成功
- * @throws \Exception
- */
- public function merge(array $pdfPaths, string $outputPath): bool
- {
- // 验证输入文件
- foreach ($pdfPaths as $path) {
- if (!file_exists($path)) {
- throw new \Exception("PDF文件不存在: {$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 {
- switch ($this->mergeTool) {
- case 'pdfunite':
- return $this->mergeWithPdfunite($pdfPaths, $outputPath, null);
- case 'qpdf':
- return $this->mergeWithQpdf($pdfPaths, $outputPath, null);
- default:
- throw new \Exception("不支持的合并工具: {$this->mergeTool}");
- }
- } catch (\Exception $e) {
- Log::error('PDF合并失败', [
- 'tool' => $this->mergeTool,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- throw $e;
- }
- }
- /**
- * 获取当前使用的合并工具
- */
- public function getMergeTool(): string
- {
- return $this->mergeTool;
- }
- /**
- * 检查环境是否支持PDF合并
- */
- public function isSupported(): bool
- {
- return in_array($this->mergeTool, ['pdfunite', 'qpdf']);
- }
- /**
- * 【新增】快速合并模式(带进度回调)
- *
- * @param array $pdfPaths PDF文件路径数组
- * @param string $outputPath 输出文件路径
- * @param callable|null $progressCallback 进度回调函数 (percentage, message) => void
- * @return bool 合并是否成功
- * @throws \Exception
- */
- 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]);
- // 【优化】设置超时时间为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,
- '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(),
- 'duration_ms' => $duration,
- 'timeout_seconds' => $timeout
- ]);
- return false;
- }
- /**
- * 使用qpdf合并PDF(带进度回调)
- * 【修复】qpdf命令格式问题 - 使用正确的页面范围语法
- * 正确格式:qpdf --empty --pages file1.pdf,file2.pdf -- output.pdf
- * qpdf会自动合并所有页面,无需手动指定页面范围
- */
- private function mergeWithQpdf(array $pdfPaths, string $outputPath, ?callable $progressCallback = null): bool
- {
- // 【修复】构建正确的qpdf命令
- // qpdf --empty --pages file1.pdf,file2.pdf -- output.pdf
- // 注意:qpdf不需要手动指定页面范围,会自动合并所有页面
- $pagesArg = implode(',', array_map('escapeshellarg', $pdfPaths));
- $command = "qpdf --empty --pages {$pagesArg} -- -- " . escapeshellarg($outputPath);
- Log::debug('执行qpdf命令', ['command' => $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,
- '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(),
- 'duration_ms' => $duration,
- 'timeout_seconds' => $timeout,
- 'command' => $command,
- 'note' => 'qpdf会自动合并所有页面,不需要指定页面范围'
- ]);
- return false;
- }
- }
|