Skip to content

Repository files navigation

Photo Lab

Privacy-focused image tools that run entirely in your browser. Resize, compress, convert, and strip metadata with Pillow running through Pyodide. No server, no upload; images never leave your device.

PyPI Version License: MIT


What this is

Photo Lab is a static Wybthon application: Python components running client-side through Pyodide, deployed straight from the main branch via GitHub Pages. Images are read with the browser's File API and processed in memory by Pillow compiled to WebAssembly. Nothing is uploaded, no analytics run, and closing the tab erases everything.

This is a starter, not a finished product. The skeleton works end to end (boot, lazy package install, drag-and-drop, live re-encoding, before/after comparison, download), and the interesting features are left to build. If you want to contribute to a Wybthon project, this is meant to be an easy place to start; see the roadmap below.

What works today

  • Load an image: drag and drop, browse, or generate a sample image in memory (so the repo ships no binaries).
  • Reactive controls: output format (JPEG, PNG, WebP), a quality slider, and a max-dimension field; an effect re-encodes the image live as you adjust them.
  • Before and after: side-by-side previews with dimensions, encoded size, and a size-savings badge.
  • Metadata removal: the output is re-encoded from pixels, so EXIF (camera, GPS, timestamps) is never copied over.
  • Download: the processed image downloads straight from a data URL; no network involved.
  • Lazy package loading: the page boots fast; Pillow and NumPy install via micropip only when the studio first mounts, behind a Loading boundary. A corrupt file raises into an ErrorBoundary instead of crashing the page.

Quick start

# Install Python (from .python-version) and all dependency groups
uv sync --all-groups

# Serve the static site
make serve            # → http://localhost:8000

The page needs nothing but a static file server; Pyodide, Wybthon, and the imaging stack are fetched at runtime from CDNs using the versions pinned in config.json and app/packages.py.

Project layout

photo-lab/
├── index.html               # Page shell, metadata, loading overlay
├── bootstrap.js             # Pyodide + micropip + manifest-driven module loader
├── config.json              # Pinned Pyodide + Wybthon versions
├── assets/
│   └── styles.css           # Deliberately minimal theme; make it yours
├── app/
│   ├── main.py              # async main(), called by bootstrap.js
│   ├── app.py               # Shell: header, boundaries, privacy note, footer
│   ├── packages.py          # Lazy micropip install of the imaging stack
│   ├── images.py            # Pure Pillow/NumPy helpers (unit-tested)
│   └── studio.py            # The studio: dropzone, controls, previews, download
├── tools/build_manifest.py  # Regenerates manifest + cache busters
└── tests/                   # Unit tests + opt-in browser smoke test

How dependencies work

There are two layers, and they're intentionally separate:

  1. In the browser (what users run): bootstrap.js installs the pinned wybthon wheel at boot, and app/packages.py installs Pillow and numpy via micropip the first time the studio mounts. Add packages there as features need them.
  2. Locally (what contributors run): the same packages are declared in pyproject.toml so Ruff, MyPy, your editor, and the unit tests resolve what the browser installs. After changing them, run uv lock and commit uv.lock.

Everyday development

Components use the @component decorator; bodies run once at mount, and zero-argument callables embedded in the tree become fine-grained reactive holes. Start with app/studio.py for the UI and app/images.py for processing; keep image logic in images.py so it stays unit-testable without a browser.

Whenever you add, rename, remove, or edit files under app/ (or touch bootstrap.js / assets/styles.css), regenerate the manifest and commit the result:

make manifest         # uv run python tools/build_manifest.py

Validate before pushing:

make ci               # lint, typecheck, unit tests, manifest freshness
make e2e              # optional: boots the real site in headless Chromium

See CONTRIBUTING.md for commit and branch conventions.

Roadmap

Ideas that fit the project's goal (fully private, in-browser image processing), roughly ordered by difficulty. Pick one and open an issue to claim it:

  • Metadata inspector: show the EXIF the original carries (Pillow's getexif()) before it gets stripped, so users see exactly what's being removed.
  • Debounced re-encoding: dragging the quality slider re-encodes on every tick; debounce it, or move encoding into a Web Worker.
  • Rotate and flip: simple orientation controls, including applying the EXIF orientation tag.
  • Target file size: binary-search the quality setting to hit a size like "under 500 KB".
  • Batch processing: accept multiple files, process them with shared settings in a keyed list, and download results (individually, or zipped via zipfile).
  • HEIF/HEIC input: install pillow-heif lazily (matching the app/packages.py pattern) so iPhone photos open directly.
  • Crop: a draggable crop rectangle over the preview.
  • OpenCV extras: optional filters (sharpen, denoise, edge detection) behind a lazily installed opencv-python.
  • Zoomable previews: click to compare at 100% with a synced pan between before and after.

Deploying with GitHub Pages

This repository deploys directly from the main branch; merging is the deployment. Go to Settings → Pages, set Source to Deploy from a branch, and choose main and / (root). The site appears at https://<user>.github.io/photo-lab/ within a minute or two. .nojekyll is required and already committed.

Credits

Built from the Wybthon demo template.

License

MIT © 2026 Owen Carey

About

Resize, compress, convert, and strip image metadata privately in your browser with Wybthon and Pyodide.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages

Generated from wybthon/demo-template