- Extract shared ID generation + timestamps into CuidEntityTrait used by all entities - Create AbstractAuditSubscriber to deduplicate audit logic across 7 subscribers - Merge per-entity history controllers into single EntityHistoryController - Delete redundant ComposantHistory/MachineHistory/PieceHistory/ProductHistoryController - Add OpenApiDecorator for API documentation customization - Disable failOnDeprecation in PHPUnit (vendor API Platform deprecation) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
140 lines
3.6 KiB
PHP
140 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service;
|
|
|
|
class PdfCompressorService
|
|
{
|
|
private ?bool $qpdfAvailable = null;
|
|
|
|
/**
|
|
* Compress an actual PDF file on disk. Returns metadata or null if no gain.
|
|
*
|
|
* @return null|array{size: int, originalSize: int, saved: int}
|
|
*/
|
|
public function compressFile(string $absolutePath): ?array
|
|
{
|
|
if (!$this->isQpdfAvailable()) {
|
|
return null;
|
|
}
|
|
|
|
if (!file_exists($absolutePath)) {
|
|
return null;
|
|
}
|
|
|
|
$originalSize = filesize($absolutePath);
|
|
if (false === $originalSize) {
|
|
return null;
|
|
}
|
|
|
|
$tempOutput = tempnam(sys_get_temp_dir(), 'pdf_out_');
|
|
|
|
$command = sprintf(
|
|
'qpdf --linearize --object-streams=generate %s %s 2>&1',
|
|
escapeshellarg($absolutePath),
|
|
escapeshellarg($tempOutput)
|
|
);
|
|
|
|
exec($command, $cmdOutput, $returnCode);
|
|
|
|
if (0 !== $returnCode || !file_exists($tempOutput)) {
|
|
@unlink($tempOutput);
|
|
|
|
return null;
|
|
}
|
|
|
|
$compressedSize = filesize($tempOutput);
|
|
if (false === $compressedSize || $compressedSize >= $originalSize) {
|
|
@unlink($tempOutput);
|
|
|
|
return null;
|
|
}
|
|
|
|
if (!rename($tempOutput, $absolutePath)) {
|
|
@unlink($tempOutput);
|
|
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'size' => $compressedSize,
|
|
'originalSize' => $originalSize,
|
|
'saved' => $originalSize - $compressedSize,
|
|
];
|
|
}
|
|
|
|
public function compressBase64Pdf(string $base64Data): ?array
|
|
{
|
|
if (!$this->isQpdfAvailable()) {
|
|
return null;
|
|
}
|
|
|
|
// Remove data URI prefix if present
|
|
$originalBase64 = $base64Data;
|
|
if (str_contains($base64Data, ',')) {
|
|
$base64Data = explode(',', $base64Data, 2)[1];
|
|
}
|
|
|
|
$pdfContent = base64_decode($base64Data, true);
|
|
if (false === $pdfContent) {
|
|
return null;
|
|
}
|
|
|
|
$originalSize = strlen($pdfContent);
|
|
|
|
// Create temp files
|
|
$tempInput = tempnam(sys_get_temp_dir(), 'pdf_in_');
|
|
$tempOutput = tempnam(sys_get_temp_dir(), 'pdf_out_');
|
|
|
|
file_put_contents($tempInput, $pdfContent);
|
|
|
|
// Compress with qpdf (lossless)
|
|
$command = sprintf(
|
|
'qpdf --linearize --object-streams=generate %s %s 2>&1',
|
|
escapeshellarg($tempInput),
|
|
escapeshellarg($tempOutput)
|
|
);
|
|
|
|
exec($command, $cmdOutput, $returnCode);
|
|
|
|
if (0 !== $returnCode || !file_exists($tempOutput)) {
|
|
@unlink($tempInput);
|
|
@unlink($tempOutput);
|
|
|
|
return null;
|
|
}
|
|
|
|
$compressedContent = file_get_contents($tempOutput);
|
|
$compressedSize = strlen($compressedContent);
|
|
|
|
@unlink($tempInput);
|
|
@unlink($tempOutput);
|
|
|
|
// Only return compressed version if it's smaller
|
|
if ($compressedSize >= $originalSize) {
|
|
return null;
|
|
}
|
|
|
|
// Rebuild with data URI prefix
|
|
$newBase64 = 'data:application/pdf;base64,'.base64_encode($compressedContent);
|
|
|
|
return [
|
|
'path' => $newBase64,
|
|
'size' => $compressedSize,
|
|
'originalSize' => $originalSize,
|
|
'saved' => $originalSize - $compressedSize,
|
|
];
|
|
}
|
|
|
|
private function isQpdfAvailable(): bool
|
|
{
|
|
if (null === $this->qpdfAvailable) {
|
|
exec('which qpdf', $qpdfPath, $returnCode);
|
|
$this->qpdfAvailable = 0 === $returnCode;
|
|
}
|
|
|
|
return $this->qpdfAvailable;
|
|
}
|
|
}
|