Compare commits

...

12 commits

Author SHA1 Message Date
mertalev
f8580e567d
strip metadata with exiftool 2023-10-23 22:30:11 -04:00
Jonathan Jogenfors
f140da2ca1 fix: lint 2023-10-15 23:43:49 +02:00
Jonathan Jogenfors
4c1cac71c9 Merge branch 'main' of https://github.com/immich-app/immich into 4382-thumbnail-metadata 2023-10-15 23:19:37 +02:00
Jonathan Jogenfors
18889753b2 feat: use upload in e2e test 2023-10-15 23:19:31 +02:00
Jonathan Jogenfors
d14e686f60 feat: test metadata of both webp and jpg 2023-10-15 23:08:47 +02:00
Jonathan Jogenfors
770ac0063e fix: revert switch to tiff 2023-10-15 22:37:42 +02:00
Jonathan Jogenfors
4c56ef0526 fix: use tiff thumbnails in first step + e2e fix 2023-10-14 23:50:12 +02:00
Alex Tran
259ed35b62 Merge branch 'main' of github.com:immich-app/immich into 4382-thumbnail-metadata 2023-10-14 15:00:53 -05:00
Jonathan Jogenfors
276eb43196 Merge branch 'main' of https://github.com/immich-app/immich into 4382-thumbnail-metadata 2023-10-14 00:37:17 +02:00
Jonathan Jogenfors
0a25d50822 feat. basic metadata e2e test 2023-10-14 00:37:11 +02:00
Jonathan Jogenfors
beb2a48339 Merge branch 'main' of https://github.com/immich-app/immich into 4382-thumbnail-metadata 2023-10-13 13:58:21 +02:00
Thomas Way
c647385847
fix(server): strip metadata from thumbnails
#3658 introduced support for thumbnail ICC profiles, but also inadvertently
included all thumbnail metadata. It seems this has to be explicitly disabled.

Refs: #4382
2023-10-11 22:16:59 +01:00
4 changed files with 115 additions and 2 deletions

View file

@ -6,6 +6,7 @@ import fs from 'fs/promises';
import sharp from 'sharp'; import sharp from 'sharp';
import { Writable } from 'stream'; import { Writable } from 'stream';
import { promisify } from 'util'; import { promisify } from 'util';
import { exiftool, Tags, WriteTags } from 'exiftool-vendored';
const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe); const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
sharp.concurrency(0); sharp.concurrency(0);
@ -26,14 +27,19 @@ export class MediaRepository implements IMediaRepository {
} }
async resize(input: string | Buffer, output: string, options: ResizeOptions): Promise<void> { async resize(input: string | Buffer, output: string, options: ResizeOptions): Promise<void> {
const chromaSubsampling = options.quality >= 80 ? '4:4:4' : '4:2:0'; // this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
await sharp(input, { failOn: 'none' }) await sharp(input, { failOn: 'none' })
.pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16') .pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16')
.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true }) .resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true })
.rotate() .rotate()
.withMetadata({ icc: options.colorspace }) .withMetadata({ icc: options.colorspace })
.toFormat(options.format, { quality: options.quality, chromaSubsampling }) .toFormat(options.format, {
quality: options.quality,
// this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0',
})
.toFile(output); .toFile(output);
await exiftool.write(output, {EXIF: null} as any as WriteTags, ['-g', '-overwrite_original']);
} }
async probe(input: string): Promise<VideoInfo> { async probe(input: string): Promise<VideoInfo> {

View file

@ -36,4 +36,18 @@ export const assetApi = {
expect(status).toBe(201); expect(status).toBe(201);
return body as AssetFileUploadResponseDto; return body as AssetFileUploadResponseDto;
}, },
getWebpThumbnail: async (server: any, accessToken: string, assetId: string) => {
const { body, status } = await request(server)
.get(`/asset/thumbnail/${assetId}`)
.set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200);
return body;
},
getJpegThumbnail: async (server: any, accessToken: string, assetId: string) => {
const { body, status } = await request(server)
.get(`/asset/thumbnail/${assetId}?format=JPEG`)
.set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200);
return body;
},
}; };

View file

@ -0,0 +1,91 @@
import { AssetResponseDto, LoginResponseDto } from '@app/domain';
import { AssetController } from '@app/immich';
import { INestApplication } from '@nestjs/common';
import { api } from '@test/api';
import * as fs from 'fs';
import {
IMMICH_TEST_ASSET_PATH,
IMMICH_TEST_ASSET_TEMP_PATH,
createTestApp,
db,
itif,
restoreTempFolder,
runAllTests,
} from '@test/test-utils';
import { exiftool } from 'exiftool-vendored';
describe(`${AssetController.name} (e2e)`, () => {
let app: INestApplication;
let server: any;
let admin: LoginResponseDto;
beforeAll(async () => {
app = await createTestApp(true);
server = app.getHttpServer();
});
beforeEach(async () => {
await db.reset();
await restoreTempFolder();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);
});
afterAll(async () => {
await db.disconnect();
await app.close();
await restoreTempFolder();
});
describe.only('should strip metadata of', () => {
let assetWithLocation: AssetResponseDto;
beforeEach(async () => {
const fileContent = await fs.promises.readFile(
`${IMMICH_TEST_ASSET_PATH}/metadata/gps-position/thompson-springs.jpg`,
);
await api.assetApi.upload(server, admin.accessToken, 'test-asset-id', { content: fileContent });
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(assets).toHaveLength(1);
assetWithLocation = assets[0];
expect(assetWithLocation).toEqual(
expect.objectContaining({
exifInfo: expect.objectContaining({ latitude: 39.115, longitude: -108.400968333333 }),
}),
);
});
itif(runAllTests)('small webp thumbnails', async () => {
const assetId = assetWithLocation.id;
const thumbnail = await api.assetApi.getWebpThumbnail(server, admin.accessToken, assetId);
await fs.promises.writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`, thumbnail);
const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`);
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
itif(runAllTests)('large jpeg thumbnails', async () => {
const assetId = assetWithLocation.id;
const thumbnail = await api.assetApi.getJpegThumbnail(server, admin.accessToken, assetId);
await fs.promises.writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`, thumbnail);
const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`);
console.log(assetWithLocation);
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
});
});

View file

@ -70,6 +70,8 @@ export async function createTestApp(runJobs = false, log = false): Promise<INest
export const runAllTests: boolean = process.env.IMMICH_RUN_ALL_TESTS === 'true'; export const runAllTests: boolean = process.env.IMMICH_RUN_ALL_TESTS === 'true';
export const itif = (condition: boolean) => (condition ? it : it.skip);
const directoryExists = async (dirPath: string) => const directoryExists = async (dirPath: string) =>
await fs.promises await fs.promises
.access(dirPath) .access(dirPath)