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.
This commit is contained in:
@@ -5,6 +5,7 @@ import { api } from './api';
|
||||
import { keep, recall } from './remember';
|
||||
import { readGps, readSpecs, specsLine } from './engine/imageOps';
|
||||
import { isRawName, rawThumbnail } from './engine/rawDevelop';
|
||||
import { heicToJpeg, isHeicName } from './engine/heicDevelop';
|
||||
import {
|
||||
cachedPhotos,
|
||||
canBrowseFolders,
|
||||
@@ -154,7 +155,47 @@ const why = (err: unknown) => {
|
||||
|
||||
|
||||
|
||||
// The catalogue is for people with an account. The shelves hold nothing the app
|
||||
// did not read off the visitor's own disk, but browsing them is still the one
|
||||
// screen that hands a folder over to be walked, and a guest one file picker away
|
||||
// from a folder that would be read for nothing is not the offer. The gate is
|
||||
// here rather than in the router so the scan never starts for a guest — the
|
||||
// catalogue component below is not mounted at all until the account is known.
|
||||
export function Library() {
|
||||
const { t } = useI18n();
|
||||
const [state, setState] = useState<'loading' | 'guest' | 'ready'>('loading');
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
api
|
||||
.me()
|
||||
.then((me) => {
|
||||
if (alive) setState(me?.user ? 'ready' : 'guest');
|
||||
})
|
||||
// No answer is treated like no account: the API being away is not a
|
||||
// reason to open the shelves to whoever is on the page.
|
||||
.catch(() => {
|
||||
if (alive) setState('guest');
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (state === 'loading') return <p className="hint adm-note">{t('auth.busy')}</p>;
|
||||
if (state === 'guest')
|
||||
return (
|
||||
<p className="adm-note">
|
||||
{t('lib.locked')}{' '}
|
||||
<a className="btn primary" href="/?auth=login">
|
||||
{t('nav.login')}
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
return <Catalogue />;
|
||||
}
|
||||
|
||||
function Catalogue() {
|
||||
const { t } = useI18n();
|
||||
const [folders, setFolders] = useState<LibraryFolder[]>([]);
|
||||
const [ready, setReady] = useState<Set<string>>(new Set());
|
||||
@@ -1075,7 +1116,14 @@ export function Library() {
|
||||
if (!live) return;
|
||||
|
||||
let url: string | null = null;
|
||||
if (isRawName(file.name)) {
|
||||
if (isHeicName(file.name)) {
|
||||
// The tile is the phone's own 320×240 copy; the stage wants more, so
|
||||
// the frame itself is decoded and capped at what a screen shows (see
|
||||
// heicWorker.ts — the decode is about a second for a 12MP frame, and
|
||||
// the tile stands in until it lands).
|
||||
const jpeg = await heicToJpeg(new Uint8Array(await file.arrayBuffer()), 2048);
|
||||
if (live) url = URL.createObjectURL(new Blob([jpeg as BlobPart], { type: 'image/jpeg' }));
|
||||
} else if (isRawName(file.name)) {
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
if (!live) return;
|
||||
const previewBytes = await rawThumbnail(bytes);
|
||||
@@ -1141,8 +1189,10 @@ export function Library() {
|
||||
);
|
||||
}, []);
|
||||
|
||||
// The tile: the thumbnail the catalogue kept, or a stand-in for the RAW whose
|
||||
// preview the camera never wrote. A tile only carries a handler where a click
|
||||
// The tile: the thumbnail the catalogue kept, or a stand-in for the frame
|
||||
// whose own small copy the file never carried — a RAW whose preview the camera
|
||||
// never wrote, a HEIC with no thumbnail item (see engine/heicDevelop.ts). A tile
|
||||
// only carries a handler where a click
|
||||
// on the picture means "open it" — in the strip the click belongs to the frame
|
||||
// it raises, not to the studio. The word is only for a frame that has no
|
||||
// thumbnail to give: one that has one and has not been handed it yet — the wall
|
||||
@@ -1157,7 +1207,7 @@ export function Library() {
|
||||
/>
|
||||
) : (
|
||||
<div className="lib-noimg" onClick={onPick} title={onPick ? t('lib.open') : undefined}>
|
||||
{photo.thumb ? '' : 'RAW'}
|
||||
{photo.thumb ? '' : isRawName(photo.name) ? 'RAW' : 'HEIC'}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user