144 lines
5.2 KiB
TypeScript
144 lines
5.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import { zoomDimensions, zoomSourceFor } from '../zoomSource';
|
||
|
||
/**
|
||
* Which file the zoom may show (#467). The rule that matters most is the
|
||
* negative one: a format whose metadata the scrubber does not edit must never
|
||
* be served, whatever a browser could display.
|
||
*/
|
||
describe('zoomSourceFor', () => {
|
||
const image = (originalMimeType: string | null, originalFileName = 'x') => ({
|
||
type: 'IMAGE',
|
||
originalMimeType,
|
||
originalFileName,
|
||
});
|
||
|
||
it('serves JPEG and AVIF originals, which the scrubber cleans', () => {
|
||
expect(zoomSourceFor(image('image/jpeg'))).toBe('original');
|
||
expect(zoomSourceFor(image('image/jpg'))).toBe('original');
|
||
expect(zoomSourceFor(image('image/avif'))).toBe('original');
|
||
expect(zoomSourceFor(image('IMAGE/JPEG; charset=binary'))).toBe('original');
|
||
});
|
||
|
||
it('uses Immich’s rendition for what a browser cannot show', () => {
|
||
for (const mime of [
|
||
'image/heic',
|
||
'image/heif',
|
||
'image/x-adobe-dng',
|
||
'image/x-sony-arw',
|
||
'image/tiff',
|
||
'image/jxl',
|
||
'image/bmp',
|
||
]) {
|
||
expect(zoomSourceFor(image(mime)), mime).toBe('fullsize');
|
||
}
|
||
});
|
||
|
||
it('never zooms PNG, WebP or GIF: their metadata would go out unscrubbed', () => {
|
||
for (const mime of ['image/png', 'image/webp', 'image/gif', 'image/svg+xml']) {
|
||
expect(zoomSourceFor(image(mime)), mime).toBeNull();
|
||
}
|
||
});
|
||
|
||
it('never zooms a video', () => {
|
||
expect(
|
||
zoomSourceFor({
|
||
type: 'VIDEO',
|
||
originalMimeType: 'video/quicktime',
|
||
originalFileName: 'a.mov',
|
||
}),
|
||
).toBeNull();
|
||
// Even one mislabelled as an image type.
|
||
expect(zoomSourceFor({ type: 'VIDEO', originalMimeType: 'image/jpeg' })).toBeNull();
|
||
});
|
||
|
||
it('falls back to the file extension without a MIME type', () => {
|
||
expect(zoomSourceFor(image(null, 'L1001783.JPG'))).toBe('original');
|
||
expect(zoomSourceFor(image(null, 'export.avif'))).toBe('original');
|
||
expect(zoomSourceFor(image(null, 'IMG_6109.HEIC'))).toBe('fullsize');
|
||
expect(zoomSourceFor(image(null, 'scan.png'))).toBeNull();
|
||
expect(zoomSourceFor(image(null, 'no-extension'))).toBeNull();
|
||
});
|
||
|
||
it('zooms a photo edited in Immich into its edited rendition (#831)', () => {
|
||
// Immich serves the unedited file as the original: a crop would come
|
||
// undone. The edited full-size rendition matches the edited preview.
|
||
expect(zoomSourceFor({ ...image('image/jpeg'), isEdited: true })).toBe('edited');
|
||
expect(zoomSourceFor({ ...image('image/heic'), isEdited: true })).toBe('edited');
|
||
expect(zoomSourceFor({ ...image('image/jpeg'), isEdited: false })).toBe('original');
|
||
expect(zoomSourceFor({ ...image('image/heic'), isEdited: false })).toBe('fullsize');
|
||
// Edits never make a video zoomable.
|
||
expect(
|
||
zoomSourceFor({ type: 'VIDEO', originalMimeType: 'video/mp4', isEdited: true }),
|
||
).toBeNull();
|
||
});
|
||
|
||
it('refuses a non-image MIME type on an image asset', () => {
|
||
expect(zoomSourceFor(image('application/octet-stream'))).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('zoomDimensions', () => {
|
||
it('gives an edited photo without its edited size no size at all (review of #832)', () => {
|
||
// EXIF describes the unedited file; a rotated photo would get the box of
|
||
// the unrotated one. No size means no zoom.
|
||
expect(
|
||
zoomDimensions({
|
||
isEdited: true,
|
||
exifInfo: { exifImageWidth: 1076, exifImageHeight: 723, orientation: null },
|
||
}),
|
||
).toBeUndefined();
|
||
});
|
||
|
||
it('gives an edited photo its edited size, which is what its rendition has (#831)', () => {
|
||
// Measured on Immich 3.2: an iPhone HEIC stored 5712×4284 with
|
||
// orientation 6, turned back to landscape in Immich's editor. The edited
|
||
// full-size rendition is 5712×4284; EXIF alone would say 4284×5712.
|
||
expect(
|
||
zoomDimensions({
|
||
width: 5712,
|
||
height: 4284,
|
||
exifInfo: { exifImageWidth: 5712, exifImageHeight: 4284, orientation: '6' },
|
||
}),
|
||
).toEqual({ width: 5712, height: 4284 });
|
||
// A crop: stored 8064×6048 (orientation 6), cropped to 6048×4838.
|
||
expect(
|
||
zoomDimensions({
|
||
width: 6048,
|
||
height: 4838,
|
||
exifInfo: { exifImageWidth: 8064, exifImageHeight: 6048, orientation: '6' },
|
||
}),
|
||
).toEqual({ width: 6048, height: 4838 });
|
||
});
|
||
|
||
it('prefers Immich’s upright width and height', () => {
|
||
expect(
|
||
zoomDimensions({
|
||
width: 3024,
|
||
height: 4032,
|
||
exifInfo: { exifImageWidth: 4032, exifImageHeight: 3024, orientation: '6' },
|
||
}),
|
||
).toEqual({ width: 3024, height: 4032 });
|
||
});
|
||
|
||
it('turns EXIF dimensions for a quarter-turn orientation', () => {
|
||
expect(
|
||
zoomDimensions({
|
||
exifInfo: { exifImageWidth: 4032, exifImageHeight: 3024, orientation: '6' },
|
||
}),
|
||
).toEqual({ width: 3024, height: 4032 });
|
||
expect(
|
||
zoomDimensions({
|
||
exifInfo: { exifImageWidth: 7242, exifImageHeight: 4294, orientation: null },
|
||
}),
|
||
).toEqual({ width: 7242, height: 4294 });
|
||
});
|
||
|
||
it('is undefined when nothing is known', () => {
|
||
expect(zoomDimensions({})).toBeUndefined();
|
||
expect(
|
||
zoomDimensions({ exifInfo: { exifImageWidth: 0, exifImageHeight: 10 } }),
|
||
).toBeUndefined();
|
||
});
|
||
});
|