画像サイズからセルサイズを自動判定し、いい感じでマージンなんかもいれつつ、openpyxlでエクセルに画像を挿入する

# はじめに
以前書いた[SDFを構造式の画像も含めてサクッとエクセルに変換する](https://qiita.com/kimisyo/items/5a8b1a07bdc0ec33398b)の続きであるが、いまいち記事の反響がなかったのでタイトルをより一般向けにしてみた。

前回SDFから構造式画像を生成したのだが、セルのサイズを手動設定した点に敗北感が残っているため、勝手にリベンジしてみる。

# 環境
こんな感じ。

- python
- openpyxl
- RDKit 

# ソース
はい、いきなりのどん。解説は後程。

```python
import argparse
from rdkit import Chem
from rdkit.Chem import AllChem
from rdkit.Chem.Draw import rdMolDraw2D
import openpyxl
from openpyxl.styles import numbers 
from openpyxl.drawing.spreadsheet_drawing import AbsoluteAnchor, OneCellAnchor, AnchorMarker
from openpyxl.drawing.xdr import XDRPoint2D, XDRPositiveSize2D
from openpyxl.utils.units import pixels_to_EMU, cm_to_EMU, pixels_to_points
import openpyxl.drawing.image
from io import BytesIO
from cairosvg import svg2png
import PIL


def is_float(value):
    try:
        float(value)
        return True
    except ValueError:
        return False

def generate_image(mol, size):

    image_data = BytesIO()
    view = rdMolDraw2D.MolDraw2DSVG(size[0], size[1])
    tm = rdMolDraw2D.PrepareMolForDrawing(mol)

    view.SetLineWidth(1)
    view.SetFontSize(1.3 * view.FontSize())
    option = view.drawOptions()    
    option.multipleBondOffset = 0.09
    option.padding = 0.11
    view.DrawMolecule(tm)
    view.FinishDrawing()
    svg = view.GetDrawingText()
    svg2png(bytestring=svg, write_to=image_data)
    #image_data.seek(0)
    return PIL.Image.open(image_data)        

def set_sell_image(ws, row, col, mol, size=(300, 150), margin=5, rate=1.0):
    # https://stackoverflow.com/questions/55309671/more-precise-image-placement-possible-with-openpyxl-pixel-coordinates-instead
    image = generate_image(mol, (int(size[0]*rate), int(size[1]*rate)))
    image =  openpyxl.drawing.image.Image(image)
    col_offset = pixels_to_EMU(margin)
    row_offset = pixels_to_EMU(margin)
    size_ext = XDRPositiveSize2D(pixels_to_EMU(size[0]), pixels_to_EMU(size[1]))
    maker = AnchorMarker(col=col, colOff=col_offset, row=row, rowOff=row_offset)
    image.anchor = OneCellAnchor(_from=maker, ext=size_ext)
    ws.add_image(image) 

def main():
    # 引数の解釈
    parser = argparse.ArgumentParser()
    parser.add_argument('-i', type=str, required=True)   
    parser.add_argument('-o', type=str, required=True)
    parser.add_argument('-x', type=float, default=150)
    parser.add_argument('-y', type=float, default=100)
    parser.add_argument('-r', type=float, default=1)
    args = parser.parse_args()

    #ピクセル単位からエクセルの単位への変換
    IMAGE_MARGIN_PX = 5
    EXEL_WIDTH_UNIT = (args.x + IMAGE_MARGIN_PX * 2)/8
    EXEL_HEIGHT_UNIT = pixels_to_points(args.y + IMAGE_MARGIN_PX * 2)

    # SDFの読み込み(1回目はパラメータ名を全て読み取る)
    sdf_sup = Chem.SDMolSupplier(args.i)
    props = []
    props.append("_Name")
    for mol in sdf_sup:
        for name in mol.GetPropNames():
            if name not in props:
                props.append(name)

    # SDFの読み込み(2回目は化合物のパラメータを取得。なければエラー)
    datas = []
    mols = []
    sdf_sup = Chem.SDMolSupplier(args.i)
    for mol in sdf_sup:
        # 名前の取得
        data = []
        if mol is not None:
            for name in props:
                if mol.HasProp(name):
                    value = mol.GetProp(name)
                    data.append(value)
                else:
                    data.append(None)
            datas.append(data)
            mols.append(mol)

    wb = openpyxl.Workbook()
    wb.remove(wb.worksheets[-1])
    ws = wb.create_sheet(title="Result")
    wb["Result"].cell(1,1).value = "Image"
    for i, prop in enumerate(props):
        wb["Result"].cell(1,(i+2)).value = prop

    # 横幅の設定(1列目)
    wb["Result"].column_dimensions['A'].width = EXEL_WIDTH_UNIT

    for i, data in enumerate(datas):
        # 縦幅の設定(2列目)
        wb["Result"].row_dimensions[(i+2)].height = EXEL_HEIGHT_UNIT

        # 画像を最初の列に入れる
        set_sell_image(wb["Result"], i+1, 0, mols[i], size=(args.x, args.y), margin=IMAGE_MARGIN_PX, rate=args.r)
        # 2列目以降はプロパティを格納する。
        for j, prop in enumerate(props):
            wb["Result"].cell((i+2), (j+2)).alignment = \
                    openpyxl.styles.Alignment(wrapText=True, vertical="center")
            if data[j].isdecimal():
                wb["Result"].cell((i+2), (j+2)).number_format = numbers.FORMAT_NUMBER
                wb["Result"].cell((i+2), (j+2)).value = int(data[j])
            elif is_float(data[j]):
                wb["Result"].cell((i+2), (j+2)).number_format = '0.000'
                wb["Result"].cell((i+2), (j+2)).value = float(data[j])
            else:
                wb["Result"].cell((i+2), (j+2)).value = data[j]

    wb.save(args.o)


if __name__ == "__main__":
    main()        
```

## 解説
化合物からプロパティを読み込んだり、画像を生成したりする部分は、前回説明済のため割愛する。

- 引数のxとyにより画像サイズがピクセル単位で指定されると、```EXEL_WIDTH_UNIT```, ```EXEL_HEIGHT_UNIT```の変数に対し、画像サイズおよびマージン(ここでは5px固定とした)からセルの幅と高さを算出し、設定する。ポイントとしては、エクセルにおいて **行の高さの単位は「ポイント (pt)」、列幅の単位は「文字数」** である点であり、それぞれ異なったロジックでピクセルから変換している。列の幅の方はフォントサイズなんかも絡んでくるようで、残念ながら、**頑張って定規で測って導き出した**マジックナンバー8で割り算した。列の高さはopenpyxlの関数を使えば簡単である。そして、これらの値で、画像が表示される列の幅と行の高さを設定する。
- ```set_sell_image```関数の中で、化学構造式画像を生成する。その後```pixels_to_EMU```関数を使って、ピクセルで指定したマージンのサイズをEMUという基本単位に変換する。次の ``` size_ext = XDRPositiveSize2D(pixels_to_EMU(size[0]), pixels_to_EMU(size[1]))``` にてエクセルに張り付ける画像サイズについてもピクセルからEMUに変換している。さらにその次の```maker = AnchorMarker(col=col, colOff=col_offset, row=row, rowOff=row_offset)```で、画像を張り付けるセルの情報とマージンを指定している。最後に```OneCellAnchor```により画像を張り付けることで、先ほど設定した列の幅と行の高さの中に、きれいに画像が収まるはずだ。
- ちなみに```OneCellAnchor```の```ext```オプションで指定する画像サイズは、**エクセルに表示される画像サイズ**であり、それより大きな画像を指定すると縮小して表示される。したがってあらかじめ大きな画像を生成しておけば、**エクセル上で画像を拡大した時に、元の大きな解像度で画像が見れるので便利** である。

# 使い方
こんな感じで、-i オプションに入力SDFを、-oオプションに出力xlsxファイル、-x でエクセルに表示させる画像の幅(ピクセル)、-y でエクセルに表示させる画像の高さ(ピクセル)、-r でエクセルの表示サイズの何倍の大きさの画像生成するか、を指定する。

```sh
python sdf2xlsx.py -i solubility.test.sdf -o solubility.test.xlsx  -x 150 -y 100 -r 2  
```

前回と大きな違いはないがこんな感じで表示される。マージが考慮されているため画像のセルの枠線がきちんと見えているのがうれしい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/247c5b9f-19c0-4745-4d32-f2976e25b752.png)

なお、原子数が多く表示サイズだと潰れて見える化合物について、r オプションを1にした場合と3にした場合で、エクセル上で拡大した時の見え方の違いをお見せする。左がr=1、右がr=3 の場合であり左は拡大しても良く見えないが、右はきれいに見えており違いは一目瞭然である。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/5d27744d-b031-5fea-c340-2fe06ef7b92d.png) ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/410ea3d8-55ce-ee2b-8846-fec4a24e2e49.png)

# おわりに
化合物ネタではあったが、エクセルに画像を表示したい方に本記事がお役に立てれば幸いである。

# 参考
- [More precise image placement possible with openpyxl? (Pixel coordinates instead of cell coordinates?)](  https://stackoverflow.com/questions/55309671/more-precise-image-placement-possible-with-openpyxl-pixel-coordinates-instead)