Pillow oferece leitura, transformação e escrita de imagens. Ao processar uploads, limite tamanho e formatos antes de gastar CPU e memória, pois o nome do arquivo não comprova seu conteúdo.
from pathlib import Path
from PIL import Image, ImageOps
def criar_thumbnail(origem: Path, destino: Path) -> None:
with Image.open(origem) as imagem:
imagem = ImageOps.exif_transpose(imagem)
imagem.thumbnail((1200, 1200), Image.Resampling.LANCZOS)
if imagem.mode not in ("RGB", "L"):
imagem = imagem.convert("RGB")
imagem.save(destino, format="JPEG", quality=85, optimize=True)
thumbnail preserva proporção e altera o objeto. exif_transpose aplica a orientação capturada pela câmera. Converter para RGB evita falha ao salvar modos com transparência em JPEG, mas descarte de transparência deve ser uma decisão consciente.
Não sobrescreva o original antes de validar o resultado. Defina limite de bytes e pixels, mantenha Pillow atualizado e armazene uploads fora de caminhos executáveis. O guia de pathlib em Python ajuda no tratamento seguro de caminhos.
A documentação oficial do Pillow, consultada em 22 de julho de 2026, descreve formatos, resampling e recomendações de segurança. Teste orientação, transparência, perfis de cor e imagens grandes com amostras controladas.
Instalação e primeiro diagnóstico
Instale em um ambiente virtual com python -m pip install Pillow. O import continua sendo from PIL import Image por compatibilidade com a biblioteca original. Ao abrir um arquivo, examine format, size e mode: eles descrevem o contêiner detectado, as dimensões e a representação dos pixels.
from PIL import Image
with Image.open("entrada.png") as imagem:
print(imagem.format)
print(imagem.size)
print(imagem.mode)
imagem.load()
Image.open() identifica o arquivo e deixa a leitura de pixels para depois. load() força a decodificação enquanto o arquivo ainda está aberto. Isso é importante porque tentar usar uma imagem lazy depois que o contexto fechou pode falhar. Se for necessário conservar o resultado, faça imagem.copy() dentro do bloco.
Extensão e cabeçalho HTTP não bastam para identificar conteúdo. Pillow reconhece o formato pelos bytes, mas reconhecer não significa considerar seguro. A aplicação deve impor limites de upload, formatos aceitos e dimensões máximas antes de seguir para transformações custosas.
Validar sem processar duas vezes
verify() procura problemas estruturais sem decodificar todos os pixels e invalida o objeto para uso posterior. Por isso, validação e processamento exigem reabrir o arquivo. Capture exceções específicas, rejeite o conteúdo e não tente “consertar” silenciosamente uma entrada desconhecida.
from PIL import Image, UnidentifiedImageError
def validar_imagem(caminho):
try:
with Image.open(caminho) as imagem:
if imagem.format not in {"JPEG", "PNG", "WEBP"}:
raise ValueError("Formato não permitido")
largura, altura = imagem.size
if largura < 1 or altura < 1 or largura * altura > 25_000_000:
raise ValueError("Dimensões não permitidas")
imagem.verify()
except UnidentifiedImageError as erro:
raise ValueError("Arquivo não é uma imagem reconhecida") from erro
Pillow emite DecompressionBombWarning quando uma imagem excede o limite de pixels configurado e pode lançar DecompressionBombError em casos maiores. Não remova globalmente essa proteção. Ajuste Image.MAX_IMAGE_PIXELS apenas com base em requisitos reais e limite também os bytes recebidos, pois pixels e tamanho compactado medem riscos diferentes.
Redimensionar, criar thumbnail ou recortar
resize() produz exatamente as dimensões informadas e pode distorcer a imagem se a proporção mudar. thumbnail() reduz para caber em uma caixa, preserva proporção e nunca amplia por padrão. ImageOps.fit() preenche uma caixa e recorta o excesso, sendo adequado para avatares e cards com proporção fixa.
from PIL import Image, ImageOps
with Image.open("foto.jpg") as original:
corrigida = ImageOps.exif_transpose(original)
card = ImageOps.fit(
corrigida,
(1200, 675),
method=Image.Resampling.LANCZOS,
centering=(0.5, 0.4),
)
card.save("card.jpg", quality=85, optimize=True)
O parâmetro centering desloca a região preservada no recorte. Para enquadramento automático de rostos ou objetos, Pillow sozinho não detecta o assunto; é preciso fornecer coordenadas obtidas por outra etapa. Não prometa um recorte inteligente quando a regra é apenas central.
Filtros de reamostragem afetam custo e qualidade. LANCZOS funciona bem para reduzir fotografias, enquanto NEAREST preserva pixels duros de arte pixelada. Avalie a saída no tamanho de exibição, pois nitidez excessiva e halos podem surgir em bordas de alto contraste.
Orientação, transparência e modos de cor
Fotos de celular podem armazenar pixels em uma orientação e registrar a rotação em EXIF. ImageOps.exif_transpose() materializa essa orientação e remove o marcador correspondente. Faça isso antes de calcular recortes, senão largura e altura aparentes podem estar invertidas.
JPEG não possui canal alfa. Converter uma imagem RGBA diretamente para RGB substitui transparência sem oferecer controle visual, frequentemente criando fundo preto. Componha sobre a cor esperada.
from PIL import Image
def remover_alpha(imagem, fundo=(255, 255, 255)):
rgba = imagem.convert("RGBA")
base = Image.new("RGBA", rgba.size, fundo + (255,))
composta = Image.alpha_composite(base, rgba)
return composta.convert("RGB")
Os modos 1, L, P, RGB, RGBA e CMYK têm significados distintos. Para publicação web, RGB e RGBA são os casos mais comuns. Imagens CMYK vindas de impressão podem apresentar cores inesperadas se apenas convertidas; fluxos com fidelidade de cor devem considerar perfis ICC e validar o resultado em ferramentas apropriadas.
Escolher formato e opções de saída
JPEG é adequado para fotografias sem transparência. PNG preserva transparência e bordas exatas, mas pode ficar grande em fotos. WebP pode oferecer boa compactação, desde que o destino aceite o formato. A escolha deve considerar o tipo de conteúdo, compatibilidade e política do produto.
quality não é uma porcentagem objetiva de qualidade visual. Valores muito altos aumentam bastante o arquivo com ganho pequeno. optimize=True faz trabalho adicional para codificar melhor, e progressive=True permite que JPEG carregue em passagens. Compare amostras representativas, tamanho e artefatos, em vez de definir um número universal.
imagem.save(
"saida.jpg",
format="JPEG",
quality=85,
optimize=True,
progressive=True,
exif=b"",
)
Metadados podem carregar localização, modelo da câmera e outros dados privados. Remover EXIF em derivados públicos costuma ser prudente, mas preserve direitos autorais e requisitos editoriais quando aplicáveis. Nunca altere o original arquivado sem uma política explícita.
Processamento em memória e em lote
APIs podem receber bytes por stream. BytesIO integra esse conteúdo ao Pillow, mas não elimina a necessidade de limitar o corpo antes de armazená-lo na memória.
from io import BytesIO
from PIL import Image
def dimensoes(data: bytes) -> tuple[int, int]:
if len(data) > 10 * 1024 * 1024:
raise ValueError("Arquivo excede 10 MB")
with Image.open(BytesIO(data)) as imagem:
imagem.load()
return imagem.size
Em lote, gere um destino separado, salve primeiro em arquivo temporário no mesmo volume e substitua o resultado somente depois de uma gravação bem-sucedida. Evite capturar Exception e continuar sem relatório. Registre origem, destino, formato, dimensões e erro, sem incluir conteúdo binário no log.
Pillow executa trabalho de CPU e decodificação. Em um servidor assíncrono, não faça grandes transformações diretamente no loop de eventos; envie o trabalho a uma fila ou executor com concorrência limitada. Muitos jobs paralelos multiplicam uso de memória.
Operações úteis sem perder o original
crop() usa uma caixa (esquerda, topo, direita, base) e devolve outra imagem. rotate() pode expandir a tela com expand=True. ImageOps.contain() preserva toda a imagem dentro de um limite; ImageOps.pad() completa a área restante com uma cor. Escolha a operação pela regra editorial, não apenas pelo tamanho final.
Pillow também oferece ajustes com ImageEnhance e filtros com ImageFilter. Cada transformação destrutiva deve ter parâmetros documentados e fixtures de regressão. Reaplicar compressão JPEG em cada etapa acumula perdas; mantenha as operações em memória e salve uma única vez no fim.
Testes e checklist de produção
Crie fixtures pequenas para paisagem, retrato, transparência, animação, EXIF rotacionado, modo CMYK, arquivo truncado e dimensões acima do limite. Confirme tamanho final, modo, formato e proporção. Para qualidade visual, uma comparação humana continua importante; métricas automatizadas não detectam todo artefato relevante.
Imagens GIF e WebP podem conter vários frames. Processar apenas o primeiro pode remover animação sem aviso. Decida se o produto rejeita animações, preserva todos os frames ou gera conscientemente uma miniatura estática. Documente a escolha na interface.
Antes de publicar o pipeline, confirme limites de bytes e pixels, lista de formatos, nomes de destino gerados pela aplicação, diretório sem execução, Pillow atualizado e descarte de temporários. Verifique ainda orientação, fundo de transparência, metadados, consumo máximo de memória e comportamento diante de entradas corrompidas. Um pipeline seguro é previsível tanto no resultado visual quanto na falha.