Comment redimensionner et filigraner des images en PHP (Imagick)
Pour redimensionner une photo et y ajouter un filigrane avec PHP Imagick, appliquez d’abord son orientation, redimensionnez les pixels, puis dessinez le texte à la taille finale. Le script ci-dessous prend un JPEG ou un PNG local, réduit les largeurs supérieures à 800 pixels sans recadrage et enregistre un PNG distinct avec « © Your Company » dans le coin inférieur droit. Votre original reste inchangé, et un fichier de sortie existant n’est jamais écrasé.
Imagick est l’interface de PHP vers ImageMagick. Elle vous permet d’effectuer ces opérations sur une seule image en mémoire. Cet exemple concerne des images fixes que vous maîtrisez : utilisez une photo sRGB ou un PNG, y compris avec transparence. Les entrées GIF, WebP, PDF et CMYK sont rejetées. Le PNG animé sort du cadre de ce tutoriel ; un décodeur PNG peut n’exposer que sa première trame, donc vérifier le nombre de trames décodées ne permet pas d’établir qu’un PNG est statique.
Installer PHP et Imagick
La configuration suivante utilise Docker et un terminal compatible Bash sous Linux. Elle a été
testée sous Linux x86_64 avec PHP 8.4.26, Imagick 3.8.1, ImageMagick 7.1.2-29 et DejaVu Sans 2.37.
Créez un répertoire de travail vide et ouvrez-y votre terminal. Enregistrez le contenu suivant sous
le nom Dockerfile :
FROM php:8.4.26-cli-alpine3.23
RUN apk add --no-cache imagemagick=7.1.2.29-r0 imagemagick-jpeg=7.1.2.29-r0 font-dejavu=2.37-r6 \
&& apk add --no-cache --virtual .build-deps $PHPIZE_DEPS imagemagick-dev=7.1.2.29-r0 \
&& pecl install imagick-3.8.1 \
&& docker-php-ext-enable imagick \
&& apk del .build-deps
WORKDIR /work
Construisez l’image locale ; cette étape nécessite un accès à Internet. L’installation de l’extension suit les instructions PECL de l’image PHP officielle.
docker build -t imagick-demo:local .
Attendez que la construction réussisse avant de continuer. Alpine fournit le
codeur JPEG dans un paquet séparé,
c’est pourquoi la configuration l’installe explicitement. L’image contient la police à
l’emplacement /usr/share/fonts/dejavu/DejaVuSans.ttf. Si cette pile PHP/Imagick est déjà installée
localement, vous pouvez exécuter le script PHP directement avec votre propre fichier de police ; le
conteneur ci-dessous fournit les trois dépendances sans modifier l’installation PHP de votre hôte.
Enregistrer le script de redimensionnement et de filigrane
Enregistrez le code complet ci-dessous sous le nom resize-watermark.php à côté du
Dockerfile. Les trois arguments sont l’image d’entrée, un nouveau chemin de sortie
.png et un fichier de police TrueType lisible. Le répertoire parent du
fichier de sortie doit déjà exister.
<?php
declare(strict_types=1);
$image = null;
$input = null;
$output = null;
$created = false;
$status = 0;
try {
if ($argc !== 4) {
throw new RuntimeException('Usage: php resize-watermark.php INPUT OUTPUT.png FONT.ttf');
}
if (!extension_loaded('imagick')) {
throw new RuntimeException('Enable the Imagick extension in your CLI PHP.');
}
$inputPath = realpath($argv[1]);
$outputDirectory = realpath(dirname($argv[2]));
$fontPath = realpath($argv[3]);
if ($inputPath === false || !is_file($inputPath) || !is_readable($inputPath)) {
throw new RuntimeException('Input must be a readable local image file.');
}
if ($fontPath === false || !is_file($fontPath) || !is_readable($fontPath)) {
throw new RuntimeException('Font must be a readable local TrueType file.');
}
if ($outputDirectory === false || strtolower(pathinfo($argv[2], PATHINFO_EXTENSION)) !== 'png') {
throw new RuntimeException('Use a .png output in an existing directory.');
}
$outputPath = $outputDirectory . DIRECTORY_SEPARATOR . basename($argv[2]);
$input = fopen($inputPath, 'rb');
if ($input === false) {
throw new RuntimeException('Could not open the input.');
}
$image = new Imagick();
$image->setOption('png:preserve-iCCP', 'true');
$image->readImageFile($input);
if ($image->getNumberImages() !== 1 || !in_array($image->getImageFormat(), ['JPEG', 'PNG'], true)) {
throw new RuntimeException('Use a still JPEG or PNG image.');
}
if ($image->getImageColorspace() === Imagick::COLORSPACE_CMYK) {
throw new RuntimeException('Convert CMYK images to sRGB before using this example.');
}
$image->autoOrient();
if ($image->getImageWidth() > 800) {
$image->resizeImage(800, 0, Imagick::FILTER_LANCZOS, 1);
}
$image->setImagePage(0, 0, 0, 0);
$text = '© Your Company';
$draw = new ImagickDraw();
$draw->setFont($fontPath);
$draw->setFontSize(24);
$draw->setFillColor('white');
$draw->setFillOpacity(0.8);
$draw->setStrokeColor('black');
$draw->setStrokeWidth(1);
$draw->setGravity(Imagick::GRAVITY_SOUTHEAST);
$metrics = $image->queryFontMetrics($draw, $text);
if ($metrics['textWidth'] + 32 > $image->getImageWidth()
|| $metrics['textHeight'] + 32 > $image->getImageHeight()) {
throw new RuntimeException('Image is too small for this watermark. Shorten the text or reduce its font size.');
}
$image->annotateImage($draw, 16, 16, 0, $text);
$profiles = $image->getImageProfiles('icc', true);
$image->stripImage();
// stripImage also tells the PNG encoder to omit ICC, even if we restore the profile.
$image->setImageArtifact('png:exclude-chunk', 'eXIf,date');
if (isset($profiles['icc'])) {
$image->profileImage('icc', $profiles['icc']);
}
// Decoded EXIF properties can otherwise be copied into PNG text chunks.
foreach ($image->getImageProperties('*', false) as $property) {
$image->deleteImageProperty($property);
}
$image->setImageFormat('png');
$output = @fopen($outputPath, 'xb');
if ($output === false) {
throw new RuntimeException('Cannot create output: it already exists or the directory is not writable.');
}
$created = true;
if (!$image->writeImageFile($output, 'png') || !fflush($output)) {
throw new RuntimeException('Could not finish writing the PNG.');
}
printf("Wrote %s (%d × %d)\n", $argv[2], $image->getImageWidth(), $image->getImageHeight());
} catch (Throwable $error) {
fwrite(STDERR, 'Resize failed: ' . $error->getMessage() . PHP_EOL);
$status = 1;
} finally {
if (is_resource($input)) {
fclose($input);
}
if (is_resource($output)) {
fclose($output);
}
if ($status !== 0 && $created) {
unlink($outputPath);
}
$image?->clear();
}
exit($status);
Le script transmet un descripteur de fichier ouvert à
readImageFile(), afin qu’ImageMagick n’interprète
pas le nom du fichier d’entrée comme un préfixe de format ou un sélecteur de trame. Il écrit aussi
via un descripteur de fichier : le mode xb
de PHP refuse un fichier de sortie existant, y compris le fichier d’entrée lui-même. Utilisez ce
script comme une commande locale séquentielle ; le fichier de sortie devient visible pendant son
écriture. Un échec d’écriture intercepté supprime le nouveau fichier partiel, mais un processus
interrompu peut en laisser un.
Exécuter le script sur une image locale
Copiez une image fixe dans ce répertoire sous le nom input.jpg, ou remplacez
l’argument d’entrée ci-dessous par le nom de votre fichier PNG. Exécutez ensuite :
docker run --rm --network none --user "$(id -u):$(id -g)" \
--mount "type=bind,source=$PWD,target=/work" --workdir /work \
imagick-demo:local php resize-watermark.php input.jpg output.png \
/usr/share/fonts/dejavu/DejaVuSans.ttf
Le répertoire monté rend output.png disponible sur votre hôte. Pour une entrée
de 1 600 × 1 000 sans rotation, la commande affiche Wrote output.png (800 × 500). Ouvrez ce
fichier : l’image entière doit être présente, avec le texte en retrait par rapport au bord inférieur
droit. Si vous relancez la même commande, elle échoue avec un code de retour non nul et laisse le
PNG existant inchangé. Choisissez un nouveau nom de fichier de sortie pour la version suivante.
Ajuster la taille et le filigrane
autoOrient() applique l’orientation enregistrée
avant la mesure de la largeur. C’est important pour les photos prises au téléphone : un portrait
stocké de côté doit être pivoté avant que vous ne choisissiez ses dimensions de sortie. Dans
resizeImage(), une hauteur de zéro entraîne le
calcul de la hauteur proportionnelle. La vérification de la largeur évite d’agrandir les images plus
petites ; la hauteur n’est pas plafonnée.
Remplacez $text par votre filigrane court, sur une seule ligne. Le script
définit la taille de police après le redimensionnement, de sorte que le libellé ne varie pas avec
les dimensions en pixels de l’image source. Un remplissage blanc avec un contour noir aide le texte
à rester visible sur les zones claires comme sur les zones sombres. La vérification des métriques de
police rejette les images trop petites pour contenir le libellé avec sa marge intérieure. Il s’agit
d’une marque d’attribution visible, et non d’une protection contre le recadrage ou la modification
de l’image par un tiers.
Choisir le comportement de sortie et des métadonnées
Le PNG conserve les zones transparentes au lieu de les aplatir sur un arrière-plan. Il peut aussi
être plus volumineux que le JPEG d’origine, même après redimensionnement ; ce script ne promet
aucune réduction de la taille du fichier. Renommer le fichier de sortie en
.jpg ne change pas l’encodeur, c’est pourquoi le script exige une
extension .png.
stripImage() supprime les profils et les
commentaires. Ici, cette méthode s’exécute après l’application de l’orientation. Le script restaure
le profil colorimétrique ICC et ajuste les paramètres des blocs PNG
pour le conserver. Il efface aussi les propriétés de l’image afin que les champs EXIF décodés ne
soient pas copiés dans les blocs de texte PNG. L’encodeur peut encore ajouter des métadonnées de
format et de couleur ; cela ne garantit donc pas un fichier sans métadonnées. Utilisez des images
sources sRGB pour ce flux de travail, et une conversion distincte avec gestion des couleurs pour les
ressources destinées à l’impression ou à large gamut.
Gérer les échecs et les fichiers non fiables
Une entrée manquante, une police manquante, une image indécodable, un format non pris en charge ou
un fichier de sortie existant entraîne un code de retour non nul et un message sur la sortie
d’erreur standard. Le fichier de sortie n’est ouvert qu’une fois le décodage, le redimensionnement
et le dessin réussis. Si le chemin de la police existe mais ne désigne pas une police valide,
Imagick signale l’échec lors de la mesure ou du dessin du texte. Pour une vignette minuscule,
raccourcissez le libellé ou réduisez setFontSize(24) dans le script.
Il s’agit d’un exemple local de retouche d’image, et non d’un point de terminaison de téléversement. ImageMagick doit décoder l’entrée avant que le script puisse vérifier son format et son nombre de trames, et un décodeur peut tolérer certains fichiers endommagés. Un traitement réussi ne certifie pas qu’une entrée est complète ou sûre. Pour les fichiers téléversés non fiables, configurez une politique de sécurité ImageMagick qui restreint les codeurs, les délégués, les dimensions, les trames, la mémoire, le disque et le temps de traitement, en plus de limites de taille des requêtes et d’une isolation des processus de travail. Les limites de mémoire seules peuvent faire déborder le cache de pixels sur le disque ; elles ne bornent pas l’ensemble de la tâche.
