Skip to Content
FeaturesImage Processing

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

JobLibraryNotes
Image transformsSharp libvips under the hood — fastest mainstream image lib for Node
EXIF extractionexifr Tag-by-tag, no temp files
Video processingFFmpeg (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:

  1. MIME + extension validation against the Allowed File Types setting in General.
  2. Size check against the per-photo and per-batch upload caps.
  3. Storage write — original photo is written to events/active/{slug}/{filename} (local) or the equivalent S3 key.
  4. Sharp metadata read — width, height, format. Persisted to the photos.width / photos.height columns. These are used by the masonry and justified gallery layouts to compute aspect ratios on the client.
  5. EXIF capture date extraction — see below. Persisted to photos.captured_at.
  6. 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):

  1. EXIF DateTimeOriginal
  2. EXIF CreateDate
  3. EXIF ModifyDate
  4. 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

AspectBehavior
WhenSynchronously on upload
Formatjpeg / png / webp (configurable)
Quality85 default (configurable)
Fit modecover default — crops to exact target dimensions
EXIFStripped from thumbnails (privacy: no GPS leak via thumbnail download)
Paththumbnails/thumb_{originalFilename}
Lazy regenIf 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).

AspectValue
Dimensions1920 × 1080, fit cover
Quality85 (JPEG)
EXIFStripped, like thumbnails and previews (see Metadata in derived images)
Pathhero/{photo_id}_hero.{ext}
Focal pointPer-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, default 30000) 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, default 200), 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 404 instead 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:

  1. The original is fetched from storage.
  2. The brand watermark logo is composited over it at the configured position, opacity, and size.
  3. 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:

AspectBehavior
TranscodeOnly if the source is not already H.264/H.265 in a browser-friendly container. Otherwise passthrough.
ThumbnailSingle frame extracted at the 1-second mark via FFmpeg. Same dimensions as photo thumbnails.
StorageOriginal under videos/, thumbnail under thumbnails/
StreamingRange-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 + EXIF
  • backend/src/services/videoProcessor.js — FFmpeg pipeline
  • backend/src/services/watermarkService.js — composite logic
  • backend/src/services/renditionRefresh.js — background rebuild of renditions written before EXIF stripping
  • backend/src/services/galleryAssetPolicy.js — the maximum protection cap
Last updated on