Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4acc8d1c01 | ||
|
|
49ff15f18d | ||
|
|
7a02617d48 | ||
|
|
e52eef0491 | ||
|
|
a5118305d3 |
29
README.md
29
README.md
@@ -44,6 +44,35 @@ make dev-nuxt
|
|||||||
```
|
```
|
||||||
Le front sera accessible sur http://localhost:3000
|
Le front sera accessible sur http://localhost:3000
|
||||||
|
|
||||||
|
## Compression automatique des PDFs
|
||||||
|
|
||||||
|
Les documents PDF uploadés sont automatiquement compressés sans perte de qualité grâce à **qpdf**.
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
```bash
|
||||||
|
# Installation de qpdf (outil système)
|
||||||
|
sudo apt install qpdf
|
||||||
|
|
||||||
|
# Ou dans Docker
|
||||||
|
docker exec -it php-inventory-apache apt update && apt install -y qpdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fonctionnement
|
||||||
|
- À chaque upload de PDF, le système compresse automatiquement le fichier
|
||||||
|
- Compression lossless (sans perte de qualité)
|
||||||
|
- Le PDF est compressé uniquement si la taille diminue
|
||||||
|
- Si qpdf n'est pas installé, le système fonctionne normalement sans compression
|
||||||
|
|
||||||
|
### Compresser les PDFs existants
|
||||||
|
Pour compresser tous les PDFs déjà en base :
|
||||||
|
```bash
|
||||||
|
# Voir ce qui serait compressé (dry-run)
|
||||||
|
php bin/console app:compress-pdf --dry-run
|
||||||
|
|
||||||
|
# Compresser tous les PDFs
|
||||||
|
php bin/console app:compress-pdf
|
||||||
|
```
|
||||||
|
|
||||||
## Commandes utiles
|
## Commandes utiles
|
||||||
Pour restart le container
|
Pour restart le container
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
api_platform:
|
api_platform:
|
||||||
title: Hello API Platform
|
title: Hello API Platform
|
||||||
version: 1.1.0
|
version: 1.1.1
|
||||||
defaults:
|
defaults:
|
||||||
stateless: false
|
stateless: false
|
||||||
cache_headers:
|
cache_headers:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
wget \
|
wget \
|
||||||
git \
|
git \
|
||||||
unzip \
|
unzip \
|
||||||
|
qpdf \
|
||||||
&& docker-php-ext-install -j$(nproc) \
|
&& docker-php-ext-install -j$(nproc) \
|
||||||
intl \
|
intl \
|
||||||
zip \
|
zip \
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ if ! git diff --quiet --exit-code || ! git diff --cached --quiet --exit-code; th
|
|||||||
echo
|
echo
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "chore(release): prepare v$new_version"
|
git commit -m "chore(release) : prepare v$new_version"
|
||||||
else
|
else
|
||||||
echo -e "${RED}Erreur:${NC} Veuillez d'abord commiter les changements du submodule."
|
echo -e "${RED}Erreur:${NC} Veuillez d'abord commiter les changements du submodule."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -168,7 +168,7 @@ sed -i "s/version: .*/version: $new_version/" "$API_PLATFORM_FILE"
|
|||||||
# ===========================================
|
# ===========================================
|
||||||
echo -e "${BLUE}[5/6]${NC} Création du commit principal..."
|
echo -e "${BLUE}[5/6]${NC} Création du commit principal..."
|
||||||
git add "$VERSION_FILE" "$API_PLATFORM_FILE" "$FRONTEND_DIR"
|
git add "$VERSION_FILE" "$API_PLATFORM_FILE" "$FRONTEND_DIR"
|
||||||
git commit -m "chore(release): v$new_version"
|
git commit -m "chore(release) : v$new_version"
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# ÉTAPE 6 : Tag principal
|
# ÉTAPE 6 : Tag principal
|
||||||
|
|||||||
175
src/Command/CompressPdfCommand.php
Normal file
175
src/Command/CompressPdfCommand.php
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Repository\DocumentRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:compress-pdf',
|
||||||
|
description: 'Compress all PDF documents stored in database without quality loss',
|
||||||
|
)]
|
||||||
|
class CompressPdfCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DocumentRepository $documentRepository,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be compressed without actually doing it')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$dryRun = $input->getOption('dry-run');
|
||||||
|
|
||||||
|
// Check if qpdf is installed
|
||||||
|
exec('which qpdf', $qpdfPath, $returnCode);
|
||||||
|
if (0 !== $returnCode) {
|
||||||
|
$io->error('qpdf is not installed. Run: sudo apt install qpdf');
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['mimeType' => 'application/pdf']);
|
||||||
|
|
||||||
|
if (empty($documents)) {
|
||||||
|
$io->info('No PDF documents found.');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->title('PDF Compression');
|
||||||
|
$io->text(sprintf('Found %d PDF documents', count($documents)));
|
||||||
|
|
||||||
|
$totalSaved = 0;
|
||||||
|
$compressed = 0;
|
||||||
|
|
||||||
|
foreach ($documents as $document) {
|
||||||
|
$base64Data = $document->getPath();
|
||||||
|
|
||||||
|
// Remove data URI prefix if present
|
||||||
|
if (str_contains($base64Data, ',')) {
|
||||||
|
$base64Data = explode(',', $base64Data, 2)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdfContent = base64_decode($base64Data, true);
|
||||||
|
if (false === $pdfContent) {
|
||||||
|
$io->warning(sprintf('Failed to decode document: %s', $document->getName()));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalSize = strlen($pdfContent);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$io->text(sprintf(
|
||||||
|
' [DRY-RUN] Would compress: %s (%s)',
|
||||||
|
$document->getName(),
|
||||||
|
$this->formatBytes($originalSize)
|
||||||
|
));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)) {
|
||||||
|
$io->warning(sprintf('Failed to compress: %s', $document->getName()));
|
||||||
|
@unlink($tempInput);
|
||||||
|
@unlink($tempOutput);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$compressedContent = file_get_contents($tempOutput);
|
||||||
|
$compressedSize = strlen($compressedContent);
|
||||||
|
|
||||||
|
// Only update if we actually saved space
|
||||||
|
if ($compressedSize < $originalSize) {
|
||||||
|
$saved = $originalSize - $compressedSize;
|
||||||
|
$totalSaved += $saved;
|
||||||
|
++$compressed;
|
||||||
|
|
||||||
|
// Rebuild base64 with data URI prefix
|
||||||
|
$newBase64 = 'data:application/pdf;base64,'.base64_encode($compressedContent);
|
||||||
|
$document->setPath($newBase64);
|
||||||
|
$document->setSize($compressedSize);
|
||||||
|
|
||||||
|
$io->text(sprintf(
|
||||||
|
' ✓ %s: %s → %s (-%s, -%.1f%%)',
|
||||||
|
$document->getName(),
|
||||||
|
$this->formatBytes($originalSize),
|
||||||
|
$this->formatBytes($compressedSize),
|
||||||
|
$this->formatBytes($saved),
|
||||||
|
($saved / $originalSize) * 100
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$io->text(sprintf(
|
||||||
|
' - %s: Already optimal (%s)',
|
||||||
|
$document->getName(),
|
||||||
|
$this->formatBytes($originalSize)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@unlink($tempInput);
|
||||||
|
@unlink($tempOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dryRun && $compressed > 0) {
|
||||||
|
$this->em->flush();
|
||||||
|
$io->success(sprintf(
|
||||||
|
'Compressed %d/%d PDFs. Total space saved: %s',
|
||||||
|
$compressed,
|
||||||
|
count($documents),
|
||||||
|
$this->formatBytes($totalSaved)
|
||||||
|
));
|
||||||
|
} elseif ($dryRun) {
|
||||||
|
$io->info('Dry run completed. No changes made.');
|
||||||
|
} else {
|
||||||
|
$io->info('No PDFs needed compression.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$i = 0;
|
||||||
|
while ($bytes >= 1024 && $i < count($units) - 1) {
|
||||||
|
$bytes /= 1024;
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($bytes, 2).' '.$units[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/EventListener/DocumentPdfCompressorListener.php
Normal file
54
src/EventListener/DocumentPdfCompressorListener.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventListener;
|
||||||
|
|
||||||
|
use App\Entity\Document;
|
||||||
|
use App\Service\PdfCompressorService;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
#[AsEntityListener(event: Events::prePersist, method: 'prePersist', entity: Document::class)]
|
||||||
|
#[AsEntityListener(event: Events::preUpdate, method: 'preUpdate', entity: Document::class)]
|
||||||
|
class DocumentPdfCompressorListener
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly PdfCompressorService $pdfCompressor,
|
||||||
|
private readonly ?LoggerInterface $logger = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function prePersist(Document $document): void
|
||||||
|
{
|
||||||
|
$this->compressIfPdf($document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preUpdate(Document $document): void
|
||||||
|
{
|
||||||
|
$this->compressIfPdf($document);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function compressIfPdf(Document $document): void
|
||||||
|
{
|
||||||
|
if ('application/pdf' !== $document->getMimeType()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->pdfCompressor->compressBase64Pdf($document->getPath());
|
||||||
|
|
||||||
|
if (null === $result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->setPath($result['path']);
|
||||||
|
$document->setSize($result['size']);
|
||||||
|
|
||||||
|
$this->logger?->info('PDF compressed', [
|
||||||
|
'document' => $document->getName(),
|
||||||
|
'originalSize' => $result['originalSize'],
|
||||||
|
'compressedSize' => $result['size'],
|
||||||
|
'saved' => $result['saved'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/Service/PdfCompressorService.php
Normal file
73
src/Service/PdfCompressorService.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
class PdfCompressorService
|
||||||
|
{
|
||||||
|
public function compressBase64Pdf(string $base64Data): ?array
|
||||||
|
{
|
||||||
|
// Check if qpdf is available
|
||||||
|
exec('which qpdf', $qpdfPath, $returnCode);
|
||||||
|
if (0 !== $returnCode) {
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user