diff --git a/web/package-lock.json b/web/package-lock.json
index 4ddc6d9baa..7bcd5c2b01 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -17,7 +17,6 @@
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
"@photo-sphere-viewer/video-plugin": "^5.7.2",
"@zoom-image/svelte": "^0.2.6",
- "copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0",
"handlebars": "^4.7.8",
"intl-messageformat": "^10.5.14",
@@ -3284,11 +3283,6 @@
"node": ">= 0.6"
}
},
- "node_modules/copy-image-clipboard": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/copy-image-clipboard/-/copy-image-clipboard-2.1.2.tgz",
- "integrity": "sha512-3VCXVl2IpFfOyD8drv9DozcNlwmqBqxOlsgkEGyVAzadjlPk1go8YNZyy8QmTnwHPxSFpeCR9OdsStEdVK7qDA=="
- },
"node_modules/core-js-compat": {
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
diff --git a/web/package.json b/web/package.json
index 1ba350022d..c84bbf0db4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -73,7 +73,6 @@
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
"@photo-sphere-viewer/video-plugin": "^5.7.2",
"@zoom-image/svelte": "^0.2.6",
- "copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0",
"handlebars": "^4.7.8",
"intl-messageformat": "^10.5.14",
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
index 0f75f9bb83..db216641d5 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
@@ -41,7 +41,7 @@
mdiPresentationPlay,
mdiUpload,
} from '@mdi/js';
- import { canCopyImagesToClipboard } from 'copy-image-clipboard';
+ import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
import { t } from 'svelte-i18n';
export let asset: AssetResponseDto;
@@ -101,7 +101,7 @@
on:click={onZoomImage}
/>
{/if}
- {#if canCopyImagesToClipboard() && asset.type === AssetTypeEnum.Image}
+ {#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image}
{/if}
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte
index 40a36fa0e0..4157c558d2 100644
--- a/web/src/lib/components/asset-viewer/photo-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte
@@ -8,17 +8,17 @@
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
- import { isWebCompatibleImage } from '$lib/utils/asset-utils';
+ import { isWebCompatibleImage, canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
import { getBoundingBox } from '$lib/utils/people-utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
- import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
+ import { handleError } from '$lib/utils/handle-error';
export let asset: AssetResponseDto;
export let preloadAssets: AssetResponseDto[] | undefined = undefined;
@@ -81,23 +81,19 @@
};
copyImage = async () => {
- if (!canCopyImagesToClipboard()) {
+ if (!canCopyImageToClipboard()) {
return;
}
try {
- await copyImageToClipboard(assetFileUrl);
+ await copyImageToClipboard($photoViewer ?? assetFileUrl);
notificationController.show({
type: NotificationType.Info,
message: $t('copied_image_to_clipboard'),
timeout: 3000,
});
} catch (error) {
- console.error('Error [photo-viewer]:', error);
- notificationController.show({
- type: NotificationType.Error,
- message: 'Copying image to clipboard failed.',
- });
+ handleError(error, $t('copy_error'));
}
};
diff --git a/web/src/lib/utils/asset-utils.spec.ts b/web/src/lib/utils/asset-utils.spec.ts
index 8970a6a652..b3a668192d 100644
--- a/web/src/lib/utils/asset-utils.spec.ts
+++ b/web/src/lib/utils/asset-utils.spec.ts
@@ -1,5 +1,5 @@
import type { AssetResponseDto } from '@immich/sdk';
-import { getAssetFilename, getFilenameExtension } from './asset-utils';
+import { canCopyImageToClipboard, getAssetFilename, getFilenameExtension } from './asset-utils';
describe('get file extension from filename', () => {
it('returns the extension without including the dot', () => {
@@ -56,3 +56,9 @@ describe('get asset filename', () => {
}
});
});
+
+describe('copy image to clipboard', () => {
+ it('should not allow copy image to clipboard', () => {
+ expect(canCopyImageToClipboard()).toEqual(false);
+ });
+});
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index e309db5ff6..84a896452f 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -527,3 +527,41 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean
export const delay = async (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
+
+export const canCopyImageToClipboard = (): boolean => {
+ return !!(navigator.clipboard && window.ClipboardItem);
+};
+
+const imgToBlob = async (imageElement: HTMLImageElement) => {
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+
+ canvas.width = imageElement.naturalWidth;
+ canvas.height = imageElement.naturalHeight;
+
+ if (context) {
+ context.drawImage(imageElement, 0, 0);
+
+ return await new Promise((resolve) => {
+ canvas.toBlob((blob) => {
+ if (blob) {
+ resolve(blob);
+ } else {
+ throw new Error('Canvas conversion to Blob failed');
+ }
+ });
+ });
+ }
+
+ throw new Error('Canvas context is null');
+};
+
+const urlToBlob = async (imageSource: string) => {
+ const response = await fetch(imageSource);
+ return await response.blob();
+};
+
+export const copyImageToClipboard = async (source: HTMLImageElement | string) => {
+ const blob = source instanceof HTMLImageElement ? await imgToBlob(source) : await urlToBlob(source);
+ await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
+};