44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
/**
|
|||
|
|
* [Gitea #164] Copy an image to the system clipboard.
|
||
|
|
*
|
||
|
|
* The async Clipboard API only guarantees `image/png`, so anything else (JPEG,
|
||
|
|
* WebP, a GIF's first frame) is re-encoded through a canvas first. The blob is
|
||
|
|
* handed to `ClipboardItem` as a promise so the write starts inside the click's
|
||
|
|
* user activation — Safari rejects a write that begins after an await.
|
||
|
|
*/
|
||
|
|
export function canCopyImage(): boolean {
|
||
|
|
return (
|
||
|
|
typeof ClipboardItem !== 'undefined' &&
|
||
|
|
typeof navigator !== 'undefined' &&
|
||
|
|
typeof navigator.clipboard?.write === 'function'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function toPngBlob(blob: Blob): Promise<Blob> {
|
||
|
|
if (blob.type === 'image/png') return blob;
|
||
|
|
const bitmap = await createImageBitmap(blob);
|
||
|
|
try {
|
||
|
|
const canvas = document.createElement('canvas');
|
||
|
|
canvas.width = bitmap.width;
|
||
|
|
canvas.height = bitmap.height;
|
||
|
|
const ctx = canvas.getContext('2d');
|
||
|
|
if (!ctx) throw new Error('no 2d context');
|
||
|
|
ctx.drawImage(bitmap, 0, 0);
|
||
|
|
return await new Promise<Blob>((resolve, reject) => {
|
||
|
|
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('png encode failed'))), 'image/png');
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
bitmap.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function copyImageFromUrl(url: string): Promise<void> {
|
||
|
|
const png = fetch(url)
|
||
|
|
.then((res) => {
|
||
|
|
if (!res.ok) throw new Error(`fetch failed: ${res.status}`);
|
||
|
|
return res.blob();
|
||
|
|
})
|
||
|
|
.then(toPngBlob);
|
||
|
|
await navigator.clipboard.write([new ClipboardItem({ 'image/png': png })]);
|
||
|
|
}
|