MS Office ドキュメントや PDF ファイルを grep したり diff したりする

[textconv]: https://github.com/kaz-utashiro/optex-textconv

以前書いたこの記事は珍しく好評だった。

- [Word や PowerPoint のファイルを grep したり diff したりする - Qiita](https://qiita.com/kaz-utashiro/items/30594c16ed6d931324f9)

これは、`greple` コマンド用の `-Mmsdoc` モジュールを元にして、`optex` コマンドを使って office ファイルに対して様々なコマンドを実行するという内容だった。

最近 PDF ファイルを git で管理する機会があり、やはりこれも git diff したいと考えた結果、前回とは異なるアプローチで実現してみた。

# pdftotext

PDF ファイルをテキストベースで diff すること自体は、さして難しくはない。`pdftotext` というコマンドを使えば、一時ファイルを作るまでもなく、たとえば bash だったらこんな風に比較することができる。

```bash
$ diff <(pdftotext A.pdf -) <(pdftotext B.pdf -)
```

`<(command)` は、bash の「プロセス置換」と呼ばれる機能で、コマンド出力をファイルのように扱うことができる。僕は、このプロセス置換を使いたいためだけに tcsh から bash に乗り換えた。

プロセス置換を使わずとも、こんな風にトリッキーに実行することもできる。打ち込むのは面倒だが、alias や関数にしてしまえばどうっていうことはない。

```sh
$ pdftotext A.pdf - | (pdftotext B.pdf - | diff /dev/fd/3 -) 3<&0
```

ただ、git のために設定しようとするとどうもうまくいかず、間にスクリプトを介さなければならなかった。実現したいのは、このような仕様の違いを吸収して、diff に限らず、コマンドの引数として普通にファイルを指定する統一的なインタフェースを提供することだ。

```sh
$ diff A.pdf B.pdf
```

# optex -Mtextconv

今回もやはり `optex` のモジュールで対応することにする。

https://qiita.com/kaz-utashiro/items/2df8c7fbd2fcb880cee6

結論としては、様々な種類のファイルをテキスト形式に変換する [textconv] というモジュールを作った。以前 `greple` のモジュールとして実現した変換機能も組み込み、単独で動作する。現在のところ、`docx`, `pptx`, `xlsx` に加えて `pdf` の拡張子に対応している。ついでに `jpg` データが指定されると `exif` コマンドの出力に置き換えるようになっているが、役に立つかどうかは不明だ。

追記: その後、`-Mtextconv` と打ち込むのが面倒になって `-Mtc` というエイリアスモジュールを同梱することにした。2020年3月リリースの Version 0.08 以降で利用可能だ。

追記: XML フォーマットの `docx` 等に加えて、`doc` と `xls` フォーマットにも対応した。(2022-12)

追記: `gpg` ファイルも処理するようにした。(2023-06)

使い方は簡単で、`optex -Mtextconv` に続けて実行したいコマンドを指定するだけだ。grep だろうが diff だろうが、なんだったら vi だって実行できる。試しにやってみたところ vi で開いて `:w` もできたが、書いたデータを読む方法はわからない。

```
$ optex -Mtextconv vi shishin4.pdf
```
<img width="768" alt="スクリーンショット 2019-04-21 15.06.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/b80fc565-7fec-b6eb-a27f-1f3d99120daf.png">


そういえば、今度は PowerPoint のページもちゃんと並び替えてます。

## PDF を比較する

本来の使い方としては、`-Mtextconv` はコマンドオプションとして指定するのだが、このモジュールの場合には `optex` のオプションに指定しても、ほぼ同じ効果を持つ。

```
$ optex -Mtextconv diff -u shishin4rc.pdf shishin4.pdf
```

<img width="1242" alt="スクリーンショット 2019-04-21 11.14.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/8c3979fc-b361-8a0d-986f-806595a447db.png">

見にくいので `diff` の代わりに `sdif` を使うとこんな風に見える。目次のドットの数が変わっているなど、目視では確認しずらいこともわかる。

```
$ optex -Mtextconv sdif -u shishin4rc.pdf shishin4.pdf
```

ちなみに、`-u` を付けずに実行した場合、古い `sdif` だとエラーになることがある。/dev/fd を使う場合特有の問題だ。最新版では修正されているので、使うのならまずそれをインストールした方がいい。

<img width="1242" alt="スクリーンショット 2019-04-21 11.09.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/fe3e897f-9055-3c68-ee4c-b968e465828d.png">

## Word と PDF も diff できる

`-Mtextconv` モジュールは、複数種類のファイルを扱うことができるので、それらを混在させることも可能だ。だから、Word と PDF を比較することもできる。ただ、構造が変わってしまうので、思ったような結果が得られるとは限らない。可能なら、文章構造を保っている Word ファイルで比較する方が、有用な結果が得られる。

```
$ optex -Mtextconv sdif -u hakodate-rc.docx hakodate-gaiyo.pdf 
```

<img width="1242" alt="スクリーンショット 2019-04-21 11.39.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/ba7c66e3-3de2-db2c-7219-297a66f0540c.png">

## diff.rc

前の記事にも書いたが、`~/.optex.d` 以下に、コマンド毎の設定ファイルを置くことができる。`diff` コマンドの設定ファイルは `~/.optex.d/diff.rc` なので、ここに以下のような設定を書いておくと、自動的に `-Mtextconv` オプション付きで実行される。さらに `optex --ln diff` で `~/.optex.d/bin` にシンボリックリンクを作ってパスに通しておけば、単に `diff` コマンドを実行するだけでテキストベースでの比較ができるようになる。これで当初の目的は達成した。

```~/.optex.d/diff.rc
option default --textconv
option --textconv -Mtextconv $<move>
```

# git の設定

変換ツールを統一したので git の設定は単純だ。`~/.config/git/attributes` ファイルに以下のような設定をして、ファイル拡張子に対する属性を定義する。全部同じにしてしまっても別に構わない。

```toml:~/.config/git/attributes
*.docx   diff=msdoc
*.pptx   diff=msdoc
*.xlsx   diff=msdoc
*.pdf    diff=pdf
```

そして `textconv` にテキストに変換するコマンドを指定する。といっても、`optex` 経由で `cat` を実行するだけだ。ちなみに、これを `pdftotext` コマンドでやろうとすると、標準出力に出すオプションがなくてちょっと困る。

```
$ git config --global diff.msdoc.textconv "optex -Mtextconv cat"
$ git config --global diff.pdf.textconv "optex -Mtextconv cat"
```

```toml:~/.gitconfig
[diff "msdoc"]
	textconv = optex -Mtextconv cat
[diff "pdf"]
	textconv = optex -Mtextconv cat
```

この状態で `git diff` を実行すると、自分の場合、出力は `sdif` を通して表示される。

<img width="1242" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/bb26be10-8298-be7e-de79-ad4cfa9e6cee.png">

git の設定には diff コマンドを指定する方法もあるが、それは以前の記事を参照して欲しい。diff を `optex` 経由で実行すればいいだけだ。

# 実は URL も diff できる

実は URL が指定されると w3m コマンドで変換するようになっているので、URL を diff することできる。たとえば、以下のようにすることで Macbook と Macbook Air の仕様を比較できる。

```
$ optex -Mtextconv sdif --non -u 'https://support.apple.com/kb/'{SP757,SP783}'?viewlocale=ja_JP&locale=ja_JP' 
```

<img width="1202" alt="スクリーンショット 2019-04-21 14.04.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/7b1d5a0e-0553-4c99-52ba-9b3691650d19.png">


# インストール

## optex::textconv

cpanminus をインストールしてからでもいいが、とりあえずこれでインストールはできる。たまにしか使わないなら、むしろこの方がいいかも。

```
$ curl -sL http://cpanmin.us | perl - App::optex::textconv
```

## sdif

`sdif` を使いたい場合は [App::sdif](https://github.com/kaz-utashiro/sdif-tools) をインストールする。

```
$ curl -sL http://cpanmin.us | perl - App::sdif
```
細かい設定等は前の記事に書いてあるので見てください。


# 拡張性

モジュール構成はこんな風になっている。`-Mtextconv` モジュールには変換機能は何も入っていなくて、`default.pm` と `msdoc.pm` という2つのモジュールに実装されている。

```
App/optex/
    ├── textconv.pm
    └── textconv/
        ├── Converter.pm
        ├── default.pm
        └── msdoc.pm
```

`default.pm` の中身は `@CONVERTER` という配列にファイル名のパターンと変換するコマンドを指定してあるだけだ。ファイル名は `sprintf` の引数で与えられるので `%s` の部分に展開される。ユーザ設定で簡単に増やせるようにはなっていないが、この部分を編集すればいくらでも拡張は可能だ。

```
package App::optex::textconv::default;
use App::optex::textconv::Converter 'import';
our @CONVERTER = (
#   [ qr/\.pdf$/i    => "pdftotext -nopgbrk \"%s\" -" ],
    [ qr/\.jpe?g$/i  => "exif \"%s\"" ],
    [ qr[^https?://] => "w3m -dump \"%s\"" ],
    );
1;
```

`msdoc.pm` の方はもう少し凝っていて、中でテキストへの変換を実装している。この場合、`to_text` というサブルーチンが定義されていると、それを呼ぶインタフェースになっている。独自の変換モジュールを作りたい場合は、同じように `to_text` を定義すればいい。

# PDF

特に PowerPoint が生成したものの場合そうなのだが、PDF ファイルのページの切れ目がわかった方が見やすい。そこで PDF は `pdf.pm` という特別のモジュールで処理するようにした。デフォルトではページ境界にダッシュ78個の行を出力する。オプションでページ番号を表示したり、単に改ページ記号を出力することもできる。

# 外字と未定義文字

PDF ファイルには外字が含まれていることがある。また、理由はわからないが Word ファイルの中に Unicode の未定義文字が含まれていることがある。

そこで、Unicode のプライベート領域と未使用領域の文字は「〓」に置き換えるようになっている。

Version 0.05

# SEE ALSO

https://github.com/kaz-utashiro/optex-textconv