A Chrome extension (Manifest V3) for grabbing photos out of your self-hosted Immich library at the size you actually want — instead of always landing the full-resolution original on your clipboard.
See PRIVACY.md for exactly what data this extension touches and where it goes.
chrome://extensions.https://photos.example.com or http://192.168.1.50:2283) and paste the API key.Alternatively, choose “Use logged-in browser session” as the auth mode to piggyback on your existing Immich web login instead of an API key (no key to manage, but it only works while you’re logged into the Immich web UI in some tab).
Server details (label, address, auth mode, API key) are all editable afterward — click “Edit” on a saved server rather than needing to remove and re-add it. Changing the address requests Chrome permission for the new domain automatically; leaving the API key field blank when editing keeps whatever’s already saved rather than clearing it.
Export settings (in Options) downloads everything on the page — servers, shortcuts, AI provider settings, prompt templates — as a single JSON file, for moving to a new browser or keeping a backup before making changes. This file contains your API keys in plain text (both Immich and any configured AI provider key) — treat it like a password, not something to share or commit to a public repo.
Import settings replaces your current configuration with what’s in a previously-exported file. Since Chrome’s per-domain permissions are tied to the specific browser profile that granted them, imported servers automatically get their permission re-requested in the new profile — Chrome may show a prompt per server; if one doesn’t confirm automatically, fix it individually afterward via that server’s “Edit” → “Save changes”.
| Shortcut | Action |
|---|---|
1 |
Copy thumbnail to clipboard |
2 |
Copy preview to clipboard (recommended default for slides) |
3 |
Copy original to clipboard |
Shift+1 |
Download thumbnail |
Shift+2 |
Download preview |
Shift+3 |
Download original |
Shift+E |
Export the whole currently-open album |
Shift+S |
Export every marked photo, in one run |
E |
Mark/unmark the current photo for export |
X |
Open the crop tool on the currently open photo |
M |
Build a journey map of the current album |
S |
Open Location Stories for the current album |
D |
AI-suggest a description for the open photo |
All of these are fully remappable from the options page — click a shortcut box and press your preferred combo.
Shortcuts work when:
They’re inactive while typing in a text field, and only active on domains you’ve added as a server.
Marks persist across page reloads and revisits (stored locally per-server), so you can build up a hand-picked list over multiple browsing sessions before exporting. Clear marks always asks for confirmation first — the one time it doesn’t is right after a successful export, since the marks having done their job isn’t something worth re-confirming.
Two ways to mark a photo, both feeding the same list:
E while hovering a thumbnail or with a photo open in the lightbox — no mouse combo needed, works on whatever you’re currently looking at.Marked photos get a blue outline and an “Export” badge in the corner. Press Shift+S (or “Export marked” in the panel/popup) to export every marked photo in one run — same size/format settings as bulk album export, just working from your hand-picked list instead of a whole album.
“Export whole album” is the separate, simpler option: exports everything in the currently-open album with no picking required.
One implementation note, since Immich’s grid virtualizes its DOM (the same on-screen element gets reused for a different photo as you scroll): rather than styling a thumbnail once and trusting that reference to stay valid, the extension re-checks every currently-rendered thumbnail against the real marked list periodically and on hover, so outlines/badges can’t end up glued to the wrong photo after scrolling.
Chrome’s Clipboard API only reliably accepts PNG for images — JPEG and WebP aren’t supported for a normal cross-app paste (Chrome’s own ClipboardItem docs guarantee only text/plain, text/html, and image/png). So every Copy action gets re-encoded from Immich’s JPEG/WebP into PNG before it reaches the clipboard. PNG is lossless, so a re-encoded photo can easily come out 5–10x larger than the source JPEG (a 400 KB preview can become several MB) — that’s expected, not a bug, and it’s why Copy and Download can differ so much in size for the “same” photo. Downloads aren’t affected — they save Immich’s original bytes as-is, no re-encoding involved.
To keep Copy payloads reasonable, the extension caps the image’s pixel dimensions before the PNG conversion (default: 1600px on the longest edge) — fewer pixels means a proportionally smaller PNG even though it’s still lossless. Adjust or disable this under Options → Clipboard copy size.
A small collapsible panel appears in the bottom-left corner of any Immich page you’ve added as a server, with the same copy/download-by-size actions as buttons, plus Export whole album and Export selection buttons. It’s intentionally a self-contained overlay rather than buttons injected into Immich’s own toolbar — Immich’s UI is built with Svelte and uses hashed, version-specific element IDs, so grafting into it directly would be the first thing to break on an Immich update.
The panel’s buttons enable/disable based on context (an asset open or hovered, an album page, or an active selection) and can be collapsed with the – in its header. Turn it off entirely from Options → In-page overlay if you’d rather rely on keyboard shortcuts only.
All downloads use Chrome’s downloads API with saveAs: false, so the extension never triggers a Save-As dialog — files go straight into your default Downloads folder. The filename template (Options → Bulk export → Filename template, default {album}/{date}_{filename}) puts each file into an album-named subfolder automatically, which Chrome creates if it doesn’t already exist.
If you’re still seeing a save prompt on every download, that’s coming from Chrome itself, not the extension — check chrome://settings/downloads and turn off “Ask where to save each file before downloading.” That setting overrides every extension’s request to skip the dialog.
Single-photo downloads figure out the album name even when you’re viewing the photo in the lightbox (where Immich’s URL drops the /albums/{id} part and becomes just /photos/{id}) by remembering the last album you were browsing in that tab. If you open a photo without having come from an album — e.g. straight from search or the main timeline — it falls back to an immich/ folder.
The {date} token in the filename template uses the photo’s own capture date (from Immich’s exifInfo.dateTimeOriginal, falling back to fileCreatedAt), not the date you downloaded it — so 2026-08-19_20260524_183332_preview.jpg means captured 2026-08-19, with Immich’s own capture-timestamp filename and the size you chose appended.
Press X (or the “Crop open photo…” button) while a photo is open in the lightbox to enter crop mode: drag to select a region directly on the image, drag the selection to move it, drag a handle to resize it, then Copy crop or Download crop. Esc or the Cancel button exits without doing anything.
The crop selection defaults to the same aspect ratio as the source photo. The toolbar’s aspect-ratio dropdown offers Free (no constraint — resize however you like) plus common presets (1:1, 4:3, 3:2, 16:9, 2:1), and the ⇄ button flips whichever ratio is active between landscape and portrait orientation (e.g. 4:3 ↔ 3:4). Switching ratio or flipping re-fits your current selection to the new shape immediately, centered on where it already was.
The size dropdown (default: Full-size) controls which Immich-generated size is fetched as the source for the crop — a higher source size gives a sharper crop at the cost of a slightly slower fetch. This is separate from your regular Copy/Download size defaults, since crop quality needs are usually different from a quick whole-photo grab.
A few notes on how cropping works technically:
data-testid="preview" attribute in Immich’s markup, with a fallback that matches by asset ID — if a future Immich version renames that attribute, the fallback should still find it, but crop mode may need updating if both break.Press M (or “🗺️ Journey map…”) while on an album page — or on a photo from one — to generate a stylized route map: your album’s geotagged photo locations, grouped into stops, connected in chronological order, with labels you can customize.
Four built-in styles (Minimal Dark, Vintage Paper, Neon Night, Ocean Blueprint) draw a stylized diagram from scratch (shapes, gradients, text) using only the coordinates Immich already knows about your photos — locations plotted proportionally to their real relative distances (with a latitude correction so east-west spacing looks right). That’s a deliberate choice, not a shortcut: real map tiles (OpenStreetMap and most free alternatives) don’t send CORS headers, which means any pixels drawn from them onto a canvas become unreadable to canvas.toBlob() — the exact call both Copy and Download depend on. Rather than ship a “journey map” that silently fails to export, or add a paid tile-provider dependency this project has otherwise avoided entirely, these four styles skip real map tiles entirely.
The fifth style, World Atlas, is a genuine, real vector world map — not a decorative approximation — built from actual country-outline path data (the same technique full mapping libraries use, just without a tile server), so unlike raster tiles it isn’t a cross-origin resource and never risks tainting the canvas. Your route is drawn directly on it: since there’s no documented formula for exactly how this particular map’s paths were projected, the extension calibrates one itself from ~50 countries with well-known real-world coordinates, spread across the globe with extra density through the Caucasus, Central Asia, and East Asia (regions that showed the largest placement errors when anchor coverage was sparser there). Rather than fitting one single global transform, each point is projected using a locally-weighted fit — nearby anchors dominate, distant ones contribute less — so accuracy adapts to whatever the map’s actual local distortion is instead of assuming one straight-line formula holds everywhere. That’s still an approximation, not a survey-grade projection, but it’s generally close: on a plain equirectangular map (the common case for this style of SVG) the fit is exact. The view auto-crops to your trip’s actual extent (with padding) rather than always showing the whole globe — a compact trip only covers a slice of the world, and fitting the entire planet would waste most of the canvas and cram markers together.
Dense trips (hundreds or thousands of geotagged photos clustered around a handful of cities) get automatically grouped into “stops” by real-world distance — a red count badge shows how many photos are in a stop. The Group within slider (0–500km) controls the base grouping distance, and it’s zoom-adaptive: the effective distance shrinks as you zoom in (revealing finer detail — a “region” cluster splits into individual cities) and grows as you zoom out (consolidating further), the same way clustering works in Google Maps or Leaflet. Re-clustering during an active zoom/drag is debounced briefly so it stays responsive rather than recomputing on every scroll tick.
Labels use collision detection — bigger stops get priority, and a label that would overlap one already placed is simply skipped rather than drawn on top of it. If a region feels cluttered even after that, drag the slider looser or zoom in until stops separate enough for their labels to fit.
Click any stop (or single point) to add or edit its label — a text prompt lets you type anything, overriding the automatic city name Immich provided (or adding one where there wasn’t any). This works the same way whether it’s a 2000-photo cluster or a single photo.
Drag a visible label to reposition it anywhere on the canvas — useful for pulling a label out of a dense cluttered area (e.g. down to an empty corner) without hiding it. A dragged label is exempt from the automatic overlap-hiding, since you clearly wanted it visible where you put it. Click a label without dragging it to snap it back to its default position. Custom positions (and custom text) survive zoom-triggered re-grouping — the extension matches each edited stop to its nearest counterpart in the newly re-clustered set, so scrolling to zoom doesn’t wipe out label work you’ve already done.
Drag to pan, scroll to zoom on the map itself, or use the − / + / Reset view buttons in the toolbar. This crops what’s visible without affecting which photos are included — it’s purely about framing the exported image.
Background can be overridden with a custom color picker if you want something other than the selected style’s default — “Theme default” resets it.
Show counts turns off the red count badge on stops (keeping just the plain marker), and Cluster size (0–200%) controls marker size — at 0% every stop becomes the same small uniform dot regardless of photo count, not just a flattened version of the count-based scaling; at 100% it’s the normal count-scaled behavior; above that, big stops get exaggerated further.
Location data comes from the same GET /api/map/markers endpoint you can see in DevTools when browsing Immich’s own map view — one request for the whole library, filtered down to your album’s asset IDs, rather than fetching per-photo location one asset at a time.
Press S (or “📖 Location Stories…”) while on an album page — or on a photo from one — for AI-written background information and exportable slides about the places in your trip. This is the one feature in the extension that talks to something other than your own Immich server — everything else here only ever calls your Immich instance.
The first time you open Location Stories without an AI key configured, a short setup screen explains what it does, what it costs (roughly — check your provider’s own dashboard for exact numbers, since this varies by model), and lets you pick a provider, paste a key, and test the connection right there — no need to leave to Options and come back. “Skip for now” is remembered so it won’t ask again every time; everything except the AI-generated parts (browsing locations, renaming, etc.) works with no key at all regardless.
The album’s geotagged photos are organized into a three-level hierarchy:
country field where available; falling back to a point-in-polygon lookup against the same world map data the Journey Map’s World Atlas style uses, when Immich hasn’t tagged a country for a photo.City radius is adjustable (both live in the tool and as an Options default) because Immich’s own reverse geocoding often reports the district a photo was taken in (e.g. “Yanta”) rather than the overarching metro area (e.g. “Xi’an”) — for a large, sprawling city, a too-tight city radius will fragment it into several separate city-level entries, one per district, instead of one coherent city. Widen it if that happens.
POI and city detection are adjustable live inside the tool — a city radius slider, POI radius slider, minimum-photos slider, and minimum-share-of-city slider sit right above the location list and re-cluster on the fly, with matching defaults in Options if you want to change your starting point.
Each location gets a customizable prompt template with a choice of tone — Default, Factual (dry, statistic-heavy, no jokes), Humorous (playful and witty), Poetic (evocative, atmospheric) — plus room to add your own in Options. Templates support variables like {location_name}, {country}, {date_range}, {photo_count}, and {personal_note} that get filled in automatically. The model returns three things, not just a paragraph: background text as a bulleted list (3–5 short, punchy facts, shown left-aligned with bullet markers on the slide — one bullet per line in the editor, so add/remove/rewrite a bullet by editing its line), up to 3 fun facts (“did you know?” callouts), and KPIs — short label/value pairs for notable quantifiable stats about the place (founding year, population, elevation, whatever fits). Everything is editable — write your own instead of, or on top of, whatever the AI returns. Fun facts and KPIs are independently toggleable as groups (one checkbox each) — turn either off to leave it out of the slide regardless of what the AI supplied.
You control exactly how many fun facts and KPIs the AI should generate — independent 0/1/2/3 selectors next to each checkbox. At 0 it’s told to return none; at 1–3 it’s explicitly told to give its best plausible guess rather than skip one it isn’t fully certain about, since leaving the instruction open-ended (“include one if you’re confident”) tended to make the model play it safe and return nothing. The response schema also recognizes several alternate field names a model might use despite the explicit instructions (facts/stats/trivia instead of funFacts/kpis, name/stat instead of label/value), so a model that doesn’t follow the exact requested shape still gets picked up correctly rather than coming back empty.
The JSON schema instructions are never baked into a saved prompt template — they’re always appended fresh at generation time, checked for and added if missing rather than assumed to already be there. This matters because a template saved to your browser’s storage (the built-in default, a named tone, or one you’ve edited yourself) gets frozen exactly as it was the moment it was saved; if the schema requirements were only ever baked into that stored text, every later improvement to them would silently never reach anyone whose settings already existed, including anyone who’d customized their own template on top of an older version. Generating text always uses the current schema regardless of how old or how customized the underlying template is, while still fully preserving whatever you’ve customized in the content/tone portion.
Generated text (background, fun facts, KPIs) is saved per location and restored automatically the next time you open Location Stories for that album — so closing the tool or reloading the page doesn’t force a wasteful regeneration (and another round of API spend) just to get back to text you’d already generated. “Regenerate text” only re-spends tokens when you explicitly ask for a different take.
A personal note / memory field lets you add your own anecdote about a stop (optional, per-location) — if filled in, it’s woven into the prompt so the AI’s generated text can reference it naturally, rather than the text being generic to the place regardless of your own trip.
The model is asked to respond in structured JSON, and parsing is deliberately resilient across several layers: JSON-mode is requested from the API directly where supported, trailing commas are tolerated, and if one field still breaks strict JSON syntax (a model occasionally emits a raw line break or a trailing comma), the other fields are recovered independently via field-level extraction — including a last-resort scan for individual KPI objects even when the surrounding array itself is malformed — rather than the whole response being lost to one bad field.
Slide rendering is also guarded against a race condition where a slow operation (AI image generation in particular can take several seconds) finishes after a newer render has already started — without the guard, two overlapping renders could paint onto the same canvas out of order. And a since-fixed bug meant a single-photo slide (including AI-generated-image slides) could silently stop drawing right after the title — background bullets, fun facts, and KPIs never appeared — with no visible error; rendering errors now surface as a toast instead of failing silently.
The location name is an always-editable text field with a Save button — not just something that appears for unnamed spots. “🔎 Suggest names with AI” asks the model for 10 distinct candidate names at once (rather than one at a time), rendered as clickable chips below the name field; clicking one fills it in, and you can still type anything you want manually and hit Save (or Enter) regardless of what the AI suggested. For places in countries that don’t primarily use the Latin alphabet, the model is asked to include the local native-script name alongside the romanized one for at least a couple of candidates (e.g. “Xi’an / 西安市”) rather than only the international form. Nothing is ever applied automatically. Custom names persist — saved locally per server and album, and reapplied automatically the next time you open Location Stories for that album (including after adjusting the city/POI clustering sliders within a session), so renaming work isn’t lost when the tool closes or the page reloads.
Because Immich’s own per-photo city field is coarse (just the city name), it’s common for the finer POI clustering here to split one city into several genuinely distinct spots that all still show up sharing the same generic name — e.g. three separate “Yangshuo” entries for three different areas you actually visited. Each entry is still a distinct, correctly-clustered location; only the label is duplicated.
“🪄 Auto-name duplicates” in the toolbar finds every such group and sends each one to the AI in a single batch request — the coordinates and photo counts for the whole group, asking for genuinely distinct names rather than guessing one at a time. One confirmation covers the whole batch (not one popup per location), and every suggested name stays fully editable afterward via the same “Refine name with AI” button or by typing directly.
Locations that already have a slide added to the current album (see “Adding a slide back into your Immich gallery” below) show a ✅ badge in both the location list and the detail pane, with the date it was added. This is tracked locally per server+album, so it persists across reopening the tool, and re-adding a slide for an already-marked location asks for confirmation first rather than silently duplicating it.
Toggle include/exclude/no-preference for photos with people (in the tool itself, or as a default in Options). This uses Immich’s own facial-recognition data — the people array Immich’s ML pipeline already computes per photo — not any new face detection run by the extension itself. Favorited photos always get priority in the selection, regardless of the face preference — a photo you’ve starred in Immich outranks everything else, even one with no faces in it, on the theory that you starring it is a stronger signal than any automatic heuristic. Favorited picks show a small ⭐ badge in the photo strip.
Photos with an Immich caption/description also get prioritized (📝 badge, hover to see the caption) — a described photo ranks above an undescribed one among the non-favorite fill-in picks, on the same logic as favorites: a caption is a real signal the traveler considered that photo worth noting. Those captions are also fed into the AI text-generation prompt as real context (not just used to pick photos) — via a {photo_descriptions} template token — so the model can work with actual specifics from your own notes rather than only generic location knowledge.
To decide which photos to even check, an evenly-spaced sample across the whole cluster is taken — not just the earliest photos chronologically, and larger for bigger scopes (up to 150 for a country, 100 for a city, 60 for a POI) — so favorites scattered anywhere in a large cluster have a real chance of being seen rather than only the first handful chronologically. For a cluster large enough that even that sample doesn’t cover everything, a “Load more photos” button appears below the photo strip, showing how many of the location’s total photos have been checked so far — each click roughly doubles the sample (reusing already-checked results rather than re-fetching them) until every photo in the cluster has been considered.
Country and city-level entries get a proportionally larger representative-photo count than an individual POI (up to 12 for a city, 16 for a country, vs. the configurable per-location default — 4 by default — for a POI), since they’re summarizing far more content. Favorited photos are never dropped just because of that cap, at any level — if a city’s child POIs each have several favorites, the city-level view expands to include all of them (up to a hard ceiling of 16) rather than being squeezed down to a flat default shared with a single small POI. Selected photos are always sent at Immich’s smallest thumbnail size to keep token cost down. Click any thumbnail in the photo strip to include/exclude it from the slide.
Four built-in styles — Minimal Dark, Vintage Postcard, Bold Modern, Editorial Light — each with their own background, typography, and accent color for the fact/KPI callouts, selectable per location. Aspect ratio is separately adjustable (16:9 / 4:3 / 1:1). Text is left-aligned throughout (title, bullets, fun fact) rather than centered, matching how slide decks are normally laid out.
KPI position is flippable — top-right (the default), top-left, bottom-right, or bottom-left. The text column automatically shifts to whichever side the KPIs aren’t on, rather than just being capped at a fixed width regardless of where the badges actually land, so a left-anchored KPI choice doesn’t collide with left-anchored text.
Layout is a separate, independently adjustable choice — Auto (the default) picks based on how many photos are selected: full-bleed background with overlaid text for one photo, a grid with a caption band for several (capped at 8 grid cells even if more are selected, so cells stay legible), plain text-only if none. Full-bleed photo, Photo grid, and Text only override that and force a specific look regardless of photo count — e.g. force a single photo (or the AI-generated image) into the grid layout instead of full-bleed for a different feel, or force full-bleed using just the first of several selected photos. A layout that needs photos but has none selected falls back to text-only gracefully rather than showing a broken slide.
“🎨 Generate AI image from selected photos” sends all of a location’s currently-selected photos (not just the first) to an image-generation-capable model (OpenRouter only — default google/gemini-3.1-flash-image) with a prompt asking for a stylized rendering that blends elements from all of them, suitable as a slide background, and swaps it in for the slide once generated. Click the generated-image badge to toggle back to the original photos at any time — nothing is destructive or permanent.
“➕ Add to Immich gallery” uploads the finished slide as a new asset in your Immich library (and, if you’re viewing an album, adds it there too), and always also saves a local copy to your downloads as a safety net alongside the upload — if the upload silently fails or something goes wrong server-side, the slide isn’t lost. Slides destined for the gallery are exported as JPEG specifically so a real EXIF block can be embedded — reusing the same writer built for preview downloads — with GPS coordinates from the location itself, and a timestamp set to one minute before the location’s earliest photo, in that photo’s own local time (not today’s upload date, and not a date in the middle of the location’s photos). This is computed entirely from Immich’s own naive local-wall-clock timestamp for that photo, deliberately avoiding any UTC conversion round-trip — mixing UTC math with wall-clock EXIF writing is what previously caused a slide’s timestamp to silently drift by the photo’s own UTC offset (e.g. landing 9 hours off for a JST photo) instead of landing exactly one minute earlier. In a chronological slideshow or timeline view, the slide now reliably appears immediately before the photos it’s introducing.
When the slide’s background is an AI-generated image, every local save (both the standalone “Download” button and the automatic safety-net copy that comes with “Add to Immich gallery”) also saves the raw generated image as its own separate file ({name}_background.png alongside {name}.png), so the actual generated artwork is preserved on its own, not only ever baked together with the text overlay. Immich itself only ever receives the one composited slide — the clean background variant is a local-only extra, never uploaded. This doesn’t apply when the slide uses a grid of your own real photos, since those already exist untouched in your Immich library.
“🎨 Generate AI image from selected photos” sends all currently-selected photos as input, with an optional free-text style field (e.g. “watercolor” or “moody cinematic”) — leave it blank for a natural photographic enhance. The last style you use is remembered and pre-fills as the starting point for any other location you haven’t customized yet, saved locally so it persists across reloads — while still fully overridable or clearable per location. The generation prompt also asks the model to bake a small, stylized typographic tag of the location name into the top-left corner of the image itself, styled like an authentic local artist’s signage rather than a plain rendered font — so when the AI image is in use, the slide’s plain text headline is automatically skipped in favor of that in-image tag (click the photo strip’s AI-image badge to toggle back to the original photos and the plain headline).
The generated image itself is saved locally (as base64, since it’s the result of an actual paid API call) and restored the next time you open Location Stories for that album — so it isn’t lost to a reload the same way the generated text and custom names aren’t. This is also why the extension requests the unlimitedStorage permission — a handful of generated images can add up past the browser’s default 5MB extension-storage quota.
The generation prompt explicitly requests the image be composed for the slide’s current aspect ratio (16:9/4:3/1:1), rather than leaving the model to produce whatever proportions it defaults to and having it cropped to fit — a mismatch there is exactly what would risk cutting off the top-left tag. As a fallback for the cases a model doesn’t hit the requested ratio exactly, the AI-generated background specifically (not regular photo grids, which keep a normal centered crop) is cropped with a slight top-left bias rather than centered, so the tag corner stays the one most likely to survive.
Press D (or “✨ AI Describe…”) on an open photo for an AI-suggested description of what it shows, using Immich’s own description caption field — not a separate note stored by the extension. The request sends the photo at Immich’s preview size (not thumbnail, for better recognition accuracy) plus location context (city/country from the photo’s own EXIF) plus whatever description already exists for that photo, so the AI builds on existing information rather than starting blind or overwriting it. The suggestion is shown for review and editing before anything is saved — clicking “Save to Immich” writes it via Immich’s own asset-update endpoint, immediately visible anywhere else Immich shows that photo’s description.
The prompt is customizable in Options → AI (Location Stories), separately from the Location Stories templates — it’s a different feature with different available tokens ({location_context} and {existing_description}, filled in automatically rather than needing manual substitution).
After your first successful download and your first successful AI-generated story, a small dismissible banner appears once — a quick “built by byphil.eu” mention with a link — then never shows again for that milestone. It only ever appears after something has already worked, never mid-task and never repeatedly; closing it (or just ignoring it, it fades on its own) is remembered the same way everything else here is.
Location names, coordinates, dates, and — only if you explicitly enable it — small photo thumbnails, get sent to whichever AI provider you configure (OpenRouter or Google Gemini directly for text; OpenRouter only for image generation), using an API key you supply in Options → AI (Location Stories). This is opt-in end to end: the feature makes zero network calls to any AI provider until you set an API key, and photo-sending specifically requires a separate checkbox even after that. Set up your provider, model (default google/gemini-3.7-flash via OpenRouter), and API key there, and use “Test connection” to confirm it works before relying on it.
thumbnail/preview/fullsize downloads are server-generated derivatives (re-encoded, resized JPEG/WebP for fast web display), and whether Immich’s image pipeline carries the full EXIF block — especially GPS coordinates — into those derivatives isn’t guaranteed by its API. Only original downloads are byte-identical to the uploaded file, so that’s the one to use if you need location/EXIF data intact in the file itself.
That said: Immich’s asset-info API exposes GPS coordinates, capture date, and camera make/model as structured metadata regardless of what’s in the derivative image, so the extension writes that data into JPEG preview/thumbnail downloads itself (Options → GPS / EXIF metadata, on by default) rather than relying on Immich’s thumbnail pipeline to preserve it. This is scoped narrowly:
original downloads — they already have real EXIF, and not applied to Copy actions, since Copy always re-encodes to PNG for the clipboard anyway, which strips all metadata regardless of what was in the source JPEG.If a download fails to get EXIF-embedded for any reason, it silently falls back to a normal, unmodified download rather than failing outright.
Built and verified against Immich 3.0 and 3.1, and designed to also work with 2.x:
GET /api/assets/{id}/thumbnail?size=thumbnail|preview|fullsize and GET /api/assets/{id}/original endpoints for image data — these were not part of the v3.0 breaking-change set.x-api-key header auth, unchanged across v2 → v3 (only internal OpenAPI schema names were renamed in v3, not the header).navigator.clipboard only exists in secure contexts, so it’s unavailable on a plain-HTTP LAN Immich server (e.g. http://192.168.1.50:2283) if called from the page itself; (2) the Clipboard API also requires the calling document to be focused, and an offscreen document can never be focused or painted — an earlier version of this extension used execCommand('copy') there instead, which avoids the focus check but has nothing real to rasterize into the clipboard, producing an empty/1×1px image. A popup window is a real, paintable, focusable page, so it’s the one context where a programmatic image copy works reliably regardless of the Immich server’s protocol. You’ll see a brief window flash each time you copy — that’s expected.{statusCode, error, message}) and the v3 shape (which dropped statusCode/error and kept message).GET /api/server/version, which is unaffected by the v3 removal of /api/server/theme.POST /api/search/metadata endpoint, not GET /api/albums/{id}. Immich’s own v3 migration notes state the assets field was removed from the album-info response in v3 specifically because of this; relying on it (as an earlier version of this extension did) silently returns zero assets against a v3 server. search/metadata works against both v2 and v3.When you add a server, the options page pings it and shows the detected version; if it’s outside the 2.x/3.x range you’ll see an “(untested)” badge rather than a hard failure.
Open an album in Immich, press Shift+E (or use the popup button). You’ll get either:
depending on the “Bulk export → Export format” setting in options.
The ZIP is built client-side with no compression (images are already JPEG/WEBP, so re-compressing wouldn’t help) and no third-party dependencies.
chrome.storage.local). Immich API keys are only ever sent to the Immich server they belong to; the AI provider API key is only ever sent to that provider.unlimitedStorage permission only affects local browser storage quota — it does not grant any additional network access or change what data leaves your browser. It exists because generated slide text, images, and location names are all saved locally so they survive a reload, and a handful of AI-generated images (stored as base64) can add up past the browser’s default 5MB per-extension storage limit.