はじめに
普段Android実装をしていてPNG画像を使うときにFigmaなどから各サイズを持ってくるのがめんどうだったり、デザイナさんに用意してもらうのに時間がかかったりするので、自動で用意できるスクリプトを組んでみました
コード
大事なことはコメントに残してあります
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
macos_png_map_android.py
------------------------
Mac用: ~/Downloads/export_image に「最大倍率(4x = xxxhdpi)」のPNGを置くだけで、
~/Downloads/AndroidMappedAssets/ に drawable-ldpi 〜 drawable-xxxhdpi をまとめて生成します。
- 入力: ~/Downloads/export_image/ にある PNG(xxxhdpi 相当 = 4x)
- 出力: ~/Downloads/AndroidMappedAssets/ 以下に drawable-*dpi フォルダを作成
- 9-patch (.9.png) はリサイズせず全dpiへコピー
- sRGB 化・アルファ保持
- 必要に応じて --out で出力先変更可能
使い方(ターミナル):
python3 macos_png_map_android.py
# または出力先変更:
python3 macos_png_map_android.py --out ~/Downloads/MyAndroidRes
オプション:
--in 入力ディレクトリ (デフォルト: ~/Downloads/export_image)
--out 出力ディレクトリ (デフォルト: ~/Downloads/AndroidMappedAssets)
--targets 生成する密度 (カンマ区切り、デフォルト: ldpi,mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi)
--to-webp PNGに加えてlossless WebPも出力 (true/false, デフォルト: false)
"""
import argparse
import os
from pathlib import Path
from PIL import Image, ImageCms
SCALE_MAP = {
"ldpi": 0.75,
"mdpi": 1.0,
"hdpi": 1.5,
"xhdpi": 2.0,
"xxhdpi": 3.0,
"xxxhdpi": 4.0,
}
def parse_args():
home = Path.home()
p = argparse.ArgumentParser()
p.add_argument("--in", dest="input_dir",
default=str(home / "Downloads" / "export_image"),
help="入力フォルダ (xxxhdpi=4x 相当のPNGを置く)")
p.add_argument("--out", dest="output_dir",
default=str(home / "Downloads" / "AndroidMappedAssets"),
help="出力先フォルダ (drawable-* をまとめて作成)")
p.add_argument("--targets",
default="ldpi,mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi",
help="生成する密度 (カンマ区切り)")
p.add_argument("--to-webp", default="false",
choices=["true","false"],
help="lossless WebP も併せて出力するか")
return p.parse_args()
def ensure_srgb(img: Image.Image) -> Image.Image:
try:
if "icc_profile" in img.info and img.info["icc_profile"]:
srgb_profile = ImageCms.createProfile("sRGB")
src_profile = ImageCms.ImageCmsProfile(bytes(img.info["icc_profile"]))
return ImageCms.profileToProfile(img, src_profile, srgb_profile, outputMode=img.mode)
except Exception:
pass
return img
def resize_png(src_path: Path, dst_path: Path, out_size, to_webp: bool):
dst_path.parent.mkdir(parents=True, exist_ok=True)
with Image.open(src_path) as im:
im = ensure_srgb(im.convert("RGBA"))
resized = im.resize(out_size, Image.LANCZOS)
resized.save(dst_path, format="PNG", optimize=True)
if to_webp:
webp_path = dst_path.with_suffix(".webp")
resized.save(webp_path, format="WEBP", lossless=True, method=6)
def main():
args = parse_args()
input_dir = Path(args.input_dir).expanduser()
output_dir = Path(args.output_dir).expanduser()
to_webp = (args.to_webp.lower() == "true")
targets = [t.strip() for t in args.targets.split(",") if t.strip()]
assert input_dir.is_dir(), f"入力フォルダが見つかりません: {input_dir}"
# 入力は xxxhdpi (4x) を想定
source_scale = SCALE_MAP["xxxhdpi"]
pngs = [p for p in input_dir.rglob("*.png") if not p.name.endswith(".9.png")]
nine_patch = [p for p in input_dir.rglob("*.9.png")]
if not pngs and not nine_patch:
print(f"PNGが見つかりませんでした: {input_dir}")
return
if nine_patch:
print(f"NOTE: 9-patchはリサイズせず全dpiへコピー: {len(nine_patch)}")
processed = 0
# 画像ごとに処理(ベースは xxxhdpi なので、各dpiに縮小)
for src in pngs:
with Image.open(src) as im:
src_w, src_h = im.size
base_w = int(round(src_w / source_scale)) # mdpi換算の基準サイズ
base_h = int(round(src_h / source_scale))
for dpi in targets:
scale = SCALE_MAP[dpi]
out_w = int(round(base_w * scale))
out_h = int(round(base_h * scale))
rel = src.relative_to(input_dir)
dst = output_dir / f"drawable-{dpi}" / rel.name
resize_png(src, dst, (out_w, out_h), to_webp)
processed += 1
print(f"{src.name} → drawable-{dpi}/{rel.name} ({out_w}x{out_h})")
# 9-patchは縮小・拡大せず全dpiへコピー
for src in nine_patch:
rel = src.relative_to(input_dir)
for dpi in targets:
dst = output_dir / f"drawable-{dpi}" / rel.name
dst.parent.mkdir(parents=True, exist_ok=True)
with open(src, "rb") as fsrc, open(dst, "wb") as fdst:
fdst.write(fsrc.read())
print(f"9-patch copy → drawable-{dpi}/{rel.name}")
print(f"\n完了: 生成 {processed} 枚, 出力先: {output_dir}")
if __name__ == "__main__":
main()
最後に
各サイズをエクスポートしたり、名前を自分でそろえたりしないで済むので一気に楽になりました
どなたかのお役に立てれば幸いです