画像のExcelize


## はじめに

この時事がQiita初投稿です。
書き方等不適切な点などがありましたら，コメントで教えていただけるとありがたいです。
また，ソースコードについてもよりいい書き方などがありましたらコメントにてお知らせください。

## 動機

某学校のとある授業で，「エクセルで思い通りのグラフを出力できるようになる」という目標がありました。
しかし，ExcelをはじめとするOffice製品は勝手に書式を変えたりして思うように動いてくれません。
そこで，「他のツールで作成したグラフをビットマップとしてExcelに入れてしまえばいいのではないか!」ということに気づいてしまい，画像のExcel化(勝手にExcelizationと呼んでいます)ツールを作成しました。

## 環境

- Windows 10
- Python 3.7.4

Pythonはコマンドプロンプトから呼んで実行しました。

### ライブラリ

画像の読み込みにはPIL，Excelの操作にはopenpyxlを使用しました。
また，進捗の表示にtqdmを使用しています。
それぞれ`pip install Pillow` ，`pip install openpyxl`，`pip install tqdm`でインストールできます。
(必要に応じて`sudo`などは足してください)

## ソースコード

```python:excelize.py
from itertools import product
from numpy import array
from openpyxl import Workbook
from openpyxl.styles import PatternFill
from openpyxl.utils import get_column_letter
from os.path import splitext
from PIL import Image
from sys import argv
from tqdm import tqdm


# セルのアドレス(0始まり)からExcel形式のアドレスを作成
def get_addr_str(col, row):
  return get_column_letter(col+1) + str(row+1)


# カラーコードの先頭に必要なだけ0を足す
def padding(value, length=6):
  return '0'*(length-len(value)) + value


def main():
  args = argv[1:]
  for arg in args:
    # 保存するブック名は画像の拡張子を".xlsx"に変更したもの
    dst = splitext(arg)[0] + ".xlsx"
    print(f"Src: {arg}")
    print(f"Dst: {dst}")

    # 画像読み込み
    try:
      print('Loading... ', end='')
      img = Image.open(arg)
      print('Complete')
    except IOError:
      print(f"\nCould not open the image: {arg}")
      continue

    # 画像のサイズを取得し，各ピクセルを配列に格納
    width, height = img.size
    bmp = array([[img.getpixel((i,j)) for j in range(height)] for i in range(width)])

    print(f"Size: {width}x{height}")

    # Excelのブックを作成
    wb = Workbook()

    # シートの名前を変更し，グリッド線を消す
    ws = wb.active
    ws.title = "img"
    ws.sheet_view.showGridLines = False

    # 画像となる範囲のセルの大きさを1x1(ピクセル)に設定
    for i in range(width):
      ws.column_dimensions[get_column_letter(i+1)].width = 1/11
    for i in range(height):
      ws.row_dimensions[i+1].height = 1.0

    # 各ピクセルの色をExcelに反映
    for x, y in tqdm(product(range(width), range(height)), total=width*height):  # totalを指定しないと進捗バーが表示されない
      rgb = bmp[x][y]  # 対象のピクセルのRGBを取得
      color = (rgb[0] << 16) | (rgb[1] << 8) | (rgb[2])  # RGBを1つの数値にまとめる
      color = (color & 0x00F8F8F8) | 0x00040404  # 32,768色に減色
      hcolor = "00" + padding(hex(color)[2:])  # "AARRGGBB"の16進数表現に変換(ここではAAは00)
      ws[get_addr_str(x, y)].fill = PatternFill(start_color=hcolor, end_color=hcolor, fill_type='solid')  # Excelのセルに色を設定

    # ブックを保存
    print('Saving... ', end='')
    wb.save(dst)
    print('Complete')

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

## 実行方法

コマンドラインで

```console
$ python excelize.py foo.png bar.png
```
のようにしてあげればそれぞれのファイルがExcelizeされます。
読み込みに失敗した画像はエラーを吐いて飛ばされます。

## 実行例

こんな感じのExcelができます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/211353/e4d135bb-d15f-9433-2dbf-f8a1838f02f6.png" width="80%">

また，ExcelのスクリーンショットをExcelizeして，セルの結合をすることで究極のエクセル方眼紙(?)を作ることもできます。

## 最後に

我ながらどうでもいいものを作ってしまった感じがします。
実用的には何の役にも立ちませんが，こういうバカっぽいことができるんだな，と笑ってもらえれば幸いです。

## 追加・変更点など

### 追加 (2018/08/31)

[ExcelizeをPowerShellで実装した方の記事](https://qiita.com/takaaki3110/items/f1e35596520280fc34b6)で知ったのですが，1600万色で塗りつぶすとブックが開けなくなるらしいです。
3万2000色に落とせば問題ないそうなので，減色処理を追加しました。
(テストに使った画像が少なかったので気づきませんでした)

`hex`関数で頭のゼロが落とされてしまう問題も発覚したので，足りない分のゼロを足す処理も追加しました。

さらに，ついでといっては何ですが進捗を表示するようにしました。

### 変更 (2019/11/22)

ピクセルのイテレートを，ネストされた`for`から`itertools.product`に変更しました。
また，カラーコードのパディング処理を若干変更しました(内部処理を変更しただけで，動作は変わりません)。
(ついでに文章の添削も行いました)

## 参考文献

- [初めてのPython画像処理](https://qiita.com/uosansatox/items/4fa34e1d8d95d8783536)
- [Python openpyxlでExcelを操作](https://qiita.com/tftf/items/07e4332293c2c59799d1)