Unterlumen is a local image browser and culler. It runs as a lightweight web server — open it in any browser, no cloud, no subscription.
What it does:
- Browse photos in justified, grid, or list view with breadcrumb navigation
- Cull: review shots, mark rejects, restore or delete in bulk
- Organize: one folder in front of you, your target folders one key away (move, copy, undo)
- Batch rename using EXIF fields (date, camera, film simulation, …)
- Export & convert: resize, change format (JPEG/PNG/WebP), strip GPS, download as ZIP
- Geolocation: set or remove GPS coordinates via interactive map
- Publish your own website: turn collected photos into a complete, multi-album website with navigation and a sitemap, or into a single gallery behind a private share link for family and friends — no photo platform in between. You need your own webspace reachable over SSH; Unterlumen uploads with rsync. Without it, the result is a folder you can put online any way you like.
- Map: every photo with a location on one map, grouped by place
- Timeline: every dated library photo on one time axis, with a graph of when they were taken
- People: faces found on your computer and grouped, named once and found everywhere, a face the detector missed marked by hand
- Statistics: what your photos have in common and how that changed, in charts and 3D views
- Optional — Digital Asset Management (DAM): build a persistent, searchable catalog of your photos. Search by aperture, focal length, camera, lens, or Fujifilm film simulation across multiple libraries. Track where and when photos were published, with platform presets (Instagram 1080 px, Mastodon 1920 px, …).
What it is not:
- Not an image editor — no color grading, no retouching, no compositing
- No RAW file support — JPEG, PNG, WebP, and HEIF/HEIC (via ffmpeg) only
- Not a video editor — videos are listed, filtered and played, never cut, converted or renamed
The pictures below are made by npm run screenshots in e2e/ from the example photos in src/examples and the invented people of e2e/faces (generated with Google Gemini; no real person is shown), so they always show the current interface. Click one for the full size.
Your photo folders as a justified grid, with the metadata and the place a photo was taken beside it. The bar at the bottom holds everything you can do with a selection.
Unterlumen follows the system's light or dark setting; Settings can fix one.
| Light | Dark |
|---|---|
![]() |
![]() |
One photo at a time, with the film strip below and its metadata and location beside it. The arrow keys step through the folder; Crop and Delete are one click away.
Everyone named in your libraries, found by their faces on this computer. Groups of faces that look alike wait for a name, some with a suggestion ("Is this Kofi?"); a person's page shows all their faces, newest photo first, their age and who is most often with them. A face that was not found — in profile, half hidden — is framed by hand in the viewer and named. The people in these pictures are invented: the photos were generated with Google Gemini for Unterlumen's tests.
![]() |
![]() |
![]() |
![]() |
The folder you are sorting on the left, your target folders on the right with a number key each: select, press the key or drag the photos there, done. ⌥ copies instead, U takes the last move back, and "Mark for deletion" stands first, always on 0.
Marked photos and folders disappear from Folders and libraries but stay on disk until you delete them here, or restore them. A photo goes with its XMP sidecar, unless a RAW or video of the same name still uses it. When deleting empties a folder, Unterlumen asks whether to remove the folder too.
The versions of one photo across all libraries, side by side: the same file twice, the same picture scaled or saved again, and edited or cropped copies. Each shows its pixels, file size, format, name and folder, the largest first. Mark the ones you don't want; they are deleted in Marked for deletion.
A library indexes a folder tree once. Its folders then show what they hold: the four newest photos, how many there are and the years they span. Select a folder and the info panel describes it: its size and date range, its formats, the cameras and lenses it was shot with, and at which hours.
Because it is indexed, a library can be filtered across all of it: date, shutter speed, aperture, focal length, ISO, camera, lens, film simulation. The results replace the folders beside the filter.
Each library is its own folder, anywhere: on this computer, a disk or a NAS. Shared from Edit library, it is the same library on a second installation, which adds the folder and gets its name; each keeps its own index. Each installation checks its shared libraries against their folders at start and every 30 minutes, so photos added, deleted or edited and titles, fields and galleries changed on the other one show without a scan. A check only reads the folder, so the order in which the two installations look never undoes a change. See ADR-0052.
What you shoot with, when, where and in what colours, by topic: Equipment (cameras, lenses, formats, film simulations), Exposure (focal length, aperture, ISO), Time (time of day, weekday and hour, calendar), Frame (aspect ratio), Colour (black and white, main colours, warm and cool), Places (space and time, countries, cities, journeys, a density landscape on the world map) and Cataloguing (how complete titles, descriptions, keywords, places, people and stars are — a click on what is missing shows those photos — stars over time, keywords, own fields), plus views such as phone or camera, the risk of shake, exposure times, sessions, portrait or landscape, colours through the day, and two more 3D views: the palette space and the meaning space, where photos stand by what they show. Each with how it changed over the years. The overview has a card per topic showing a few of its charts, and the topics are in the sidebar while Statistics is open. Click a bar, a slice or a point and the photos it counts appear beside the charts.
| Overview | A topic |
|---|---|
![]() |
![]() |
In Colour, Colour combinations shows how many photos have the colours of a well-known combination — orange and teal, blue and yellow, red and green and others — as chords across the hue circle, and Your combination counts any two or three colours you choose on a wheel. A photo has a colour when it covers at least a tenth of the frame. Each shows its photos; the library filter has the same Colours.
Exposure, Time, Colour and Places end with a view in three dimensions, drawn with WebGPU: every photo is a point of light, and where many photos meet, it glows. Drag to turn a view, scroll to zoom, point at a light to read it and click it to see the photo. Full view shows a view and its photos over the whole window.
| Colour space | Exposure space |
|---|---|
![]() |
![]() |
| Daylight | Space and time |
![]() |
![]() |
Things finds animals, objects and scenes in the photos of your libraries, on this computer, with Google's SigLIP 2. It also finds which photos are blurred, too dark or too bright. The things go into each photo's sidecar as keywords that Lightroom and Apple Photos read too, in the language you choose for the library. Statistics → Things counts them: which come most often, how that changed and which come together. The search above every place finds them by name, or by words in many languages, and the info panel lists a photo's things, where × takes one away.
| Statistics → Things | Search by a thing |
|---|---|
![]() |
![]() |
The interface is in English or German. By default it follows the browser's language, and Settings → Language changes it for this browser. Things have German names too, so the search bar suggests "Boot" for a boat.
Every photo with a location on one map. Photos close together form a group showing the newest one; zooming in splits the groups. The Photos column lists what is in view, the filter in the head narrows the map. View shows the earth as a globe, flat (Mercator) or tilted in 3D with the buildings standing; Map switches between grey and colour tiles.
Every dated photo of every library on one time axis, oldest on the left. The graph below shows when the photos were taken; the frame marks what the rows above show, and the overview limits both to a span of months.
Tools for a selection: rename from EXIF tokens with a live preview, export to JPEG, PNG or WebP with size and metadata options, and set or remove the location of several photos at once. A selected folder is renamed from the same bar; its photos keep what their library knows of them.
| Rename | Export | Location |
|---|---|---|
![]() |
![]() |
![]() |
Your photos, on your own website. Photos you collect for a destination become galleries, and Publish turns them into static HTML on your own webspace:
- A complete website — many albums with an index, navigation and a sitemap, growing with every gallery you publish, and found by search engines.
- Share links — one gallery per link under an unguessable address, not indexed, for sending to family and friends.
- Files — the photos sized for a platform such as Instagram, in a folder, to post by hand.
Publish is the one action that exports, builds, uploads and checks the link; each gallery says in words where it stands. Uploading needs your own webspace reachable over SSH and uses rsync. Without an upload target the build stays in a local folder you can copy anywhere.
What Publish builds — a website with its albums, one album of it, and a gallery behind a share link:
| Website | Album | Share link |
|---|---|---|
![]() |
![]() |
![]() |
On a phone Unterlumen is for looking: libraries, photos, the map, the timeline, the people in your photos and the statistics, with a tab bar at the bottom and the search above every place. People on a phone finds a person as you type their name and shows their photos by year, with who is most often in them: tap one to see the photos of both together. Naming and everything else that changes files or settings — Galleries and publishing among them — is desktop work.
| Folders | Map | Timeline |
|---|---|---|
![]() |
![]() |
![]() |
- Screenshots
- Features
- Install
- Usage
- Docker / Podman
- Keyboard Shortcuts
- Documentation
- Development & Testing
- Notes
- License
- Browse & Cull mode — Justified, grid, or list view of photos in a directory with breadcrumb navigation
- Organize — One source folder shown large, your target folders as a list with a number key each: select, press the key or drag the photos onto the target, done. Add target… opens on your libraries, where a library or a folder in it is two clicks away. Moves run immediately with an undo beside the result; ⌥ copies instead. More than five files go in a progress dialog that counts them and can be cancelled; adding them to the library follows in the background, in the status line. Targets are remembered between sessions, and "Mark for deletion" is a target like any other (folders included), first in the list and always on key
0 - Waste bin — Mark photos for deletion, review in a dedicated view, restore or permanently delete
- Duplicates — A place under Cull (
#duplicates) that finds the versions of one photo in all libraries: identical files, the same picture scaled or saved again, and edited or cropped copies taken in the same moment (bursts stay apart). Each group shows every version with its pixels, file size, format, name and path, the largest first; Mark for deletion sends the ones you don't want to Marked for deletion, where only that copy is deleted. Found from the index, nothing is measured again. See ADR-0059 - Metadata — A place under Catalog (
#metadata) for what photos and videos say about themselves: date taken, title, description, keywords, creator, copyright and rating. Select photos in Folders, Organize, a library or the viewer and choose Edit metadata…. Write to stands at the top: Sidecar (XMP · NFO) writes the XMP sidecar beside a photo and an NFO file beside a video (as Kodi and Jellyfin read it) and leaves the files as they are; File (EXIF) writes into the photos themselves with exiftool, which changes their content and so their ID in a library. Videos are never changed, they always get the NFO. The date is corrected for many files at once: Set the day keeps each file's own time of day, Shift moves all by the same amount. Each file shows its date before and after before anything is written. Fields the files disagree on say "Several values" and stay as they are unless changed; keywords are added and removed, never replaced. What lies beside a file wins when reading, and every value says where it came from. See ADR-0060 - Stars — Rate photos with the keys 1–5 (0 takes the rating away) on the selection in Folders or a library, or on the photo in the viewer, or click the stars in the info panel or the viewer's toolbar. The rating is written to the XMP sidecar (a video's NFO), so the file stays as it is; a rating in the file itself, from the camera or Lightroom, is read too, and one beside it wins. Tiles show the stars (with Details on), the list has a Rating column, and the library filter has a Rating choice: Doesn’t matter (the default) · ★ or more … ★★★★ or more · ★★★★★ · Not rated. The number keys no longer open places. See ADR-0060
- IPTC and own fields — Metadata groups its fields: Date taken, Description, Location (sublocation, city, province or state, country, country code), Event and headline, Origin and rights (credit line, source, usage terms, web statement, instructions, job ID), Shown (people and organisations, added and removed like keywords) and Own fields. A group empty in every chosen file starts folded; a folded group says when it holds a change. The IPTC fields go where Lightroom and digiKam look for them — the sidecar, the file (XMP and IPTC) or a video's NFO. Own fields are key and value for anything else, always in the sidecar; As keyword also writes one as a machine tag,
ul:key=value. Country from the GPS position fills Country and Country code for each file, offline, from Natural Earth's country borders built into the app (disputed areas as Germany sees them). The info panel shows the filled IPTC fields and edits them in place, and any photo — in Folders too — has its title and own fields there. A value that lies beside the file carries a small XMP (or NFO) mark; one without is in the file itself, and hovering the mark shows what the file says under it. The library filter offers country, city, sublocation, event and the people and organisations shown, and the search suggests their values ("Kiel · City"). See ADR-0061 - Libraries (DAM) — Index a folder into a SQLite library (no CGo). Photos are identified by SHA-256 so metadata survives renames. Full-text EXIF search (the filter points to Statistics for finding photos by colour, warmth or hour), key/value annotations, HQ thumbnails, and re-index progress via Server-Sent Events. Library data stored in
~/.unterlumen/libraries/<id>/(overridable with--lib-dir/UNTERLUMEN_LIB_DIR) - Publish to your own website — Needs your own webspace reachable over SSH; the upload uses rsync (without it, the build stays in a local folder). A two-phase collect-then-publish workflow. From library mode, select photos (from the folder tree or EXIF filter results, within a single library or across libraries) and use the selection bar's "Add to gallery…" to add them to a gallery's pending draft — a new or existing gallery/album. Nothing is exported yet, so a gallery can be built up incrementally across sessions. When ready, open Galleries and press Publish: one action exports the photos, builds the HTML (writing an XMP sidecar per photo using a custom
xmlns:ulnamespace — non-destructive and portable), uploads the result where an upload is configured (rsync channels), and checks the link, showing each step as it runs. A failed upload keeps the build, so the gallery notes "built, upload missing" and the retry only uploads. Afterwards you can open the gallery in a browser, copy the local path, reveal it in Finder/Explorer, or preview it locally. Supports named accounts (e.g. two Mastodon logins), optional grouped post IDs for carousels, and platform-optimised export (channel presets: Instagram 1080px, Mastodon 1920px, Website 2400px). Two publishing modes: a multi-album site (a real, growing website whose albums are navigable and indexable) and single gallery (one host holding many unrelated albums, each under its own unguessable 24-hex URL — for sharing one album by link with a specific group, marked Unlisted by default so it carries anoindextag). A single-gallery channel holds as many albums as you like; each is published, renamed and deleted on its own. Channel settings managed via a dedicated UI; stored globally in~/.unterlumen/channels.json(overridable with-channels-dir/UNTERLUMEN_CHANNELS_DIR, e.g. to share channel config between multiple installations — see Sharing channel config across installations) - On a phone — Below 700 px the sidebar becomes a tab bar (Libraries, Map, Timeline, People, Statistics; Folders through its links), each place's head is one row, and Unterlumen becomes read-only: browse libraries and folders, open a photo full screen and swipe through the set, read its metadata in a sheet from the bottom, look at statistics, and find a person and their photos. Everything that changes files or settings stays on the desktop; a desktop-only place says so instead of showing controls that cannot work there. Reach it by binding the server to your network (
UNTERLUMEN_BIND=0.0.0.0) or through the Docker deployment - One filter for Map, Statistics and Timeline — The head of each of these places has the same filter: libraries, a span of months (not on the Timeline, whose axis is time), cameras and lenses, each a list with every entry on at first, so nothing is filtered until you narrow it, and people named in People, added one by one — several must all be in the photo. The lenses offered follow the cameras chosen. The choice is shared by the three places and kept in the browser; Reset widens it again. On a phone it folds behind a Filter button. The server filters, so counts, charts, the map and the photo column all count within it. See ADR-0050
- Map — Every photo with a location, from every library, on one large map. Photos taken close together form a group, shown as a round thumbnail of the newest one with the number of photos beside it; zooming in splits the groups until each photo stands at its own place. Clicking a group zooms in; a group whose photos share one spot, or a single photo, opens in the viewer (read-only: no crop, no marking for deletion). The shared filter — libraries, when taken, cameras, lenses and people — narrows the map; its conditions show as chips in the head. A column beside the map shows the photos in the part of the map on screen, newest first; it is open when you arrive on the desk, and the Photos button hides or shows it (on a phone it waits for the button). The column follows the map as you move and zoom; its Hide button or Escape closes it, and it stays closed in that browser. The world is a globe by default, with countries at their true size, and turns into the flat map close up. View: Globe · Mercator · 3D in the head switches it; 3D tilts the map with its buildings standing. The map is grey by default, light or dark with the theme; Map: Grey · Colour in the head shows it in colour. Its tiles come from OpenFreeMap and need an internet connection. Only photos in a library appear. See ADR-0039
- About — A place at the foot of the sidebar (
#about, also behind the logo) with topics: How it works, Your data (what stays on your computer and the few things that leave it, and when — there is no telemetry and no update check), No warranty (what Unterlumen does to files, and why to keep a backup) and Licenses and thanks (every project it is built on or calls, and the map data it shows, with its license, website and, where it takes support, a link to support it; see License). A first start says once, at the foot of the sidebar, that Unterlumen comes without warranty - Explains itself — Under the title of each place one sentence says what it is and what it does to your files, and links the places it names. "How Unterlumen works" (
#guide, linked from each sentence and Settings, and a topic of About) shows the whole model in a diagram — your folders, Folders, libraries, Map and Timeline, galleries, destinations — with a paragraph for each. See ADR-0041 - Statistics — A place under Explore (
#statistics) with a topic per sidebar sub-entry, shown while Statistics is open: Equipment, Exposure, Time, Frame, Colour, Places and People. The overview shows a card for each, which is how to reach them with the sidebar collapsed. Each card previews up to four of its topic's charts. Exposure, Time, Colour, Places and People end with 3D views. A library select limits it to one library; a library's Statistics button opens it for that library and the folder shown. Developments over time are lines, one per camera or frame shape, with a legend and every value of a period on hover. Colour shows black and white against colour, the colour of each period as a strip of swatches, the main colours on a hue wheel, and warm against cool through the year (all years together or one year), from the measurements of ADR-0044. In Colour, Colour space shows every photo as a glowing point at its main colour in 3D (lightness up, hue around, chroma outward), with a trail through each period's mean colour. You can turn and zoom it, hover over a point to read it and click it to see the photo. In Exposure, Exposure space shows every photo by focal length, aperture and ISO in stops, coloured by camera, so your shooting habits show as glowing clusters. In Time, Daylight shows every photo by day of the year (around), hour (outward) and brightness (up). In Colour, Character shows every photo by brightness, contrast and colourfulness. In Places, Space and time shows every located photo by where it was taken, on a floor of the world seen from the middle of your photos with distance on a log scale that widens beyond 1,000 km, and when, upward. A trail of days climbs the home column and reaches out on journeys. Coastlines and borders are Natural Earth data (public domain), included in the app. People counts named people, put together across libraries by name (Compare, in its head, puts the people you choose side by side): who appears most, people over time, who with whom (photos two people share), and photos by the number of faces found in them; its 3D views are Face space (every face placed by what it looks like, in its person's colour), Circle of people (people drawn together by the photos they share) and Life lines (a lane per person through time, as high as their photos per period). Full view shows only the 3D view and its photos, dark, over the whole window. They need a browser with WebGPU (ADR-0046). The statistics and the search read from indexes that cover them, so they stay fast on libraries of tens of thousands of photos (ADR-0045). A column beside the charts shows every photo of the scope when you arrive on the desk; clicking a value — a lens, an f-stop, an hour, a day, a camera in one year, a hue — shows its photos there instead; they open read-only. See ADR-0043 Colour combinations counts the photos of well-known colour pairs and triads (orange and teal, say) as chords across the hue circle, and Your combination any two or three hues chosen on a wheel; the library filter has the same Colours (ADR-0049). - Photo appearance — After each scan a library measures each photo's thumbnail: black and white, toned or colour, its average and main colours, brightness and contrast, sharpness and detail, and a hash for look-alikes. Photos indexed before are measured at the next start; Edit library → Maintenance → "Read everything again…" measures a library anew. The values are the ground for statistics and search by colour to come. See ADR-0044
- Timeline — Every dated photo of every library on one time axis, each photo once even when it sits in several libraries. On a desk the photos run left (oldest) to right (newest) in two to four rows (⋯ menu) above a time bar: an overview with bracket handles that limits the view to a span of months, and an axis with a graph of how many photos were taken when, years and month names, and a frame marking what the rows show. Drag the frame or click the axis to go somewhere; scroll the photos and the frame follows. A click selects a photo and the info panel (I) shows it, a double click or Enter opens it read-only. On a phone the newest come first, grouped by month, with a scrubber down the right edge. Only the photos near the screen are loaded. Photos without a date taken are left out and counted. See ADR-0040
- People — A place under Explore (
#people) for the people in your photos. See ADR-0051 - Things and search — Animals, objects and scenes found on this computer by Google's SigLIP 2 (Statistics → Things, Download and turn on, 690 MB), and which photos are blurred, too dark or too bright. They are written into each photo's sidecar as keywords, in the library's keyword language. A search sits above every place for people, things and any words, in many languages (
/). See ADR-0054 and ADR-0055 - English and German — Settings → Language switches the interface; by default it follows the browser. A library's things are written into the sidecars as keywords in a language chosen when the library is made, because Lightroom, Apple Photos and other programs read them. Editing the library changes it later, and then Unterlumen rewrites only its own keywords. See ADR-0056
- Found on this computer — ONNX Runtime and two models from OpenCV (YuNet finds faces, SFace tells them apart). Download and turn on fetches them once (about 50–120 MB, depending on the system), checked against fixed checksums; nothing is sent anywhere afterwards. The single binary stays cgo-free: ONNX Runtime is loaded at run time. After each scan new photos are looked at in the background. Not available on Intel Macs.
- Everyone in one place — People opens with everyone named in any of your libraries, linked by name, a field to find them, and the libraries where groups of faces wait for a name.
- Naming — Faces that look alike form a group, one line each: type a name, take the suggestion ("Is this Anna?"), say No, or hide faces nobody is to name. A group's page starts from its suggestion and names all or the selected faces; the ones left stay there to be named too. Hidden faces are listed under N faces hidden — for one library on its page, for all of them on People's first page — and can be shown again. A group's page names all its faces or only those you select. Every name field offers the names of all libraries. Resting the pointer on a face shows it framed in its whole photo.
- A person's page — All their faces in one grid, newest photo first, across libraries. Select faces to make one the cover, give them to someone else (Is instead) or send them back to the groups. Rename… and Take the name away… choose the libraries. Often with names who is most often with them; a click shows the photos of both. Maybe Anna lists the unnamed faces that look most like them, to take or refuse. For a library without the name, the page says who there looks like them.
- Faces that were not found — A face in profile or half hidden is marked in the viewer: Mark a face, drag a frame around it on the photo and type who it is. It then counts like any other; × removes it.
- In a photo's info — Who is in it, with their age that day: type a name beside a face, take the suggestion, or × to take one away.
- Suggestions that learn — A face is compared with each named person's own faces, the nearest three, so every face you name makes the next suggestions better; a No (or "This is not Anna") is kept, so the same question does not come back. Nothing is named without you.
- Birthdays and ages — A person's ⋯ menu takes their birthday, kept by name in the folder your installations share (not in the photos). Ages read "8 years old".
- Filters and statistics — The library filter has a Person chip; Map, Timeline and Statistics add people in their filter. Several people find the photos with all of them. Statistics has a People topic: who appears most, over time, who with whom, by age, and three 3D views; Compare puts the people you choose side by side.
- In the sidecar — Names are written into each photo's XMP sidecar as Metadata Working Group face regions, which Lightroom, digiKam and your other installation read; names other programs wrote there are picked up. A name given, changed or taken away on one installation follows on the other. Hiding a face, a No and the cover chosen for a person are kept in the folder your installations share (
face-decisions.jsonbesidechannels.json), not in the photos, and follow too. - On a phone — Find a person and see their photos by year, with who is most often with them.
- Galleries — A dedicated "Galleries" place lists every gallery across every destination (the UI name for a channel), grouped by destination and showing its state in words: Not published, or Published with its date, with a note where something is left (photos waiting, built but not uploaded). An action appears only where there is something to do — "Publish", "Publish N changes", "Check again". Link reachability is checked once when the screen opens and after each publish, and is reported next to the state with the time of the check rather than as a state of its own. Opening a gallery shows the photos waiting to go online (removable individually), its title, date, visibility toggle (single-gallery destinations), address, and Unpublish — with a scoped remote delete over SSH for rsync destinations. The channels list links to a destination's public site and to its galleries A destination that only exports files, such as Instagram, keeps no list of its own; its posts are listed from the photos' publication records in the sidecars, an untitled one as "Post of" its day.
- Image viewer — Full-screen image view with keyboard navigation, and a button that downloads the original file (a HEIF stays HEIF). Its ⋯ menu does to the photo on screen what an overview does to a selection — Add to gallery…, Export…, Rename…, Set location…, Show in Organize — also when it was opened from the Map, Statistics or the Timeline; Show in library opens the library at the photo's folder with the photo selected
- Download originals — Download in the selection bar saves the selected photos as they are: one as its file, several as a ZIP of the originals
- Crop tool — Interactive crop in the fullscreen viewer. Draw a rectangle, pick an aspect ratio (free, standard, or cinema formats), and save in-place. All metadata including Fujifilm film simulation is preserved via exiftool
- Info panel — Sidebar, open from the start on a desk (closed, it stays closed for the session), showing file metadata, EXIF data, and location map for GPS-tagged photos. In library mode: editable title (stored as
dc:titlein the XMP sidecar, interoperable with Lightroom/Capture One) and free fields (stored asul:Fieldsin the same sidecar, so a second installation reads them too; the library's index only copies them — see ADR-0048), and a Publications section showing compact cards for each channel a photo was published to. Clicking a folder shows a folder dashboard: total size, file count, nesting depth, a squarified treemap of subfolder sizes (click to navigate), and a file-type breakdown. In library mode the folder dashboard also shows EXIF-based photo statistics (shooting date range, format breakdown, camera × lens usage, hourly activity chart). Available in browse, library, and fullscreen viewer - Convert & Export — Export selected images to JPEG, PNG, or WebP with quality control, flexible scaling (original, percentage, max dimension), and EXIF metadata options (strip, keep, or keep without GPS). Shows per-file estimated output size and pixel dimensions. Saves to a local folder or downloads as a ZIP; server mode (
UNTERLUMEN_ROOT_PATH) is ZIP-only - Batch rename — Rename multiple photos using EXIF-based patterns (date, camera, film simulation, image title, etc.) with color-coded draggable token pills, live preview and conflict resolution. While it runs, the dialog says how many files it is renaming; the server reports no count, so none is shown. The
{title}token inserts the photo's slugified title. Works in browse mode and all library views. Also includes a simple single-file rename option - Geolocation editing — Set or remove GPS coordinates on one or more images via an interactive map picker (requires exiftool)
- Folder previews — Fast (a photo's library thumbnail, or the small preview stored in the file, JPEG and HEIF alike) or Sharp (the whole image decoded at the size your screen needs), selectable in Settings; only Folders uses it
- Storage — Settings shows what the cache and the data folder hold, part by part, with what each part is for: previews and viewer images (each can be cleared; the cache keeps within a limit, 5 GB unless you choose another, dropping the files used least recently), and each library's thumbnails and index, face recognition and destination output (shown, with a link to where they are looked after). The installed app opens either folder in Finder or the file manager
- Sorting — By name, date, or size, ascending or descending
- Multi-select — Click, Shift+click, Ctrl/Cmd+click for bulk operations
- Status bar — Live image count and selection count in every pane
- Activity and the status line — Everything that takes a moment says what it is doing in words ("Reading the folder…"), and after 3 seconds for how long. Where the total is known it shows a bar and "412 of 1 280 photos". Nothing shows for quick answers. Long work — library scans, ZIP exports, publishing, site rebuilds, deploys — also appears at the foot of the sidebar and stays there when you move to another place; a failure stays until you click it. See ADR-0036
- EXIF/HEIF orientation — Portrait and rotated images display correctly
- HEIF support — Automatic conversion via ffmpeg (requires ffmpeg installed)
- Read-ahead prefetch — The viewer prefetches the next two images on each navigation for near-instant forward navigation, even over a NAS
- Fujifilm film simulation — Film simulation name (e.g. Classic Chrome, Velvia, Acros) shown in the info panel and as a grid overlay badge for Fujifilm images
- Formats — JPEG, PNG, GIF, WebP natively; HEIF/HEIC/HIF via ffmpeg
- Videos — MP4, MOV, M4V and WebM play in the browser; MKV, AVI and MTS are listed with a still frame and downloaded. A video tile shows its length and plays a three-second silent clip while the pointer rests on it (made once by ffmpeg, then cached). The clip of an iPhone Live Photo is not listed on its own: the photo says "Live" and plays it in the viewer. Videos in a place's ⋯ shows or hides them; Kind at the top of the library filter (Photos and videos · Photos · Videos) finds photos or videos only, and overrules videos hidden in the ⋯. The filter of Map, Statistics and the Timeline has Kind too. Videos are never changed: crop, location, rename, export and publishing are for photos. See ADR-0058
Paste one line into a terminal. It downloads the newest release, checks it, installs the helper programs Unterlumen uses (ffmpeg, exiftool, cwebp, heif-convert), sets Unterlumen up as an app with an icon and opens it. Running the same line again updates it.
macOS and Linux — open Terminal and paste:
curl -fsSL https://huepattl.de/unterlumen/install | sh
Windows — open PowerShell and paste:
irm https://huepattl.de/unterlumen/install.ps1 | iex
Unterlumen then opens on its setup page: choose the folder that holds your photos, and press Set up Unterlumen. If another Unterlumen installation already works in that folder — on a NAS, say — the page offers to share its destinations and galleries (see Sharing channel config across installations). Settings links back to the setup to change the folder later. Wherever the installed app asks for a folder, it opens your system's own folder dialog, which shows your NAS and external disks.
Where the helper programs come from: Homebrew when it is installed, otherwise, on a Mac, the makers' own downloads (ffmpeg by martin-riedl.de, exiftool from exiftool.org's SourceForge, cwebp from Google) into Unterlumen's own tools folder; apt, dnf or pacman on Linux (sudo asks for your password); winget on Windows. The scripts are install/install.sh and install/install.ps1 — read them before you run them if you like. UNTERLUMEN_SKIP_TOOLS=1 leaves the helper programs alone, UNTERLUMEN_VERSION=0.13.0 installs that version.
- macOS: Unterlumen.dmg — for Apple Silicon and Intel. Open it and drag Unterlumen onto Applications. The first time you open it macOS says it cannot check the app, because it is not signed by an Apple developer account: open System Settings → Privacy & Security, scroll down to the note about Unterlumen and click Open Anyway.
- Windows: Unterlumen-Setup.exe — installs for your user without administrator rights. If Windows shows "Windows protected your PC", click More info, then Run anyway.
A package cannot install ffmpeg and exiftool for you; the setup page (and Settings) offers Install the missing ones instead.
Download the latest release for your platform from the Releases page and extract the archive — you will get a single file called unterlumen (or unterlumen.exe on Windows).
The installer sets Unterlumen up as a proper desktop application with an icon, so you can open it from Spotlight, Launchpad, or the Start Menu just like any other app — no terminal needed afterwards.
-
Open Terminal — press Cmd + Space, type
Terminal, and press Enter. A window with a text prompt appears. -
Go to your Downloads folder — type the following and press Enter:
cd ~/Downloads -
Allow the file to run — type the following two commands, pressing Enter after each:
xattr -d com.apple.quarantine unterlumen chmod +x unterlumenThe first command removes macOS's download restriction — macOS blocks programs downloaded from the internet by default, and this tells it the file is safe to run. The second makes the file executable.
-
Run the installer — type the following and press Enter:
./unterlumen -desktop-install -
Done. Open Unterlumen: Folders browses any folder on your Mac, and Libraries → New library… catalogs one. Unterlumen now appears in Spotlight (press Cmd + Space and type "Unterlumen") and in Launchpad. You can close the Terminal window.
If macOS blocks the app when you first open it ("cannot be opened because the developer cannot be verified"), open System Settings → Privacy & Security, scroll down, and click Open Anyway. If that button does not appear (common on macOS Sonoma and later), open Terminal and run:
xattr -d com.apple.quarantine ~/Applications/Unterlumen.appThen try opening Unterlumen again from Spotlight or Launchpad.
-
Open PowerShell — press the Windows key, type
PowerShell, and press Enter. A blue window with a text prompt appears. -
Go to your Downloads folder — type the following and press Enter:
cd $HOME\Downloads -
Run the installer — type the following and press Enter:
.\unterlumen.exe -desktop-install -
Done. Unterlumen now appears in the Start Menu. You can close the PowerShell window. Open it: Folders browses any folder, and Libraries → New library… catalogs one.
-
Open a terminal — on GNOME, press the Super key (the Windows key on most keyboards), type
Terminal, and press Enter. On KDE, right-click the desktop and choose Open Terminal. The terminal name varies by distribution (GNOME Terminal, Konsole, xterm, etc.) but any of them will work. -
Go to your Downloads folder — type the following and press Enter:
cd ~/Downloads -
Allow the file to run — type the following and press Enter:
chmod +x unterlumen -
Run the installer — type the following and press Enter:
./unterlumen -desktop-install -
Done. Unterlumen now appears in your application launcher (GNOME Activities, KDE application menu, etc.). You can close the terminal window. Open it: Folders browses any folder, and Libraries → New library… catalogs one.
To update Unterlumen, run the one line again, or download the new binary and run -desktop-install again — it replaces the app and keeps your settings. They are in config.json in Unterlumen's configuration folder (~/Library/Application Support/Unterlumen on macOS, %APPDATA%\Unterlumen on Windows, ~/.config/Unterlumen on Linux). Installing over an older version copies the settings its launcher carried into that file first.
Pre-built images for linux/amd64 and linux/arm64 are on the GitHub Container Registry and include ffmpeg, heif-convert, cwebp, and exiftool — no separate install needed. Jump to the Docker / Podman section below.
Requires Go 1.27+. Binaries built with it run on macOS 13 Ventura or later.
cd src && go build -o ../unterlumen .
- ffmpeg — required for HEIF/HEIC/HIF support (embedded preview extraction and HEVC decode fallback), WebP export (when built with
libwebp), and videos' still frames, length and date, and their hover clips (encoded withlibx264) - cwebp (from
libwebp/brew install webp) — required for WebP export when ffmpeg is built withoutlibwebp(e.g. the default Homebrew ffmpeg on macOS). If ffmpeg already has WebP support, cwebp is not needed. - heif-convert (from
libheif-examples/libheif) — recommended alongside ffmpeg; handles HEIF files that ffmpeg cannot parse, such as standard Fujifilm HEIC files that carry no embedded JPEG preview stream. Without it those files show a placeholder instead of a thumbnail. - glibc on Linux — the Linux binary is linked against glibc's loader (so that it can load ONNX Runtime for People) and runs on Debian, Ubuntu, Fedora, Arch, Raspberry Pi OS and the like. On musl systems such as Alpine, or on NixOS without nix-ld, use the container image. See ADR-0051
- ONNX Runtime and the face models — needed for People; downloaded from inside the app (People → Download and turn on) into the data folder, not installed system-wide
- exiftool — required for Set/Remove Geolocation, Batch Rename, and Export EXIF copy/GPS-strip
Once installed with -desktop-install, just open Unterlumen from your OS launcher (Spotlight / Launchpad on macOS, Start Menu on Windows, application grid on Linux). The app opens in its own window and closes cleanly when you are done.
You can also run Unterlumen directly from the terminal without installing it. This is useful for scripting, server deployments, or trying it out before installing.
./unterlumen [flags] [directory]
Arguments:
| Argument | Description |
|---|---|
directory |
Directory to start in (default: home directory). Navigation is unrestricted — users can navigate to any directory on the filesystem. |
Flags:
| Flag | Default | Description |
|---|---|---|
-port |
8080 |
HTTP server port (env: UNTERLUMEN_PORT) |
-bind |
localhost |
Bind address (0.0.0.0 for remote access) (env: UNTERLUMEN_BIND) |
-lib-dir |
~/.unterlumen |
Root directory for library data (env: UNTERLUMEN_LIB_DIR) |
-channels-dir |
(same as -lib-dir) |
Directory for channels.json; override to share channel config across installations (env: UNTERLUMEN_CHANNELS_DIR) |
-desktop |
off | Open in a Chrome/Chromium app window (no URL bar). Server exits when the window is closed. Falls back to the default browser if Chrome is not found. |
-desktop-install |
— | Installer: sets up a native app launcher with icon (macOS .app, Linux .desktop, Windows Start Menu shortcut). It asks nothing; the app is set up on its first start. |
Environment variables:
| Variable | Description |
|---|---|
UNTERLUMEN_PORT |
HTTP server port. Overridden by -port flag. |
UNTERLUMEN_BIND |
Bind address. Overridden by -bind flag. |
UNTERLUMEN_ROOT_PATH |
Restrict navigation to this directory. The server starts here and users cannot navigate above it. Takes effect only when no directory argument is provided. |
UNTERLUMEN_LIB_DIR |
Root directory for library data (SQLite databases, thumbnails, channel exports). Default: ~/.unterlumen. Overridden by -lib-dir flag. |
UNTERLUMEN_CHANNELS_DIR |
Directory for channels.json. Default: same as -lib-dir. Overridden by -channels-dir flag. See Sharing channel config across installations. |
The installed app's settings: started without a directory and without UNTERLUMEN_ROOT_PATH — the way the installed app's launcher starts it — Unterlumen reads -lib-dir, its shared folder (-channels-dir) and port from config.json in its configuration folder; Settings → Shared folder changes them. It has no photo folder: Folders browses the whole disk and each library names its own folder. A photosDir from an older version is removed at start, and its .unterlumen-shared becomes the shared folder. A flag or an environment variable still wins over the file.
Path resolution priority:
- Command-line argument — starts in the given directory; navigation unrestricted (up to filesystem root)
UNTERLUMEN_ROOT_PATHenv var — starts there and restricts navigation to that directory- Default — starts in the user's home directory; navigation unrestricted
Examples:
# Browse photos in ~/Pictures and open in the browser manually
./unterlumen ~/Pictures
# Open as a desktop app window (Chrome required; falls back to default browser)
./unterlumen -desktop ~/Pictures
# Use a different port
./unterlumen -port 3000 ~/Pictures
# Allow access from other machines on the network
./unterlumen -bind 0.0.0.0 ~/Pictures
# Restrict navigation to /mnt/photos (useful for self-hosted setups)
UNTERLUMEN_ROOT_PATH=/mnt/photos ./unterlumen
Then open http://localhost:8080 in your browser (or whichever port you configured).
Pre-built images for linux/amd64 and linux/arm64 are published to the GitHub Container Registry and include ffmpeg, heif-convert, cwebp, and exiftool — no separate install needed.
docker run -p 8080:8080 -v /path/to/photos:/photos ghcr.io/bjblazko/unterlumen:latest
Podman: The container runs as UID 1000, but on macOS the mounted directory is owned by your host user (typically UID 501 or 502). Pass --user $(id -u):$(id -g) to run as your own UID:
podman run --user $(id -u):$(id -g) -p 8080:8080 -v /path/to/photos:/photos:ro ghcr.io/bjblazko/unterlumen:latest
Then open http://localhost:8080.
By default the container runs in server mode — navigation is locked to /photos. Override environment variables to change behaviour:
| Variable | Default (container) | Description |
|---|---|---|
UNTERLUMEN_PORT |
8080 |
HTTP port |
UNTERLUMEN_BIND |
0.0.0.0 |
Bind address |
UNTERLUMEN_ROOT_PATH |
/photos |
Root directory (navigation locked here) |
UNTERLUMEN_LIB_DIR |
/data |
Libraries, thumbnails and generated galleries; mount a volume here |
UNTERLUMEN_CACHE_DIR |
/cache |
Preview cache |
Example with Docker Compose — or let huepattl.de/products/unterlumen-nas write it for your NAS, with the steps for Synology, QNAP, TrueNAS, Unraid or Portainer:
services:
unterlumen:
image: ghcr.io/bjblazko/unterlumen:latest
user: "1026:100" # the owner of the photos; Unterlumen writes there
ports:
- "8080:8080"
volumes:
- "/volume1/photo:/photos"
- unterlumen-data:/data # libraries, thumbnails, generated galleries
- unterlumen-cache:/cache
restart: unless-stopped
volumes:
unterlumen-data:
unterlumen-cache:Portainer: enter https://huepattl.de/unterlumen/portainer.json under Settings → App Templates; Unterlumen then appears under App Templates and asks for the photo folder, its owner and the port. (That URL replaces the templates Portainer lists by default.)
The image keeps its libraries in /data (UNTERLUMEN_LIB_DIR) and its cache in /cache; both are writable for any user, so user: can be the owner of the photos.
If you run Unterlumen on more than one machine against the same photo folders (e.g. a Docker instance on a NAS that also serves the photos, plus a native install on a Mac that mounts them over the network), each installation normally has its own independent channels.json under its own -lib-dir — so a channel published from one install won't be recognized by name on the other, even though the underlying "published" record in the photo's .xmp sidecar is already shared and portable.
Point -channels-dir (or UNTERLUMEN_CHANNELS_DIR) on both installations at the same directory on a filesystem they can both reach (e.g. a folder alongside the photo library on the NAS) to share channel definitions between them, while -lib-dir (SQLite database, thumbnails, search index) stays independent and local to each machine as usual:
# NAS (Docker), photos already mounted at /photos:
docker run -p 8080:8080 \
-v /path/to/photos:/photos \
-e UNTERLUMEN_CHANNELS_DIR=/photos/.unterlumen-shared \
ghcr.io/bjblazko/unterlumen:latest
# Mac (native), same folder reachable over the network mount:
./unterlumen -channels-dir "/Volumes/<share>/.unterlumen-shared" ~/Pictures
By convention: on a server, a folder named .unterlumen-shared directly inside the folder it serves is used this way without any flag. On the NAS, Settings → Sharing → Share with another installation makes it — so with the NAS example above, the Mac only has to choose that folder (as it mounts it) under Settings → Shared folder. Choosing a folder there also makes .unterlumen-shared when none is there yet, and copies this installation's destinations into it; what is already shared there is never overwritten.
Libraries are shared one by one. In Edit library, Share with other installations writes a small .unterlumen-library.json (the library's ID, name and description, no path) into the library's folder. The other installation adds that folder with New library… and gets the same library — same ID, same name — with its own index. A library made of that folder before it was shared offers Join instead, which keeps its index and thumbnails. Libraries you do not share stay on one installation, so two installations can have some libraries in common and not others. See ADR-0047.
Notes:
- The mount used for
UNTERLUMEN_CHANNELS_DIRmust be writable (the read-only:romount shown above works for browsing but not for a channels directory located on it). - A website channel also keeps its album list in the shared directory, one file per album under
albums/<channel>/, so publishing album A from one installation and album B from the other leaves a site index and sitemap with both. Shared:channels.json, the album register (one<postID>.jsonper album, and a<postID>.deletedtombstone for a deleted one), and the XMP sidecars next to the photos, which record each album's address. Per machine:-lib-dir(database, thumbnails, search index), each destination's output folder (output-paths.jsonin-lib-dir), and the generated output, including itssite.jsoncache. The album files contain no paths, so the two installations may disagree about the photo folder's location. When upgrading an existing setup, update and open the installation that publishes (and holds the realsite.json) first: it copies its albums into the register once, and only then should the other installation start. Rebuild album list (Destinations → Advanced) then writes each album's address into its photos' sidecars, and restores the list from them if it is ever lost; see ADR-0035. channels.jsoncan include credentials (e.g. publish tokens) for some channel handlers — only point installations at a shared directory you trust equally.
| Key | Action |
|---|---|
| Arrow keys | Move through folders and photos in browse view — Up and Down to the tile above or below; prev/next in image viewer |
| Enter | Open the focused folder or image (the one the arrow keys moved to) |
| Space | Toggle selection of focused item |
| Escape | Close viewer / go up a directory / close the Map's photos column |
I |
Collapse or expand the info panel |
\ |
Collapse or expand the sidebar |
| Backspace / Delete / Cmd+D | Mark selected files for deletion |
| Cmd/Ctrl+A | Select all files in current pane |
| 1–5 / 0 | Give the selected photos (or the focused one, or the photo in the viewer) that many stars / take the rating away — in Folders, a library and the viewer; written to the sidecar |
, |
Settings |
| 1–9 / 0 in Organize, with a selection | Send it to that target / mark it for deletion |
| Tab | Switch panes in File Manager mode |
| F5 | Copy selected files (File Manager) |
| F6 | Move selected files (File Manager) |
| Ctrl/Cmd + Click | Toggle selection |
| Shift + Click | Range selection |
- Changelog
- Architecture (arc42) — system overview, building blocks, decisions, and ADR index
Requires the binary to be built first.
cd src && go build -o ../unterlumen .
cd e2e && npm ci
npm run setup # download test fixtures once
npm test # run all tests headlessly (CI mode)
npm run test:headed # run with browser visible
To use the Playwright interactive UI — watch tests run step-by-step, inspect DOM snapshots, and re-run individual specs:
cd e2e && npx playwright test --ui
This opens a browser-based test runner at a local port. Select any spec or individual test in the sidebar and click the play button to run it with a live preview pane.
Test reports and failure screenshots/videos are saved to e2e/playwright-report/ and e2e/test-results/.
The pictures in Screenshots come from a script, not by hand:
cd src && go build -o ../unterlumen .
cd e2e && npm run screenshots # all of them
npm run screenshots -- map viewer # only these
It starts a throwaway server on port 8097 with a copy of src/examples and its own library directory, sets up a library, destinations and galleries through the API, and writes WebP files to doc/screenshots/ (light theme; folders-dark is the dark twin). It needs cwebp (brew install webp) and a network connection for the map tiles. The shots themselves are in e2e/screenshots/shots.mjs.
The Things shots need SigLIP 2, the object detector and ONNX Runtime in e2e/fixtures/.unterlumen-things/ (things/ and onnxruntime/ as a Things download leaves them in a library directory, about 735 MB, not in git); the script links them in.
- Browse/cull/file-manager state is in-memory and discarded on exit. Library mode writes SQLite databases and thumbnails to
~/.unterlumen/(or the configuredlib-dir) - By default the server binds to
localhostonly; use-bind 0.0.0.0if you need remote access (no authentication is provided) - HEIF/HEIC/HIF conversion shells out to ffmpeg; file paths are passed as arguments (not interpolated into a shell string)
UNTERLUMEN_ROOT_PATHis ignored when a directory argument is also provided on the command line
Unterlumen is licensed under the Apache License 2.0.
It is built on open-source projects that keep their own licenses. About › Licenses in the app lists each of them with its license, its website and, where it takes support, a link to support it. The license texts of everything built in are inside the binary and in the licenses/ folder of every release archive; the list is src/web/licenses/credits.json.
The helper programs Unterlumen calls — FFmpeg, ExifTool, libheif, libwebp — are not part of it and keep their own licenses, some of them GPL or LGPL. The container image includes them from Debian; each package's copyright file is in /usr/share/doc/<package>/copyright, and its source is at sources.debian.org.
Maps show data © OpenStreetMap contributors, served by OpenFreeMap.
The example photos in src/examples/ are © Timo Böwing, all rights reserved, and not under the Apache License; see src/examples/LICENSE.


































