Lightroom in the browser. Free. Offline. No login. No nag.
Svelte 5 · Vite · imgstry · bits-ui · works on a plane
You took 600 shots on vacation. You're on a 9-hour flight back. Your laptop has 2% battery anxiety, no Wi‑Fi, and your subscription editor wants to phone home before it'll let you push a slider.
imgstry studio is a single-page web app you install once (PWA) and then run
forever, offline, with zero accounts, zero telemetry, zero cloud round-trips.
Drop a photo in, drag sliders, export. Pixels never leave your device.
Develop module - Snapseed-style adjust strip on mobile, Lightroom-style panels on desktop. Same engine on both.
- Light - exposure (stops), contrast, highlights, shadows, whites, blacks
- Color - temperature, tint, vibrance, saturation, hue
- Presence - texture, clarity (luminance unsharp), sharpen, denoise
- Tone Curve - drag-and-drop spline, per-channel (RGB / R / G / B)
- Color Grading - split tone with shadow + highlight tint
- Effects - radial vignette, film grain
- Creative - sepia, B&W, invert, color overlay (RGB / HSV / HEX / CMYK pickers)
- Presets - 11 curated looks across Cinematic / Vintage / Modern / B&W
- History - snapshot timeline with one-tap restore + dedupe
Editor mechanics
- Hold to compare with original (
\\keyboard shortcut) - Pinch / wheel zoom + drag pan with a floating zoom badge
- Live RGB histogram (real-time, computed by the engine cache)
- Dark + light theme, persisted to localStorage
- PNG / JPEG export with one tap
Formats
- Standard: JPG, PNG, WEBP, AVIF, GIF, BMP
Camera RAW
A top-bar badge tells you which path the current file took:
RAW · 14-bit sensor(green) - decoded the actual sensor data. Exposure pulls bring back highlight headroom that an 8-bit JPEG would have clipped.RAW · JPEG preview(orange) - sensor decode unsupported for this file; editing the camera's baked JPEG. No extra headroom.
Sensor path supports RGGB Bayer cameras with TIFF / TIFF-EP / DNG containers, 8-16 bit precision, uncompressed strips or lossless-JPEG (Compression=7, T.81 Annex H, predictor 1):
DNG(Adobe converter, native DNG cameras): full sensor path.CR2(Canon, all bodies through EOS R-series): full sensor path.NEF,NRW(Nikon): full sensor path.ARW,SR2(Sony, uncompressed + standard lossless): full sensor. Sony's lossy-compressed ARW (cRAW) falls back to JPEG preview.ORF(Olympus),PEF(Pentax),RW2(Panasonic): full sensor for the LJPEG-compressed bodies; uncompressed fallback otherwise.
Preview-only (today):
CR3(Canon) - HEIF-wrapped sensor data.RAF(Fuji X-Trans) - 6x6 CFA pattern, not yet demosaiced.X3F(Sigma Foveon) - layered sensor, not Bayer.
Recognised RAW extensions: CR2, CR3, NEF, NRW, ARW, SR2, DNG, ORF, RW2, PEF, RAF, X3F, 3FR, CRW, MRW, DCR, KDC, MEF, MOS, ERF.
Plane mode
- Installable PWA: home-screen icon, standalone window, no browser chrome
- Workbox precache (~300 KiB) - first load locks the shell, subsequent loads work fully offline
- Offline toast tells the user what's going on
- Service worker self-updates with an inline "Reload" prompt
→ https://visual-cortex.github.io/imgstry-ui/
On mobile, hit the share sheet → Add to Home Screen once and it'll behave like a native app from then on.
src/
App.svelte # studio shell, responsive layout
app.css # token consumer + utility classes
main.ts # entry; boots theme + PWA singletons
lib/
components/ # Svelte UI surface
editor/
editor.svelte.ts # single source of truth, drives imgstry
adjustments.ts # typed adjustment shape + defaults
color.ts # color-space conversion helpers
presets.ts # named looks + group hues
mobile.svelte.ts # mobile pane / active adjustment state
theme.svelte.ts # dark/light + persistence
pwa.svelte.ts # SW lifecycle + online + install state
styles/
palette/ # color ramps (shade + blue/purple/orange/...)
numeric-increments/ # --ni-N spacing tokens
sizing/ # radius + border + space semantics
typography/ # font scale + weights + tracking
transitions/ # durations + easings
layers/ # z-index slots
theme/ # global tokens + dark.css + light.css
public/
favicon.svg # source mark
pwa-*.png # generated app icons
scripts/
smoke-*.mjs # Playwright behavior probes
The UI is presentational. Components describe intent on editor.adjustments;
the editor object translates intent into imgstry
pipeline operations on a debounced render tick.
The UI consumes the engine straight from a sibling checkout via a Vite alias. Clone both repos next to each other:
git clone https://github.com/visual-cortex/imgstry.git
git clone https://github.com/visual-cortex/imgstry-ui.git
cd imgstry && npm install
cd ../imgstry-ui && npm install
npm run dev # http://localhost:5173You'll need:
- Node
>= 22 - npm
>= 10 - A Chromium-based browser for smoke tests
| Command | What it does |
|---|---|
npm run dev |
Vite dev server with HMR |
npm run build |
Production build → dist/ |
npm run preview |
Serve the production build locally |
npm run check |
Type + Svelte check |
npm run lint |
svelte-check in lint mode |
npm run pwa:icons |
Regenerate PWA icons from public/favicon.svg |
node scripts/smoke-develop.mjs |
Desktop develop module flow |
node scripts/smoke-mobile.mjs |
iPhone 14 Pro viewport flow |
node scripts/smoke-color.mjs |
Color picker mode-switch flow |
node scripts/smoke-raw.mjs |
RAW (.nef) embedded-JPEG fallback flow |
node scripts/smoke-raw-full.mjs |
Synthetic DNG sensor decode + exposure rebake |
Smoke tests assume a dev server on :5199. Launch one with:
npx vite --port 5199 --strictPortPull requests are welcome - small, surgical, focused. The vibe is "ship a thing that delights".
- Fork → branch → commit → push → PR. Branch from
master. - Run
npm run checkand the smoke scripts before pushing. - Use Conventional Commits for the
subject line:
feat(viewport): …,fix(curve): …,chore(deps): …. Breaking changes use the!marker. - Squash trivial fixups before review.
Conventions live in .agents/rules/ - they're the single
source of truth (the code agents read them, humans should too):
project.md- tech stack, structure, commit standards.code-principles.md- pure functions, immutability, type safety, etc.implementation.md- naming, Svelte 5 specifics, engine boundary.testing.md- smoke philosophy.components.md/editor.md- domain rules per subsystem.
Highlights:
- Svelte 5 runes (
$state,$derived,$effect). Noexport let, no legacy stores. - TypeScript strict, no
any, narrow at boundaries. - The UI never imports pixel-level helpers from the engine - call the public
surface (
Imgstry,CubicSpline,GaussianBlur, …). - Components are presentational. Mutations live on
editor.svelte.ts. - Reach for an existing token (
--color-bg,--space-md,--ni-12) before introducing a bespoke value.
Most adjustments follow the same shape:
- Add the typed key to
Adjustments(src/lib/editor/adjustments.ts) + a default value. - Wire it in the render queue inside
_queueOperationsofeditor.svelte.tsso the engine consumes it. - Drop an
<AdjustmentSlider>in the right Panel ofRightRail.svelteplus aMobileSliderSpecentry inmobile.svelte.tsif it should surface on mobile. - Update or add a smoke script if the user-facing behaviour is novel.
Append to PRESETS in src/lib/editor/presets.ts. The chosen group
controls the swatch color via GROUP_HUE. Both the desktop rail and the
mobile Tools sheet read from the same array, so a single entry shows up
in both places.
Found a bug? Open one. Include the browser + OS, the steps, and a screenshot. If you have a stack trace from the dev tools, even better.
MIT - go wild. If you ship something built on top, a backlink in your About page is appreciated but not required.
- vlad & friends, somewhere over the Atlantic