Image Processing
Every photo and video uploaded to PicPeak runs through a small pipeline before it’s stored: format normalisation, EXIF extraction, dimension capture, thumbnail generation, optional watermarking. This page documents what happens and where to configure it.
Library stack
| Job | Library | Notes |
|---|---|---|
| Image transforms | Sharp | libvips under the hood — fastest mainstream image lib for Node |
| EXIF extraction | exifr | Tag-by-tag, no temp files |
| Video processing | FFmpeg (bundled via ffmpeg-static) | Used for thumbnail extraction and optional transcode |
Upload pipeline
When a photo lands on the backend (admin upload, guest upload, S3 prefix walker import, or auto-import from filewatcher), the following runs synchronously:
- MIME + extension validation against the
Allowed File Typessetting in General. - Size check against the per-photo and per-batch upload caps.
- Storage write — original photo is written to
events/active/{slug}/{filename}(local) or the equivalent S3 key. - Sharp metadata read — width, height, format. Persisted to the
photos.width/photos.heightcolumns. These are used by the masonry and justified gallery layouts to compute aspect ratios on the client. - EXIF capture date extraction — see below. Persisted to
photos.captured_at. - Thumbnail generation — see Thumbnails for the configuration. The output goes to
thumbnails/thumb_{filename}.
Steps 1–6 happen before the upload response returns, so the photo is fully usable as soon as the upload completes.
Capture date extraction
The fallback chain (first non-empty wins):
- EXIF
DateTimeOriginal - EXIF
CreateDate - EXIF
ModifyDate - File system
mtime
If all four are missing the photo’s captured_at stays NULL and it sorts to the end of “by capture date” sorts.
Thumbnail generation
| Aspect | Behavior |
|---|---|
| When | Synchronously on upload |
| Format | jpeg / png / webp (configurable) |
| Quality | 85 default (configurable) |
| Fit mode | cover default — crops to exact target dimensions |
| EXIF | Stripped from thumbnails (privacy: no GPS leak via thumbnail download) |
| Path | thumbnails/thumb_{originalFilename} |
| Lazy regen | If a thumbnail is missing or unreadable when requested, the backend generates it on demand and caches it back |
See Thumbnails settings for tuning.
Hero photos
Larger pre-rendered version used as the gallery hero image (the big photo at the top of the gallery page).
| Aspect | Value |
|---|---|
| Dimensions | 1920 × 1080, fit cover |
| Quality | 85 (JPEG) |
| EXIF | Stripped, like thumbnails and previews (see Metadata in derived images) |
| Path | hero/{photo_id}_hero.{ext} |
| Focal point | Per-event setting (e.g. top, center, bottom, or 35% 60%). Defaults to center. Used by Sharp’s extract to pick which part of a tall photo to use as the landscape hero. |
Metadata in derived images
Thumbnails, previews and hero images — what the gallery shows in the browser, including the public link-preview cover — carry no EXIF, GPS, XMP or IPTC data. Orientation is applied to the pixels before encoding, so photos still display upright.
Downloads are the opposite: a download keeps the photo’s EXIF, XMP and IPTC (photographer, copyright, GPS and so on). The stored original is never re-encoded, and a download that is re-encoded — resized to the gallery’s download standard, a picked resolution, or watermarked — keeps the metadata too, with the Orientation tag reset to match the rotated pixels and the original’s colour profile retained. A watermarked copy shown in the gallery (not downloaded) is stripped.
Before v3.174.2-beta.0 (beta channel), thumbnails, previews and heroes still contained the source’s EXIF, GPS included, because of a Sharp behaviour change. Stable strips them from v3.134.7 on (downloads keep their metadata from v3.133.6), but has no automatic clean-up of renditions written before it — use the regenerate actions under Settings → Thumbnails to rebuild thumbnails and previews.
Background rendition refresh
On the beta channel from v3.174.2-beta.0, renditions written before the stripping are rebuilt once in the background after the upgrade:
- It starts about 30 seconds after boot (
RENDITION_REFRESH_DELAY_MS, default30000) and runs on one replica only; it resumes after a restart. - Link-preview cover photos go first, then every live photo of every non-archived gallery. Videos are skipped; originals and downloads are not touched.
- One photo at a time, with a pause between photos (
RENDITION_REFRESH_PAUSE_MS, default200), and it waits while uploads are being processed. Expect a few hundred milliseconds per photo, more on S3 or a NAS. - Per photo it rebuilds the thumbnail, preview and hero, and deletes the cached size variants. A photo that fails, or is in the trash, is retried on the next pass.
- Until its cover has been rebuilt, the public link-preview cover endpoint regenerates an old cover on first request, and answers
404instead of the old file if that fails. - A files-only or selective restore marks the restored photos for another pass; a full or database restore triggers a pass after its migrations.
Progress: GET /api/admin/photos/refresh-renditions/status (needs system.manage) and backend log lines starting with Rendition refresh. Set RENDITION_REFRESH_DISABLED=true to skip it. See Environment variables.
Watermarking
When Watermark downloads is enabled on an event:
- The original is fetched from storage.
- The brand watermark logo is composited over it at the configured position, opacity, and size.
- The composite is sent to the browser as the download.
Watermarking happens on demand, every time the user clicks Download. The watermarked file is not persisted — only the original lives on disk. This avoids storage bloat and lets you re-brand watermarks without regenerating every photo, but does add a few hundred milliseconds of CPU time to each download.
Thumbnails are not watermarked by default — the brand logo is too large relative to a 300 px thumbnail to read clearly. Enable per-event “Watermark thumbnails” if you specifically want that.
Protection levels
The server treats basic, standard and enhanced alike: they serve the normal photo and preview files. Canvas rendering and DevTools detection happen in the browser. There is no fingerprinting or tile fragmentation.
At maximum (beta channel, from v3.175.0-beta.0) the full-size photo route and the preview route answer a per-request copy cut from the stored preview: it fits inside 1920 × 1080, is re-encoded at quality 60, carries no EXIF, and the route answers 503 when the image workers are busy. Videos and downloads are not capped.
See Image Security for the per-level breakdown.
Video processing
Video uploads (.mp4, .mov, .webm) are handled separately:
| Aspect | Behavior |
|---|---|
| Transcode | Only if the source is not already H.264/H.265 in a browser-friendly container. Otherwise passthrough. |
| Thumbnail | Single frame extracted at the 1-second mark via FFmpeg. Same dimensions as photo thumbnails. |
| Storage | Original under videos/, thumbnail under thumbnails/ |
| Streaming | Range-request supported by the backend so seeks work without full download |
See Video Support for upload limits and reverse-proxy notes.
Where the code lives
backend/src/services/imageProcessor.js— Sharp pipeline + EXIFbackend/src/services/videoProcessor.js— FFmpeg pipelinebackend/src/services/watermarkService.js— composite logicbackend/src/services/renditionRefresh.js— background rebuild of renditions written before EXIF strippingbackend/src/services/galleryAssetPolicy.js— themaximumprotection cap