Web APIs

JavaScript Blob API: Complete Binary Data Handling Guide

Master the Blob API in JavaScript for handling binary data. Learn file creation, manipulation, streaming, and advanced blob operations.

By JavaScriptDoc Team
blobbinaryfilesdatajavascript

JavaScript Blob API: Complete Binary Data Handling Guide

The Blob API represents immutable, raw data that can be read as text or binary data, or converted into a ReadableStream for processing. It's essential for file handling, data downloads, and binary operations in web applications.

Understanding the Blob API

A Blob (Binary Large Object) represents a file-like object of immutable, raw data. Blobs can represent data that isn't necessarily in a JavaScript-native format.

// Create a simple text blob
const textBlob = new Blob(['Hello, World!'], { type: 'text/plain' });
console.log('Blob size:', textBlob.size, 'bytes');
console.log('Blob type:', textBlob.type);

// Create a JSON blob
const jsonData = { name: 'John', age: 30 };
const jsonBlob = new Blob([JSON.stringify(jsonData)], {
  type: 'application/json',
});

// Create a blob from multiple parts
const parts = ['Hello', ' ', 'World', '!'];
const multiPartBlob = new Blob(parts, { type: 'text/plain' });

// Create a blob from typed arrays
const uint8Array = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
const binaryBlob = new Blob([uint8Array], { type: 'application/octet-stream' });

// Blob properties
console.log({
  size: textBlob.size, // Size in bytes
  type: textBlob.type, // MIME type
  // Blobs are immutable - no direct data access
});

Blob Creation and Manipulation

Advanced Blob Manager

class BlobManager {
  constructor() {
    this.blobs = new Map();
    this.urls = new Map();
  }

  // Create blob from various sources
  createBlob(data, options = {}) {
    const { type = 'application/octet-stream', endings = 'transparent' } =
      options;

    let blobParts = [];

    // Handle different data types
    if (typeof data === 'string') {
      blobParts = [data];
    } else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
      blobParts = [data];
    } else if (data instanceof Blob) {
      return data; // Already a blob
    } else if (Array.isArray(data)) {
      blobParts = data;
    } else if (typeof data === 'object') {
      // Convert object to JSON
      blobParts = [JSON.stringify(data, null, 2)];
      options.type = options.type || 'application/json';
    }

    const blob = new Blob(blobParts, { type, endings });

    // Store blob with metadata
    const id = this.generateId();
    this.blobs.set(id, {
      blob,
      created: Date.now(),
      name: options.name,
      metadata: options.metadata || {},
    });

    return { id, blob };
  }

  // Slice blob
  sliceBlob(blob, start = 0, end = blob.size, contentType = '') {
    return blob.slice(start, end, contentType);
  }

  // Concatenate multiple blobs
  concatenateBlobs(blobs, type = '') {
    const parts = blobs
      .map((blob) => {
        if (blob instanceof Blob) {
          return blob;
        } else if (typeof blob === 'string') {
          const { id } = blob;
          const stored = this.blobs.get(id);
          return stored ? stored.blob : null;
        }
        return null;
      })
      .filter(Boolean);

    return new Blob(parts, { type });
  }

  // Read blob as text
  async readAsText(blob, encoding = 'UTF-8') {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);

      reader.readAsText(blob, encoding);
    });
  }

  // Read blob as ArrayBuffer
  async readAsArrayBuffer(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);

      reader.readAsArrayBuffer(blob);
    });
  }

  // Read blob as data URL
  async readAsDataURL(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);

      reader.readAsDataURL(blob);
    });
  }

  // Read blob as stream
  readAsStream(blob) {
    if (blob.stream) {
      return blob.stream();
    }

    // Fallback for older browsers
    return new ReadableStream({
      async start(controller) {
        const buffer = await this.readAsArrayBuffer(blob);
        controller.enqueue(new Uint8Array(buffer));
        controller.close();
      },
    });
  }

  // Convert blob to different formats
  async convertBlob(blob, targetType) {
    switch (targetType) {
      case 'base64':
        const dataUrl = await this.readAsDataURL(blob);
        return dataUrl.split(',')[1]; // Remove data URL prefix

      case 'hex':
        const buffer = await this.readAsArrayBuffer(blob);
        const bytes = new Uint8Array(buffer);
        return Array.from(bytes)
          .map((b) => b.toString(16).padStart(2, '0'))
          .join('');

      case 'binary':
        const arrayBuffer = await this.readAsArrayBuffer(blob);
        return new Uint8Array(arrayBuffer);

      case 'text':
        return this.readAsText(blob);

      default:
        throw new Error(`Unknown target type: ${targetType}`);
    }
  }

  // Create object URL
  createObjectURL(blob) {
    const url = URL.createObjectURL(blob);

    // Track URLs for cleanup
    this.urls.set(url, {
      blob,
      created: Date.now(),
    });

    return url;
  }

  // Revoke object URL
  revokeObjectURL(url) {
    URL.revokeObjectURL(url);
    this.urls.delete(url);
  }

  // Cleanup all object URLs
  cleanupURLs() {
    this.urls.forEach((_, url) => {
      URL.revokeObjectURL(url);
    });
    this.urls.clear();
  }

  // Generate unique ID
  generateId() {
    return `blob-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  // Get blob info
  getBlobInfo(blob) {
    return {
      size: blob.size,
      type: blob.type,
      sizeFormatted: this.formatSize(blob.size),
      isImage: blob.type.startsWith('image/'),
      isVideo: blob.type.startsWith('video/'),
      isAudio: blob.type.startsWith('audio/'),
      isText: blob.type.startsWith('text/'),
      isJSON: blob.type === 'application/json',
    };
  }

  // Format file size
  formatSize(bytes) {
    const units = ['B', 'KB', 'MB', 'GB', 'TB'];
    let size = bytes;
    let unitIndex = 0;

    while (size >= 1024 && unitIndex < units.length - 1) {
      size /= 1024;
      unitIndex++;
    }

    return `${size.toFixed(2)} ${units[unitIndex]}`;
  }
}

// Usage
const blobManager = new BlobManager();

// Create text blob
const { id: textId, blob: textBlob } = blobManager.createBlob('Hello, World!', {
  type: 'text/plain',
  name: 'greeting.txt',
});

// Create JSON blob
const { blob: jsonBlob } = blobManager.createBlob({
  user: 'John',
  data: [1, 2, 3],
});

// Read blob content
blobManager.readAsText(textBlob).then((text) => {
  console.log('Text content:', text);
});

// Convert to base64
blobManager.convertBlob(textBlob, 'base64').then((base64) => {
  console.log('Base64:', base64);
});

// Create object URL
const url = blobManager.createObjectURL(textBlob);
console.log('Object URL:', url);

// Cleanup
window.addEventListener('beforeunload', () => {
  blobManager.cleanupURLs();
});

File Operations

File Handler

class FileHandler {
  constructor() {
    this.blobManager = new BlobManager();
    this.supportedTypes = new Map();
    this.initializeSupportedTypes();
  }

  // Initialize supported file types
  initializeSupportedTypes() {
    // Images
    this.supportedTypes.set('image', [
      'image/jpeg',
      'image/png',
      'image/gif',
      'image/webp',
      'image/svg+xml',
    ]);

    // Videos
    this.supportedTypes.set('video', ['video/mp4', 'video/webm', 'video/ogg']);

    // Audio
    this.supportedTypes.set('audio', [
      'audio/mpeg',
      'audio/ogg',
      'audio/wav',
      'audio/webm',
    ]);

    // Documents
    this.supportedTypes.set('document', [
      'application/pdf',
      'application/msword',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    ]);

    // Text
    this.supportedTypes.set('text', [
      'text/plain',
      'text/html',
      'text/css',
      'text/javascript',
      'application/json',
      'application/xml',
    ]);
  }

  // Create File from Blob
  createFile(blob, filename, options = {}) {
    const { lastModified = Date.now(), type = blob.type } = options;

    // File extends Blob
    return new File([blob], filename, {
      type,
      lastModified,
    });
  }

  // Handle file input
  handleFileInput(input) {
    return new Promise((resolve) => {
      const handler = (event) => {
        const files = Array.from(event.target.files || []);
        input.removeEventListener('change', handler);
        resolve(files);
      };

      input.addEventListener('change', handler);
      input.click();
    });
  }

  // Create file input programmatically
  createFileInput(options = {}) {
    const { accept = '*/*', multiple = false, capture = false } = options;

    const input = document.createElement('input');
    input.type = 'file';
    input.accept = accept;
    input.multiple = multiple;

    if (capture) {
      input.capture = capture;
    }

    return input;
  }

  // Read file with progress
  async readFileWithProgress(file, onProgress) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onloadstart = () => {
        onProgress?.({ loaded: 0, total: file.size, percent: 0 });
      };

      reader.onprogress = (event) => {
        if (event.lengthComputable) {
          const percent = (event.loaded / event.total) * 100;
          onProgress?.({
            loaded: event.loaded,
            total: event.total,
            percent: Math.round(percent),
          });
        }
      };

      reader.onload = () => {
        onProgress?.({ loaded: file.size, total: file.size, percent: 100 });
        resolve(reader.result);
      };

      reader.onerror = () => reject(reader.error);

      reader.readAsArrayBuffer(file);
    });
  }

  // Validate file
  validateFile(file, options = {}) {
    const {
      maxSize = Infinity,
      allowedTypes = [],
      allowedExtensions = [],
    } = options;

    const errors = [];

    // Check file size
    if (file.size > maxSize) {
      errors.push({
        type: 'size',
        message: `File size (${this.blobManager.formatSize(file.size)}) exceeds maximum (${this.blobManager.formatSize(maxSize)})`,
      });
    }

    // Check file type
    if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
      errors.push({
        type: 'type',
        message: `File type "${file.type}" is not allowed`,
      });
    }

    // Check file extension
    if (allowedExtensions.length > 0) {
      const extension = file.name.split('.').pop().toLowerCase();
      if (!allowedExtensions.includes(extension)) {
        errors.push({
          type: 'extension',
          message: `File extension ".${extension}" is not allowed`,
        });
      }
    }

    return {
      valid: errors.length === 0,
      errors,
    };
  }

  // Process image file
  async processImageFile(file, options = {}) {
    const {
      maxWidth = 1920,
      maxHeight = 1080,
      quality = 0.9,
      format = 'image/jpeg',
    } = options;

    // Create image
    const img = new Image();
    const url = this.blobManager.createObjectURL(file);

    return new Promise((resolve, reject) => {
      img.onload = () => {
        // Calculate new dimensions
        let { width, height } = img;

        if (width > maxWidth || height > maxHeight) {
          const ratio = Math.min(maxWidth / width, maxHeight / height);
          width *= ratio;
          height *= ratio;
        }

        // Create canvas
        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = height;

        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, width, height);

        // Convert to blob
        canvas.toBlob(
          (blob) => {
            this.blobManager.revokeObjectURL(url);

            if (blob) {
              resolve({
                blob,
                width,
                height,
                originalWidth: img.width,
                originalHeight: img.height,
              });
            } else {
              reject(new Error('Failed to process image'));
            }
          },
          format,
          quality
        );
      };

      img.onerror = () => {
        this.blobManager.revokeObjectURL(url);
        reject(new Error('Failed to load image'));
      };

      img.src = url;
    });
  }

  // Extract file metadata
  async extractMetadata(file) {
    const metadata = {
      name: file.name,
      size: file.size,
      type: file.type,
      lastModified: new Date(file.lastModified),
      extension: file.name.split('.').pop().toLowerCase(),
    };

    // Type-specific metadata
    if (file.type.startsWith('image/')) {
      metadata.image = await this.extractImageMetadata(file);
    } else if (file.type.startsWith('video/')) {
      metadata.video = await this.extractVideoMetadata(file);
    } else if (file.type.startsWith('audio/')) {
      metadata.audio = await this.extractAudioMetadata(file);
    }

    return metadata;
  }

  // Extract image metadata
  async extractImageMetadata(file) {
    return new Promise((resolve) => {
      const img = new Image();
      const url = this.blobManager.createObjectURL(file);

      img.onload = () => {
        this.blobManager.revokeObjectURL(url);
        resolve({
          width: img.width,
          height: img.height,
          aspectRatio: img.width / img.height,
        });
      };

      img.onerror = () => {
        this.blobManager.revokeObjectURL(url);
        resolve(null);
      };

      img.src = url;
    });
  }

  // Extract video metadata
  async extractVideoMetadata(file) {
    return new Promise((resolve) => {
      const video = document.createElement('video');
      const url = this.blobManager.createObjectURL(file);

      video.onloadedmetadata = () => {
        resolve({
          duration: video.duration,
          width: video.videoWidth,
          height: video.videoHeight,
          aspectRatio: video.videoWidth / video.videoHeight,
        });

        this.blobManager.revokeObjectURL(url);
      };

      video.onerror = () => {
        this.blobManager.revokeObjectURL(url);
        resolve(null);
      };

      video.src = url;
    });
  }

  // Extract audio metadata
  async extractAudioMetadata(file) {
    return new Promise((resolve) => {
      const audio = new Audio();
      const url = this.blobManager.createObjectURL(file);

      audio.onloadedmetadata = () => {
        resolve({
          duration: audio.duration,
        });

        this.blobManager.revokeObjectURL(url);
      };

      audio.onerror = () => {
        this.blobManager.revokeObjectURL(url);
        resolve(null);
      };

      audio.src = url;
    });
  }

  // Split large file into chunks
  async *splitFileIntoChunks(file, chunkSize = 1024 * 1024) {
    // 1MB chunks
    let offset = 0;

    while (offset < file.size) {
      const chunk = file.slice(offset, offset + chunkSize);
      yield {
        chunk,
        offset,
        index: Math.floor(offset / chunkSize),
        isLast: offset + chunkSize >= file.size,
      };

      offset += chunkSize;
    }
  }

  // Merge file chunks
  async mergeFileChunks(chunks, filename, type) {
    const blob = new Blob(chunks, { type });
    return this.createFile(blob, filename);
  }
}

// Usage
const fileHandler = new FileHandler();

// Create file input
const input = fileHandler.createFileInput({
  accept: 'image/*',
  multiple: true,
});

// Handle file selection
fileHandler.handleFileInput(input).then(async (files) => {
  for (const file of files) {
    // Validate file
    const validation = fileHandler.validateFile(file, {
      maxSize: 5 * 1024 * 1024, // 5MB
      allowedTypes: ['image/jpeg', 'image/png'],
    });

    if (!validation.valid) {
      console.error('Validation errors:', validation.errors);
      continue;
    }

    // Extract metadata
    const metadata = await fileHandler.extractMetadata(file);
    console.log('File metadata:', metadata);

    // Process image
    if (file.type.startsWith('image/')) {
      const processed = await fileHandler.processImageFile(file, {
        maxWidth: 800,
        maxHeight: 600,
        quality: 0.8,
      });

      console.log('Processed image:', processed);
    }
  }
});

// Read file with progress
const file = new File(['Large content...'], 'large.txt');
fileHandler.readFileWithProgress(file, (progress) => {
  console.log(`Progress: ${progress.percent}%`);
});

Blob Streaming

Stream Processing

class BlobStreamProcessor {
  constructor() {
    this.transformers = new Map();
  }

  // Create readable stream from blob
  createReadableStream(blob, options = {}) {
    const {
      chunkSize = 64 * 1024, // 64KB chunks
    } = options;

    if (blob.stream) {
      return blob.stream();
    }

    // Polyfill for older browsers
    return new ReadableStream({
      async start(controller) {
        let offset = 0;

        while (offset < blob.size) {
          const chunk = blob.slice(offset, offset + chunkSize);
          const buffer = await chunk.arrayBuffer();

          controller.enqueue(new Uint8Array(buffer));
          offset += chunkSize;
        }

        controller.close();
      },
    });
  }

  // Create blob from stream
  async createBlobFromStream(stream, type = 'application/octet-stream') {
    const chunks = [];
    const reader = stream.getReader();

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(value);
      }
    } finally {
      reader.releaseLock();
    }

    return new Blob(chunks, { type });
  }

  // Transform stream
  transformStream(stream, transformer) {
    const transformStream = new TransformStream({
      transform: transformer,
    });

    return stream.pipeThrough(transformStream);
  }

  // Compress stream
  compressStream(stream, format = 'gzip') {
    if ('CompressionStream' in window) {
      return stream.pipeThrough(new CompressionStream(format));
    }

    // Fallback
    return this.transformStream(stream, async (chunk, controller) => {
      // Simple compression simulation
      controller.enqueue(chunk);
    });
  }

  // Decompress stream
  decompressStream(stream, format = 'gzip') {
    if ('DecompressionStream' in window) {
      return stream.pipeThrough(new DecompressionStream(format));
    }

    // Fallback
    return stream;
  }

  // Encrypt stream
  async encryptStream(stream, key) {
    const iv = crypto.getRandomValues(new Uint8Array(12));

    return this.transformStream(stream, async (chunk, controller) => {
      const encrypted = await crypto.subtle.encrypt(
        {
          name: 'AES-GCM',
          iv: iv,
        },
        key,
        chunk
      );

      controller.enqueue(new Uint8Array(encrypted));
    });
  }

  // Process stream in chunks
  async processStreamInChunks(stream, processor, options = {}) {
    const {
      chunkSize = 1024 * 1024, // 1MB
      onProgress,
    } = options;

    const reader = stream.getReader();
    const results = [];
    let totalBytesRead = 0;

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        totalBytesRead += value.byteLength;

        // Process chunk
        const result = await processor(value, {
          bytesRead: totalBytesRead,
          chunk: results.length,
        });

        results.push(result);

        // Report progress
        onProgress?.({
          bytesRead: totalBytesRead,
          chunks: results.length,
        });
      }
    } finally {
      reader.releaseLock();
    }

    return results;
  }

  // Tee stream for multiple consumers
  teeStream(stream, count = 2) {
    const tees = [];
    let currentStream = stream;

    for (let i = 0; i < count - 1; i++) {
      const [tee1, tee2] = currentStream.tee();
      tees.push(tee1);
      currentStream = tee2;
    }

    tees.push(currentStream);
    return tees;
  }

  // Monitor stream progress
  monitorStream(stream, onProgress) {
    let bytesRead = 0;

    return this.transformStream(stream, (chunk, controller) => {
      bytesRead += chunk.byteLength;
      onProgress({ bytesRead, chunk: chunk.byteLength });
      controller.enqueue(chunk);
    });
  }

  // Hash stream content
  async hashStream(stream, algorithm = 'SHA-256') {
    const reader = stream.getReader();
    const chunks = [];

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(value);
      }
    } finally {
      reader.releaseLock();
    }

    // Concatenate chunks
    const totalLength = chunks.reduce(
      (sum, chunk) => sum + chunk.byteLength,
      0
    );
    const concatenated = new Uint8Array(totalLength);
    let offset = 0;

    for (const chunk of chunks) {
      concatenated.set(chunk, offset);
      offset += chunk.byteLength;
    }

    // Calculate hash
    const hashBuffer = await crypto.subtle.digest(algorithm, concatenated);

    // Convert to hex string
    return Array.from(new Uint8Array(hashBuffer))
      .map((b) => b.toString(16).padStart(2, '0'))
      .join('');
  }
}

// Usage
const streamProcessor = new BlobStreamProcessor();

// Create blob
const largeBlob = new Blob(['Large content...'.repeat(10000)]);

// Create stream
const stream = streamProcessor.createReadableStream(largeBlob);

// Process stream
streamProcessor.processStreamInChunks(
  stream,
  async (chunk) => {
    console.log('Processing chunk:', chunk.byteLength, 'bytes');
    // Process chunk...
    return chunk;
  },
  {
    onProgress: ({ bytesRead, chunks }) => {
      console.log(`Processed ${bytesRead} bytes in ${chunks} chunks`);
    },
  }
);

// Compress blob
async function compressBlob(blob) {
  const stream = streamProcessor.createReadableStream(blob);
  const compressed = streamProcessor.compressStream(stream);
  return streamProcessor.createBlobFromStream(compressed);
}

// Monitor download progress
function downloadWithProgress(blob, filename) {
  const stream = streamProcessor.createReadableStream(blob);
  const monitoredStream = streamProcessor.monitorStream(
    stream,
    ({ bytesRead }) => {
      console.log(`Downloaded: ${bytesRead} bytes`);
    }
  );

  // Create download...
}

Download Management

Blob Downloader

class BlobDownloader {
  constructor() {
    this.downloads = new Map();
    this.blobManager = new BlobManager();
  }

  // Download blob as file
  downloadBlob(blob, filename, options = {}) {
    const { onStart, onProgress, onComplete, onError } = options;

    const downloadId = this.generateDownloadId();
    const url = this.blobManager.createObjectURL(blob);

    // Create download info
    const download = {
      id: downloadId,
      blob,
      filename,
      url,
      size: blob.size,
      startTime: Date.now(),
      status: 'pending',
    };

    this.downloads.set(downloadId, download);

    try {
      // Notify start
      onStart?.(download);

      // Create download link
      const link = document.createElement('a');
      link.href = url;
      link.download = filename;
      link.style.display = 'none';

      document.body.appendChild(link);

      // Trigger download
      link.click();

      // Cleanup
      setTimeout(() => {
        document.body.removeChild(link);
        this.blobManager.revokeObjectURL(url);

        download.status = 'completed';
        download.endTime = Date.now();

        onComplete?.(download);
      }, 100);

      return downloadId;
    } catch (error) {
      download.status = 'failed';
      download.error = error;

      onError?.(error, download);

      throw error;
    }
  }

  // Download with custom UI
  downloadWithUI(blob, filename) {
    const ui = this.createDownloadUI(filename, blob.size);
    document.body.appendChild(ui.container);

    // Simulate progress (browsers don't provide real download progress)
    let progress = 0;
    const interval = setInterval(() => {
      progress += Math.random() * 20;
      progress = Math.min(progress, 100);

      ui.updateProgress(progress);

      if (progress >= 100) {
        clearInterval(interval);
        setTimeout(() => {
          ui.complete();
          setTimeout(() => ui.remove(), 2000);
        }, 500);
      }
    }, 200);

    // Start download
    this.downloadBlob(blob, filename);

    return ui;
  }

  // Create download UI
  createDownloadUI(filename, fileSize) {
    const container = document.createElement('div');
    container.className = 'download-ui';
    container.innerHTML = `
      <div class="download-card">
        <div class="download-icon">📁</div>
        <div class="download-info">
          <div class="download-filename">${this.escapeHtml(filename)}</div>
          <div class="download-size">${this.blobManager.formatSize(fileSize)}</div>
        </div>
        <div class="download-progress">
          <div class="progress-bar">
            <div class="progress-fill"></div>
          </div>
          <div class="progress-text">0%</div>
        </div>
        <button class="download-cancel">✕</button>
      </div>
    `;

    this.applyDownloadStyles();

    const progressFill = container.querySelector('.progress-fill');
    const progressText = container.querySelector('.progress-text');
    const cancelBtn = container.querySelector('.download-cancel');

    return {
      container,
      updateProgress(percent) {
        progressFill.style.width = `${percent}%`;
        progressText.textContent = `${Math.round(percent)}%`;
      },
      complete() {
        container.querySelector('.download-card').classList.add('completed');
        progressText.textContent = 'Complete';
        cancelBtn.style.display = 'none';
      },
      remove() {
        container.remove();
      },
    };
  }

  // Apply download UI styles
  applyDownloadStyles() {
    if (document.getElementById('download-ui-styles')) return;

    const style = document.createElement('style');
    style.id = 'download-ui-styles';
    style.textContent = `
      .download-ui {
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 10000;
      }

      .download-card {
        background: white;
        border-radius: 8px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        padding: 16px;
        width: 300px;
        display: flex;
        align-items: center;
        gap: 12px;
        position: relative;
        transition: all 0.3s ease;
      }

      .download-card.completed {
        background: #e8f5e9;
      }

      .download-icon {
        font-size: 32px;
      }

      .download-info {
        flex: 1;
      }

      .download-filename {
        font-weight: 600;
        margin-bottom: 4px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .download-size {
        font-size: 12px;
        color: #666;
      }

      .download-progress {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        height: 4px;
        background: #f0f0f0;
        border-radius: 0 0 8px 8px;
        overflow: hidden;
      }

      .progress-bar {
        height: 100%;
        position: relative;
      }

      .progress-fill {
        height: 100%;
        background: #4caf50;
        width: 0%;
        transition: width 0.3s ease;
      }

      .progress-text {
        position: absolute;
        bottom: 8px;
        right: 40px;
        font-size: 12px;
        color: #666;
      }

      .download-cancel {
        position: absolute;
        top: 8px;
        right: 8px;
        background: none;
        border: none;
        font-size: 16px;
        cursor: pointer;
        color: #666;
        padding: 4px;
      }

      .download-cancel:hover {
        color: #333;
      }
    `;

    document.head.appendChild(style);
  }

  // Create download from URL
  async downloadFromURL(url, filename, options = {}) {
    const { onProgress, headers = {}, method = 'GET' } = options;

    try {
      const response = await fetch(url, { method, headers });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      // Get total size
      const contentLength = response.headers.get('content-length');
      const total = contentLength ? parseInt(contentLength, 10) : 0;

      // Read response as stream
      const reader = response.body.getReader();
      const chunks = [];
      let loaded = 0;

      while (true) {
        const { done, value } = await reader.read();

        if (done) break;

        chunks.push(value);
        loaded += value.length;

        if (onProgress && total) {
          onProgress({
            loaded,
            total,
            percent: (loaded / total) * 100,
          });
        }
      }

      // Create blob from chunks
      const blob = new Blob(chunks);

      // Determine filename
      if (!filename) {
        const disposition = response.headers.get('content-disposition');
        filename =
          this.extractFilenameFromDisposition(disposition) ||
          this.extractFilenameFromURL(url) ||
          'download';
      }

      // Download blob
      return this.downloadBlob(blob, filename, options);
    } catch (error) {
      console.error('Download failed:', error);
      throw error;
    }
  }

  // Extract filename from Content-Disposition header
  extractFilenameFromDisposition(disposition) {
    if (!disposition) return null;

    const match = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
    if (match && match[1]) {
      return match[1].replace(/['"]/g, '');
    }

    return null;
  }

  // Extract filename from URL
  extractFilenameFromURL(url) {
    try {
      const urlObj = new URL(url);
      const path = urlObj.pathname;
      const filename = path.substring(path.lastIndexOf('/') + 1);
      return filename || null;
    } catch {
      return null;
    }
  }

  // Batch download
  async batchDownload(items, options = {}) {
    const { concurrent = 3, onItemComplete, onAllComplete } = options;

    const queue = [...items];
    const active = new Set();
    const results = [];

    const downloadNext = async () => {
      if (queue.length === 0 || active.size >= concurrent) {
        return;
      }

      const item = queue.shift();
      active.add(item);

      try {
        const result = await this.downloadBlob(item.blob, item.filename);
        results.push({ success: true, item, result });
        onItemComplete?.({ success: true, item, result });
      } catch (error) {
        results.push({ success: false, item, error });
        onItemComplete?.({ success: false, item, error });
      }

      active.delete(item);

      if (queue.length > 0) {
        downloadNext();
      } else if (active.size === 0) {
        onAllComplete?.(results);
      }
    };

    // Start concurrent downloads
    for (let i = 0; i < Math.min(concurrent, queue.length); i++) {
      downloadNext();
    }
  }

  // Create zip download
  async createZipDownload(files, zipFilename = 'archive.zip') {
    // This would require a zip library like JSZip
    console.log('Creating zip with files:', files);

    // Placeholder - in real implementation, use JSZip
    const zipContent = files.map((f) => f.filename).join('\n');
    const zipBlob = new Blob([zipContent], { type: 'application/zip' });

    return this.downloadBlob(zipBlob, zipFilename);
  }

  // Generate download ID
  generateDownloadId() {
    return `dl-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  // Escape HTML
  escapeHtml(str) {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
  }

  // Get download history
  getDownloadHistory() {
    return Array.from(this.downloads.values()).sort(
      (a, b) => b.startTime - a.startTime
    );
  }

  // Clear download history
  clearDownloadHistory() {
    this.downloads.clear();
  }
}

// Usage
const downloader = new BlobDownloader();

// Download text file
const textBlob = new Blob(['Hello, World!'], { type: 'text/plain' });
downloader.downloadBlob(textBlob, 'hello.txt');

// Download with UI
const largeBlob = new Blob(['Large content...'.repeat(1000)]);
downloader.downloadWithUI(largeBlob, 'large-file.dat');

// Download from URL with progress
downloader.downloadFromURL('https://example.com/file.pdf', null, {
  onProgress: ({ loaded, total, percent }) => {
    console.log(`Downloaded: ${percent.toFixed(2)}% (${loaded}/${total})`);
  },
});

// Batch download
const files = [
  { blob: new Blob(['File 1']), filename: 'file1.txt' },
  { blob: new Blob(['File 2']), filename: 'file2.txt' },
  { blob: new Blob(['File 3']), filename: 'file3.txt' },
];

downloader.batchDownload(files, {
  concurrent: 2,
  onItemComplete: ({ success, item }) => {
    console.log(
      `Downloaded ${item.filename}: ${success ? 'Success' : 'Failed'}`
    );
  },
  onAllComplete: (results) => {
    console.log('All downloads complete:', results);
  },
});

Image Processing

Blob Image Processor

class BlobImageProcessor {
  constructor() {
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
  }

  // Load image from blob
  async loadImageFromBlob(blob) {
    const url = URL.createObjectURL(blob);

    try {
      const img = new Image();

      return new Promise((resolve, reject) => {
        img.onload = () => {
          URL.revokeObjectURL(url);
          resolve(img);
        };

        img.onerror = () => {
          URL.revokeObjectURL(url);
          reject(new Error('Failed to load image'));
        };

        img.src = url;
      });
    } catch (error) {
      URL.revokeObjectURL(url);
      throw error;
    }
  }

  // Resize image
  async resizeImage(blob, options = {}) {
    const {
      width,
      height,
      maxWidth,
      maxHeight,
      maintainAspectRatio = true,
      quality = 0.9,
      format = blob.type || 'image/jpeg',
    } = options;

    const img = await this.loadImageFromBlob(blob);

    // Calculate dimensions
    let targetWidth = width || img.width;
    let targetHeight = height || img.height;

    if (maintainAspectRatio) {
      const ratio = img.width / img.height;

      if (maxWidth && targetWidth > maxWidth) {
        targetWidth = maxWidth;
        targetHeight = targetWidth / ratio;
      }

      if (maxHeight && targetHeight > maxHeight) {
        targetHeight = maxHeight;
        targetWidth = targetHeight * ratio;
      }
    }

    // Resize
    this.canvas.width = targetWidth;
    this.canvas.height = targetHeight;

    this.ctx.drawImage(img, 0, 0, targetWidth, targetHeight);

    // Convert to blob
    return this.canvasToBlob(format, quality);
  }

  // Crop image
  async cropImage(blob, cropArea, options = {}) {
    const { quality = 0.9, format = blob.type || 'image/jpeg' } = options;

    const img = await this.loadImageFromBlob(blob);

    // Set canvas size to crop area
    this.canvas.width = cropArea.width;
    this.canvas.height = cropArea.height;

    // Draw cropped area
    this.ctx.drawImage(
      img,
      cropArea.x,
      cropArea.y,
      cropArea.width,
      cropArea.height,
      0,
      0,
      cropArea.width,
      cropArea.height
    );

    return this.canvasToBlob(format, quality);
  }

  // Apply filters
  async applyFilters(blob, filters = {}) {
    const img = await this.loadImageFromBlob(blob);

    this.canvas.width = img.width;
    this.canvas.height = img.height;

    // Apply CSS filters
    const filterString = Object.entries(filters)
      .map(([key, value]) => {
        switch (key) {
          case 'brightness':
            return `brightness(${value}%)`;
          case 'contrast':
            return `contrast(${value}%)`;
          case 'saturate':
            return `saturate(${value}%)`;
          case 'grayscale':
            return `grayscale(${value}%)`;
          case 'sepia':
            return `sepia(${value}%)`;
          case 'blur':
            return `blur(${value}px)`;
          case 'hue-rotate':
            return `hue-rotate(${value}deg)`;
          default:
            return '';
        }
      })
      .filter(Boolean)
      .join(' ');

    this.ctx.filter = filterString;
    this.ctx.drawImage(img, 0, 0);

    return this.canvasToBlob(blob.type || 'image/jpeg');
  }

  // Rotate image
  async rotateImage(blob, degrees, options = {}) {
    const { quality = 0.9, format = blob.type || 'image/jpeg' } = options;

    const img = await this.loadImageFromBlob(blob);
    const radians = (degrees * Math.PI) / 180;

    // Calculate new dimensions
    const sin = Math.abs(Math.sin(radians));
    const cos = Math.abs(Math.cos(radians));

    const newWidth = img.width * cos + img.height * sin;
    const newHeight = img.width * sin + img.height * cos;

    this.canvas.width = newWidth;
    this.canvas.height = newHeight;

    // Rotate
    this.ctx.translate(newWidth / 2, newHeight / 2);
    this.ctx.rotate(radians);
    this.ctx.drawImage(img, -img.width / 2, -img.height / 2);

    // Reset transformation
    this.ctx.setTransform(1, 0, 0, 1, 0, 0);

    return this.canvasToBlob(format, quality);
  }

  // Convert image format
  async convertImageFormat(blob, targetFormat, quality = 0.9) {
    const img = await this.loadImageFromBlob(blob);

    this.canvas.width = img.width;
    this.canvas.height = img.height;

    this.ctx.drawImage(img, 0, 0);

    return this.canvasToBlob(targetFormat, quality);
  }

  // Extract image data
  async extractImageData(blob) {
    const img = await this.loadImageFromBlob(blob);

    this.canvas.width = img.width;
    this.canvas.height = img.height;

    this.ctx.drawImage(img, 0, 0);

    return this.ctx.getImageData(0, 0, img.width, img.height);
  }

  // Create thumbnail
  async createThumbnail(blob, size = 150, options = {}) {
    const {
      quality = 0.8,
      format = 'image/jpeg',
      fit = 'cover', // 'cover' | 'contain' | 'fill'
    } = options;

    const img = await this.loadImageFromBlob(blob);

    let sx = 0,
      sy = 0,
      sWidth = img.width,
      sHeight = img.height;
    let dx = 0,
      dy = 0,
      dWidth = size,
      dHeight = size;

    if (fit === 'cover') {
      const scale = Math.max(size / img.width, size / img.height);
      const scaledWidth = img.width * scale;
      const scaledHeight = img.height * scale;

      dx = (size - scaledWidth) / 2;
      dy = (size - scaledHeight) / 2;
      dWidth = scaledWidth;
      dHeight = scaledHeight;
    } else if (fit === 'contain') {
      const scale = Math.min(size / img.width, size / img.height);
      dWidth = img.width * scale;
      dHeight = img.height * scale;
      dx = (size - dWidth) / 2;
      dy = (size - dHeight) / 2;
    }

    this.canvas.width = size;
    this.canvas.height = size;

    // Clear canvas
    this.ctx.fillStyle = '#fff';
    this.ctx.fillRect(0, 0, size, size);

    // Draw thumbnail
    this.ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

    return this.canvasToBlob(format, quality);
  }

  // Add watermark
  async addWatermark(blob, watermarkBlob, options = {}) {
    const {
      position = 'bottom-right',
      opacity = 0.5,
      scale = 0.2,
      margin = 10,
      quality = 0.9,
      format = blob.type || 'image/jpeg',
    } = options;

    const [img, watermark] = await Promise.all([
      this.loadImageFromBlob(blob),
      this.loadImageFromBlob(watermarkBlob),
    ]);

    this.canvas.width = img.width;
    this.canvas.height = img.height;

    // Draw main image
    this.ctx.drawImage(img, 0, 0);

    // Calculate watermark size
    const watermarkWidth = img.width * scale;
    const watermarkHeight =
      (watermark.height / watermark.width) * watermarkWidth;

    // Calculate position
    let x = margin;
    let y = margin;

    switch (position) {
      case 'top-left':
        // Already set
        break;
      case 'top-right':
        x = img.width - watermarkWidth - margin;
        break;
      case 'bottom-left':
        y = img.height - watermarkHeight - margin;
        break;
      case 'bottom-right':
        x = img.width - watermarkWidth - margin;
        y = img.height - watermarkHeight - margin;
        break;
      case 'center':
        x = (img.width - watermarkWidth) / 2;
        y = (img.height - watermarkHeight) / 2;
        break;
    }

    // Apply opacity and draw watermark
    this.ctx.globalAlpha = opacity;
    this.ctx.drawImage(watermark, x, y, watermarkWidth, watermarkHeight);
    this.ctx.globalAlpha = 1;

    return this.canvasToBlob(format, quality);
  }

  // Canvas to blob helper
  canvasToBlob(type = 'image/jpeg', quality = 0.9) {
    return new Promise((resolve, reject) => {
      this.canvas.toBlob(
        (blob) => {
          if (blob) {
            resolve(blob);
          } else {
            reject(new Error('Failed to create blob'));
          }
        },
        type,
        quality
      );
    });
  }

  // Create image preview
  async createImagePreview(blob, options = {}) {
    const { maxWidth = 300, maxHeight = 300, quality = 0.8 } = options;

    const resized = await this.resizeImage(blob, {
      maxWidth,
      maxHeight,
      quality,
      format: 'image/jpeg',
    });

    const dataUrl = await new Promise((resolve) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.readAsDataURL(resized);
    });

    return {
      blob: resized,
      dataUrl,
      size: resized.size,
    };
  }
}

// Usage
const processor = new BlobImageProcessor();

// Resize image
const imageBlob = new Blob(); // Image blob
processor
  .resizeImage(imageBlob, {
    maxWidth: 800,
    maxHeight: 600,
    quality: 0.85,
  })
  .then((resized) => {
    console.log('Resized image:', resized);
  });

// Apply filters
processor
  .applyFilters(imageBlob, {
    brightness: 110,
    contrast: 120,
    saturate: 150,
  })
  .then((filtered) => {
    console.log('Filtered image:', filtered);
  });

// Create thumbnail
processor
  .createThumbnail(imageBlob, 150, {
    fit: 'cover',
    quality: 0.7,
  })
  .then((thumbnail) => {
    console.log('Thumbnail created:', thumbnail);
  });

// Add watermark
const watermarkBlob = new Blob(); // Watermark image
processor
  .addWatermark(imageBlob, watermarkBlob, {
    position: 'bottom-right',
    opacity: 0.3,
    scale: 0.15,
  })
  .then((watermarked) => {
    console.log('Watermarked image:', watermarked);
  });

Best Practices

  1. Always revoke object URLs

    const url = URL.createObjectURL(blob);
    // Use URL...
    URL.revokeObjectURL(url);
    
  2. Use appropriate MIME types

    const blob = new Blob([data], {
      type: 'application/json', // Specify correct type
    });
    
  3. Handle large blobs with streams

    // For large blobs, use streaming
    const stream = blob.stream();
    const reader = stream.getReader();
    
  4. Validate blob data

    if (blob.size > MAX_SIZE) {
      throw new Error('Blob too large');
    }
    

Conclusion

The Blob API is fundamental for handling binary data in web applications:

  • File creation and manipulation
  • Binary data processing
  • Stream operations for large data
  • Image processing and transformations
  • Download management with progress
  • Cross-browser compatibility

Key takeaways:

  • Blobs are immutable - create new ones for modifications
  • Always clean up object URLs
  • Use streams for large data
  • Specify correct MIME types
  • Consider memory usage with large blobs
  • Leverage FileReader for various formats

Master the Blob API to handle files and binary data efficiently in your web applications!