Saltar al contenido principal

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​

Autenticación Requerida

@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 por options.success.
  • Los errores van a options.error. Sin error, 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 con try/catch: define siempre error.
  • 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. y The current browser does not support image compression.
  • Con strict: true (default), success recibe el archivo original solo si el comprimido pesa más, mimeType se fijó explícitamente igual al tipo del archivo (con el default 'auto' no aplica), retainExif es false y 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ónFirmaDescripción
isPositiveNumber(value: number) => booleantrue si el valor es finito y mayor que 0
isImageType(value: string) => booleantrue si el MIME empieza por image/
imageTypeToExtension(value: string) => stringMIME a extensión, p. ej. 'image/jpeg' → '.jpg'
getStringFromCharCode(dataView: DataView, start: number, length: number) => stringLee length caracteres desde start de un DataView
arrayBufferToDataURL(arrayBuffer: ArrayBuffer, mimeType: string) => stringCodifica un buffer como Data URL
resetAndGetOrientation(arrayBuffer: ArrayBuffer) => numberDevuelve 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) => numberRedondea 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[]) => Uint8ArrayInserta segmentos EXIF en un JPEG
canvasToBlob(canvas: HTMLCanvasElement, callback: (blob: Blob | null) => void, type?: string, quality?: number) => voidcanvas.toBlob con respaldo vía Data URL
dataURLtoBlob(dataURI: string) => BlobConvierte 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.8 como punto de partida (buen balance calidad/tamaño)
  • Establece maxWidth y maxHeight según el contexto de uso
  • Activa checkOrientation: true para fotos de móviles
  • Usa strict: true para evitar aumentar tamaño innecesariamente
  • Implementa manejo de errores con callback error
  • Convierte PNG grandes a JPEG con convertTypes y convertSize

Evitar​

  • No uses quality muy baja (< 0.5) para imágenes visibles
  • No omitas maxWidth/maxHeight para imágenes de usuarios
  • No uses retainExif: true si 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

Referencias​