Files
arcs/res/arcs-mini_doll_v2/emoji_merge.py
2026-08-13 16:50:52 +08:00

130 lines
4.9 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
from typing import Iterable, List, Tuple
from PIL import Image
def _find_frame_files(dir_path: Path) -> List[Path]:
if not dir_path.is_dir():
return []
return sorted(p for p in dir_path.iterdir() if p.is_file() and p.name.startswith("frame-") and p.suffix.lower() == ".png")
def _pair_frames(left_dir: Path, right_dir: Path) -> List[Tuple[Path, Path]]:
left_frames = {p.name: p for p in _find_frame_files(left_dir)}
right_frames = {p.name: p for p in _find_frame_files(right_dir)}
pairs = []
for name in sorted(set(left_frames) | set(right_frames)):
if name in left_frames and name in right_frames:
pairs.append((left_frames[name], right_frames[name]))
else:
missing = "left" if name not in left_frames else "right"
print(f"[WARN] missing {missing} frame: {name} in {left_dir.parent.name}")
return pairs
def _merge_pair(left_path: Path, right_path: Path, out_path: Path, distance: int, offset: int) -> None:
with Image.open(left_path) as left_img, Image.open(right_path) as right_img:
# Swap left/right placement as requested.
left = right_img.convert("RGBA")
right = left_img.convert("RGBA")
extra_top = max(offset, 0)
extra_bottom = max(-offset, 0)
y_offset = extra_top
half = distance / 2.0
left_x = int(round(-half))
right_x = int(round(left.width + half))
if distance > 0:
min_x = min(left_x, right_x)
max_x = max(left_x + left.width, right_x + right.width)
out_width = max_x - min_x
out_height = max(left.height, right.height) + extra_top + extra_bottom
merged = Image.new("RGBA", (out_width, out_height), (0, 0, 0, 0))
offset = -min_x
merged.paste(left, (left_x + offset, y_offset))
merged.paste(right, (right_x + offset, y_offset))
else:
out_width = left.width + right.width
out_height = max(left.height, right.height) + extra_top + extra_bottom
merged = Image.new("RGBA", (out_width, out_height), (0, 0, 0, 0))
if distance < 0:
midline = left.width
left_max_x = midline - left_x
right_min_x = midline - right_x
if left_max_x > 0:
left_crop = left.crop((0, 0, min(left.width, left_max_x), left.height))
merged.paste(left_crop, (left_x, y_offset))
if right_min_x < right.width:
right_crop = right.crop((max(0, right_min_x), 0, right.width, right.height))
merged.paste(right_crop, (right_x + max(0, right_min_x), y_offset))
else:
merged.paste(left, (left_x, y_offset))
merged.paste(right, (right_x, y_offset))
out_path.parent.mkdir(parents=True, exist_ok=True)
merged.save(out_path, format="PNG")
def merge_all(input_root: Path, output_root: Path, distance: int, offset: int) -> None:
if not input_root.is_dir():
raise FileNotFoundError(f"input root not found: {input_root}")
emoji_ids = [p for p in input_root.iterdir() if p.is_dir()]
for emoji_dir in sorted(emoji_ids, key=lambda p: p.name):
left_dir = emoji_dir / "left"
right_dir = emoji_dir / "right"
if not left_dir.is_dir() or not right_dir.is_dir():
print(f"[WARN] skip {emoji_dir.name}: missing left/right dirs")
continue
for left_path, right_path in _pair_frames(left_dir, right_dir):
out_path = output_root / emoji_dir.name / left_path.name
_merge_pair(left_path, right_path, out_path, distance, offset)
print(f"[OK] {emoji_dir.name}/{left_path.name}")
def _parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Merge left/right emoji frames into single frames.")
parser.add_argument(
"--input",
default="res/emoji_src",
help="Input root directory, default: res/emoji_src",
)
parser.add_argument(
"--output",
default="res/emoji",
help="Output root directory, default: res/emoji",
)
parser.add_argument(
"--distance",
type=int,
default=0,
help="Horizontal distance between left/right. Positive moves outward (can overflow); negative moves inward and clips at midline.",
)
parser.add_argument(
"--offset",
type=int,
default=0,
help="Vertical offset padding. Positive adds blank above; negative adds blank below.",
)
return parser.parse_args(list(argv) if argv is not None else None)
def main() -> None:
args = _parse_args()
merge_all(Path(args.input), Path(args.output), int(args.distance), int(args.offset))
if __name__ == "__main__":
main()