#!/usr/bin/env python3 """Generate every Falcon Eye logo file from mark.json + the brand fonts. Text is converted to outlines, so the SVGs render without the fonts installed. Run: python3 build_logos.py (needs fontTools + uharfbuzz; PNGs need Google Chrome) """ import io, json, os, subprocess import uharfbuzz as hb from fontTools.ttLib import TTFont from fontTools.varLib import instancer from fontTools.pens.svgPathPen import SVGPathPen from fontTools.pens.transformPen import TransformPen HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) OUT = os.path.join(ROOT, "logo") os.makedirs(OUT, exist_ok=True) LATIN = os.path.join(ROOT, "fonts", "Montserrat.ttf") ARABIC = os.path.join(ROOT, "fonts", "Alexandria.ttf") OBSIDIAN, GRAPHITE, LIME, OFFWHITE, STEEL = "#0A0D0F", "#171C20", "#B9FF38", "#F3F5F2", "#7B858B" # ---------------- the mark ---------------- M = json.load(open(os.path.join(HERE, "mark.json"))) bx0, by0, bx1, by1 = M["bbox"] PAD = 3 MX, MY = bx0 - PAD, by0 - PAD MW, MH = bx1 - bx0 + 2 * PAD, by1 - by0 + 2 * PAD SIL = " ".join(M["silver"]); EYE = " ".join(M["eye"]); PX, PY, PR = M["pupil"] # head tones: silver for dark backgrounds, graphite for light ones, flat for one-colour TONES = { "silver": ("#F6F8F5", "#8C959A"), "graphite": ("#2B3237", "#5F696F"), } _uid = [0] def mark(tone="silver", eye=LIME, pupil=OBSIDIAN, flat=None): """Mark in its own 0..MW x 0..MH box.""" _uid[0] += 1; u = _uid[0] if flat: head = f'fill="{flat}"'; defs = "" else: a, b = TONES[tone] defs = (f'' f'') head = f'fill="url(#g{u})"' defs += f'' return (f'{defs}' f'' f'' f'') def mark_knockout(color): """One-colour mark with the pupil cut out (works on any background).""" _uid[0] += 1; u = _uid[0] return (f'' f'' f'' f'' f'' f'') # ---------------- text to outlines ---------------- _fonts = {} def _font(path, wght): key = (path, wght) if key not in _fonts: st = instancer.instantiateVariableFont(TTFont(path), {"wght": wght}) buf = io.BytesIO(); st.save(buf); data = buf.getvalue() _fonts[key] = (TTFont(io.BytesIO(data)), data) return _fonts[key] def text(s, size, path=LATIN, wght=600, tracking=0.0): """-> (svg path d with baseline at y=0, advance width, cap height)""" tt, data = _font(path, wght) upem = tt["head"].unitsPerEm f = hb.Font(hb.Face(data)) buf = hb.Buffer(); buf.add_str(s); buf.guess_segment_properties() hb.shape(f, buf, {"kern": True}) gs, order = tt.getGlyphSet(), tt.getGlyphOrder() k = size / upem; x = 0; parts = [] n = len(buf.glyph_infos) for i, (gi, gp) in enumerate(zip(buf.glyph_infos, buf.glyph_positions)): pen = SVGPathPen(gs) gs[order[gi.codepoint]].draw(TransformPen(pen, (k, 0, 0, -k, (x + gp.x_offset) * k, -gp.y_offset * k))) if pen.getCommands(): parts.append(pen.getCommands()) x += gp.x_advance + (tracking * upem if i < n - 1 else 0) return " ".join(parts), x * k, tt["OS/2"].sCapHeight * k # wordmark pieces. The A is drawn as Λ (no crossbar), like the reference. FAL, falw, CAP = text("FΛLCON ", 60, wght=600, tracking=0.06) EYEW, eyew, _ = text("EYE", 60, wght=600, tracking=0.06) WORDW = falw + eyew TAG, tagw, TAGCAP = text("SEE FURTHER", 15, wght=500, tracking=0.62) AR_EYE, ar_eyew, _ = text("عين", 64, ARABIC, 600) AR_FAL, ar_falw, _ = text("الصقر", 64, ARABIC, 600) SPACE_AR = 16 ARW = ar_eyew + SPACE_AR + ar_falw AR_TAG, ar_tagw, _ = text("تشوف أبعد", 20, ARABIC, 400) def wordmark(x, y, fg, accent=LIME): return (f'' f'') def ar_wordmark(x, y, fg, accent=LIME, scale=1.0): # RTL: "عين" sits on the right, "الصقر" on the left return (f'' f'' f'') def save(name, w, h, body, bg=None): rect = f'' if bg else "" open(os.path.join(OUT, name), "w").write( f'{rect}{body}\n') def scaled(body, s, dx=0, dy=0): return f'{body}' # variants: (suffix, head tone, wordmark colour, tagline colour, accent) VARIANTS = [("", "silver", OFFWHITE, STEEL, LIME), # for dark backgrounds (primary) ("-on-light", "graphite", GRAPHITE, STEEL, GRAPHITE)] # on light, lime stays only in the eye for sfx, tone, fg, tagc, acc in VARIANTS: # mark only save(f"falcon-eye-mark{sfx}.svg", MW, MH, mark(tone)) # primary stacked: mark, wordmark, tagline — centred ms = 1.0; W = max(WORDW, MW) + 40 body = (scaled(mark(tone), ms, (W - MW * ms) / 2, 0) + wordmark((W - WORDW) / 2, MH + 70, fg, acc) + f'') save(f"falcon-eye-logo-stacked{sfx}.svg", W, MH + 124, body) # horizontal EN: mark left, wordmark + tagline right s = 110 / MH; gap = 22 tx = MW * s + gap body = (scaled(mark(tone), s) + wordmark(tx, 64, fg, acc) + f'') save(f"falcon-eye-logo-horizontal{sfx}.svg", tx + WORDW + 4, 110, body) # horizontal AR: mark stays on the left so the falcon faces the name body = (scaled(mark(tone), s) + ar_wordmark(tx, 66, fg, acc) + f'') save(f"falcon-eye-logo-ar{sfx}.svg", tx + ARW + 4, 110, body) # bilingual: mark + EN wordmark over AR wordmark arsc = 0.62 body = (scaled(mark(tone), s) + wordmark(tx, 52, fg, acc) + ar_wordmark(tx + WORDW - ARW * arsc, 100, fg, acc, scale=arsc)) save(f"falcon-eye-logo-bilingual{sfx}.svg", tx + WORDW + 4, 112, body) # wordmark only save(f"falcon-eye-wordmark{sfx}.svg", WORDW + 4, CAP + 4, wordmark(0, CAP + 2, fg, acc)) # one-colour marks save("falcon-eye-mark-mono-white.svg", MW, MH, mark_knockout("#FFFFFF")) save("falcon-eye-mark-mono-black.svg", MW, MH, mark_knockout("#000000")) # app icon: graphite tile, silver mark (square, no rounding — the OS masks it) S = 1024; ms = 640 / MW icon = (f'' + scaled(mark("silver"), ms, (S - MW * ms) / 2 - 10, (S - MH * ms) / 2 + 6)) save("falcon-eye-app-icon.svg", S, S, icon) # favicon (mark on obsidian, tight) fs = 56 / MW save("falcon-eye-favicon.svg", 64, 64, f'' + scaled(mark("silver"), fs, 4, (64 - MH * fs) / 2)) print("SVGs ->", OUT) # ---------------- PNG exports via headless Chrome ---------------- CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" PNG = os.path.join(OUT, "png"); os.makedirs(PNG, exist_ok=True) def png(svg, w, h, out=None, bg="transparent"): html = os.path.join(PNG, "_tmp.html") open(html, "w").write(f'') subprocess.run([CHROME, "--headless=new", "--disable-gpu", "--hide-scrollbars", "--default-background-color=00000000", f"--window-size={w},{h}", f"--screenshot={os.path.join(PNG, out or svg.replace('.svg', '.png'))}", "file://" + html], capture_output=True) os.remove(html) if os.path.exists(CHROME): png("falcon-eye-app-icon.svg", 1024, 1024) png("falcon-eye-favicon.svg", 64, 64) r = MH / MW png("falcon-eye-mark.svg", 1024, round(1024 * r)) png("falcon-eye-mark-on-light.svg", 1024, round(1024 * r)) for sfx in ["", "-on-light"]: for n, w in [("stacked", 1200), ("horizontal", 1600), ("bilingual", 1600)]: svg = f"falcon-eye-logo-{n}{sfx}.svg" vb = open(os.path.join(OUT, svg)).read().split('viewBox="0 0 ')[1].split('"')[0].split() png(svg, w, round(w * float(vb[1]) / float(vb[0]))) print("PNGs ->", PNG)