|
| 1 | +""" |
| 2 | +Instagram için görsel optimizasyonu. |
| 3 | +- Girdi klasoru: convert |
| 4 | +- Cikti klasoru: converted |
| 5 | +
|
| 6 | +requirements.txt: |
| 7 | +Pillow |
| 8 | +pillow-heif |
| 9 | +rawpy |
| 10 | +
|
| 11 | +""" |
| 12 | + |
| 13 | +from __future__ import annotations |
| 14 | + |
| 15 | +import io, sys |
| 16 | +from pathlib import Path |
| 17 | +from typing import Iterable, Tuple |
| 18 | + |
| 19 | +from PIL import Image, ImageCms, ImageFilter |
| 20 | + |
| 21 | +# RAW destegi icin istege bagli kutuphane |
| 22 | +try: |
| 23 | + import rawpy |
| 24 | + RAW_AVAILABLE = True |
| 25 | +except Exception: |
| 26 | + RAW_AVAILABLE = False |
| 27 | + |
| 28 | +# HEIC/HEIF destegi icin istege bagli kutuphane |
| 29 | +try: |
| 30 | + from pillow_heif import register_heif_opener |
| 31 | + register_heif_opener() |
| 32 | + HEIF_AVAILABLE = True |
| 33 | +except Exception: HEIF_AVAILABLE = False |
| 34 | + |
| 35 | + |
| 36 | +RAW_EXTS = {".arw"} |
| 37 | +SUPPORTED_EXTS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp", ".webp", ".heic", ".heif"} | RAW_EXTS |
| 38 | + |
| 39 | + |
| 40 | +def ask_output_format() -> Tuple[int, int]: |
| 41 | + """Kullanicidan hedef Instagram formatini alir ve boyut dondurur.""" |
| 42 | + print("Hangi Instagram formatini istiyorsunuz?") |
| 43 | + print("1) Dikey 4:5 1080x1350") |
| 44 | + print("2) Kare 1:1 1080x1080") |
| 45 | + print("3) Yatay 1.91:1 1080x566") |
| 46 | + print("4) Reels 9:16 1080x1920") |
| 47 | + |
| 48 | + choice = input("Seciminiz (1/2/3/4) [varsayilan: 1]: ").strip() |
| 49 | + if not choice: choice = "1" |
| 50 | + if choice == "1": return (2160, 2700) |
| 51 | + if choice == "2": return (2160, 2160) |
| 52 | + if choice == "3": return (2160, 1131) |
| 53 | + if choice == "4": return (2160, 3840) |
| 54 | + |
| 55 | + print("Gecersiz secim.") |
| 56 | + return ask_output_format() |
| 57 | + |
| 58 | + |
| 59 | +def ask_background_mode() -> str: |
| 60 | + """Kullanicidan arka plan modunu alir.""" |
| 61 | + print("Arka plan tercihi nedir?") |
| 62 | + print("1) Siyah") |
| 63 | + print("2) Beyaz") |
| 64 | + print("3) Blur") |
| 65 | + |
| 66 | + choice = input("Seciminiz (1/2/3) [varsayilan: 1]: ").strip() |
| 67 | + if not choice: choice = "1" |
| 68 | + if choice == "1": return "black" |
| 69 | + if choice == "2": return "white" |
| 70 | + if choice == "3": return "blur" |
| 71 | + |
| 72 | + print("Gecersiz secim.") |
| 73 | + return ask_background_mode() |
| 74 | + |
| 75 | + |
| 76 | +def iter_images(folder: Path) -> Iterable[Path]: |
| 77 | + """Desteklenen uzantilara sahip dosyalari listeler.""" |
| 78 | + for path in sorted(folder.iterdir()): |
| 79 | + if path.is_file() and path.suffix.lower() in SUPPORTED_EXTS: |
| 80 | + yield path |
| 81 | + |
| 82 | + |
| 83 | +def load_input_image(path: Path) -> Image.Image: |
| 84 | + """Girdiyi PIL Image olarak yukler (RAW dahil).""" |
| 85 | + if path.suffix.lower() in RAW_EXTS: |
| 86 | + if not RAW_AVAILABLE: |
| 87 | + raise RuntimeError("RAW icin 'rawpy' kurulu degil") |
| 88 | + with rawpy.imread(str(path)) as raw: |
| 89 | + rgb = raw.postprocess(use_camera_wb=True, output_bps=8) |
| 90 | + return Image.fromarray(rgb) |
| 91 | + |
| 92 | + with Image.open(path) as img: |
| 93 | + return img.copy() |
| 94 | + |
| 95 | + |
| 96 | +def convert_to_srgb(img: Image.Image) -> Image.Image: |
| 97 | + """Gorseli sRGB renk uzayina cevirir.""" |
| 98 | + # ICC profili varsa, profileToProfile ile donusum yapilir. |
| 99 | + icc_bytes = img.info.get("icc_profile") |
| 100 | + if icc_bytes: |
| 101 | + try: |
| 102 | + src_profile = ImageCms.ImageCmsProfile(io.BytesIO(icc_bytes)) |
| 103 | + dst_profile = ImageCms.createProfile("sRGB") |
| 104 | + return ImageCms.profileToProfile(img, src_profile, dst_profile, outputMode="RGB") |
| 105 | + except Exception: pass |
| 106 | + |
| 107 | + # ICC yoksa (veya hata varsa) RGB'ye zorla. |
| 108 | + if img.mode != "RGB": return img.convert("RGB") |
| 109 | + return img |
| 110 | + |
| 111 | + |
| 112 | +def ensure_rgb_no_alpha(img: Image.Image) -> Image.Image: |
| 113 | + """JPEG kaydi icin alfa kanali varsa beyaz zemine birlestirir.""" |
| 114 | + if img.mode in {"RGBA", "LA"}: |
| 115 | + background = Image.new("RGB", img.size, (255, 255, 255)) |
| 116 | + background.paste(img, mask=img.split()[-1]) |
| 117 | + return background |
| 118 | + if img.mode != "RGB": return img.convert("RGB") |
| 119 | + return img |
| 120 | + |
| 121 | + |
| 122 | +def fit_with_background(img: Image.Image, target_w: int, target_h: int, mode: str) -> Image.Image: |
| 123 | + """Orani bozmadan sigdir ve arka planla tamamla.""" |
| 124 | + src_w, src_h = img.size |
| 125 | + scale = min(target_w / src_w, target_h / src_h) |
| 126 | + new_w = int(round(src_w * scale)) |
| 127 | + new_h = int(round(src_h * scale)) |
| 128 | + |
| 129 | + resized = img.resize((new_w, new_h), Image.Resampling.LANCZOS) |
| 130 | + if mode == "white": |
| 131 | + canvas = Image.new("RGB", (target_w, target_h), (255, 255, 255)) |
| 132 | + elif mode == "blur": |
| 133 | + base = img.resize((target_w, target_h), Image.Resampling.LANCZOS) |
| 134 | + canvas = base.filter(ImageFilter.GaussianBlur(radius=32)) |
| 135 | + else: |
| 136 | + canvas = Image.new("RGB", (target_w, target_h), (0, 0, 0)) |
| 137 | + left = (target_w - new_w) // 2 |
| 138 | + top = (target_h - new_h) // 2 |
| 139 | + canvas.paste(resized, (left, top)) |
| 140 | + return canvas |
| 141 | + |
| 142 | + |
| 143 | +def save_jpeg_under_limit(img: Image.Image, out_path: Path, max_bytes: int = 8 * 1024 * 1024) -> None: |
| 144 | + """JPEG olarak kaydeder; 8MB uzerindeyse kaliteyi dusurur.""" |
| 145 | + quality = 100 |
| 146 | + min_quality = 60 |
| 147 | + |
| 148 | + while True: |
| 149 | + buffer = io.BytesIO() |
| 150 | + img.save(buffer, format="JPEG", quality=quality, optimize=True) |
| 151 | + size = buffer.tell() |
| 152 | + |
| 153 | + if size <= max_bytes or quality <= min_quality: |
| 154 | + out_path.write_bytes(buffer.getvalue()) |
| 155 | + return |
| 156 | + |
| 157 | + # Boyut fazla ise kaliteyi kademeli dusur. |
| 158 | + quality -= 5 |
| 159 | + |
| 160 | + |
| 161 | +def main() -> None: |
| 162 | + target_w, target_h = ask_output_format() |
| 163 | + bg_mode = ask_background_mode() |
| 164 | + |
| 165 | + base_dir = Path(__file__).resolve().parent |
| 166 | + input_dir = base_dir / "convert" |
| 167 | + output_dir = base_dir / "converted" |
| 168 | + |
| 169 | + # Klasorler yoksa olustur. |
| 170 | + input_dir.mkdir(parents=True, exist_ok=True) |
| 171 | + output_dir.mkdir(parents=True, exist_ok=True) |
| 172 | + |
| 173 | + if not HEIF_AVAILABLE: |
| 174 | + print("Not: HEIC/HEIF icin 'pillow-heif' kurulu degil, bu dosyalar atlanacak.") |
| 175 | + if not RAW_AVAILABLE: |
| 176 | + print("Not: RAW (ARW) icin 'rawpy' kurulu degil, bu dosyalar atlanacak.") |
| 177 | + |
| 178 | + images = list(iter_images(input_dir)) |
| 179 | + if not images: |
| 180 | + print(f"Islenecek gorsel bulunamadi: {input_dir}") |
| 181 | + return |
| 182 | + |
| 183 | + total = len(images) |
| 184 | + for idx, path in enumerate(images, start=1): |
| 185 | + if path.suffix.lower() in {".heic", ".heif"} and not HEIF_AVAILABLE: |
| 186 | + print(f"{idx}/{total} atlandi (HEIC destegi yok): {path.name}") |
| 187 | + continue |
| 188 | + if path.suffix.lower() in RAW_EXTS and not RAW_AVAILABLE: |
| 189 | + print(f"{idx}/{total} atlandi (RAW destegi yok): {path.name}") |
| 190 | + continue |
| 191 | + try: |
| 192 | + img = load_input_image(path) |
| 193 | + # Renk uzayini sRGB'ye donustur. |
| 194 | + img = convert_to_srgb(img) |
| 195 | + # JPEG icin alfa kanali temizle. |
| 196 | + img = ensure_rgb_no_alpha(img) |
| 197 | + # Orani bozmadan sigdir ve arka planla tamamla. |
| 198 | + img = fit_with_background(img, target_w, target_h, bg_mode) |
| 199 | + |
| 200 | + out_name = path.stem + ".jpg" |
| 201 | + out_path = output_dir / out_name |
| 202 | + save_jpeg_under_limit(img, out_path) |
| 203 | + |
| 204 | + print(f"{idx}/{total} islendi: {path.name}") |
| 205 | + except Exception as exc: |
| 206 | + print(f"{idx}/{total} atlandi (hata): {path.name} -> {exc}") |
| 207 | + |
| 208 | + |
| 209 | +if __name__ == "__main__": |
| 210 | + main() |
0 commit comments