1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebアプリとExcelでSVGを双方向コピペする方法:Clipboard APIと保存再オープン検証

1
Posted at

Web製図・ダイアグラム・CAD系のアプリでは、「SVGを書き出す」のではなく、WebアプリでCtrl+C → ExcelでCtrl+Vしたいという要件が出ます。さらに実運用では、Excel上の図をWebへ戻し、できればPNG一枚ではなく線・文字・図形を再利用したくなります。

今回の実測で重要だったのは、「SVGがクリップボードにある」ことと「Web側でベクターとして受理できる」ことは別だった点です。貼った直後は成功しても、Excelを保存・終了・再オープンした後だけ、vector-richなSVGを受信側が拒否してPNGへフォールバックするケースも確認しました。

最初に結果を3つへ分けます。

Contract 今回の実測 この記事で扱うこと
Web → Excel 対応Chromeでimage/svg+xmlを書き、ExcelへSVGとして貼付できた 最小writeコードとfeature detection
Excel → Web SVGを取得できた。SVG/PNG等が同居するケースもあった MIME、payload構造、受信側判定の分離
Web → Excel → save/close/reopen → Web Fresh成功とReopen成功は同義ではなかった SVG serialization差、receiver reject、PNG fallback

この記事は単なるClipboard API入門ではなく、ExcelへのSVG貼り付け、ExcelからWebへのSVG取得、ラスター表示の原因、保存再オープン後の変化を一続きに追います。最終UIだけから「ExcelがSVGを捨てた」と判断しないための切り分け方まで含めます。

Web → Excel:image/svg+xmlを書き込む

対応Browserでは、SVG文字列をBlobにしてClipboardItemへ渡せます。

const SVG_MIME = "image/svg+xml";

async function copySvgForExcel(svgText) {
  if (!navigator.clipboard?.write || typeof ClipboardItem === "undefined") {
    throw new Error("Clipboard write is unavailable");
  }

  if (typeof ClipboardItem.supports === "function" &&
      !ClipboardItem.supports(SVG_MIME)) {
    throw new Error("SVG clipboard write is unsupported");
  }

  const blob = new Blob([svgText], {
    type: "image/svg+xml;charset=utf-8",
  });

  await navigator.clipboard.write([
    new ClipboardItem({ [SVG_MIME]: blob })
  ]);
}

今回のChrome / Excel環境では、このClipboardをExcelへSVG画像として貼り付けられました。

image/svg+xml対応はブラウザーごとに差があるため、実製品ではClipboardItem.supports("image/svg+xml")などでfeature detectionを行い、未対応を成功扱いしないことが重要です。

SVGはxmlnsと正のviewBoxを持たせ、外部URLや外部CSSだけに依存しないself-containedな構成にしておく方が扱いやすくなります。アプリ独自のdata-*やmetadataは、ExcelへSVGとして渡すための必須条件ではありません。

Excel → Web:BrowserからSVGを読む

Async Clipboardでimage/svg+xmlが取得できる環境なら、最小形は次のようになります。

async function readSvgFromClipboard() {
  const items = await navigator.clipboard.read();

  for (const item of items) {
    if (!item.types.includes("image/svg+xml")) continue;
    const blob = await item.getType("image/svg+xml");
    return await blob.text();
  }

  return null;
}

ただし、ここからが実装上の本番です。

  • image/svg+xmlがあっても、中身がRasterを包んだSVGの場合がある
  • SVGとPNGが同じClipboardに同居する場合がある
  • PasteEventではimage/svg+xmlが見えても本文が空で、Async Clipboardでは読める場合がある
  • Webアプリ側がSVGを取得した後、パーサーで拒否してPNGへフォールバックする場合がある

そのため「Rasterとして貼られた」だけでは、ExcelがSVGを捨てたとは言えません。

保存して開き直すと何が変わるのか:双方向成功とRound-trip成功は別

Web → Excel

と、

Excel → Web

がそれぞれ成功しても、

Web
→ Excel
→ save
→ close
→ reopen
→ copy
→ Web

が同じように成功するとは限りません。

今回の実測では、Excelを閉じて再オープンした後にClipboardへ出るSVGのシリアライズ表現が変わり、SVG自体にはベクター情報が残っているのに、受信側が一部のtransformを安全に正規化できずPNGへフォールバックするcaseがありました。

この記事で分かること

  • WebアプリのSVGをファイル保存なしでExcelへCtrl+Vする最小コード
  • ExcelからWebアプリへSVGを受け取る最小コード
  • image/svg+xmlがあるのにRasterになる理由の切り分け方
  • SVG / PNG / Office形式が同居するmulti-format Clipboardの考え方
  • PasteEventとAsync Clipboardを分けて観測する理由
  • Freshでは成功するのに保存→再オープン後だけ失敗する問題の調べ方
  • 双方向copy/pasteとRound-tripを別Acceptanceにする方法

対象読者

Web製図、diagram、whiteboard、CAD、グラフ、Office連携SaaSなどで、**「WebとExcelの間で図をベクターのままコピペしたい」**開発者向けです。

ここから先は、実際にどこで表現が変わったかを実測で分解します。


検証環境

  • Microsoft Excel 2024 MSO
  • Version 2608
  • Build 16.0.20326.20072
  • 64-bit
  • Chrome 153.0.0.0 / Win64 x64
  • 検証期間: 主研究 2026-09-20〜2026-09-21 / 追加の合成fixture実機確認 2026-09-26

Edge、別Excel build、macOS Excel、Word/PowerPointには一般化しません。


A〜G: まずcontent combinationを分ける

同一セッション / fresh placementで、次の7ケースを作りました。

Case Vector SVG Raster
A ✓
B ✓
C ✓
D ✓ ✓
E ✓ ✓
F ✓ ✓
G ✓ ✓ ✓

このMatrixの目的は網羅ではなく、正常caseと異常caseの差を一つにすることです。

特にFとGはRasterの有無だけが差になります。


Windows Clipboardは一つのformatではない

今回のExcel Drawing copyでは、SVG、PNG、EMF、DIB、Office登録形式など複数representationが同時に存在しました。

したがって、

PNGがある

と、

SVGがない

は同義ではありません。

受け側はformat presenceだけでなく、どのrepresentationを優先し、どうsemanticに解釈するかを決める必要があります。


PasteEventとAsync Clipboardは分けて見る

今回のChrome環境では、PasteEvent側でimage/svg+xmlがadvertiseされていてもgetData("image/svg+xml")で本文が取れず、Async Clipboardのnavigator.clipboard.read()ではSVG Blobを取得できるcaseがありました。

const items = await navigator.clipboard.read();
for (const item of items) {
  if (item.types.includes("image/svg+xml")) {
    const blob = await item.getType("image/svg+xml");
    const svg = await blob.text();
    console.log(blob.size, svg.slice(0, 80));
  }
}

API surfaceの失敗を、そのままsource absenceとみなさないことが重要です。


ラスター(Raster)だけの図でもSVGが存在した

Case CはRasterだけですが、image/svg+xmlが存在しました。

ただし中身はeditable vectorではなく、概念的には次のようなwrapperです。

<svg>
  <defs>...</defs>
  <use ... />
  <image href="data:image/..." />
</svg>

したがって、

MIME = image/svg+xml
≠ semantic content = vector

です。


Mixed SVGにはベクター・Text・Rasterが共存した

DやGでは同一SVG内にpath、text、embedded imageが共存しました。

この時点で「Excelが全部PNG一枚へflattenした」という仮説は外れます。

SVGに情報が残っているなら、次はreceiver側のparser/canonicalizerを見ます。


Case F: ラスターに見えたが、実際はSVG reject→PNGフォールバック

FはVector + SVG / Rasterなしです。

生のSVGはvector-richでした。それでもRaster用UIへ流れました。

実際の処理は、

SVG acquisition成功
↓
strict parser
↓
Office由来と判断するsignal不足
↓
near-similarityでreject
↓
companion PNG フォールバック
↓
Raster用UI

でした。

つまり最終UIはsource classificationではありません。


保存・終了・再オープン後にだけ壊れるケース

同一セッションで通るなら十分だと思っていました。

しかし、Excelに貼った素材をブックへ保存し、Excelを閉じ、同じ.xlsxを再オープンしてコピーすると挙動が変わりました。

ここで、状態を3つに分けました。

PRE      保存前
SAVED    Ctrl+S直後、Excelは開いたまま
REOPEN   保存後にExcelを閉じ、同じfileを再オープン

Ctrl+S直後

一つのcontrolではPREとSAVEDのSVG payloadが同一でした。

つまり少なくともこのオブジェクトでは、保存ボタンを押した瞬間にSVG本文が書き換わったわけではありません。

ただしSAVEDではPNG companionが追加されました。

Close / Reopen後

REOPENではSVG本文自体が別シリアライズ表現になり、source app固有metadataが消えました。

ここで初めて「保存」と「再オープン」を別境界として扱う必要があると分かりました。


独立controlでdirect Vector→reopen Rasterを再現

元のcontrolを使い回さず、別のvector-only素材で再確認しました。

Direct copy

  • Clipboard: image/svg+xml
  • SVG: 44,369 bytes
  • result: Vector

Excelへ貼付→保存→終了→再オープン→copy

  • Clipboard: image/svg+xml + image/png
  • SVG: 12,805 bytes
  • result: Raster フォールバック

ここで重要なのは、12,805 bytesのSVGが存在していたことです。

中にはline / path / rect / ellipse / textなどvector構造が残っていました。

Excelがvectorを全部捨ててRasterだけにしたわけではありません。


では何がrejectされたのか

reopen後のSVGを受信側パーサーへoffline replayすると、transformed ellipseでFail-Closedしました。

概念的には、

<g transform="matrix(...)">
  <ellipse ... />
</g>

のような形です。

今回のcanonical modelでは、このellipse + transformをlosslessにflattenできないためrejectしました。

そこでdiagnostic controlとして、そのellipseだけを見た目が等価なpathへ置換し、他のsave/reopen構造は維持しました。

するとVector importへ変化しました。

このdiscriminatorから、今回の問題は、

Excel保存後にvectorが消えた

ではなく、

Excel再シリアライズ表現後のvector-rich SVGの一部primitive/transformをreceiverが安全にcanonicalizeできなかった

と整理できます。


2026-09-26 追加実機検証:byte差と劣化は同じではない

公開用の完全合成fixtureを使い、主研究環境でWeb→Excel→WebとSave→Close→Reopen→Webを追加確認しました。対象はBasic Vector、回転ellipse、回転text、stroke+非等方scale、Mixed、Raster wrapper、意図的なexcessive-distortion negativeの7ケースです。

7ケースすべてでWeb→Excel pasteを確認し、記録したDirect/Reopen metadataではimage/svg+xmlとimage/pngを観測しました。ここで重要だったのは、SVGのSHA-256が変わっても、それだけでは劣化を意味しないことです。

実機では、

  • rotate(...) → 等価なmatrix(...)
  • matrix(...) → 等価なscale(...)
  • href → xlink:href
  • 色表記や属性順序の正規化

のような再シリアライズが起きました。一方、今回比較したpairでは構造・transform意味・埋め込みRaster payloadが維持され、tested renderer上で視覚的に同等なケースを確認しました。2026-09-27の独立CairoSVG/Pillow再描画では、2つのre-serialization pairで34/204,800 pixels(0.0166%、max channel delta 1)の微小差も観測したため、pixel完全一致は一般化しません。

また、Mixed SVGとRaster wrapperはどちらもClipboard MIMEとしてはimage/svg+xmlです。したがって、

image/svg+xmlがある
≠ 純Vector
≠ receiverが安全に受理できる
≠ 編集可能modelへ戻せる

を実機でも確認できました。さらにnegative fixtureはExcelへ正常に貼れ、Reopen後も約3.616%のcondition distortionを保持しました。Excelが貼れることと、Web receiverがそのtransformを受理すべきことも別契約です。

研究Matrixはcontentだけでなくlifecycleも必要

今後はA〜Gだけでは不十分です。

最低でも、

Content axis
A〜G

Lifecycle axis
Direct / fresh
Saved-open
Closed + reopened

を分けます。

「貼った直後に動く」と「ブックを翌日開き直しても動く」は別Acceptanceです。


実装時の診断順序

  1. Windows native formatを確認
  2. PasteEvent typesを確認
  3. Async Clipboard typesとpayloadを確認
  4. 生のSVGを固定
  5. SVG構造を数える
  6. parser reject reasonを記録
  7. フォールバック sourceを記録
  8. document lifecycle stateを記録
  9. raw replayへ移す
  10. 最後の実機試験は仮説を分けるdiscriminatorにする

まとめ

今回の特定環境で確認したことは次です。

  • Clipboardはmulti-formatだった
  • 同一セッション A〜GではSVGを取得できた
  • Raster-onlyでもSVG wrapperになるcaseがあった
  • Mixed SVGにvector/text/rasterが共存するcaseがあった
  • Raster用UIはRaster sourceを意味しない場合があった
  • Case FはSVG reject後PNG フォールバックだった
  • Ctrl+S直後とclose/reopenは別境界だった
  • 独立controlでdirect Vector→reopen Rasterを再現した
  • reopen後もvector-rich SVGは残っていた
  • 今回はtransformed ellipseのcanonicalizationでFail-Closedしていた
  • Web/HTML→Excelはimage/svg+xml Clipboard writeでSVGとして渡せた
  • ただしOutbound成功とsave/reopen後のRound-trip成功は別Acceptance

最終UIから原因を逆算しない。さらに、同じオブジェクトでもlifecycle stateをCase IDへ含める。

これが今回まで含めた最大の教訓です。


Limitations

本記事は一つのExcel/Windows/Chrome系環境での実測です。別Excel build、Edge、macOS、Word/PowerPointで同じシリアライズ表現になることは確認していません。

約0.516%は過去fixtureで観測した値で、Excelの仕様値ではありません。1%もMicrosoft推奨値ではなく、本研究で用いたbounded compatibility policy例です。

参考資料

自分の実装へ持ち帰るなら

無料で再現するだけなら、完全合成fixtureと最小Clipboard demoで十分です。実製品へ入れる場合は、次の順で進めると調査範囲を抑えられます。

  1. MIME/format presenceを観測
  2. 生のSVGを固定
  3. structural semanticsを数える
  4. parser reject reasonを固定
  5. フォールバック sourceを記録
  6. freshとreopenを別Regressionへ入れる

関連コンテンツ

この記事は、技術的な答え・実測結果・最小実装を無料で確認できる入口としてまとめています。ここから先は目的で選べます。

調査workflowを案件へ移したい

Case ID、Evidence Sheet、offline replay、discriminator、Release Acceptanceの作り方を、調査手順として整理しています。

note:ExcelとWebアプリのSVGコピペが壊れるときの調査方法
https://note.com/xxsiaxx/n/na1f8984e7fd9

実装設計まで体系的に整理したい

Clipboard観測、SVG構造、near-similarity、Fail-Closed、lifecycle regression、Web→Excel write、release acceptanceまで37章で扱っています。

Zenn Book:Web ↔ Excel SVG Clipboard 実装・診断ガイド
https://zenn.dev/xxshinxx/books/31d5a51839f1df

自分の環境で実際に観測・比較したい

完全合成fixture、Browser / Windows probe、raw SVG capture、offline analyzer、checklist、Reference Evidenceをまとめた診断Kitです。

BOOTH:Webアプリ↔Excel SVGクリップボード診断Kit
https://sia-music.booth.pm/items/8907143

有料物は結論を隠すためではなく、理解・診断・実装の作業を再構築する時間を減らすために分けています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?