Files
xianyan/tools/remove_icon_padding.py
Developer 91f1d80a9f 资源
2026-06-04 00:29:44 +08:00

161 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""
Created: 2026-06-02
Updated: 2026-06-02
Name: remove_icon_padding.py
Desc: Remove transparent padding from icon images, fill content to full size,
and apply iOS-style rounded corners (superellipse mask).
Original files are backed up to a 'backup' subfolder.
"""
import os
import shutil
import numpy as np
from PIL import Image, ImageDraw
INPUT_DIR = '/Users/wushu/Documents/trae_projects/project/xianyan/assets/templates/resized'
BACKUP_DIR = os.path.join(INPUT_DIR, 'backup_original')
ALPHA_THRESHOLD = 10
CORNER_RATIO = 0.2237
def find_content_bounds(arr):
"""Find bounding box of non-transparent content."""
alpha = arr[:, :, 3]
rows = np.any(alpha > ALPHA_THRESHOLD, axis=1)
cols = np.any(alpha > ALPHA_THRESHOLD, axis=0)
if not rows.any():
return None
rmin, rmax = np.where(rows)[0][[0, -1]]
cmin, cmax = np.where(cols)[0][[0, -1]]
return (cmin, rmin, cmax + 1, rmax + 1)
def create_superellipse_mask(size, n=5):
"""
Create iOS-style superellipse (squircle) mask.
The equation: |x|^n + |y|^n = 1 with n=5 approximates Apple's icon shape.
"""
w, h = size
mask = Image.new('L', (w, h), 0)
draw = ImageDraw.Draw(mask)
cx, cy = w / 2.0, h / 2.0
a = w / 2.0
b = h / 2.0
points = []
num_points = 360
for i in range(num_points + 1):
t = 2.0 * np.pi * i / num_points
cos_t = np.cos(t)
sin_t = np.sin(t)
x = np.sign(cos_t) * abs(cos_t) ** (2.0 / n) * a
y = np.sign(sin_t) * abs(sin_t) ** (2.0 / n) * b
points.append((cx + x, cy + y))
draw.polygon(points, fill=255)
return mask
def create_rounded_rect_mask(size, radius):
"""Create a simple rounded rectangle mask as fallback."""
w, h = size
mask = Image.new('L', (w, h), 0)
draw = ImageDraw.Draw(mask)
draw.rounded_rectangle([(0, 0), (w - 1, h - 1)], radius=radius, fill=255)
return mask
def process_icon(filepath, backup_dir):
"""Process a single icon: backup, crop padding, resize, apply rounded mask."""
filename = os.path.basename(filepath)
img = Image.open(filepath)
original_size = img.size
arr = np.array(img)
if img.mode != 'RGBA':
print(f" SKIP {filename}: not RGBA mode ({img.mode})")
return False
bounds = find_content_bounds(arr)
if bounds is None:
print(f" SKIP {filename}: no visible content found")
return False
cmin, rmin, cmax, rmax = bounds
h, w = arr.shape[:2]
pad_top = rmin
pad_bottom = h - rmax
pad_left = cmin
pad_right = w - cmax
if pad_top < 2 and pad_bottom < 2 and pad_left < 2 and pad_right < 2:
print(f" SKIP {filename}: no significant padding detected")
return False
print(f" Padding: top={pad_top} bottom={pad_bottom} left={pad_left} right={pad_right}")
# Backup original
backup_path = os.path.join(backup_dir, filename)
if not os.path.exists(backup_path):
shutil.copy2(filepath, backup_path)
print(f" Backed up to {backup_path}")
# Crop to content
cropped = img.crop(bounds)
# Resize back to original dimensions (high quality)
resized = cropped.resize(original_size, Image.LANCZOS)
# Apply iOS superellipse mask for rounded corners
mask = create_superellipse_mask(original_size, n=5)
# Composite: apply mask as alpha channel
result = resized.copy()
result.putalpha(mask)
# Save result
result.save(filepath, 'PNG')
print(f" DONE: {filename} -> cropped, resized to {original_size}, superellipse mask applied")
return True
def main():
os.makedirs(BACKUP_DIR, exist_ok=True)
print(f"Input dir: {INPUT_DIR}")
print(f"Backup dir: {BACKUP_DIR}")
print()
files = sorted([
f for f in os.listdir(INPUT_DIR)
if f.lower().endswith('.png')
])
processed = 0
skipped = 0
for f in files:
filepath = os.path.join(INPUT_DIR, f)
if not os.path.isfile(filepath):
continue
print(f"[{processed + skipped + 1}/{len(files)}] {f}")
if process_icon(filepath, BACKUP_DIR):
processed += 1
else:
skipped += 1
print(f"\n{'='*50}")
print(f"Total: {len(files)} files")
print(f"Processed: {processed}")
print(f"Skipped: {skipped}")
print(f"Backup location: {BACKUP_DIR}")
if __name__ == '__main__':
main()