InvokeAI/invokeai/frontend/web/src/common/hooks/useImage.ts
psychedelicious 19c5435332 fix(ui): copy image via img onload to blob
There's a bug in chrome that screws with headers on fetch requests and 307 responses. This causes images to fail to copy in the commercial environment.

This change attempts to get around this by copying images in a different way (similar to how the canvas works). When the user requests a copy we:
- create an `<img />` element
- set `crossOrigin` if needed
- add an onload handler:
  - create a canvas element
  - draw image onto it
  - export canvas to blob

This is wrapped in a promise which resolves to the blob, which can then be copied to clipboard.

---

A customized version of Konva's `useImage` hook is also included, which returns the image blob in addition to the `<img />` element. Unfortunately, this hook is not suitable for use across the app, because it does all the image fetching up front, regardless of whether we actually want to copy the image.

In other words, we'd have to fetch the whole image file even if the user is just skipping through image metadata, in order to have the blob to copy. The callback approach means we only fetch the image when the user clicks copy. The hook is thus currently unused.
2023-10-17 06:43:19 +11:00

103 lines
2.9 KiB
TypeScript

import { useLayoutEffect, useRef, useState } from 'react';
// Adapted from https://github.com/konvajs/use-image
type CrossOrigin = 'anonymous' | 'use-credentials';
type ReferrerPolicy =
| 'no-referrer'
| 'no-referrer-when-downgrade'
| 'origin'
| 'origin-when-cross-origin'
| 'same-origin'
| 'strict-origin'
| 'strict-origin-when-cross-origin'
| 'unsafe-url';
type ImageStatus = 'loaded' | 'loading' | 'failed';
export const useImage = (
url: string,
crossOrigin?: CrossOrigin,
referrerpolicy?: ReferrerPolicy
): [undefined | HTMLImageElement, ImageStatus, Blob | null] => {
// lets use refs for image and status
// so we can update them during render
// to have instant update in status/image when new data comes in
const statusRef = useRef<ImageStatus>('loading');
const imageRef = useRef<HTMLImageElement>();
const blobRef = useRef<Blob | null>(null);
// we are not going to use token
// but we need to just to trigger state update
const [_, setStateToken] = useState(0);
// keep track of old props to trigger changes
const oldUrl = useRef<string>();
const oldCrossOrigin = useRef<string>();
const oldReferrerPolicy = useRef<string>();
if (
oldUrl.current !== url ||
oldCrossOrigin.current !== crossOrigin ||
oldReferrerPolicy.current !== referrerpolicy
) {
statusRef.current = 'loading';
imageRef.current = undefined;
oldUrl.current = url;
oldCrossOrigin.current = crossOrigin;
oldReferrerPolicy.current = referrerpolicy;
}
useLayoutEffect(
function () {
if (!url) {
return;
}
const img = document.createElement('img');
function onload() {
statusRef.current = 'loaded';
imageRef.current = img;
const canvas = document.createElement('canvas');
canvas.width = img.clientWidth;
canvas.height = img.clientHeight;
const context = canvas.getContext('2d');
if (context) {
context.drawImage(img, 0, 0);
canvas.toBlob(function (blob) {
blobRef.current = blob;
}, 'image/png');
}
setStateToken(Math.random());
}
function onerror() {
statusRef.current = 'failed';
imageRef.current = undefined;
setStateToken(Math.random());
}
img.addEventListener('load', onload);
img.addEventListener('error', onerror);
if (crossOrigin) {
img.crossOrigin = crossOrigin;
}
if (referrerpolicy) {
img.referrerPolicy = referrerpolicy;
}
img.src = url;
return function cleanup() {
img.removeEventListener('load', onload);
img.removeEventListener('error', onerror);
};
},
[url, crossOrigin, referrerpolicy]
);
// return array because it is better to use in case of several useImage hooks
// const [background, backgroundStatus] = useImage(url1);
// const [patter] = useImage(url2);
return [imageRef.current, statusRef.current, blobRef.current];
};