#!/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'\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)