Files
inkwell/frontend/src/notes/attachments.ts
T
bvandeusenandClaude Opus 5.5 19487b079a Attached images open full size and copy back out (#6198)
Scribe #6198 "An attached image can't be viewed large or copied back out".

Web and desktop: clicking an image in the editor opens it full size, with
Copy image (re-encoded to PNG, the one type every clipboard accepts),
Download and Close. Copy hides where the browser has no async clipboard,
such as a server reached over plain http; right-clicking the full-size
image still copies there.

Android: long-pressing an image in the editor copies it to the clipboard;
a tap still opens it full size in the phone's viewer. Opening and copying
share one hand-out that copies the file out of the blob store behind a
FileProvider URI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 17:14:32 -04:00

47 lines
2.1 KiB
TypeScript

// How an attachment is drawn, decided in one place for the card and the editor.
/** Whether a file is shown as a picture rather than as a chip to download.
*
* Every `image/` type except SVG. An SVG is a document that can carry its own
* script, so neither surface serves one to render: the server sends it as a download
* (#1981) and the desktop's blob scheme as opaque bytes, where an `<img>` of it would
* only ever show broken. */
export function rendersInline(mime: string): boolean {
return mime.startsWith("image/") && mime !== "image/svg+xml";
}
/** Whether this browser can put a picture on the clipboard at all.
*
* The async clipboard only exists in a secure context, so a server reached over plain
* http has none — the viewer then leaves copying to the browser's own context menu
* on the full-size image. */
export function canCopyImages(): boolean {
return typeof ClipboardItem !== "undefined" && typeof navigator.clipboard?.write === "function";
}
/** Put the image at `url` on the clipboard.
*
* As PNG whatever it was stored as: PNG is the one image type every browser's
* clipboard accepts, so a JPEG or WebP is redrawn through a canvas first. The item is
* given a promise rather than a blob so the write starts inside the click — Safari
* refuses a clipboard write that begins after an await. */
export async function copyImage(url: string): Promise<void> {
await navigator.clipboard.write([new ClipboardItem({ "image/png": asPng(url) })]);
}
async function asPng(url: string): Promise<Blob> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const blob = await res.blob();
if (blob.type === "image/png") return blob;
const bitmap = await createImageBitmap(blob);
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext("2d")?.drawImage(bitmap, 0, 0);
bitmap.close();
return new Promise((resolve, reject) =>
canvas.toBlob((png) => (png ? resolve(png) : reject(new Error("Could not encode the image."))), "image/png"),
);
}