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.