Skip to content

Add clipboard support for image/webp #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions src/client/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,61 @@ declare interface ClipboardItem {
getType(type: string): Promise<Blob>;
}

export async function writeImageToClipboard(blob: Blob) {
const item = new ClipboardItem({ 'image/png': blob });
async function convertWebPToPNG(webpBlob: Blob): Promise<Blob> {
return new Promise((resolve, reject) => {
const image = new Image();
const url = URL.createObjectURL(webpBlob);

image.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;

const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Unable to get canvas context'));
return;
}

ctx.drawImage(image, 0, 0);

canvas.toBlob((pngBlob) => {
if (pngBlob) {
resolve(pngBlob);
} else {
reject(new Error('Canvas to Blob conversion failed'));
}
}, 'image/png');

URL.revokeObjectURL(url);
};

image.onerror = () => {
reject(new Error('Error loading WebP image'));
URL.revokeObjectURL(url);
};

image.src = url;
});
}

export async function writeImageToClipboard(blob: Blob, mimeType: string) {
let imageBlob = blob;

// Convert WebP to PNG if necessary
if (mimeType === 'image/webp') {
imageBlob = await convertWebPToPNG(blob);
mimeType = 'image/png'; // Update the mimeType to 'image/png'
}

// Create ClipboardItem with the correct mimeType
const item = new ClipboardItem({ [mimeType]: imageBlob });

// Check if clipboard API is supported
if (!('write' in navigator.clipboard)) {
throw new Error('navigator.clipboard.write not supported');
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (navigator.clipboard as any).write([item]);
}
4 changes: 2 additions & 2 deletions src/client/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class CellOutput extends React.Component<ICellOutputProps> {
mimeType.toLowerCase().includes('svg') && typeof data === 'string' ? undefined : URL.createObjectURL(data);
const customMetadata = metadata.metadata as PartialJSONObject | undefined;
const showPlotViewer = metadata.__displayOpenPlotIcon === true;
const showCopyImage = mimeType === 'image/png';
const showCopyImage = mimeType === 'image/png' || mimeType === 'image/webp';
const copyButtonMargin = showPlotViewer ? '85px' : '45px';
if (customMetadata && typeof customMetadata.needs_background === 'string') {
imgStyle.backgroundColor = customMetadata.needs_background === 'light' ? 'white' : 'black';
Expand Down Expand Up @@ -128,7 +128,7 @@ export class CellOutput extends React.Component<ICellOutputProps> {
}
};
const copyPlotImage = () => {
writeImageToClipboard(data as Blob).then(noop);
writeImageToClipboard(data as Blob, this.props.mimeType).then(noop);
};
const onMouseOver = () => {
if (!(globalThis as any).__isJupyterInstalled) {
Expand Down
Loading