RRM logo Ops Team

Resize images + convert any format (HEIC, JPEG, etc) to .png + normalizes name of images to Kebab Case

Category: Development
Language: python
Created by: lvcaspacifico
Created At: Mar 10, 2024
Updated: Mar 10, 2024

Observations

Please pay attention to the comment section in the code, it contains instructions on how to run the script.

Script

"""
Media normalizer (recursivo)
----------------------------
Para cada arquivo na pasta apontada e em todas as subpastas:
- HEIC          → convertido para PNG lossless, renomeado para kebab-case
- JPG/JPEG/BMP/
    TIFF/WEBP/PNG → convertido para PNG lossless, renomeado para kebab-case
- MOV (ou
    qualquer outro) → copiado com nome kebab-case, extensão mantida

Toda a saída vai para uma subpasta "converted/" dentro da pasta raiz,
espelhando a estrutura de subpastas original. Os originais nunca são tocados.
Colisões de nome são resolvidas automaticamente: foto.png → foto-2.png

Regra de renomeação: 'Hello World Image' / 'MyClientPhoto' → 'hello-world-image'

Requisitos:
pip install pillow pillow-heif

Uso:
python convert_heic.py                        # processa a pasta atual
python convert_heic.py "C:/caminho/pasta"     # processa pasta específica
"""

import os
import re
import sys
import shutil

IMAGE_EXTENSIONS = {'.heic', '.jpg', '.jpeg', '.bmp', '.tiff', '.tif', '.webp', '.png'}
MAX_WIDTH = 2500


# ── Helpers ───────────────────────────────────────────────────────────────────

def normalize_name(stem: str) -> str:
    stem = re.sub(r'(?<=[a-z0-9])(?=[A-Z])', ' ', stem)  # split PascalCase
    stem = stem.lower()
    stem = re.sub(r'[\s_\-]+', '-', stem)                 # espaços → hífens
    return stem.strip('-')


def unique_dst(path: str) -> str:
    """Se o caminho já existir, adiciona -2, -3, etc. até encontrar um livre."""
    if not os.path.exists(path):
        return path
    base, ext = os.path.splitext(path)
    counter = 2
    while True:
        candidate = f"{base}-{counter}{ext}"
        if not os.path.exists(candidate):
            return candidate
        counter += 1


def collect_files(root_folder: str, out_dir: str):
    """
    Percorre root_folder recursivamente (excluindo out_dir).
    Retorna duas listas de tuplas (src_path, dst_path):
    - images: arquivos de imagem a converter para PNG
    - others: demais arquivos a copiar com renomeação
    """
    images, others = [], []

    for dirpath, dirnames, filenames in os.walk(root_folder):
        # Evita entrar na pasta de saída
        dirnames[:] = [
            d for d in sorted(dirnames)
            if os.path.abspath(os.path.join(dirpath, d)) != os.path.abspath(out_dir)
        ]

        # Calcula o subcaminho relativo à pasta raiz
        rel_dir = os.path.relpath(dirpath, root_folder)
        dst_dir = os.path.join(out_dir, rel_dir) if rel_dir != '.' else out_dir

        for f in sorted(filenames):
            src = os.path.join(dirpath, f)
            ext = os.path.splitext(f)[1].lower()
            stem = os.path.splitext(f)[0]

            if ext in IMAGE_EXTENSIONS:
                dst = unique_dst(os.path.join(dst_dir, normalize_name(stem) + '.png'))
                images.append((src, dst, dst_dir))
            else:
                dst = unique_dst(os.path.join(dst_dir, normalize_name(stem) + ext.lower()))
                others.append((src, dst, dst_dir))

    return images, others


# ── Conversão ─────────────────────────────────────────────────────────────────

def convert_to_png(src: str, dst: str) -> bool:
    try:
        from pillow_heif import register_heif_opener
        from PIL import Image
        register_heif_opener()
        img = Image.open(src)
        if img.mode not in ('RGB', 'RGBA', 'L', 'LA'):
            img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB')
        if img.width > MAX_WIDTH:
            new_height = round(img.height * (MAX_WIDTH / img.width))
            img = img.resize((MAX_WIDTH, new_height), Image.LANCZOS)
            print(f"    redimensionado para {MAX_WIDTH}x{new_height}")
        img.save(dst, format='PNG', optimize=False, compress_level=0)
        return True
    except Exception as e:
        print(f"    [erro] {e}")
        return False


# ── Main ──────────────────────────────────────────────────────────────────────

def main():
    folder = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
    folder = os.path.abspath(folder)

    if not os.path.isdir(folder):
        print(f"Erro: '{folder}' não é um diretório válido.")
        sys.exit(1)

    out_dir = os.path.join(folder, 'converted')

    images, others = collect_files(folder, out_dir)
    total = len(images) + len(others)

    if total == 0:
        print(f"Nenhum arquivo encontrado em: {folder}")
        sys.exit(0)

    os.makedirs(out_dir, exist_ok=True)

    print(f"Origem:  {folder}")
    print(f"Saída:   {out_dir}")
    print(f"Encontrado(s): {len(images)} imagem(ns) para converter  +  {len(others)} arquivo(s) para renomear\n")

    converted = renamed = failed = 0

    # ── Imagens → PNG ─────────────────────────────────────────────────────────
    if images:
        print("── Imagens ──────────────────────────────────────────────")
    for src, dst, dst_dir in images:
        os.makedirs(dst_dir, exist_ok=True)

        # Mostra caminho relativo para facilitar leitura no terminal
        src_rel = os.path.relpath(src, folder)
        dst_rel = os.path.relpath(dst, out_dir)

        print(f"  ...   {src_rel}{dst_rel}")
        if convert_to_png(src, dst):
            print(f"  OK    {dst_rel}")
            converted += 1
        else:
            print(f"  FAIL  {src_rel}")
            failed += 1

    # ── Outros arquivos → copiar e renomear ───────────────────────────────────
    if others:
        print("\n── Outros arquivos (somente renomeação) ─────────────────")
    for src, dst, dst_dir in others:
        os.makedirs(dst_dir, exist_ok=True)

        src_rel = os.path.relpath(src, folder)
        dst_rel = os.path.relpath(dst, out_dir)

        shutil.copy2(src, dst)
        print(f"  OK    {src_rel}{dst_rel}")
        renamed += 1

    print(f"""
Concluído.  {converted} imagem(ns) convertida(s) para PNG
            {renamed} arquivo(s) copiado(s) e renomeado(s)
            {failed} falha(s)
""")


if __name__ == '__main__':
    main()