0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

最大倍率から各倍率を用意するスクリプト

0
Posted at

はじめに

普段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()

最後に

各サイズをエクスポートしたり、名前を自分でそろえたりしないで済むので一気に楽になりました
どなたかのお役に立てれば幸いです

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?