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>
47 lines
2.1 KiB
TypeScript
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"),
|
|
);
|
|
}
|