Corrija uploads de imagens rotacionadas em PHP com jpegtran
Para corrigir um upload JPEG que aparece de lado, transforme a imagem armazenada de acordo com a orientação EXIF dela e, em seguida, redefina essa orientação. O comando PHP abaixo salva um JPEG separado e corretamente orientado, sem outra etapa de compressão com perdas. Ele recusa entradas danificadas e rotações que deixariam uma faixa sem correção ao longo de uma borda da imagem.
Entenda os dados de orientação EXIF
Uma tag de orientação EXIF informa ao visualizador como exibir a imagem armazenada. Alterar apenas
essa tag não rotaciona os dados da imagem; rotacionar os dados sem redefinir a tag pode fazer com
que o visualizador a rotacione de novo. Leia a orientação IFD0 da imagem principal, e não a
orientação de uma miniatura incorporada.
Todos os oito valores importam, incluindo os casos espelhados. Estas são as operações a aplicar à imagem armazenada, com ângulos de rotação medidos no sentido horário:
| Valor EXIF | Correção | Argumentos do jpegtran |
|---|---|---|
| 1 | Manter como está | Nenhuma transformação |
| 2 | Espelhar da esquerda para a direita | -flip horizontal |
| 3 | Virar de cabeça para baixo | -rotate 180 |
| 4 | Espelhar de cima para baixo | -flip vertical |
| 5 | Refletir na diagonal do canto superior esquerdo ao inferior direito | -transpose |
| 6 | Girar 90° | -rotate 90 |
| 7 | Refletir na diagonal do canto superior direito ao inferior esquerdo | -transverse |
| 8 | Girar 270° | -rotate 270 |
O mapeamento segue as definições de orientação do ExifTool. Quando a imagem principal não tem tag de orientação, este exemplo mantém a geometria dela inalterada. Ele não consegue deduzir para que lado uma foto sem tag deveria ficar. Um valor fora de 1–8 é um erro.
Use o jpegtran para rotação sem perdas
jpegtran reorganiza os coeficientes DCT quantizados, evitando o ciclo de decodificação e recompressão
do JPEG de uma rotação com GD ou ImageMagick. Aqui, “sem perdas” descreve essa transformação de
coeficientes; os bytes e os metadados do arquivo de saída vão mudar.
Transformações perfeitas dependem dos limites dos blocos JPEG. -perfect recusa uma transformação
não suportada; -trim descartaria pixels das bordas. Use também -strict para tratar avisos do
decodificador, incluindo dados de imagem truncados, como falhas. Essas opções estão documentadas no
guia de uso do libjpeg-turbo.
Este exemplo cria uma cópia para exibição. -copy icc mantém o perfil de cor, mas descarta os demais
metadados da origem, incluindo GPS, detalhes da câmera, campos de copyright, XMP, comentários e
miniaturas incorporadas. Em seguida, o ExifTool grava um novo valor EXIF Orientation igual a 1.
Guarde o original se você precisar dos metadados dele. Usar -copy all exigiria uma política
separada para campos de orientação e pré-visualizações desatualizados.
Prepare a linha de comando no Linux
Use o PHP CLI com a extensão EXIF, o jpegtran do libjpeg-turbo e o ExifTool instalados e disponíveis
no PATH. Este passo a passo foi testado no Linux com PHP 8.5.10, libjpeg-turbo 3.2.0 e ExifTool
13.55. Para a saída, use um diretório local privado em um sistema de arquivos que suporte hard links.
Verifique sua instalação atual antes de salvar o script:
php -d extension=exif -r 'if (!extension_loaded("exif") || !function_exists("proc_open")) { fwrite(STDERR, "PHP needs EXIF and proc_open.\n"); exit(1); } echo "PHP ", PHP_VERSION, ": EXIF and proc_open available\n";' &&
jpegtran -version &&
exiftool -ver
A opção -d extension=exif ativa uma extensão EXIF compartilhada já instalada para esta execução. Se a
EXIF já estiver ativada, omita essa opção em todos os comandos; carregá-la duas vezes gera um aviso.
Se o PHP não conseguir carregá-la, instale a extensão correspondente ao seu build do PHP antes de
continuar. Consulte as
instruções de instalação da EXIF no PHP e as
opções de configuração da CLI.
As verificações de versão devem identificar o libjpeg-turbo e o ExifTool, sem avisos de
inicialização do PHP.
Salve o comando PHP completo
Salve isto como orient-jpeg.php. Ele aceita um caminho de entrada e um novo caminho de saída. Ele usa
exif_read_data() com seções separadas
e rejeita avisos na leitura de metadados. Até JPEGs com orientação 1 e sem tag passam pelo jpegtran;
a ausência de uma tag de orientação nunca é motivo para copiar bytes sem verificação.
proc_open() com um array de argumentos executa
as ferramentas sem um shell. Os caminhos de entrada são resolvidos para caminhos absolutos, então um
nome de arquivo que começa com hífen não é interpretado como opção de ferramenta. Ao adaptar este
comando para um worker, escolha os caminhos no código da sua aplicação; o exemplo não autoriza
caminhos fornecidos por um cliente HTTP.
<?php
declare(strict_types=1);
function runTool(array $arguments): void
{
$process = proc_open($arguments, [
0 => ['file', '/dev/null', 'r'],
1 => ['file', '/dev/null', 'w'],
2 => STDERR,
], $pipes);
if ($process === false || proc_close($process) !== 0) {
throw new RuntimeException($arguments[0] . ' failed; see the diagnostic above.');
}
}
function orientJpeg(string $input, string $output): string
{
$source = realpath($input);
if ($source === false || !is_file($source) || !is_readable($source)) {
throw new RuntimeException('Input must be an existing, readable local file.');
}
$directory = realpath(dirname($output));
if ($directory === false || !is_dir($directory) || !is_writable($directory)) {
throw new RuntimeException('Output directory must exist and be writable.');
}
$destination = $directory . DIRECTORY_SEPARATOR . basename($output);
if (file_exists($destination) || is_link($destination)) {
throw new RuntimeException('Output already exists; choose a new filename.');
}
$metadata = exif_read_data($source, null, true);
if ($metadata === false) {
throw new RuntimeException('Cannot read JPEG headers.');
}
$orientation = $metadata['IFD0']['Orientation'] ?? 1;
$transforms = [
1 => [],
2 => ['-flip', 'horizontal'],
3 => ['-rotate', '180'],
4 => ['-flip', 'vertical'],
5 => ['-transpose'],
6 => ['-rotate', '90'],
7 => ['-transverse'],
8 => ['-rotate', '270'],
];
if (!is_int($orientation) || !isset($transforms[$orientation])) {
throw new RuntimeException('EXIF Orientation must be an integer from 1 to 8.');
}
$temporary = tempnam($directory, '.orient-');
if ($temporary === false) {
throw new RuntimeException('Cannot create a temporary output.');
}
try {
runTool([
'jpegtran', '-strict', '-perfect', '-copy', 'icc',
...$transforms[$orientation], '-outfile', $temporary, $source,
]);
runTool(['exiftool', '-overwrite_original', '-IFD0:Orientation#=1', $temporary]);
$written = exif_read_data($temporary, null, true);
if (($written['IFD0']['Orientation'] ?? null) !== 1) {
throw new RuntimeException('Could not verify the output orientation tag.');
}
// A hard link publishes the finished file without replacing an existing name.
if (!link($temporary, $destination)) {
throw new RuntimeException('Could not create the output; choose a new filename.');
}
} finally {
unlink($temporary);
}
return $destination;
}
// Reject PHP warnings, including unreadable or malformed metadata, instead of guessing.
set_error_handler(static function (int $severity, string $message): never {
throw new ErrorException($message, 0, $severity);
});
try {
if ($argc !== 3) {
throw new RuntimeException('Usage: php orient-jpeg.php INPUT.jpg OUTPUT.jpg');
}
if (!extension_loaded('exif') || !function_exists('proc_open')) {
throw new RuntimeException('PHP needs EXIF and proc_open enabled.');
}
$output = orientJpeg($argv[1], $argv[2]);
fwrite(STDOUT, "Saved: $output\n");
} catch (Throwable $error) {
fwrite(STDERR, 'Failed: ' . $error->getMessage() . "\n");
exit(1);
}
Somente o arquivo temporário é passado para a opção
-overwrite_original
do ExifTool. A origem permanece intacta. A função link()
do PHP cria o destino depois que o processamento é concluído com sucesso; um arquivo ou symlink
existente é recusado. Um bloco finally remove o nome temporário tanto em caso de sucesso quanto
de erros comuns.
Execute em um JPEG enviado
Coloque um JPEG real chamado photo.jpg ao lado do script e execute:
php -d extension=exif orient-jpeg.php photo.jpg photo-fixed.jpg
Uma execução bem-sucedida imprime Saved: seguido do caminho absoluto de saída e termina com status 0.
Em caso de falha, o comando grava um diagnóstico na saída de erro padrão e termina com status 1.
Executar o mesmo comando de novo recusa sobrescrever photo-fixed.jpg. Coloque entre aspas os caminhos que
contêm espaços.
Inspecione os metadados resultantes:
exiftool -n -IFD0:Orientation -ImageWidth -ImageHeight ./photo-fixed.jpg
Espere que Orientation seja 1. As orientações 5–8 trocam a largura pela altura. Abra a saída e confira um elemento assimétrico, como um texto, para confirmar tanto a rotação quanto se a imagem está espelhada. Reprocessar essa saída com outro nome de arquivo deve manter a geometria da imagem inalterada.
Trate arquivos recusados e tarefas em lote
Um diagnóstico “transformation is not perfect” significa que a operação solicitada não consegue
transformar todos os blocos da borda. Uma rotação de 90° de um JPEG com um bloco inferior parcial é
um exemplo. Guarde o original e decida se a sua aplicação permite recortar a imagem ou gerar um
derivado decodificado e recodificado. Remover -perfect altera essa decisão silenciosamente e pode
deixar uma faixa da borda com a orientação incorreta.
Arquivos vazios, arquivos que não são JPEG, dados de imagem truncados e valores de orientação inválidos falham em vez de produzir uma mensagem de sucesso. Entradas em JPEG progressivo são aceitas; este comando não solicita saída progressiva. Ele também não promete conversão para um perfil de compatibilidade JPEG específico.
Para uma fila ou um lote, invoque o comando uma vez por entrada, com um destino distinto, e registre o status de saída de cada execução. Só tente de novo os arquivos recusados depois de decidir como tratar a falha deles. Mantenha essas tarefas fora da requisição HTTP: imponha limites de tamanho de upload e de recursos, mantenha os diagnósticos das ferramentas nos logs do worker e retorne uma mensagem de falha simples ao cliente. O comando local cobre a normalização da orientação, não a autenticação de uploads nem os timeouts do worker.
