diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index ac5ea101e..f3a36510f 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -483,6 +483,12 @@ export interface AssetBulkUpdateDto { * @memberof AssetBulkUpdateDto */ 'longitude'?: number; + /** + * + * @type {number} + * @memberof AssetBulkUpdateDto + */ + 'orientation'?: number; /** * * @type {boolean} @@ -4191,6 +4197,12 @@ export interface UpdateAssetDto { * @memberof UpdateAssetDto */ 'longitude'?: number; + /** + * + * @type {number} + * @memberof UpdateAssetDto + */ + 'orientation'?: number; } /** * diff --git a/mobile/openapi/doc/AssetBulkUpdateDto.md b/mobile/openapi/doc/AssetBulkUpdateDto.md index 40ebe6a41..7471a4622 100644 --- a/mobile/openapi/doc/AssetBulkUpdateDto.md +++ b/mobile/openapi/doc/AssetBulkUpdateDto.md @@ -14,6 +14,7 @@ Name | Type | Description | Notes **isFavorite** | **bool** | | [optional] **latitude** | **num** | | [optional] **longitude** | **num** | | [optional] +**orientation** | **num** | | [optional] **removeParent** | **bool** | | [optional] **stackParentId** | **String** | | [optional] diff --git a/mobile/openapi/doc/UpdateAssetDto.md b/mobile/openapi/doc/UpdateAssetDto.md index cfd8f604d..6e67b3c98 100644 --- a/mobile/openapi/doc/UpdateAssetDto.md +++ b/mobile/openapi/doc/UpdateAssetDto.md @@ -14,6 +14,7 @@ Name | Type | Description | Notes **isFavorite** | **bool** | | [optional] **latitude** | **num** | | [optional] **longitude** | **num** | | [optional] +**orientation** | **num** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index 60cab8c74..56b7b330d 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -19,6 +19,7 @@ class AssetBulkUpdateDto { this.isFavorite, this.latitude, this.longitude, + this.orientation, this.removeParent, this.stackParentId, }); @@ -65,6 +66,14 @@ class AssetBulkUpdateDto { /// num? longitude; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? orientation; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -89,6 +98,7 @@ class AssetBulkUpdateDto { other.isFavorite == isFavorite && other.latitude == latitude && other.longitude == longitude && + other.orientation == orientation && other.removeParent == removeParent && other.stackParentId == stackParentId; @@ -101,11 +111,12 @@ class AssetBulkUpdateDto { (isFavorite == null ? 0 : isFavorite!.hashCode) + (latitude == null ? 0 : latitude!.hashCode) + (longitude == null ? 0 : longitude!.hashCode) + + (orientation == null ? 0 : orientation!.hashCode) + (removeParent == null ? 0 : removeParent!.hashCode) + (stackParentId == null ? 0 : stackParentId!.hashCode); @override - String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, removeParent=$removeParent, stackParentId=$stackParentId]'; + String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, orientation=$orientation, removeParent=$removeParent, stackParentId=$stackParentId]'; Map toJson() { final json = {}; @@ -135,6 +146,11 @@ class AssetBulkUpdateDto { } else { // json[r'longitude'] = null; } + if (this.orientation != null) { + json[r'orientation'] = this.orientation; + } else { + // json[r'orientation'] = null; + } if (this.removeParent != null) { json[r'removeParent'] = this.removeParent; } else { @@ -168,6 +184,9 @@ class AssetBulkUpdateDto { longitude: json[r'longitude'] == null ? null : num.parse(json[r'longitude'].toString()), + orientation: json[r'orientation'] == null + ? null + : num.parse(json[r'orientation'].toString()), removeParent: mapValueOfType(json, r'removeParent'), stackParentId: mapValueOfType(json, r'stackParentId'), ); diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index d90b365b7..911592a8d 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -19,6 +19,7 @@ class UpdateAssetDto { this.isFavorite, this.latitude, this.longitude, + this.orientation, }); /// @@ -69,6 +70,14 @@ class UpdateAssetDto { /// num? longitude; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? orientation; + @override bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto && other.dateTimeOriginal == dateTimeOriginal && @@ -76,7 +85,8 @@ class UpdateAssetDto { other.isArchived == isArchived && other.isFavorite == isFavorite && other.latitude == latitude && - other.longitude == longitude; + other.longitude == longitude && + other.orientation == orientation; @override int get hashCode => @@ -86,10 +96,11 @@ class UpdateAssetDto { (isArchived == null ? 0 : isArchived!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (latitude == null ? 0 : latitude!.hashCode) + - (longitude == null ? 0 : longitude!.hashCode); + (longitude == null ? 0 : longitude!.hashCode) + + (orientation == null ? 0 : orientation!.hashCode); @override - String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude]'; + String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, orientation=$orientation]'; Map toJson() { final json = {}; @@ -123,6 +134,11 @@ class UpdateAssetDto { } else { // json[r'longitude'] = null; } + if (this.orientation != null) { + json[r'orientation'] = this.orientation; + } else { + // json[r'orientation'] = null; + } return json; } @@ -144,6 +160,9 @@ class UpdateAssetDto { longitude: json[r'longitude'] == null ? null : num.parse(json[r'longitude'].toString()), + orientation: json[r'orientation'] == null + ? null + : num.parse(json[r'orientation'].toString()), ); } return null; diff --git a/mobile/openapi/test/asset_bulk_update_dto_test.dart b/mobile/openapi/test/asset_bulk_update_dto_test.dart index d04bdd809..757022086 100644 --- a/mobile/openapi/test/asset_bulk_update_dto_test.dart +++ b/mobile/openapi/test/asset_bulk_update_dto_test.dart @@ -46,6 +46,11 @@ void main() { // TODO }); + // num orientation + test('to test the property `orientation`', () async { + // TODO + }); + // bool removeParent test('to test the property `removeParent`', () async { // TODO diff --git a/mobile/openapi/test/update_asset_dto_test.dart b/mobile/openapi/test/update_asset_dto_test.dart index 9d9874beb..6820b15ec 100644 --- a/mobile/openapi/test/update_asset_dto_test.dart +++ b/mobile/openapi/test/update_asset_dto_test.dart @@ -46,6 +46,11 @@ void main() { // TODO }); + // num orientation + test('to test the property `orientation`', () async { + // TODO + }); + }); diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 356399813..19ac63be9 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -6471,6 +6471,9 @@ "longitude": { "type": "number" }, + "orientation": { + "type": "number" + }, "removeParent": { "type": "boolean" }, @@ -9369,6 +9372,9 @@ }, "longitude": { "type": "number" + }, + "orientation": { + "type": "number" } }, "type": "object" diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index c547d6a6d..9e407d582 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -393,8 +393,8 @@ export class AssetService { async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise { await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id); - const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto; - await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude }); + const { description, dateTimeOriginal, latitude, longitude, orientation, ...rest } = dto; + await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, orientation }); const asset = await this.assetRepository.save({ id, ...rest }); await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [id] } }); @@ -402,7 +402,7 @@ export class AssetService { } async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise { - const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto; + const { ids, removeParent, dateTimeOriginal, latitude, longitude, orientation, ...options } = dto; await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids); if (removeParent) { @@ -423,7 +423,7 @@ export class AssetService { } for (const id of ids) { - await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); + await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude, orientation }); } await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } }); @@ -591,8 +591,9 @@ export class AssetService { } private async updateMetadata(dto: ISidecarWriteJob) { - const { id, description, dateTimeOriginal, latitude, longitude } = dto; - const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude }, _.isUndefined); + const { id, description, dateTimeOriginal, latitude, longitude, orientation } = dto; + const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, orientation }, _.isUndefined); + console.log('updatemetadta', orientation); if (Object.keys(writes).length > 0) { await this.assetRepository.upsertExif({ assetId: id, ...writes }); await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } }); diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts index ac50f2242..6cc5ec5bb 100644 --- a/server/src/domain/asset/dto/asset.dto.ts +++ b/server/src/domain/asset/dto/asset.dto.ts @@ -202,6 +202,11 @@ export class AssetBulkUpdateDto extends BulkIdsDto { @IsLongitude() @IsNotEmpty() longitude?: number; + + @Optional() + @IsInt() + @Type(() => Number) + orientation?: number; } export class UpdateAssetDto { @@ -230,6 +235,11 @@ export class UpdateAssetDto { @IsLongitude() @IsNotEmpty() longitude?: number; + + @Optional() + @IsInt() + @Type(() => Number) + orientation?: number; } export class RandomAssetsDto { diff --git a/server/src/domain/job/job.interface.ts b/server/src/domain/job/job.interface.ts index be76f6645..74c2db1ce 100644 --- a/server/src/domain/job/job.interface.ts +++ b/server/src/domain/job/job.interface.ts @@ -39,4 +39,5 @@ export interface ISidecarWriteJob extends IEntityJob { dateTimeOriginal?: string; latitude?: number; longitude?: number; + orientation?: number; } diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index e9c7ff931..c266438bd 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -245,7 +245,7 @@ export class MetadataService { } async handleSidecarWrite(job: ISidecarWriteJob) { - const { id, description, dateTimeOriginal, latitude, longitude } = job; + const { id, description, dateTimeOriginal, latitude, longitude, orientation } = job; const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { return false; @@ -258,6 +258,7 @@ export class MetadataService { CreationDate: dateTimeOriginal, GPSLatitude: latitude, GPSLongitude: longitude, + Orientation: orientation, }, _.isUndefined, ); diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/domain/repositories/metadata.repository.ts index e8d4d1e4e..47408b12d 100644 --- a/server/src/domain/repositories/metadata.repository.ts +++ b/server/src/domain/repositories/metadata.repository.ts @@ -27,6 +27,7 @@ export interface ImmichTags extends Omit { ImagePixelDepth?: string; FocalLength?: number; Duration?: number | ExifDuration; + Orientation?: number; } export interface IMetadataRepository { diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index ac5ea101e..f3a36510f 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -483,6 +483,12 @@ export interface AssetBulkUpdateDto { * @memberof AssetBulkUpdateDto */ 'longitude'?: number; + /** + * + * @type {number} + * @memberof AssetBulkUpdateDto + */ + 'orientation'?: number; /** * * @type {boolean} @@ -4191,6 +4197,12 @@ export interface UpdateAssetDto { * @memberof UpdateAssetDto */ 'longitude'?: number; + /** + * + * @type {number} + * @memberof UpdateAssetDto + */ + 'orientation'?: number; } /** * diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index f848e6caa..2d45dbb52 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -55,7 +55,7 @@ export let album: AlbumResponseDto | null = null; let reactions: ActivityResponseDto[] = []; - let rotatePhotoviewer: () => void; + let rotatePhotoviewer: () => Promise; const { setAssetId } = assetViewingStore; const { restartProgress: restartSlideshowProgress, diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index f92bfbefc..aad6d9dce 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -6,13 +6,56 @@ import { notificationController, NotificationType } from '../shared-components/notification/notification'; import { useZoomImageWheel } from '@zoom-image/svelte'; import { photoZoomState } from '$lib/stores/zoom-image.store'; - import { isWebCompatibleImage } from '$lib/utils/asset-utils'; + import { getAssetRatio, isWebCompatibleImage } from '$lib/utils/asset-utils'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; + import { handleError } from '$lib/utils/handle-error'; export let asset: AssetResponseDto; export let element: HTMLDivElement | undefined = undefined; export let haveFadeTransition = true; - export const rotate = () => setZoomImageWheelState({ currentRotation: $zoomImageWheelState.currentRotation - 90 }); + + const getRotation = (value: string): number => { + switch (value) { + case '1': + return 0; + case '3': + return 180; + case '6': + return 90; + case '8': + return 270; + default: + return 0; + } + }; + + const getRotationString = (rotation: number): number => { + switch (rotation % 360) { + case 0: + return 1; + case 90: + return 6; + case 180: + return 3; + case 270: + return 8; + default: + return 1; + } + }; + + export const rotate = async () => { + setZoomImageWheelState({ currentRotation: $zoomImageWheelState.currentRotation - 90 }); + console.log(getRotationString($zoomImageWheelState.currentRotation)); + try { + await api.assetApi.updateAsset({ + id: asset.id, + updateAssetDto: { orientation: getRotationString($zoomImageWheelState.currentRotation) }, + }); + } catch (error) { + handleError(error, 'Unable to change orientation'); + } + }; let imgElement: HTMLDivElement; let assetData: string; @@ -115,6 +158,16 @@ maxZoom: 10, wheelZoomRatio: 0.2, }); + if (asset.exifInfo?.orientation) { + const { width, height } = getAssetRatio(asset); + if (width > height && parseInt(asset.exifInfo?.orientation) != 1) { + setZoomImageWheelState({ currentRotation: getRotation(asset.exifInfo?.orientation) }); + } + + if (width < height && parseInt(asset.exifInfo?.orientation) != 6) { + setZoomImageWheelState({ currentRotation: getRotation(asset.exifInfo?.orientation) }); + } + } }