This document describes Lychee's image processing architecture, including size variant generation, processing pipeline, and storage strategy.
Lychee handles multiple image operations to provide optimal viewing experiences across different devices and use cases. The system automatically generates size variants, extracts metadata, and organizes files efficiently.
Photos are stored in multiple sizes to optimize performance and bandwidth:
| Value | Name | Description |
|---|---|---|
| 0 | RAW | Original camera RAW / HEIC / PSD file (preserved unmodified) |
| 1 | Original | Full-resolution uploaded image (JPEG after conversion, or native for non-RAW uploads) |
| 2 | Medium2x | High-DPI web-optimised version (2× resolution) |
| 3 | Medium | Standard web-optimised version |
| 4 | Small2x | High-DPI thumbnail version (2× resolution) |
| 5 | Small | Standard thumbnail version |
| 6 | Thumb2x | High-DPI small thumbnail for galleries (2× resolution) |
| 7 | Thumb | Standard small thumbnail for galleries |
| 8 | Placeholder | Low-quality image placeholder (LQIP) |
Each variant type has configurable dimensions and quality settings:
// Example configuration
'medium' => [
'max_width' => 1920,
'max_height' => 1080,
'quality' => 90,
],
'small' => [
'max_width' => 720,
'max_height' => 480,
'quality' => 85,
],
'thumb' => [
'max_width' => 200,
'max_height' => 200,
'quality' => 80,
],- Upload: Original file received and validated
- Metadata Extraction: EXIF data parsed (GPS, camera info, timestamps)
- Size Generation: Multiple variants created based on configuration
- Color Analysis: Dominant color palette extracted
- Storage: Files organized by naming strategy
- Database: Photo and size variant records created
The SizeVariantDefaultFactory handles variant generation:
// app/Image/SizeVariantDefaultFactory.php
class SizeVariantDefaultFactory implements SizeVariantFactory
{
public function createSizeVariants(Photo $photo): Collection
{
// Generate different sizes based on configuration
// Returns collection of SizeVariant models
}
}The photo creation process uses a pipeline with multiple stages:
- Validation Stage: Validate file type, size, and integrity
- Upload Stage: Store original file
- Metadata Stage: Extract EXIF, GPS, and camera data
- Variant Stage: Generate size variants
- Palette Stage: Extract color palette
- Finalization Stage: Create database records
For detailed information about the photo processing pipeline, see app/Actions/Photo/README.md.
Camera RAW files (NEF, CR2, CR3, ARW, DNG, ORF, RW2, RAF, PEF, SRW, NRW, PSD, HEIC, HEIF) are handled by a dual-variant pipeline that preserves the unmodified source file alongside a displayable JPEG original.
The DetectAndStoreRaw Init pipe (replacing the former ConvertUnsupportedMedia) performs:
- Extension check against
CONVERTIBLE_RAW_EXTENSIONSconstant. - On match: original file is stashed in
InitDTO::$raw_source_file;RawToJpegconverts it to JPEG via Imagick (quality 92) and the JPEG replaces thesource_filein the DTO. - On Imagick failure: graceful fallback — file is kept as-is, no RAW variant is stored, a warning is logged.
- PDF exception:
.pdffiles are not treated as convertible RAW formats; they remain as ORIGINAL.
The CreateRawSizeVariant Standalone pipe (runs after CreateOriginalSizeVariant) copies the raw source file to permanent storage and creates a size_variants DB row with:
type = 0(RAW)width = 0,height = 0(dimensions not decoded for RAW files)- Native file extension preserved.
RAW downloads are controlled by the raw_download_enabled configuration key (boolean, default false, category Image Processing). When disabled, ZipRequest::authorize() returns false for DownloadVariantType::RAW requests.
The following classes were removed as part of this refactoring:
| Removed | Replacement |
|---|---|
HeifToJpeg |
RawToJpeg (handles all convertible formats) |
ConvertUnsupportedMedia |
DetectAndStoreRaw |
PhotoConverterFactory |
Direct RawToJpeg instantiation |
ConvertableImageType enum |
FileExtensionService::CONVERTIBLE_RAW_EXTENSIONS constant |
PhotoConverter interface |
Removed (no multiple converters needed) |
Extracted metadata includes:
- Camera Information: Make, model, lens
- Capture Settings: ISO, aperture, shutter speed, focal length
- Timestamps: Original capture time, digitization time
- GPS Coordinates: Latitude, longitude, altitude
- Image Properties: Width, height, orientation
Lychee carefully handles timestamps from multiple sources:
- Photo capture time (
taken_at) - File creation time
- EXIF timestamps
- Upload time
For detailed information about timestamp handling, see Timestamps Handling.
Files are organized using a configurable naming strategy:
- Original files: Stored with checksums for deduplication
- Variants: Named with size suffix (e.g.,
photo_medium.jpg) - Storage disks: Configurable (local, S3, etc.)
Each SizeVariant tracks:
class SizeVariant extends Model
{
public string $photo_id; // Parent photo
public string $type; // Variant type (original, medium, small, thumb)
public int $width; // Pixel width
public int $height; // Pixel height
public int $filesize; // File size in bytes
public string $storage_disk; // Storage location
public string $short_path; // Relative file path
}Lychee supports multiple image processing libraries:
- GD: Built-in PHP image processing
- ImageMagick: Advanced image processing with more features
The system automatically selects the best available engine.
- Lazy Generation: Variants generated on-demand when requested
- Caching: Processed images cached for quick retrieval
- Progressive Processing: Large batches processed in background jobs
- Quality vs Size: Configurable quality settings balance file size and visual quality
Large upload operations can be processed asynchronously:
// app/Jobs/ProcessPhotoJob.php
class ProcessPhotoJob implements ShouldQueue
{
public function handle()
{
// Process photo variants in background
}
}The Palette model stores dominant colors extracted from each photo:
class Palette extends Model
{
public string $photo_id; // Parent photo (primary key)
public array $colors; // Array of hex color values
}Usage:
- Theme generation
- Color-based photo search
- UI theming based on photo content
- Type checking: Only allowed image formats accepted
- Size limits: Maximum file size enforced
- Content validation: Image integrity verified
- Malware scanning: Optional virus scanning integration
- Access control: Files stored outside web root
- Private access: Served through application layer with authorization
- Checksums: SHA-256 checksums for integrity verification
- Database Schema - Photo and SizeVariant models
- Backend Architecture - Overall backend structure
- Request Lifecycle: Photo Upload - Detailed upload flow
Last updated: February 28, 2026