PdfMerger.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\Log;
  4. use Illuminate\Support\Facades\Process;
  5. use Illuminate\Support\Str;
  6. /**
  7. * PDF合并工具类
  8. * 支持pdfunite(生产环境)和qpdf(本地开发)
  9. */
  10. class PdfMerger
  11. {
  12. private string $mergeTool;
  13. private bool $isProduction;
  14. public function __construct()
  15. {
  16. $this->isProduction = app()->environment('production');
  17. $this->mergeTool = $this->detectMergeTool();
  18. }
  19. /**
  20. * 检测系统中可用的PDF合并工具
  21. */
  22. private function detectMergeTool(): string
  23. {
  24. // 生产环境优先使用pdfunite
  25. if ($this->isProduction) {
  26. if ($this->commandExists('pdfunite')) {
  27. return 'pdfunite';
  28. }
  29. }
  30. // 本地开发环境使用qpdf
  31. if ($this->commandExists('qpdf')) {
  32. return 'qpdf';
  33. }
  34. // 备选:尝试pdfunite
  35. if ($this->commandExists('pdfunite')) {
  36. return 'pdfunite';
  37. }
  38. throw new \Exception('未找到可用的PDF合并工具(pdfunite或qpdf)');
  39. }
  40. /**
  41. * 检查命令是否存在
  42. */
  43. private function commandExists(string $command): bool
  44. {
  45. // 【修复】异步环境中PATH可能不完整,尝试绝对路径
  46. $fullPaths = [
  47. "/opt/homebrew/bin/{$command}",
  48. "/usr/bin/{$command}",
  49. "/usr/local/bin/{$command}",
  50. ];
  51. Log::debug("检查命令是否存在: {$command}", [
  52. 'checking_absolute_paths' => $fullPaths
  53. ]);
  54. // 首先尝试绝对路径
  55. foreach ($fullPaths as $path) {
  56. if (file_exists($path) && is_executable($path)) {
  57. Log::debug("找到命令(绝对路径): {$command} -> {$path}");
  58. return true;
  59. }
  60. }
  61. // 如果绝对路径都不行,再尝试which命令
  62. Log::debug("绝对路径未找到,尝试which命令: {$command}");
  63. $output = Process::run("which {$command}");
  64. $result = $output->successful();
  65. Log::debug("which命令结果", [
  66. 'command' => $command,
  67. 'successful' => $result,
  68. 'output' => $output->output(),
  69. 'error' => $output->errorOutput()
  70. ]);
  71. return $result;
  72. }
  73. /**
  74. * 合并多个PDF文件
  75. *
  76. * @param array $pdfPaths PDF文件路径数组
  77. * @param string $outputPath 输出文件路径
  78. * @return bool 合并是否成功
  79. * @throws \Exception
  80. */
  81. public function merge(array $pdfPaths, string $outputPath): bool
  82. {
  83. // 验证输入文件
  84. foreach ($pdfPaths as $path) {
  85. if (!file_exists($path)) {
  86. throw new \Exception("PDF文件不存在: {$path}");
  87. }
  88. }
  89. // 确保输出目录存在
  90. $outputDir = dirname($outputPath);
  91. if (!is_dir($outputDir)) {
  92. mkdir($outputDir, 0755, true);
  93. }
  94. Log::info('开始合并PDF', [
  95. 'tool' => $this->mergeTool,
  96. 'input_count' => count($pdfPaths),
  97. 'output_path' => $outputPath
  98. ]);
  99. try {
  100. switch ($this->mergeTool) {
  101. case 'pdfunite':
  102. return $this->mergeWithPdfunite($pdfPaths, $outputPath);
  103. case 'qpdf':
  104. return $this->mergeWithQpdf($pdfPaths, $outputPath);
  105. default:
  106. throw new \Exception("不支持的合并工具: {$this->mergeTool}");
  107. }
  108. } catch (\Exception $e) {
  109. Log::error('PDF合并失败', [
  110. 'tool' => $this->mergeTool,
  111. 'error' => $e->getMessage(),
  112. 'trace' => $e->getTraceAsString()
  113. ]);
  114. throw $e;
  115. }
  116. }
  117. /**
  118. * 使用pdfunite合并PDF
  119. * pdfunite file1.pdf file2.pdf ... output.pdf
  120. */
  121. private function mergeWithPdfunite(array $pdfPaths, string $outputPath): bool
  122. {
  123. $command = 'pdfunite ' . implode(' ', array_map('escapeshellarg', $pdfPaths)) . ' ' . escapeshellarg($outputPath);
  124. Log::debug('执行pdfunite命令', ['command' => $command]);
  125. $output = Process::run($command);
  126. if ($output->successful()) {
  127. Log::info('pdfunite合并成功', ['output_path' => $outputPath]);
  128. return true;
  129. }
  130. Log::error('pdfunite合并失败', [
  131. 'exit_code' => $output->exitCode(),
  132. 'output' => $output->output(),
  133. 'error' => $output->errorOutput()
  134. ]);
  135. return false;
  136. }
  137. /**
  138. * 使用qpdf合并PDF
  139. * qpdf --empty --pages file1.pdf file2.pdf -- -- output.pdf
  140. */
  141. private function mergeWithQpdf(array $pdfPaths, string $outputPath): bool
  142. {
  143. // 构建qpdf命令
  144. $pagesArg = implode(' ', array_map('escapeshellarg', $pdfPaths));
  145. $command = "qpdf --empty --pages {$pagesArg} -- -- " . escapeshellarg($outputPath);
  146. Log::debug('执行qpdf命令', ['command' => $command]);
  147. $output = Process::run($command);
  148. if ($output->successful()) {
  149. Log::info('qpdf合并成功', ['output_path' => $outputPath]);
  150. return true;
  151. }
  152. Log::error('qpdf合并失败', [
  153. 'exit_code' => $output->exitCode(),
  154. 'output' => $output->output(),
  155. 'error' => $output->errorOutput()
  156. ]);
  157. return false;
  158. }
  159. /**
  160. * 获取当前使用的合并工具
  161. */
  162. public function getMergeTool(): string
  163. {
  164. return $this->mergeTool;
  165. }
  166. /**
  167. * 检查环境是否支持PDF合并
  168. */
  169. public function isSupported(): bool
  170. {
  171. return in_array($this->mergeTool, ['pdfunite', 'qpdf']);
  172. }
  173. }