// 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 `` 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 { await navigator.clipboard.write([new ClipboardItem({ "image/png": asPng(url) })]); } async function asPng(url: string): Promise { 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"), ); }