be2ec0a4a1
Sharp's bundled libvips has no raw loader, so a DNG can't be thumbnailed directly. This adds a preview-extraction step so RAW/DNG uploads get a proper thumbnail + gallery preview while the original RAW is kept for download. - imageProcessor: isRawFilename() + extractRawPreview() (exiftool extracts the embedded full-res JPEG — JpgFromRaw → PreviewImage → ThumbnailImage, validated with sharp) + withProcessableImage() which is a pass-through for ordinary images and swaps in the extracted JPEG for RAW. Wired into ingest (photoProcessor) and all three on-demand generators (ensureThumbnail/Hero/ Preview). generateHeroImage/generatePreviewImage gained outputBasename so RAW-derived outputs stay named after the source. - Dockerfile: add exiftool (confirmed present in Alpine v3.24 community). - Format maps: dng → image/x-adobe-dng in uploadSettings.js and fileTypes.ts; ALLOWED_MEDIA_TYPES gains a DNG entry (TIFF magic numbers) so it passes the security file-validator. Strictly gated by extension: nothing in this path runs for jpg/png/webp/etc, so existing photos are unaffected. If extraction fails (corrupt RAW, no embedded preview), the photo is marked 'failed' with a clear error — same as any unreadable upload. Verification boundary (please validate on a real DNG after the image rebuilds): the exiftool extraction itself couldn't be exercised in the dev sandbox (exiftool isn't a dev dependency and there's no DNG fixture). Unit tests cover the gating (RAW detection + non-RAW pass-through + clean failure without exiftool); existing processPhoto tests still pass. Known limitation: a DNG is only accepted when the browser reports its MIME as image/x-adobe-dng (Chrome does); browsers that send an empty type reject it client- and server-side — a follow-up can add extension-based acceptance for the RAW set. Companion to the HEIC/dynamic-hint PR; targets main only.
51 lines
2.2 KiB
JavaScript
51 lines
2.2 KiB
JavaScript
/**
|
|
* Unit tests for the RAW/DNG handling helpers (#821). The actual exiftool
|
|
* extraction can only be exercised in the built image (exiftool isn't a dev
|
|
* dependency), so these cover the gating logic: which files are treated as RAW,
|
|
* and that ordinary images pass through untouched (zero cost / no extraction).
|
|
*/
|
|
const path = require('path');
|
|
const { isRawFilename, withProcessableImage, RAW_EXTENSIONS } = require('../../src/services/imageProcessor');
|
|
|
|
describe('isRawFilename', () => {
|
|
it('recognises common RAW / DNG extensions', () => {
|
|
for (const ext of ['dng', 'cr2', 'cr3', 'nef', 'arw', 'raf', 'rw2', 'orf']) {
|
|
expect(isRawFilename(`IMG_1234.${ext}`)).toBe(true);
|
|
expect(isRawFilename(`IMG_1234.${ext.toUpperCase()}`)).toBe(true); // case-insensitive
|
|
}
|
|
});
|
|
|
|
it('does not treat ordinary images/videos as RAW', () => {
|
|
for (const name of ['photo.jpg', 'photo.jpeg', 'photo.png', 'photo.webp', 'clip.mp4', 'clip.mov', 'photo.heic']) {
|
|
expect(isRawFilename(name)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('is null/empty safe', () => {
|
|
expect(isRawFilename(null)).toBe(false);
|
|
expect(isRawFilename('')).toBe(false);
|
|
expect(isRawFilename('noextension')).toBe(false);
|
|
});
|
|
|
|
it('RAW_EXTENSIONS includes dng (Apple ProRAW)', () => {
|
|
expect(RAW_EXTENSIONS.has('dng')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('withProcessableImage', () => {
|
|
it('passes ordinary images through with no extraction and a no-op cleanup', async () => {
|
|
const localPath = '/tmp/whatever/photo.jpg';
|
|
const proc = await withProcessableImage(localPath, 'photo.jpg');
|
|
expect(proc.path).toBe(localPath); // unchanged — sharp reads it directly
|
|
expect(proc.outputBasename).toBeUndefined(); // generators keep their default naming
|
|
await expect(Promise.resolve(proc.cleanup())).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('routes RAW files to extraction (which fails cleanly without exiftool/preview)', async () => {
|
|
// In the dev sandbox exiftool isn't installed, so extraction throws — the
|
|
// caller turns that into a normal processing failure. In the built image
|
|
// (exiftool present) this instead returns the embedded JPEG preview.
|
|
await expect(withProcessableImage('/tmp/whatever/IMG_1234.dng', 'IMG_1234.dng')).rejects.toThrow();
|
|
});
|
|
});
|