mirror of
https://github.com/creeper2eletricboogaloo/L4JDownloadableContent.git
synced 2026-09-14 19:34:43 +02:00
282 lines
12 KiB
Python
282 lines
12 KiB
Python
import argparse
|
|
import io
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from zipfile import ZIP_DEFLATED, ZipFile
|
|
|
|
from PIL import Image
|
|
|
|
|
|
LOGO_WIDTH = 335
|
|
LOGO_TOP = 14
|
|
CANVAS_SIZE = (360, 360)
|
|
SKINPACK_MATCH_Y = 125
|
|
SKINPACK_MATCH_THRESHOLD = 1.0
|
|
SKIP_SKINPACKS = {
|
|
"fromtheshadows_preview.png",
|
|
"doctor_who_volume_1_preview.png",
|
|
"doctor_who_volume_2_preview.png",
|
|
}
|
|
|
|
|
|
def repo_root() -> Path:
|
|
return Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def cover_image(image: Image.Image, size: tuple[int, int] = CANVAS_SIZE) -> Image.Image:
|
|
image = image.convert("RGBA")
|
|
scale = max(size[0] / image.width, size[1] / image.height)
|
|
resized = image.resize((round(image.width * scale), round(image.height * scale)), Image.Resampling.LANCZOS)
|
|
left = (resized.width - size[0]) // 2
|
|
top = (resized.height - size[1]) // 2
|
|
return resized.crop((left, top, left + size[0], top + size[1]))
|
|
|
|
|
|
def add_header(base: Image.Image, logo: Image.Image) -> Image.Image:
|
|
canvas = cover_image(base)
|
|
logo_height = round(logo.height * LOGO_WIDTH / logo.width)
|
|
overlay = logo.resize((LOGO_WIDTH, logo_height), Image.Resampling.LANCZOS)
|
|
canvas.alpha_composite(overlay, ((canvas.width - overlay.width) // 2, LOGO_TOP))
|
|
return canvas.convert("RGB")
|
|
|
|
|
|
def save_preview(base_path: Path, output_path: Path, logo: Image.Image) -> None:
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
add_header(Image.open(base_path), logo).save(output_path)
|
|
|
|
|
|
def paste_logo(base: Image.Image, logo: Image.Image, logo_width: int, top: int) -> Image.Image:
|
|
canvas = cover_image(base)
|
|
logo_height = round(logo.height * logo_width / logo.width)
|
|
overlay = logo.resize((logo_width, logo_height), Image.Resampling.LANCZOS)
|
|
canvas.alpha_composite(overlay, ((canvas.width - overlay.width) // 2, top))
|
|
return canvas.convert("RGB")
|
|
|
|
|
|
def save_logo_preview(base_path: Path, logo_path: Path, output_path: Path, logo_width: int, top: int) -> None:
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
paste_logo(Image.open(base_path), Image.open(logo_path).convert("RGBA"), logo_width, top).save(output_path)
|
|
|
|
|
|
def replace_zip_png(zip_path: Path, member_name: str, image_path: Path) -> None:
|
|
buffer = io.BytesIO()
|
|
Image.open(image_path).convert("RGB").save(buffer, format="PNG")
|
|
replacement = buffer.getvalue()
|
|
temp_path = zip_path.with_suffix(zip_path.suffix + ".tmp")
|
|
with ZipFile(zip_path, "r") as zin, ZipFile(temp_path, "w", ZIP_DEFLATED) as zout:
|
|
replaced = False
|
|
for item in zin.infolist():
|
|
if item.filename == member_name:
|
|
zout.writestr(item, replacement)
|
|
replaced = True
|
|
else:
|
|
zout.writestr(item, zin.read(item.filename))
|
|
if not replaced:
|
|
zout.writestr(member_name, replacement)
|
|
temp_path.replace(zip_path)
|
|
|
|
|
|
def mse_lower_region(left: Image.Image, right: Image.Image) -> float:
|
|
left = cover_image(left).convert("RGB").crop((0, SKINPACK_MATCH_Y, 360, 360))
|
|
right = cover_image(right).convert("RGB").crop((0, SKINPACK_MATCH_Y, 360, 360))
|
|
total = 0
|
|
count = 0
|
|
for a, b in zip(left.tobytes(), right.tobytes()):
|
|
delta = a - b
|
|
total += delta * delta
|
|
count += 1
|
|
return total / count
|
|
|
|
|
|
def regenerate_skinpacks(root: Path, logo: Image.Image) -> list[tuple[Path, Path]]:
|
|
source_dir = root / "resources" / "lce skinpack icons"
|
|
output_dir = root / "skinpacks" / "preview_images"
|
|
sources = [(path, Image.open(path)) for path in sorted(source_dir.glob("*.png"))]
|
|
written: list[tuple[Path, Path]] = []
|
|
for output_path in sorted(output_dir.glob("*.png")):
|
|
if output_path.name in SKIP_SKINPACKS:
|
|
continue
|
|
output_image = Image.open(output_path)
|
|
matches = sorted((mse_lower_region(output_image, source_image), source_path) for source_path, source_image in sources)
|
|
if not matches or matches[0][0] > SKINPACK_MATCH_THRESHOLD:
|
|
continue
|
|
source_path = matches[0][1]
|
|
save_preview(source_path, output_path, logo)
|
|
written.append((source_path, output_path))
|
|
return written
|
|
|
|
|
|
def regenerate_texture_packs(root: Path, logo: Image.Image) -> list[tuple[Path, Path]]:
|
|
source_dir = root / "resources" / "store icons"
|
|
output_dir = root / "texture_packs" / "preview_images"
|
|
written: list[tuple[Path, Path]] = []
|
|
for output_path in sorted(output_dir.glob("*.png")):
|
|
source_path = source_dir / output_path.name
|
|
if not source_path.exists():
|
|
continue
|
|
save_preview(source_path, output_path, logo)
|
|
written.append((source_path, output_path))
|
|
return written
|
|
|
|
|
|
def regenerate_doctor_who(root: Path, logo: Image.Image) -> list[tuple[Path, Path]]:
|
|
source_path = root / "resources" / "store icons" / "doctor_who_preview_clean.png"
|
|
outputs = [
|
|
root / "skinpacks" / "preview_images" / "doctor_who_volume_1_preview.png",
|
|
root / "skinpacks" / "preview_images" / "doctor_who_volume_2_preview.png",
|
|
]
|
|
if not source_path.exists():
|
|
raise FileNotFoundError(f"Missing clean Doctor Who base: {source_path}")
|
|
written: list[tuple[Path, Path]] = []
|
|
for output_path in outputs:
|
|
save_preview(source_path, output_path, logo)
|
|
written.append((source_path, output_path))
|
|
return written
|
|
|
|
|
|
def regenerate_from_the_shadows(root: Path) -> list[tuple[Path, Path]]:
|
|
source_path = root / "resources" / "lce skinpack icons" / "fromtheshadowsskinpack.png"
|
|
logo_path = root / "resources" / "preview logos" / "fromtheshadowslogo.png"
|
|
output_path = root / "skinpacks" / "preview_images" / "fromtheshadows_preview.png"
|
|
save_logo_preview(source_path, logo_path, output_path, 338, 8)
|
|
return [(source_path, output_path)]
|
|
|
|
|
|
def regenerate_legacy4j_specials(root: Path) -> list[tuple[Path, Path]]:
|
|
preview_dir = root / "Legacy4J" / "preview_images"
|
|
pack_dir = root / "Legacy4J" / "packs"
|
|
logo_dir = root / "resources" / "preview logos"
|
|
background = root / "resources" / "blanklegacy4jbackground.png"
|
|
compressed_base = root / "resources" / "store icons" / "compressed_legacy_music_clean.png"
|
|
items = [
|
|
(compressed_base, logo_dir / "originalmusiclogo.png", preview_dir / "compressed_legacy_music_preview.png", 340, 14, pack_dir / "compressed_legacy_music.zip"),
|
|
(background, logo_dir / "legacybedslogo.png", preview_dir / "legacy_beds_preview.png", 300, 104, pack_dir / "legacybeds.zip"),
|
|
(background, logo_dir / "pre113waterlogo.png", preview_dir / "pre_1_13_waters_preview.png", 300, 104, pack_dir / "pre_1_13_waters.zip"),
|
|
(background, logo_dir / "legacytitleslogo.png", preview_dir / "legacy_titles_preview.png", 300, 104, None),
|
|
]
|
|
written: list[tuple[Path, Path]] = []
|
|
for source_path, logo_path, output_path, logo_width, top, zip_path in items:
|
|
if not source_path.exists():
|
|
raise FileNotFoundError(f"Missing clean base: {source_path}")
|
|
save_logo_preview(source_path, logo_path, output_path, logo_width, top)
|
|
if zip_path is not None:
|
|
replace_zip_png(zip_path, "pack.png", output_path)
|
|
written.append((source_path, output_path))
|
|
return written
|
|
|
|
|
|
def regenerate_legacy4j_generic(root: Path, logo: Image.Image) -> list[tuple[Path, Path]]:
|
|
clean_dir = root / "resources" / "store icons" / "legacy4j_clean"
|
|
preview_dir = root / "Legacy4J" / "preview_images"
|
|
items = [
|
|
("faithful_legacy_clean.png", "faithful_legacy_preview.png"),
|
|
("old_ui_clean.png", "old_ui_preview_image.png"),
|
|
("ore4j_clean.png", "ore4j_preview.png"),
|
|
("legacy_tutorial_worlds_clean.png", "legacy_tutorial_worlds_preview.png"),
|
|
("legacy_panorama_tu5_clean.png", "legacy_panorama_tu5_preview.png"),
|
|
("legacy_panorama_tu7_clean.png", "legacy_panorama_tu7_preview.png"),
|
|
("legacy_panorama_tu12_clean.png", "legacy_panorama_tu12_preview.png"),
|
|
("legacy_panorama_tu20_clean.png", "legacy_panorama_tu20_preview.png"),
|
|
("legacy_panorama_tu31_clean.png", "legacy_panorama_tu31_preview.png"),
|
|
("legacy_panorama_tu46_clean.png", "legacy_panorama_tu46_preview.png"),
|
|
]
|
|
written: list[tuple[Path, Path]] = []
|
|
for source_name, output_name in items:
|
|
source_path = clean_dir / source_name
|
|
output_path = preview_dir / output_name
|
|
if not source_path.exists():
|
|
raise FileNotFoundError(f"Missing clean Legacy4J base: {source_path}")
|
|
save_preview(source_path, output_path, logo)
|
|
written.append((source_path, output_path))
|
|
return written
|
|
|
|
|
|
def regenerate_starter_and_hide_seek(root: Path, logo: Image.Image) -> list[tuple[Path, Path]]:
|
|
source_path = Path.home() / "Downloads" / "pack.png"
|
|
if not source_path.exists():
|
|
return []
|
|
outputs = [
|
|
root / "STARTERPACKS" / "preview_images" / "starterpacks_bundle_preview.png",
|
|
root / "community_skinpacks" / "preview_images" / "hermitcraft_skin_pack_preview.png",
|
|
root / "community_skinpacks" / "preview_images" / "hide_and_seek_preview.png",
|
|
]
|
|
written: list[tuple[Path, Path]] = []
|
|
for output_path in outputs:
|
|
save_preview(source_path, output_path, logo)
|
|
written.append((source_path, output_path))
|
|
return written
|
|
|
|
|
|
def regenerate_bundles(root: Path) -> list[tuple[Path, Path]]:
|
|
bundles = {
|
|
"builders bundle": root / "Bundle Packs" / "preview_images" / "builders_pack_preview.png",
|
|
"mega skinpack bundle": root / "Bundle Packs" / "preview_images" / "skinpack_megabundle_preview_image.png",
|
|
"merry bundle": root / "Bundle Packs" / "preview_images" / "merrybundle_preview_image.png",
|
|
"spooky bundle": root / "Bundle Packs" / "preview_images" / "spookybundle_preview_image.png",
|
|
}
|
|
template_root = root / "resources" / "bundle preview image template"
|
|
script = root / "scripts" / "make_bundle_preview.py"
|
|
written: list[tuple[Path, Path]] = []
|
|
for bundle_name, output_path in bundles.items():
|
|
bundle_dir = template_root / bundle_name
|
|
command = [sys.executable, str(script), "--bundle-dir", str(bundle_dir)]
|
|
if bundle_name in {"merry bundle", "spooky bundle"}:
|
|
command.extend(["--text-image-max-height", "36"])
|
|
subprocess.run(command, check=True)
|
|
source_path = bundle_dir / "preview.png"
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
Image.open(source_path).convert("RGB").resize(CANVAS_SIZE, Image.Resampling.LANCZOS).save(output_path)
|
|
written.append((source_path, output_path))
|
|
return written
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Regenerate previews that use the generic Minecraft Legacy4J header.")
|
|
parser.add_argument(
|
|
"--category",
|
|
action="append",
|
|
choices=[
|
|
"skinpacks",
|
|
"texture-packs",
|
|
"doctor-who",
|
|
"from-the-shadows",
|
|
"legacy4j-specials",
|
|
"legacy4j-generic",
|
|
"starterpacks",
|
|
"bundles",
|
|
],
|
|
help="Limit regeneration to one or more categories.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
root = repo_root()
|
|
logo = Image.open(root / "resources" / "preview logos" / "header.png").convert("RGBA")
|
|
categories = set(args.category or ["skinpacks", "texture-packs", "doctor-who", "from-the-shadows", "legacy4j-specials", "legacy4j-generic", "starterpacks", "bundles"])
|
|
jobs = []
|
|
if "skinpacks" in categories:
|
|
jobs.extend(regenerate_skinpacks(root, logo))
|
|
if "texture-packs" in categories:
|
|
jobs.extend(regenerate_texture_packs(root, logo))
|
|
if "doctor-who" in categories:
|
|
jobs.extend(regenerate_doctor_who(root, logo))
|
|
if "from-the-shadows" in categories:
|
|
jobs.extend(regenerate_from_the_shadows(root))
|
|
if "legacy4j-specials" in categories:
|
|
jobs.extend(regenerate_legacy4j_specials(root))
|
|
if "legacy4j-generic" in categories:
|
|
jobs.extend(regenerate_legacy4j_generic(root, logo))
|
|
if "starterpacks" in categories:
|
|
jobs.extend(regenerate_starter_and_hide_seek(root, logo))
|
|
if "bundles" in categories:
|
|
jobs.extend(regenerate_bundles(root))
|
|
|
|
for source_path, output_path in jobs:
|
|
print(f"{source_path.relative_to(root) if source_path.is_relative_to(root) else source_path} -> {output_path.relative_to(root)}")
|
|
print(f"Regenerated {len(jobs)} previews")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|