0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-11 02:23:09 -05:00

fix(web): tag people in video (#16351)

This commit is contained in:
Alex 2025-02-26 12:55:32 -06:00 committed by GitHub
parent 2969e25ff7
commit c778516ce2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 79 additions and 28 deletions

View file

@ -12,13 +12,13 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
interface Props { interface Props {
imgElement: HTMLImageElement; htmlElement: HTMLImageElement | HTMLVideoElement;
containerWidth: number; containerWidth: number;
containerHeight: number; containerHeight: number;
assetId: string; assetId: string;
} }
let { imgElement, containerWidth, containerHeight, assetId }: Props = $props(); let { htmlElement, containerWidth, containerHeight, assetId }: Props = $props();
let canvasEl: HTMLCanvasElement | undefined = $state(); let canvasEl: HTMLCanvasElement | undefined = $state();
let canvas: Canvas | undefined = $state(); let canvas: Canvas | undefined = $state();
@ -39,7 +39,7 @@
}; };
const setupCanvas = () => { const setupCanvas = () => {
if (!canvasEl || !imgElement) { if (!canvasEl || !htmlElement) {
return; return;
} }
@ -68,7 +68,7 @@
}); });
$effect(() => { $effect(() => {
const { actualWidth, actualHeight } = getContainedSize(imgElement); const { actualWidth, actualHeight } = getContainedSize(htmlElement);
const offsetArea = { const offsetArea = {
width: (containerWidth - actualWidth) / 2, width: (containerWidth - actualWidth) / 2,
height: (containerHeight - actualHeight) / 2, height: (containerHeight - actualHeight) / 2,
@ -103,15 +103,30 @@
positionFaceSelector(); positionFaceSelector();
}); });
const getContainedSize = (img: HTMLImageElement): { actualWidth: number; actualHeight: number } => { const getContainedSize = (
const ratio = img.naturalWidth / img.naturalHeight; img: HTMLImageElement | HTMLVideoElement,
let actualWidth = img.height * ratio; ): { actualWidth: number; actualHeight: number } => {
let actualHeight = img.height; if (img instanceof HTMLImageElement) {
if (actualWidth > img.width) { const ratio = img.naturalWidth / img.naturalHeight;
actualWidth = img.width; let actualWidth = img.height * ratio;
actualHeight = img.width / ratio; let actualHeight = img.height;
if (actualWidth > img.width) {
actualWidth = img.width;
actualHeight = img.width / ratio;
}
return { actualWidth, actualHeight };
} else if (img instanceof HTMLVideoElement) {
const ratio = img.videoWidth / img.videoHeight;
let actualWidth = img.clientHeight * ratio;
let actualHeight = img.clientHeight;
if (actualWidth > img.clientWidth) {
actualWidth = img.clientWidth;
actualHeight = img.clientWidth / ratio;
}
return { actualWidth, actualHeight };
} }
return { actualWidth, actualHeight };
return { actualWidth: 0, actualHeight: 0 };
}; };
const cancel = () => { const cancel = () => {
@ -202,12 +217,12 @@
}); });
const getFaceCroppedCoordinates = () => { const getFaceCroppedCoordinates = () => {
if (!faceRect || !imgElement) { if (!faceRect || !htmlElement) {
return; return;
} }
const { left, top, width, height } = faceRect.getBoundingRect(); const { left, top, width, height } = faceRect.getBoundingRect();
const { actualWidth, actualHeight } = getContainedSize(imgElement); const { actualWidth, actualHeight } = getContainedSize(htmlElement);
const offsetArea = { const offsetArea = {
width: (containerWidth - actualWidth) / 2, width: (containerWidth - actualWidth) / 2,
@ -220,19 +235,35 @@
const y2Coeff = (top + height - offsetArea.height) / actualHeight; const y2Coeff = (top + height - offsetArea.height) / actualHeight;
// transpose to the natural image location // transpose to the natural image location
const x1 = x1Coeff * imgElement.naturalWidth; if (htmlElement instanceof HTMLImageElement) {
const y1 = y1Coeff * imgElement.naturalHeight; const x1 = x1Coeff * htmlElement.naturalWidth;
const x2 = x2Coeff * imgElement.naturalWidth; const y1 = y1Coeff * htmlElement.naturalHeight;
const y2 = y2Coeff * imgElement.naturalHeight; const x2 = x2Coeff * htmlElement.naturalWidth;
const y2 = y2Coeff * htmlElement.naturalHeight;
return { return {
imageWidth: imgElement.naturalWidth, imageWidth: htmlElement.naturalWidth,
imageHeight: imgElement.naturalHeight, imageHeight: htmlElement.naturalHeight,
x: Math.floor(x1), x: Math.floor(x1),
y: Math.floor(y1), y: Math.floor(y1),
width: Math.floor(x2 - x1), width: Math.floor(x2 - x1),
height: Math.floor(y2 - y1), height: Math.floor(y2 - y1),
}; };
} else if (htmlElement instanceof HTMLVideoElement) {
const x1 = x1Coeff * htmlElement.videoWidth;
const y1 = y1Coeff * htmlElement.videoHeight;
const x2 = x2Coeff * htmlElement.videoWidth;
const y2 = y2Coeff * htmlElement.videoHeight;
return {
imageWidth: htmlElement.videoWidth,
imageHeight: htmlElement.videoHeight,
x: Math.floor(x1),
y: Math.floor(y1),
width: Math.floor(x2 - x1),
height: Math.floor(y2 - y1),
};
}
}; };
const tagFace = async (person: PersonResponseDto) => { const tagFace = async (person: PersonResponseDto) => {

View file

@ -234,7 +234,7 @@
</div> </div>
{#if isFaceEditMode.value} {#if isFaceEditMode.value}
<FaceEditor imgElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} /> <FaceEditor htmlElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} />
{/if} {/if}
{/if} {/if}
</div> </div>

View file

@ -9,6 +9,8 @@
import type { SwipeCustomEvent } from 'svelte-gestures'; import type { SwipeCustomEvent } from 'svelte-gestures';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
interface Props { interface Props {
assetId: string; assetId: string;
@ -84,9 +86,23 @@
onPreviousAsset(); onPreviousAsset();
} }
}; };
let containerWidth = $state(0);
let containerHeight = $state(0);
$effect(() => {
if (isFaceEditMode.value) {
videoPlayer?.pause();
}
});
</script> </script>
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center"> <div
transition:fade={{ duration: 150 }}
class="flex h-full select-none place-content-center place-items-center"
bind:clientWidth={containerWidth}
bind:clientHeight={containerHeight}
>
<video <video
bind:this={videoPlayer} bind:this={videoPlayer}
loop={$loopVideoPreference && loopVideo} loop={$loopVideoPreference && loopVideo}
@ -116,4 +132,8 @@
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
{/if} {/if}
{#if isFaceEditMode.value}
<FaceEditor htmlElement={videoPlayer} {containerWidth} {containerHeight} {assetId} />
{/if}
</div> </div>