PHP Imagick: Wie Bilder skalieren und Wasserzeichen hinzufügen?
Um ein Foto mit PHP Imagick zu skalieren und mit einem Wasserzeichen zu versehen, wenden Sie zuerst seine Ausrichtung an, skalieren dann die Pixel und zeichnen anschließend den Text in der endgültigen Größe. Das folgende Skript verarbeitet ein lokales JPEG oder PNG, reduziert Breiten über 800 Pixel ohne Zuschnitt und speichert ein separates PNG mit „© Your Company“ in der unteren rechten Ecke. Ihr Original bleibt unverändert, und eine vorhandene Ausgabedatei wird niemals überschrieben.
Imagick ist die PHP-Schnittstelle zu ImageMagick. Damit lassen sich diese Vorgänge an einem Bild im Arbeitsspeicher ausführen. Dieses Beispiel ist für nicht animierte Bilder gedacht, die Sie selbst kontrollieren: Verwenden Sie ein sRGB-Foto oder ein PNG, auch mit Transparenz. GIF, WebP, PDF und CMYK-Eingaben werden abgelehnt. Animierte PNGs sind nicht Gegenstand dieses Tutorials. Ein PNG-Decoder kann nur deren erstes Frame bereitstellen, sodass die Prüfung der decodierten Frame-Anzahl nicht belegt, dass ein PNG statisch ist.
PHP und Imagick einrichten
Die folgende Einrichtung nutzt Docker und ein Bash-kompatibles Terminal unter Linux. Sie wurde
unter Linux x86_64 mit PHP 8.4.26, Imagick 3.8.1, ImageMagick 7.1.2-29 und DejaVu Sans 2.37 getestet.
Erstellen Sie ein leeres Arbeitsverzeichnis und öffnen Sie dort Ihr Terminal. Speichern Sie
Folgendes als 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
Erstellen Sie das lokale Image; dieser Schritt benötigt Internetzugang. Die Installation der Erweiterung folgt der PECL-Anleitung des offiziellen PHP-Images.
docker build -t imagick-demo:local .
Warten Sie, bis der Build erfolgreich abgeschlossen ist, bevor Sie fortfahren. Alpine stellt den
JPEG-Coder als separates Paket bereit, daher wird er hier explizit
installiert. Das Image enthält die Schriftart unter
/usr/share/fonts/dejavu/DejaVuSans.ttf. Wenn Sie diesen PHP/Imagick-Stack bereits lokal installiert haben,
können Sie das PHP-Skript direkt mit Ihrer eigenen Schriftdatei ausführen. Der folgende Container
stellt alle drei Abhängigkeiten bereit, ohne die PHP-Installation auf Ihrem Host zu ändern.
Skript zum Skalieren und Hinzufügen von Wasserzeichen speichern
Speichern Sie den vollständigen folgenden Code als resize-watermark.php neben dem Dockerfile.
Die drei Argumente sind das Eingabebild, ein neuer Ausgabepfad mit der Endung
.png und eine lesbare TrueType-Schriftdatei. Das übergeordnete Verzeichnis
der Ausgabedatei muss bereits existieren.
<?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);
Das Skript übergibt ein offenes Datei-Handle an
readImageFile(), damit ImageMagick den
Eingabedateinamen nicht als Formatpräfix oder Frame-Selektor interpretiert. Auch das Schreiben
erfolgt über ein Datei-Handle: Der PHP-Modus xb
lehnt eine vorhandene Ausgabedatei ab, auch die Eingabedatei selbst. Verwenden Sie das Skript als
sequenziellen lokalen Befehl; die Ausgabedatei wird bereits während des Schreibens sichtbar.
Bei einem abgefangenen Schreibfehler wird die neue Teildatei entfernt. Ein beendeter Prozess kann
sie jedoch zurücklassen.
Mit einem lokalen Bild ausführen
Kopieren Sie ein nicht animiertes Bild als input.jpg in dieses Verzeichnis,
oder ändern Sie das folgende Eingabeargument in den Namen Ihrer PNG-Datei. Führen Sie dann
Folgendes aus:
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
Das eingebundene Verzeichnis macht output.png auf Ihrem Host verfügbar.
Bei einer Eingabe mit 1.600 × 1.000 ohne Drehung meldet der Befehl
Wrote output.png (800 × 500). Öffnen Sie diese Datei: Das vollständige Bild sollte vorhanden sein,
mit dem Text etwas vom unteren rechten Rand eingerückt. Wenn Sie denselben Befehl erneut ausführen,
schlägt er mit einem Status ungleich null fehl und lässt das vorhandene PNG unverändert. Wählen Sie
für die nächste Version einen neuen Ausgabedateinamen.
Größe und Wasserzeichen anpassen
autoOrient() wendet die gespeicherte Ausrichtung an,
bevor die Breite gemessen wird. Das ist bei Smartphone-Fotos wichtig: Ein seitlich gespeichertes
Hochformatfoto muss gedreht werden, bevor Sie seine Ausgabeabmessungen wählen. Bei
resizeImage() wird mit einer Höhe von null
die proportionale Höhe berechnet. Die Breitenprüfung verhindert, dass kleinere Bilder vergrößert
werden; es gibt keine Höhenbegrenzung.
Ändern Sie $text in Ihr kurzes, einzeiliges Wasserzeichen. Das Skript legt
die Schriftgröße nach dem Skalieren fest, sodass sich der Text nicht mit den Pixelabmessungen des
Quellbilds skaliert. Eine weiße Füllung mit schwarzer Kontur hilft, den Text sowohl auf hellen als
auch auf dunklen Bereichen sichtbar zu halten. Die Prüfung der Schriftmetriken lehnt Bilder ab,
die zu klein sind, um den Text samt Randabstand aufzunehmen. Dies ist ein sichtbarer
Urheberhinweis, kein Schutz davor, dass jemand das Bild zuschneidet oder bearbeitet.
Verhalten bei Ausgabe und Metadaten wählen
PNG erhält transparente Bereiche, statt sie mit einem Hintergrund zusammenzuführen. Die Datei kann
auch nach dem Skalieren größer als das ursprüngliche JPEG sein; dieses Skript verspricht keine
Reduzierung der Dateigröße. Das Umbenennen der Ausgabe in .jpg ändert den
Encoder nicht. Daher verlangt das Skript die Dateiendung .png.
stripImage() entfernt Profile und Kommentare.
Hier wird die Methode ausgeführt, nachdem die Ausrichtung angewendet wurde. Das Skript stellt das
ICC-Farbprofil wieder her und passt die PNG-Chunk-Einstellungen an,
um es zu erhalten. Es löscht außerdem Bildeigenschaften, damit decodierte EXIF-Felder nicht in
PNG-Text-Chunks kopiert werden. Der Encoder kann weiterhin Format- und Farbmetadaten hinzufügen;
eine metadatenfreie Datei wird damit nicht garantiert. Verwenden Sie für diesen Workflow
sRGB-Quellbilder und für Druckvorlagen oder Bilder mit großem Farbumfang eine separate Konvertierung
mit Farbmanagement.
Fehler und nicht vertrauenswürdige Dateien handhaben
Eine fehlende Eingabedatei, eine fehlende Schriftart, ein nicht decodierbares Bild, ein nicht
unterstütztes Format oder eine vorhandene Ausgabedatei führen zu einem Exit-Status ungleich null und
einer Meldung auf der Standardfehlerausgabe. Die Ausgabedatei wird erst geöffnet, wenn Decodieren,
Skalieren und Zeichnen erfolgreich waren. Wenn der Schriftpfad existiert, aber keine gültige
Schriftart enthält, meldet Imagick den Fehler beim Messen oder Zeichnen des Texts. Kürzen Sie bei
einem winzigen Thumbnail den Text oder reduzieren Sie setFontSize(24) im Skript.
Dies ist ein Beispiel zur lokalen Bildbearbeitung, kein Upload-Endpunkt. ImageMagick muss die Eingabe decodieren, bevor das Skript deren Format und Frame-Anzahl prüfen kann. Ein Decoder kann zudem manche beschädigten Dateien tolerieren. Eine erfolgreiche Verarbeitung ist kein Nachweis dafür, dass eine Eingabe vollständig oder sicher ist. Konfigurieren Sie für nicht vertrauenswürdige Uploads eine ImageMagick-Sicherheitsrichtlinie, die Coder, Delegates, Abmessungen, Frames, Arbeitsspeicher, Festplattenspeicher und Verarbeitungszeit einschränkt. Begrenzen Sie zusätzlich die Anfragegröße und isolieren Sie die Worker. Arbeitsspeicherlimits allein können dazu führen, dass der Pixel-Cache auf die Festplatte ausgelagert wird; sie begrenzen nicht den gesamten Job.
