結婚式のためにリアルタイムスライドショー作ってみた

# 事の経緯
結婚したので、結婚式することにしました。
ついでに余興としてリアルタイムで写真を投稿できるスライドショーを作ってみました。

前回 [結婚式のためにリアルタイムスライドショー作ってみる〜概要編〜](https://qiita.com/tommy-hayashida/items/fe86cab9b1aec1fcb993)

# プログラム
###全体の概要

![wedding.png](https://qiita-image-store.s3.amazonaws.com/0/205531/99daf538-bcb4-ce30-0ff2-573228834d18.png)

規模が小さいので効率性はそこまで気にしてません。初心者なので、まずきちんと動くことを優先しました。



### JSON生成と画像の回転
当初はシェルスクリプトで書くつもりでしたが、結局Pythonで書きました。

```python:create_json.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from PIL import Image
from PIL.ExifTags import TAGS
import os
import json
import collections as cl
import time


def get_exif_of_image(file):
    img = Image.open(file)

    # Exif データを取得
    # 存在しなければ-1を返す
    try:
        exif = img._getexif()
    except AttributeError:
        return -1

    # タグIDそのままでは人が読めないのでデコードして
    # テーブルに格納する
    exif_table = {}
    for tag_id, value in exif.items():
        tag = TAGS.get(tag_id, tag_id)
        exif_table[tag] = value

    return exif_table


def get_exif_rotation(orientation_num):
    if orientation_num == 0:
        return 0
    elif orientation_num == 1:
        return 0
    elif orientation_num == 2:
        return 0
    elif orientation_num == 3:
        return 180
    elif orientation_num == 4:
        return 180
    elif orientation_num == 5:
        return 270
    elif orientation_num == 6:
        return 270
    elif orientation_num == 7:
        return 90
    elif orientation_num == 8:
        return 90
    else:
        return 0


def rotation_img(exif, path, source_dir):
    to_save_path = source_dir + '/' + path

    if exif == -1:
        rotate = 0
    elif 'Orientation' in exif:
        rotate = get_exif_rotation(exif['Orientation'])
    else:
        rotate = 0

    if rotate != 0:
        with Image.open(source_dir + path) as img:
            data = img.getdata()
            mode = img.mode
            size = img.size

            with Image.new(mode, size) as dst:
                dst.putdata(data)                    
                dst = dst.rotate(rotate, expand=True)
                dst.save(to_save_path)

    return 0


def main():
    while(1):
        source_dir = '/path/to/img_dir/' # Dropboxと連携して写真が保存されるフォルダ
        imgListName = 'imgList.json' # 写真リスト

        with open(imgListName, mode='r') as f:
            JsonData = json.load(f)
            imgListJson = [a["src"] for a in JsonData]
            
        imgList = os.listdir(source_dir)

        if len(imgListJson) > 0:
            noJsonImg = list(set(imgList) - set(imgListJson))
        else:
            noJsonImg = imgList

        if len(noJsonImg) > 0:
            # 画像を回転させる
            for img_path in noJsonImg:
                try:
                    exif = get_exif_of_image(source_dir + img_path)
                except:
                    os.remove(source_dir + img_path)
                    imgList.remove(img_path)
                    print('remove')
                else:
                    rotation_img(exif, img_path, source_dir)

        writeData = []
        for i in range(len(imgList)):
            imgData = cl.OrderedDict()
            imgData["flag"] = 0
            imgData["src"] = imgList[i]
            writeData.append(imgData)

        if len(noJsonImg) > 0:
            with open(imgListName, mode='w') as f:
                json.dump(writeData, f, ensure_ascii=False, indent=4, sort_keys=True, separators=(',', ': '))
                print('write')

        time.sleep(1)


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

```json:imgList.json
[
    {
        "flag": 0,
        "src": "氏名 - sample.jpg"
    }
]
```

まずDropbox経由でローカルのフォルダに写真がダウンロードされます。写真フォルダのファイル一覧からJSONを生成します。JSONの中のflagは当初使うつもりだったけど、結局使わずじまい。

JSONを生成するのと一緒に、rotation_img()でExif情報にもとづいて画像を回転させます。画像には回転や反転、撮影日時といったメタデータをまとめたExifという情報が埋め込まれています。画像ビューワなんかはExif情報に基づいて画像を回転させて表示してくれますが、ブラウザだと回転させないまま表示されてしまいます。そのためExif情報に基づいて、Pythonの画像処理ライブラリ[Pillow](https://pillow.readthedocs.io/en/5.2.x/#)で画像を回転させて保存します。コードは[こちら](http://flame-blaze.net/archives/7406)を参考にしました。万が一Dropboxに写真以外のファイルが投稿されてもここで画像ファイルか判定して弾くので、JSONには出力されません。

本番中はこのプログラムをバックグラウンドでずっと回します。

### スライドショーの表示
```html:slide_show.html
<!DOCTYPE html>
<html>
    <head>
        <met a charset="utf-8">
            <script type="text/javascript" src="http://code.jquery.com/jquery-1.11.0.min.js"></script>            
            <link rel="stylesheet" type="text/css" href="slick.css"/>
            <link rel="stylesheet" type="text/css" href="slick-theme.css"/>
            <link rel="stylesheet" href="slide.css" type="text/css">
            <script src="/home/yuri/js/slick-1.8.1/slick/slick.js" type="text/javascript"></script>
            <title>slide show</title>
    </head>
    
    <body>
        <div class='container'>
            <div class='slider'>
                <div>
                    <img src="sample1.jpg">
                </div>
                <div>
                    <img src="sample2.jpg">
                </div>
                <div>
                    <img src="sample3.jpg">
                </div>
            </div>
        </div>
        <script src="slide.js" type="text/javascript"></script>
    </body>
</html>
```
```css:slide.css
.body {
    background-color: black
}

.container {
    margin: 0 auto;
    padding: 10px;
    width: 95%;
    color: #333;
    background: black;
}

.slider {
    text-align: center;
    color: #419be0;
    background: black;
}

.slider img {
    padding: 10px;
    background: black;
    width: auto;
	height: 300px;
    margin:0 auto;
}
```
```javascript:slide.js
function getArrayDiff(arr1, arr2) {
    // arr1にarr2の要素がなかったらarrに加える
    let arr = [];
    for(k=0;k<arr1.length;k++){
        for(l=0;l<arr2.length;l++){
            if(arr1[k].src == arr2[l].src){
                break;
            }
        }
        if(l == arr2.length){
            arr.push(arr1[k]);
        }
    }
    return arr;
}


$(function() {
    var $slider = $('.slider');

    // スライダーの設定
    $slider.slick({
        lazyLoad: 'ondemand',
  	    pauseOnHover: false,
	    arrows: false,
	    autoplay: true,
	    autoplaySpeed: 1000,
        accessibility: true, // 高さを合わせる
        useTransform: true, 
	    fade: true,
	    centerMode: true, // 中央揃え
	    speed:3000,
	    rows:1
    });

    // 新規のスライドを追加
    var imgList = [];
    $slider.on('afterChange',function(event, slick, currentSlide, nextSlide){
        $.getJSON('imgList.json', function(imgListJSON) {
            let imgListAdd = getArrayDiff(imgListJSON, imgList);
            let imgListRemove = getArrayDiff(imgList, imgListJSON);
            let pathToImg = '/path/to/img_dir/'; // 写真を保存するディレクトリ
            let k = 1;
            for (let i=0; i<imgListAdd.length; i++) {
                $slider.slick("slickAdd", '<div><img data-lazy = "' + pathToImg + imgListAdd[i].src + '"></div>', currentSlide+k);
                k++;
            };
            var slick_obj = $slider.slick('getSlick');
            imgList = imgListJSON;
        });
    });
});
```

スライドショーとして見せるため、スライダープラグイン[slick](http://kenwheeler.github.io/slick/)を使いました。JSONファイルを読んで、新しく追加された写真があればスライドを追加します。

slickは画面の切り替えにfadeを使っています。fadeをfalseにして通常のスライドをさせると、新しく画像を挿入したとき位置ずれが起きるからです。ただfadeにすると一つのスライダーにつき一枚ずつしか見せられないのがネックですね。

クライアント(夫)からは「一つの画面に4枚は映したい!」と言われたので、本番はスライダーを4つ用意してそれぞれのスライダー毎に写真フォルダやJSONファイルを用意するという力技をやりました。スマートではないけれど、まあ小規模なアプリケーションなのでなんとかなりました。

他に問題なのが、**一度投稿した写真を消すことができない**こと。投稿者が誤って投稿した写真をローカルフォルダから消しても、スライドショーのその部分はブランクが出てしまいます。本番ではそのような場面はありませんでしたが、要改善点です。

### Chromeの起動
本番ではChromeで全画面表示してスクリーンに映しましたが、一つ注意点が。通常Chromeではローカルに保存してあるJSONを読もうとするとセキュリティの関係上読んでくれません。そのためオプションをつけて起動させることで、ローカルのJSONを読むことを許可します。

```
$ google-chrome-stable --allow-file-access-from-files
```

# 本番
当日は前日に挟んだ修正がバグ出してその場でバージョン戻したりしつつ、なんとか映りました。

**しかし!!!**

Wi-Fiが切れて途中から更新が止まるという痛恨のミス……。

というわけで、ここで供養したいと思い投稿しました。

# 反省
- PCがLubuntuだったので、スクリーンの設定がわかりにくかった。
- Lubuntuを扱い慣れた人が私しかいなかったので不具合対応が難しかった。
- 前日にコード書き換えたのに、テストが不十分だった。
- 式当日のスケジュールがカツカツで全然余裕なかった。
- モバイルルーターの通信容量が少なかった。

また懲りずに何かやりたいです。

# 参考元
[親友の結婚式二次会のためにAngularJSでリアルタイムスライドショーを開発した話](https://qiita.com/ktkiyoshi/items/bb0d48aa7219df11ed50) (2018/9/28 リンク貼り直し)
[[PYTHON] EXIFの回転情報に合わせて画像を回転する [PIL]](http://flame-blaze.net/archives/7406)
[どこよりも詳しい万能スライダーjQueryプラグインslick.jsの使い方](http://ithat.me/2016/10/17/how-to-use-slick-jquery-plugin)