โ† Kevin Yoder
Digital Dentistry

PhotoDrop

Scan a QR on the clinic screen, tap-select phone photos, and they land in a local folder the imaging software can import โ€” no cables, no cloud, photos never leave the practice LAN.

Built Jan 2026 Offline ยท token-gated Verified working

โš™ How it works ๐Ÿ–ผ Screenshots

01 Overview

PhotoDrop is a small utility that solves an everyday clinic annoyance: getting a good intraoral or clinical photo off a cell phone and onto the operatory computer's imaging system without cables, an email round-trip, or a cloud account. Run one Python file on the PC โ€” it starts a local web server and opens a dashboard showing a QR code. Scan it from a phone on the same WiFi, tap-select photos, and they upload straight into a folder the imaging software watches. It also doubles as the intake front door for the SmileVision and dental-narrative apps.

The whole thing is a single Python file: the entire responsive web UI โ€” both desktop and mobile modes โ€” is embedded as one string in the source and renders with no internet, its one small dependency (segno) generating the pairing QR locally instead of calling a web service. A per-session access token rides inside that QR, so only a device that scanned this screen can reach the gallery or upload.

On the PC

Desktop dashboard

A QR pairing card, a photos-received counter with the save path, and a live gallery that re-renders as new photos arrive.

On the phone

Mobile uploader

The same page in its mobile branch: a tap-to-select dropzone, multi-file upload with per-file progress, and a "sent N photos" confirmation.

02 Why I built it

In practice, the phone is often the best camera in the room โ€” but there's no friction-free way to move a photo from it onto the operatory PC's imaging system. Cables get lost, email adds a cloud hop and a compression step, and installed transfer apps are one more thing to manage on shared clinic hardware. I wanted the shortest possible path: point the phone at a QR on the screen, pick the photos, and have them appear in a folder the X-ray software can already import. Keeping it a single self-contained file was part of the point โ€” it should be close to double-click-and-go on any clinic machine, work with no internet, and the images should never leave the local network.

03 What I built & how it works

One process, one file: an HTTP server on the PC and a single page that renders two different UIs โ€” one per device.

Phone on the clinic WiFi โ€” same LAN, no cloud Phone โ€” mobile uploader same page in mobile mode ยท tap-select photos POST /api/upload ยท raw file bytes PC โ€” HTTP server ยท 0.0.0.0:8000 serves both UIs by ?mode= ยท GET /api/list poll ~2s POST /api/upload?name=โ€ฆ receives the raw bytes writes to disk dropped_photos/<name> basename sanitize ยท timestamp suffix on collision imaging software imports from the watched folder

Fig. 1 โ€” the desktop and mobile experiences are the same page, which reads a ?mode= query parameter to render the right UI on each device. The upload sends the raw file body with the name in the query string, sidestepping multipart parsing.

  1. Start โ€” run the single file; it autodetects the LAN IP (a connectionless UDP-route trick, no traffic sent), binds the server, and auto-opens the desktop dashboard with a QR of its own URL.
  2. Pair โ€” scan the QR on the clinic screen; the phone loads the same page's mobile branch on the same LAN.
  3. Select โ€” tap-pick one or more photos; each POSTs its raw bytes with the filename in the query string.
  4. Land โ€” the PC writes them into dropped_photos/, sanitizing the name and suffixing a timestamp on collisions.
  5. Import โ€” the desktop gallery re-renders within about two seconds, and the imaging software picks the files up from the folder.

04 ๐Ÿ›  Skills & tech used

Languages
Python 3 (stdlib + segno)Vanilla JSHTML / CSS
Infra
LAN HTTP service (0.0.0.0)LAN-IP autodetect (UDP-route trick)custom GET/POST router over SimpleHTTPRequestHandler
Frontend
single-string embedded UIinline hand-written CSS (no CDN)emoji icons ยท system fontsresponsive desktop + mobile modeslocally-generated QR (segno)poll-based live gallery
Data / protocol
JSON list APIraw-binary upload protocolfilename sanitization + collision timestamping
Security
per-session access token (403 gate)constant-time token compareoptional self-signed TLS (--https)fully offline ยท no third-party calls
Techniques
server-side template injectiondual-mode page via query parampoll-based near-real-time syncauto-open browser + clean shutdown

05 Notable challenges & decisions

Protocol

Raw bytes instead of multipart

The stdlib http.server has no multipart parser, so rather than pull one in, the client POSTs the raw file body with the filename in the query string. The server writes the bytes straight to disk after an os.path.basename sanitize and a timestamp suffix on name collisions โ€” a deliberately small protocol that kept file handling on the standard library.

One-file frontend

Three templating syntaxes in one string

The whole UI lives as one embedded string, which is convenient until placeholders collide. The one recorded bugfix: a ${UPLOAD_DIR} token looked like a JavaScript template literal but sat inside a Python string served verbatim, so it rendered literally. The fix was a {{UPLOAD_DIR}} placeholder injected server-side with .replace() โ€” a reminder that Python, JS, and mustache-style tokens all coexist inside that one string.

Scope

A single blocking server, on purpose

It uses one blocking TCP server with no threading โ€” fine for a single uploader on a LAN, where large simultaneous uploads would simply serialize. Keeping it single-threaded kept the whole tool readable in one file.

Hardened for clinical use. The first pass leaned on the open internet โ€” it pulled its UI (CSS, icons, a font) and even the pairing QR from third-party CDNs, and ran over plain HTTP with no authentication, so anyone on the network could upload or browse the received folder. Fine for a personal utility, not for anything near patient data. The shipped version closes all of that: the UI is inlined and the QR is generated locally, so it makes no third-party calls and works with no internet; a per-session access token gates every page and data request; and a built-in --https mode serves it over TLS. Same tool and the same flow โ€” just self-contained and access-controlled.

06 Results

1
Python file โ€” the entire app
0
third-party requests โ€” the UI is fully offline
334
lines of code
9
photos moved in one verified session
~2s
desktop gallery refresh interval

Sources: the single photodrop_secure.py file and its on-disk dropped_photos/ folder.

07 Screenshots

PhotoDrop is a LAN tool by design: open it on the clinic PC, scan the QR with a phone on the same Wi-Fi, and the photos you pick land in the desktop gallery within about two seconds. The captures below are from the shipped, self-contained version โ€” the desktop dashboard just after a few clinical photos have arrived, and the phone's uploader.

The PhotoDrop desktop dashboard: a Connect Device card with a locally-generated QR code, a photos-received counter with the save path, and a gallery showing intraoral and extraoral clinical photos that just arrived from a phone.
The PC dashboard: the QR pairing card, the save path and received count, and the gallery โ€” here showing a short series of clinical photos that just landed from a phone.
The PhotoDrop mobile uploader on a phone: a Connected header and a large Tap to Select Photos dropzone.
The phone's mobile branch: a tap-to-select dropzone that sends photos straight to the desktop gallery.

08 Honest status

PhotoDrop was built with an LLM in January 2026 and verified working end to end โ€” the server starts, serves the desktop and mobile pages, requires the access token on every page and data request, and accepts, lists, and serves an upload back. It is a single-file personal utility, not a deployed product: no git history, and it has only been exercised on a couple of machines. The offline UI, locally-generated QR, per-session access token, and optional self-signed TLS cover the basics for a trusted operatory LAN โ€” but the TLS is self-signed rather than CA-trusted, so a fully warning-free, audited clinical deployment would still want a proper certificate and a security review. As it stands, it does one small thing well: getting phone photos onto the operatory PC with as little friction as possible, without them ever leaving the local network.

PhotoDrop โ€” Run Guide

A zero-cable LAN photo-transfer tool. Run it on the computer you want photos on; it shows a QR code; a phone on the same Wi-Fi scans it and taps to upload photos, which land in a local folder and appear in a live desktop gallery. It is self-contained and access-controlled: the interface is inlined (no CDN), the QR is generated locally, and a per-session token gates every page and data request.

Clinical use case: capture high-quality intraoral/clinical photos on a phone and get them onto the operatory computer's imaging folder with no cables, no email, no app โ€” the photos land in a local directory the imaging software can import, and nothing leaves the local network.

Status: โœ… Verified โ€” the whole server-side flow was exercised end-to-end: the server starts, serves the desktop and mobile pages, refuses page and data requests without the access token (403), accepts a raw-binary upload (POST /api/upload โ†’ Success), lists it (/api/list), and serves the image back (HTTP 200) โ€” with a locally-generated QR and zero CDN refs confirmed. The one part that can't be automated is the physical phone QR-scan โ€” that's a manual check.

Sensitive data: none. No secrets, no accounts, no config. Uploaded photos land in a local ./dropped_photos/ next to the script.


What it is

A single ~330-line Python file (photodrop_secure.py) โ€” Python standard library plus one small pure-Python dependency, segno, used to generate the pairing QR locally. The entire responsive web UI (desktop + mobile) is embedded as a string, with hand-written CSS and emoji icons, so it renders with no internet. It auto-detects the machine's LAN IP, serves on port 8000, and opens the desktop view in your browser. Every request carries a per-session access token (it rides inside the QR); a request without it gets a 403.

Prerequisites

Python 3.8+ and one small package:

pip install segno

(New machine? Install Python per ../SETUP.md.) No virtualenv or Docker needed.

Run it โ€” this is the whole recipe

python photodrop_secure.py            # add --https for TLS (see Encryption below)

It prints the Desktop URL (which carries the access token), opens your browser to it, and shows a QR code. On your phone (same Wi-Fi): scan the QR โ†’ Tap to Select Photos โ†’ they upload and appear in the desktop gallery, saved to ./dropped_photos/. Ctrl-C to stop.

  • Pin a fixed token instead of a random per-session one: PHOTODROP_TOKEN=some-token python photodrop_secure.py.
  • Change the port with --port 9000; change the save folder by editing UPLOAD_DIR at the top of the file.

Why there's no prebuilt-image / Docker tier

Deliberately omitted โ€” it would add friction for zero benefit ("if it isn't needed, don't include it"). PhotoDrop must present the host's LAN IP and be reachable from phones on the LAN, so a container would need host networking and still gain nothing over python photodrop_secure.py. One file, one command is the distribution.

Verify

pip install segno
PHOTODROP_TOKEN=testtok python photodrop_secure.py &                        # start it; token pinned for this check
curl -s "http://localhost:8000/?token=testtok" | grep PhotoDrop             # desktop page
curl -s "http://localhost:8000/?mode=mobile&token=testtok" | grep "Tap to"  # mobile upload page
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/             # โ†’ 403 (no token = denied)
curl -s -X POST "http://localhost:8000/api/upload?name=t.png&token=testtok" --data-binary @some.png   # โ†’ Success
curl -s "http://localhost:8000/api/list?token=testtok"                      # โ†’ the file appears

Then the real acceptance test: open the desktop page, scan the QR with a phone, and upload a photo.

Encryption โ€” --https (built in)

The token blocks other devices, but plain HTTP means someone sniffing the same Wi-Fi could still capture the token/photos. There is a built-in self-signed TLS mode:

pip install cryptography              # (or just have `openssl` on PATH) โ€” used to generate the cert
python photodrop_secure.py --https    # auto-creates cert.pem/key.pem, serves over https://

On first run it generates a self-signed cert for your LAN IP (reused afterward), wraps the socket with stdlib ssl, and the QR switches to https://. Real encryption โ€” the one cost is a one-time "not private" warning on the phone (self-signed isn't CA-trusted); tap through. Bring your own cert with --cert / --key.

Warning-free alternative: use mkcert / your own CA to issue a cert for the LAN IP and install the CA root on each device once โ€” green-lock HTTPS, no warnings, but per-device setup. A public CA (Let's Encrypt) is impractical for a bare LAN IP (needs a public domain name).

Caveats & clinical notes

  • Plain HTTP unless you pass --https. The access token stops other devices on the Wi-Fi from viewing or uploading, but without TLS a determined sniffer on the same network could still capture traffic. Use --https for anything sensitive.
  • Self-signed TLS is real encryption but not CA-trusted โ€” expect a one-time "not private" warning on the phone (or install your own CA root; see above).
  • Single-machine, single-uploader scope. One blocking server; it's a personal utility, not an audited product.
  • Port 8000 must be free (--port to change).

Earlier version

The repo also keeps photodrop.py โ€” the original quick pass. Same tool, same flow, but it loaded its UI (Tailwind, icons, a font) and the QR image from CDNs (so it needed internet and sent the LAN URL to a third party) and ran open over HTTP with no token. photodrop_secure.py supersedes it; run the secure one โ€” photodrop.py is kept only for reference.

Provenance

No datasets, no model, no training, no external accounts, no secrets โ€” a self-contained stdlib script plus one small pure-python dependency (segno, for the offline QR). Built with an LLM, Jan 2026.