社外に出した技術提案書に対して、相手から返ってきたのは「開けない」の一言だった。先方の業務端末は古い Office で、資料の中にある 36 行の表がそこで崩れていた。こちらの画面では何ともなかった。
こういうのは初めてではない。少し前には、送った提案書の数字を相手側が書き換えてそのまま転送し、話がこちらに戻ってきたこともある。原本をそのまま送るという行為には、開けないリスクと、変えられるリスクの二つが同時に乗っている。
それ以来、確定した文書を社外に出すときは、自己完結した読み取り専用の HTML を一緒に送るようにした。ファイルは一つ。ダブルクリックで開く。Office もネットワークもログインも要らないし、編集もできない。
面白いのは発想そのものではなく、受け入れ条件が測れるようになったことだ。「開けること」は検証しようがない。「相手の端末に何も入っていなくてもファイルが自立していること」なら測れる。
何で試したか
実案件のファイルで実験する気にはなれなかったので、普段書いている提案書に似せたサンプルを自作した。会社名も伝票番号も金額も人名も全部架空だ。壊れやすいものを意図的に詰め込んである。A4 レイアウト、6 か所の改ページ、3 階層の番号付きリスト、36 行 6 列の長い表、ヘッダーとフッター、脚注 4 件、画像 3 枚、数式 1 つ、そしてワードアートの表紙タイトル。28,516 バイト。
変換にはブラウザ上で動く ImgIng(https://imging.ai/)を使った。DOCX から HTML への変換はローカルで完結する。ネットワークパネルを開いたまま 6 回まわしたが、読み込みから変換完了まで GET 以外のリクエストは 0 件だった。出力は 85,903 バイトの HTML ファイル一つで、同名のリソースフォルダは付いてこない。
ちなみにサイズが 3 倍になったことには意味がない。.docx は zip アーカイブで、出力は無圧縮の単一ファイルだ。比べるべきなのは「相手が何をインストールしなければならないか」のほうだ。
確認したのは三点
ファイルが一つであること。 いわゆる「Web ページとして保存」は HTML と同名フォルダを吐くことが多い。メールに添付するときフォルダを落とせばそれで終わりだ。
外部リソースがゼロであること。 外部の script、link、img が一つでも残っていれば、それはいつか失敗するリクエストになる。オフラインの端末、社内プロキシ、数年後に移動した CDN のパス。この出力にはどれも無い。画像 3 枚は base64 で埋め込まれていて全体の 31.0%、本文とスタイルが 56.8%、スクリプトが 12.2% だった。
スクリプトを切っても本文が残ること。 相手のブラウザが社内ポリシーで制限されていたり、ブロッカーが入っていたり、組み込みの WebView だったりする可能性があるので、ここが一番気になっていた。
もう一つ、見抜きにくい失敗の型がある。.docx 丸ごととパーサーを HTML に詰め込み、開いた瞬間にブラウザ内で展開して描画する作りだ。確かに単一ファイルで、確かにオフラインで開ける。だが本文はファイルの静的なバイト列の中には無いし、結局のところ原本をそのまま渡してしまっている。JSZip、fflate、pako、mammoth、inflateRaw、OOXML の名前空間、<w: タグ、zip を base64 化したときの先頭 UEsDB、.wasm を順に検索した。すべて 0 件だった。
スクリプトを切って比べた結果
デスクトップ表示領域 1440×1000。同じファイルをスクリプト有効で一度、無効で一度開く。
最初の計測は間違っていたので、先に書いておく。document.body.innerText を本文の文字数として使ったところ、有効時 5,997 文字、無効時 3,224 文字となり、半分近く消えたように見えた。本文が落ちたと結論づける寸前で、左側のサムネイルレーンに気づいた。スクリプト有効時にはそこが各ページの内容をもう一度描画する。差分の 2,773 文字は全部その重複だった。
本文コンテナだけを測り直したら数字が揃った。両方とも 9,822 バイト、326 行。行単位の差分を取ると、有効側だけに出る行が 0 行、無効側だけに出る行も 0 行。全選択でコピーできる文字数は 3,178 文字で、こちらも両方同じ。7 ページの改ページ、各ページのヘッダー、37 行 222 セルの表、画像 3 枚、脚注 4 件はすべて元の位置に残り、コンソールエラーも 0 件だった。
検証したのはデスクトップ幅だけで、狭い画面は挙動が別なので、そこまで広げて言うつもりはない。
本文の欠落も突き合わせた。document.xml から可視段落を 286 段落抜き出し、出力側で一つずつ探して 285 段落が一字一句一致した。合わなかった一つは、脚注の上付き番号が文の途中に挿入されていただけで、文字は何も失われていない。数式は MathML になっていて選択もできた。これは期待以上だった。
代償のほう
サンプル上で崩れた箇所は三つあり、送る前に毎回そこを見ている。フッターのページ番号フィールドは再計算されず、7 ページすべてが「1 ページ / 全 10 ページ」と表示される。元ファイルに保存されていたキャッシュ値だ。表紙のワードアートは、場所だけ取って中身が空の塊になる。埋め込んだ Excel オブジェクトは丸ごと消える。36 行の表もページをまたがず、4 ページ目の高さが 1123 px から 1733 px に伸びる。画面では気づかないが、紙に出すとはみ出す。
埋め込みフォントも付いてこない。別のサンプルで所定の方式でフォント本体を .docx に埋め込んでみたが、出力に @font-face は 0 件で、フォント名だけが残った。これは見た目の問題では済まない。ページの高さは変換時に計算されてマークアップに書き込まれるので、相手の端末に同名のフォントが入っていると、文字の高さだけが 45% から倍以上に伸び、ページの枠は動かず、内容が枠の外に押し出される。実務的な結論は一つ、社外に出す文書にシステムフォント以外を使わないこと。
予想より良かった点も一つ。VBA のマクロプロジェクト、OLE オブジェクト、ActiveX コントロールをそれぞれ固有のマーカー文字列付きでサンプルに仕込み、出力を平文で検索し、さらに base64 を全部デコードしてから検索し直した。どちらも 0 件で、CFB や ZIP のファイルヘッダーも出てこない。渡すファイルに実行可能なものが一切入っていないということで、こちらの社内ではセキュリティレビューが一回減る。
一方で「Word で開いたのと同一」とは書かない。実際の Office のレンダリングとピクセル単位でもページ数でも比較していないし、そもそも改ページはフォントメトリクスやプリンタ設定に左右される。担保できるのは三つだけだ。ファイルが一つ、外部リソースがゼロ、スクリプトを切っても本文が残る。

