Markdown を Word にエクスポートすると画像が消える?クロスオリジンと WebP 互換性の解決方法
md-to.com の Markdown から Word へのエクスポート機能を開発中、厄介なバグに遭遇しました:ユーザーが貼り付けた Markdown 内の画像が、Word にエクスポートするとすべて消えてしまうという問題です。
原因は2つの層にまたがっていました:ブラウザのセキュリティポリシー(CORS)と画像フォーマットの互換性(WebP)。この記事では、デバッグの過程と最終的な解決策を詳しく記録します。
症状
ユーザーがエディタに以下のような Markdown を貼り付けます:


プレビューでは画像が正常に表示されます。しかしエクスポートした Word ファイルでは、2枚とも画像が消えて、[エディタ画面]、[サイトアイコン] というプレースホルダーテキストだけが残ります。
問題1:Word が WebP をサポートしていない
原因分析
Word ドキュメントの生成には docx ライブラリを使用しています。このライブラリの ImageRun コンポーネントは4種類の画像フォーマットのみサポートしています:
type DocxImageType = 'jpg' | 'png' | 'gif' | 'bmp';
現代のウェブサイトでは WebP フォーマットが広く使われています(PNG より25-35%小さい)。コードがサポート外のフォーマットを検出すると、Unsupported image type エラーをスローし、画像が失われていました。
解決策:Canvas による自動変換
ブラウザは WebP のレンダリングをネイティブサポートしているため、Canvas API を使って 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);
});
}
この関数は2種類の入力を受け付けます:
- Blob:すでに fetch 済みの画像のフォーマット変換用
- URL 文字列:CORS フォールバック用(後述)
入力フォーマットが何であれ(WebP、AVIF など)、出力は常に PNG です——Word で完璧に動作します。
問題2: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;
}
これにより、すべてのクロスオリジン画像リクエストが同一オリジンリクエストに変わり、CORS の問題が解消されます。
最終アーキテクチャ
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)
- 1画像あたり 10 MB のサイズ制限
- レスポンスに24時間キャッシュ
まとめ
| 問題 | 原因 | 解決策 |
|---|---|---|
| WebP 画像が Word に表示されない | docx ライブラリが WebP 非対応 | Canvas API で PNG に変換 |
| クロスオリジン画像が取得できない | ブラウザの CORS セキュリティポリシー | サーバーサイド Edge Function プロキシ |
| 開発環境でも画像が失敗する | localhost → 外部ドメインはクロスオリジン | Vite dev server プロキシミドルウェア |
重要な教訓:ブラウザのセキュリティモデルは「レンダリング」と「データアクセス」を区別している。<img> がクロスオリジン画像を表示できるからといって、JavaScript がそのデータを読み取れるわけではありません。クライアントサイドでクロスオリジンリソースを処理する必要がある場合、サーバーサイドプロキシがほぼ唯一の信頼できるアプローチです。
実際に試してみませんか?md-to.com Markdown to Word にアクセスして、外部画像を含む Markdown を貼り付けてエクスポートしてみてください。
関連ツール
無料 Markdown から Word 変換 - オンラインで MD を DOCX に変換
無料オンライン Markdown から Word コンバーター。リアルタイムプレビュー、ワンクリックで DOCX ダウンロード。フォーマット完全保持、登録不要、ローカル処理でプライバシー保護。今すぐお試しください!
無料 Word から Markdown 変換 - オンラインで DOCX を MD に変換
無料オンラインで Word 文書を Markdown に変換。DOCX から MD コンバーターで見出し、表、リスト、書式を完全保持。登録不要、今すぐお試しください!
無料 Markdown から LaTeX 変換 - オンラインで MD を TeX に変換
オンラインで無料で Markdown を LaTeX に変換。MD ドキュメントを学術論文や技術文書用の TeX 形式に変換。登録不要!