先週、Word の提案書を渡されて「HTML にしたら Word と全く同じ見た目になるのか」と聞かれました。ならない、と答えたら責任逃れだと思われたようです。そのときの自分の説明は「フォーマットが違うので」だけで、これでは何も言っていないのと同じでした。なので自分でサンプルを作って、実際にどこがどう変わるのかを測ってみました。
サンプルはテンプレートを使わず OOXML を手書きしました。A4 の用紙設定、明示的な改ページ 6 か所、36 行 6 列の長い表(先頭行に w:tblHeader)、3 階層の番号付きリスト 22 項目、画像 3 枚、脚注 4 件、ヘッダーとフッター、フッターには PAGE と NUMPAGES のフィールド、表紙には VML のワードアートを置いています。会社名も伝票番号も金額も全部こちらで作った架空のもので、実在の文書ではありません。変換には ImgIng(https://imging.ai/)の DOCX → HTML を使いました。処理はブラウザ内で完結していて、Network パネルを開いたまま 6 回まわして非 GET リクエストは合計 0 件でした。この条件は結構重要で、サーバーが介在しない以上、後から見つかる差分はすべて解析とレイアウト再構築の層に帰着します。
.docx は「ページの終わり」を持っていない
zip を展開して word/document.xml の中身を数えてみました。w:pgSz は 11906×16838 twips、つまり A4 そのもの。w:type="page" の明示的な改ページは 6 か所で、これは自分で入れたものです。そして lastRenderedPageBreak は 0 件でした。これは Word がレンダリング後にファイルへ書き戻すキャッシュで、「前回はここで行が折れた」という記録にすぎません。手書きのパッケージには当然入っていませんし、入っていたとしても記録であって約束ではありません。word/settings.xml にはもう一つ compatibilityMode(自分のファイルでは 15)があり、どの世代の組版ルールで計算するかを決めています。値を変えれば同じ本文が別の位置で折れます。
つまり .docx が渡してくるのは、用紙サイズ、余白、フォント名、行間、そして「必ずここで切る」数か所だけです。3 ページ目の最後の行がどの一文かという答えはファイルの中に存在せず、開いたプログラムがその場で計算します。計算にはグリフごとの幅と高さが要り、その字形は開く側のマシンにあるフォントから来ます。Word はさらにプリンタードライバーが返すメトリックまで計算に混ぜます。フォントの有無とプリンターのメトリックは、2 台のマシンの間で最も一致しにくい二つです。同じ .docx が同僚の PC で違うページ数になるのはそのためで、HTML 変換は計算する主体が変わっただけです。
基準がなくても分かる 3 つの差
成果物は 85,903 バイトの単一 HTML で、外部リソースは 0 件、画像 3 枚は base64 で埋め込まれ全体の 31% を占めます。本文の再現は良好で、元文書の 286 段落が 286 件一致、全選択で 3,178 文字コピーできました。それでも、Word と突き合わせなくても見える欠けが 3 つあります。
一つ目はフッターのページ番号です。成果物は 7 ページですが、2 ページ目から 7 ページ目までの 6 ページすべてに「第 1 页 共 10 页」(1 ページ/全 10 ページ)と出ています。元ファイルの PAGE と NUMPAGES にはキャッシュ値の 1 と 10 が添えられていて、フィールドが評価されないままキャッシュ値だけが運ばれた形です。ビューアー自身のツールバーには 01 / 07 と出ているのに、です。
二つ目は長い表。7 つのページ枠のうち 6 つは 794 x 1123(96 dpi の A4)ですが、4 ページ目だけ 794 x 1733 でした。36 行の表はページ分割されず、そのページ自体が 1,123 px から 1,733 px まで引き伸ばされています。w:tblHeader も失われていて <th> は 0、<thead> もありません。このページを印刷すると A4 からはみ出します。
三つ目は表紙のワードアートで、<svg style="width:430pt;height:46pt" width="0" height="0"> という空の枠だけが残りました。573×61 px の領域を占めながら、テキストノードもパスも 0 です。Excel の OLE オブジェクトを入れた別サンプルでも同じで、520×300 の静的プレビュー画像ごと消え、空の <p> が 2 つ残るだけでした。
同じエンジンでも PPTX は安定する
棒グラフ、円グラフ、SmartArt、7×5 の表、回転した図形 4 つ、2 秒の埋め込み動画を入れた 8 枚の四半期レビューも作って同じ場所で変換しました。まず元ファイルのキャンバスを読みます。
P = '{http://schemas.openxmlformats.org/presentationml/2006/main}'
deck = ET.fromstring(zipfile.ZipFile('B_review.pptx').read('ppt/presentation.xml'))
sz = deck.find(P + 'sldSz')
cx, cy = int(sz.get('cx')), int(sz.get('cy'))
print('キャンバス EMU:', cx, cy, '比率', round(cx / cy, 4))
出力は キャンバス EMU: 12191695 6858000 比率 1.7777。成果物は 8 枚とも data-width=1280 data-height=720 で比率 1.7778。回転角は −22°、15°、−160°、−28° で、元の 338°、15°、200°、332° と一つずつ対応します。表は本物の <table> のまま 7 行 35 セルでした。
ここでも失われるものはあります。2 つのグラフは Canvas に描かれてから PNG として固定され、軸の分類名もデータラベルも成果物の HTML 内に 0 回しか出てきません。選択できるのは HTML で描かれた凡例だけです。SmartArt はベクターのままで、<svg> が 8 個、5 つの工程名はすべて選択できました。アニメーションと画面切り替えは実行されず、CSS の @keyframes は 0 です。
差が出るのは努力の量ではなく、それぞれの形式が何を保存しているかです。PPTX が持つのはキャンバスサイズとオブジェクトごとの絶対座標・回転角で、環境に依存しない数値なのでどこへ持っていっても同じに計算できます。DOCX が持つのは再流し込みのパラメーターで、最終的なレイアウトは読み手のマシンにあるメトリックから計算し直すしかありません。固定レイアウトのものは固定レイアウトに変換でき、リフロー型のものは近似しかできない、というのが今の自分の説明です。送る前に見るのはフッターのページ番号、ワードアートの見出し、埋め込みの表オブジェクトの 3 か所だけです。
一つ断っておくと、このマシンには Word も LibreOffice もないので、権威あるページ数と突き合わせてはいません。上のページ数に関する記述はすべて、自分が入れた 6 か所の改ページに対して照合したものです。3 つの欠けは基準なしで見えるので、言い切れるのはその 3 つだけです。

