Principais pontos
- Armazene as posições de recorte em relação às dimensões de origem ou como frações normalizadas.
- Corrija a orientação EXIF antes de converter as coordenadas do ponteiro.
- Separe o retângulo de pré-visualização na tela das dimensões finais de saída.
Uma ferramenta de recorte em JavaScript tem duas funções: ajudar uma pessoa a escolher uma região e descrever essa região sem ambiguidade. Misturar pixels da pré-visualização, coordenadas escaladas por CSS e pixels de origem é a causa mais comum de recortes incorretos.
O que mais importa
- Evite decodificar imagens muito grandes repetidamente em dispositivos com memória limitada.
- Valide novamente as coordenadas e as dimensões mínimas no servidor.
Estabeleça um único modelo de coordenadas de referência
Uma ferramenta de recorte no navegador lida com pelo menos três espaços: coordenadas da viewport vindas de eventos de ponteiro, coordenadas renderizadas dentro da pré-visualização e pixels de origem na imagem decodificada. clientWidth e clientHeight descrevem a caixa CSS, enquanto naturalWidth e naturalHeight descrevem as dimensões decodificadas. Salvar o retângulo de pré-visualização como se estivesse em pixels de origem faz a seleção se deslocar sempre que o tamanho da pré-visualização muda.
Use coordenadas de origem normalizadas no modelo persistente. Armazene x, y, largura e altura como frações de zero a um, ou armazene coordenadas de cantos normalizadas. Converta para valores renderizados apenas na exibição e para pixels de origem inteiros apenas na renderização. Declare se as bordas inferior e direita são exclusivas, para que toda implementação arredonde e meça a mesma região.
function normalizeCrop(crop, sourceWidth, sourceHeight) {
return {
x: crop.x / sourceWidth,
y: crop.y / sourceHeight,
width: crop.width / sourceWidth,
height: crop.height / sourceHeight,
}
}
function cropToSourcePixels(crop, sourceWidth, sourceHeight) {
return {
left: Math.round(crop.x * sourceWidth),
top: Math.round(crop.y * sourceHeight),
right: Math.round((crop.x + crop.width) * sourceWidth),
bottom: Math.round((crop.y + crop.height) * sourceHeight),
}
}Espaço da viewport
Coordenadas do ponteiro relativas à viewport do navegador, antes de subtrair o retângulo delimitador da pré-visualização.
Espaço da pré-visualização
Pixels CSS usados para desenhar a seleção interativa.
Espaço de origem
Pixels na imagem com orientação normalizada, usados para o recorte de referência.
Espaço de saída
Pixels no derivado final codificado, que podem diferir das dimensões de origem da região de recorte.
Mapeie os pixels exibidos, não apenas a caixa do elemento
O conteúdo pode não ocupar todo o elemento img. object-fit: contain pode criar áreas vazias ao redor da imagem (letterboxing), enquanto object-fit: cover oculta parte da origem antes de a sobreposição de recorte ser aplicada. Calcule o retângulo real da imagem renderizada, incluindo escala e deslocamento, e depois inverta essa transformação. Com uma pré-visualização completa sem transformações, um mapeamento básico é sourceX = previewX multiplicado por naturalWidth dividido por renderedImageWidth.
Eventos de ponteiro informam posições na viewport, e getBoundingClientRect retorna coordenadas relativas à viewport; assim, subtrair left e top do retângulo do conteúdo gera valores relativos ao elemento, sem uma correção de rolagem separada. Os deslocamentos de rolagem só importam quando coordenadas de página, como pageX, entram no cálculo. Se transformações CSS implementarem o zoom ou a rotação da pré-visualização, inclua a inversa delas no mapeamento ou mantenha essas transformações em um único modelo. Restrinja os valores normalizados finais ao intervalo válido (clamp) e rejeite uma região vazia ou invertida em vez de corrigi-la silenciosamente.
Normalize a orientação antes dos cálculos de coordenadas
Arquivos de câmera costumam armazenar dados de pixels em paisagem com metadados que instruem os visualizadores a girá-los. Por isso, a largura, a altura e os eixos exibidos podem diferir da matriz codificada. Defina que as coordenadas de recorte se referem a uma origem com orientação corrigida, crie a pré-visualização sob essa convenção e envie o estado de orientação com a solicitação de recorte. Misturar coordenadas de exibição corrigidas com pixels de origem não corrigidos produz recortes girados ou espelhados.
Não presuma que todo fluxo de decodificação e de canvas aplica os metadados da mesma forma. Verifique, com arquivos de teste, todos os casos de orientação que ocorrem entre os uploads que você recebe, incluindo rotações de 90 graus em que largura e altura se invertem. Depois que o backend normalizar a orientação, remova ou atualize os metadados de orientação antigos no resultado para que visualizadores posteriores não girem novamente os pixels já corrigidos.
Mantenha a interação separada da codificação no canvas
O ideal é que o arraste atualize um pequeno modelo de recorte e uma sobreposição de baixo custo, em vez de codificar repetidamente um bitmap grande. Use a captura de ponteiro para que o arraste continue ativo quando o ponteiro sair de uma alça, e restrinja o movimento em coordenadas normalizadas. Renderize uma pré-visualização em resolução mais baixa que preserve a proporção da origem. A seleção final ainda pode se referir ao original porque o mapeamento é explícito.
O canvas é útil para uma pré-visualização imediata. A forma de drawImage com nove argumentos aceita um retângulo de origem e um retângulo de destino, permitindo desenhar os pixels de origem selecionados em um pequeno canvas de pré-visualização. A proporção de pixels do dispositivo deve alterar a resolução interna do bitmap do canvas, não o recorte armazenado. Aplique debounce ao trabalho de pré-visualização não essencial e evite ler dados de pixels a cada movimento do ponteiro.
function renderCrop(image, crop, maxDimension = 1024) {
const width = crop.right - crop.left
const height = crop.bottom - crop.top
if (![width, height, maxDimension].every((value) => Number.isFinite(value) && value > 0)) {
throw new Error('Crop dimensions must be finite and positive')
}
const scale = Math.min(1, maxDimension / Math.max(width, height))
const canvas = document.createElement('canvas')
canvas.width = Math.max(1, Math.round(width * scale))
canvas.height = Math.max(1, Math.round(height * scale))
const context = canvas.getContext('2d')
if (context == null) throw new Error('2D canvas is unavailable')
context.drawImage(
image,
crop.left,
crop.top,
width,
height,
0,
0,
canvas.width,
canvas.height,
)
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob == null) reject(new Error('The browser could not encode the crop'))
// Browsers fall back to image/png when the requested type is unsupported
else if (blob.type !== 'image/webp') reject(new Error('Encoder fell back to ' + blob.type))
else resolve(blob)
}, 'image/webp', 0.86)
})
}Atualização do modelo
Registre a seleção normalizada de forma síncrona para que a interação continue previsível.
Renderização de pré-visualização
Desenhe uma representação em escala depois que a seleção mudar, sem tratá-la como o arquivo de produção.
Renderização definitiva
Aplique as coordenadas de origem validadas em um pipeline de backend após o envio.
Controle a memória do navegador e os efeitos colaterais da exportação
O tamanho do arquivo comprimido é uma estimativa ruim da memória decodificada. Uma fotografia grande se expande para a largura multiplicada pela altura multiplicada pelo armazenamento por pixel, e o canvas pode exigir buffers adicionais. Limite a contagem de pixels da origem, use uma resolução de pré-visualização limitada e evite manter vários canvas ou cópias decodificadas. URLs de Blob evitam a expansão do base64, mas revogue cada URL após a substituição ou a desmontagem.
A exportação do canvas pode alterar metadados, perfis de cor, animação e qualidade do codificador. Ela também pode falhar quando uma imagem de outra origem (cross-origin) não tem permissão para uso em canvas, deixando o canvas contaminado (tainted). Trate a saída do navegador como uma conveniência, a menos que essas alterações sejam aceitáveis e testadas. Fazer upload do original junto com os metadados de recorte preserva uma versão mestre recuperável e produz uma codificação consistente entre os dispositivos clientes.
Envie um contrato de recorte restrito e validado
Envie o identificador da origem, a convenção de orientação, as coordenadas normalizadas, a predefinição de destino e uma versão de esquema. O servidor deve analisar os números, rejeitar valores não finitos, impor 0 <= x1 < x2 <= 1 e 0 <= y1 < y2 <= 1, exigir dimensões mínimas úteis e limitar as predefinições de saída aceitas. A validação no cliente melhora o feedback, mas não pode autorizar processamento caro nem caminhos de armazenamento confiáveis.
Mantenha as credenciais de processamento e as opções de transformação irrestritas fora do navegador. Envie o identificador do arquivo original junto com a geometria de recorte normalizada para um endpoint de backend restrito. O backend deve verificar a autorização para o arquivo, restringir as coordenadas aos limites válidos, rejeitar regiões vazias ou abaixo do tamanho mínimo, aplicar o recorte sobre a origem com orientação normalizada e redimensionar em uma operação separada quando também for necessária uma versão final exata.
Teste a equivalência entre a pré-visualização e o resultado
Crie dados de teste determinísticos para imagens de origem quadradas, em retrato, panorâmicas, transparentes, rotacionadas e muito grandes. Selecione regiões em todas as bordas e compare a saída do backend com a pré-visualização do navegador usando tolerâncias de coordenadas que considerem o arredondamento documentado. Inclua o letterboxing do object-fit, o zoom da pré-visualização, a rolagem da página, o zoom do navegador e os tamanhos do bitmap interno do canvas em alta densidade.
Teste a interação sem dispositivo apontador. As alças de recorte precisam de foco visível, nomes acessíveis claros e operações de teclado para mover e redimensionar a região. Anuncie falhas de validação importantes e a conclusão, mas não cada incremento de arrasto. Teste também o cancelamento, a falha de upload, a URL de pré-visualização revogada, metadados malformados, conteúdo não suportado e a repetição de requisição, para que o comportamento confiável não se limite ao recorte no cenário em que tudo dá certo.
Detalhes técnicos que vale a pena conhecer
- naturalWidth e naturalHeight descrevem as dimensões da imagem decodificada, enquanto clientWidth e clientHeight descrevem a caixa CSS. As coordenadas de recorte precisam ser convertidas entre esses espaços.
- Imagens de câmera podem conter metadados de orientação que alteram a largura, a altura e os eixos visuais sem alterar a ordem dos pixels armazenados; por isso, a orientação precisa ser normalizada antes dos cálculos com coordenadas.
- URLs de Blob evitam o acréscimo de aproximadamente um terço no tamanho das data URLs em base64, mas cada URL mantém seu Blob subjacente até que URL.revokeObjectURL seja chamado ou o documento seja descarregado.
- A exportação do canvas pode alterar perfis de cor, metadados, animação e qualidade de codificação, o que é mais um motivo para tratar o resultado do navegador como uma pré-visualização, e não como a versão mestre.
- As coordenadas do ponteiro são relativas à viewport até serem convertidas considerando o retângulo delimitador do elemento, a rolagem, o zoom e quaisquer transformações CSS aplicadas.
- Imagens decodificadas muito grandes podem exceder os limites de canvas em dispositivos móveis mesmo quando o upload comprimido é pequeno; por isso, as dimensões da pré-visualização e a contagem de pixels da origem precisam de limites separados.
Uma abordagem prática
- 1
Leia uma vez as dimensões e a orientação da origem e depois defina um sistema de coordenadas único.
- 2
Renderize uma pré-visualização leve e atualize um modelo de recorte em vez de reescrever o arquivo a cada arraste.
- 3
Envie as coordenadas normalizadas junto com o upload ou com a solicitação de processamento subsequente.
- 4
Compare o resultado do backend com a pré-visualização usando arquivos de teste rotacionados, panorâmicos e em retrato.
Limite da arquitetura
O canvas do navegador é útil para prévias interativas, mas consome memória do cliente e não garante uma codificação idêntica entre dispositivos. Não deixe um celular responsável por todas as imagens derivadas de produção.
Perguntas frequentes
As coordenadas de recorte em JavaScript devem ser armazenadas em pixels ou em porcentagens?
Frações normalizadas costumam ser as mais portáveis. Converta-as em pixels da imagem de origem no backend depois de verificar a orientação e as dimensões da origem.
Por que um recorte no backend difere da seleção feita no navegador?
As causas comuns são usar a caixa do elemento img em vez do retângulo do conteúdo renderizado, ignorar os deslocamentos de object-fit, misturar pixels CSS com pixels da imagem de origem ou aplicar a orientação EXIF de forma diferente.
Um Blob gerado pelo canvas é adequado como imagem mestre?
Geralmente não. A exportação do canvas pode alterar metadados, perfis, animação e codificação. Preserve o original enviado por upload e use o resultado do canvas como prévia, a menos que essas mudanças sejam intencionais.
Por que o canvas pode falhar com uma imagem carregada de outro domínio?
Se o servidor remoto não conceder o acesso cross-origin necessário, desenhar a imagem pode contaminar (taint) o canvas e bloquear a leitura de pixels ou a exportação.
O que o servidor deve validar em uma solicitação de recorte?
Valide limites numéricos, ordem das coordenadas, convenção de orientação, propriedade da origem, dimensões mínimas úteis, predefinição de saída, tipo de arquivo, limites de pixels e a autorização para iniciar o processamento.