mirror of
https://github.com/samsonjs/immich.git
synced 2026-03-25 09:15:56 +00:00
* fix(web): Ensure profile picture is cropped to 1:1 ratio Fixes #20097 The profile picture was being captured from the PhotoViewer element which could have non-square dimensions based on the original image. Changed to capture from the crop container element which has the aspect-square class, ensuring the output is always 1:1 ratio. * fix: remove trailing whitespace to pass prettier check --------- Co-authored-by: Aditya Gaurav <aditya-ai-architect@users.noreply.github.com>
95 lines
3 KiB
Svelte
95 lines
3 KiB
Svelte
<script lang="ts">
|
|
import { user } from '$lib/stores/user.store';
|
|
import { handleError } from '$lib/utils/handle-error';
|
|
import { createProfileImage, type AssetResponseDto } from '@immich/sdk';
|
|
import { FormModal, toastManager } from '@immich/ui';
|
|
import domtoimage from 'dom-to-image';
|
|
import { onMount } from 'svelte';
|
|
import { t } from 'svelte-i18n';
|
|
import PhotoViewer from '../components/asset-viewer/photo-viewer.svelte';
|
|
|
|
interface Props {
|
|
asset: AssetResponseDto;
|
|
onClose: () => void;
|
|
}
|
|
|
|
let { asset, onClose }: Props = $props();
|
|
|
|
let imgElement: HTMLDivElement | undefined = $state();
|
|
let cropContainer: HTMLDivElement | undefined = $state();
|
|
|
|
onMount(() => {
|
|
if (!imgElement) {
|
|
return;
|
|
}
|
|
|
|
imgElement.style.width = '100%';
|
|
});
|
|
|
|
const hasTransparentPixels = async (blob: Blob) => {
|
|
const img = new Image();
|
|
img.src = URL.createObjectURL(blob);
|
|
await img.decode();
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
const context = canvas.getContext('2d');
|
|
if (!context) {
|
|
throw new Error('Could not get canvas context.');
|
|
}
|
|
context.drawImage(img, 0, 0);
|
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
const data = imageData?.data;
|
|
if (!data) {
|
|
throw new Error('Could not get image data.');
|
|
}
|
|
for (let index = 0; index < data.length; index += 4) {
|
|
if (data[index + 3] < 255) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const onSubmit = async () => {
|
|
if (!cropContainer) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Get the container dimensions (which is always square due to aspect-square class)
|
|
const containerSize = cropContainer.offsetWidth;
|
|
|
|
// Capture the crop container which maintains 1:1 aspect ratio
|
|
const blob = await domtoimage.toBlob(cropContainer, {
|
|
width: containerSize,
|
|
height: containerSize,
|
|
});
|
|
|
|
if (await hasTransparentPixels(blob)) {
|
|
toastManager.danger($t('errors.profile_picture_transparent_pixels'));
|
|
return;
|
|
}
|
|
const file = new File([blob], 'profile-picture.png', { type: 'image/png' });
|
|
const { profileImagePath, profileChangedAt } = await createProfileImage({ createProfileImageDto: { file } });
|
|
toastManager.success($t('profile_picture_set'));
|
|
$user.profileImagePath = profileImagePath;
|
|
$user.profileChangedAt = profileChangedAt;
|
|
|
|
onClose();
|
|
} catch (error) {
|
|
handleError(error, $t('errors.unable_to_set_profile_picture'));
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<FormModal size="small" title={$t('set_profile_picture')} {onClose} {onSubmit}>
|
|
<div class="flex place-items-center items-center justify-center">
|
|
<div
|
|
bind:this={cropContainer}
|
|
class="relative flex aspect-square w-62.5 overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
|
|
>
|
|
<PhotoViewer bind:element={imgElement} cursor={{ current: asset }} />
|
|
</div>
|
|
</div>
|
|
</FormModal>
|