<

Anne Mariel

/>

Full Stack Developer

<- Back to blog
3 min readAnne Mariel Catapang

What it actually took to add gallery upload: four decisions in one line of code

Adding gallery upload to DoodleTrove sounded like deleting one HTML attribute. It was four decisions about not quietly corrupting years of archive.

  • nextjs
  • typescript
  • side-projects
  • product
What it actually took to add gallery upload: four decisions in one line of code

DoodleTrove's capture feature got built first. The camera flow is good: open the app, hit the big square, the rear camera is live, then photograph the drawing in front of you. However, "photograph it right now" was never meant to be the only way in. There's a whole camera roll of older drawings, scans, photos other people send.

So I went to add the obvious missing piece: upload from the gallery.

It sounds like deleting one HTML attribute. The big square carries capture="environment", which tells the browser skip the picker, open the camera. Remove it and the OS shows its normal picker โ€” camera, gallery, and files. One line. Ship it.

It wasn't. That one sentence hid four decisions, and three of them were about not quietly corrupting the archive I'm keeping for years.

1. Add a path, don't replace one.

The temptation is to delete capture and let the OS picker mediate everything. That's one line, but it taxes the path that already works. Every upload, including the camera, now stops on an OS sheet to pick its source, mid-gesture, on a small target. The camera path is the time-sensitive one (you're standing over a drawing); the gallery path is the calmer one (you're sitting down with the phone). They aren't the same job, so they don't share one input.

Two buttons: the big square stays the camera, a smaller "Choose from gallery" sits below it. The camera flow keeps its speed.

2. The date that wasn't there

The form reads the date from the photo's EXIF. Camera photos always have it. Gallery photos (a forwarded image, a scan, and a screenshot) often don't.

The old code silently fell back to today.

That's the bug. A drawing your partner photographed six months ago, texted to you, uploaded today, gets logged as made today. Forever. In an archive I intend to keep for decades, a silent wrong date is worse than no date.

const taken = exif?.DateTimeOriginal ?? exif?.CreateDate;
if (taken instanceof Date && !Number.isNaN(taken.getTime())) {
  setDateMade(toLocalDate(taken));
  return;
}
// No EXIF. Don't default to today โ€” make the user confirm.
setDateMade("");
setNeedsConfirming(true);

3. The orphan

The save is two steps: create the artwork record, then upload the photo. If step two fails, step one leaves a record with no photo โ€” an empty grey square in the gallery, because every tile assumes photos[0] exists.

Gallery uploads raise the odds of that failure: HEIC, forwarded files, big images. The lazy fix is to live with it. The right fix is to reorder: compress and decode-validate the image before creating the record, so an undecodable photo fails before anything is saved. The rare case where the network drops after create rolls the record back. The gallery never sees a photoless tile.

4. HEIC, or: don't optimize for a browser you don't use

iPhones shoot HEIC. The compression library draws to canvas, so whether HEIC decodes is the browser's call. On the installed iOS PWA โ€” the only place this app runs โ€” it almost certainly works. So I didn't build a server-side fallback.

I shipped the clean failure ("couldn't process this photo, try a JPEG") along with a note to self: try a real HEIC upload on the phone, and only reach for sharp if it actually breaks. Don't pre-optimize for a failure that probably doesn't happen for your users.

The feature is the decisions

"Add gallery upload" is one sentence. It contained four decisions, and three of them โ€” the date, the orphan, the HEIC failure โ€” were about not quietly lying to a future version of oneself about their own kids' artworks.

That's the same lesson as the first time around. The bugs that get you aren't the ones that crash. They're the ones that look plausible. A photo dated today. A grey square you learn to ignore. A wrong record nobody checks.

capture really is one attribute. The feature isn't the code. The feature is the decisions, and the decisions are where the integrity lives.

The drawings are safe. Now so are the ones that were already on the camera roll.