Instantly share code, notes, and snippets.

@3846masa /TBF2017_寄稿.md Secret
Last active Sep 17, 2017

Embed
What would you like to do?
技術書典 寄稿 2017/04

O'CREILLY

こんにちは、 O'CREILLY (オクライリー)^ocreillyです。 「役立つことはなさそうなのでお蔵入りだが面白い」をモットーに ソフトウェア技術の同人誌を刊行しています。 今回は拡張子にまつわるお蔵入りをお届けします。 少しでも興味を持った方はぜひサイトや同人イベントで会いにいらしてください。

拡張子とは

「拡張子を .pdf に変えただけの Word ファイルが送られてきた」 パソコンに慣れていない人によくありがちな間違いです。 一笑しがちな話ではありますが、ちょっと真剣に考えてみましょう。 そもそも「拡張子」とは何なのでしょうか。

俗に拡張子の説明は、『ファイルの種類を示す。』^def-extension とされます。 これが誤解を招く要因でしょう。 拡張子を正確に言い表すならば、「プログラムに該当ファイルをどのように扱ってほしいかを示す文字列」となります。 つまり、拡張子を変えてもファイル自体は変わらないわけです。

当たり前の話で退屈だったかもしれませんが、ここからが本題です。 「拡張子がファイルの種類を示す」ならば、 「拡張子を変えるとファイル自体が変わる」という流れは自然に想起されます。 自然に想起されるというならば、システムもそうあるべきです。 今回は拡張子を変えるとファイル自体が変わる世界を実現してみます。

拡張子でフォーマットが変わる文章ファイル

拡張子が .docx ならば Word ファイルとして、 .pdf ならば PDF ファイルとして 認識されるファイルを作ります。 さきほど述べた通り、拡張子は「どのように扱ってほしいか」を示すだけです。 つまり、どちらのフォーマット解釈でも正しいファイルを作る拡張子を変えるだけでファイルが入れ替わるように見せることができます。

2種類のファイル形式を合わせる方法としては、 一方のファイル構造を保ちつつ、もう一方を埋め込む手段があります。 そのためには、それぞれのファイルがどのような構造であるかを考える必要があります。 今回は .docx.pdf を埋め込む方針で進めていきましょう。

Office Open XML フォーマット

.docx は、 Office Open XML フォーマットの拡張子です。 いわゆる Office 系ソフトウェアの標準フォーマットとして制定されています。 このフォーマットの興味深いところは、 文書ファイルを構成する XML やその他ファイル群を ZIP 形式でまとめているところです。 端的に言えば、 .docx のファイルは ZIP 形式としても扱えます。 実際に試してみましょう。 適当な .docx ファイルの拡張子を .zip に変えて展開します。 そのあと、任意のアーカイバソフトで ZIP 圧縮します。 最後に拡張子を .zip から .docx に変えて開いてみましょう。 問題なく文章が表示されると思います。

ZIP 形式であれば、ファイルの追加も容易に行なえます。 しかしながら、フォーマットに沿わないファイルを含む .docx ファイルは、 読み込みエラーが発生してしまいます。 さきほど展開した中に適当なファイルを入れて、再度圧縮して拡張子を変えます。 Word では、以下の画像のようなエラーが表示されるでしょう。

破損ファイルとして扱われる

フォーマットに沿ってファイルを埋め込む方法として、 Object Linking and Embedding の仕組みを使います。 Word ではメニューから行えるので、簡単にファイル埋め込みを実現できます。 埋め込んだファイルは、 ZIP 展開するとword/embeddings/oleObject1.binに見つかります。

Word にはファイル埋め込み機能がある

ZIP フォーマット

ZIP はアーカイブファイルフォーマットです。 通常、収容されたファイルは圧縮されており、元のデータとは異なります。 これでは、収容した PDF をそのまま読み込むことができません。 しかし、 ZIP 形式ではファイルごとに圧縮・非圧縮を選択できます。 例えば、次の Node.js プログラムでは収容ファイルをすべて非圧縮にできます。

const fs = require('fs');
const JSZip = require("jszip");

fs.readFile("example.docx", (_err, data) => {
  JSZip.loadAsync(data).then((zip) => {
    zip.generateNodeStream({
      compression: 'STORE',
    })
    .pipe(fs.createWriteStream('export.docx'))
    .on('finish', () => console.log('done.'));
  });
});

続いては、ZIP ファイルでの格納順序です。 後述する PDF の仕様に合わせるために、 埋め込んだ PDF ファイルを含むデータ群を 先頭に移動します。 ZIP の仕様に沿って、オフセットなどを調整しながら 入れ替えるプログラムを書けばよいでしょう。^zip-note

PDF フォーマット

今回 PDF は埋め込まれる側ですから、 内部の細かい仕様などは気にする必要はありません。 ただし、ファイルの開始位置・終了位置については知っておく必要があります。

PDF は、 %PDF-M.n のようなバージョン指定から始まり、%%EOF で終わります。 このバージョンを含むヘッダーを読み込めるようにして、 Word ファイルに埋め込む必要があります。 ヘッダーの仕様をもう少し見ていきましょう。

PDF の仕様は Adobe のサイト^adobe-siteから閲覧することができます。 今回は version 1.7 のもの^adobe-pdfを参照していきます。 仕様にある APPENDIX H の H.3 Implementation Notes には、 実装についての話が書いてあります。 H.3.13 ではヘッダーに関する次の文章が掲載されています。

Acrobat viewers require only that the header appear somewhere within the first 1024 bytes of the file.

簡単に言うと、「先頭から 1024 バイトにヘッダーがあるようにすること」になります。 実際に、 Mozilla の開発する PDF.js^pdfjs の実装を見ると、 先頭 1024 バイトの中で %PDF- の文字列を検索しています。^pdfjs-github

つまり、 PDF を他のファイルに埋め込む場合には、 先頭 1024 バイトより離れた場所では読み込めないといえます。 この仕様を守るためにも ZIP に格納する順番は、 PDF ファイルが先頭になるようにしなければなりません。

実際に試してみる

以上の話から、Word ファイルと PDF ファイルの混合ファイルは次のようにして作成できます。

  1. Word ファイルに PDF ファイルを OLE Object として埋め込む
  2. 埋め込んだ OLE Object を非圧縮方式で ZIP 形式にて保存し直す
  3. ZIP 形式内での OLE Object の順序を先頭にするようプログラムで編集する

実際に試して作った結果、 Word ファイルとして開くことには成功しました。 また、 PDF ファイルとして Microsoft Edge や Google Chrome などのブラウザで開くことにも成功しました。 しかし、残念ながら Acrobat Reader DC で開いた際にエラーが発生してしまいました。 これは Acrobat Reader が、既存の他フォーマットのマジックナンバーと検証していることに起因します。 他フォーマットであると判定された場合は、正しい PDF フォーマットでも開けませんでした。

Adobe 公式のツールで開けないため、失敗とも言えます。 Windows 10 から PDF を開く標準ソフトとなった Edge で開けるため、成功とも言えます。 Word ファイルと PDF ファイルを拡張子を変えるだけで変換することは、 技術的には可能であると今回は結論づけたいと思います。 ただし、ビュアー依存を考えると実用性には乏しいのでお蔵入りですね。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment