返回部落格

Markdown 匯出 Word 時圖片遺失?我們如何解決跨域圖片與 WebP 相容性問題

MD-TO 團隊

在開發 md-to.com 的 Markdown 轉 Word 功能時,我們遇到了一個令人頭疼的問題:使用者貼上的 Markdown 裡的圖片,匯出成 Word 後全部消失了

這個問題最終涉及兩個層面:瀏覽器安全策略(CORS)和圖片格式相容性(WebP)。本文詳細記錄問題的排查過程和最終解決方案。


問題現象

使用者在編輯器中貼上了這樣的 Markdown:

![編輯器介面](https://example.com/screenshot.webp)
![網站圖示](https://example.com/icon.png)

預覽區域圖片正常顯示,但匯出的 Word 檔案中兩張圖片都不見了,只剩下 [編輯器介面][網站圖示] 這樣的佔位文字。


問題一:WebP 格式不被 Word 支援

原因分析

我們使用 docx 函式庫產生 Word 文件。這個函式庫的 ImageRun 元件只支援四種圖片格式:

type DocxImageType = 'jpg' | 'png' | 'gif' | 'bmp';

而現代網站大量使用 WebP 格式(體積比 PNG 小 25-35%)。當我們的程式碼偵測到不支援的格式時,直接拋出了 Unsupported image type 錯誤,圖片就遺失了。

解決方案:Canvas 自動轉換

瀏覽器原生支援渲染 WebP,因此我們可以利用 Canvas API 將 WebP 轉成 PNG:

function loadImageViaCanvas(
  source: Blob | string,
): Promise<{ blob: Blob; width: number; height: number }> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const isBlobSource = source instanceof Blob;
    if (!isBlobSource) {
      img.crossOrigin = 'anonymous';
    }
    const blobUrl = isBlobSource ? URL.createObjectURL(source) : null;

    img.onload = () => {
      if (blobUrl) URL.revokeObjectURL(blobUrl);
      const { naturalWidth: width, naturalHeight: height } = img;
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d')!;
      ctx.drawImage(img, 0, 0);
      canvas.toBlob(
        (pngBlob) => {
          if (pngBlob) resolve({ blob: pngBlob, width, height });
          else reject(new Error('Canvas toBlob failed'));
        },
        'image/png',
      );
    };
    img.onerror = () => reject(new Error('Failed to load image'));
    img.src = blobUrl ?? (source as string);
  });
}

這個函式接受兩種輸入:

  • Blob:用於已經 fetch 到的圖片做純格式轉換
  • URL 字串:用於 CORS 備援(後面詳述)

無論輸入是什麼格式(WebP、AVIF 等),輸出始終是 PNG——Word 完美支援。


問題二:CORS 阻止取得跨域圖片資料

解決了 WebP 問題後,我們發現另一個更棘手的問題:跨域圖片完全無法取得

原因分析

我們的程式碼使用 fetch() 取得圖片資料:

const response = await fetch(imageUrl);
const blob = await response.blob();

當圖片 URL 和網站不在同一網域(例如網站是 md-to.com,圖片在 example.com)時,瀏覽器的 CORS(跨來源資源共享) 安全策略會攔截請求:

Access to fetch at 'https://example.com/image.png'
from origin 'https://md-to.com' has been blocked by CORS policy

為什麼預覽能顯示但匯出不行?

這是因為瀏覽器對不同操作有不同的安全等級:

操作是否允許跨域原因
<img src="外部URL"> 渲染✅ 允許瀏覽器渲染圖片不需要 CORS
fetch() 取得資料❌ 攔截JS 讀取二進位資料需要 CORS
<img crossOrigin> + Canvas 擷取❌ 攔截Canvas 被「污染」後無法匯出
fetch({mode: 'no-cors'})❌ 無法讀取回傳空 body

預覽區用的是 <img> 標籤(允許跨域渲染),而匯出需要用 JS 取得圖片的二進位資料(被 CORS 攔截)。這就是為什麼「看得到但匯不出」。

為什麼 Base64 也救不了?

有人可能會想:「把圖片轉成 Base64 不就行了?」

但 Base64 只是一種編碼格式,不是取得資料的手段。問題不在於「存成什麼格式」,而在於「瀏覽器不讓你拿到原始位元組」。無論你想編碼成 Base64、ArrayBuffer 還是任何形式,第一步都是要拿到資料——而這一步被 CORS 徹底封死了。

解決方案:伺服端圖片代理

既然瀏覽器端無解,就讓伺服器來請求圖片。伺服器沒有 CORS 限制,可以自由請求任何 URL。

我們建立了一個輕量級的 Edge Function 作為圖片代理:

瀏覽器 → /api/image-proxy?url=https://example.com/image.png → 我們的伺服器 → example.com
                                                               ↑ 沒有CORS限制

代理函式(部署在 Edge):

export async function onRequest({ request }) {
  const { searchParams } = new URL(request.url);
  const target = searchParams.get('url');

  // 安全校驗:只允許 http(s) 協定、只代理圖片類型、限制大小
  const upstream = await fetch(target);
  const contentType = upstream.headers.get('content-type');

  return new Response(await upstream.arrayBuffer(), {
    headers: {
      'Content-Type': contentType,
      'Cache-Control': 'public, max-age=86400',
      'Access-Control-Allow-Origin': '*',
    },
  });
}

用戶端自動路由

function resolveImageSrc(src: string): string {
  const resolved = new URL(src, window.location.href).toString();

  // 跨域圖片走代理
  if (new URL(resolved).origin !== window.location.origin) {
    return `/api/image-proxy?url=${encodeURIComponent(resolved)}`;
  }

  return resolved;
}

這樣所有跨域圖片請求變成了同域請求,瀏覽器不再攔截。


最終架構

Markdown 中的圖片 URL


  resolveImageSrc()

        ├── 同域圖片 → 直接 fetch

        └── 跨域圖片 → /api/image-proxy?url=xxx


                        Edge Function 轉發


                         取得 Blob


                    getDocxImageType() 檢查格式

                    ┌─────────┴──────────┐
                    ▼                    ▼
              jpg/png/gif/bmp      webp/avif/其他
              直接嵌入 Word      Canvas 轉 PNG 後嵌入

開發環境

Vite dev server 中新增了同路徑的代理中介層,確保開發和正式環境行為一致。

安全措施

圖片代理不是無限制的開放代理:

  • 只允許 http://https:// 協定
  • 只放行圖片 Content-Type(png/jpeg/gif/bmp/webp/avif/svg)
  • 單張圖片大小限制 10 MB
  • 回應帶 24 小時快取

總結

問題原因方案
WebP 圖片在 Word 中不顯示docx 函式庫不支援 WebP 格式Canvas API 轉為 PNG
跨域圖片完全無法取得瀏覽器 CORS 安全策略伺服端 Edge Function 代理
開發環境圖片也失敗localhost → 外部網域是跨域Vite dev server 代理中介層

核心教訓:瀏覽器的安全模型區分「渲染」和「資料存取」<img> 能顯示跨域圖片不代表 JS 能讀取它的資料。遇到需要在用戶端處理跨域資源的場景,伺服端代理幾乎是唯一可靠的方案。


想試試效果?前往 md-to.com Markdown 轉 Word,貼上帶有外部圖片的 Markdown,匯出看看。