1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
"""Build a WhatsApp user-created sticker pack of Fuse mixes.
usage: build_pack.py OUT_DIR NAME A.was:B.was [A.was:B.was ...]
Each A:B pair becomes one sticker (A's sticker layer + B's effect, every file copied
byte for byte). Writes OUT_DIR/pack.zip ("<waName>.was" entries + "<packid>.png" tray
icon, the layout of a received STK_PK), thumb.png (252x252) and manifest.tsv (a header line
id/name/publisher/tray, then fileName/emojis/alt text per sticker): the input of
`sendsticker <session> --pack OUT_DIR`.
"""
import base64, hashlib, io, json, os, subprocess, sys, uuid, zipfile
M, E, T, META = "animation/animation.json", "animation/animation_secondary.json", ".trust_token", "animation/animation.json.overridden_metadata"
LOTTIE = "/root/wa-stickers/tools/lottie.min.js"
out, name, pairs = sys.argv[1], sys.argv[2], [p.split(":") for p in sys.argv[3:]]
os.makedirs(out, exist_ok=True)
pack_id = str(uuid.uuid4())
def wa_name(b):
return base64.b64encode(hashlib.sha256(b).digest()).decode().replace("/", "-") + ".was"
def mix(a_path, b_path):
a, b = zipfile.ZipFile(a_path), zipfile.ZipFile(b_path)
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("animation/", b"")
for n, src in [(M, a), (E, b), (M + T, a), (E + T, b), (META, a)]:
z.writestr(n, src.read(n))
meta = json.loads(a.read(META))
return buf.getvalue(), meta, a.read(M).decode()
def render(lottie_json, size, path):
html = os.path.join(out, "render.html")
open(html, "w").write(
f'<!doctype html><html><body style="margin:0;background:transparent">'
f'<div id="c" style="width:{size}px;height:{size}px"></div><script>{open(LOTTIE).read()}</script>'
f'<script>const a=lottie.loadAnimation({{container:document.getElementById("c"),renderer:"svg",loop:false,autoplay:false,animationData:{lottie_json}}});'
f'a.goToAndStop(Math.floor(a.totalFrames*0.45),true);</script></body></html>')
subprocess.run(["timeout", "60", "chromium", "--headless=new", "--no-sandbox", "--disable-gpu", "--hide-scrollbars",
f"--window-size={size},{size}", "--default-background-color=00000000", "--virtual-time-budget=3000",
f"--screenshot={path}", f"file://{html}"], check=True, capture_output=True)
os.remove(html)
rows, files, first_main = [], [], None
for a_path, b_path in pairs:
data, meta, main_json = mix(a_path, b_path)
fn = wa_name(data)
files.append((fn, data))
rows.append((fn, " ".join(meta.get("emojis", [])), meta.get("accessibility-text", "")))
first_main = first_main or main_json
tray = f"{pack_id}.png"
render(first_main, 96, os.path.join(out, tray))
render(first_main, 252, os.path.join(out, "thumb.png"))
with zipfile.ZipFile(os.path.join(out, "pack.zip"), "w", zipfile.ZIP_DEFLATED) as z:
for fn, data in files:
z.writestr(fn, data)
z.write(os.path.join(out, tray), tray)
with open(os.path.join(out, "manifest.tsv"), "w") as f:
f.write(f"{pack_id}\t{name}\tFuse\t{tray}\n")
for r in rows:
f.write("\t".join(x.replace("\t", " ").replace("\n", " ") for x in r) + "\n")
print(pack_id, len(files), "stickers,", os.path.getsize(os.path.join(out, "pack.zip")), "bytes")