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.
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
-
Always revoke object URLs
const url = URL.createObjectURL(blob); // Use URL... URL.revokeObjectURL(url);
-
Use appropriate MIME types
const blob = new Blob([data], { type: 'application/json', // Specify correct type });
-
Handle large blobs with streams
// For large blobs, use streaming const stream = blob.stream(); const reader = stream.getReader();
-
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!