Corrige las subidas de imágenes rotadas en PHP con Jpegtran
Los dispositivos móviles suelen incrustar datos de orientación en las fotos, lo que hace que las
imágenes aparezcan rotadas al subirlas a aplicaciones web. Este problema tan común puede frustrar
tanto a usuarios como a desarrolladores. Por suerte, jpegtran, un pequeño programa de línea de
comandos que se distribuye con libjpeg, puede rotar JPEG sin volver a codificarlos, así que
corriges la orientación conservando cada píxel intacto.
Comprende los datos de orientación exif
Las fotos tomadas con teléfonos modernos incluyen metadatos EXIF que registran, entre muchas otras cosas, cómo se sostenía la cámara. Los visores que respetan este campo mostrarán la imagen en su orientación correcta, mientras que el software que lo ignora mostrará los píxeles sin procesar, y por eso las imágenes recién subidas a veces se ven de lado o al revés.
La etiqueta de orientación puede contener estos valores relevantes:
- 1: normal
- 3: rotación de 180 grados
- 6: rotación de 90 grados en el sentido de las agujas del reloj
- 8: rotación de 90 grados en sentido contrario a las agujas del reloj
Rota imágenes en PHP con gd o ImageMagick
La solución clásica en PHP es cargar el JPEG en GD o ImageMagick, rotarlo y volver a escribirlo:
<?php
$image = imagecreatefromjpeg('photo.jpg');
$rotated = imagerotate($image, 90, 0);
imagejpeg($rotated, 'photo-fixed.jpg');
Funciona, pero el archivo se descomprime y se vuelve a comprimir, lo que puede suavizar los detalles finos y consumir ciclos de CPU en servidores con mucha carga.
Usa jpegtran para una rotación sin pérdida
jpegtran transforma los coeficientes DCT cuantizados sin otra pasada de compresión con pérdida. Las
rotaciones y los volteos perfectos dependen de los límites de MCU (Minimum Coded Unit) de la imagen.
La opción -copy all copia los metadatos, que luego deben actualizarse para que coincidan con la
imagen transformada.
Instala jpegtran en tu entorno PHP
# Debian/Ubuntu
sudo apt-get install libjpeg-progs libimage-exiftool-perl
# RHEL/CentOS (ExifTool may require an additional distribution repository)
sudo yum install libjpeg-turbo-utils perl-Image-ExifTool
# macOS (homebrew)
brew install jpeg exiftool
Verifica que el binario esté disponible:
jpegtran -version
exiftool -ver
Comprueba los requisitos del sistema
Antes de meterte de lleno, comprueba la extensión EXIF, jpegtran y ExifTool. ExifTool restablece los
metadatos de orientación después de transformar los píxeles, lo que evita una segunda rotación en
los visores.
<?php
function checkRequirements(): void
{
if (!extension_loaded('exif')) {
throw new RuntimeException('The EXIF extension is not enabled.');
}
foreach (['jpegtran', 'exiftool'] as $binary) {
$output = [];
exec('command -v ' . escapeshellarg($binary), $output, $status);
if ($status !== 0 || empty($output)) {
throw new RuntimeException($binary . ' is not installed or not in PATH.');
}
}
}
Lee datos EXIF con PHP
<?php
function getOrientation(string $file): int
{
if (!is_readable($file)) {
throw new RuntimeException("File {$file} is not readable.");
}
$exif = @exif_read_data($file);
if ($exif === false) {
// No EXIF block or unreadable — assume upright
// You might want to log a warning here if EXIF data was expected
return 1;
}
return (int) ($exif['Orientation'] ?? 1);
}
Llama a jpegtran desde PHP de forma segura
<?php
function rotateJpeg(string $src, ?string $dst = null): string
{
if (!is_readable($src)) {
throw new RuntimeException("Source file {$src} is not readable.");
}
$dst ??= 'rotated_' . basename($src);
if (file_exists($dst)) {
throw new RuntimeException('Choose a new destination; existing files are not overwritten.');
}
$src = realpath($src);
$orientation = getOrientation($src);
// Map EXIF orientation to jpegtran switch
$map = [
2 => '-flip horizontal', 3 => '-rotate 180', 4 => '-flip vertical',
5 => '-transpose', 6 => '-rotate 90', 7 => '-transverse', 8 => '-rotate 270',
];
if (!isset($map[$orientation])) {
// If orientation is 1 (normal) or any other unhandled value,
// copy the file as is.
if (!copy($src, $dst)) {
throw new RuntimeException("Failed to copy {$src} to {$dst}.");
}
return $dst;
}
$cmd = sprintf(
'jpegtran -perfect %s -copy all -outfile %s %s',
$map[$orientation],
escapeshellarg($dst),
escapeshellarg($src)
);
exec($cmd, $output, $status);
clearstatcache(true, $dst);
if ($status !== 0 || !is_file($dst) || filesize($dst) === 0) {
if (is_file($dst)) unlink($dst);
throw new RuntimeException('jpegtran could not perform a perfect lossless transform.');
}
// Remove the stale thumbnail and mark the transformed pixels as upright.
exec('exiftool -overwrite_original -Orientation=1 -n -ThumbnailImage= ' .
escapeshellarg(realpath($dst)), $metadataOutput, $metadataStatus);
if ($metadataStatus !== 0) {
unlink($dst);
throw new RuntimeException('Could not reset image orientation metadata.');
}
return $dst;
}
Procesamiento por lotes de varias imágenes
-perfect rechaza las transformaciones que no pueden conservar todos los píxeles de los bordes en los
límites de bloque JPEG. Gestiona esos fallos de forma deliberada con un flujo de trabajo de
recodificación aparte. Trabaja en un directorio de salida privado y nunca pases el archivo de origen
como destino.
Si tienes varias imágenes que procesar, puedes recorrerlas en un bucle y aplicar la lógica de rotación. Aquí tienes un ejemplo básico:
<?php
function batchRotateImages(array $sourceFiles, string $destinationDir): array
{
if (!is_dir($destinationDir) || !is_writable($destinationDir)) {
throw new RuntimeException("Destination directory {$destinationDir} is not writable or does not exist.");
}
$processedFiles = [];
foreach ($sourceFiles as $srcFile) {
try {
$baseName = basename($srcFile);
$dstFile = $destinationDir . DIRECTORY_SEPARATOR . 'rotated_' . $baseName;
$processedFiles[$srcFile] = rotateJpeg($srcFile, $dstFile);
} catch (RuntimeException $e) {
// Log error for this specific file and continue with others
error_log("Failed to process {$srcFile}: " . $e->getMessage());
$processedFiles[$srcFile] = false; // Indicate failure
}
}
return $processedFiles;
}
// Example usage:
// $filesToProcess = ['image1.jpg', 'path/to/image2.jpg'];
// $outputDirectory = 'processed_images';
// if (!is_dir($outputDirectory)) {
// mkdir($outputDirectory, 0755, true);
// }
// $results = batchRotateImages($filesToProcess, $outputDirectory);
// print_r($results);
Asegúrate de que el directorio de destino exista y de que tu script PHP pueda escribir en él.
Ejemplo completo y funcional con manejo de errores
<?php
// Ensure these functions are defined or included from where they are declared above.
// function checkRequirements(): void { ... }
// function getOrientation(string $file): int { ... }
// function rotateJpeg(string $src, string $dst = null): string { ... }
// function batchRotateImages(array $sourceFiles, string $destinationDir): array { ... }
try {
checkRequirements(); // Checks for EXIF extension and jpegtran
$sourceFile = 'photo.jpg'; // Ensure this file exists for testing
if (!file_exists($sourceFile)) {
// Create a dummy file for testing if it doesn't exist
// In a real scenario, ensure photo.jpg is a valid JPEG with EXIF data.
if (!touch($sourceFile)) {
error_log("Warning: Could not create dummy file {$sourceFile}. Ensure the directory is writable.");
} else {
error_log("Warning: {$sourceFile} does not exist. A dummy file was touched for the example to run. Rotation may not occur as expected without valid EXIF data.");
}
}
$fixed = rotateJpeg($sourceFile, 'rotated_photo.jpg');
echo "Saved correctly oriented file to $fixed\n";
// Example for batch processing (optional, uncomment to test)
/*
$filesToProcess = [$sourceFile]; // Add more files as needed
$outputDirectory = 'processed_batch';
if (!is_dir($outputDirectory)) {
if (!mkdir($outputDirectory, 0755, true)) {
throw new RuntimeException("Could not create directory: {$outputDirectory}");
}
}
echo "\nStarting batch processing...\n";
$results = batchRotateImages($filesToProcess, $outputDirectory);
print_r($results);
echo "Batch processing finished. Check the '{$outputDirectory}' directory.\n";
*/
} catch (Throwable $e) {
error_log("Error: " . $e->getMessage());
// It's generally better to let PHP handle the response code
// or set it based on the context (e.g., web request vs. CLI script)
// http_response_code(500);
echo "An error occurred. Check the error log for details.\n";
}
Las tres funciones (checkRequirements, getOrientation y rotateJpeg) cubren las comprobaciones de
dependencias, el análisis de EXIF, la ejecución de comandos, la actualización de metadatos y la
validación de la salida.
Consideraciones de seguridad con exec
- Escapa todos los argumentos con
escapeshellarg(): nunca concatenes entrada de usuario sin procesar. Esto es crucial para evitar vulnerabilidades de inyección de comandos. - Valida las dependencias (
command -v jpegtran) antes de llamarlas. Asegúrate de quejpegtranesté instalado y accesible en el PATH del sistema. - Comprueba el éxito del comando mediante el argumento de estado de salida que se pasa a
exec(), y luego verifica que el archivo resultante exista y tenga un tamaño distinto de cero. - Limita las ubicaciones de escritura a directorios fuera de la raíz web pública cuando sea posible, y asegura permisos de archivo adecuados para las operaciones de lectura y escritura.
- Sanea las rutas de archivo: asegúrate de que las rutas de los archivos de entrada y los destinos de salida se validen y saneen para evitar ataques de salto de directorio o escrituras en ubicaciones no deseadas.
Maneja casos límite y escenarios de error
- Faltan datos EXIF: la función
getOrientationusa por defecto la orientación 1 (normal) y, en esos casos,rotateJpegcopiará el archivo tal cual. - Archivo de origen ilegible: las funciones
getOrientationyrotateJpegahora incluyen comprobaciones conis_readable()y lanzan excepciones si no se puede leer un archivo. - Fallo del comando
jpegtran:rotateJpegcomprueba el estado de salida y el archivo de salida, elimina la salida fallida y lanza una excepción. - JPEG progresivos:
jpegtranacepta entrada progresiva. Las restricciones de transformación perfecta siguen aplicándose, y la salida progresiva requiere la opción-progressive. - Imágenes grandes: los búferes de coeficientes siguen consumiendo memoria. Aplica límites de recursos y mantén suficiente espacio en disco para la salida, sobre todo al procesar trabajos concurrentes.
- Valores de orientación no admitidos: el código actual copia la imagen tal cual si el valor de orientación EXIF es 1 o no se reconoce. Podrías modificarlo para que lance una excepción con los valores no admitidos si necesitas un manejo estricto.
- Errores de permisos: asegúrate de que el script PHP tenga permisos de lectura para los
archivos de origen y permisos de escritura para el directorio de destino. Las comprobaciones de
is_readable()ayudan, y los fallos en las operaciones de archivo (comocopy()o la redirección de salida dejpegtran) suelen provocar excepciones o comprobaciones de salida fallidas. - Limpieza de archivos: si se crean archivos temporales o si hay que eliminar los archivos
originales tras un procesamiento correcto, implementa un mecanismo de limpieza. Los ejemplos
actuales crean archivos nuevos (por ejemplo,
rotated_photo.jpg), por lo que puede que necesites limpiar los originales de forma explícita según tu flujo de trabajo.
Por qué jpegtran destaca en la rotación de JPEG
- Sin pérdida: transforma los coeficientes cuantizados sin otra pasada de codificación con pérdida.
- Respetuoso con los metadatos:
-copy allconserva EXIF, XMP y los perfiles ICC. - A menudo más rápido: evita una decodificación y una recodificación completas en el dominio de píxeles, pero mide tu propia carga de trabajo para asegurarte.
- Estable: forma parte de libjpeg desde hace décadas y está disponible en prácticamente todas las plataformas de servidor.
Conclusión
Con un puñado de líneas puedes leer la orientación EXIF, llamar a jpegtran y entregar a tus
usuarios una foto bien orientada, sin perder ni un solo bit de calidad. Coloca las funciones
anteriores en tu manejador de subidas o en tu worker de cola, y las selfies de lado serán cosa del
pasado.
En Transloadit usamos técnicas avanzadas de optimización de imágenes en nuestro Robot 🤖 /image/optimize, que admite los formatos JPEG, PNG, GIF, WebP y SVG con prioridades de optimización configurables.
