ImageCompressor
Paquete: @tiendana/utils
Clase moderna de compresión y optimización de imágenes en el navegador, completamente escrita en TypeScript, tree-shakeable y sin dependencias externas.
Características
✅ TypeScript puro - Implementación moderna sin dependencias
✅ Tree-shakeable - Importa solo lo que necesitas
✅ Control total - Dimensiones, calidad, tipo MIME configurable
✅ Orientación EXIF - Corrige automáticamente la rotación de imágenes
✅ Retención de metadatos - Preserva información EXIF si es necesario
✅ Modo estricto - Con mimeType igual al original, conserva el archivo original si la compresión lo agranda
✅ Hooks personalizables - beforeDraw y drew para manipulación avanzada
✅ Soporte multiplataforma - Funciona en todos los navegadores modernos
Instalación
@tiendana/utils está alojado en AWS CodeArtifact privado. Autentica npm con Tiendana CLI (tdn auth npm) antes de instalar; ver la guía de CodeArtifact.
npm install @tiendana/utils
Uso Básico
Importación
import { ImageCompressor } from '@tiendana/utils';
Ejemplo Simple
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
new ImageCompressor(file, {
quality: 0.8,
maxWidth: 1920,
maxHeight: 1080,
success(result) {
console.log('Imagen comprimida:', result);
// result es un Blob optimizado
// Crear FormData para enviar
const formData = new FormData();
formData.append('image', result, 'image.jpg');
// Enviar al servidor
fetch('/api/upload', {
method: 'POST',
body: formData
});
},
error(error) {
console.error('Error al comprimir:', error.message);
}
});
});
Opciones de Configuración
ImageCompressorOptions
interface ImageCompressorOptions {
// Control de calidad
quality?: number; // 0-1, calidad de compresión (default: 0.8)
// Control de dimensiones
maxWidth?: number; // Ancho máximo en píxeles (default: Infinity)
maxHeight?: number; // Alto máximo en píxeles (default: Infinity)
minWidth?: number; // Ancho mínimo en píxeles (default: 0)
minHeight?: number; // Alto mínimo en píxeles (default: 0)
width?: number; // Ancho exacto
height?: number; // Alto exacto
// Modo de redimensionamiento
resize?: 'none' | 'contain' | 'cover'; // Cómo ajustar dimensiones (default: 'none')
// Tipo de imagen
mimeType?: string; // Tipo MIME de salida (default: 'auto')
convertTypes?: string[]; // Tipos a convertir a JPEG (default: ['image/png'])
convertSize?: number; // Tamaño mínimo para conversión (default: 5MB)
// Metadatos
checkOrientation?: boolean; // Corregir orientación EXIF (default: true)
retainExif?: boolean; // Mantener metadatos EXIF (default: false)
// Comportamiento
strict?: boolean; // Conservar el original si la compresión lo agranda; requiere mimeType explícito igual al original (default: true)
// Hooks de personalización
beforeDraw?: (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void;
drew?: (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void;
// Callbacks
success?: (result: File | Blob) => void;
error?: (error: Error) => void;
}
Ciclo de Vida y Errores
new ImageCompressor(file: File | Blob, options?: ImageCompressorOptions)
- La compresión empieza en el constructor: no hay método
start(). El resultado llega poroptions.success. - Los errores van a
options.error. Sinerror, los de validación (los tres mensajes de abajo) se lanzan desde el constructor; los asíncronos (lectura, carga de la imagen,abort()) se lanzan dentro de un callback y no se pueden capturar contry/catch: define siempreerror. - Mensajes de validación:
The first argument must be a File or Blob object.,The first argument must be an image File or Blob object.yThe current browser does not support image compression. - Con
strict: true(default),successrecibe el archivo original solo si el comprimido pesa más,mimeTypese fijó explícitamente igual al tipo del archivo (con el default'auto'no aplica),retainExifesfalsey ninguna restricción de tamaño (width,height,min*,max*) entra en juego.
abort()
abort(): void cancela una compresión en curso. Si el FileReader sigue leyendo, lo aborta y error recibe Aborted to read the image with FileReader.; si la imagen ya cargó (incluida la fase de canvas), invoca error con The compression process has been aborted.. Llamarlo más de una vez no tiene efecto.
const compressor = new ImageCompressor(file, {
quality: 0.8,
success(result) { upload(result); },
error(err) { console.warn(err.message); }
});
// El usuario cancela la subida
cancelButton.addEventListener('click', () => compressor.abort());
Ejemplos de Uso
Compresión con Calidad
new ImageCompressor(file, {
quality: 0.6, // Calidad 60%
success(result) {
console.log('Tamaño original:', file.size);
console.log('Tamaño comprimido:', result.size);
console.log('Reducción:', ((1 - result.size / file.size) * 100).toFixed(2) + '%');
}
});
Redimensionamiento con Límites
new ImageCompressor(file, {
maxWidth: 800,
maxHeight: 600,
quality: 0.8,
success(result) {
console.log('Imagen redimensionada a máximo 800x600');
}
});
Convertir PNG Grande a JPEG
new ImageCompressor(file, {
convertTypes: ['image/png'],
convertSize: 1000000, // 1MB
mimeType: 'image/jpeg',
quality: 0.85,
success(result) {
console.log('PNG convertido a JPEG:', result.type);
}
});
Redimensionamiento con Modo Cover
new ImageCompressor(file, {
width: 400,
height: 400,
resize: 'cover', // Recorta para llenar 400x400
quality: 0.9,
success(result) {
console.log('Thumbnail cuadrado creado');
}
});
Redimensionamiento con Modo Contain
new ImageCompressor(file, {
width: 800,
height: 600,
resize: 'contain', // Ajusta dentro de 800x600 sin recortar
quality: 0.9,
success(result) {
console.log('Imagen ajustada sin recorte');
}
});
Preservar Metadatos EXIF
new ImageCompressor(file, {
quality: 0.8,
retainExif: true, // Mantener metadatos de cámara
checkOrientation: true, // Corregir rotación
success(result) {
console.log('Imagen comprimida con metadatos originales');
}
});
Hooks Personalizados (beforeDraw / drew)
new ImageCompressor(file, {
quality: 0.8,
beforeDraw(ctx, canvas) {
// Ejecuta ANTES de dibujar la imagen
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
},
drew(ctx, canvas) {
// Ejecuta DESPUÉS de dibujar la imagen
// Agregar marca de agua
ctx.font = '20px Arial';
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillText('© Tiendana', 10, canvas.height - 10);
},
success(result) {
console.log('Imagen con marca de agua');
}
});
Modo Estricto Desactivado
new ImageCompressor(file, {
quality: 0.9,
strict: false, // Siempre comprime, incluso si aumenta tamaño
success(result) {
console.log('Compresión aplicada sin importar tamaño final');
}
});
Convertir a WebP
new ImageCompressor(file, {
mimeType: 'image/webp',
quality: 0.8,
success(result) {
console.log('Convertido a WebP:', result.type);
}
});
Uso con Async/Await
function compressImage(file: File, options: ImageCompressorOptions): Promise<Blob> {
return new Promise((resolve, reject) => {
new ImageCompressor(file, {
...options,
success: resolve,
error: reject
});
});
}
// Uso
try {
const compressed = await compressImage(file, {
quality: 0.8,
maxWidth: 1920
});
console.log('Compresión exitosa:', compressed.size);
} catch (error) {
console.error('Error:', error.message);
}
Integración con Formularios
Con Input File Nativo
const fileInput = document.getElementById('avatar') as HTMLInputElement;
fileInput.addEventListener('change', async (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
try {
const compressed = await compressImage(file, {
maxWidth: 500,
maxHeight: 500,
quality: 0.85
});
// Enviar imagen comprimida
const formData = new FormData();
formData.append('avatar', compressed, 'avatar.jpg');
await fetch('/api/users/avatar', {
method: 'POST',
body: formData
});
} catch (error) {
console.error('Error al procesar imagen:', error);
}
});
Con Drag and Drop
const dropzone = document.getElementById('dropzone');
dropzone.addEventListener('drop', async (event) => {
event.preventDefault();
const file = event.dataTransfer?.files[0];
if (!file || !file.type.startsWith('image/')) return;
try {
const compressed = await compressImage(file, {
maxWidth: 1200,
quality: 0.8
});
// Mostrar preview
const preview = document.getElementById('preview') as HTMLImageElement;
preview.src = URL.createObjectURL(compressed);
} catch (error) {
console.error('Error:', error);
}
});
Utilidades Avanzadas
Además de la clase, el paquete exporta las funciones de bajo nivel que usa internamente. Todas se importan desde @tiendana/utils; el tipo ImageCompressorOptions también. Las interfaces internas (LoadData, DrawData, Orientation, AdjustedSizes, SizeConstraints) no se exportan.
import {
isPositiveNumber,
isImageType,
imageTypeToExtension,
getStringFromCharCode,
arrayBufferToDataURL,
resetAndGetOrientation,
parseOrientation,
normalizeDecimalNumber,
getAdjustedSizes,
getExif,
insertExif,
canvasToBlob,
dataURLtoBlob
} from '@tiendana/utils';
| Función | Firma | Descripción |
|---|---|---|
isPositiveNumber | (value: number) => boolean | true si el valor es finito y mayor que 0 |
isImageType | (value: string) => boolean | true si el MIME empieza por image/ |
imageTypeToExtension | (value: string) => string | MIME a extensión, p. ej. 'image/jpeg' → '.jpg' |
getStringFromCharCode | (dataView: DataView, start: number, length: number) => string | Lee length caracteres desde start de un DataView |
arrayBufferToDataURL | (arrayBuffer: ArrayBuffer, mimeType: string) => string | Codifica un buffer como Data URL |
resetAndGetOrientation | (arrayBuffer: ArrayBuffer) => number | Devuelve la orientación EXIF (1 a 8) y la restablece a 1 en el buffer; devuelve 1 si falla |
parseOrientation | (orientation: number) => { rotate: number; scaleX: number; scaleY: number } | Convierte la orientación EXIF en rotación y escala |
normalizeDecimalNumber | (value: number, times = 100000000000) => number | Redondea errores de coma flotante en decimales |
getAdjustedSizes | (constraints: { aspectRatio: number; width: number; height: number }, type?: 'none' | 'contain' | 'cover') => { width: number; height: number } | Calcula ancho y alto que respetan la proporción y el modo de ajuste (default 'none') |
getExif | (arrayBuffer: ArrayBuffer) => number[] | Extrae los segmentos EXIF (APP1) de un JPEG |
insertExif | (arrayBuffer: ArrayBuffer, exifArray: number[]) => Uint8Array | Inserta segmentos EXIF en un JPEG |
canvasToBlob | (canvas: HTMLCanvasElement, callback: (blob: Blob | null) => void, type?: string, quality?: number) => void | canvas.toBlob con respaldo vía Data URL |
dataURLtoBlob | (dataURI: string) => Blob | Convierte un Data URL en Blob; lanza invalid data URI si no es válido |
if (isImageType(file.type)) {
console.log('Es una imagen válida');
}
const ext = imageTypeToExtension('image/jpeg'); // '.jpg'
const dataUrl = arrayBufferToDataURL(arrayBuffer, 'image/png');
const orientation = resetAndGetOrientation(arrayBuffer);
// Calcular el tamaño de un thumbnail con proporción 16:9 que llene 200x200
getAdjustedSizes({ aspectRatio: 16 / 9, width: 200, height: 200 }, 'cover');
canvasToBlob(canvas, (blob) => {
console.log('Canvas convertido:', blob);
}, 'image/jpeg', 0.8);
Buenas Prácticas
Hacer
- Usa
quality: 0.8como punto de partida (buen balance calidad/tamaño) - Establece
maxWidthymaxHeightsegún el contexto de uso - Activa
checkOrientation: truepara fotos de móviles - Usa
strict: truepara evitar aumentar tamaño innecesariamente - Implementa manejo de errores con callback
error - Convierte PNG grandes a JPEG con
convertTypesyconvertSize
Evitar
- No uses
qualitymuy baja (< 0.5) para imágenes visibles - No omitas
maxWidth/maxHeightpara imágenes de usuarios - No uses
retainExif: truesi no necesitas metadatos (aumenta tamaño) - No bloquees UI durante compresión (usa async/await o callbacks)
- No comprimas imágenes ya optimizadas (ej: WebP)
Casos de Uso Comunes
Avatar de Usuario
new ImageCompressor(file, {
maxWidth: 400,
maxHeight: 400,
resize: 'cover',
quality: 0.9,
checkOrientation: true
});
Galería de Productos
new ImageCompressor(file, {
maxWidth: 1200,
maxHeight: 1200,
quality: 0.85,
convertTypes: ['image/png'],
convertSize: 2000000 // 2MB
});
Thumbnails
new ImageCompressor(file, {
width: 200,
height: 200,
resize: 'cover',
quality: 0.7
});
Imagen de Portada
new ImageCompressor(file, {
maxWidth: 1920,
maxHeight: 1080,
resize: 'cover',
quality: 0.85,
checkOrientation: true
});
Compatibilidad
- Navegadores: Chrome/Edge 90+, Firefox 88+, Safari 14+
- Node.js: ❌ Solo navegador (requiere Canvas API)
- TypeScript: 5.0+
Componentes Relacionados
- TdnInputImage - Componente Angular que usa ImageCompressor internamente
- openSnackbar - Notificaciones para feedback de carga