fix: embed images when copying to WeChat

This commit is contained in:
tianma-if committed 2026-07-23 22:27:07 +08:00
1 parent 7b20f7039a
commit 1ea94559bd
2 files changed
+76 -19

No files matched your search

+75 -18
View File
@@ -94,9 +94,46 @@ const WECHAT_IMAGE_MIME_TYPES = new Set([
"image/webp",
]);
const convertImageToPng = async (blob: Blob) => {
if (WECHAT_IMAGE_MIME_TYPES.has(blob.type.toLocaleLowerCase())) {
return blob;
const canvasToPng = (canvas: HTMLCanvasElement) => new Promise<Blob>((resolve, reject) => {
canvas.toBlob((png) => {
if (png) {
resolve(png);
return;
}
reject(new Error("Could not convert image to PNG"));
}, "image/png");
});
const rasterizeImageElement = async (image: HTMLImageElement) => {
if (!image.complete || image.naturalWidth === 0) {
await image.decode();
}
if (image.naturalWidth === 0 || image.naturalHeight === 0) {
throw new Error("Could not decode image for clipboard");
}
const canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Could not create image canvas");
}
context.drawImage(image, 0, 0);
return canvasToPng(canvas);
};
const rasterizeBlob = async (blob: Blob) => {
if (blob.type.toLocaleLowerCase() === "image/svg+xml") {
const objectUrl = URL.createObjectURL(blob);
try {
const image = new Image();
image.src = objectUrl;
await image.decode();
return rasterizeImageElement(image);
} finally {
URL.revokeObjectURL(objectUrl);
}
}
const bitmap = await createImageBitmap(blob);
@@ -109,37 +146,56 @@ const convertImageToPng = async (blob: Blob) => {
throw new Error("Could not create image canvas");
}
context.drawImage(bitmap, 0, 0);
return await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((png) => {
if (png) {
resolve(png);
return;
}
reject(new Error("Could not convert image to PNG"));
}, "image/png");
});
return canvasToPng(canvas);
} finally {
bitmap.close();
}
};
const embedImagesForWeChat = async (root: HTMLElement) => {
const convertImageToPng = async (blob: Blob) => {
if (WECHAT_IMAGE_MIME_TYPES.has(blob.type.toLocaleLowerCase())) {
return blob;
}
return rasterizeBlob(blob);
};
const isSameOrigin = (source: string) => {
try {
return new URL(source, window.location.href).origin === window.location.origin;
} catch {
return false;
}
};
const embedImagesForWeChat = async (root: HTMLElement, originalImages: HTMLImageElement[] = []) => {
const images = Array.from(root.querySelectorAll<HTMLImageElement>("img"));
await Promise.all(images.map(async (image) => {
await Promise.all(images.map(async (image, index) => {
const source = image.getAttribute("src")?.trim();
if (!source || source.startsWith("data:")) {
return;
}
try {
const originalImage = originalImages[index];
if (originalImage) {
image.setAttribute("src", await blobToDataUrl(await rasterizeImageElement(originalImage)));
image.removeAttribute("srcset");
return;
}
const response = await fetch(source, { credentials: "include" });
if (!response.ok) {
return;
throw new Error(`Could not fetch image (${response.status})`);
}
image.setAttribute("src", await blobToDataUrl(await convertImageToPng(await response.blob())));
image.removeAttribute("srcset");
} catch {
// Keep the original URL when the browser cannot fetch an external image.
} catch (error) {
// Never put a private same-origin URL into the clipboard: WeChat cannot access it.
if (isSameOrigin(source)) {
throw error;
}
// External images may still be reachable by the WeChat editor.
}
}));
};
@@ -149,7 +205,8 @@ export const buildWeChatClipboardHtml = async (editor: Editor) => {
container.innerHTML = editor.getHTML();
const editorTheme = editor.view.dom.closest<HTMLElement>("[data-editor-theme]")?.dataset.editorTheme;
applyInlineStyles(container, editorTheme);
await embedImagesForWeChat(container);
const originalImages = Array.from(editor.view.dom.querySelectorAll<HTMLImageElement>("img"));
await embedImagesForWeChat(container, originalImages);
return container.outerHTML;
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "edgeever",
"version": "1.5.12",
"version": "1.5.13",
"private": true,
"type": "module",
"workspaces": [