feat: image editing (#24155)

This commit is contained in:
Brandon Wees
2026-01-09 17:59:52 -05:00
committed by GitHub
parent 76241a7b2b
commit e8c80d88a5
141 changed files with 7836 additions and 1634 deletions
+209 -22
View File
@@ -1,43 +1,61 @@
import { AssetFace, AssetFile, Exif } from 'src/database';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto';
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { StorageAsset } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub';
import { userStub } from 'test/fixtures/user.stub';
import { factory } from 'test/small.factory';
export const previewFile: AssetFile = {
id: 'file-1',
type: AssetFileType.Preview,
path: '/uploads/user-id/thumbs/path.jpg',
};
export const previewFile = factory.assetFile({ type: AssetFileType.Preview });
const thumbnailFile: AssetFile = {
id: 'file-2',
const thumbnailFile = factory.assetFile({
type: AssetFileType.Thumbnail,
path: '/uploads/user-id/webp/path.ext',
};
});
const fullsizeFile: AssetFile = {
id: 'file-3',
const fullsizeFile = factory.assetFile({
type: AssetFileType.FullSize,
path: '/uploads/user-id/fullsize/path.webp',
};
});
const sidecarFileWithExt: AssetFile = {
id: 'sidecar-with-ext',
const sidecarFileWithExt = factory.assetFile({
type: AssetFileType.Sidecar,
path: '/original/path.ext.xmp',
};
});
const sidecarFileWithoutExt: AssetFile = {
id: 'sidecar-without-ext',
const sidecarFileWithoutExt = factory.assetFile({
type: AssetFileType.Sidecar,
path: '/original/path.xmp',
};
});
const editedPreviewFile = factory.assetFile({
type: AssetFileType.PreviewEdited,
path: '/uploads/user-id/preview/path_edited.jpg',
});
const editedThumbnailFile = factory.assetFile({
type: AssetFileType.ThumbnailEdited,
path: '/uploads/user-id/thumbnail/path_edited.jpg',
});
const editedFullsizeFile = factory.assetFile({
type: AssetFileType.FullSizeEdited,
path: '/uploads/user-id/fullsize/path_edited.jpg',
});
const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile];
const editedFiles: AssetFile[] = [
fullsizeFile,
previewFile,
thumbnailFile,
editedFullsizeFile,
editedPreviewFile,
editedThumbnailFile,
];
export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => {
return {
id: stackId,
@@ -104,6 +122,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
noWebpPath: Object.freeze({
@@ -142,6 +163,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
noThumbhash: Object.freeze({
@@ -177,6 +201,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
primaryImage: Object.freeze({
@@ -222,6 +249,9 @@ export const assetStub = {
updateId: '42',
libraryId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
image: Object.freeze({
@@ -264,9 +294,10 @@ export const assetStub = {
stack: null,
orientation: '',
projectionType: null,
height: 3840,
width: 2160,
height: null,
width: null,
visibility: AssetVisibility.Timeline,
edits: [],
}),
trashed: Object.freeze({
@@ -307,6 +338,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
trashedOffline: Object.freeze({
@@ -347,6 +381,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
archived: Object.freeze({
id: 'asset-id',
@@ -386,6 +423,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
external: Object.freeze({
@@ -425,6 +465,9 @@ export const assetStub = {
stackId: null,
stack: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
image1: Object.freeze({
@@ -464,6 +507,9 @@ export const assetStub = {
libraryId: null,
stack: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
imageFrom2015: Object.freeze({
@@ -502,6 +548,9 @@ export const assetStub = {
duplicateId: null,
isOffline: false,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
video: Object.freeze({
@@ -542,6 +591,9 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
livePhotoMotionAsset: Object.freeze({
@@ -559,7 +611,10 @@ export const assetStub = {
files: [] as AssetFile[],
libraryId: null,
visibility: AssetVisibility.Hidden,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }),
width: null,
height: null,
edits: [] as AssetEditActionItem[],
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif; edits: AssetEditActionItem[] }),
livePhotoStillAsset: Object.freeze({
id: 'live-photo-still-asset',
@@ -577,7 +632,10 @@ export const assetStub = {
files,
faces: [] as AssetFace[],
visibility: AssetVisibility.Timeline,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[] }),
width: null,
height: null,
edits: [] as AssetEditActionItem[],
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
livePhotoWithOriginalFileName: Object.freeze({
id: 'live-photo-still-asset',
@@ -597,7 +655,10 @@ export const assetStub = {
libraryId: null,
faces: [] as AssetFace[],
visibility: AssetVisibility.Timeline,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[] }),
width: null,
height: null,
edits: [] as AssetEditActionItem[],
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
withLocation: Object.freeze({
id: 'asset-with-favorite-id',
@@ -641,6 +702,9 @@ export const assetStub = {
isOffline: false,
tags: [],
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
sidecar: Object.freeze({
@@ -676,6 +740,9 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
sidecarWithoutExt: Object.freeze({
@@ -708,6 +775,9 @@ export const assetStub = {
duplicateId: null,
isOffline: false,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
hasEncodedVideo: Object.freeze({
@@ -747,6 +817,9 @@ export const assetStub = {
stackId: null,
stack: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
hasFileExtension: Object.freeze({
@@ -783,6 +856,9 @@ export const assetStub = {
duplicateId: null,
isOffline: false,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
imageDng: Object.freeze({
@@ -823,6 +899,9 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
imageHif: Object.freeze({
@@ -863,6 +942,9 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
panoramaTif: Object.freeze({
id: 'asset-id',
@@ -902,5 +984,110 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
withCropEdit: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
files,
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2025-01-01T01:02:03.456Z'),
isFavorite: true,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
updateId: 'foo',
libraryId: null,
stackId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
deletedAt: null,
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as Exif,
duplicateId: null,
isOffline: false,
stack: null,
orientation: '',
projectionType: null,
height: 3840,
width: 2160,
visibility: AssetVisibility.Timeline,
edits: [
{
action: AssetEditAction.Crop,
parameters: {
width: 1512,
height: 1152,
x: 216,
y: 1512,
},
},
] as AssetEditActionItem[],
}),
withoutEdits: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
files: editedFiles,
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2025-01-01T01:02:03.456Z'),
isFavorite: true,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
updateId: 'foo',
libraryId: null,
stackId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
deletedAt: null,
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as Exif,
duplicateId: null,
isOffline: false,
stack: null,
orientation: '',
projectionType: null,
height: 3840,
width: 2160,
visibility: AssetVisibility.Timeline,
edits: [],
}),
};
+8
View File
@@ -25,6 +25,7 @@ export const faceStub = {
deletedAt: new Date(),
updatedAt: new Date('2023-01-01T00:00:00Z'),
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
isVisible: true,
}),
primaryFace1: Object.freeze({
id: 'assetFaceId2',
@@ -43,6 +44,7 @@ export const faceStub = {
deletedAt: null,
updatedAt: new Date('2023-01-01T00:00:00Z'),
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
isVisible: true,
}),
mergeFace1: Object.freeze({
id: 'assetFaceId3',
@@ -61,6 +63,7 @@ export const faceStub = {
deletedAt: null,
updatedAt: new Date('2023-01-01T00:00:00Z'),
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
isVisible: true,
}),
noPerson1: Object.freeze({
id: 'assetFaceId8',
@@ -79,6 +82,7 @@ export const faceStub = {
deletedAt: null,
updatedAt: new Date('2023-01-01T00:00:00Z'),
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
isVisible: true,
}),
noPerson2: Object.freeze({
id: 'assetFaceId9',
@@ -97,6 +101,7 @@ export const faceStub = {
deletedAt: null,
updatedAt: new Date('2023-01-01T00:00:00Z'),
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
isVisible: true,
}),
fromExif1: Object.freeze({
id: 'assetFaceId9',
@@ -114,6 +119,7 @@ export const faceStub = {
deletedAt: null,
updatedAt: new Date('2023-01-01T00:00:00Z'),
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
isVisible: true,
}),
fromExif2: Object.freeze({
id: 'assetFaceId9',
@@ -131,6 +137,7 @@ export const faceStub = {
deletedAt: null,
updatedAt: new Date('2023-01-01T00:00:00Z'),
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
isVisible: true,
}),
withBirthDate: Object.freeze({
id: 'assetFaceId10',
@@ -148,5 +155,6 @@ export const faceStub = {
deletedAt: null,
updatedAt: new Date('2023-01-01T00:00:00Z'),
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
isVisible: true,
}),
};
+7
View File
@@ -142,6 +142,11 @@ export const sharedLinkStub = {
rating: 3,
updatedAt: today,
updateId: '42',
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: 500,
height: 500,
},
sharedLinks: [],
faces: [],
@@ -152,6 +157,8 @@ export const sharedLinkStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: 500,
height: 500,
},
],
albumId: null,