fix(library): support thumbnail generation and display for HEIC and TIF files
This commit is contained in:
@@ -4,8 +4,8 @@ import { go } from './nav';
|
|||||||
import { api } from './api';
|
import { api } from './api';
|
||||||
import { keep, recall } from './remember';
|
import { keep, recall } from './remember';
|
||||||
import { readGps, readSpecs, specsLine } from './engine/imageOps';
|
import { readGps, readSpecs, specsLine } from './engine/imageOps';
|
||||||
import { isRawName, rawThumbnail } from './engine/rawDevelop';
|
import { isRawName, rawThumbnail, tiffThumbnail } from './engine/rawDevelop';
|
||||||
import { heicToJpeg, isHeicName } from './engine/heicDevelop';
|
import { heicThumbnail, heicToJpeg, isHeicName } from './engine/heicDevelop';
|
||||||
import {
|
import {
|
||||||
cachedFolders,
|
cachedFolders,
|
||||||
cachedPhotos,
|
cachedPhotos,
|
||||||
@@ -2443,18 +2443,40 @@ function useThumbUrls(photos: LibraryPhoto[], also?: LibraryPhoto | null): Recor
|
|||||||
|
|
||||||
if (idsToFetch.length === 0) return;
|
if (idsToFetch.length === 0) return;
|
||||||
|
|
||||||
void getPhotoThumbs(idsToFetch).then((thumbMap) => {
|
void getPhotoThumbs(idsToFetch).then(async (thumbMap) => {
|
||||||
if (!alive) return;
|
if (!alive) return;
|
||||||
setUrls((prev) => {
|
const updates: Record<string, string> = {};
|
||||||
const next = { ...prev };
|
for (const photo of list) {
|
||||||
for (const photo of list) {
|
let blob = thumbMap.get(photo.id) ?? photo.thumb ?? null;
|
||||||
const blob = thumbMap.get(photo.id) ?? photo.thumb ?? null;
|
if (!blob && photo.handle) {
|
||||||
if (blob) {
|
try {
|
||||||
next[photo.id] = cacheObjectUrl(photo.id, blob);
|
const file = await readPhotoFile(photo);
|
||||||
|
if (!alive) return;
|
||||||
|
const name = file.name;
|
||||||
|
if (isHeicName(name)) {
|
||||||
|
const fullBytes = new Uint8Array(await file.arrayBuffer());
|
||||||
|
let prevBytes = await heicThumbnail(fullBytes);
|
||||||
|
if (!prevBytes) prevBytes = await heicToJpeg(fullBytes, 320);
|
||||||
|
if (prevBytes) blob = new Blob([prevBytes as BlobPart], { type: 'image/jpeg' });
|
||||||
|
} else if (isRawName(name) || /\.(tiff?)$/i.test(name)) {
|
||||||
|
const fullBytes = new Uint8Array(await file.arrayBuffer());
|
||||||
|
let prevBytes = await rawThumbnail(fullBytes, name);
|
||||||
|
if (!prevBytes && /\.(tiff?)$/i.test(name)) prevBytes = tiffThumbnail(fullBytes);
|
||||||
|
if (prevBytes) blob = new Blob([prevBytes as BlobPart], { type: 'image/jpeg' });
|
||||||
|
} else {
|
||||||
|
blob = file;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
blob = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return next;
|
if (blob && alive) {
|
||||||
});
|
updates[photo.id] = cacheObjectUrl(photo.id, blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alive && Object.keys(updates).length > 0) {
|
||||||
|
setUrls((prev) => ({ ...prev, ...updates }));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
// smart collections) that hand-rolled cursors would make ugly.
|
// smart collections) that hand-rolled cursors would make ugly.
|
||||||
import type { Recipe } from '../../shared/types';
|
import type { Recipe } from '../../shared/types';
|
||||||
import { readCapturedAt } from './imageOps';
|
import { readCapturedAt } from './imageOps';
|
||||||
import { isRawName, rawThumbnail } from './rawDevelop';
|
import { isRawName, rawThumbnail, tiffThumbnail } from './rawDevelop';
|
||||||
import { heicThumbnail, isHeicName } from './heicDevelop';
|
import { heicThumbnail, heicToJpeg, isHeicName } from './heicDevelop';
|
||||||
import { openAt, walkPass, type Walk, type WalkFile } from './rollWalk';
|
import { openAt, walkPass, type Walk, type WalkFile } from './rollWalk';
|
||||||
|
|
||||||
const DB_NAME = 'recipescam-library';
|
const DB_NAME = 'recipescam-library';
|
||||||
@@ -905,20 +905,38 @@ export async function scanFolder(
|
|||||||
}
|
}
|
||||||
const raw = isRawName(file.name);
|
const raw = isRawName(file.name);
|
||||||
const isTiff = /\.(tiff?)$/i.test(file.name);
|
const isTiff = /\.(tiff?)$/i.test(file.name);
|
||||||
// A HEIC is read for its tile the way a RAW is, and reads less: the head of
|
|
||||||
// the file is the whole of what its own thumbnail item needs (see
|
|
||||||
// heicDevelop.ts), so it takes the same slice an ordinary photo does.
|
|
||||||
const heic = isHeicName(file.name);
|
const heic = isHeicName(file.name);
|
||||||
const RAW_HEADER_BYTES = 16 * 1024 * 1024;
|
|
||||||
const sliceBuffer = await file.slice(0, (raw || isTiff) ? Math.min(file.size, RAW_HEADER_BYTES) : HEAD_BYTES).arrayBuffer();
|
// Slice buffer: HEIC uses up to 4MB; RAW & TIFF use up to 32MB (or full file if smaller).
|
||||||
|
const MAX_SLICE_BYTES = (raw || isTiff) ? 32 * 1024 * 1024 : heic ? 4 * 1024 * 1024 : HEAD_BYTES;
|
||||||
|
const sliceBuffer = await file.slice(0, Math.min(file.size, MAX_SLICE_BYTES)).arrayBuffer();
|
||||||
const bytes = new Uint8Array(sliceBuffer);
|
const bytes = new Uint8Array(sliceBuffer);
|
||||||
const preview = heic
|
|
||||||
? await heicThumbnail(bytes)
|
let preview: Uint8Array | null = null;
|
||||||
: raw || isTiff
|
if (heic) {
|
||||||
? await rawThumbnail(bytes, file.name)
|
preview = await heicThumbnail(bytes);
|
||||||
: null;
|
if (!preview) {
|
||||||
const src = preview ? new Blob([preview as BlobPart], { type: 'image/jpeg' }) : raw || heic ? null : file;
|
try {
|
||||||
const size = preview ? jpegSize(preview) : raw || heic ? null : jpegSize(bytes);
|
const fullBytes = new Uint8Array(await file.arrayBuffer());
|
||||||
|
preview = await heicToJpeg(fullBytes, 320);
|
||||||
|
} catch {
|
||||||
|
preview = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (raw || isTiff) {
|
||||||
|
preview = await rawThumbnail(bytes, file.name);
|
||||||
|
if (!preview && isTiff) {
|
||||||
|
try {
|
||||||
|
const fullBytes = new Uint8Array(await file.arrayBuffer());
|
||||||
|
preview = tiffThumbnail(fullBytes);
|
||||||
|
} catch {
|
||||||
|
preview = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const src = preview ? new Blob([preview as BlobPart], { type: 'image/jpeg' }) : (raw || heic || isTiff) ? null : file;
|
||||||
|
const size = preview ? jpegSize(preview) : (raw || heic || isTiff) ? null : jpegSize(bytes);
|
||||||
const thumb = src ? await tile(src, size) : null;
|
const thumb = src ? await tile(src, size) : null;
|
||||||
const taken = (await readCapturedAt(bytes)) ?? file.lastModified;
|
const taken = (await readCapturedAt(bytes)) ?? file.lastModified;
|
||||||
const cut = rel.lastIndexOf('/');
|
const cut = rel.lastIndexOf('/');
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ half4 main(float2 pos) {
|
|||||||
const RAW_EXT = [
|
const RAW_EXT = [
|
||||||
'arw', 'srf', 'sr2', 'cr2', 'cr3', 'crw', 'nef', 'nrw', 'dng', 'orf', 'raf',
|
'arw', 'srf', 'sr2', 'cr2', 'cr3', 'crw', 'nef', 'nrw', 'dng', 'orf', 'raf',
|
||||||
'rw2', 'pef', 'srw', 'raw', 'rwl', '3fr', 'fff', 'iiq', 'mos', 'mrw', 'x3f',
|
'rw2', 'pef', 'srw', 'raw', 'rwl', '3fr', 'fff', 'iiq', 'mos', 'mrw', 'x3f',
|
||||||
|
'tif', 'tiff',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const RAW_ACCEPT = RAW_EXT.map((e) => `.${e}`).join(',');
|
export const RAW_ACCEPT = RAW_EXT.map((e) => `.${e}`).join(',');
|
||||||
|
|||||||
Reference in New Issue
Block a user