Files
AggloSign/backend/app/Signature/ImageLibrary.php
T

115 lines
4.1 KiB
PHP

<?php
namespace App\Signature;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
/**
* Bibliothèque des images de signature (logos, bannières), servies publiquement par Nginx sous /img.
* Formats acceptés : PNG, JPEG, GIF. Pas de SVG (mal supporté par Outlook et vecteur de script),
* pas de WebP (non affiché par plusieurs clients de messagerie).
*/
final class ImageLibrary
{
public const EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif'];
/** Règles de validation d'un fichier déposé. */
public static function uploadRules(): array
{
return ['file', 'mimes:png,jpg,jpeg,gif', 'mimetypes:image/png,image/jpeg,image/gif', 'dimensions:min_width=1', 'max:'.config('signature.image_max_kb')];
}
/** Messages associés, à préfixer par le nom du champ. */
public static function uploadMessages(string $field): array
{
return [
"{$field}.mimes" => 'Formats acceptés : PNG, JPEG ou GIF.',
"{$field}.mimetypes" => 'Le fichier n\'est pas une image PNG, JPEG ou GIF valide.',
"{$field}.dimensions" => 'Image illisible : le fichier est incomplet ou endommagé.',
"{$field}.uploaded" => 'L\'envoi a échoué : fichier trop lourd pour le serveur.',
"{$field}.max" => 'Image trop lourde (2 Mo maximum ; visez moins de 100 Ko pour une signature).',
];
}
public function dir(): string
{
$dir = config('signature.image_path');
File::ensureDirectoryExists($dir);
return $dir;
}
public function path(string $name): string
{
return $this->dir().DIRECTORY_SEPARATOR.basename($name);
}
public function exists(string $name): bool
{
return $name !== '' && $name === basename($name)
&& in_array(strtolower(pathinfo($name, PATHINFO_EXTENSION)), self::EXTENSIONS, true)
&& File::isFile($this->path($name));
}
public function url(string $name): string
{
return config('signature.public_url').'/img/'.rawurlencode($name);
}
/** Largeur réelle en pixels, ou null si le fichier est illisible. */
public function width(string $name): ?int
{
$size = $this->exists($name) ? @getimagesize($this->path($name)) : false;
return $size ? (int) $size[0] : null;
}
/** @return Collection<int, array{name:string, url:string, kb:int, width:?int, height:?int, modified:int}> */
public function all(): Collection
{
return collect(File::files($this->dir()))
->filter(fn ($f) => in_array(strtolower($f->getExtension()), self::EXTENSIONS, true))
->map(function ($f) {
$size = @getimagesize($f->getPathname()) ?: [null, null];
return [
'name' => $f->getFilename(),
'url' => $this->url($f->getFilename()),
'kb' => (int) ceil($f->getSize() / 1024),
'width' => $size[0],
'height' => $size[1],
'modified' => $f->getMTime(),
];
})
->sortByDesc('modified')->values();
}
/** Enregistre un fichier déjà validé (uploadRules) sous un nom nettoyé, sans écraser l'existant. Retourne le nom final. */
public function store(UploadedFile $file, ?string $name = null): string
{
// Extension déduite du contenu réel, jamais du nom envoyé par le navigateur.
$info = @getimagesize($file->getRealPath());
$ext = [IMAGETYPE_PNG => 'png', IMAGETYPE_JPEG => 'jpg', IMAGETYPE_GIF => 'gif'][$info[2] ?? 0] ?? null;
abort_unless($ext, 422, 'Image illisible.');
$base = Str::slug(pathinfo($name ?: $file->getClientOriginalName(), PATHINFO_FILENAME)) ?: 'image';
$stored = "{$base}.{$ext}";
for ($i = 2; File::exists($this->path($stored)); $i++) {
$stored = "{$base}-{$i}.{$ext}";
}
$file->move($this->dir(), $stored);
@chmod($this->path($stored), 0644);
return $stored;
}
public function delete(string $name): void
{
File::delete($this->path($name));
}
}