Python + WandでPDFの画像をpngにする

# 諸般の事情
私の住んでいるマンションの管理組合では、各棟に掲示物を貼り出すことができます（極端に政治的に偏ったものとか、公序良俗に反するものでない限りはOK）。これを管理センターの職員さんが複合機で電子データにしてgmailに送ってもらってそれをFC2ブログでも公開しています。

しかし、その電子データがPDFで、場合によって複数ページにくっついて送られてくるので、PDFの画像データを取り出して2MB以下のpngファイルにして…というのが重かったりします。最終的にはこの処理をほぼ自動化したいのですが、手始めに「PDF画像から画像を取得して、適当な名前でセーブする」までを行いました[^1], [^2]。
[^1]: 本当は掲示物を作った人が電子データをくれれば話早いのですが、年寄りの方も多くて、一部手書きとか混ざってるので、そこまで要求できません…。
[^2]: 管理センターの職員さんにもそんな高度なことは要求できないので、せいぜい「複合機でスキャンしたものをgmail宛に送る」位しかできません…。

# Python環境
## 用意したもの
* Python3.6
* [Wand](http://docs.wand-py.org/en/0.4.4/ "Wand 0.4.4")
* [ImageMagick](https://www.imagemagick.org/script/index.php "ImageMagick")
* [ghostscript](https://www.ghostscript.com/ "Ghostscript")

もっとエレガントなモジュールもありそうな気はしたけれど、簡単に済ませられるやつで…。
Wand自体はpip installで入りますが、外部プログラムでImageMagickとghostscriptは必須なので、別途インストールが必要です。

# ソースコード

```python:convert.py
import os
from wand.image import Image
from wand.display import display
from wand.sequence import Sequence

class ImageNotFound(Exception):
    def __init__(self, filename):
        self.filename = filename

class ImagesForUpload:
    def __init__(self, filename):
        self.filename = filename
        self.tnFilename = None
        self.img = []
        self.thumbnail = []
        self.sequence = 0

    def openImage(self, formatType='png'):
        img = Image(filename=self.filename, resolution=200) 
        if len(img.sequence) > 1:
            for i in img.sequence:
                nImg = Image(i)
                nImg.format = formatType
                nImg.quantize(16, 'rgb', 0, True, False)
                self.img.append(nImg)
        else:
            img.format = formatType
            img.quantize(16, 'rgb', 0, True, False)
            self.img.append(img)

        self.sequence  = len(self.img)
        self.filename = os.path.splitext(os.path.basename(self.filename))[0] 
        return self.img
    
    def makeThumbnailImage(self, width =500, formatType= 'png'):
        if len(self.img) == 0:
            raise ImageNotFound(self.filename)
        
        for (i, img) in enumerate(self.img):
            print('%d / %d consective images...' % (i+1, len(self.img)))
            tImg = img.clone()
            w = tImg.width
            h = tImg.height
            height = int((width * h) / w)
            tImg.resize(width = width, height = height)
            tImg.format = formatType
            
            self.thumbnail.append(tImg)
            
        self.tnFilename = os.path.splitext(os.path.basename(self.filename))[0] + '-s'
        
        return self.thumbnail

    def saveImage(self, saveAs=None):
        for (i, img) in enumerate(self.img):
            img.save(filename=self.filename+'-'+str(i) +'.'+img.format)

    def saveThumbnailImage(self, saveAs=None):
        for (i, img) in enumerate(self.thumbnail):
            img.save(filename = self.tnFilename + '-' + str(i) + '.' + img.format)
```

## はまりポイント
### SimpleImageオブジェクトの罠

Wandは、複数ページあるPDFデータをSimpleImageクラスに保存して、そのリストをImage.sequenceに保存しているのですが、SimpleImageクラスそのものにresizeなどを適用しても反映されません。正確には「最初の1ページだけ反映されて、残りは無視されている」ような結果になります。

ImageクラスのコンストラクタにはSimpleImageクラスを渡せるので、一個一個渡していますImageにしてしまえば何でもありにできるので。

### 色の減色に関するドキュメントがなかった

Wandは下回りは、古くからのUnixユーザならおなじみのImageMagickを使っていますが、画像中の色の減色をすることができたはずなのに、Wand側にちゃんとしたドキュメントがありませんでした。

Image.quantizeメソッドというのがあるそうですが、ここはソースがドキュメントになっていますね…。

``` python:image.py
    def quantize(self, colors, colorspace=None, tree_depth=1, dither=False, measure_error=False):
        ''' Limit the colours present in an image to a fixed amount.
            colors - The number of colors in the new image.
            colorspace - Perform color reduction in this colorspace, defaults to the image's colorspace.
            tree_depth - Default is to choose an optimal tree depth of
                         Log4(colors).  A tree of this depth generally allows
                         the best representation of the reference image with
                         the least amount of memory and the fastest
                         computational speed. In some cases, such as an image
                         with low color dispersion (a few number of colors), a
                         value other than Log4(number_colors) is required. To
                         expand the color tree completely, use a value of 8.
            dither - If True, the image is dithered.  Defaults to False.
            measure_error - If True, measures the difference between the
                            original and quantized images as an error.
                            Defaults to False.'''
```

通常は、tree_depthはデフォルト値で大丈夫です。ditherはTrue、measure_errorは今回の用途には不要なのでFalseを与えています。
マンションの掲示物なんて、そんなにフルカラー要求されることはないので、16色くらいまで豪快に落としています。

よく考えれば、2MB以下になるまでquantizeし続けて一番良い色数にする、というのもありですが、めんどくさかったので決め打ちにしてます。

# おわりに
* Wand、当初の目論見よりめんどくさかった（きっぱり）。
* 実はGmail APIでメール取得とFC2ブログにXML-RPC経由で投稿する、というところまでは作ったけれど、完成度が全然なので、もう少しこなれてから。