8
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-19

はじめに

以前書いたSDFを構造式の画像も含めてサクッとエクセルに変換するの続きであるが、いまいち記事の反響がなかったのでタイトルをより一般向けにしてみた。

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

環境

こんな感じ。

  • python
  • openpyxl
  • RDKit

ソース

はい、いきなりのどん。解説は後程。

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により画像を張り付けることで、先ほど設定した列の幅と行の高さの中に、きれいに画像が収まるはずだ。
  • ちなみにOneCellAnchorextオプションで指定する画像サイズは、エクセルに表示される画像サイズであり、それより大きな画像を指定すると縮小して表示される。したがってあらかじめ大きな画像を生成しておけば、エクセル上で画像を拡大した時に、元の大きな解像度で画像が見れるので便利 である。

使い方

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

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

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

image.png

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

image.png image.png

おわりに

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

参考

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4