Advanced Image Processing & Gallery Management Systems
Deze pagina behandelt advanced image processing, gallery management systems, en visual content optimization voor web applications. Modern image galleries require responsive design, lazy loading, progressive enhancement, en accessibility features voor optimal user experience. Image processing includes format optimization, automatic resizing, CDN integration, en metadata extraction voor comprehensive media management.
Image Processing & Optimization
Contemporary image processing implements automatic format conversion (WebP, AVIF), intelligent compression algorithms, en responsive image generation voor different viewport sizes. Server-side processing tools like Sharp, ImageMagick, en cloud-based solutions provide scalable image transformation capabilities. Progressive JPEG encoding, blur-up placeholders, en adaptive bitrate streaming ensure optimal loading performance across different network conditions.
Gallery Architecture & User Experience
Modern gallery systems feature infinite scroll, masonry layouts, full-screen lightbox experiences, en touch gesture support voor mobile devices. Virtual scrolling techniques handle large image collections efficiently, preventing memory exhaustion en maintaining smooth performance. Integration with content delivery networks enables global image distribution, automatic format negotiation, en edge caching voor reduced latency worldwide.
Advanced Gallery Implementation
// React Image Gallery with Advanced Features
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useIntersectionObserver } from './hooks/useIntersectionObserver';
import { useVirtualScroll } from './hooks/useVirtualScroll';
interface ImageItem {
id: string;
src: string;
thumbnail: string;
alt: string;
width: number;
height: number;
blurhash?: string;
metadata?: {
camera?: string;
lens?: string;
settings?: string;
location?: string;
capturedAt?: Date;
};
}
interface GalleryProps {
images: ImageItem[];
columns?: number;
gap?: number;
lazy?: boolean;
lightbox?: boolean;
infiniteScroll?: boolean;
onImageLoad?: (image: ImageItem) => void;
}
const AdvancedImageGallery: React.FC<GalleryProps> = ({
images,
columns = 3,
gap = 16,
lazy = true,
lightbox = true,
infiniteScroll = false,
onImageLoad
}) => {
const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null);
const [loadedImages, setLoadedImages] = useState<Set<string>>(new Set());
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });
const containerRef = useRef<HTMLDivElement>(null);
// Virtual scrolling for large galleries
const { containerHeight, itemHeight } = useVirtualScroll({
items: images,
containerRef,
itemHeight: 300,
enabled: infiniteScroll && images.length > 100
});
// Intersection Observer for lazy loading
const { observe, unobserve } = useIntersectionObserver({
callback: (entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const imageId = entry.target.getAttribute('data-image-id');
if (imageId) {
handleImageVisible(imageId);
}
}
});
},
options: {
rootMargin: '100px 0px',
threshold: 0.1
}
});
const handleImageVisible = useCallback((imageId: string) => {
if (!loadedImages.has(imageId)) {
setLoadedImages(prev => new Set([...prev, imageId]));
const image = images.find(img => img.id === imageId);
if (image && onImageLoad) {
onImageLoad(image);
}
}
}, [images, loadedImages, onImageLoad]);
const handleImageClick = useCallback((image: ImageItem) => {
if (lightbox) {
setSelectedImage(image);
}
}, [lightbox]);
const closeLightbox = useCallback(() => {
setSelectedImage(null);
}, []);
// Keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!selectedImage) return;
const currentIndex = images.findIndex(img => img.id === selectedImage.id);
switch (e.key) {
case 'Escape':
closeLightbox();
break;
case 'ArrowLeft':
if (currentIndex > 0) {
setSelectedImage(images[currentIndex - 1]);
}
break;
case 'ArrowRight':
if (currentIndex < images.length - 1) {
setSelectedImage(images[currentIndex + 1]);
}
break;
}
};
if (selectedImage) {
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
};
}, [selectedImage, images, closeLightbox]);
const renderImage = useCallback((image: ImageItem, index: number) => {
const shouldLoad = !lazy || loadedImages.has(image.id);
return (
<div
key={image.id}
className="gallery-item"
style={{
aspectRatio: `${image.width} / ${image.height}`,
cursor: lightbox ? 'pointer' : 'default'
}}
onClick={() => handleImageClick(image)}
data-image-id={image.id}
ref={node => {
if (node && lazy && !loadedImages.has(image.id)) {
observe(node);
}
}}
>
{shouldLoad ? (
<img
src={image.thumbnail}
alt={image.alt}
loading="lazy"
onLoad={() => handleImageVisible(image.id)}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
transition: 'opacity 0.3s ease'
}}
/>
) : (
<div
className="image-placeholder"
style={{
width: '100%',
height: '100%',
background: image.blurhash
? `url("data:image/svg+xml;base64,${btoa(generateBlurSVG(image.blurhash))}")`
: '#f0f0f0',
backgroundSize: 'cover'
}}
/>
)}
{image.metadata && (
<div className="image-overlay">
<div className="image-info">
{image.metadata.camera && (
<span className="camera">{image.metadata.camera}</span>
)}
{image.metadata.settings && (
<span className="settings">{image.metadata.settings}</span>
)}
</div>
</div>
)}
</div>
);
}, [lazy, loadedImages, lightbox, handleImageClick, observe, handleImageVisible]);
return (
<div className="advanced-gallery" ref={containerRef}>
<div
className="gallery-grid"
style={{
display: 'grid',
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: `${gap}px`,
height: infiniteScroll ? containerHeight : 'auto'
}}
>
{images
.slice(visibleRange.start, visibleRange.end)
.map((image, index) => renderImage(image, index))}
</div>
{selectedImage && lightbox && (
<LightboxModal
image={selectedImage}
images={images}
onClose={closeLightbox}
onNavigate={setSelectedImage}
/>
)}
</div>
);
};
// Lightbox Modal Component
const LightboxModal: React.FC<{
image: ImageItem;
images: ImageItem[];
onClose: () => void;
onNavigate: (image: ImageItem) => void;
}> = ({ image, images, onClose, onNavigate }) => {
const [isLoading, setIsLoading] = useState(true);
const [imageError, setImageError] = useState(false);
const currentIndex = images.findIndex(img => img.id === image.id);
const hasNext = currentIndex < images.length - 1;
const hasPrev = currentIndex > 0;
const handleImageLoad = () => setIsLoading(false);
const handleImageError = () => {
setIsLoading(false);
setImageError(true);
};
const navigateNext = () => {
if (hasNext) {
onNavigate(images[currentIndex + 1]);
setIsLoading(true);
setImageError(false);
}
};
const navigatePrev = () => {
if (hasPrev) {
onNavigate(images[currentIndex - 1]);
setIsLoading(true);
setImageError(false);
}
};
return (
<div className="lightbox-overlay" onClick={onClose}>
<div className="lightbox-container" onClick={e => e.stopPropagation()}>
<button className="lightbox-close" onClick={onClose}>
✕
</button>
{hasPrev && (
<button className="lightbox-nav prev" onClick={navigatePrev}>
‹
</button>
)}
{hasNext && (
<button className="lightbox-nav next" onClick={navigateNext}>
›
</button>
)}
<div className="lightbox-content">
{isLoading && (
<div className="lightbox-loading">
Loading...
</div>
)}
{imageError ? (
<div className="lightbox-error">
Failed to load image
</div>
) : (
<img
src={image.src}
alt={image.alt}
onLoad={handleImageLoad}
onError={handleImageError}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease'
}}
/>
)}
{image.metadata && (
<div className="lightbox-metadata">
<h3>{image.alt}</h3>
{image.metadata.camera && <p>Camera: {image.metadata.camera}</p>}
{image.metadata.lens && <p>Lens: {image.metadata.lens}</p>}
{image.metadata.settings && <p>Settings: {image.metadata.settings}</p>}
{image.metadata.location && <p>Location: {image.metadata.location}</p>}
{image.metadata.capturedAt && (
<p>Captured: {image.metadata.capturedAt.toLocaleDateString()}</p>
)}
</div>
)}
</div>
<div className="lightbox-counter">
{currentIndex + 1} / {images.length}
</div>
</div>
</div>
);
};
// Helper function for blur placeholder
const generateBlurSVG = (blurhash: string): string => {
// Simplified blurhash to SVG conversion
return `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40">
<filter id="blur"><feGaussianBlur stdDeviation="2"/></filter>
<rect width="100%" height="100%" fill="${blurhash.slice(0, 7)}" filter="url(#blur)"/>
</svg>`;
};
# Server-side Image Processing
import asyncio
import aiofiles
from PIL import Image, ImageOps, ImageFilter
from pillow_heif import register_heif_opener
import numpy as np
from typing import List, Dict, Optional, Tuple
import hashlib
import json
from pathlib import Path
register_heif_opener()
class ImageProcessor:
def __init__(self, cache_dir: str = "./cache", max_size: int = 2048):
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
self.max_size = max_size
# Supported formats
self.input_formats = {'.jpg', '.jpeg', '.png', '.webp', '.tiff', '.heic', '.heif'}
self.output_formats = ['webp', 'jpeg', 'png']
async def process_image(
self,
input_path: str,
output_formats: Optional[List[str]] = None,
sizes: Optional[List[int]] = None,
quality: int = 85,
progressive: bool = True
) -> Dict[str, List[str]]:
"""Process image into multiple formats and sizes."""
input_path = Path(input_path)
if not input_path.exists():
raise FileNotFoundError(f"Image not found: {input_path}")
if input_path.suffix.lower() not in self.input_formats:
raise ValueError(f"Unsupported format: {input_path.suffix}")
# Generate cache key
cache_key = self.generate_cache_key(input_path, output_formats, sizes, quality)
cache_file = self.cache_dir / f"{cache_key}.json"
# Check cache
if cache_file.exists():
async with aiofiles.open(cache_file, 'r') as f:
cached_result = json.loads(await f.read())
# Verify all files still exist
if self.validate_cached_files(cached_result):
return cached_result
# Load and analyze image
with Image.open(input_path) as img:
# Fix orientation
img = ImageOps.exif_transpose(img)
original_width, original_height = img.size
aspect_ratio = original_width / original_height
# Default sizes if not specified
if sizes is None:
sizes = self.calculate_responsive_sizes(original_width)
if output_formats is None:
output_formats = ['webp', 'jpeg']
results = {}
for fmt in output_formats:
results[fmt] = []
for size in sizes:
# Skip if size is larger than original
if size > max(original_width, original_height):
continue
# Calculate dimensions maintaining aspect ratio
if original_width > original_height:
new_width = min(size, original_width)
new_height = int(new_width / aspect_ratio)
else:
new_height = min(size, original_height)
new_width = int(new_height * aspect_ratio)
# Generate output filename
output_name = f"{input_path.stem}_{new_width}x{new_height}.{fmt}"
output_path = self.cache_dir / output_name
# Resize and save
await self.resize_and_save(
img, output_path, (new_width, new_height), fmt, quality, progressive
)
results[fmt].append({
'path': str(output_path),
'width': new_width,
'height': new_height,
'size_bytes': output_path.stat().st_size
})
# Cache results
async with aiofiles.open(cache_file, 'w') as f:
await f.write(json.dumps(results, indent=2))
return results
async def resize_and_save(
self,
img: Image.Image,
output_path: Path,
size: Tuple[int, int],
fmt: str,
quality: int,
progressive: bool
):
"""Resize and save image with optimization."""
# Resize with high-quality resampling
resized = img.resize(size, Image.Resampling.LANCZOS)
# Apply sharpening for small images
if max(size) < 800:
resized = resized.filter(ImageFilter.UnsharpMask(radius=1, percent=150, threshold=3))
# Save with format-specific optimizations
save_kwargs = {'optimize': True}
if fmt == 'webp':
save_kwargs.update({
'format': 'WebP',
'quality': quality,
'method': 6, # Best compression
'lossless': quality == 100
})
elif fmt == 'jpeg':
save_kwargs.update({
'format': 'JPEG',
'quality': quality,
'progressive': progressive,
'optimize': True
})
# Convert to RGB if necessary
if resized.mode in ('RGBA', 'P'):
background = Image.new('RGB', resized.size, (255, 255, 255))
if resized.mode == 'P':
resized = resized.convert('RGBA')
background.paste(resized, mask=resized.split()[-1] if resized.mode == 'RGBA' else None)
resized = background
elif fmt == 'png':
save_kwargs.update({
'format': 'PNG',
'optimize': True,
'compress_level': 9
})
# Save in a separate thread to avoid blocking
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, resized.save, output_path, **save_kwargs)
def calculate_responsive_sizes(self, original_width: int) -> List[int]:
"""Calculate responsive image sizes."""
breakpoints = [320, 640, 768, 1024, 1366, 1920, 2560]
# Only include sizes smaller than or equal to original
relevant_sizes = [size for size in breakpoints if size <= original_width]
# Always include original size
if original_width not in relevant_sizes:
relevant_sizes.append(original_width)
return sorted(relevant_sizes)
async def extract_metadata(self, image_path: str) -> Dict:
"""Extract comprehensive image metadata."""
with Image.open(image_path) as img:
# Basic info
metadata = {
'filename': Path(image_path).name,
'format': img.format,
'mode': img.mode,
'size': img.size,
'file_size': Path(image_path).stat().st_size
}
# EXIF data
if hasattr(img, '_getexif') and img._getexif():
exif = img._getexif()
if exif:
# Extract common EXIF tags
exif_tags = {
'DateTime': exif.get(306),
'Camera': exif.get(272), # Make
'Model': exif.get(271), # Model
'Lens': exif.get(42036), # LensModel
'ISO': exif.get(34855),
'FNumber': exif.get(33437),
'ExposureTime': exif.get(33434),
'FocalLength': exif.get(37386),
'GPS': self.extract_gps_data(exif)
}
metadata['exif'] = {k: v for k, v in exif_tags.items() if v is not None}
# Color analysis
metadata['colors'] = await self.analyze_colors(img)
# Generate blurhash placeholder
metadata['blurhash'] = await self.generate_blurhash(img)
return metadata
async def analyze_colors(self, img: Image.Image) -> Dict:
"""Analyze dominant colors in the image."""
# Resize for faster processing
small_img = img.resize((50, 50))
# Convert to RGB
if small_img.mode != 'RGB':
small_img = small_img.convert('RGB')
# Get pixel data
pixels = np.array(small_img)
pixels = pixels.reshape(-1, 3)
# Find dominant colors using k-means clustering
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=5, random_state=42)
kmeans.fit(pixels)
colors = []
for i, color in enumerate(kmeans.cluster_centers_):
color_count = np.sum(kmeans.labels_ == i)
percentage = color_count / len(pixels) * 100
colors.append({
'rgb': [int(c) for c in color],
'hex': '#{:02x}{:02x}{:02x}'.format(*[int(c) for c in color]),
'percentage': round(percentage, 2)
})
# Sort by percentage
colors.sort(key=lambda x: x['percentage'], reverse=True)
return {
'dominant': colors[0]['hex'],
'palette': colors,
'average_brightness': np.mean(pixels) / 255
}
async def generate_blurhash(self, img: Image.Image) -> str:
"""Generate blurhash for progressive loading placeholder."""
# Simple blurhash approximation
# In production, use the blurhash library
# Resize to small size
small = img.resize((4, 3))
if small.mode != 'RGB':
small = small.convert('RGB')
# Get average color as simple hash
pixels = np.array(small)
avg_color = np.mean(pixels.reshape(-1, 3), axis=0)
# Create a simple hash representation
hash_str = ''.join([f'{int(c):02x}' for c in avg_color])
return f"L{hash_str[:6]}"
def extract_gps_data(self, exif: Dict) -> Optional[Dict]:
"""Extract GPS coordinates from EXIF data."""
gps_tags = {
'GPSLatitude': 2,
'GPSLatitudeRef': 1,
'GPSLongitude': 4,
'GPSLongitudeRef': 3
}
gps_data = {}
for tag, key in gps_tags.items():
if key in exif:
gps_data[tag] = exif[key]
if len(gps_data) >= 4:
# Convert to decimal degrees
lat = self.convert_to_degrees(gps_data['GPSLatitude'])
if gps_data['GPSLatitudeRef'] == 'S':
lat = -lat
lon = self.convert_to_degrees(gps_data['GPSLongitude'])
if gps_data['GPSLongitudeRef'] == 'W':
lon = -lon
return {'latitude': lat, 'longitude': lon}
return None
def convert_to_degrees(self, value) -> float:
"""Convert GPS coordinates to decimal degrees."""
d, m, s = value
return d + (m / 60.0) + (s / 3600.0)
def generate_cache_key(self, input_path: Path, formats: List[str], sizes: List[int], quality: int) -> str:
"""Generate cache key for processed images."""
# Include file modification time in cache key
mtime = input_path.stat().st_mtime
key_data = f"{input_path}_{formats}_{sizes}_{quality}_{mtime}"
return hashlib.md5(key_data.encode()).hexdigest()
def validate_cached_files(self, cached_result: Dict) -> bool:
"""Validate that all cached files still exist."""
for format_files in cached_result.values():
for file_info in format_files:
if not Path(file_info['path']).exists():
return False
return True
Performance Optimization & Accessibility
Modern gallery systems implement performance optimization strategies including image compression, CDN delivery, en progressive loading techniques. Accessibility features include keyboard navigation, screen reader support, alt text management, en color contrast optimization voor inclusive user experiences. Analytics integration tracks user engagement, image performance metrics, en conversion rates voor data-driven gallery optimization.
Implementeer moderne ontwikkelmethodes met focus op gebruikerservaring, prestaties en onderhoudbaarheid. Zorg voor complete test coverage en gebruik continuous integration pipelines voor automated image processing, quality validation, en deployment workflows.