manga-nanobanana / utils /imageOptimization.ts
Akhil-Theerthala's picture
Upload 25 files
2cdfc6e verified
raw
history blame
6.9 kB
export interface OptimizedImage {
compressed: string; // For display (smaller size)
full: string; // For PDF generation (full quality)
width: number;
height: number;
compressedSize: number;
fullSize: number;
}
export interface PerformanceTiming {
stepName: string;
startTime: number;
endTime: number;
duration: number;
timestamp: string;
}
/**
* Compress base64 image for display purposes while maintaining quality for PDF
*/
export async function optimizeImage(base64Image: string, quality: number = 0.7): Promise<OptimizedImage> {
return new Promise((resolve, reject) => {
try {
const img = new Image();
img.onload = () => {
// Create canvas for compression
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
// Set canvas size to image size
canvas.width = img.width;
canvas.height = img.height;
// Draw image on canvas
ctx.drawImage(img, 0, 0);
// Create compressed version for display
const compressedBase64 = canvas.toDataURL('image/jpeg', quality);
// Calculate sizes
const fullSize = base64Image.length * 0.75; // Approximate bytes
const compressedSize = compressedBase64.split(',')[1].length * 0.75;
resolve({
compressed: compressedBase64.split(',')[1], // Remove data:image/jpeg;base64, prefix
full: base64Image,
width: img.width,
height: img.height,
compressedSize: Math.round(compressedSize),
fullSize: Math.round(fullSize)
});
};
img.onerror = () => reject(new Error('Failed to load image for optimization'));
img.src = `data:image/jpeg;base64,${base64Image}`;
} catch (error) {
reject(error);
}
});
}
/**
* Batch optimize multiple images
*/
export async function optimizeImages(images: string[], quality: number = 0.7): Promise<OptimizedImage[]> {
const optimizationPromises = images.map(img => optimizeImage(img, quality));
return Promise.all(optimizationPromises);
}
/**
* Performance timing utilities
*/
export class PerformanceTracker {
private timings: PerformanceTiming[] = [];
private activeTimers: Map<string, number> = new Map();
startTimer(stepName: string): void {
const startTime = performance.now();
this.activeTimers.set(stepName, startTime);
}
endTimer(stepName: string): PerformanceTiming {
const endTime = performance.now();
const startTime = this.activeTimers.get(stepName);
if (!startTime) {
throw new Error(`Timer for "${stepName}" was not started`);
}
const timing: PerformanceTiming = {
stepName,
startTime,
endTime,
duration: endTime - startTime,
timestamp: new Date().toISOString()
};
this.timings.push(timing);
this.activeTimers.delete(stepName);
return timing;
}
getTimings(): PerformanceTiming[] {
return [...this.timings];
}
getTotalDuration(): number {
return this.timings.reduce((total, timing) => total + timing.duration, 0);
}
getTimingByStep(stepName: string): PerformanceTiming | undefined {
return this.timings.find(timing => timing.stepName === stepName);
}
clear(): void {
this.timings = [];
this.activeTimers.clear();
}
getFormattedSummary(): string {
if (this.timings.length === 0) return 'No performance data available';
const summary = this.timings.map(timing =>
`${timing.stepName}: ${(timing.duration / 1000).toFixed(2)}s`
).join('\n');
const total = (this.getTotalDuration() / 1000).toFixed(2);
return `${summary}\n\nTotal: ${total}s`;
}
}
/**
* Lazy loading utilities
*/
export interface LazyLoadOptions {
rootMargin?: string;
threshold?: number;
fallbackDelay?: number;
}
export function createLazyLoader(options: LazyLoadOptions = {}) {
const { rootMargin = '50px', threshold = 0.1, fallbackDelay = 300 } = options;
if ('IntersectionObserver' in window) {
return new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
const src = img.dataset.src;
if (src) {
img.src = src;
img.classList.remove('lazy');
img.classList.add('loaded');
}
}
});
}, { rootMargin, threshold });
}
// Fallback for older browsers
return {
observe: (element: Element) => {
setTimeout(() => {
const img = element as HTMLImageElement;
const src = img.dataset.src;
if (src) {
img.src = src;
img.classList.remove('lazy');
img.classList.add('loaded');
}
}, fallbackDelay);
},
disconnect: () => {},
unobserve: () => {}
};
}
/**
* Memory usage monitoring
*/
export function getMemoryUsage(): { used: number; total: number; percentage: number } | null {
if ('memory' in performance) {
const memory = (performance as any).memory;
return {
used: Math.round(memory.usedJSHeapSize / 1024 / 1024), // MB
total: Math.round(memory.totalJSHeapSize / 1024 / 1024), // MB
percentage: Math.round((memory.usedJSHeapSize / memory.totalJSHeapSize) * 100)
};
}
return null;
}
/**
* Debounce utility for performance
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
/**
* Throttle utility for performance
*/
export function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}