大量のJPEGファイルからサムネイルと閲覧用htmlファイルを自動生成するPythonスクリプト(2019.3.23)

Summary

大量のJPEGファイルを効率よく共有するために、サムネイル画像と閲覧用htmlを自動生成するPythonスクリプトを作成。 JPEGファイルの入ったフォルダを指定すると、ファイルサイズが大きいJPEGファイルに対して、軽量のサムネイル画像を自動生成し、且つ、それらを一覧表示して元画像へのリンクを貼ったhtmlテキストも生成する。 運動会やパーティなどのイベントで、調子に乗って大量に写真を撮影すると、後で参加者に共有するのが面倒なので、共有用のhtmlを自動生成できるようにした。

何ができる?

例えば、下記のように“./JPG/”以下にJPEGファイルがたくさんあるとき、対応するサムネイル画像を“./JPG/s/”以下に生成し、且つ、それらの画像ファイルを参照するための./index.htmlを出力する。 “[]”で囲んだファイルが、本スクリプトが生成するファイル。 生成された./index.htmlと./JPG/以下のファイルをwebサーバに配置すれば、サムネイル画像を参照してファイルをダウンロードできるようになる。

.
├── JPG
│   ├── IMG_2975.JPG
│            :
│   ├── IMG_3000.JPG
│   └── s
│       ├── [IMG_2975s.JPG]
│                 :
│       └── [IMG_3000s.JPG]
├── [index.html]
└── pictlist.py

使い方

pictlist.pyをindex.htmlが生成されるべき場所に置いて、JPEGファイルが入ったフォルダのパスを指定する。このとき、サムネイル画像はJPEGファイルのフォルダの中にs/という名前のフォルダを作っておくこと。 たとえば、配布したいJPEGファイルが“/www/party/JPG"にあり、サムネイルフォルダが"/www/party/JPG/s/”であるようなケースを想定している。(元画像のファイル名がimg001.jpgならサムネイル画像ファイル名はimg001s.jpg) この場合に、“/www/party/"にpictlist.pyを置いて以下のように実行を設定し、"/www/party/”にカレントディレクトリを移動して実行するする。(JPEGファイルを参照するindex.htmlが生成されるべきフォルダにcdした状態でpictlist.pyを実行する。) htmlは標準出力に出力されるので、リダイレクトでindex.htmlに保存する。一度サムネイルが作成されると、画像の処理は行わずhtmlのみを出力する。-Hオプションの値を変更して上書きしたい場合は-Fオプションを指定する。

準備

$ cd ~/www/party/
$ cp [どこか]/pitclist.py ./
$ chmod a+x ./pictlist.py

実行方法

$ ./pictlist.py ./JPG/ -H 200 -t "送別会" | tee ./index.html

第一引数にJPEGファイルのフォルダを指定するが、index.htmlファイルは、現在のディレクトリからの相対位置で与えられたURLを生成することに注意。もしも./JPG/フォルダの中にindex.htmlを置きたいなら、 カレントディレクトリを./JPG/内に移動して、以下のように実行する。 このとき、サムネイルフォルダ“./s/”が存在している必要がある。 画像の縦の長さを’-H 200’のようにpixel指定する。’-t “タイトル”’でタイトルを指定。

$ ./pictlist.py ./ -H 200 -t "送別会" | tee ./index.html

ソースコード(pictlist.py)

thumbnail画像の自動生成にはPillowを使用した。 工夫した点は以下の通り


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# =======================================================
#  JPEGファイルを表示するhtmlを自動生成するスクリプト
#  JPEGファイルのサイズが大きい時はthumbnail画像を自動生成
#
#  pictlist.py
#  coded by Noboru Harada (noboru@ieee.org)
#
#  Changes:
#  2019/03/23: First version
#
#  使い方
#  > pictlist.py ./JPG/ -H 200 -t "運動会" | tee index.html
#  -Hオプションで画像の高さを指定
#  -tオプションでhtmlに表示されるタイトル文字列を指定
#  thumbnailファイルは、元のファイル名にsをつけたもの
#  たとえば"./JPG/img01.jpg"なら"./JPG/s/img01s.jpg"
#  指定したフォルダの中に s/があることを想定
#  -sで指定可能 -s ./JPG/s/
#  元のJPEGファイルのサイズが一定以下の場合はサムネイル画像は作らずコピー
#  サムネイル画像が既に存在する場合には作成しない
#  ただし、-Fオプションが指定されて入れば上書き生成
# =======================================================


# generate html text for picture and thumbnail
import os
import shutil
import subprocess as sp
import re
from PIL import Image
import argparse

# 初期値の設定
title       = "写真"
jpg_path    = "./JPG/"
s_path      = "./JPG/s/"
s_height    = 200
max_fsize   = 200000    # 200KB
Force        = False

# コマンドライン引数の読み込み設定
def getargs():
    parser = argparse.ArgumentParser(
        description="generate html text for picture and thumbnail")
    parser.add_argument("jpg_path", type=str,
                        help="JPEG file path (e.g. ./JPG/)")
    parser.add_argument("-s", "--s_path", type=str,
                        help="specify thumbnail dir (if not specified, jpg_path/s/")
    parser.add_argument("-t", "--title", type=str, default="写真",
                        help="specify title (default \"写真\")")
    parser.add_argument("-H", "--Height", type=int, default=s_height,
                        help="height of the thumbnail picture in number of pixels (defult = 200)")
    parser.add_argument("-m", "--maxsize", type=str,
                        help="file size threshold for generating thumbnail (default 200KB)")
    parser.add_argument("-F", "--Force", action='store_true',
                        help="Force over-wright for generating thumbnail files")
    args = parser.parse_args()
    return args


def put_body(jpg_path, s_path, s_height, max_fsize):
    # use scandir
    ls_file_name = [f.name for f in os.scandir(jpg_path)]
    # sortしないと順番がバラバラになる
    ls_file_name.sort()

    #print(ls_file_name)

    for fname in ls_file_name:
        #print("try: " + jpg_path + fname)
        if os.path.isfile(jpg_path+fname):
            try:
                image = Image.open(jpg_path + fname)
                fsize = os.path.getsize(jpg_path + fname)

                # 縦横比率を保存して、高さが-Hになるようなサムネイル画像のサイズを計算
                w, h = image.size
                s_width = w * s_height / h
                s_size = s_width, s_height

                if '.JPG' in fname:
                    sfile = fname.replace('.JPG', 's.JPG')
                elif '.jpg' in fname:
                    sfile = fname.replace('.jpg', 's.jpg')

                # サムネイル画像ファイルが既に存在する場合は何もしない
                if not os.path.exists(s_path+sfile):
                    if fsize > max_fsize:
                        image.thumbnail(s_size)
                        image.save(s_path+sfile, "JPEG")
                    else:
                        shutil.copyfile(jpg_path+fname, s_path+sfile)
                # Forceオプションが指定されている場合はファイルを上書き
                elif Force:
                    if fsize > max_fsize:
                        image.thumbnail(s_size)
                        image.save(s_path+sfile, "JPEG")
                    else:
                        shutil.copyfile(jpg_path+fname, s_path+sfile)

                # サムネイル画像を表示して元画像を参照するリンクを生成
                print(" <a href=\"" + jpg_path + fname + "\"><img src=\""
                      + s_path + sfile + "\" height=\""+str(s_height)+"\" alt=\""
                      + jpg_path + fname + "\" /></a>")

            except IOError:
                #print("IOError: "+fname+" is not an Image file")
                pass

def put_header(title):
    print(
        "<!DOCTYPE html>\n"
        "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"\" xml:lang=\"\">\n"
        "<head>\n"
        "<meta charset=\"utf-8\" />\n"
        "<meta name=\"generator\" content=\"pictlist.py\" />\n"
        "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\" />\n"
        "<title>index</title>\n"
        "<style type=\"text/css\">\n"
        "code{white-space: pre-wrap;}\n"
        "span.smallcaps{font-variant: small-caps;}\n"
        "span.underline{text-decoration: underline;}\n"
        "div.column{display: inline-block; vertical-align: top; width: 50%;}\n"
        "</style>\n"
        "<!--[if lt IE 9]>\n"
        "<script src=\"//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js\"></script>\n"
        "<![endif]-->\n"
        "</head>\n"
        "<body>\n"
        "<h1 id=\""+title+"\">"+title+"</h1>\n"
        "サムネイル画像が表示されています。クリックすると元サイズの画像が表示されます。<br>\n"
        "このhtmlはPythonスクリプト(<a href=\"http://www.nharada.jpn.org/programming/pictlist.html\">pictlist.py</a>)を使用して作成しました。\n"
        "<p>"
    )

def put_footer():
    print(
        "</p>\n"
        "</body>\n"
        "</html>"
    )


if __name__ == '__main__':

    # コマンドライン引数の読み込み
    args = getargs()

    jpg_path = args.jpg_path.rstrip('/¥')+"/"
    if args.s_path:
        s_path = args.s_path.rstrip('/¥')+"/"
    else:
        s_path = jpg_path+"s/"

    # ファイルの入出力フォルダが見つからない場合はエラー終了
    if not os.path.exists(jpg_path):
        print(jpg_path+" does not exist")
        exit(-1)
    if not os.path.exists(s_path):
        print(s_path+" does not exist")
        exit(-1)

    if args.maxsize:
        if "MB" in args.maxsize.upper():
            max_fsize = 1000 * 1000 * int(args.maxsize.upper().replace("MB", ""))
        elif "KB" in args.maxsize.upper():
            max_fsize = 1000 * int(args.maxsize.upper().replace("KB", ""))
        else:
            max_fsize = int(args.maxsize)

    if args.Height:
        s_height = args.Height

    if args.title:
        title = args.title
    Force = args.Force

    # 実際のhtml書き出し処理
    put_header(title)
    #generate thumbnail only when JPEG file size exceeds max otherwise copy
    put_body(jpg_path, s_path, s_height, max_fsize)
    put_footer()

参考

Back to Index