PdfMerger.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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. // 本地开发路径
  48. "/opt/homebrew/bin/{$command}",
  49. "/usr/local/bin/{$command}",
  50. // 系统路径
  51. "/usr/bin/{$command}",
  52. "/usr/sbin/{$command}",
  53. // Docker/Laravel 容器常见路径
  54. "/bin/{$command}",
  55. "/sbin/{$command}",
  56. "/usr/local/sbin/{$command}",
  57. ];
  58. Log::debug("检查命令是否存在: {$command}", [
  59. 'checking_absolute_paths' => $fullPaths
  60. ]);
  61. // 首先尝试绝对路径
  62. foreach ($fullPaths as $path) {
  63. if (file_exists($path) && is_executable($path)) {
  64. Log::debug("找到命令(绝对路径): {$command} -> {$path}");
  65. return true;
  66. }
  67. }
  68. // 如果绝对路径都不行,再尝试which命令
  69. Log::debug("绝对路径未找到,尝试which命令: {$command}");
  70. $output = Process::run("which {$command}");
  71. $result = $output->successful();
  72. Log::debug("which命令结果", [
  73. 'command' => $command,
  74. 'successful' => $result,
  75. 'output' => $output->output(),
  76. 'error' => $output->errorOutput()
  77. ]);
  78. return $result;
  79. }
  80. /**
  81. * 合并多个PDF文件
  82. *
  83. * @param array $pdfPaths PDF文件路径数组
  84. * @param string $outputPath 输出文件路径
  85. * @return bool 合并是否成功
  86. * @throws \Exception
  87. */
  88. public function merge(array $pdfPaths, string $outputPath): bool
  89. {
  90. // 验证输入文件
  91. foreach ($pdfPaths as $path) {
  92. if (!file_exists($path)) {
  93. throw new \Exception("PDF文件不存在: {$path}");
  94. }
  95. }
  96. // 确保输出目录存在
  97. $outputDir = dirname($outputPath);
  98. if (!is_dir($outputDir)) {
  99. mkdir($outputDir, 0755, true);
  100. }
  101. Log::info('开始合并PDF', [
  102. 'tool' => $this->mergeTool,
  103. 'input_count' => count($pdfPaths),
  104. 'output_path' => $outputPath
  105. ]);
  106. try {
  107. switch ($this->mergeTool) {
  108. case 'pdfunite':
  109. return $this->mergeWithPdfunite($pdfPaths, $outputPath);
  110. case 'qpdf':
  111. return $this->mergeWithQpdf($pdfPaths, $outputPath);
  112. default:
  113. throw new \Exception("不支持的合并工具: {$this->mergeTool}");
  114. }
  115. } catch (\Exception $e) {
  116. Log::error('PDF合并失败', [
  117. 'tool' => $this->mergeTool,
  118. 'error' => $e->getMessage(),
  119. 'trace' => $e->getTraceAsString()
  120. ]);
  121. throw $e;
  122. }
  123. }
  124. /**
  125. * 使用pdfunite合并PDF
  126. * pdfunite file1.pdf file2.pdf ... output.pdf
  127. */
  128. private function mergeWithPdfunite(array $pdfPaths, string $outputPath): bool
  129. {
  130. $command = 'pdfunite ' . implode(' ', array_map('escapeshellarg', $pdfPaths)) . ' ' . escapeshellarg($outputPath);
  131. Log::debug('执行pdfunite命令', ['command' => $command]);
  132. $output = Process::run($command);
  133. if ($output->successful()) {
  134. Log::info('pdfunite合并成功', ['output_path' => $outputPath]);
  135. return true;
  136. }
  137. Log::error('pdfunite合并失败', [
  138. 'exit_code' => $output->exitCode(),
  139. 'output' => $output->output(),
  140. 'error' => $output->errorOutput()
  141. ]);
  142. return false;
  143. }
  144. /**
  145. * 使用qpdf合并PDF
  146. * qpdf --empty --pages file1.pdf file2.pdf -- -- output.pdf
  147. */
  148. private function mergeWithQpdf(array $pdfPaths, string $outputPath): bool
  149. {
  150. // 构建qpdf命令
  151. $pagesArg = implode(' ', array_map('escapeshellarg', $pdfPaths));
  152. $command = "qpdf --empty --pages {$pagesArg} -- -- " . escapeshellarg($outputPath);
  153. Log::debug('执行qpdf命令', ['command' => $command]);
  154. $output = Process::run($command);
  155. if ($output->successful()) {
  156. Log::info('qpdf合并成功', ['output_path' => $outputPath]);
  157. return true;
  158. }
  159. Log::error('qpdf合并失败', [
  160. 'exit_code' => $output->exitCode(),
  161. 'output' => $output->output(),
  162. 'error' => $output->errorOutput()
  163. ]);
  164. return false;
  165. }
  166. /**
  167. * 获取当前使用的合并工具
  168. */
  169. public function getMergeTool(): string
  170. {
  171. return $this->mergeTool;
  172. }
  173. /**
  174. * 检查环境是否支持PDF合并
  175. */
  176. public function isSupported(): bool
  177. {
  178. return in_array($this->mergeTool, ['pdfunite', 'qpdf']);
  179. }
  180. }