1/** Resolves web references without inheriting the desktop renderer's custom app scheme. */
2export function resolveProtocolRelativeMediaUrl(src: string): string {
3 if (!src.startsWith("//")) return src;
4 const protocol =
5 typeof window !== "undefined" && window.location.protocol === "http:" ? "http:" : "https:";
6 return `${protocol}${src}`;
7}
8
9/** Reads media only for an explicit save/copy action; remote hosts must allow browser CORS. */
10async function readMediaBlob(src: string): Promise<Blob> {
11 let response: Response;
12 try {
13 response = await fetch(src);
14 } catch (cause) {
15 throw new Error(
16 "The file could not be fetched. The host may block browser access (CORS), or the connection may be unavailable.",
17 { cause },
18 );
19 }
20 if (!response.ok) throw new Error(`The file could not be fetched (HTTP ${response.status}).`);
21 const blob = await response.blob();
22 if (blob.type.split(";", 1)[0] === "text/html") {
23 throw new Error("This link returned a web page instead of media. Open the original URL.");
24 }
25 return blob;
26}
27
28/** Downloads the original bytes with their original filename, without changing playback URLs. */
29export async function downloadMedia(src: string, name: string): Promise<void> {
30 const url = URL.createObjectURL(await readMediaBlob(src));
31 try {
32 const anchor = document.createElement("a");
33 anchor.href = url;
34 anchor.download = name;
35 anchor.click();
36 } finally {
37 setTimeout(() => URL.revokeObjectURL(url), 30_000);
38 }
39}
40
41/** Converts browser-decodable images, including SVG, into the clipboard's portable PNG format. */
42export async function readMediaPng(src: string): Promise<Blob> {
43 const blob = await readMediaBlob(src);
44 if (blob.type.split(";", 1)[0] === "image/png") return blob;
45
46 const url = URL.createObjectURL(blob);
47 const image = new Image();
48 image.src = url;
49 try {
50 try {
51 await image.decode();
52 } catch (cause) {
53 throw new Error(
54 "The browser could not decode this image for copying. Try saving it instead.",
55 {
56 cause,
57 },
58 );
59 }
60 const { naturalWidth: width, naturalHeight: height } = image;
61 if (width <= 0 || height <= 0 || width * height > 64_000_000) {
62 throw new Error(
63 "This image is too large or has no usable dimensions. Try saving it instead.",
64 );
65 }
66 const canvas = document.createElement("canvas");
67 canvas.width = width;
68 canvas.height = height;
69 const context = canvas.getContext("2d");
70 if (!context) throw new Error("Image copying is unavailable in this browser.");
71 context.drawImage(image, 0, 0);
72 return await new Promise<Blob>((resolve, reject) => {
73 canvas.toBlob(
74 (png) =>
75 png ? resolve(png) : reject(new Error("The image could not be converted to PNG.")),
76 "image/png",
77 );
78 });
79 } finally {
80 URL.revokeObjectURL(url);
81 }
82}