SHA256
- Эмодзи-пикер: js/components/emoji-picker.js + каталог telegram-emoji-*.js (smileys/people/animals_nature/food_drink/travel_places/activity/objects/ symbols/flags) + 631 ассет в assets/emoji (7.2 МБ). - Интеграция пикера: chat-view.js, app.js, index.html, styles/components.css. - Дев-скрипты генерации превью + THIRD_PARTY_NOTICES. - Работа НЕ завершена (WIP), сохранено как чекпоинт. С origin (+56) пока НЕ мержится — большой мерж будет после завершения, отдельным шагом. - VERSION: client 1.2.263. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.2 KiB
Python
91 lines
3.2 KiB
Python
"""Generate local first-frame PNG previews for the Telegram emoji catalog."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
from urllib.parse import quote
|
|
from urllib.request import Request, urlopen
|
|
|
|
from PIL import Image
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
CATALOG_DIR = ROOT / "js" / "components"
|
|
OUTPUT_DIR = ROOT / "assets" / "emoji" / "telegram-previews"
|
|
ASSET_BASE_URL = "https://raw.githubusercontent.com/Tarikul-Islam-Anik/Telegram-Animated-Emojis/main/"
|
|
FILE_PATTERN = re.compile(r'"file"\s*:\s*"([^"]+\.webp)"', re.IGNORECASE)
|
|
|
|
|
|
def catalog_files() -> list[str]:
|
|
files: set[str] = set()
|
|
for path in sorted(CATALOG_DIR.glob("telegram-emoji-*.js")):
|
|
files.update(FILE_PATTERN.findall(path.read_text(encoding="utf-8")))
|
|
return sorted(files)
|
|
|
|
|
|
def preview_path(source_file: str) -> Path:
|
|
return OUTPUT_DIR / Path(source_file).with_suffix(".png")
|
|
|
|
|
|
def generate_one(source_file: str, force: bool) -> tuple[str, str]:
|
|
output = preview_path(source_file)
|
|
if output.exists() and not force:
|
|
return source_file, "cached"
|
|
|
|
url = ASSET_BASE_URL + quote(source_file, safe="/")
|
|
request = Request(url, headers={"User-Agent": "SHiNE emoji preview generator"})
|
|
with urlopen(request, timeout=30) as response:
|
|
payload = response.read()
|
|
|
|
with Image.open(BytesIO(payload)) as source:
|
|
source.seek(0)
|
|
frame = source.convert("RGBA")
|
|
frame.thumbnail((96, 96), Image.Resampling.LANCZOS)
|
|
canvas = Image.new("RGBA", (96, 96), (0, 0, 0, 0))
|
|
offset = ((96 - frame.width) // 2, (96 - frame.height) // 2)
|
|
canvas.alpha_composite(frame, offset)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
canvas.save(output, format="PNG", optimize=True)
|
|
return source_file, "generated"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--force", action="store_true", help="Regenerate existing previews")
|
|
parser.add_argument("--workers", type=int, default=8)
|
|
args = parser.parse_args()
|
|
|
|
files = catalog_files()
|
|
if not files:
|
|
print("No catalog files found", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"Generating {len(files)} previews into {OUTPUT_DIR}")
|
|
failures: list[tuple[str, str]] = []
|
|
generated = cached = 0
|
|
with ThreadPoolExecutor(max_workers=max(1, args.workers)) as executor:
|
|
futures = {executor.submit(generate_one, source_file, args.force): source_file for source_file in files}
|
|
for future in as_completed(futures):
|
|
source_file = futures[future]
|
|
try:
|
|
_, status = future.result()
|
|
except Exception as error: # noqa: BLE001 - report all asset failures together
|
|
failures.append((source_file, str(error)))
|
|
continue
|
|
generated += status == "generated"
|
|
cached += status == "cached"
|
|
|
|
print(f"Generated: {generated}; cached: {cached}; failed: {len(failures)}")
|
|
for source_file, error in failures:
|
|
print(f"FAILED {source_file}: {error}", file=sys.stderr)
|
|
return 1 if failures else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|