スマホやデジカメ、Web素材など、日常的に扱う画像ファイルは形式もサイズもさまざまです。
複数の形式をまとめて変換したい、画質を調整したい、Web向けに軽量化したい…そんな時に便利なのが、今回紹介する万能変換スクリプトです。
このスクリプトを使えば、JPEG・PNG・WebP・TIFF・BMP・GIFに加えて、オプションでHEIC/HEIFにも対応し、変換・圧縮・リサイズ・透過処理・メタデータ制御まで一括で行えます。
1. 環境構築
必須ライブラリ
pip install pillow
HEIC/HEIF対応(必要な場合)
pip install pillow-heif
- Pillow:Pythonの標準的な画像処理ライブラリ
- pillow-heif:HEIC/HEIF画像をPillowで扱えるようにするプラグイン(HEICを変換したい場合のみ必要)
2. スクリプト全体
以下が今回のスクリプトです。
各部分に詳細なコメントを入れていますので、コードの役割が分かりやすくなっています。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
img_convert.py
主要な画像形式(JPEG/PNG/WebP/TIFF/BMP/GIF/(任意で HEIC/HEIF))の相互変換・圧縮を行うCLIツール。
依存:
必須: pip install pillow
任意: pip install pillow-heif # HEIC/HEIF を扱う場合
主な機能:
- 入力: ファイル複数指定 or ディレクトリ(再帰検索)
- 対応形式: JPEG, PNG, WEBP, TIFF, BMP, GIF (+ HEIC/HEIF は pillow-heif がある場合)
- 画質・圧縮: JPEG/WebP 品質, PNG 圧縮レベル, progressive/optimize
- リサイズ: 最大幅・最大高・スケール倍率での縮小(拡大抑止)
- 透過 -> 非透過形式への変換時の背景色合成(例: JPEG)
- メタデータ: EXIF/ICC の保持 or 破棄
- 出力: 上書き抑止/許可, 接尾辞, 出力ディレクトリ, 出力拡張子自動付与
"""
import argparse
import fnmatch
import os
import sys
from typing import Iterable, List, Optional, Tuple
from PIL import Image, ImageOps
# ------------------------------------------------------------
# 1) HEIC/HEIF対応の有無を確認して必要ならオープナ登録
# ------------------------------------------------------------
def _try_register_heif() -> bool:
try:
import pillow_heif
pillow_heif.register_heif_opener()
return True
except Exception:
return False
HEIF_ENABLED = _try_register_heif()
# ------------------------------------------------------------
# 対応拡張子のセット
# ------------------------------------------------------------
INPUT_EXTS = {
".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff", ".bmp", ".gif"
}
if HEIF_ENABLED:
INPUT_EXTS |= {".heic", ".heif"} # pillow-heifが使える場合のみ追加
OUTPUT_EXTS = {"jpeg", "jpg", "png", "webp", "tiff", "bmp", "gif"}
# ------------------------------------------------------------
# カラーコードをRGBタプルに変換
# ------------------------------------------------------------
def parse_color(s: str) -> Tuple[int, int, int]:
"""
'#fff', '#ffffff', '255,255,255' のような文字列を (R, G, B) タプルに変換
"""
s = s.strip()
if s.startswith("#"):
s = s.lstrip("#")
if len(s) == 3:
r, g, b = [int(c*2, 16) for c in s]
elif len(s) == 6:
r = int(s[0:2], 16)
g = int(s[2:4], 16)
b = int(s[4:6], 16)
else:
raise ValueError("不正なカラーコードです(#RGB or #RRGGBB)")
return (r, g, b)
else:
parts = s.split(",")
if len(parts) != 3:
raise ValueError("不正なカラー指定です(R,G,B)")
return tuple(int(p) for p in parts) # type: ignore
# ------------------------------------------------------------
# ファイルが対象の画像形式か判定
# ------------------------------------------------------------
def is_image_file(path: str) -> bool:
return os.path.splitext(path)[1].lower() in INPUT_EXTS
# ------------------------------------------------------------
# 入力パスから対象ファイルを列挙
# ------------------------------------------------------------
def iter_targets(paths: List[str], recursive: bool, include: List[str], exclude: List[str]) -> Iterable[str]:
def match_patterns(name: str, patterns: List[str]) -> bool:
return any(fnmatch.fnmatch(name, pat) for pat in patterns) if patterns else True
for p in paths:
if os.path.isfile(p):
if is_image_file(p) and match_patterns(os.path.basename(p), include) and not match_patterns(os.path.basename(p), exclude):
yield p
elif os.path.isdir(p):
for root, dirs, files in os.walk(p):
for f in files:
fp = os.path.join(root, f)
if is_image_file(fp) and match_patterns(f, include) and not match_patterns(f, exclude):
yield fp
if not recursive:
break
# ------------------------------------------------------------
# 出力パスを構築
# ------------------------------------------------------------
def build_output_path(src: str, outdir: Optional[str], fmt: str, suffix: str, keep_tree: bool, root_base: Optional[str]) -> str:
base, _ = os.path.splitext(src)
if outdir:
if keep_tree and root_base and src.startswith(root_base):
rel = os.path.relpath(base, root_base)
target_base_dir = os.path.join(outdir, os.path.dirname(rel))
os.makedirs(target_base_dir, exist_ok=True)
base_out = os.path.join(target_base_dir, os.path.basename(rel))
else:
os.makedirs(outdir, exist_ok=True)
base_out = os.path.join(outdir, os.path.basename(base))
else:
base_out = base
if suffix:
base_out += suffix
ext = fmt.lower()
if ext in ("jpeg", "jpg"):
ext = "jpg"
return f"{base_out}.{ext}"
# ------------------------------------------------------------
# リサイズ処理
# ------------------------------------------------------------
def resize_if_needed(img: Image.Image, max_w: Optional[int], max_h: Optional[int], scale: Optional[float]) -> Image.Image:
w, h = img.size
# スケール優先
if scale is not None and 0 < scale < 1.0:
return img.resize((max(1, int(w*scale)), max(1, int(h*scale))), resample=Image.LANCZOS)
# 最大幅/高さ指定
if max_w is None and max_h is None:
return img
mw = max_w if max_w is not None else 10**9
mh = max_h if max_h is not None else 10**9
if w <= mw and h <= mh:
return img
ratio = min(mw / w, mh / h)
return img.resize((max(1, int(w*ratio)), max(1, int(h*ratio))), resample=Image.LANCZOS)
# ------------------------------------------------------------
# アルファ合成(JPEG保存時など)
# ------------------------------------------------------------
def flatten_alpha(img: Image.Image, bg: Tuple[int, int, int]) -> Image.Image:
if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
base = Image.new("RGB", img.size, bg)
return Image.alpha_composite(base, img.convert("RGBA"))
if img.mode not in ("RGB", "L"):
return img.convert("RGB")
return img
# ------------------------------------------------------------
# 保存処理
# ------------------------------------------------------------
def save_image(img: Image.Image, dest: str, fmt: str, quality: int, png_compress_level: int,
progressive: bool, optimize: bool, lossless: bool,
keep_exif: bool, keep_icc: bool, original_info: dict) -> None:
params = {}
fmt_upper = fmt.upper()
exif_bytes = original_info.get("exif", None) if keep_exif else None
icc_bytes = original_info.get("icc_profile", None) if keep_icc else None
if exif_bytes:
params["exif"] = exif_bytes
if icc_bytes:
params["icc_profile"] = icc_bytes
if fmt_upper in ("JPEG", "JPG"):
params.update(dict(quality=quality, optimize=optimize, progressive=progressive, subsampling="4:2:0"))
elif fmt_upper == "PNG":
params.update(dict(optimize=optimize, compress_level=max(0, min(png_compress_level, 9))))
elif fmt_upper == "WEBP":
if lossless:
params.update(dict(lossless=True, method=6))
else:
params.update(dict(quality=quality, method=6))
elif fmt_upper == "TIFF":
params.update(dict(compression="tiff_deflate"))
img.save(dest, format=fmt_upper, **params)
# ------------------------------------------------------------
# 変換処理(1ファイル)
# ------------------------------------------------------------
def convert_one(src: str, dest: str, fmt: str, quality: int, png_compress_level: int,
progressive: bool, optimize: bool, lossless: bool,
bg_rgb: Optional[Tuple[int, int, int]], max_w: Optional[int], max_h: Optional[int],
scale: Optional[float], keep_exif: bool, keep_icc: bool):
try:
with Image.open(src) as im:
info = im.info.copy()
im = ImageOps.exif_transpose(im)
im = resize_if_needed(im, max_w=max_w, max_h=max_h, scale=scale)
if fmt.lower() in ("jpeg", "jpg") and bg_rgb is not None:
im = flatten_alpha(im, bg_rgb)
elif fmt.lower() in ("jpeg", "jpg") and im.mode in ("RGBA", "LA", "P"):
im = im.convert("RGB")
save_image(im, dest, fmt, quality, png_compress_level, progressive, optimize, lossless, keep_exif, keep_icc, info)
return True, f"OK : {src} -> {dest}"
except Exception as e:
return False, f"FAIL: {src} ({e})"
# ------------------------------------------------------------
# メイン処理
# ------------------------------------------------------------
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description="メジャーな画像形式の相互変換・圧縮を行うツール",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
# 入力指定
ap.add_argument("paths", nargs="+", help="入力するファイル/ディレクトリ(複数可)")
ap.add_argument("-r", "--recursive", action="store_true", help="ディレクトリを再帰的に探索")
ap.add_argument("--include", action="append", default=[], help="処理対象のファイル名グロブ(例: --include *.png)")
ap.add_argument("--exclude", action="append", default=[], help="除外するファイル名グロブ")
# 出力設定
ap.add_argument("-f", "--format", required=True, choices=sorted(list(OUTPUT_EXTS)), help="出力形式")
ap.add_argument("-o", "--outdir", default=None, help="出力先ディレクトリ")
ap.add_argument("--keep-tree", action="store_true", help="元ディレクトリ構造を保持")
ap.add_argument("--suffix", default="", help="ファイル名の接尾辞(例: _min)")
ap.add_argument("--overwrite", action="store_true", help="既存ファイルを上書き保存")
# 品質・圧縮
ap.add_argument("-q", "--quality", type=int, default=90, help="JPEG/WEBP の品質(1-100)")
ap.add_argument("--png-compress-level", type=int, default=6, help="PNG の圧縮レベル(0-9)")
ap.add_argument("--progressive", action="store_true", help="プログレッシブJPEG(JPEGのみ)")
ap.add_argument("--optimize", action="store_true", help="最適化フラグ(JPEG/PNG/WEBP)")
ap.add_argument("--lossless", action="store_true", help="WebP を可逆で保存")
# リサイズ
ap.add_argument("--max-width", type=int, default=None, help="最大幅px(等比・拡大しない)")
ap.add_argument("--max-height", type=int, default=None, help="最大高px(等比・拡大しない)")
ap.add_argument("--scale", type=float, default=None, help="倍率で縮小(0 < scale < 1.0)")
# 透過処理
ap.add_argument("--bg", type=str, default=None, help="JPEG保存時の背景色")
# メタデータ
ap.add_argument("--keep-exif", action="store_true", help="EXIF保持")
ap.add_argument("--keep-icc", action="store_true", help="ICC保持")
ap.add_argument("--strip-meta", action="store_true", help="メタデータ削除(keep-* より優先)")
args = ap.parse_args(argv)
fmt = args.format.lower()
if fmt == "jpg":
fmt = "jpeg"
keep_exif = args.keep_exif and not args.strip_meta
keep_icc = args.keep_icc and not args.strip_meta
bg_rgb = parse_color(args.bg) if args.bg else None
targets = list(iter_targets(args.paths, recursive=args.recursive, include=args.include, exclude=args.exclude))
if not targets:
print("対象ファイルが見つかりません。", file=sys.stderr)
return 1
root_base = None
for p in args.paths:
if os.path.isdir(p):
root_base = os.path.abspath(p)
break
ok = skip = fail = 0
for src in targets:
dest = build_output_path(src, args.outdir, fmt, args.suffix, args.keep_tree, root_base)
if (not args.overwrite) and os.path.exists(dest):
print(f"SKIP: {dest} (exists)")
skip += 1
continue
success, msg = convert_one(src, dest, fmt, max(1, min(args.quality, 100)),
max(0, min(args.png_compress_level, 9)),
args.progressive, args.optimize, args.lossless,
bg_rgb, args.max_width, args.max_height, args.scale,
keep_exif, keep_icc)
print(msg)
if success:
ok += 1
else:
fail += 1
print("-" * 60)
print(f"Done. success={ok}, skip={skip}, fail={fail}")
return 0 if fail == 0 else 2
if __name__ == "__main__":
sys.exit(main())
3. 主なオプション解説
オプション | 説明 |
---|---|
-f, --format | 出力形式(jpeg , png , webp など) |
-o, --outdir | 出力先ディレクトリ |
--keep-tree | ディレクトリ構造を維持して出力 |
--suffix | 出力ファイル名の末尾に付加する文字列 |
--overwrite | 同名ファイルを上書き保存 |
-q, --quality | JPEG/WEBPの品質(1〜100) |
--png-compress-level | PNGの圧縮レベル(0=速いが大きい、9=遅いが小さい) |
--progressive | プログレッシブJPEGで保存 |
--optimize | 最適化フラグを有効化 |
--lossless | WebPを可逆圧縮で保存 |
--max-width , --max-height | 最大幅・高さを指定(拡大はしない) |
--scale | 縮小倍率(0 < scale < 1.0) |
--bg | JPEG保存時の背景色(透過処理用) |
--keep-exif | EXIF保持 |
--keep-icc | ICCプロファイル保持 |
--strip-meta | メタデータ削除(保持指定より優先) |
4. 使用例
# PNGとJPEGをWebP(品質80)に変換
python img_convert.py img1.png img2.jpg --format webp -q 80
# ディレクトリ内のPNGをJPEG(白背景合成)に変換
python img_convert.py ./pngs -r --include "*.png" --format jpeg -q 85 --bg "#ffffff" --optimize --progressive
# HEICをJPEGに変換
ブログ記事用の原稿をまとめました。
スクリプトは全コード掲載で、各部分に可能な限り詳細なコメントを入れてあります。
オプションの解説や使用例も充実させていますので、そのまま記事として公開できる内容になっています。
タイトル案
「Pythonで主要な画像形式を一括変換&圧縮!HEIC対応の万能スクリプト」
1. はじめに
JPEG・PNG・WebP・TIFF・BMP・GIF、さらにHEICまで、複数の画像形式をまとめて変換・圧縮したいときはありませんか?
この記事では、PythonとPillowライブラリを使った万能変換スクリプトを紹介します。形式変換だけでなく、画質調整・リサイズ・透過処理・メタデータ管理まで一括処理可能です。
2. 環境構築
必須:
pip install pillow
HEIC/HEIF対応(必要な場合):
pip install pillow-heif
3. スクリプト全体(詳細コメント付き)
(ここに先ほど提示した img_convert.py
全コードを掲載)
4. オプション解説
オプション | 内容 |
---|---|
-f, --format | 出力形式(jpeg/jpg, png, webp, tiff, bmp, gif) |
-o, --outdir | 出力先ディレクトリ |
--keep-tree | 元のフォルダ構造を出力先でも再現 |
--suffix | 出力ファイル名に付与する文字列 |
--overwrite | 既存ファイルを上書き |
-q, --quality | JPEG/WEBPの品質(1〜100) |
--png-compress-level | PNG圧縮レベル(0〜9) |
--progressive | プログレッシブJPEGで保存 |
--optimize | 保存時に最適化 |
--lossless | WebPを可逆変換 |
--max-width , --max-height | 最大幅・高さ(拡大なし) |
--scale | 縮小倍率(0 < 値 < 1) |
--bg | JPEG化時の透過背景色 |
--keep-exif | EXIF保持 |
--keep-icc | ICCプロファイル保持 |
--strip-meta | メタデータ削除 |
5. 使用例
# JPEGとPNGをWebP(品質80)に
python img_convert.py img1.jpg img2.png --format webp -q 80
# ディレクトリ内のPNGをJPEGに(白背景・品質85)
python img_convert.py ./pngs -r --include "*.png" --format jpeg -q 85 --bg "#ffffff" --optimize --progressive
# HEICをJPEGに(HEIC対応ライブラリ必須)
python img_convert.py ./heics -r --format jpeg --strip-meta
# 最大1600pxに縮小しJPEG化
python img_convert.py ./imgs --max-width 1600 --max-height 1600 --format jpeg -q 82 --suffix _1600
コメント