Cómo escalar imágenes y añadir marcas de agua con PHP Imagick
Para redimensionar una foto y añadirle una marca de agua con PHP Imagick, aplica primero su orientación, redimensiona los píxeles y luego dibuja el texto al tamaño final. El siguiente script recibe un JPEG o PNG local, reduce los anchos superiores a 800 píxeles sin recortar y guarda un PNG independiente con «© Your Company» en la esquina inferior derecha. El original no cambia y nunca se sobrescribe un archivo de salida existente.
Imagick es la interfaz de PHP para ImageMagick. Te permite realizar estas operaciones sobre una imagen en memoria. Este ejemplo está pensado para imágenes estáticas que tú controlas: usa una foto o un PNG en sRGB, incluso con transparencia. Se rechazan las entradas GIF, WebP, PDF y CMYK. Los PNG animados quedan fuera del alcance de este tutorial; un decodificador PNG puede exponer solo el primer fotograma, por lo que comprobar el número de fotogramas decodificados no permite determinar que un PNG sea estático.
Configura PHP e Imagick
La siguiente configuración usa Docker y una terminal compatible con Bash en Linux. Se probó en
Linux x86_64 con PHP 8.4.26, Imagick 3.8.1, ImageMagick 7.1.2-29 y DejaVu Sans 2.37. Crea un
directorio de trabajo vacío y abre tu terminal allí. Guarda lo siguiente como
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
Construye la imagen local; este paso necesita acceso a internet. La instalación de la extensión sigue las instrucciones de PECL de la imagen oficial de PHP.
docker build -t imagick-demo:local .
Espera a que la compilación finalice correctamente antes de continuar. Alpine distribuye el
codificador JPEG por separado,
por lo que la configuración lo instala explícitamente. La imagen contiene la fuente en
/usr/share/fonts/dejavu/DejaVuSans.ttf. Si ya tienes este entorno de PHP/Imagick instalado localmente,
puedes ejecutar el script PHP directamente con tu propio archivo de fuente; el contenedor que se
usa a continuación proporciona las tres dependencias sin cambiar la instalación de PHP de tu equipo.
Guarda el script para redimensionar y añadir marcas de agua
Guarda el código completo que aparece a continuación como resize-watermark.php junto
al Dockerfile. Los tres argumentos son la imagen de entrada, una nueva ruta de salida
.png y un archivo de fuente TrueType con permisos de lectura. El
directorio padre del archivo de salida ya debe existir.
<?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);
El script pasa un manejador de archivo abierto a
readImageFile(), de modo que ImageMagick no
interpreta el nombre del archivo de entrada como un prefijo de formato ni como un selector de
fotogramas. También escribe mediante un manejador de archivo: el
modo xb de PHP rechaza un archivo de
salida existente, incluido el propio archivo de entrada. Úsalo como un comando local secuencial;
el archivo de salida se vuelve visible mientras se escribe. Si se captura un error de escritura,
se elimina el nuevo archivo parcial, pero un proceso terminado puede dejarlo en el disco.
Ejecútalo sobre una imagen local
Copia una imagen estática en este directorio como input.jpg, o cambia el
argumento de entrada que aparece a continuación por el nombre de tu archivo PNG. Luego ejecuta:
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
El directorio montado permite acceder a output.png desde tu equipo. Para una
entrada de 1.600 × 1.000 sin rotación, el comando informa Wrote output.png (800 × 500). Abre ese
archivo: debería contener la imagen completa, con el texto separado del borde inferior derecho.
Si ejecutas de nuevo el mismo comando, falla con un estado distinto de cero y deja el PNG
existente sin cambios. Elige un nuevo nombre de archivo de salida para la siguiente versión.
Ajusta el tamaño y la marca de agua
autoOrient() aplica la orientación almacenada
antes de medir el ancho. Esto es importante para las fotos de teléfonos: una foto vertical
almacenada de lado debe rotarse antes de elegir sus dimensiones de salida. En
resizeImage(), una altura de cero hace que se
calcule la altura proporcional. La comprobación del ancho evita ampliar imágenes más pequeñas;
no hay un límite de altura.
Cambia $text por tu marca de agua breve, de una sola línea. El script
establece el tamaño de la fuente después de redimensionar, de modo que el texto no cambia de
escala según las dimensiones en píxeles de la imagen original. El relleno blanco con contorno
negro ayuda a que el texto siga siendo visible tanto en zonas claras como oscuras. La comprobación
de las métricas de la fuente rechaza imágenes demasiado pequeñas para que quepa el texto con sus
márgenes. Es una marca de atribución visible, no una protección contra quien recorte o edite la imagen.
Elige el comportamiento de la salida y los metadatos
PNG conserva las áreas transparentes en lugar de aplanarlas sobre un fondo. También puede ocupar
más espacio que el JPEG original, incluso después de redimensionarlo; este script no promete
reducir el tamaño del archivo. Cambiar el nombre de la salida a .jpg no
cambia el codificador, por lo que el script exige la extensión .png.
stripImage() elimina perfiles y comentarios.
Aquí se ejecuta después de aplicar la orientación. El script restaura el perfil de color ICC y
ajusta la configuración de los bloques PNG para conservarlo.
También borra las propiedades de la imagen para que los campos EXIF decodificados no se copien en
los bloques de texto del PNG. El codificador aún puede añadir metadatos de formato y color; esto
no garantiza un archivo sin metadatos. Usa imágenes de origen sRGB para este flujo de trabajo y
una conversión independiente con gestión del color para activos de impresión o de gama cromática amplia.
Gestiona los errores y los archivos no confiables
La falta de un archivo de entrada o de una fuente, una imagen que no se pueda decodificar, un
formato no compatible o un archivo de salida existente provocan la salida con un código distinto
de cero y un mensaje en la salida de error estándar. El archivo de salida no se abre hasta que la
decodificación, el redimensionamiento y el dibujo hayan finalizado correctamente. Si la ruta de
la fuente existe pero no corresponde a una fuente válida, Imagick informa del error al medir o
dibujar el texto. Para una miniatura muy pequeña, acorta el texto o reduce
setFontSize(24) en el script.
Este es un ejemplo de edición local de imágenes, no un endpoint de subida. ImageMagick debe decodificar la entrada antes de que el script pueda comprobar su formato y número de fotogramas, y un decodificador puede tolerar algunos archivos dañados. Que el procesamiento finalice correctamente no certifica que una entrada esté completa o sea segura. Para subidas no confiables, configura una política de seguridad de ImageMagick que restrinja codificadores, delegados, dimensiones, fotogramas, memoria, disco y tiempo de procesamiento, junto con límites de tamaño de las solicitudes y aislamiento de los procesos de trabajo. Los límites de memoria por sí solos pueden hacer que la caché de píxeles se vuelque al disco; no limitan toda la tarea.
