Initial commit

This commit is contained in:
Codex
2026-04-07 21:48:23 +08:00
commit 8f111823a4
4707 changed files with 12030 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
.wrangler/
.dev.vars
*.log
__pycache__/
*.pyc

221
convert_to_webp.py Normal file
View File

@@ -0,0 +1,221 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import io
import os
import sys
import zipfile
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Callable
try:
from PIL import Image, ImageSequence
except ImportError as exc: # pragma: no cover
raise SystemExit(
"This script requires Pillow. Install it with: python3 -m pip install pillow"
) from exc
IMAGE_EXTENSIONS = {
".avif",
".bmp",
".gif",
".jfif",
".jpeg",
".jpg",
".jpe",
".png",
".tif",
".tiff",
}
@dataclass(frozen=True)
class Job:
source_label: str
target_path: Path
loader: Callable[[], bytes]
def is_image_path(path: Path) -> bool:
return path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS
def convert_to_webp(data: bytes) -> tuple[bytes, bool]:
with Image.open(io.BytesIO(data)) as im:
is_animated = bool(
getattr(im, "is_animated", False) or getattr(im, "n_frames", 1) > 1
)
if is_animated:
frames = []
durations = []
loop = im.info.get("loop", 0)
for frame in ImageSequence.Iterator(im):
frames.append(frame.convert("RGBA"))
durations.append(frame.info.get("duration", im.info.get("duration", 100)))
output = io.BytesIO()
frames[0].save(
output,
format="WEBP",
save_all=True,
append_images=frames[1:],
duration=durations,
loop=loop,
lossless=True,
method=4,
)
return output.getvalue(), True
output = io.BytesIO()
im.convert("RGBA").save(output, format="WEBP", lossless=True, method=4)
return output.getvalue(), False
def _read_zip_member(zip_path: Path, member_name: str) -> bytes:
with zipfile.ZipFile(zip_path) as zf:
return zf.read(member_name)
def collect_directory_jobs(root: Path, output_base: Path) -> list[Job]:
grouped: dict[Path, list[Path]] = {}
for path in sorted(root.rglob("*")):
if not is_image_path(path):
continue
grouped.setdefault(path.parent, []).append(path)
jobs: list[Job] = []
for directory in sorted(grouped):
files = sorted(grouped[directory], key=lambda p: p.name)
relative_dir = directory.relative_to(root)
target_dir = output_base / relative_dir
for index, path in enumerate(files, start=1):
jobs.append(
Job(
source_label=path.relative_to(root).as_posix(),
target_path=target_dir / f"{index}.webp",
loader=lambda p=path: p.read_bytes(),
)
)
return jobs
def collect_zip_jobs(zip_path: Path, output_base: Path) -> list[Job]:
grouped: dict[PurePosixPath, list[PurePosixPath]] = {}
with zipfile.ZipFile(zip_path) as zf:
for info in zf.infolist():
if info.is_dir():
continue
member_path = PurePosixPath(info.filename)
if member_path.suffix.lower() not in IMAGE_EXTENSIONS:
continue
grouped.setdefault(member_path.parent, []).append(member_path)
jobs: list[Job] = []
for directory in sorted(grouped, key=lambda p: str(p)):
files = sorted(grouped[directory], key=lambda p: str(p.name))
for index, member_path in enumerate(files, start=1):
target_dir = output_base / Path(str(directory))
jobs.append(
Job(
source_label=member_path.as_posix(),
target_path=target_dir / f"{index}.webp",
loader=lambda name=member_path.as_posix(), zp=zip_path: _read_zip_member(
zp, name
),
)
)
return jobs
def collect_jobs(source: Path, output_base: Path | None) -> list[Job]:
if source.is_dir():
base = output_base if output_base is not None else source.parent / f"{source.name}-out"
base.mkdir(parents=True, exist_ok=True)
return collect_directory_jobs(source, base)
if source.is_file() and source.suffix.lower() == ".zip":
base = output_base if output_base is not None else source.parent / source.stem
base.mkdir(parents=True, exist_ok=True)
return collect_zip_jobs(source, base)
if is_image_path(source):
return [
Job(
source_label=source.name,
target_path=source.parent / "1.webp",
loader=lambda p=source: p.read_bytes(),
)
]
return []
def run_job(job: Job) -> tuple[bool, str, Path, bool | None, str | None]:
try:
data = job.loader()
webp_data, animated = convert_to_webp(data)
job.target_path.parent.mkdir(parents=True, exist_ok=True)
job.target_path.write_bytes(webp_data)
return True, job.source_label, job.target_path, animated, None
except Exception as exc:
return False, job.source_label, job.target_path, None, str(exc)
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
description="Convert images to WebP in place, with independent numbering per folder."
)
parser.add_argument(
"source",
nargs="?",
default=".",
help="Source folder, image file, or .zip archive. Default: current directory.",
)
parser.add_argument(
"--output-base",
default=None,
help="Output root. Default for folders: <source>-out next to the source. Default for zip: a folder next to the zip with the same name.",
)
parser.add_argument(
"-j",
"--jobs",
type=int,
default=max(1, min(32, os.cpu_count() or 4)),
help="Number of worker threads. Default: number of CPU cores.",
)
args = parser.parse_args(argv)
source = Path(args.source).expanduser().resolve()
output_base = Path(args.output_base).expanduser().resolve() if args.output_base else None
jobs = collect_jobs(source, output_base)
jobs.sort(key=lambda job: str(job.target_path))
if not jobs:
print(f"No supported images found in: {source}")
return 1
converted = 0
skipped = 0
with ThreadPoolExecutor(max_workers=max(1, args.jobs)) as pool:
for ok, label, target, animated, error in pool.map(run_job, jobs):
if ok:
kind = "animated" if animated else "static"
print(f"{label} -> {target} ({kind})")
converted += 1
else:
print(f"[skip] {label}: {error}", file=sys.stderr)
skipped += 1
print(f"Done. converted={converted}, skipped={skipped}")
return 0 if converted else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))

BIN
meme/cheems/1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
meme/deepseek/1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
meme/deepseek/2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
meme/emoji/1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
meme/emoji/10.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
meme/emoji/11.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
meme/emoji/12.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
meme/emoji/13.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

BIN
meme/emoji/14.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
meme/emoji/15.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

BIN
meme/emoji/16.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

BIN
meme/emoji/17.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
meme/emoji/18.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

BIN
meme/emoji/19.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
meme/emoji/2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
meme/emoji/20.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
meme/emoji/21.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

BIN
meme/emoji/22.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

BIN
meme/emoji/23.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
meme/emoji/24.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

BIN
meme/emoji/25.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

BIN
meme/emoji/26.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
meme/emoji/27.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
meme/emoji/28.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
meme/emoji/29.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
meme/emoji/3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

BIN
meme/emoji/30.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

BIN
meme/emoji/31.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

BIN
meme/emoji/32.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

BIN
meme/emoji/33.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

BIN
meme/emoji/34.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

BIN
meme/emoji/35.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
meme/emoji/36.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
meme/emoji/37.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

BIN
meme/emoji/38.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
meme/emoji/39.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
meme/emoji/4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
meme/emoji/40.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
meme/emoji/41.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
meme/emoji/42.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
meme/emoji/43.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
meme/emoji/44.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
meme/emoji/45.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
meme/emoji/46.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
meme/emoji/47.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
meme/emoji/48.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

BIN
meme/emoji/49.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

BIN
meme/emoji/5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
meme/emoji/50.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
meme/emoji/51.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
meme/emoji/52.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
meme/emoji/53.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
meme/emoji/54.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
meme/emoji/55.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
meme/emoji/56.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

BIN
meme/emoji/57.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
meme/emoji/58.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

BIN
meme/emoji/59.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
meme/emoji/6.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

BIN
meme/emoji/60.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
meme/emoji/61.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
meme/emoji/62.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

BIN
meme/emoji/63.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
meme/emoji/64.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

BIN
meme/emoji/65.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
meme/emoji/66.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

BIN
meme/emoji/67.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

BIN
meme/emoji/68.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
meme/emoji/69.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
meme/emoji/7.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
meme/emoji/70.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
meme/emoji/71.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
meme/emoji/72.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
meme/emoji/73.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

BIN
meme/emoji/74.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
meme/emoji/75.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
meme/emoji/76.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
meme/emoji/77.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
meme/emoji/78.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
meme/emoji/79.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
meme/emoji/8.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
meme/emoji/80.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
meme/emoji/81.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
meme/emoji/82.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
meme/emoji/83.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
meme/emoji/84.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
meme/emoji/9.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
meme/happycat/1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
meme/happycat/2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

BIN
meme/happycat/3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
meme/momo/1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
meme/momo/10.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
meme/momo/11.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
meme/momo/12.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

BIN
meme/momo/13.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

BIN
meme/momo/14.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
meme/momo/15.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
meme/momo/16.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Some files were not shown because too many files have changed in this diff Show More