Ver3.5 ๐ผ๏ธ ๋๋ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ ์๋ํ์์ค์ฝ๋ ์์ ์ ๋ณต

๐ผ๏ธ ๋๋ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ ์๋ํ
์์ค์ฝ๋ ์์ ์ ๋ณต
์๋ฐฑ ์ฅ๋ 1์ด์ปท? ๊ทธ๊ฑฐ ์ฝ๋๋ก ๊ฐ๋ฅํจ ใ
ใ
Python๋ถํฐ Node.js๊น์ง ์ค์ ์์ค ์ด์ ๋ฆฌ!
๐ ์ค๋ ๋ฐฐ์ธ ๋ด์ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
์ ์๋ํ?
์๋ ๋ฆฌ์ฌ์ด์ฆ์ ํ๊ณ
Python Pillow
๊ฐ์ฅ ๋์ค์ ์ธ ๋ฐฉ๋ฒ
Node.js Sharp
์ด๊ณ ์ ์ฒ๋ฆฌ
Bash ์คํฌ๋ฆฝํธ
ImageMagick ํ์ฉ
ํด๋ ๊ตฌ์กฐ ์ค๊ณ
์ค์ ํ๋ก์ ํธ ์ธํ
๊ณ ๊ธ ์ต์
์ํฐ๋งํฌยทํฌ๋งท๋ณํ
์ฑ๋ฅ ์ต์ ํ
๋ฉํฐ์ค๋ ๋ฉ ์ฒ๋ฆฌ
์ค์ ํ
์๋ฌ ํธ๋ค๋ง๊น์ง
๐ค ์๋์ผ๋ก ํ๋ฉด ์ ๋๋์? (๋ฉ๋๋ค๋ง...)
์์งํ ๋งํ๋ฉด ์ด๋ฏธ์ง 1~2์ฅ์ ํฌํ ์ต์ด๋ ๊ทธ๋ฆผํ์ผ๋ก๋ ์ถฉ๋ถํจ ใ
ใ
๊ทผ๋ฐ ํ์ค์ ๊ทธ๋ ์ง ์์์์?
์ผํ๋ชฐ ์ด์ํ๋ฉด์ ์ํ ์ด๋ฏธ์ง 500์ฅ์ ์น์ฉยท์ธ๋ค์ผ์ฉยท๋ชจ๋ฐ์ผ์ฉ์ผ๋ก ๊ฐ๊ฐ ๋ง๋ค์ด์ผ ํ๋ค๊ณ ์๊ฐํด๋ด์.
500์ฅ ร 3๊ฐ์ง ์ฌ์ด์ฆ = 1,500๋ฒ์ ์๋ ์์
๐ฑ
์ด๊ฒ ๋ฐ๋ก ์๋ํ๊ฐ ํ์ํ ์ด์ ์. ์ฝ๋ ํ ๋ฒ ์ง๋์ผ๋ฉด ๊ทธ ๋ค์๋ถํฐ๋ ๊ทธ๋ฅ ์คํ๋ง ํ๋ฉด ๋๊ฑฐ๋ ์!
โ ๋ธ๋ก๊ทธ/SNS์ฉ ์ด๋ฏธ์ง ์ฌ์ด์ฆ ๊ท๊ฒฉ ๋ง์ถ๊ธฐ
โ ์ฑ ๊ฐ๋ฐ ์ ์์ด์ฝ ์ฌ๋ฌ ํด์๋ ์์ฑ (1x, 2x, 3x)
โ ์น์ฌ์ดํธ ๋ฐ์ํ ์ด๋ฏธ์ง ์ธํธ ์์ฑ
โ ์ฌ์ง ์์นด์ด๋ธ ์ ๋ฆฌ ๋ฐ ์ธ๋ค์ผ ์ผ๊ด ์์ฑ
- โ ๏ธ ์๋ ์์ ์ ํ์ค์ ์ธ ๋ฌธ์ ์
- โ ์๊ฐ์ด ๋๋ฌด ์ค๋ ๊ฑธ๋ฆผ (500์ฅ ์๋ = ๋ช ์๊ฐ)
- โ ์ฌ๋์ด ํ๋ค ๋ณด๋ฉด ์ค์ ๋ฐ์ (์ฌ์ด์ฆ ์๋ชป ์ ๋ ฅ ๋ฑ)
- โ ์ผ๊ด์ฑ์ด ์์ด์ง (๋งค๋ฒ ์กฐ๊ธ์ฉ ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฌผ)
- โ ๋ฐ๋ณต ์์ ์ผ๋ก ์ธํ ๋ฒ์์ ใ ใ
๐ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ ๊ธฐ๋ณธ ๊ฐ๋ ์ ๋ฆฌ
์ฝ๋ ๋ค์ด๊ฐ๊ธฐ ์ ์ ๊ธฐ๋ณธ ๊ฐ๋ ์ข ์ก๊ณ ๊ฐ์ผ ํด์. ์ด๊ฑฐ ๋ชจ๋ฅด๋ฉด ๋์ค์ ๊ฒฐ๊ณผ๋ฌผ ๋ณด๊ณ "์ ์ด์ํ์ง?" ํ๊ฒ ๋จ ใ ใ
๐ธ ๋ฆฌ์ฌ์ด์ฆ ๋ฐฉ์์ ์ข ๋ฅ
| ๋ฐฉ์ | ์ค๋ช | ์ธ์ ์ฐ๋ |
|---|---|---|
| resize | ๊ฐ๋กยท์ธ๋ก ๋น์จ ๋ฌด์ํ๊ณ ๊ฐ์ ๋ณํ | ์ ํํ ์ฌ์ด์ฆ๊ฐ ํ์ํ ๋ |
| thumbnail | ๋น์จ ์ ์งํ๋ฉฐ ์ง์ ํฌ๊ธฐ ์์ ๋ง์ถค | ์ธ๋ค์ผ ์์ฑ ์ |
| crop | ๋น์จ ์ ์งํ๋ฉฐ ์๋ผ๋ด๊ธฐ | ์ ์ฌ๊ฐํ ํ๋กํ ์ด๋ฏธ์ง ๋ฑ |
| fit/contain | ๋น์จ ์ ์ง, ๋น ๊ณต๊ฐ์ ๋ฐฐ๊ฒฝ์์ผ๋ก | ๊ท ์ผํ ์บ๋ฒ์ค ํฌ๊ธฐ ํ์ ์ |
| cover | ๋น์จ ์ ์ง, ์์ญ์ ๊ฝ ์ฑ์ฐ๋ฉฐ ๋์น๋ ๋ถ๋ถ ์๋ผ๋ | ๋ฐฐ๋ยท์ปค๋ฒ ์ด๋ฏธ์ง |
๐ธ ์ด๋ฏธ์ง ํฌ๋งท๋ณ ํน์ง
์ฌ์ง์ ์ต์ ํ
์์ค ์์ถ
ํฌ๋ช ๋ ์์
ํฌ๋ช ๋ ์ง์
๋ฌด์์ค ์์ถ
ํ์ผ ํฌ๊ธฐ ํผ
๊ตฌ๊ธ ๊ฐ๋ฐ
JPEG๋ณด๋ค 30% ์์
์ต์ ๋ธ๋ผ์ฐ์ ์ง์
์ฐจ์ธ๋ ํฌ๋งท
WebP๋ณด๋ค ๋ ์์
์ง์ ๋ฒ์ ํ๋ ์ค
โข NEAREST: ๊ฐ์ฅ ๋น ๋ฆ, ํ์ง ๋ฎ์ (ํฝ์ ์ํธ์ ์ ํฉ)
โข BILINEAR: ์ค๊ฐ ์๋, ์ค๊ฐ ํ์ง
โข BICUBIC: ๋๋ฆฌ์ง๋ง ํ์ง ์ข์ (์ผ๋ฐ์ ์ผ๋ก ๋ง์ด ์)
โข LANCZOS: ๊ฐ์ฅ ๊ณ ํ์ง, ๊ฐ์ฅ ๋๋ฆผ (๊ณ ํ์ง ์ถ๋ ฅ ์)
๐ Python + Pillow๋ก ๋๋ ๋ฆฌ์ฌ์ด์ฆ ์๋ํ
Python์ Pillow ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ด๋ฏธ์ง ์ฒ๋ฆฌ์ ๊ตญ๋ฃฐ์ ใ
ใ
์ค์น๋ ์ฝ๊ณ , ๋ฌธ์๋ ์ ๋์ด ์๊ณ , ์ปค๋ฎค๋ํฐ๋ ํ๋ฐํด์ ์
๋ฌธ์ํํ
๊ฐ์ถ!
๐ฆ ์ค์น ๋ฐฉ๋ฒ
# pip์ผ๋ก ๊ฐ๋จ ์ค์น!
pip install Pillow
# ์ค์น ํ์ธ
python -c "from PIL import Image; print('Pillow ์ค์น ์๋ฃ!')"
๐ฐ ๊ธฐ๋ณธ ๋จ์ผ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ
๋จผ์ ๊ธฐ๋ณธ๋ถํฐ ์ตํ๊ณ ๊ฐ์ผ์ฃ !
from PIL import Image
# ์ด๋ฏธ์ง ์ด๊ธฐ
img = Image.open("์๋ณธ์ด๋ฏธ์ง.jpg")
# ๋ฐฉ๋ฒ 1: ๊ฐ์ ๋ฆฌ์ฌ์ด์ฆ (๋น์จ ๋ฌด์)
resized = img.resize((800, 600))
resized.save("๊ฒฐ๊ณผ_๊ฐ์ .jpg")
# ๋ฐฉ๋ฒ 2: ๋น์จ ์ ์งํ๋ฉฐ ๋ฆฌ์ฌ์ด์ฆ (thumbnail)
img_copy = img.copy()
img_copy.thumbnail((800, 600), Image.LANCZOS)
img_copy.save("๊ฒฐ๊ณผ_๋น์จ์ ์ง.jpg")
# ๋ฐฉ๋ฒ 3: ํ์ง ์ค์ ํ๋ฉฐ ์ ์ฅ (JPEG๋ 1~95)
resized.save("๊ฒฐ๊ณผ_๊ณ ํ์ง.jpg", quality=90, optimize=True)
print(f"์๋ณธ ํฌ๊ธฐ: {img.size}")
print(f"๋ณํ ํ ํฌ๊ธฐ: {resized.size}")
๐ ๋๋ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ - ๊ธฐ๋ณธ ๋ฒ์
์ด์ ์ง์ง ํต์ฌ! ํด๋ ์์ ๋ชจ๋ ์ด๋ฏธ์ง๋ฅผ ํ ๋ฒ์ ์ฒ๋ฆฌํ๋ ์ฝ๋์ ๐
import os
from PIL import Image
# ===== ์ค์ ๋ถ๋ถ (์ฌ๊ธฐ๋ง ์์ ํ๋ฉด ๋จ!) =====
INPUT_FOLDER = "./์๋ณธ์ด๋ฏธ์ง" # ์๋ณธ ์ด๋ฏธ์ง ํด๋
OUTPUT_FOLDER = "./๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ" # ๊ฒฐ๊ณผ๋ฌผ ์ ์ฅ ํด๋
TARGET_WIDTH = 800 # ๋ชฉํ ๊ฐ๋ก ํฌ๊ธฐ
TARGET_HEIGHT = 600 # ๋ชฉํ ์ธ๋ก ํฌ๊ธฐ
QUALITY = 85 # JPEG ํ์ง (1~95)
# ==========================================
# ์ง์ํ๋ ์ด๋ฏธ์ง ํ์ฅ์
SUPPORTED_FORMATS = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp', '.tiff')
def resize_images_bulk(input_folder, output_folder, width, height, quality=85):
"""ํด๋ ๋ด ๋ชจ๋ ์ด๋ฏธ์ง๋ฅผ ์ผ๊ด ๋ฆฌ์ฌ์ด์ฆ"""
# ๊ฒฐ๊ณผ ํด๋ ์์ผ๋ฉด ์๋ ์์ฑ
os.makedirs(output_folder, exist_ok=True)
# ์ฒ๋ฆฌ ํต๊ณ
success_count = 0
fail_count = 0
# ํด๋ ๋ด ํ์ผ ์ํ
for filename in os.listdir(input_folder):
# ์ด๋ฏธ์ง ํ์ผ๋ง ์ฒ๋ฆฌ
if not filename.lower().endswith(SUPPORTED_FORMATS):
continue
input_path = os.path.join(input_folder, filename)
output_path = os.path.join(output_folder, filename)
try:
with Image.open(input_path) as img:
# RGBA(ํฌ๋ช
๋) ์ด๋ฏธ์ง๋ RGB๋ก ๋ณํ ํ ์ ์ฅ
if img.mode in ('RGBA', 'LA', 'P'):
img = img.convert('RGB')
# ๋น์จ ์ ์งํ๋ฉฐ ๋ฆฌ์ฌ์ด์ฆ
img.thumbnail((width, height), Image.LANCZOS)
# ์ ์ฅ (JPEG ํ์ง ์ค์ )
if filename.lower().endswith(('.jpg', '.jpeg')):
img.save(output_path, 'JPEG', quality=quality, optimize=True)
else:
img.save(output_path)
success_count += 1
print(f"โ
์๋ฃ: {filename} โ {img.size}")
except Exception as e:
fail_count += 1
print(f"โ ์คํจ: {filename} - ์ค๋ฅ: {e}")
print(f"\n๐ ์ฒ๋ฆฌ ์๋ฃ! ์ฑ๊ณต: {success_count}์ฅ, ์คํจ: {fail_count}์ฅ")
# ์คํ!
resize_images_bulk(INPUT_FOLDER, OUTPUT_FOLDER, TARGET_WIDTH, TARGET_HEIGHT, QUALITY)
โก ๋ฉํฐ์ค๋ ๋ฉ์ผ๋ก ์๋ ์ ๊ทธ๋ ์ด๋!
์ด๋ฏธ์ง๊ฐ ์๋ฐฑ ์ฅ์ด๋ฉด ์ ์ฝ๋๋ ์ข ๋๋ฆด ์ ์์ด์.
๋ฉํฐ์ค๋ ๋ฉ ์ฐ๋ฉด CPU ์ฝ์ด๋ฅผ ์ฌ๋ฌ ๊ฐ ํ์ฉํด์ ํจ์ฌ ๋น ๋ฆ! ๐ฅ
import os
from PIL import Image
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
INPUT_FOLDER = "./์๋ณธ์ด๋ฏธ์ง"
OUTPUT_FOLDER = "./๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ"
TARGET_SIZE = (800, 600)
QUALITY = 85
MAX_WORKERS = 8 # ๋์ ์ฒ๋ฆฌ ์ค๋ ๋ ์ (CPU ์ฝ์ด ์์ ๋ง๊ฒ ์กฐ์ )
SUPPORTED_FORMATS = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp', '.tiff')
# ์ค๋ ๋ ์์ ํ ์นด์ดํฐ
lock = threading.Lock()
success_count = 0
fail_count = 0
def process_single_image(args):
"""๋จ์ผ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ํจ์ (์ค๋ ๋์์ ์คํ๋จ)"""
global success_count, fail_count
input_path, output_path, filename = args
try:
with Image.open(input_path) as img:
if img.mode in ('RGBA', 'LA', 'P'):
img = img.convert('RGB')
img.thumbnail(TARGET_SIZE, Image.LANCZOS)
if filename.lower().endswith(('.jpg', '.jpeg')):
img.save(output_path, 'JPEG', quality=QUALITY, optimize=True)
else:
img.save(output_path)
with lock:
success_count += 1
return f"โ
{filename}"
except Exception as e:
with lock:
fail_count += 1
return f"โ {filename}: {e}"
def bulk_resize_multithreaded():
"""๋ฉํฐ์ค๋ ๋ฉ ๋๋ ๋ฆฌ์ฌ์ด์ฆ"""
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
# ์ฒ๋ฆฌํ ํ์ผ ๋ชฉ๋ก ์์ฑ
tasks = []
for filename in os.listdir(INPUT_FOLDER):
if filename.lower().endswith(SUPPORTED_FORMATS):
input_path = os.path.join(INPUT_FOLDER, filename)
output_path = os.path.join(OUTPUT_FOLDER, filename)
tasks.append((input_path, output_path, filename))
print(f"๐ ์ด {len(tasks)}๊ฐ ํ์ผ ์ฒ๋ฆฌ ์์...")
# ๋ฉํฐ์ค๋ ๋ฉ ์คํ
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = [executor.submit(process_single_image, task) for task in tasks]
for future in as_completed(futures):
print(future.result())
print(f"\n๐ ์๋ฃ! ์ฑ๊ณต: {success_count}์ฅ, ์คํจ: {fail_count}์ฅ")
bulk_resize_multithreaded()
๐ฏ ์ฌ๋ฌ ์ฌ์ด์ฆ ๋์ ์์ฑ (์ค์ ์ต๊ฐ ๋ฒ์ !)
์น ๊ฐ๋ฐํ ๋ ์ง์ง ๋ง์ด ์ฐ๋ ํจํด์.
์๋ณธ ํ๋๋ก ์ฌ๋ฌ ์ฌ์ด์ฆ๋ฅผ ํ ๋ฒ์ ๋ฝ์๋ด๋ ์ฝ๋! ๐
import os
from PIL import Image
from pathlib import Path
INPUT_FOLDER = "./์๋ณธ์ด๋ฏธ์ง"
OUTPUT_FOLDER = "./๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ"
# ์์ฑํ ์ฌ์ด์ฆ ํ๋ฆฌ์
์ ์
SIZE_PRESETS = {
"thumbnail": (150, 150), # ์ธ๋ค์ผ
"small": (320, 240), # ์ํ
"medium": (640, 480), # ์คํ
"large": (1280, 960), # ๋ํ
"hd": (1920, 1080), # HD
"square_sm": (200, 200), # ์ ์ฌ๊ฐํ ์
"square_lg": (500, 500), # ์ ์ฌ๊ฐํ ๋
}
SUPPORTED_FORMATS = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
def resize_to_multiple_sizes(input_folder, output_folder, presets):
"""์ฌ๋ฌ ์ฌ์ด์ฆ๋ก ์ผ๊ด ๋ณํ"""
# ๊ฐ ์ฌ์ด์ฆ๋ณ ํด๋ ์์ฑ
for preset_name in presets:
os.makedirs(os.path.join(output_folder, preset_name), exist_ok=True)
files = [f for f in os.listdir(input_folder)
if f.lower().endswith(SUPPORTED_FORMATS)]
print(f"๐ {len(files)}๊ฐ ํ์ผ ร {len(presets)}๊ฐ ์ฌ์ด์ฆ = {len(files)*len(presets)}๊ฐ ์์ฑ ์์ ")
for i, filename in enumerate(files, 1):
input_path = os.path.join(input_folder, filename)
name_without_ext = Path(filename).stem
ext = Path(filename).suffix
print(f"\n[{i}/{len(files)}] ์ฒ๋ฆฌ ์ค: {filename}")
try:
with Image.open(input_path) as original:
for preset_name, (width, height) in presets.items():
img = original.copy()
if img.mode in ('RGBA', 'LA', 'P'):
img = img.convert('RGB')
# ๋น์จ ์ ์ง ๋ฆฌ์ฌ์ด์ฆ
img.thumbnail((width, height), Image.LANCZOS)
# ํ์ผ๋ช
: ์๋ณธ๋ช
_์ฌ์ด์ฆ.ํ์ฅ์
output_filename = f"{name_without_ext}_{preset_name}{ext}"
output_path = os.path.join(output_folder, preset_name, output_filename)
if ext.lower() in ('.jpg', '.jpeg'):
img.save(output_path, 'JPEG', quality=85, optimize=True)
else:
img.save(output_path)
print(f" โ
{preset_name}: {img.size}")
except Exception as e:
print(f" โ ์ค๋ฅ: {e}")
print(f"\n๐ ๋ชจ๋ ์์
์๋ฃ!")
resize_to_multiple_sizes(INPUT_FOLDER, OUTPUT_FOLDER, SIZE_PRESETS)
์ด๋ฐ ์๋ํ ์คํฌ๋ฆฝํธ ํ๋ ๋ง๋ค์ด๋๋ฉด ํด๋ผ์ด์ธํธ ์์ ์๊ฐ์ ์์ฒญ๋๊ฒ ๋จ์ถํ ์ ์์ด์ ๊ฒฝ์๋ ฅ์ด ์๊น ใ ใ
โก Node.js + Sharp๋ก ์ด๊ณ ์ ๋ฆฌ์ฌ์ด์ฆ
Sharp๋ libvips ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ธฐ๋ฐ์ผ๋ก ๋ง๋ค์ด์ง Node.js ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๋ชจ๋์.
Pillow๋ณด๋ค ํจ์ฌ ๋น ๋ฅด๊ณ , ํนํ WebPยทAVIF ๊ฐ์ ์ต์ ํฌ๋งท ์ฒ๋ฆฌ์ ๊ฐํจ! ๐ฅ
์น ์๋ฒ์์ ์ค์๊ฐ์ผ๋ก ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆํ ๋ ํนํ ๋ง์ด ์.
Next.js์ Image ์ปดํฌ๋ํธ๋ ๋ด๋ถ์ ์ผ๋ก Sharp ์ ใ
ใ
๐ฆ ์ค์น ๋ฐฉ๋ฒ
# npm์ผ๋ก ์ค์น
npm install sharp
# ๋๋ yarn
yarn add sharp
# ์ค์น ํ์ธ
node -e "const sharp = require('sharp'); console.log('Sharp ์ค์น ์๋ฃ!');"
๐ฐ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
const sharp = require('sharp');
// ๊ธฐ๋ณธ ๋ฆฌ์ฌ์ด์ฆ
sharp('์๋ณธ์ด๋ฏธ์ง.jpg')
.resize(800, 600) // ๊ฐ๋ก 800, ์ธ๋ก 600
.toFile('๊ฒฐ๊ณผ.jpg')
.then(info => console.log('์๋ฃ:', info))
.catch(err => console.error('์ค๋ฅ:', err));
// ๋น์จ ์ ์ง ๋ฆฌ์ฌ์ด์ฆ (fit ์ต์
)
sharp('์๋ณธ์ด๋ฏธ์ง.jpg')
.resize(800, 600, {
fit: 'inside', // ๋น์จ ์ ์ง, ์์ชฝ์ ๋ง์ถค
withoutEnlargement: true // ์๋ณธ๋ณด๋ค ํฌ๊ฒ ํ๋ ์ ํจ
})
.jpeg({ quality: 85 }) // JPEG ํ์ง ์ค์
.toFile('๊ฒฐ๊ณผ_๋น์จ์ ์ง.jpg');
// WebP๋ก ๋ณํํ๋ฉฐ ๋ฆฌ์ฌ์ด์ฆ
sharp('์๋ณธ์ด๋ฏธ์ง.jpg')
.resize(800) // ๊ฐ๋ก๋ง ์ง์ (์ธ๋ก๋ ๋น์จ ์๋)
.webp({ quality: 80 })
.toFile('๊ฒฐ๊ณผ.webp');
๐ ๋๋ ์ฒ๋ฆฌ - Promise.all ํ์ฉ
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
// ===== ์ค์ =====
const INPUT_DIR = './์๋ณธ์ด๋ฏธ์ง';
const OUTPUT_DIR = './๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ';
const TARGET_WIDTH = 800;
const TARGET_HEIGHT = 600;
const JPEG_QUALITY = 85;
// ================
const SUPPORTED_EXTS = ['.jpg', '.jpeg', '.png', '.webp', '.tiff', '.bmp'];
async function resizeSingleImage(filename) {
const inputPath = path.join(INPUT_DIR, filename);
const outputPath = path.join(OUTPUT_DIR, filename);
const ext = path.extname(filename).toLowerCase();
try {
let pipeline = sharp(inputPath)
.resize(TARGET_WIDTH, TARGET_HEIGHT, {
fit: 'inside',
withoutEnlargement: true
});
// ํ์ฅ์์ ๋ฐ๋ผ ์ถ๋ ฅ ํฌ๋งท ์ค์
if (ext === '.jpg' || ext === '.jpeg') {
pipeline = pipeline.jpeg({ quality: JPEG_QUALITY, mozjpeg: true });
} else if (ext === '.png') {
pipeline = pipeline.png({ compressionLevel: 8 });
} else if (ext === '.webp') {
pipeline = pipeline.webp({ quality: JPEG_QUALITY });
}
const info = await pipeline.toFile(outputPath);
return { success: true, filename, info };
} catch (error) {
return { success: false, filename, error: error.message };
}
}
async function bulkResize() {
// ์ถ๋ ฅ ํด๋ ์์ฑ
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
// ์ด๋ฏธ์ง ํ์ผ ๋ชฉ๋ก ํํฐ๋ง
const files = fs.readdirSync(INPUT_DIR)
.filter(f => SUPPORTED_EXTS.includes(path.extname(f).toLowerCase()));
console.log(`๐ ์ด ${files.length}๊ฐ ํ์ผ ์ฒ๋ฆฌ ์์...`);
// ๋ฐฐ์น ์ฒ๋ฆฌ (ํ ๋ฒ์ ๋๋ฌด ๋ง์ด ์ด๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ ๊ฐ๋ฅ)
const BATCH_SIZE = 20;
let successCount = 0;
let failCount = 0;
for (let i = 0; i < files.length; i += BATCH_SIZE) {
const batch = files.slice(i, i + BATCH_SIZE);
const results = await Promise.all(batch.map(resizeSingleImage));
results.forEach(result => {
if (result.success) {
successCount++;
console.log(`โ
${result.filename} (${result.info.width}x${result.info.height})`);
} else {
failCount++;
console.log(`โ ${result.filename}: ${result.error}`);
}
});
console.log(`์งํ: ${Math.min(i + BATCH_SIZE, files.length)}/${files.length}`);
}
console.log(`\n๐ ์๋ฃ! ์ฑ๊ณต: ${successCount}์ฅ, ์คํจ: ${failCount}์ฅ`);
}
bulkResize();
๐ฏ ์ฌ๋ฌ ์ฌ์ด์ฆ + ํฌ๋งท ๋์ ๋ณํ (Sharp ๊ณ ๊ธ)
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
const INPUT_DIR = './์๋ณธ์ด๋ฏธ์ง';
const OUTPUT_DIR = './๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ';
// ์ถ๋ ฅ ํ๋ฆฌ์
์ ์
const OUTPUT_PRESETS = [
{ name: 'thumbnail', width: 150, height: 150, fit: 'cover', format: 'webp', quality: 80 },
{ name: 'small', width: 320, height: null, format: 'webp', quality: 80 },
{ name: 'medium', width: 640, height: null, format: 'jpeg', quality: 85 },
{ name: 'large', width: 1280, height: null, format: 'jpeg', quality: 90 },
{ name: 'og_image', width: 1200, height: 630, fit: 'cover', format: 'jpeg', quality: 90 },
];
async function processImageAllPresets(filename) {
const inputPath = path.join(INPUT_DIR, filename);
const nameWithoutExt = path.basename(filename, path.extname(filename));
const results = [];
for (const preset of OUTPUT_PRESETS) {
const outputFilename = `${nameWithoutExt}_${preset.name}.${preset.format}`;
const outputDir = path.join(OUTPUT_DIR, preset.name);
const outputPath = path.join(outputDir, outputFilename);
// ํด๋ ์์ฑ
fs.mkdirSync(outputDir, { recursive: true });
try {
let pipeline = sharp(inputPath).resize(preset.width, preset.height, {
fit: preset.fit || 'inside',
withoutEnlargement: true,
position: 'center'
});
if (preset.format === 'webp') {
pipeline = pipeline.webp({ quality: preset.quality });
} else if (preset.format === 'jpeg') {
pipeline = pipeline.jpeg({ quality: preset.quality, mozjpeg: true });
} else if (preset.format === 'avif') {
pipeline = pipeline.avif({ quality: preset.quality });
}
const info = await pipeline.toFile(outputPath);
results.push({ preset: preset.name, success: true, size: `${info.width}x${info.height}`, bytes: info.size });
} catch (err) {
results.push({ preset: preset.name, success: false, error: err.message });
}
}
return { filename, results };
}
async function main() {
const SUPPORTED_EXTS = ['.jpg', '.jpeg', '.png', '.webp', '.tiff'];
const files = fs.readdirSync(INPUT_DIR)
.filter(f => SUPPORTED_EXTS.includes(path.extname(f).toLowerCase()));
console.log(`๐ ${files.length}๊ฐ ํ์ผ ร ${OUTPUT_PRESETS.length}๊ฐ ํ๋ฆฌ์
์ฒ๋ฆฌ ์์!`);
for (const file of files) {
const result = await processImageAllPresets(file);
console.log(`\n๐ธ ${result.filename}`);
result.results.forEach(r => {
if (r.success) {
console.log(` โ
${r.preset}: ${r.size} (${(r.bytes/1024).toFixed(1)}KB)`);
} else {
console.log(` โ ${r.preset}: ${r.error}`);
}
});
}
console.log('\n๐ ์ ์ฒด ์๋ฃ!');
}
main();
๐ฅ๏ธ Bash + ImageMagick์ผ๋ก ์ปค๋งจ๋๋ผ์ธ ์๋ํ
Python์ด๋ Node.js ์์ด ํฐ๋ฏธ๋ ๋ช
๋ น์ด๋ง์ผ๋ก๋ ๋๋ ๋ฆฌ์ฌ์ด์ฆ ๊ฐ๋ฅํจ!
Linux/Mac ์๋ฒ ํ๊ฒฝ์์ ํนํ ์ ์ฉํ๊ณ , cron job์ด๋ ์ฐ๋ํ๋ฉด ์ค์ผ์ค ์๋ํ๋ ๋จ ๐
๐ฆ ImageMagick ์ค์น
# Ubuntu/Debian
sudo apt-get install imagemagick
# macOS (Homebrew)
brew install imagemagick
# ์ค์น ํ์ธ
convert --version
# ๋๋ ์ต์ ๋ฒ์ ์
magick --version
๐ฐ ๊ธฐ๋ณธ ๋ช ๋ น์ด
# ๋จ์ผ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ (๋น์จ ์ ์ง)
convert ์๋ณธ.jpg -resize 800x600 ๊ฒฐ๊ณผ.jpg
# ๊ฐ์ ๋ฆฌ์ฌ์ด์ฆ (๋น์จ ๋ฌด์, ! ์ฌ์ฉ)
convert ์๋ณธ.jpg -resize 800x600! ๊ฒฐ๊ณผ.jpg
# ๊ฐ๋ก๋ง ์ง์ (์ธ๋ก ์๋)
convert ์๋ณธ.jpg -resize 800 ๊ฒฐ๊ณผ.jpg
# ์ธ๋ก๋ง ์ง์ (๊ฐ๋ก ์๋)
convert ์๋ณธ.jpg -resize x600 ๊ฒฐ๊ณผ.jpg
# ํ์ง ์ค์
convert ์๋ณธ.jpg -resize 800x600 -quality 85 ๊ฒฐ๊ณผ.jpg
# WebP๋ก ๋ณํ
convert ์๋ณธ.jpg -resize 800x600 -quality 80 ๊ฒฐ๊ณผ.webp
๐ Bash ์คํฌ๋ฆฝํธ - ๋๋ ์ฒ๋ฆฌ
#!/bin/bash
# bulk_resize.sh - ๋๋ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ ์คํฌ๋ฆฝํธ
# ===== ์ค์ =====
INPUT_DIR="./์๋ณธ์ด๋ฏธ์ง"
OUTPUT_DIR="./๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ"
TARGET_SIZE="800x600"
QUALITY=85
# ================
# ๊ฒฐ๊ณผ ํด๋ ์์ฑ
mkdir -p "$OUTPUT_DIR"
# ์นด์ดํฐ ์ด๊ธฐํ
SUCCESS=0
FAIL=0
echo "๐ ๋๋ ๋ฆฌ์ฌ์ด์ฆ ์์..."
echo "์
๋ ฅ: $INPUT_DIR โ ์ถ๋ ฅ: $OUTPUT_DIR"
echo "๋ชฉํ ํฌ๊ธฐ: $TARGET_SIZE"
echo "================================"
# ์ง์ ํฌ๋งท ์ฒ๋ฆฌ
for ext in jpg jpeg png bmp gif webp tiff; do
for file in "$INPUT_DIR"/*.$ext "$INPUT_DIR"/*.${ext^^}; do
# ํ์ผ์ด ์ค์ ๋ก ์กด์ฌํ๋์ง ํ์ธ
[ -f "$file" ] || continue
filename=$(basename "$file")
output_path="$OUTPUT_DIR/$filename"
# ImageMagick์ผ๋ก ๋ฆฌ์ฌ์ด์ฆ
if convert "$file" \
-resize "$TARGET_SIZE" \
-quality "$QUALITY" \
-strip \
"$output_path" 2>/dev/null; then
echo "โ
์๋ฃ: $filename"
((SUCCESS++))
else
echo "โ ์คํจ: $filename"
((FAIL++))
fi
done
done
echo "================================"
echo "๐ ์๋ฃ! ์ฑ๊ณต: ${SUCCESS}์ฅ, ์คํจ: ${FAIL}์ฅ"
# ์คํฌ๋ฆฝํธ ์คํ ๊ถํ ๋ถ์ฌ ํ ์คํ
chmod +x bulk_resize.sh
./bulk_resize.sh
# ๋๋ mogrify ๋ช
๋ น์ด๋ก ๋ ๊ฐ๋จํ๊ฒ! (์๋ณธ ๋ฎ์ด์ฐ๊ธฐ ์ฃผ์!)
# ์๋ณธ ํด๋ ๋ณต์ฌ ํ ์ฌ์ฉ ๊ถ์ฅ
cp -r ./์๋ณธ์ด๋ฏธ์ง ./๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ
mogrify -resize 800x600 -quality 85 ./๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ/*.jpg
# ์ฌ๋ฌ ํฌ๋งท ํ ๋ฒ์
mogrify -resize 800x600 -quality 85 ./๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ/*.{jpg,jpeg,png}
mogrify๋ ์๋ณธ ํ์ผ์ ์ง์ ์์ ํจ!๋ฐ๋์ ์๋ณธ ํด๋๋ฅผ ๋ฐฑ์ ํ๊ฑฐ๋ ๋ณต์ฌํ ํ์ ์ฌ์ฉํ์ธ์.
์ค์๋ก ์๋ณธ ๋ ๋ฆฌ๋ฉด ๋ณต๊ตฌ ๋ถ๊ฐ๋ฅ ใ ใ
๐ง ๊ณ ๊ธ ๊ธฐ๋ฅ - ์ํฐ๋งํฌ, ํฌ๋งท ๋ณํ, EXIF ์ฒ๋ฆฌ
๐ง ์ํฐ๋งํฌ ์๋ ์ฝ์ (Python)
๋ฆฌ์ฌ์ด์ฆํ๋ฉด์ ์ํฐ๋งํฌ๊น์ง ํ ๋ฒ์ ๋ฐ์๋ฒ๋ฆฌ๋ ์ฝ๋! ์ผํ๋ชฐ์์ ์ง์ง ๋ง์ด ์ ใ ใ
from PIL import Image, ImageDraw, ImageFont
import os
def add_watermark_and_resize(input_path, output_path,
target_size=(800, 600),
watermark_text="ยฉ MyBrand",
opacity=128):
"""๋ฆฌ์ฌ์ด์ฆ + ์ํฐ๋งํฌ ๋์ ์ฒ๋ฆฌ"""
with Image.open(input_path) as img:
# RGB ๋ณํ
if img.mode != 'RGB':
img = img.convert('RGB')
# ๋ฆฌ์ฌ์ด์ฆ
img.thumbnail(target_size, Image.LANCZOS)
# ์ํฐ๋งํฌ ๋ ์ด์ด ์์ฑ (ํฌ๋ช
)
watermark_layer = Image.new('RGBA', img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(watermark_layer)
# ํฐํธ ์ค์ (์์คํ
ํฐํธ ๋๋ ๊ธฐ๋ณธ ํฐํธ)
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
size=max(20, img.width // 20))
except:
font = ImageFont.load_default()
# ํ
์คํธ ํฌ๊ธฐ ๊ณ์ฐ
bbox = draw.textbbox((0, 0), watermark_text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# ์ฐ์ธก ํ๋จ์ ์ํฐ๋งํฌ ๋ฐฐ์น
margin = 10
x = img.width - text_width - margin
y = img.height - text_height - margin
# ๊ทธ๋ฆผ์ ํจ๊ณผ (๊ฐ๋
์ฑ ํฅ์)
draw.text((x+2, y+2), watermark_text, font=font, fill=(0, 0, 0, opacity))
draw.text((x, y), watermark_text, font=font, fill=(255, 255, 255, opacity))
# ์ํฐ๋งํฌ ํฉ์ฑ
img_rgba = img.convert('RGBA')
img_rgba = Image.alpha_composite(img_rgba, watermark_layer)
final_img = img_rgba.convert('RGB')
# ์ ์ฅ
final_img.save(output_path, 'JPEG', quality=85, optimize=True)
print(f"โ
์๋ฃ: {os.path.basename(output_path)}")
# ํด๋ ์ ์ฒด ์ฒ๋ฆฌ
input_folder = "./์๋ณธ์ด๋ฏธ์ง"
output_folder = "./์ํฐ๋งํฌ๊ฒฐ๊ณผ"
os.makedirs(output_folder, exist_ok=True)
for filename in os.listdir(input_folder):
if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
add_watermark_and_resize(
os.path.join(input_folder, filename),
os.path.join(output_folder, filename),
target_size=(1000, 800),
watermark_text="ยฉ 2024 MyShop"
)
๐ EXIF ๋ฐ์ดํฐ ์ฒ๋ฆฌ
์ค๋งํธํฐ์ผ๋ก ์ฐ์ ์ฌ์ง์ EXIF ๋ฐ์ดํฐ์ ํ์ ์ ๋ณด๊ฐ ๋ค์ด์์ด์
๊ทธ๋ฅ ๋ฆฌ์ฌ์ด์ฆํ๋ฉด ์ด๋ฏธ์ง๊ฐ ์์ผ๋ก ๋๋ ๊ฒฝ์ฐ๊ฐ ์์ ใ
ใ
์ด๊ฑฐ ์ฒ๋ฆฌํด์ผ ํจ!
from PIL import Image, ExifTags
import os
def fix_rotation_and_resize(input_path, output_path, target_size=(800, 600)):
"""EXIF ํ์ ์ ๋ณด ๋ฐ์ + ๋ฆฌ์ฌ์ด์ฆ"""
with Image.open(input_path) as img:
# EXIF ํ์ ์ ๋ณด ์๋ ์์
try:
# EXIF ํ๊ทธ์์ Orientation ์ฐพ๊ธฐ
exif = img._getexif()
if exif:
for tag_id, value in exif.items():
tag = ExifTags.TAGS.get(tag_id, tag_id)
if tag == 'Orientation':
if value == 3:
img = img.rotate(180, expand=True)
elif value == 6:
img = img.rotate(270, expand=True)
elif value == 8:
img = img.rotate(90, expand=True)
break
except (AttributeError, Exception):
pass # EXIF ์์ผ๋ฉด ๊ทธ๋ฅ ์งํ
# RGB ๋ณํ
if img.mode != 'RGB':
img = img.convert('RGB')
# ๋ฆฌ์ฌ์ด์ฆ
img.thumbnail(target_size, Image.LANCZOS)
# EXIF ์ ๊ฑฐํ๊ณ ์ ์ฅ (๊ฐ์ธ์ ๋ณด ๋ณดํธ + ํ์ผ ํฌ๊ธฐ ๊ฐ์)
img.save(output_path, 'JPEG', quality=85, optimize=True)
print(f"โ
{os.path.basename(input_path)} โ {img.size}")
# ์คํ
input_folder = "./์๋ณธ์ด๋ฏธ์ง"
output_folder = "./์ฒ๋ฆฌ๊ฒฐ๊ณผ"
os.makedirs(output_folder, exist_ok=True)
for filename in os.listdir(input_folder):
if filename.lower().endswith(('.jpg', '.jpeg')):
fix_rotation_and_resize(
os.path.join(input_folder, filename),
os.path.join(output_folder, filename)
)
๐ ํฌ๋งท ์ผ๊ด ๋ณํ (PNG โ WebP)
from PIL import Image
import os
from pathlib import Path
def convert_to_webp_bulk(input_folder, output_folder, quality=80, resize_width=None):
"""๋ชจ๋ ์ด๋ฏธ์ง๋ฅผ WebP๋ก ๋ณํ (์ ํ์ ๋ฆฌ์ฌ์ด์ฆ ํฌํจ)"""
os.makedirs(output_folder, exist_ok=True)
supported = ('.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif')
files = [f for f in os.listdir(input_folder)
if f.lower().endswith(supported)]
print(f"๐ {len(files)}๊ฐ ํ์ผ์ WebP๋ก ๋ณํ ์์...")
total_original_size = 0
total_webp_size = 0
for filename in files:
input_path = os.path.join(input_folder, filename)
# ํ์ฅ์๋ฅผ .webp๋ก ๋ณ๊ฒฝ
output_filename = Path(filename).stem + '.webp'
output_path = os.path.join(output_folder, output_filename)
original_size = os.path.getsize(input_path)
try:
with Image.open(input_path) as img:
if img.mode in ('RGBA', 'LA'):
# ํฌ๋ช
๋ ์๋ ์ด๋ฏธ์ง๋ RGBA ์ ์ง
pass
else:
img = img.convert('RGB')
# ์ ํ์ ๋ฆฌ์ฌ์ด์ฆ
if resize_width:
ratio = resize_width / img.width
new_height = int(img.height * ratio)
img = img.resize((resize_width, new_height), Image.LANCZOS)
img.save(output_path, 'WEBP', quality=quality, method=6)
webp_size = os.path.getsize(output_path)
total_original_size += original_size
total_webp_size += webp_size
reduction = (1 - webp_size/original_size) * 100
print(f"โ
{filename} โ {output_filename}")
print(f" {original_size//1024}KB โ {webp_size//1024}KB ({reduction:.1f}% ๊ฐ์)")
except Exception as e:
print(f"โ {filename}: {e}")
total_reduction = (1 - total_webp_size/total_original_size) * 100 if total_original_size > 0 else 0
print(f"\n๐ ์ ์ฒด ํต๊ณ:")
print(f" ์๋ณธ ํฉ๊ณ: {total_original_size//1024//1024}MB")
print(f" ๋ณํ ํ: {total_webp_size//1024//1024}MB")
print(f" ์ด ์ ์ฝ: {total_reduction:.1f}%")
convert_to_webp_bulk("./์๋ณธ์ด๋ฏธ์ง", "./webp๊ฒฐ๊ณผ", quality=80, resize_width=1200)
๐๏ธ ์ค์ ํ๋ก์ ํธ ํด๋ ๊ตฌ์กฐ ์ค๊ณ
์ฝ๋๋ง ์ ์ง๋ ๊ฒ ์๋๋ผ ํด๋ ๊ตฌ์กฐ๋ ์ ์ค๊ณํด์ผ ๋์ค์ ๊ด๋ฆฌ๊ฐ ํธํจ!
์ค๋ฌด์์ ๋ง์ด ์ฐ๋ ๊ตฌ์กฐ ์๋ ค๋๋ฆผ ๐
image-resize-project/
โ
โโโ ๐ input/ # ์๋ณธ ์ด๋ฏธ์ง ๋ณด๊ด
โ โโโ products/ # ์ํ ์ด๋ฏธ์ง
โ โโโ banners/ # ๋ฐฐ๋ ์ด๋ฏธ์ง
โ โโโ profiles/ # ํ๋กํ ์ด๋ฏธ์ง
โ
โโโ ๐ output/ # ์ฒ๋ฆฌ ๊ฒฐ๊ณผ๋ฌผ
โ โโโ products/
โ โ โโโ thumbnail/ # 150x150
โ โ โโโ small/ # 320x320
โ โ โโโ medium/ # 640x640
โ โ โโโ large/ # 1200x1200
โ โโโ banners/
โ โ โโโ desktop/ # 1920x600
โ โ โโโ tablet/ # 1024x320
โ โ โโโ mobile/ # 640x200
โ โโโ profiles/
โ โโโ small/ # 50x50
โ โโโ large/ # 200x200
โ
โโโ ๐ config/
โ โโโ presets.json # ์ฌ์ด์ฆ ํ๋ฆฌ์
์ค์
โ
โโโ ๐ logs/
โ โโโ resize_log.txt # ์ฒ๋ฆฌ ๋ก๊ทธ
โ
โโโ ๐ resize_main.py # ๋ฉ์ธ ์คํ ํ์ผ
โโโ ๐ utils.py # ์ ํธ๋ฆฌํฐ ํจ์
โโโ ๐ requirements.txt # ์์กด์ฑ ๋ชฉ๋ก
โโโ ๐ README.md # ์ฌ์ฉ๋ฒ ๋ฌธ์
โ๏ธ JSON ์ค์ ํ์ผ๋ก ํ๋ฆฌ์ ๊ด๋ฆฌ
ํ๋์ฝ๋ฉ ๋์ JSON ํ์ผ๋ก ์ค์ ๊ด๋ฆฌํ๋ฉด ์ฝ๋ ์์ ์์ด ํ๋ฆฌ์ ๋ณ๊ฒฝ ๊ฐ๋ฅ!
// config/presets.json
{
"global": {
"quality": 85,
"format": "jpeg",
"strip_exif": true
},
"presets": {
"products": [
{"name": "thumbnail", "width": 150, "height": 150, "fit": "cover"},
{"name": "small", "width": 320, "height": 320, "fit": "cover"},
{"name": "medium", "width": 640, "height": 640, "fit": "cover"},
{"name": "large", "width": 1200, "height": 1200, "fit": "inside"}
],
"banners": [
{"name": "desktop", "width": 1920, "height": 600, "fit": "cover"},
{"name": "tablet", "width": 1024, "height": 320, "fit": "cover"},
{"name": "mobile", "width": 640, "height": 200, "fit": "cover"}
],
"profiles": [
{"name": "small", "width": 50, "height": 50, "fit": "cover"},
{"name": "large", "width": 200, "height": 200, "fit": "cover"}
]
}
}
import json
import os
from PIL import Image
def load_config(config_path="./config/presets.json"):
"""JSON ์ค์ ํ์ผ ๋ก๋"""
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
def process_by_config(input_folder, output_base, preset_name, config):
"""์ค์ ํ์ผ ๊ธฐ๋ฐ ์ฒ๋ฆฌ"""
global_config = config['global']
presets = config['presets'].get(preset_name, [])
if not presets:
print(f"โ ํ๋ฆฌ์
'{preset_name}'์ ์ฐพ์ ์ ์์!")
return
supported = ('.jpg', '.jpeg', '.png', '.webp', '.bmp')
files = [f for f in os.listdir(input_folder)
if f.lower().endswith(supported)]
print(f"๐ '{preset_name}' ํ๋ฆฌ์
์ผ๋ก {len(files)}๊ฐ ํ์ผ ์ฒ๋ฆฌ")
for filename in files:
input_path = os.path.join(input_folder, filename)
with Image.open(input_path) as img:
if img.mode != 'RGB':
img = img.convert('RGB')
for preset in presets:
output_dir = os.path.join(output_base, preset_name, preset['name'])
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, filename)
img_copy = img.copy()
fit = preset.get('fit', 'inside')
if fit == 'cover':
# cover: ๋น์จ ์ ์งํ๋ฉฐ ์์ญ ๊ฝ ์ฑ์ฐ๊ธฐ
img_copy = img_copy.resize(
(preset['width'], preset['height']),
Image.LANCZOS
)
else:
# inside: ๋น์จ ์ ์งํ๋ฉฐ ์์ชฝ์ ๋ง์ถค
img_copy.thumbnail(
(preset['width'], preset['height']),
Image.LANCZOS
)
quality = preset.get('quality', global_config['quality'])
img_copy.save(output_path, 'JPEG', quality=quality, optimize=True)
print(f" โ
{filename} ์ฒ๋ฆฌ ์๋ฃ")
# ์คํ
config = load_config()
process_by_config("./input/products", "./output", "products", config)
process_by_config("./input/banners", "./output", "banners", config)
๐ก๏ธ ์๋ฌ ํธ๋ค๋ง & ๋ก๊น - ์ค๋ฌด์์ ํ์!
์ฝ๋ ์งค ๋ ์๋ฌ ์ฒ๋ฆฌ ์ ํ๋ฉด ๋์ค์ ์ง์ง ๊ณ ์ํจ ใ
ใ
ํนํ ๋๋ ์ฒ๋ฆฌํ ๋ ์ค๊ฐ์ ํ๋ ์ค๋ฅ๋๋ฉด ์ ์ฒด๊ฐ ๋ฉ์ถ๋ ์ํฉ ๋ฐฉ์งํด์ผ ํด์!
import os
import logging
import json
from PIL import Image
from datetime import datetime
from pathlib import Path
# ===== ๋ก๊น
์ค์ =====
def setup_logger(log_dir="./logs"):
"""๋ก๊ฑฐ ์ค์ """
os.makedirs(log_dir, exist_ok=True)
log_filename = f"resize_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
log_path = os.path.join(log_dir, log_filename)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(log_path, encoding='utf-8'),
logging.StreamHandler() # ์ฝ์์๋ ์ถ๋ ฅ
]
)
return logging.getLogger(__name__)
logger = setup_logger()
# ===== ์์ ํ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ํจ์ =====
def safe_resize(input_path, output_path, target_size, quality=85):
"""์๋ฌ ํธ๋ค๋ง์ด ํฌํจ๋ ์์ ํ ๋ฆฌ์ฌ์ด์ฆ"""
# ํ์ผ ์กด์ฌ ํ์ธ
if not os.path.exists(input_path):
logger.error(f"ํ์ผ ์์: {input_path}")
return False
# ํ์ผ ํฌ๊ธฐ ํ์ธ (0๋ฐ์ดํธ ํ์ผ ๋ฐฉ์ง)
if os.path.getsize(input_path) == 0:
logger.error(f"๋น ํ์ผ: {input_path}")
return False
try:
with Image.open(input_path) as img:
# ์ด๋ฏธ์ง ์ ํจ์ฑ ๊ฒ์ฆ
img.verify()
# verify() ํ ๋ค์ ์ด์ด์ผ ํจ (verify๋ ํ์ผ ํฌ์ธํฐ๋ฅผ ์๋ชจํจ)
with Image.open(input_path) as img:
# ์ต๋ ์ด๋ฏธ์ง ํฌ๊ธฐ ์ ํ (๋ฉ๋ชจ๋ฆฌ ๋ณดํธ)
MAX_PIXELS = 50_000_000 # 5์ฒ๋ง ํฝ์
if img.width * img.height > MAX_PIXELS:
logger.warning(f"์ด๋ฏธ์ง๊ฐ ๋๋ฌด ํผ: {img.size} - {input_path}")
# ๊ทธ๋๋ ์ฒ๋ฆฌ๋ ๊ณ์
if img.mode not in ('RGB', 'L'):
img = img.convert('RGB')
img.thumbnail(target_size, Image.LANCZOS)
# ์ถ๋ ฅ ํด๋ ์๋ ์์ฑ
os.makedirs(os.path.dirname(output_path), exist_ok=True)
ext = Path(output_path).suffix.lower()
if ext in ('.jpg', '.jpeg'):
img.save(output_path, 'JPEG', quality=quality, optimize=True)
elif ext == '.png':
img.save(output_path, 'PNG', optimize=True)
elif ext == '.webp':
img.save(output_path, 'WEBP', quality=quality)
else:
img.save(output_path)
logger.info(f"์ฑ๊ณต: {Path(input_path).name} โ {img.size}")
return True
except Image.UnidentifiedImageError:
logger.error(f"์ด๋ฏธ์ง ํ์ผ์ด ์๋: {input_path}")
except PermissionError:
logger.error(f"๊ถํ ์์: {input_path}")
except OSError as e:
logger.error(f"OS ์ค๋ฅ ({input_path}): {e}")
except Exception as e:
logger.error(f"์ ์ ์๋ ์ค๋ฅ ({input_path}): {type(e).__name__}: {e}")
return False
# ===== ์ฒ๋ฆฌ ๊ฒฐ๊ณผ ๋ฆฌํฌํธ ์์ฑ =====
def generate_report(results, output_path="./logs/report.json"):
"""์ฒ๋ฆฌ ๊ฒฐ๊ณผ JSON ๋ฆฌํฌํธ ์์ฑ"""
success_list = [r for r in results if r['success']]
fail_list = [r for r in results if not r['success']]
report = {
"generated_at": datetime.now().isoformat(),
"summary": {
"total": len(results),
"success": len(success_list),
"failed": len(fail_list),
"success_rate": f"{len(success_list)/len(results)*100:.1f}%" if results else "0%"
},
"failed_files": [r['filename'] for r in fail_list]
}
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
logger.info(f"๋ฆฌํฌํธ ์์ฑ: {output_path}")
logger.info(f"์ด {len(results)}๊ฐ | ์ฑ๊ณต {len(success_list)}๊ฐ | ์คํจ {len(fail_list)}๊ฐ")
return report
# ===== ๋ฉ์ธ ์คํ =====
def main():
INPUT_DIR = "./์๋ณธ์ด๋ฏธ์ง"
OUTPUT_DIR = "./๋ฆฌ์ฌ์ด์ฆ๊ฒฐ๊ณผ"
TARGET_SIZE = (800, 600)
SUPPORTED = ('.jpg', '.jpeg', '.png', '.bmp', '.webp', '.tiff')
files = [f for f in os.listdir(INPUT_DIR)
if f.lower().endswith(SUPPORTED)]
logger.info(f"์ฒ๋ฆฌ ์์: {len(files)}๊ฐ ํ์ผ")
results = []
for filename in files:
input_path = os.path.join(INPUT_DIR, filename)
output_path = os.path.join(OUTPUT_DIR, filename)
success = safe_resize(input_path, output_path, TARGET_SIZE)
results.append({'filename': filename, 'success': success})
generate_report(results)
main()
๐ ์ฑ๋ฅ ์ต์ ํ ํ ๋ชจ์
๋งค๋ฒ ์ ์ฒด๋ฅผ ๋ค์ ์ฒ๋ฆฌํ๋ฉด ๋นํจ์จ์ ์. ์ด๋ฏธ ๊ฒฐ๊ณผ๋ฌผ์ด ์์ผ๋ฉด ๊ฑด๋๋ฐ๋ ๋ก์ง ์ถ๊ฐ!
# ์ด๋ฏธ ์ฒ๋ฆฌ๋ ํ์ผ ์คํต
if os.path.exists(output_path):
# ์๋ณธ๋ณด๋ค ๊ฒฐ๊ณผ๋ฌผ์ด ๋ ์ต์ ์ด๋ฉด ์คํต
if os.path.getmtime(output_path) > os.path.getmtime(input_path):
print(f"โญ๏ธ ์คํต (์ด๋ฏธ ์ฒ๋ฆฌ๋จ): {filename}")
continue
๋๋ ์ฒ๋ฆฌํ ๋ ์งํ๋ฅ ํ์ ์์ผ๋ฉด ๋ฉ์ถ ๊ฑด์ง ๋์๊ฐ๋ ๊ฑด์ง ๋ชจ๋ฆ ใ ใ
# pip install tqdm
from tqdm import tqdm
files = [...] # ์ฒ๋ฆฌํ ํ์ผ ๋ชฉ๋ก
for filename in tqdm(files, desc="๐ผ๏ธ ๋ฆฌ์ฌ์ด์ฆ ์ค", unit="์ฅ"):
# ์ฒ๋ฆฌ ์ฝ๋
process_image(filename)
์ด๋ฏธ์ง๋ฅผ ํ๊บผ๋ฒ์ ๋ฉ๋ชจ๋ฆฌ์ ์ฌ๋ฆฌ์ง ๋ง๊ณ , ์ฒ๋ฆฌ ํ ์ฆ์ ํด์ !
# ์ข์ ๋ฐฉ๋ฒ: with ๋ฌธ ์ฌ์ฉ (์๋ ๋ฉ๋ชจ๋ฆฌ ํด์ )
with Image.open(input_path) as img:
img.thumbnail(target_size, Image.LANCZOS)
img.save(output_path)
# ์ฌ๊ธฐ์ ์๋์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ํด์ ๋จ
# ๋์ ๋ฐฉ๋ฒ: ๋ช
์์ close ์์ด ๋ณ์์ ๊ณ์ ๋ณด๊ด
img = Image.open(input_path) # ๋ฉ๋ชจ๋ฆฌ์ ๊ณ์ ๋จ์์์
# ... ์ฒ๋ฆฌ ...
img.save(output_path)
# img.close() ์ ํ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฐ๋ฅ!
ThreadPoolExecutor๋ GIL ๋๋ฌธ์ CPU ์ง์ฝ์ ์์
์ ํ๊ณ๊ฐ ์์.
์ด๋ฏธ์ง ์ฒ๋ฆฌ๋ ProcessPoolExecutor๊ฐ ๋ ํจ๊ณผ์ ์ผ ์ ์์ด์!
from concurrent.futures import ProcessPoolExecutor
import multiprocessing
def process_image_worker(args):
"""๋ณ๋ ํ๋ก์ธ์ค์์ ์คํ๋ ํจ์"""
input_path, output_path, target_size = args
try:
with Image.open(input_path) as img:
if img.mode != 'RGB':
img = img.convert('RGB')
img.thumbnail(target_size, Image.LANCZOS)
img.save(output_path, 'JPEG', quality=85)
return True, input_path
except Exception as e:
return False, f"{input_path}: {e}"
# CPU ์ฝ์ด ์ ์๋ ๊ฐ์ง
cpu_count = multiprocessing.cpu_count()
print(f"๐ฅ๏ธ CPU ์ฝ์ด ์: {cpu_count}๊ฐ ํ์ฉ")
tasks = [(inp, out, (800, 600)) for inp, out in file_pairs]
with ProcessPoolExecutor(max_workers=cpu_count) as executor:
results = list(executor.map(process_image_worker, tasks))
success = sum(1 for ok, _ in results if ok)
print(f"โ
์๋ฃ: {success}/{len(tasks)}")
โข ProcessPoolExecutor: CPU ๋ฐ์ด๋ ์์ (์ด๋ฏธ์ง ๋ณํ ์ฐ์ฐ์ด ์ฃผ๋ ๋ณ๋ชฉ์ผ ๋)
์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ๋ ๋ณดํต CPU ๋ฐ์ด๋๋ผ์ ProcessPoolExecutor๊ฐ ๋ ํจ๊ณผ์ ์ด์ง๋ง,
SSD ํ๊ฒฝ์์๋ I/O๋ ๋น ๋ฅด๋๊น ๋ ๋ค ํ ์คํธํด๋ณด๊ณ ๋ ๋น ๋ฅธ ๊ฑธ ์ฐ๋ฉด ๋จ!
๐ผ ์ค์ ํ์ฉ ์๋๋ฆฌ์ค & ์์ฑ ์ฝ๋
๐ ์๋๋ฆฌ์ค 1: ์ผํ๋ชฐ ์ํ ์ด๋ฏธ์ง ์ฒ๋ฆฌ
์ผํ๋ชฐ์์ ๊ฐ์ฅ ๋ง์ด ์ฐ๋ ํจํด! ์ํ ์ด๋ฏธ์ง๋ฅผ ์ฌ๋ฌ ์ฌ์ด์ฆ๋ก + WebP ๋ณํ๊น์ง!
"""
์ผํ๋ชฐ ์ํ ์ด๋ฏธ์ง ์๋ ์ฒ๋ฆฌ ์คํฌ๋ฆฝํธ
- ์๋ณธ ๋ณด์กด
- ์ธ๋ค์ผ, ๋ชฉ๋ก, ์์ธ ์ฌ์ด์ฆ ์์ฑ
- JPEG + WebP ๋์ ์์ฑ
- EXIF ํ์ ์๋ ์์
"""
from PIL import Image, ExifTags
import os
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import logging
logging.basicConfig(level=logging.INFO, format='%(message)s')
logger = logging.getLogger(__name__)
PRODUCT_SIZES = {
'thumb': (150, 150, 'cover'), # ๋ชฉ๋ก ์ธ๋ค์ผ
'list': (400, 400, 'cover'), # ๋ชฉ๋ก ์ด๋ฏธ์ง
'detail': (800, 800, 'inside'), # ์์ธ ์ด๋ฏธ์ง
'zoom': (1500, 1500, 'inside') # ํ๋ ์ด๋ฏธ์ง
}
def fix_exif_rotation(img):
"""EXIF ํ์ ์ ๋ณด ์๋ ์์ """
try:
exif = img._getexif()
if exif:
for tag_id, value in exif.items():
if ExifTags.TAGS.get(tag_id) == 'Orientation':
rotations = {3: 180, 6: 270, 8: 90}
if value in rotations:
img = img.rotate(rotations[value], expand=True)
break
except:
pass
return img
def resize_cover(img, width, height):
"""cover ๋ฐฉ์: ๋น์จ ์ ์งํ๋ฉฐ ์์ญ ๊ฝ ์ฑ์ฐ๊ธฐ"""
img_ratio = img.width / img.height
target_ratio = width / height
if img_ratio > target_ratio:
new_height = height
new_width = int(height * img_ratio)
else:
new_width = width
new_height = int(width / img_ratio)
img = img.resize((new_width, new_height), Image.LANCZOS)
# ์ค์ ํฌ๋กญ
left = (new_width - width) // 2
top = (new_height - height) // 2
img = img.crop((left, top, left + width, top + height))
return img
def process_product_image(input_path, output_base):
"""์ํ ์ด๋ฏธ์ง ์ ์ฒด ์ฒ๋ฆฌ"""
filename = Path(input_path).stem
try:
with Image.open(input_path) as img:
img = fix_exif_rotation(img)
if img.mode != 'RGB':
img = img.convert('RGB')
for size_name, (w, h, fit) in PRODUCT_SIZES.items():
size_dir = os.path.join(output_base, size_name)
os.makedirs(size_dir, exist_ok=True)
img_copy = img.copy()
if fit == 'cover':
img_copy = resize_cover(img_copy, w, h)
else:
img_copy.thumbnail((w, h), Image.LANCZOS)
# JPEG ์ ์ฅ
jpeg_path = os.path.join(size_dir, f"{filename}.jpg")
img_copy.save(jpeg_path, 'JPEG', quality=85, optimize=True)
# WebP ์ ์ฅ (์ถ๊ฐ)
webp_path = os.path.join(size_dir, f"{filename}.webp")
img_copy.save(webp_path, 'WEBP', quality=80)
logger.info(f"โ
{filename} - {len(PRODUCT_SIZES)}๊ฐ ์ฌ์ด์ฆ ร 2ํฌ๋งท ์๋ฃ")
return True
except Exception as e:
logger.error(f"โ {filename}: {e}")
return False
def process_all_products(input_dir, output_dir, max_workers=8):
"""์ ์ฒด ์ํ ์ด๋ฏธ์ง ์ฒ๋ฆฌ"""
supported = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
files = [os.path.join(input_dir, f)
for f in os.listdir(input_dir)
if f.lower().endswith(supported)]
logger.info(f"๐ ์ํ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ์์: {len(files)}๊ฐ")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(
lambda f: process_product_image(f, output_dir),
files
))
success = sum(results)
logger.info(f"\n๐ ์๋ฃ! {success}/{len(files)}๊ฐ ์ฑ๊ณต")
# ์คํ!
process_all_products("./์ํ์ด๋ฏธ์ง_์๋ณธ", "./์ํ์ด๋ฏธ์ง_์ฒ๋ฆฌ๊ฒฐ๊ณผ")
๐ฑ ์๋๋ฆฌ์ค 2: ์ฑ ์์ด์ฝ ์๋ ์์ฑ
iOS/Android ์ฑ ๊ฐ๋ฐํ ๋ ์์ด์ฝ ์ฌ์ด์ฆ๊ฐ ์ง์ง ๋ง์ ใ
ใ
์ด๊ฒ๋ ์๋ํํ๋ฉด ํธํจ!
from PIL import Image
import os
# iOS ์์ด์ฝ ์ฌ์ด์ฆ ๋ชฉ๋ก
IOS_ICON_SIZES = [
(20, 1), (20, 2), (20, 3),
(29, 1), (29, 2), (29, 3),
(40, 1), (40, 2), (40, 3),
(60, 2), (60, 3),
(76, 1), (76, 2),
(83.5, 2),
(1024, 1) # App Store
]
# Android ์์ด์ฝ ์ฌ์ด์ฆ
ANDROID_ICON_SIZES = {
'mipmap-mdpi': 48,
'mipmap-hdpi': 72,
'mipmap-xhdpi': 96,
'mipmap-xxhdpi': 144,
'mipmap-xxxhdpi': 192,
'playstore': 512
}
def generate_ios_icons(source_image, output_dir):
"""iOS ์์ด์ฝ ์ธํธ ์์ฑ"""
os.makedirs(output_dir, exist_ok=True)
with Image.open(source_image) as img:
if img.mode != 'RGBA':
img = img.convert('RGBA')
for base_size, scale in IOS_ICON_SIZES:
pixel_size = int(base_size * scale)
icon = img.resize((pixel_size, pixel_size), Image.LANCZOS)
filename = f"icon_{int(base_size)}x{int(base_size)}@{scale}x.png"
icon.save(os.path.join(output_dir, filename), 'PNG')
print(f"โ
iOS: {filename} ({pixel_size}x{pixel_size}px)")
def generate_android_icons(source_image, output_dir):
"""Android ์์ด์ฝ ์ธํธ ์์ฑ"""
with Image.open(source_image) as img:
if img.mode != 'RGBA':
img = img.convert('RGBA')
for density, size in ANDROID_ICON_SIZES.items():
density_dir = os.path.join(output_dir, density)
os.makedirs(density_dir, exist_ok=True)
icon = img.resize((size, size), Image.LANCZOS)
icon.save(os.path.join(density_dir, 'ic_launcher.png'), 'PNG')
print(f"โ
Android {density}: {size}x{size}px")
# ์คํ (1024x1024 ์ด์์ ์๋ณธ ์ด๋ฏธ์ง ํ์)
generate_ios_icons("./app_icon_์๋ณธ.png", "./ios_icons")
generate_android_icons("./app_icon_์๋ณธ.png", "./android_icons")
print("\n๐ ์ฑ ์์ด์ฝ ์ธํธ ์์ฑ ์๋ฃ!")
๐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ํ ๊ฐ์ด๋
์ด๋ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์จ์ผ ํ ์ง ํท๊ฐ๋ฆด ๋ ์ฐธ๊ณ ํ์ธ์!
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ | ์ธ์ด | ์๋ | ๋์ด๋ | ํน์ง |
|---|---|---|---|---|
| Pillow | Python | โญโญโญ | ์ฌ์ ๐ | ๊ฐ์ฅ ๋ฒ์ฉ์ , ๋ฌธ์ ํ๋ถ |
| Sharp | Node.js | โญโญโญโญโญ | ๋ณดํต ๐ | ์ด๊ณ ์, WebP/AVIF ๊ฐ์ |
| ImageMagick | CLI/Bash | โญโญโญโญ | ๋ณดํต ๐ | ์๋ฒ ํ๊ฒฝ, ์คํฌ๋ฆฝํธ ์ฐ๋ |
| OpenCV | Python/C++ | โญโญโญโญโญ | ์ด๋ ค์ ๐ | ์ปดํจํฐ ๋น์ , ๊ณ ๊ธ ์ฒ๋ฆฌ |
| Jimp | Node.js | โญโญโญ | ์ฌ์ ๐ | ์์ JS, ์์กด์ฑ ์์ |
| GD Library | PHP | โญโญโญ | ์ฌ์ ๐ | PHP ์น์๋ฒ ํ๊ฒฝ |
โก Node.js ์น์๋ฒ ์ค์๊ฐ ์ฒ๋ฆฌ โ Sharp
๐ฅ๏ธ Linux ์๋ฒ cron ์๋ํ โ ImageMagick + Bash
๐ฌ AI/ML ํ์ดํ๋ผ์ธ ์ฐ๋ โ OpenCV
๐ PHP ๊ธฐ๋ฐ CMS โ GD Library ๋๋ Imagick
์ฐธ๊ณ ๋ก ์ฌ๋ฅ๋ท์์ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๊ด๋ จ ๊ฐ๋ฐ ์๋ขฐ๋ฅผ ์ฐพ์๋ณด๋ฉด Pillow๋ Sharp ๊ธฐ๋ฐ ์๋ํ ์คํฌ๋ฆฝํธ ์์ฒญ์ด ๊ฝค ๋ง์์.
์ด ๊ธ์์ ๋ฐฐ์ด ๋ด์ฉ์ผ๋ก ์ถฉ๋ถํ ๋์ ๊ฐ๋ฅํ๋๊น ํฌํธํด๋ฆฌ์ค๋ก ํ์ฉํด๋ด์! ๐
โ ์์ฃผ ๋ฌป๋ ์ง๋ฌธ (FAQ)
withoutEnlargement: true ์ต์
์ด๋ thumbnail() ๋ฉ์๋๋ฅผ ์ฐ๋ฉด ์๋ณธ๋ณด๋ค ํฌ๊ฒ ํ๋๋๋ ๊ฑธ ๋ฐฉ์งํ ์ ์์ด์!
A. JPEG๋ ํฌ๋ช ๋(์ํ ์ฑ๋)๋ฅผ ์ง์ ์ ํด์. ๋ณํ ์ ์ ๋ฐฐ๊ฒฝ์์ ์ฑ์์ค์ผ ํจ!
from PIL import Image
img = Image.open("ํฌ๋ช
๋ฐฐ๊ฒฝ.png")
if img.mode in ('RGBA', 'LA', 'P'):
background = Image.new('RGB', img.size, (255, 255, 255)) # ํฐ ๋ฐฐ๊ฒฝ
if img.mode == 'P':
img = img.convert('RGBA')
background.paste(img, mask=img.split()[-1]) # ์ํ ์ฑ๋์ ๋ง์คํฌ๋ก
img = background
img.save("๊ฒฐ๊ณผ.jpg", 'JPEG', quality=85)
โ ๋ฉํฐ์ค๋ ๋ฉ/๋ฉํฐํ๋ก์ธ์ฑ ์ ์ฉ
โก Sharp(Node.js) ์ฌ์ฉ ๊ณ ๋ ค (Pillow๋ณด๋ค ํจ์ฌ ๋น ๋ฆ)
โข ์ด๋ฏธ ์ฒ๋ฆฌ๋ ํ์ผ ์คํต ๋ก์ง ์ถ๊ฐ
โฃ SSD ์ฌ์ฉ (HDD๋ณด๋ค I/O ์๋ ํจ์ฌ ๋น ๋ฆ)
โค ๋ฐฐ์น ํฌ๊ธฐ ์กฐ์ (ํ ๋ฒ์ ๋๋ฌด ๋ง์ด ์ฒ๋ฆฌํ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ)
mogrify์ฒ๋ผ ์๋ณธ์ ์ง์ ์์ ํ๋ ๋ช
๋ น์ด๋ ํนํ ์ฃผ์.์ฝ๋์์
input_path์ output_path๋ฅผ ๋ค๋ฅด๊ฒ ์ค์ ํ๊ณ ,์ฒ์ ์คํ ์ ์ ์๋ณธ ํด๋ ๋ฐฑ์ ํ๋ ์ต๊ด ๋ค์ด๋ ๊ฒ ์ข์์ ใ ใ
A. ๊ฐ๋ฅํ์ง๋ง ์ข ๋ณต์กํด์. ๊ฐ ํ๋ ์์ ๊ฐ๋ณ์ ์ผ๋ก ์ฒ๋ฆฌํด์ผ ํจ!
from PIL import Image
def resize_gif(input_path, output_path, target_size):
img = Image.open(input_path)
frames = []
try:
while True:
frame = img.copy()
frame.thumbnail(target_size, Image.LANCZOS)
frames.append(frame)
img.seek(img.tell() + 1)
except EOFError:
pass
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
loop=0,
optimize=True
)
๐ฏ ๋ง๋ฌด๋ฆฌ & ํต์ฌ ์์ฝ
์ค๋ ๋๋ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ ์๋ํ์ ๋ํด ์ง์ง ๋ง์ ๋ด์ฉ์ ๋ค๋ค๋๋ฐ์ ใ
ใ
ํต์ฌ๋ง ์ ๋ฆฌํ๋ฉด ์ด๋ ๊ฒ ๋จ!
2๏ธโฃ Node.js Sharp: ์๋๊ฐ ์ต์ฐ์ ์ด๋ฉด ์ด๊ฑฐ, ํนํ ์น์๋ฒ ํ๊ฒฝ์์ ๊ฐ๋ ฅ
3๏ธโฃ ImageMagick + Bash: ์๋ฒ ํ๊ฒฝ ์๋ํ, cron ์ฐ๋ ์ ์ต๊ณ
4๏ธโฃ ๋ฉํฐ์ค๋ ๋ฉ/ํ๋ก์ธ์ฑ: ๋๋ ์ฒ๋ฆฌ ์ ํ์ ์ต์ ํ
5๏ธโฃ ์๋ฌ ํธ๋ค๋ง + ๋ก๊น : ์ค๋ฌด์์ ์ ๋ ๋น ์ง๋ฉด ์ ๋จ
6๏ธโฃ WebP ๋ณํ: ์น ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด ํจ๊ป ๊ณ ๋ คํ๋ฉด ์ข์
7๏ธโฃ ์๋ณธ ๋ฐฑ์ : ํญ์! ๋ฌด์กฐ๊ฑด! ๋ฐฑ์ ํ๊ณ ์์ํ ๊ฒ ใ ใ
์ด ์ฝ๋๋ค ์ ํ์ฉํด์ ๋ฐ๋ณต ์์
์์ ํด๋ฐฉ๋๊ธธ ๋ฐ๋! ๐
์ฝ๋ ํ ๋ฒ ์ง๋์ผ๋ฉด ๊ทธ ๋ค์๋ถํฐ๋ ์ง์ง ํธํด์.
์ฒ์ ์ธํ
ํ๋ ๋ฐ 1์๊ฐ ํฌ์ํ๋ฉด ์์ผ๋ก ์๋ฐฑ ์๊ฐ์ ์๋ ์ ์์ผ๋๊น์ ๐
โข Pillow ๊ณต์ ๋ฌธ์:
https://pillow.readthedocs.ioโข Sharp ๊ณต์ ๋ฌธ์:
https://sharp.pixelplumbing.comโข ImageMagick ๊ณต์:
https://imagemagick.org/script/command-line-processing.php
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.