import type { Faces } from '$lib/stores/people.store'; import { getAssetMediaUrl } from '$lib/utils'; import { mapNormalizedRectToContent, type ContentMetrics, type Rect, type Size } from '$lib/utils/container-utils'; import { AssetTypeEnum, type AssetFaceResponseDto } from '@immich/sdk'; export type BoundingBox = Rect & { id: string }; export const getBoundingBox = (faces: Faces[], imageSize: Size): BoundingBox[] => { const metrics: ContentMetrics = { contentWidth: imageSize.width, contentHeight: imageSize.height, offsetX: 0, offsetY: 0, }; const boxes: BoundingBox[] = []; for (const face of faces) { const rect = mapNormalizedRectToContent( { x: face.boundingBoxX1 / face.imageWidth, y: face.boundingBoxY1 / face.imageHeight }, { x: face.boundingBoxX2 / face.imageWidth, y: face.boundingBoxY2 / face.imageHeight }, metrics, ); boxes.push({ id: face.id, ...rect }); } return boxes; }; export type FaceRectState = { left: number; top: number; scaleX: number; scaleY: number; }; export type ResizeContext = Pick; export const scaleFaceRectOnResize = ( faceRect: FaceRectState, previous: ResizeContext, current: ResizeContext, ): FaceRectState => { const scale = current.contentWidth / previous.contentWidth; const imageRelativeLeft = (faceRect.left - previous.offsetX) * scale; const imageRelativeTop = (faceRect.top - previous.offsetY) * scale; return { left: current.offsetX + imageRelativeLeft, top: current.offsetY + imageRelativeTop, scaleX: faceRect.scaleX * scale, scaleY: faceRect.scaleY * scale, }; }; export const zoomImageToBase64 = async ( face: AssetFaceResponseDto, assetId: string, assetType: AssetTypeEnum, photoViewer: HTMLImageElement | undefined, ): Promise => { let image: HTMLImageElement | undefined; if (assetType === AssetTypeEnum.Image) { image = photoViewer; } else if (assetType === AssetTypeEnum.Video) { const data = getAssetMediaUrl({ id: assetId }); const img: HTMLImageElement = new Image(); img.src = data; await new Promise((resolve) => { img.addEventListener('load', () => resolve()); img.addEventListener('error', () => resolve()); }); image = img; } if (!image) { return null; } const { boundingBoxX1: x1, boundingBoxX2: x2, boundingBoxY1: y1, boundingBoxY2: y2, imageWidth, imageHeight } = face; const coordinates = { x1: (image.naturalWidth / imageWidth) * x1, x2: (image.naturalWidth / imageWidth) * x2, y1: (image.naturalHeight / imageHeight) * y1, y2: (image.naturalHeight / imageHeight) * y2, }; const faceWidth = coordinates.x2 - coordinates.x1; const faceHeight = coordinates.y2 - coordinates.y1; const faceImage = new Image(); faceImage.src = image.src; await new Promise((resolve) => { faceImage.addEventListener('load', resolve); faceImage.addEventListener('error', () => resolve(null)); }); const canvas = document.createElement('canvas'); canvas.width = faceWidth; canvas.height = faceHeight; const context = canvas.getContext('2d'); if (!context) { return null; } context.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight); return canvas.toDataURL(); };