File size: 7,953 Bytes
28c70af | 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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | #!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
"""Draw a page's share cards: the images a link preview shows (Open Graph, X/Twitter, LinkedIn, Discord, Telegram,
Slack). Three files, deterministic:
og-<name>.jpg 1200×630, the wide card (og:image, twitter:image with summary_large_image)
og-<name>-square.jpg 1200×1200, for platforms that crop square
<name>-touch-icon.png 180×180, the apple-touch-icon
The look is the DeltaVerse's: a triangulated mesh glowing cyan to violet on the landing's radial dark, the frame's
corner marks, and the DeltaVerse $ (the green of deltaverse.pythai.net/favicon.ico) drawn large as an outlined glyph.
The words are arguments, so a card says only what the page measured.
usage: python3 tools/makecards.py [--out DIR] [--name bankml] [--title bankML]
[--tagline "line one|line two"] [--stat "bit-exact|vs llama.cpp"]... [--foot "text"] [--fonts DIR]
needs: Pillow (pip install pillow) and the Noto Sans fonts (fonts-noto-core on Debian/Ubuntu). Check the result with
tools/seo.py, which also confirms each card's real size matches what the page declares."""
import argparse, math, random
from pathlib import Path
try:
from PIL import Image, ImageDraw, ImageFilter, ImageFont
except ImportError:
raise SystemExit("tools/makecards.py needs Pillow: pip install pillow")
GREEN, CYAN, VIOLET = (51, 190, 103), (34, 211, 238), (157, 78, 221)
DEFAULT_STATS = ["bit-exact|vs llama.cpp", "9.4–10×|ternary kernel", "0|dependencies"]
def mesh(w, h, seed, n):
"""A jittered grid, every cell cut on a diagonal: a triangulation with no long slivers."""
rnd = random.Random(seed)
cols, rows = n, max(3, round(n * h / w))
jit = lambda i, last, span: rnd.uniform(-.38, .38) * span if 0 < i < last else 0
pts = [[(c * w / (cols - 1) + jit(c, cols - 1, w / cols), r * h / (rows - 1) + jit(r, rows - 1, h / rows))
for c in range(cols)] for r in range(rows)]
tris = []
for r in range(rows - 1):
for c in range(cols - 1):
a, b, cc, d = pts[r][c], pts[r][c + 1], pts[r + 1][c], pts[r + 1][c + 1]
tris += [(a, b, d), (a, d, cc)] if (r + c) % 2 else [(a, b, cc), (b, d, cc)]
return tris
class Cards:
def __init__(self, fonts: Path):
self.fonts = fonts
for f in ("NotoSans-Bold.ttf", "NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-SemiBold.ttf", "NotoSansMono-Regular.ttf"):
if not (fonts / f).exists():
raise SystemExit(f"{fonts / f} not found: install fonts-noto-core or pass --fonts DIR")
def font(self, name, size):
return ImageFont.truetype(str(self.fonts / name), size)
def ground(self, w, h):
g = Image.new("L", (w, h))
gd = ImageDraw.Draw(g)
cx, cy, R = w * .5, h * .5, math.hypot(w, h) * .55
for i in range(60, 0, -1):
gd.ellipse([cx - R * i / 60, cy - R * i / 60, cx + R * i / 60, cy + R * i / 60], fill=int(22 * (1 - i / 60) ** .7))
img = Image.merge("RGB", (g.point(lambda v: v * .35), g.point(lambda v: v * .45), g.point(lambda v: v)))
m = Image.new("RGBA", (w, h), (0, 0, 0, 0))
md = ImageDraw.Draw(m)
for t in mesh(w, h, 1129, 15 if w > h else 11):
x = sum(p[0] for p in t) / 3 / w
col = tuple(int(CYAN[k] * (1 - x) + VIOLET[k] * x) for k in range(3))
md.polygon(t, outline=col + (70,), width=1)
md.polygon(t, fill=col + (int(10 + 18 * random.Random(int(x * 1e6)).random()),))
glow = m.filter(ImageFilter.GaussianBlur(3))
img.paste(glow, (0, 0), glow)
img.paste(m, (0, 0), m)
veil = Image.new("RGBA", (w, h), (4, 6, 14, 150)) # so the type reads over the mesh
img.paste(veil, (0, 0), veil)
return img
def dollar(self, img, x, y, size):
layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
ImageDraw.Draw(layer).text((x, y), "$", font=self.font("NotoSans-Bold.ttf", size), fill=GREEN + (255,), anchor="mm")
glow = layer.filter(ImageFilter.GaussianBlur(16))
img.paste(glow, (0, 0), glow)
ImageDraw.Draw(img).text((x, y), "$", font=self.font("NotoSans-Bold.ttf", size), fill=(6, 22, 12), anchor="mm",
stroke_width=max(4, size // 40), stroke_fill=GREEN)
def card(self, w, h, title, tagline, stats, foot, path):
img = self.ground(w, h)
d = ImageDraw.Draw(img)
square = h >= w
self.dollar(img, *((w * .5, h * .27) if square else (w * .2, h * .5)), int(h * (.42 if square else .78)))
tx, anchor = (w * .5, "ma") if square else (w * .40, "la")
y = h * .52 if square else h * .17
d.text((tx, y), title, font=self.font("NotoSans-Light.ttf", int(h * (.115 if square else .17))), fill=(255, 255, 255), anchor=anchor)
y += h * (.14 if square else .22)
for line in tagline:
d.text((tx, y), line, font=self.font("NotoSans-Regular.ttf", int(h * (.036 if square else .052))), fill=(214, 226, 236), anchor=anchor)
y += h * (.05 if square else .072)
y += h * .03
colw = (w * .82 if square else w * .55) / max(1, len(stats))
x0 = (w - colw * len(stats)) / 2 if square else tx
for i, (big, small) in enumerate(stats):
x = x0 + colw * i
d.text((x, y), big, font=self.font("NotoSans-SemiBold.ttf", int(h * (.045 if square else .062))), fill=CYAN, anchor="la")
d.text((x, y + h * (.058 if square else .082)), small, font=self.font("NotoSans-Regular.ttf", int(h * (.026 if square else .036))),
fill=(160, 172, 190), anchor="la")
d.text((w / 2, h * .94), foot, font=self.font("NotoSansMono-Regular.ttf", int(h * (.022 if square else .032))), fill=(150, 160, 178), anchor="mm")
for ax, ay, sx, sy in [(14, 14, 1, 1), (w - 14, 14, -1, 1), (14, h - 14, 1, -1), (w - 14, h - 14, -1, -1)]:
d.line([(ax, ay), (ax + 34 * sx, ay)], fill=CYAN, width=2) # the landing's corner marks
d.line([(ax, ay), (ax, ay + 34 * sy)], fill=CYAN, width=2)
img.save(path, quality=90, optimize=True, progressive=True)
print(f"{path} {w}×{h}")
def touch(self, path, size=180):
img = Image.new("RGB", (size, size), (0, 0, 0))
ImageDraw.Draw(img).text((size / 2, size * .53), "$", font=self.font("NotoSans-Bold.ttf", int(size * .92)), fill=(6, 22, 12),
anchor="mm", stroke_width=max(3, size // 36), stroke_fill=GREEN)
img.save(path)
print(f"{path} {size}×{size}")
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--out", default=".", help="directory to write into (default: here)")
ap.add_argument("--name", default="bankml", help="file stem: og-<name>.jpg, og-<name>-square.jpg, <name>-touch-icon.png")
ap.add_argument("--title", default="bankML")
ap.add_argument("--tagline", default="Verified 1-bit and ternary language|models on the CPU you already have", help="lines split by |")
ap.add_argument("--stat", action="append", help='"big|small", repeatable (default: the three measured claims)')
ap.add_argument("--foot", default="deltaverse.pythai.net/bankml · github.com/cryptoAGI/bankml · Rust")
ap.add_argument("--fonts", default="/usr/share/fonts/truetype/noto", help="directory holding the Noto Sans .ttf files")
a = ap.parse_args()
out = Path(a.out)
out.mkdir(parents=True, exist_ok=True)
stats = [tuple((s.split("|", 1) + [""])[:2]) for s in (a.stat or DEFAULT_STATS)]
c = Cards(Path(a.fonts))
tagline = a.tagline.split("|")
c.card(1200, 630, a.title, tagline, stats, a.foot, out / f"og-{a.name}.jpg")
c.card(1200, 1200, a.title, tagline, stats, a.foot, out / f"og-{a.name}-square.jpg")
c.touch(out / f"{a.name}-touch-icon.png")
if __name__ == "__main__":
main()
|