たくさんの写真を敷き詰めたスライドの生成【python】【python-pptx】

#概要
PowerPointのスライドに、長方形の画像を敷き詰めて貼り付けるプログラムを書きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/1b27b6f6-ca44-5115-551e-8e4e5c1d1f64.png)

#動機
記念ムービー的なもので、たくさんの写真を敷き詰めた画像がほしかったので、作りました。
また、作ったあとで微修正できるように、画像出力ではなくpptxファイルとして出力するようにしました。
#方針
長方形の画像を隙間なく敷き詰めたpptxスライドを作ることが目的です。
長方形であれば異なる（＝相似でない）画像が混在していても構いません。
貼り付ける際に拡大縮小は可能ですが、元画像のアスペクト比は維持することとします。

画像は何でも良いですが、本稿では[Wikipediaからダウンロード](https://qiita.com/shimajiroxyz/items/bebcdb06b47c6b399063)した国旗の画像を例として説明します。

#環境

```
% sw_vers
ProductName:	macOS
ProductVersion:	11.2.3
BuildVersion:	(略)
% python -V
Python 3.8.0
```
#インストール

```
pip install python-pptx
pip install Pillow
```

#実装
##左寄せ
まずは細かいことを考えずに、画像を左上から右に敷き詰めていき、それ以上敷き詰められなくなったら次の行にいく、というコードを書いてみます。画像を並べる順番は事前に定義したもの（例えばファイル名のアイウエオ順、など）で固定とします。

```Python
from pptx import Presentation
from PIL import Image
import os

#スライドサイズ
#4:3 (default) 9144000x6858000
#16:9 12193200x6858000
SLIDE_WIDTH, SLIDE_HEIGHT = 12193200, 6858000
ROW_HEIGHT = SLIDE_HEIGHT // 15

#出力ファイル名
OUTPUT_FILE_PATH = "test.pptx"
#画像の格納ディレクトリ
IMG_DIR = "./data/"

#画像のサイズを取得する
def getImageSizes(img_paths):
  sizes = []
  for img_path in img_paths:
    im = Image.open(img_path)
    im_width, im_height = im.size
    sizes.append((im_width, im_height))
  return sizes
  
#左寄せするときに各画像をどの位置にどのサイズで配置すればよいかを返す
#一行あたりの高さ、スライドの幅は固定値として与える
def leftJustifiedArrangement(image_sizes, row_height, slide_width):
  left = 0
  top = 0
  height = row_height

  positions = []
  for im_width, im_height in img_sizes:
    width = im_width * height // im_height
    #画像がはみ出すなら次の行にする
    if width > (slide_width - left) and left > 0:
      left = 0
      top += row_height
    positions.append((left, top, width, height))
    left += width

  return positions



def addPictures(slide, img_paths, positions):
  for img_path, (left, top, width, height) in zip(img_paths, positions):
    slide.shapes.add_picture(img_path, left, top, height = height) #アスペクト比を変えない前提でheightのみ指定
  return slide

    
#受け取ったプレゼンテーションオブジェクトにスライドを追加し、追加されたスライドオブジェクトを返す。
def addBlankSlide(prs):
  #白紙スライドの追加(ID=6は白紙スライド)
  blank_slide_layout = prs.slide_layouts[6]    
  slide = prs.slides.add_slide(blank_slide_layout)
  return slide

if __name__ == "__main__":

  #スライドオブジェクトの定義
  prs = Presentation()
  #スライドサイズの指定
  prs.slide_width = SLIDE_WIDTH
  prs.slide_height = SLIDE_HEIGHT

  #img画像のファイル名を取得
  img_files = os.listdir(IMG_DIR)
  #pngで終了するファイル名のみ抽出。貼り付けたい画像の拡張子に応じて変える
  img_paths = [os.path.join(IMG_DIR,name) for name in img_files if 
  name.endswith(".png") or name.endswith(".jpg")]
  #昇順にソート（この順番でスライドに貼り付けられる）
  img_paths.sort()#昇順にsort

  slide = addBlankSlide(prs)
  img_sizes = getImageSizes(img_paths)
  positions = leftJustifiedArrangement(img_sizes, ROW_HEIGHT, SLIDE_WIDTH)

  slide = addPictures(slide, img_paths, positions)
  prs.save(OUTPUT_FILE_PATH)
```

出来上がったpptxファイルを開くと以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/1b27b6f6-ca44-5115-551e-8e4e5c1d1f64.png)


ポイントは`leftJustifiedArrangement`という関数で、ここで、画像を配置するポジションを計算しています。
この関数を書き換えると、左寄せ以外のレイアウトも可能になります。


##右寄せ

右寄せしたい場合は、左寄せしたときの各行の余ったスペースの文だけずらせばよいです。
具体的には、`leftJustifiedArrangment`を以下の関数で置き換えます。

```Python
def rightJustifiedArrangement(img_sizes, row_height, slide_width):
  if len(img_sizes)  == 0: return []

  left = 0
  top = 0
  height = row_height

  lines = [[]] 
  for im_width, im_height in img_sizes:

    width = im_width / im_height * row_height
    #画像がはみ出すとき、行を更新する
    if width > (slide_width - left) and left > 0:
      lines.append([])
      left = 0
      top += row_height
    
    lines[-1].append((left, top, width, height))
    left += width

  #右寄せにleftを修正line要素が一つでもあれば右寄せ補正してpositionsに追加
  right_justified_positions = []
  for line in lines:
    rest_width = slide_width - (line[-1][0] +  line[-1][2])#その行の残りwidth
    justified_line = [(l+rest_width, t, w, h) for l,t,w,h in line]
    right_justified_positions.extend(justified_line)

  return right_justified_positions
      
```

出力は以下のような感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/2c919043-eb71-7b6a-f386-3f43a6ba281f.png)

##中央寄せ
中央寄せする場合は、ずらす量を右寄せ時の半分にすると良いです。
`leftGridArrangement`を以下で置き換えると実現されます。

```Python
def centerJustifiedArrangement(img_sizes, row_height, slide_width):
  if len(img_sizes)  == 0: return []

  left = 0
  top = 0
  height = row_height

  lines = [[]] 
  for im_width, im_height in img_sizes:

    width = im_width / im_height * row_height
    #画像がはみ出すとき、行を更新する
    if width > (slide_width - left) and left > 0:
      lines.append([])
      left = 0
      top += row_height
    
    lines[-1].append((left, top, width, height))
    left += width

  #右寄せにleftを修正line要素が一つでもあれば右寄せ補正してpositionsに追加
  center_justified_positions = []
  for line in lines:
    rest_width = slide_width - (line[-1][0] +  line[-1][2]) #その行の残りwidth
    rest_width //= 2
    justified_line = [(l+rest_width, t, w, h) for l,t,w,h in line]
    center_justified_positions.extend(justified_line)

  return center_justified_positions

      
```

出力は以下のような感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/57e44d67-9f70-3e67-ec1f-d58c599d3468.png)

##ちょうどいい高さの自動計算
これより前のコードでは画像の高さを固定値で指定していましたが、それだとスライドに画像をまんべんなく敷き詰めるための高さをプログラム実行者が事前に計算しないといけません。
これをプログラムにある程度自動で見積もらせるようにします。
やり方は、行高さがスライド高さのi分の1のときに敷き詰めがスライド内に収まるかを、iを1から増加させつつ順次判定し、見つかった時点で処理を終了します。

```Python
def balancedRowHeight(img_sizes, slide_width, slide_height):
  if len(img_sizes)==0: return -1

  row_num = 1
  for i in range(1,10000): #while Trueでもいいが保険としてループ上限を決める
    positions = leftJustifiedArrangement(img_sizes, slide_height//i, slide_width)
    last_bottom = positions[-1][1] + positions[-1][3]
    
    if last_bottom <= slide_height:
      return slide_height // i

  return -1

```

実行するときには、上記関数で取得した値を`leftJustifiedArrangement`などに代入します。

```Python
  ROW_HEIGHT = balancedRowHeight(img_sizes, SLIDE_WIDTH, SLIDE_HEIGHT)
  positions = leftJustifiedArrangement(img_sizes, ROW_HEIGHT, SLIDE_WIDTH)
  slide = addPictures(slide, img_paths, positions)
  prs.save(OUTPUT_FILE_PATH)
```

出力は以下のような感じです。
明らかに一行余っていますが、これよりもう一段階、行高さが大きい（＝iが１少ない）と、今度ははみ出してしまうので、スライドをはみ出さない最大の行高さとしては正しい値が計算できていると思われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/2b3de8e9-5ccb-5827-43bc-6cbfa85882d2.png)