chore: get dimensions directly with ExtractResult
parent
076cbfe137
commit
33b97776ee
|
|
@ -39,6 +39,7 @@ type ProgressEvent = {
|
|||
export type ExtractResult = {
|
||||
buffer: Buffer;
|
||||
format: RawExtractedFormat;
|
||||
dimensions: ImageDimensions;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -55,28 +56,28 @@ export class MediaRepository {
|
|||
async extract(input: string): Promise<ExtractResult | null> {
|
||||
try {
|
||||
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
|
||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
||||
return { buffer, format: RawExtractedFormat.Jpeg, dimensions: await this.getImageDimensions(buffer) };
|
||||
} catch (error: any) {
|
||||
this.logger.debug(`Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next: ${error}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
|
||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
||||
return { buffer, format: RawExtractedFormat.Jpeg, dimensions: await this.getImageDimensions(buffer) };
|
||||
} catch (error: any) {
|
||||
this.logger.debug(`Could not extract JPEG buffer from image, trying PreviewJXL next: ${error}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
|
||||
return { buffer, format: RawExtractedFormat.Jxl };
|
||||
return { buffer, format: RawExtractedFormat.Jxl, dimensions: await this.getImageDimensions(buffer) };
|
||||
} catch (error: any) {
|
||||
this.logger.debug(`Could not extract PreviewJXL buffer from image, trying PreviewImage next: ${error}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input);
|
||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
||||
return { buffer, format: RawExtractedFormat.Jpeg, dimensions: await this.getImageDimensions(buffer) };
|
||||
} catch (error: any) {
|
||||
this.logger.debug(`Could not extract preview buffer from image: ${error}`);
|
||||
return null;
|
||||
|
|
@ -269,7 +270,7 @@ export class MediaRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getImageDimensions(input: string | Buffer): Promise<ImageDimensions> {
|
||||
private async getImageDimensions(input: string | Buffer): Promise<ImageDimensions> {
|
||||
const { width = 0, height = 0 } = await sharp(input).metadata();
|
||||
return { width, height };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -599,8 +599,11 @@ describe(MediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should extract embedded image if enabled and available', async () => {
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.media.extract.mockResolvedValue({
|
||||
buffer: extractedBuffer,
|
||||
format: RawExtractedFormat.Jpeg,
|
||||
dimensions: { width: 3840, height: 2160 },
|
||||
});
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
|
||||
|
|
@ -615,8 +618,11 @@ describe(MediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should resize original image if embedded image is too small', async () => {
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 });
|
||||
mocks.media.extract.mockResolvedValue({
|
||||
buffer: extractedBuffer,
|
||||
format: RawExtractedFormat.Jpeg,
|
||||
dimensions: { width: 1000, height: 1000 },
|
||||
});
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
|
||||
|
|
@ -641,7 +647,6 @@ describe(MediaService.name, () => {
|
|||
processInvalidImages: false,
|
||||
size: 1440,
|
||||
});
|
||||
expect(mocks.media.getImageDimensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should resize original image if embedded image extraction is not enabled', async () => {
|
||||
|
|
@ -657,7 +662,6 @@ describe(MediaService.name, () => {
|
|||
processInvalidImages: false,
|
||||
size: 1440,
|
||||
});
|
||||
expect(mocks.media.getImageDimensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should process invalid images if enabled', async () => {
|
||||
|
|
@ -691,7 +695,6 @@ describe(MediaService.name, () => {
|
|||
expect.objectContaining({ processInvalidImages: false }),
|
||||
);
|
||||
|
||||
expect(mocks.media.getImageDimensions).not.toHaveBeenCalled();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
|
|
@ -699,8 +702,11 @@ describe(MediaService.name, () => {
|
|||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true },
|
||||
});
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.media.extract.mockResolvedValue({
|
||||
buffer: extractedBuffer,
|
||||
format: RawExtractedFormat.Jpeg,
|
||||
dimensions: { width: 3840, height: 2160 },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
|
|
@ -731,8 +737,11 @@ describe(MediaService.name, () => {
|
|||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true },
|
||||
});
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.media.extract.mockResolvedValue({
|
||||
buffer: extractedBuffer,
|
||||
format: RawExtractedFormat.Jxl,
|
||||
dimensions: { width: 3840, height: 2160 },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
|
|
@ -771,8 +780,11 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should generate full-size preview directly from RAW images when extractEmbedded is false', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.media.extract.mockResolvedValue({
|
||||
buffer: extractedBuffer,
|
||||
format: RawExtractedFormat.Jpeg,
|
||||
dimensions: { width: 3840, height: 2160 },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
|
|
@ -811,8 +823,11 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should generate full-size preview from non-web-friendly images', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.media.extract.mockResolvedValue({
|
||||
buffer: extractedBuffer,
|
||||
format: RawExtractedFormat.Jpeg,
|
||||
dimensions: { width: 3840, height: 2160 },
|
||||
});
|
||||
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
||||
|
||||
|
|
@ -840,8 +855,11 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should skip generating full-size preview for web-friendly images', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.media.extract.mockResolvedValue({
|
||||
buffer: extractedBuffer,
|
||||
format: RawExtractedFormat.Jpeg,
|
||||
dimensions: { width: 3840, height: 2160 },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
|
|
@ -863,8 +881,11 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should always generate full-size preview from non-web-friendly panoramas', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.media.extract.mockResolvedValue({
|
||||
buffer: extractedBuffer,
|
||||
format: RawExtractedFormat.Jpeg,
|
||||
dimensions: { width: 3840, height: 2160 },
|
||||
});
|
||||
mocks.metadata.readTags.mockResolvedValue({
|
||||
ProjectionType: 'equirectangular',
|
||||
PoseHeadingDegrees: 127,
|
||||
|
|
@ -913,8 +934,11 @@ describe(MediaService.name, () => {
|
|||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { fullsize: { enabled: true, format: ImageFormat.Webp, quality: 90 } },
|
||||
});
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.media.extract.mockResolvedValue({
|
||||
buffer: extractedBuffer,
|
||||
format: RawExtractedFormat.Jpeg,
|
||||
dimensions: { width: 3840, height: 2160 },
|
||||
});
|
||||
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
||||
|
||||
|
|
@ -1199,9 +1223,8 @@ describe(MediaService.name, () => {
|
|||
const extracted = Buffer.from('');
|
||||
const data = Buffer.from('');
|
||||
const info = { width: 2160, height: 3840 } as OutputInfo;
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg, dimensions: info });
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
mocks.media.getImageDimensions.mockResolvedValue(info);
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
|
|
@ -1277,8 +1300,7 @@ describe(MediaService.name, () => {
|
|||
const data = Buffer.from('');
|
||||
const info = { width: 1000, height: 1000 } as OutputInfo;
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue(info);
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg, dimensions: info });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ export class MediaService extends BaseService {
|
|||
|
||||
private async extractImage(originalPath: string, minSize: number) {
|
||||
let extracted = await this.mediaRepository.extract(originalPath);
|
||||
if (extracted && !(await this.shouldUseExtractedImage(extracted.buffer, minSize))) {
|
||||
if (extracted && !this.shouldUseExtractedImage(extracted.dimensions, minSize)) {
|
||||
extracted = null;
|
||||
}
|
||||
|
||||
|
|
@ -313,14 +313,21 @@ export class MediaService extends BaseService {
|
|||
];
|
||||
|
||||
let fullsizePath: string | undefined;
|
||||
let fullsizeSize: number | undefined;
|
||||
const originalSize =
|
||||
asset.exifInfo.exifImageWidth && asset.exifInfo.exifImageHeight
|
||||
? Math.min(asset.exifInfo.exifImageWidth, asset.exifInfo.exifImageHeight)
|
||||
: undefined;
|
||||
|
||||
if (convertFullsize) {
|
||||
// convert a new fullsize image from the same source as the thumbnail
|
||||
fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FullSize, image.fullsize.format);
|
||||
fullsizeSize = originalSize;
|
||||
const fullsizeOptions = { format: image.fullsize.format, quality: image.fullsize.quality, ...thumbnailOptions };
|
||||
promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath));
|
||||
} else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.Jpeg) {
|
||||
fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FullSize, extracted.format);
|
||||
fullsizeSize = Math.min(extracted.dimensions.width, extracted.dimensions.height);
|
||||
this.storageCore.ensureFolders(fullsizePath);
|
||||
|
||||
// Write the buffer to disk with essential EXIF data
|
||||
|
|
@ -336,14 +343,14 @@ export class MediaService extends BaseService {
|
|||
|
||||
const outputs = await Promise.all(promises);
|
||||
|
||||
const originalSize = asset.exifInfo.exifImageHeight;
|
||||
if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR' && originalSize) {
|
||||
this.copyPanoramaMetadataToThumbnails(
|
||||
await this.copyPanoramaMetadataToThumbnails(
|
||||
asset.originalPath,
|
||||
originalSize,
|
||||
previewPath,
|
||||
image.preview.size,
|
||||
fullsizePath,
|
||||
fullsizeSize,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -356,6 +363,7 @@ export class MediaService extends BaseService {
|
|||
previewPath: string,
|
||||
previewSize: number,
|
||||
fullsizePath?: string,
|
||||
fullsizeSize?: number,
|
||||
) {
|
||||
const originalTags = await this.metadataRepository.readTags(originalPath);
|
||||
|
||||
|
|
@ -379,7 +387,7 @@ export class MediaService extends BaseService {
|
|||
const promises = [
|
||||
// preview size is min(preview size, original size) so do the same for pano pixel adjustment
|
||||
scaleAndWriteData(previewPath, Math.min(1, previewSize / originalSize)),
|
||||
fullsizePath ? scaleAndWriteData(fullsizePath, 1) : Promise.resolve(),
|
||||
fullsizePath && fullsizeSize ? scaleAndWriteData(fullsizePath, fullsizeSize / originalSize) : Promise.resolve(),
|
||||
];
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
|
@ -733,8 +741,8 @@ export class MediaService extends BaseService {
|
|||
}
|
||||
}
|
||||
|
||||
private async shouldUseExtractedImage(extractedPathOrBuffer: string | Buffer, targetSize: number) {
|
||||
const { width, height } = await this.mediaRepository.getImageDimensions(extractedPathOrBuffer);
|
||||
private shouldUseExtractedImage(extractedDimensions: ImageDimensions, targetSize: number) {
|
||||
const { width, height } = extractedDimensions;
|
||||
const extractedSize = Math.min(width, height);
|
||||
return extractedSize >= targetSize;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -898,6 +898,7 @@ export const assetStub = {
|
|||
fileSizeInByte: 5000,
|
||||
projectionType: 'EQUIRECTANGULAR',
|
||||
exifImageHeight: 2160,
|
||||
exifImageWidth: 3840,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,5 @@ export const newMediaRepositoryMock = (): Mocked<RepositoryInterface<MediaReposi
|
|||
extract: vitest.fn().mockResolvedValue(null),
|
||||
probe: vitest.fn(),
|
||||
transcode: vitest.fn(),
|
||||
getImageDimensions: vitest.fn(),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue