1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-28 22:51:59 +00:00

refactor: orientation

This commit is contained in:
Jason Rasmussen 2024-09-19 11:26:54 -04:00
parent 0b02fda4e0
commit 8af7a9c1b9
No known key found for this signature in database
GPG key ID: 2EF24B77EAFA4A41
13 changed files with 134 additions and 44 deletions

View file

@ -1052,7 +1052,7 @@ describe('/asset', () => {
dateTimeOriginal: '2010-07-20T17:27:12.000Z', dateTimeOriginal: '2010-07-20T17:27:12.000Z',
latitude: null, latitude: null,
longitude: null, longitude: null,
orientation: '1', orientation: 1,
}, },
}, },
}, },

Binary file not shown.

View file

@ -9074,8 +9074,18 @@
}, },
"orientation": { "orientation": {
"default": null, "default": null,
"enum": [
1,
2,
8,
7,
3,
4,
6,
5
],
"nullable": true, "nullable": true,
"type": "string" "type": "number"
}, },
"projectionType": { "projectionType": {
"default": null, "default": null,

View file

@ -195,7 +195,7 @@ export type ExifResponseDto = {
make?: string | null; make?: string | null;
model?: string | null; model?: string | null;
modifyDate?: string | null; modifyDate?: string | null;
orientation?: string | null; orientation?: Orientation | null;
projectionType?: string | null; projectionType?: string | null;
rating?: number | null; rating?: number | null;
state?: string | null; state?: string | null;
@ -3266,6 +3266,16 @@ export enum AlbumUserRole {
Editor = "editor", Editor = "editor",
Viewer = "viewer" Viewer = "viewer"
} }
export enum Orientation {
$1 = 1,
$2 = 2,
$8 = 8,
$7 = 7,
$3 = 3,
$4 = 4,
$6 = 6,
$5 = 5
}
export enum SourceType { export enum SourceType {
MachineLearning = "machine-learning", MachineLearning = "machine-learning",
Exif = "exif" Exif = "exif"

View file

@ -1,4 +1,5 @@
import { defaults } from './fetch-client.js'; import { defaults } from './fetch-client.js';
export { Orientation as LameGeneratedOrientation } from './fetch-client.js';
export * from './fetch-client.js'; export * from './fetch-client.js';
export * from './fetch-errors.js'; export * from './fetch-errors.js';
@ -8,6 +9,17 @@ export interface InitOptions {
apiKey: string; apiKey: string;
} }
export enum Orientation {
Rotate0 = 1,
Rotate0Mirrored = 2,
Rotate90 = 8,
Rotate90Mirrored = 7,
Rotate180 = 3,
Rotate180Mirrored = 4,
Rotate270 = 6,
Rotate270Mirrored = 5,
}
export const init = ({ baseUrl, apiKey }: InitOptions) => { export const init = ({ baseUrl, apiKey }: InitOptions) => {
setBaseUrl(baseUrl); setBaseUrl(baseUrl);
setApiKey(apiKey); setApiKey(apiKey);

View file

@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { Orientation } from 'src/enum';
export class ExifResponseDto { export class ExifResponseDto {
make?: string | null = null; make?: string | null = null;
@ -9,7 +10,9 @@ export class ExifResponseDto {
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
fileSizeInByte?: number | null = null; fileSizeInByte?: number | null = null;
orientation?: string | null = null;
@ApiProperty({ enum: Orientation })
orientation?: Orientation | null = null;
dateTimeOriginal?: Date | null = null; dateTimeOriginal?: Date | null = null;
modifyDate?: Date | null = null; modifyDate?: Date | null = null;
timeZone?: string | null = null; timeZone?: string | null = null;

View file

@ -1,4 +1,5 @@
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { Orientation } from 'src/enum';
import { Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; import { Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
import { Column } from 'typeorm/decorator/columns/Column.js'; import { Column } from 'typeorm/decorator/columns/Column.js';
import { Entity } from 'typeorm/decorator/entity/Entity.js'; import { Entity } from 'typeorm/decorator/entity/Entity.js';
@ -25,8 +26,8 @@ export class ExifEntity {
@Column({ type: 'bigint', nullable: true }) @Column({ type: 'bigint', nullable: true })
fileSizeInByte!: number | null; fileSizeInByte!: number | null;
@Column({ type: 'varchar', nullable: true }) @Column({ type: 'enum', enum: Orientation, nullable: true })
orientation!: string | null; orientation!: Orientation | null;
@Column({ type: 'timestamptz', nullable: true }) @Column({ type: 'timestamptz', nullable: true })
dateTimeOriginal!: Date | null; dateTimeOriginal!: Date | null;

View file

@ -198,3 +198,14 @@ export enum ManualJobName {
TAG_CLEANUP = 'tag-cleanup', TAG_CLEANUP = 'tag-cleanup',
USER_CLEANUP = 'user-cleanup', USER_CLEANUP = 'user-cleanup',
} }
export enum Orientation {
Rotate0 = 1,
Rotate0Mirrored = 2,
Rotate90 = 8,
Rotate90Mirrored = 7,
Rotate180 = 3,
Rotate180Mirrored = 4,
Rotate270 = 6,
Rotate270Mirrored = 5,
}

View file

@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddOrientationEnum1726754669860 implements MigrationInterface {
name = 'AddOrientationEnum1726754669860'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "exif_orientation_enum" AS ENUM('1', '2', '3', '4', '5', '6', '7', '8')`)
await queryRunner.query(`
UPDATE "exif" SET "orientation" = CASE
WHEN "orientation" = '0' THEN '1'
WHEN "orientation" = '1' THEN '1'
WHEN "orientation" = '2' THEN '2'
WHEN "orientation" = '3' THEN '3'
WHEN "orientation" = '4' THEN '4'
WHEN "orientation" = '5' THEN '5'
WHEN "orientation" = '6' THEN '6'
WHEN "orientation" = '7' THEN '7'
WHEN "orientation" = '8' THEN '8'
WHEN "orientation" = '-90' THEN '6'
WHEN "orientation" = '90' THEN '8'
WHEN "orientation" = '180' THEN '3'
ELSE NULL
END`);
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "orientation" TYPE "exif_orientation_enum" USING "orientation"::"exif_orientation_enum"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "orientation" TYPE character varying USING "orientation"::text`);
await queryRunner.query(`DROP TYPE "exif_orientation_enum"`);
}
}

View file

@ -3,7 +3,7 @@ import { randomBytes } from 'node:crypto';
import { Stats } from 'node:fs'; import { Stats } from 'node:fs';
import { constants } from 'node:fs/promises'; import { constants } from 'node:fs/promises';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { AssetType, SourceType } from 'src/enum'; import { AssetType, Orientation, SourceType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@ -20,7 +20,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface'; import { ITagRepository } from 'src/interfaces/tag.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { MetadataService, Orientation } from 'src/services/metadata.service'; import { MetadataService } from 'src/services/metadata.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { fileStub } from 'test/fixtures/file.stub'; import { fileStub } from 'test/fixtures/file.stub';
import { probeStub } from 'test/fixtures/media.stub'; import { probeStub } from 'test/fixtures/media.stub';
@ -537,9 +537,7 @@ describe(MetadataService.name, () => {
await sut.handleMetadataExtraction({ id: assetStub.video.id }); await sut.handleMetadataExtraction({ id: assetStub.video.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
expect(assetMock.upsertExif).toHaveBeenCalledWith( expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ orientation: Orientation.Rotate90 }));
expect.objectContaining({ orientation: Orientation.Rotate270CW.toString() }),
);
}); });
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => { it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
@ -786,7 +784,7 @@ describe(MetadataService.name, () => {
Make: 'test-factory', Make: 'test-factory',
Model: "'mockel'", Model: "'mockel'",
ModifyDate: ExifDateTime.fromISO(dateForTest.toISOString()), ModifyDate: ExifDateTime.fromISO(dateForTest.toISOString()),
Orientation: 0, Orientation: 1,
ProfileDescription: 'extensive description', ProfileDescription: 'extensive description',
ProjectionType: 'equirectangular', ProjectionType: 'equirectangular',
tz: 'UTC-11:30', tz: 'UTC-11:30',
@ -819,7 +817,7 @@ describe(MetadataService.name, () => {
make: tags.Make, make: tags.Make,
model: tags.Model, model: tags.Model,
modifyDate: expect.any(Date), modifyDate: expect.any(Date),
orientation: tags.Orientation?.toString(), orientation: 1,
profileDescription: tags.ProfileDescription, profileDescription: tags.ProfileDescription,
projectionType: 'EQUIRECTANGULAR', projectionType: 'EQUIRECTANGULAR',
timeZone: tags.tz, timeZone: tags.tz,

View file

@ -12,7 +12,7 @@ import { OnEmit } from 'src/decorators';
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { AssetType, SourceType } from 'src/enum'; import { AssetType, Orientation, SourceType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@ -54,17 +54,6 @@ const EXIF_DATE_TAGS: Array<keyof Tags> = [
'DateTimeCreated', 'DateTimeCreated',
]; ];
export enum Orientation {
Horizontal = 1,
MirrorHorizontal = 2,
Rotate180 = 3,
MirrorVertical = 4,
MirrorHorizontalRotate270CW = 5,
Rotate90CW = 6,
MirrorHorizontalRotate90CW = 7,
Rotate270CW = 8,
}
const validate = <T>(value: T): NonNullable<T> | null => { const validate = <T>(value: T): NonNullable<T> | null => {
// handle lists of numbers // handle lists of numbers
if (Array.isArray(value)) { if (Array.isArray(value)) {
@ -243,7 +232,7 @@ export class MetadataService {
fileSizeInByte: stats.size, fileSizeInByte: stats.size,
exifImageHeight: validate(exifTags.ImageHeight), exifImageHeight: validate(exifTags.ImageHeight),
exifImageWidth: validate(exifTags.ImageWidth), exifImageWidth: validate(exifTags.ImageWidth),
orientation: validate(exifTags.Orientation)?.toString() ?? null, orientation: this.getOrientation(asset.id, exifTags.Orientation),
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
bitsPerSample: this.getBitsPerSample(exifTags), bitsPerSample: this.getBitsPerSample(exifTags),
colorspace: exifTags.ColorSpace ?? null, colorspace: exifTags.ColorSpace ?? null,
@ -669,9 +658,32 @@ export class MetadataService {
return tags.BurstID ?? tags.BurstUUID ?? tags.CameraBurstID ?? tags.MediaUniqueID ?? null; return tags.BurstID ?? tags.BurstUUID ?? tags.CameraBurstID ?? tags.MediaUniqueID ?? null;
} }
private getOrientation(id: string, orientation: ImmichTags['Orientation']) {
if (!orientation) {
return;
}
switch (orientation) {
case Orientation.Rotate0:
case Orientation.Rotate0Mirrored:
case Orientation.Rotate90:
case Orientation.Rotate90Mirrored:
case Orientation.Rotate180:
case Orientation.Rotate180Mirrored:
case Orientation.Rotate270:
case Orientation.Rotate270Mirrored: {
return orientation;
}
}
this.logger.warn(`Asset ${id} has unknown orientation: "${orientation}", setting to null`);
return null;
}
private getBitsPerSample(tags: ImmichTags): number | null { private getBitsPerSample(tags: ImmichTags): number | null {
const bitDepthTags = [ const bitDepthTags = [
tags.BitsPerSample, tags.Rotation,
tags.ComponentBitDepth, tags.ComponentBitDepth,
tags.ImagePixelDepth, tags.ImagePixelDepth,
tags.BitDepth, tags.BitDepth,
@ -695,15 +707,15 @@ export class MetadataService {
if (videoStreams[0]) { if (videoStreams[0]) {
switch (videoStreams[0].rotation) { switch (videoStreams[0].rotation) {
case -90: { case -90: {
tags.Orientation = Orientation.Rotate90CW; tags.Orientation = Orientation.Rotate270;
break; break;
} }
case 0: { case 0: {
tags.Orientation = Orientation.Horizontal; tags.Orientation = Orientation.Rotate0;
break; break;
} }
case 90: { case 90: {
tags.Orientation = Orientation.Rotate270CW; tags.Orientation = Orientation.Rotate90;
break; break;
} }
case 180: { case 180: {

View file

@ -5,7 +5,7 @@ import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
import { mapUser } from 'src/dtos/user.dto'; import { mapUser } from 'src/dtos/user.dto';
import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum'; import { AssetOrder, AssetStatus, AssetType, Orientation, SharedLinkType } from 'src/enum';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
@ -27,7 +27,7 @@ const assetInfo: ExifResponseDto = {
exifImageWidth: 500, exifImageWidth: 500,
exifImageHeight: 500, exifImageHeight: 500,
fileSizeInByte: 100, fileSizeInByte: 100,
orientation: 'orientation', orientation: Orientation.Rotate0,
dateTimeOriginal: today, dateTimeOriginal: today,
modifyDate: today, modifyDate: today,
timeZone: 'America/Los_Angeles', timeZone: 'America/Los_Angeles',
@ -227,7 +227,7 @@ export const sharedLinkStub = {
exifImageWidth: 500, exifImageWidth: 500,
exifImageHeight: 500, exifImageHeight: 500,
fileSizeInByte: 100, fileSizeInByte: 100,
orientation: 'orientation', orientation: Orientation.Rotate0,
dateTimeOriginal: today, dateTimeOriginal: today,
modifyDate: today, modifyDate: today,
timeZone: 'America/Los_Angeles', timeZone: 'America/Los_Angeles',

View file

@ -1,6 +1,6 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte'; import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
@ -19,6 +19,8 @@ import {
getBaseUrl, getBaseUrl,
getDownloadInfo, getDownloadInfo,
getStack, getStack,
LameGeneratedOrientation,
Orientation,
tagAssets as tagAllAssets, tagAssets as tagAllAssets,
untagAssets, untagAssets,
updateAsset, updateAsset,
@ -290,17 +292,17 @@ export function getAssetFilename(asset: AssetResponseDto): string {
return `${asset.originalFileName}.${fileExtension}`; return `${asset.originalFileName}.${fileExtension}`;
} }
function isRotated90CW(orientation: number) { export function isFlipped(orientation?: LameGeneratedOrientation | null) {
return orientation === 5 || orientation === 6 || orientation === 90; if (!orientation) {
} return false;
}
function isRotated270CW(orientation: number) { return [
return orientation === 7 || orientation === 8 || orientation === -90; Orientation.Rotate90,
} Orientation.Rotate90Mirrored,
Orientation.Rotate270,
export function isFlipped(orientation?: string | null) { Orientation.Rotate270Mirrored,
const value = Number(orientation); ].includes(orientation as unknown as Orientation);
return value && (isRotated270CW(value) || isRotated90CW(value));
} }
export function getFileSize(asset: AssetResponseDto): string { export function getFileSize(asset: AssetResponseDto): string {