Commit Graph

286 Commits

Author SHA1 Message Date
3dtours 3edafcfe1e library: open a phone's HEIC, and lock the shelves to accounts
A HEIC is the one camera file no engine on this platform will paint:
Chromium answers createImageBitmap with InvalidStateError and its
WebCodecs take no hvc1 still, and unlike a RAW there is no JPEG preview
inside to lift. It is decoded in software instead, by libheif built to
wasm, in a worker of its own — so a folder of them reads without the tab
stopping for a second, and what leaves the worker is the same JPEG
developRaw hands the studio. Nothing downstream knows the difference.

Into the studio it goes on the same terms as every other frame: dropped,
picked, or opened from the catalogue, no tier asked and the engine's own
colour untouched. The frame's date, ISO and position come off the HEIC
itself, which is passed along as the bytes the stamps read.

The catalogue's tile does not pay the decode: a phone writes a small
HEVC copy of its frame as an item of its own (thmb, 320x240 on an
iPhone), reachable only by walking the container by hand, and it decodes
in milliseconds where the frame takes a second — from the same 256KB the
catalogue already reads for a header.

The shelves themselves now want an account: a guest gets the note and
the way in, and the scan never starts.
2026-10-06 20:59:40 +07:00
3dtours 9fc03e28f7 assets: eight more mark faces from google/fonts
VT323, Alex Brush, Tilt Warp, Hahmlet, Pinyon Script, Allura, Dancing
Script and Amatic SC join the two the bundle already carried, each
subset to the same latin / latin-ext / vietnamese ranges and pinned to
its upright 400 instance where it is variable.
2026-10-06 20:59:34 +07:00
3dtours 96b4dcce5b chore(docker): rebuild the NAS image bundle on 66b159b 2026-10-06 18:23:24 +07:00
3dtours 66b159b3bd library: centre the quarter-turn pair on the stage bar
The bar under the picture is the grid it needs to be: equal free tracks either
side of the two buttons land them on the picture's own centre whatever OPEN and
the stars measure, and the pair keeps the ends it does not own. Under 700px the
stage is too narrow for one line, so the bar wraps as it did and the lines it
wraps to are centred.
2026-10-06 18:22:40 +07:00
3dtours 0df62b6248 chore(docker): rebuild the NAS image bundle on 2e06e5a
The frontend image carries the library's quarter turn (catalogue `rot`, the
two chips under the preview, the wall and strip tiles, and the studio's
write-back), stamped 2e06e5a at the foot of the rail.
2026-10-06 17:24:02 +07:00
3dtours 2e06e5ab5d library: stand a frame up in the catalogue, wall, strip and studio
The library stage carried no way to correct a frame shot on its side, and
nothing kept the correction. A quarter turn now rides on the catalogue row
(`rot`, beside `star`, carried over by a rescan), the two chips under the
preview step it, and the tile, the strip thumbnail and the stage all read it
through one bake so a frame is never stood on its side in one place and
upright in another. The studio opens a frame at that angle and a turn made
there is filed straight back onto the row, so either end of the turn is
what the frame comes up at next time.
2026-10-06 17:22:56 +07:00
3dtours 651e0ad09a chore(docker): rebuild the NAS image bundle on 8ed094c 2026-10-06 16:24:46 +07:00
3dtours 8ed094cd9e fix(raw): stop the develop from handing the studio zero bytes
libraw-wasm moves the buffer it is given into its worker, so raw.open() detaches
the caller's array. developRaw read its embedded JPEG preview as a view of that
same array, so the open emptied the preview, Skia rejected the detached buffer
and the catch returned the emptied preview as the develop - every RAW opened from
the LIBRARY reached the studio as 0 bytes and the app said "Could not develop
this RAW file. Try again, or use its JPG."

Hand the worker a copy instead, in the develop and in the thumbnail path. The
EXIF stamps read off the caller's bytes afterwards are intact again.

The ORF check now hands the bytes over in the transfer list the way the package
does, so it fails on any develop that forgets this.
2026-10-06 16:16:32 +07:00
3dtours 16c098cd6a fix(raw): fix infinite loop in JPEG binary scanner and ensure sub-100ms preview extraction 2026-10-06 15:35:30 +07:00
3dtours c4d95e5722 chore(pwa): bump service worker version to v10 & rebuild container 2026-10-06 15:05:33 +07:00
3dtours 5db943ddd5 fix(library): fix RAW develop fallback & optimize STUDIO to LIBRARY transition speed 2026-10-06 13:24:20 +07:00
3dtours 2e1feb195a fix(raw): fix RAW preview matching, EXIF orientation alignment & studio initial state 2026-10-06 12:23:38 +07:00
3dtours 23f534ffb8 chore(docker): rebuild the NAS image bundle on ce691aa
Both images are in it this time and both carry their tag, so a NAS loads and
runs the stack without a build. The single-image package from before the
bundle (recipescam-web-frontend-acbb2bb) is dropped.

VERSION=ce691aa is stamped into the frontend, which is what the foot of the
studio's tab rail reads.
2026-10-06 10:46:08 +07:00
3dtours ce691aa8cb fix(raw): keep the blown highlights neutral after the tone curve
The blown gate draws a clipped pixel at its own maximum -- one value across
the three channels, so the frame's highlights carry no cast -- but the
per-channel tone curve runs after it and re-tints what the gate had just
made neutral. One cubic a channel, fitted on a grid that has no block left
to fit where the frame ran out (previewMatch drops the fully blown ones),
so at the plateau the three curves agree only at 1.0 and part company
either side of it.

On the ORF this was reported on, the moonlit sky came back 254,255,255 and
253,255,254 -- red under green across a quarter of the frame.

The gate is the develop's own statement that the pixel had no colour of its
own, so it is re-read on the value that leaves the shader. The blown pixels
now measure 254.95,254.94,254.97 (R/G 1.0000) against 253.44,254.94,254.51
(0.9941) before; their two most common triples, 254,255,255 at 149315 and
253,255,254 at 122127, collapse to 255,255,255 at 278099. The bright bands
land at -0.0,-0.0,-0.0 against the embedded preview where they were
-2.1,-0.0,-1.0, and midtones and the lower half are untouched.
2026-10-06 10:45:15 +07:00
3dtours 9aa9bc0db0 fix(raw): keep the ORF off olive by dropping the 3x3 preview match
The least-squares 3x3 that mapped the develop onto the camera's JPEG won
on luminance, whose variance is ~80x the chroma's, so it paid for its
match by crushing the R-G axis: 11.6 -> 4.6 standard deviations, against
8.3 in the camera's own preview and 12.3 in its JPEG. That is the olive
cast -- the sky and the rice both shifted yellow-green.

The colour chain was never at fault: camera_mul x rgb_cam already agrees
with LibRaw to the digit, and the app's fit is bit-identical to a numpy
reference of the same problem, which is how the 3x3 was cleared.

Fit only the per-channel tone curve now. R-G comes back to 8.6 (900px)
and 10.5 (full), and B/G lands on the preview's 0.841 to three places.
2026-10-06 08:29:07 +07:00
3dtours 7363bd9099 chore(docker): rebuild the NAS image bundle on 3d759cc 2026-10-06 07:29:50 +07:00
3dtours 3d759cc71b fix(studio): let a session an older build filled heal on the next open
The previous frame stops a session being written, not read: the browser that
reported this still holds the olive develop under the ORF's name, and a reload
would paint it once more. A session photo named like a RAW is that develop and
nothing else — the RAW branch writes none — so the restore path now reads the
parked RAW first and only falls back to such a copy when there is nothing
parked to develop. An ordinary photo's name is left alone.

Measured in Chromium on the built bundle, with the session poisoned and the ORF
parked: a copy named POISON.ORF opens as the re-developed 1600x1197 frame
(mean 167.3,164.2,138) and a copy named POISON.jpg opens as itself.
2026-10-06 07:28:13 +07:00
3dtours 177f582aab chore(docker): rebuild the NAS image bundle on c346b65
The two saved images, frontend at 0.1.0+c346b65 and the API it proxies to.
recipescam-web-frontend-c346b65.tar.gz sits beside it, untracked, as the
frontend alone.
2026-10-06 07:25:14 +07:00
3dtours c346b657eb fix(studio): stop the session from serving a frame an older build developed
A RAW was parked in OPFS whole and its develop kept in the session too, so a
reload painted the stored JPEG and never went back to the sensor. That makes a
session outlive the engine that filled it: 2e36acd fixed the Olympus ORF's
colour — measured against the file's own preview, mean 167.0,163.9,137.7
against the preview's 167.0,164.8,138.6 — and a browser that had opened the ORF
before the fix kept painting the olive develop, which is what "the fix is
deployed and it still opens olive" was.

The RAW is in OPFS either way, so the RAW branch now clears the session slot
instead of filling it (forgetPhoto) and the reload develops the parked sensor
data again (App.tsx:767). Measured in Chromium against the deployed bundle:
after a drop the session holds no photo, and a reload re-develops the parked
RAW to the same 1600x1197 frame, mean 167.3,164.2,138. The cost is one develop
per reload; the comment names the build stamp that would lift it.
2026-10-06 07:23:21 +07:00
3dtours 2e36acd18b fix(raw): open Olympus ORF at the colour of its own preview via rgb_cam
The develop preferred cam_xyz, whose rows are the XYZ of each camera channel and
which nothing normalised: on the ORF this was reported on it left the frame
green and blue (R/G 0.921, B/G 0.886) where the file's own preview sits at
1.013 / 0.841, and saturated reds came back as the dark purple the frame was
reported for — a red pixel's green ran negative through a row that carries
-2.64, so it clipped to 0 while the knee pulled red down with it.

LibRaw hands back dcraw's own rgb_cam, already the camera -> sRGB transform with
rows summing to one, and it is now applied as handed back: R/G 1.020 B/G 0.920,
and the fit against the embedded preview follows to mean 167.5,164.8,138.5
against the preview's 167.0,164.8,138.6 and the camera's own JPEG's 165,162,133.
Dividing rgb_cam by pre_mul — which carries that row normalisation — is what the
frame before this one did instead, and it undoes it.

The cam_xyz chain stays as the fallback for a file with no rgb_cam, with its
rows normalised so a neutral frame opens neutral. invert3x3, unused since the
frame stopped going through the chain, is dropped.
2026-10-05 22:51:10 +07:00
3dtours cef7868db8 fix(raw): eliminate double white balance application by un-scaling pre_mul on rgb_cam 2026-10-05 21:59:51 +07:00
3dtours d7ec00c08e fix(raw): correct matrix direction and previewMatch block clipping condition for accurate RAW colors 2026-10-05 20:58:13 +07:00
3dtours 1be75efd5a fix(raw): apply invert3x3 matrix conversion to eliminate red-to-purple shadow artifacts 2026-10-05 20:37:19 +07:00
3dtours 8021529657 fix(raw): resolve RAW green-yellow color cast via cam_xyz prioritization, daylight WB fallback, and fitMatch sanity checks 2026-10-05 20:16:53 +07:00
3dtours c486397388 fix(library): fix Olympus ORF raw color conversion and add custom photo context menu modal 2026-10-05 18:22:22 +07:00
3dtours fbed70b9da fix(raw): pin tone curve ends to eliminate shadow purple artifacts and fix camera matrix WB color cast 2026-10-05 16:29:10 +07:00
3dtours f98ce2c042 fix(raw): fix RAW color matrix calculation, enable highlight recovery mode, and add dedicated tiffThumbnail decoder 2026-10-05 16:03:41 +07:00
3dtours 480fd148a3 fix(raw): enable camera white balance multipliers and fallback extraction to fix RAW color cast on Olympus and other bodies 2026-10-05 15:43:20 +07:00
3dtours 140b4f88b2 fix(library): resolve main-thread UI freeze and process kill data loss via OPFS payload capping, yield throttling, and UI batching 2026-10-05 12:13:45 +07:00
3dtours 1e93c8484b fix(library): optimize TIF/TIFF file handling via embedded JPEG extraction to prevent RAM spikes and render thumbnails 2026-10-05 10:26:59 +07:00
3dtours e63183b298 feat(library): smart subfolder auto-link and instant handle reconnection 2026-10-05 10:03:09 +07:00
3dtours c4d904a207 fix(library): instant Lightroom-style catalog restore with lazy tile fallback 2026-10-05 08:05:09 +07:00
3dtours b30c7403e3 fix(library): add one-time DB cursor migration to clean legacy Blobs and implement hierarchical Add Folder tree matching 2026-10-05 07:01:11 +07:00
3dtours 77cf872b16 fix(library): optimize image scanning, fast raw preview extraction and lazy thumbnail loading 2026-10-05 06:34:37 +07:00
3dtours 44468ac178 chore(release): repackage recipescam-images at 79f0d77
Frontend gains update, rescan and stop on the folder menu, queued per roll.
2026-10-02 22:26:35 +07:00
3dtours 79f0d77a8e feat(library): update, rescan and stop on the folder menu, queued per roll
A folder row now asks for the rest of a roll, the whole of it from the top,
or takes its request back. Requests queue behind the reading in flight and
run one at a time; a row waiting its turn draws a ring that does not turn.
2026-10-02 22:25:43 +07:00
3dtours 0616b786ea chore(release): repackage recipescam-images at e43c02c 2026-10-02 21:54:41 +07:00
3dtours e43c02c36f fix(library): spell the names the browser refuses to write
A backup folder is picked, not created: the frames in it carry the names a
camera or a person gave them, and Chromium's IsSafePathComponent will not
spell a name back onto a disk if it holds a control or format character, one
of ": * ? " < > | \ /", a space, dot or tilde at either end, or a .lnk/.scf/
.url tail. The library is read through a handle, which lets all of those
through, so the refusal only surfaced on the way out: getDirectoryHandle threw
"Name is not allowed" on the first such folder, the run stopped there and left
an empty thumbs/ behind it.

Spell a component that would be refused as %XX per its UTF-8 bytes, on the way
out and on the way back in, so those frames keep their tiles in the backup and
the restore still finds them. A name that was already safe is handed back
untouched, which keeps an existing folder readable, copyable and rsyncable by
hand.

The run also no longer dies on the one frame that will not write: it skips it,
names it in the console, and finishes the rest.
2026-10-02 21:51:25 +07:00
3dtours 895a055a03 chore(release): repackage recipescam-images at 07a38be 2026-10-02 21:18:59 +07:00
3dtours 07a38be5a9 fix(super-res): warm the export runtime's bytes, not the runtime, and release its session on the way out
Opening the export menu imported onnxruntime-web, and importing that runtime is
what starts its thread pool: one Web Worker per thread, each holding a copy of
the 27MB wasm build, held by the page until it dies whether or not an export is
ever asked for. Those workers are the child processes a visitor finds hanging off
the tab (and the tab, and the browser, goes when one of them is killed), and the
memory is what a laptop has none of when the studio is closed and opened again.

The menu now fetches the files by name and lets the browser's cache do the
warming; the runtime is imported by the export that needs it. The threads are
capped at four, and pagehide hands the session back instead of leaving the
renderer to reap it with the page.
2026-10-02 21:04:56 +07:00
3dtours 7a333c1be5 chore(release): repackage recipescam-images at c802c43 2026-10-02 20:10:50 +07:00
3dtours c802c43e04 fix(assets): give the frame sheets a build-stamped URL, so a replaced sheet stops arriving from cache 2026-10-02 20:10:02 +07:00
3dtours baf37f13d7 chore(release): repackage recipescam-images at ef48a59 2026-10-02 20:03:42 +07:00
3dtours ef48a59d44 chore(assets): replace old_film.png sheet with the tighter deckle border 2026-10-02 20:03:00 +07:00
3dtours 45b505864e chore(release): repackage recipescam-images at 8d0a46e
Frontend now carries the portrait OLD FILM fix. Both refs are :latest, so
the tarball stays ~125 MB instead of dragging the per-commit tags.
2026-10-02 19:55:35 +07:00
3dtours 8d0a46e7f5 fix(frame): give OLD FILM PORTRAIT a real portrait output
The sheet was stretched over the visitor's frame, so the portrait variant
only turned the paper inside whatever shape the photo had: a landscape
photo stayed landscape. The film now has its own opening like the walls
do — the frame is scaled up to the photo, the photo is cover-cropped into
the whole sheet, and `old-film-portrait` is the PNG turned 90° CW, so the
pair is one frame standing and one lying. The torn edge has no straight
sides, so the photo runs under all of it instead of being cut against a
measured window.
2026-10-02 19:54:42 +07:00
3dtours 177e3c3a9c chore(release): repackage recipescam-images at 84b8e64, drop the pre-frame frontend archive 2026-10-02 19:46:46 +07:00
3dtours 84b8e64652 feat(frame): add OLD FILM LANDSCAPE and OLD FILM PORTRAIT from old_film.png 2026-10-02 19:46:06 +07:00
3dtours 256fe6c2e2 fix(raw): refine RAW highlight clipping threshold to preserve sunset sun color without white dome clipping 2026-10-02 18:15:21 +07:00
3dtours 22fa4f289b fix(tone): enhance shadow fill curve to match Lightroom Classic shadow lift on dark fabric 2026-10-02 17:50:44 +07:00