How to resize and watermark images in PHP using Imagick
To resize and watermark a photo with PHP Imagick, apply its orientation first, resize the pixels, then draw the text at the final size. The script below takes a local JPEG or PNG, reduces widths above 800 pixels without cropping, and saves a separate PNG with “© Your Company” in the lower-right corner. Your original stays unchanged, and an existing output is never overwritten.
Imagick is PHP’s interface to ImageMagick. It lets you perform these operations on one image in memory. This example is for still images you control: use an sRGB photo or PNG, including one with transparency. GIF, WebP, PDF, and CMYK inputs are rejected. Animated PNG is outside this tutorial’s scope; a PNG decoder can expose only its first frame, so checking the decoded frame count does not establish that a PNG is static.
Set up PHP and Imagick
The following setup uses Docker and a Bash-compatible terminal on Linux. It was tested on Linux
x86_64 with PHP 8.4.26, Imagick 3.8.1, ImageMagick 7.1.2-29, and DejaVu Sans 2.37. Create an empty
working directory and open your terminal there. Save this as 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
Build the local image; this step needs internet access. The extension installation follows the official PHP image’s PECL instructions.
docker build -t imagick-demo:local .
Wait for a successful build before continuing. Alpine packages the
JPEG coder separately,
so the setup installs it explicitly. The image contains the font at
/usr/share/fonts/dejavu/DejaVuSans.ttf. If you already have this PHP/Imagick stack installed locally,
you can run the PHP script directly with your own font file; the container below supplies all three
dependencies without changing your host PHP installation.
Save the resize and watermark script
Save the complete code below as resize-watermark.php beside the Dockerfile. The three arguments
are the input image, a new .png output path, and a readable TrueType font file. The output’s parent
directory must already exist.
<?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);
The script passes an open file handle to
readImageFile(), so ImageMagick does not
interpret the input filename as a format prefix or frame selector. It also writes through a file
handle: PHP’s xb mode refuses an existing
output, including the input itself. Use this as a sequential local command; the output becomes
visible while it is being written. A caught write failure removes the new partial file, but a
terminated process can leave one behind.
Run it on a local image
Copy a still image into this directory as input.jpg, or change the input argument below to your
PNG filename. Then run:
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
The mounted directory makes output.png available on your host. For a 1,600 × 1,000 input without
rotation, the command reports Wrote output.png (800 × 500). Open that file: the full image should
be present, with the text inset from the lower-right edge. Running the same command again fails
with a nonzero status and leaves the existing PNG unchanged. Choose a new output filename for the
next version.
Adjust the size and watermark
autoOrient() applies the stored orientation before
width is measured. This matters for phone photos: a portrait stored sideways must be rotated before
you choose its output dimensions. In
resizeImage(), a height of zero calculates
the proportional height. The width check avoids enlarging smaller images; there is no height cap.
Change $text to your short, single-line watermark. The script sets the font size after resizing,
so the label does not scale with the source image’s pixel dimensions. White fill with a black outline
helps the text remain visible over both light and dark areas. The font-metrics check rejects images
too small to fit the label with its padding. This is a visible attribution mark, not protection against someone
cropping or editing the image.
Choose output and metadata behavior
PNG keeps transparent areas instead of flattening them onto a background. It can also be larger
than the original JPEG, even after resizing; this script makes no file-size reduction promise.
Renaming the output to .jpg does not change the encoder, so the script requires a .png extension.
stripImage() removes profiles and comments.
Here it runs after orientation has been applied. The script restores the ICC color profile and
adjusts the PNG chunk settings to retain it. It also clears image
properties so decoded EXIF fields are not copied into PNG text chunks. The encoder can still add
format and color metadata; this is not a guarantee of a metadata-free file. Use sRGB source images
for this workflow and a separate color-managed conversion for print or wide-gamut assets.
Handle failures and untrusted files
A missing input, missing font, undecodable image, unsupported format, or existing output causes a
nonzero exit and a message on standard error. The output is not opened until decoding, resizing,
and drawing have succeeded. If the font path exists but is not a valid font, Imagick reports the
failure while measuring or drawing the text. For a tiny thumbnail, shorten the label or reduce
setFontSize(24) in the script.
This is a local image-editing example, not an upload endpoint. ImageMagick must decode the input before the script can check its format and frame count, and a decoder may tolerate some damaged files. Successful processing is not a certificate that an input is complete or safe. For untrusted uploads, configure an ImageMagick security policy that restricts coders, delegates, dimensions, frames, memory, disk, and processing time, alongside request size limits and worker isolation. Memory limits alone can spill the pixel cache to disk; they do not bound the whole job.
