Python | フォルダ内の画像をサムネイル化し、EXIF日付別アルバムでHTMLギャラリー生成

Python
スポンサーリンク

写真を日付ごと(撮影日)に自動分類し、サムネイル付きHTMLギャラリーを作る実践例です。pathlibでパス管理、Pillowでサムネイル生成とEXIF読み取り、HTMLはテンプレート文字列で生成します。


前提準備と想定構成

  • ライブラリ: pip install pillow
  • フォルダ構成(例):
    • 入力: ~/Pictures/gallery_src
    • 出力: ~/Pictures/gallery_out
      • thumbs/(サムネイル格納)
      • albums/(日付別アルバムHTML)

サンプルコード(EXIF読み取り+アルバム分け+HTML生成)

from pathlib import Path
from PIL import Image, ExifTags
import html
from datetime import datetime
import shutil

# 入出力ディレクトリ
src_dir = Path.home() / "Pictures" / "gallery_src"
out_dir = Path.home() / "Pictures" / "gallery_out"
thumb_dir = out_dir / "thumbs"
albums_dir = out_dir / "albums"

# 出力フォルダ作成
out_dir.mkdir(exist_ok=True)
thumb_dir.mkdir(exist_ok=True)
albums_dir.mkdir(exist_ok=True)

# 対象拡張子
extensions = {".jpg", ".jpeg", ".png", ".webp"}

# EXIFタグ名の逆引きマップ作成("DateTimeOriginal" を探す)
EXIF_TAGS = {v: k for k, v in ExifTags.TAGS.items()}

def get_taken_date(img_path: Path) -> str:
    """画像の撮影日(YYYY-MM-DD)を返す。EXIFがなければファイル更新日を使用。"""
    try:
        with Image.open(img_path) as img:
            exif = img._getexif()
            if exif:
                dto_tag = EXIF_TAGS.get("DateTimeOriginal")
                if dto_tag in exif:
                    raw = exif[dto_tag]  # 例: "2023:07:14 12:34:56"
                    # EXIFの日時を正規化
                    try:
                        dt = datetime.strptime(raw, "%Y:%m:%d %H:%M:%S")
                        return dt.date().isoformat()  # "YYYY-MM-DD"
                    except Exception:
                        pass
    except Exception:
        pass
    # 代替: ファイルの更新日
    mtime = datetime.fromtimestamp(img_path.stat().st_mtime)
    return mtime.date().isoformat()

def make_thumbnail(src: Path, dst: Path, size=(240, 240)) -> None:
    """アスペクト比を保ったサムネイルをJPEGで保存(軽量化)。"""
    with Image.open(src) as img:
        img.thumbnail(size)
        if img.mode != "RGB":
            img = img.convert("RGB")
        dst.parent.mkdir(exist_ok=True)
        img.save(dst, "JPEG", quality=85)

# 画像メタの収集とアルバム分け
albums = {}  # date -> list[dict(full, thumb, title)]
for img_path in sorted(src_dir.iterdir()):
    if not img_path.is_file() or img_path.suffix.lower() not in extensions:
        continue

    date = get_taken_date(img_path)
    thumb_name = f"{img_path.stem}_thumb.jpg"
    thumb_path = thumb_dir / date / thumb_name  # 日付ごとにサムネイルサブフォルダ
    make_thumbnail(img_path, thumb_path)

    # 出力側に元画像もコピー(HTMLから相対参照できるように)
    copy_full_dir = out_dir / "images" / date
    copy_full_dir.mkdir(parents=True, exist_ok=True)
    full_copy_path = copy_full_dir / img_path.name
    if not full_copy_path.exists():
        shutil.copy(img_path, full_copy_path)

    item = {
        "full": str((Path("images") / date / img_path.name).as_posix()),
        "thumb": str((Path("thumbs") / date / thumb_name).as_posix()),
        "title": html.escape(img_path.stem),
    }
    albums.setdefault(date, []).append(item)

# アルバムページ生成(各日付ごと)
def render_album(date: str, items: list[dict]) -> Path:
    album_html = f"""<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>アルバム {date}</title>
<style>
  body {{ margin:0; font-family: system-ui, sans-serif; background:#111; color:#eee; }}
  header {{ padding:16px; text-align:center; font-size:18px; }}
  .grid {{
    display:grid; grid-template-columns:repeat(auto-fill, minmax(160px,1fr));
    gap:12px; padding:12px;
  }}
  .card {{ background:#1b1b1b; border-radius:8px; overflow:hidden; border:1px solid #2a2a2a; }}
  .thumb {{ display:block; width:100%; height:auto; aspect-ratio:1/1; object-fit:cover; background:#222; }}
  .meta {{ padding:8px; font-size:12px; color:#bbb; }}
  a {{ color:inherit; text-decoration:none; }}
  nav {{ padding:8px; text-align:center; }}
  a:focus-visible {{ outline:2px solid #4ea3ff; outline-offset:2px; }}
</style>
</head>
<body>
<header>アルバム {date}{len(items)}枚)</header>
<nav><a href="../index.html">一覧に戻る</a></nav>
<main class="grid">
{"".join(f'''
  <div class="card">
    <a href="{html.escape(item["full"])}" target="_blank" rel="noopener">
      <img class="thumb" src="{html.escape(item["thumb"])}" alt="{item["title"]}" loading="lazy">
      <div class="meta">{item["title"]}</div>
    </a>
  </div>
''' for item in items)}
</main>
</body>
</html>
"""
    out_path = albums_dir / f"{date}.html"
    out_path.write_text(album_html, encoding="utf-8")
    return out_path

album_links = []
for date, items in sorted(albums.items()):
    page = render_album(date, items)
    album_links.append((date, page.name, len(items)))

# インデックスページ生成(全日付へのリンク)
index_html = f"""<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>画像ギャラリー(日付別アルバム)</title>
<style>
  body {{ margin:0; font-family: system-ui, sans-serif; background:#111; color:#eee; }}
  header {{ padding:16px; text-align:center; font-size:18px; }}
  ul {{ list-style:none; padding:0 16px; }}
  li {{ margin:8px 0; padding:12px; background:#1b1b1b; border:1px solid #2a2a2a; border-radius:8px; }}
  a {{ color:#eee; text-decoration:none; }}
  a:hover {{ text-decoration:underline; }}
</style>
</head>
<body>
<header>画像ギャラリー(日付別アルバム)</header>
<ul>
{"".join(f'<li><a href="albums/{html.escape(name)}">{html.escape(date)} のアルバム({count}枚)</a></li>' for date, name, count in album_links)}
</ul>
</body>
</html>
"""
(out_dir / "index.html").write_text(index_html, encoding="utf-8")

print(f"ギャラリーのトップ: {out_dir / 'index.html'}")
print(f"アルバムページ: {albums_dir}")
print(f"サムネイル保存先: {thumb_dir}")
print(f"元画像コピー先: {out_dir / 'images'}")
Python

使い方のポイント

  • EXIFの撮影日時: 多くのカメラ・スマホは DateTimeOriginal を持っています。ない場合はファイル更新日を代用しています。
  • サムネイル軽量化: JPEGの quality=85 はバランス良い画質。さらに軽量化したい場合は 70–80 を試す。
  • 拡張子対応: extensions".gif", ".tiff", ".heic"(HEICはPillowの追加プラグインが必要)などを追加可能。
  • 相対パス: HTMLから参照しやすいように、out_dir 配下へ「images/date/ファイル」をコピーしています。
  • Lazy Load: <img loading="lazy"> で初期表示を高速化。

よくある拡張

  • 並び順: アルバム内で撮影時刻やファイル名順に並べたい場合は sorted(items, key=...) を適用。
  • EXIF補助: DateTimeDateTimeDigitized をフォールバックに使う。
  • メタ表示: EXIFのレンズ名、露出、ISOなどを meta に表示。
  • 月別インデックス: YYYY-MM ごとにまとめたページを追加。
  • 重複検出: ハッシュ(hashlib.md5)で重複画像をスキップ。

まとめ

  • pathlib: OS差異を意識せずに直感的なパス操作。
  • Pillow: EXIFで撮影日を取得、アスペクト比維持でサムネイル生成。
  • HTML自動生成: 日付別アルバムページとトップインデックスを自動作成。

Python
スポンサーリンク
シェアする
@lifehackerをフォローする
スポンサーリンク
タイトルとURLをコピーしました