Conclusiones clave
- Almacena las posiciones de recorte en relación con las dimensiones de la fuente o como fracciones normalizadas.
- Corrige la orientación EXIF antes de traducir las coordenadas del puntero.
- Separa el rectángulo de vista previa en pantalla de las dimensiones finales de salida.
Un recortador en JavaScript cumple dos tareas: ayudar a una persona a elegir una región y describir esa región sin ambigüedades. Mezclar píxeles de la vista previa, coordenadas escaladas por CSS y píxeles de la fuente es la causa más común de recortes incorrectos.
Lo más importante
- Evita decodificar imágenes muy grandes repetidamente en dispositivos con memoria limitada.
- Vuelve a validar las coordenadas y las dimensiones mínimas en el servidor.
Establece un modelo de coordenadas autoritativo único
Un recortador en el navegador maneja al menos tres espacios: las coordenadas del viewport de los eventos de puntero, las coordenadas renderizadas dentro de la vista previa y los píxeles de la fuente en la imagen decodificada. clientWidth y clientHeight describen la caja CSS, mientras que naturalWidth y naturalHeight describen las dimensiones decodificadas. Guardar el rectángulo de la vista previa como si fueran píxeles de la fuente provoca que la selección tenga deriva cada vez que cambia el tamaño de la vista previa.
Usa coordenadas normalizadas de la fuente para el modelo persistente. Almacena x, y, ancho y alto como fracciones de cero a uno, o guarda coordenadas de esquina normalizadas. Convierte a valores renderizados solo para mostrar, y a píxeles enteros de la fuente solo al renderizar. Indica si los bordes inferior y derecho son exclusivos, para que cada implementación redondee y mida la misma región.
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),
}
}Espacio del viewport
Coordenadas del puntero relativas al viewport del navegador antes de restar el rectángulo delimitador de la vista previa.
Espacio de la vista previa
Píxeles CSS usados para dibujar la selección interactiva.
Espacio de la fuente
Píxeles en la imagen normalizada por orientación, usados para el recorte autoritativo.
Espacio de salida
Píxeles en el derivado final codificado, que pueden diferir de las dimensiones de la fuente de la región de recorte.
Mapea los píxeles mostrados, no solo la caja del elemento
El contenido puede no ocupar todo el elemento img. object-fit: contain puede generar un efecto de letterboxing, mientras que object-fit: cover oculta parte de la fuente antes de aplicar la superposición de recorte. Calcula el rectángulo real de la imagen renderizada, incluyendo su escala y desplazamiento, y luego invierte esa transformación. Con una vista previa completa sin transformar, un mapeo básico es sourceX = previewX multiplicado por naturalWidth dividido entre renderedImageWidth.
Los eventos de puntero reportan posiciones del viewport, y getBoundingClientRect devuelve coordenadas relativas al viewport, por lo que restar los valores left y top del rectángulo de contenido da valores relativos al elemento sin necesidad de una corrección de desplazamiento aparte. El desplazamiento de scroll solo importa cuando se mezclan coordenadas de página como pageX. Si las transformaciones CSS implementan zoom o rotación en la vista previa, incluye su inversa en el mapeo o mantén esas transformaciones en un único modelo. Limita los valores normalizados finales y rechaza una región vacía o invertida en lugar de repararla silenciosamente.
Normaliza la orientación antes de la aritmética de coordenadas
Los archivos de cámara suelen almacenar los datos de píxeles en horizontal con metadatos que indican a los visores que los roten. El ancho, el alto y los ejes mostrados pueden, por lo tanto, diferir de la matriz codificada. Decide que las coordenadas de recorte se refieren a una fuente con orientación correcta, crea la vista previa bajo esa convención y envía el estado de orientación junto con la solicitud de recorte. Mezclar coordenadas de visualización corregidas con píxeles de la fuente sin corregir produce recortes rotados o reflejados.
No asumas que cada ruta de decodificación y canvas aplica los metadatos de la misma manera. Prueba con datos de prueba todos los casos de orientación usados por tu población de subidas, incluidas las rotaciones de 90 grados donde el ancho y el alto se intercambian. Una vez que el backend normaliza la orientación, elimina o actualiza los metadatos de orientación antiguos en su resultado para que los visores posteriores no vuelvan a rotar los píxeles ya corregidos.
Mantén la interacción separada de la codificación en el canvas
Arrastrar debería actualizar un modelo de recorte pequeño y una superposición económica, no codificar repetidamente un mapa de bits grande. Usa la captura de puntero para que un arrastre siga activo aunque el puntero salga de un asa, y restringe el movimiento en coordenadas normalizadas. Renderiza una vista previa de menor resolución que conserve la proporción de la fuente. La selección final aún puede referirse al original porque el mapeo es explícito.
Canvas resulta útil para una vista previa inmediata. La forma de drawImage con nueve argumentos acepta un rectángulo de origen y un rectángulo de destino, lo que permite dibujar los píxeles seleccionados de la fuente en un pequeño canvas de vista previa. La relación de píxeles del dispositivo debería cambiar la resolución interna del canvas, no el recorte almacenado. Aplica un retardo (debounce) al trabajo de vista previa no esencial y evita leer datos de píxeles en cada movimiento del puntero.
function renderCrop(image, crop) {
const width = crop.right - crop.left
const height = crop.bottom - crop.top
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
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,
width,
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)
})
}Actualización del modelo
Registra la selección normalizada de forma síncrona para que la interacción siga siendo predecible.
Renderizado de vista previa
Dibuja una representación escalada después de que cambia la selección, sin tratarla como el archivo de producción.
Renderizado de referencia
Aplica las coordenadas de la fuente validadas en un pipeline de backend después del envío.
Controla la memoria del navegador y los efectos secundarios de la exportación
El tamaño del archivo comprimido es un mal indicador de la memoria decodificada. Una fotografía grande se expande a ancho multiplicado por alto multiplicado por su almacenamiento de píxeles, y el canvas puede requerir búferes adicionales. Limita el número de píxeles de la fuente, usa una resolución de vista previa acotada y evita conservar varios canvases o copias decodificadas. Las URL de blob evitan la expansión de base64, pero revoca cada URL después de reemplazarla o al desmontar.
La exportación de canvas puede alterar los metadatos, los perfiles de color, la animación y la calidad del codificador. También puede fallar cuando una imagen de origen cruzado carece de permiso para usarse en canvas, dejando el canvas contaminado. Trata la salida del navegador como una comodidad, salvo que esos cambios sean aceptables y estén probados. Subir el original junto con los metadatos de recorte conserva un archivo maestro recuperable y produce una codificación consistente entre dispositivos cliente.
Envía un contrato de recorte acotado y validado
Envía el identificador de la fuente, la convención de orientación, las coordenadas normalizadas, el ajuste preestablecido de destino y una versión de esquema. El servidor debe analizar los números, rechazar valores no finitos, exigir 0 <= x1 < x2 <= 1 y 0 <= y1 < y2 <= 1, requerir dimensiones mínimas útiles y limitar los ajustes preestablecidos de salida aceptados. La validación del cliente mejora la retroalimentación, pero no puede autorizar un procesamiento costoso ni rutas de almacenamiento confiables.
Mantén las credenciales de procesamiento y las opciones de transformación sin restricciones fuera del navegador. Envía el identificador del recurso original más la geometría de recorte normalizada a un endpoint de backend acotado. El backend debería autorizar el recurso, limitar las coordenadas, rechazar regiones vacías o demasiado pequeñas, aplicar el recorte sobre la fuente con la orientación normalizada y redimensionar en una operación separada cuando también se requiera una variante final exacta.
Prueba la equivalencia entre la vista previa y el resultado
Crea datos de prueba deterministas para fuentes cuadradas, verticales, panorámicas, transparentes, rotadas y muy grandes. Selecciona regiones en cada borde y compara la salida del backend con la vista previa del navegador usando tolerancias de coordenadas que consideren el redondeo documentado. Incluye el letterboxing de object-fit, el zoom de la vista previa, el desplazamiento de la página, el zoom del navegador y los tamaños internos del canvas de alta densidad.
Prueba la interacción sin usar un puntero. Las asas de recorte necesitan foco visible, nombres accesibles claros y operaciones de teclado para mover y redimensionar la región. Anuncia los fallos de validación importantes y la finalización, pero no cada pequeño incremento de arrastre. Prueba también la cancelación, el fallo de subida, una URL de vista previa revocada, metadatos malformados, contenido no compatible y la repetición de una solicitud, de modo que el camino ideal de recorte no sea el único comportamiento confiable.
Detalles técnicos que conviene conocer
- naturalWidth y naturalHeight describen las dimensiones de la imagen decodificada, mientras que clientWidth y clientHeight describen la caja CSS. Las coordenadas de recorte deben traducirse entre esos espacios.
- Las imágenes de cámara pueden llevar metadatos de orientación que cambian el ancho, el alto y los ejes visuales sin cambiar el orden de píxeles almacenado, por lo que la orientación debe normalizarse antes de la aritmética de coordenadas.
- Las URL de blob evitan la sobrecarga de tamaño de aproximadamente un tercio que añaden las URL de datos en base64, pero cada URL conserva su Blob subyacente hasta que se llama a URL.revokeObjectURL o se cierra el documento.
- La exportación de canvas puede cambiar los perfiles de color, los metadatos, la animación y la calidad de codificación, lo cual es otra razón para tratar el resultado del navegador como una vista previa y no como el archivo maestro.
- Las coordenadas del puntero son relativas al viewport hasta que se traducen mediante el rectángulo delimitador del elemento, el desplazamiento, el zoom y cualquier transformación CSS aplicada.
- Las imágenes decodificadas muy grandes pueden superar los límites de canvas en móviles incluso cuando la subida comprimida es pequeña, por lo que las dimensiones de vista previa y el número de píxeles de la fuente necesitan límites independientes.
Un enfoque práctico
- 1
Lee las dimensiones y la orientación de la fuente una sola vez y establece un único sistema de coordenadas.
- 2
Renderiza una vista previa liviana y actualiza un modelo de recorte en lugar de reescribir el archivo con cada arrastre.
- 3
Envía las coordenadas normalizadas junto con la subida o la solicitud de procesamiento posterior.
- 4
Compara el resultado del backend con la vista previa usando datos de prueba rotados, panorámicos y en formato vertical.
Límite de la arquitectura
El canvas del navegador es útil para vistas previas interactivas, pero consume memoria del cliente y no garantiza una codificación idéntica en todos los dispositivos. No hagas que un teléfono sea responsable de cada derivado de producción.
Preguntas frecuentes
¿Deberían almacenarse las coordenadas de recorte en JavaScript en píxeles o en porcentajes?
Las fracciones normalizadas suelen ser las más portables. Conviértelas a píxeles de origen en el backend después de verificar la orientación y las dimensiones de origen.
¿Por qué un recorte en el backend difiere de la selección en el navegador?
Las causas comunes son usar la caja del elemento img en lugar del rectángulo de contenido renderizado, ignorar los desplazamientos de object-fit, mezclar píxeles CSS con píxeles de la fuente, o aplicar la orientación EXIF de forma diferente.
¿Es un Blob generado con canvas adecuado como imagen maestra?
Por lo general, no. La exportación mediante canvas puede cambiar los metadatos, los perfiles, la animación y la codificación. Conserva el original subido y usa el resultado del canvas como vista previa, a menos que esos cambios sean intencionales.
¿Por qué puede fallar canvas con una imagen cargada desde otro dominio?
Si el servidor remoto no otorga el acceso de origen cruzado necesario, dibujar la imagen puede contaminar el canvas y bloquear la lectura de píxeles o la exportación.
¿Qué debe validar el servidor en una solicitud de recorte?
Valida los límites numéricos, el orden de las coordenadas, la convención de orientación, la propiedad de la fuente, las dimensiones mínimas útiles, el ajuste preestablecido de salida, el tipo de archivo, los límites de píxeles y la autorización para iniciar el procesamiento.