| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521 |
- #!/usr/bin/env python3
- """
- palette.py — Infer design tokens from document metadata.
- Usage:
- python3 palette.py --title "AI Trends 2025" --type report --out tokens.json
- python3 palette.py --title "John Doe Resume" --type resume --out tokens.json
- python3 palette.py --meta meta.json --out tokens.json
- Outputs tokens.json consumed by all downstream scripts.
- Cover fonts are loaded via Google Fonts @import in the cover HTML (no local caching).
- Body fonts always use ReportLab system fonts (Times-Bold / Helvetica).
- Exit codes: 0 success, 1 bad args, 3 write error
- """
- import argparse
- import json
- import sys
- # ── Palette library ────────────────────────────────────────────────────────────
- # Each entry: cover colors + cover_pattern + mood
- PALETTES = {
- "report": {
- # Charcoal blue-grey cover; muted steel blue accent — authoritative, not flashy
- "cover_bg": "#1B2A38",
- "accent": "#3B6D8A",
- "accent_lt": "#E6EFF5",
- "text_light": "#EDE9E2",
- "page_bg": "#FAFAF8",
- "dark": "#1A1E24",
- "body_text": "#2C2C30",
- "muted": "#7A7A84",
- "cover_pattern": "fullbleed",
- "mood": "authoritative",
- },
- "proposal": {
- # Dark charcoal cover; slate grey-blue accent — confident, understated
- "cover_bg": "#22272E",
- "accent": "#4E6070",
- "accent_lt": "#EAECEE",
- "text_light": "#EDE9E2",
- "page_bg": "#FAFAF7",
- "dark": "#18191E",
- "body_text": "#28282E",
- "muted": "#7A7870",
- "cover_pattern": "split",
- "mood": "confident",
- },
- "resume": {
- # White; deep navy accent — clean and unambiguous
- "cover_bg": "#FFFFFF",
- "accent": "#1C3557",
- "accent_lt": "#E8EEF5",
- "text_light": "#FFFFFF",
- "page_bg": "#FFFFFF",
- "dark": "#111111",
- "body_text": "#222222",
- "muted": "#888888",
- "cover_pattern": "typographic",
- "mood": "clean",
- },
- "portfolio": {
- # Near-black charcoal; cool slate grey accent — subdued professional
- "cover_bg": "#191C20",
- "accent": "#6A7A88",
- "accent_lt": "#EAECEE",
- "text_light": "#EDE9E4",
- "page_bg": "#F8F8F8",
- "dark": "#18191E",
- "body_text": "#28282E",
- "muted": "#8A8A96",
- "cover_pattern": "atmospheric",
- "mood": "expressive",
- },
- "academic": {
- # Warm white; classic navy accent — scholarly standard
- "cover_bg": "#F5F4F0",
- "accent": "#2A436A",
- "accent_lt": "#E6EBF4",
- "text_light": "#FFFFFF",
- "page_bg": "#F5F4F0",
- "dark": "#1A1A28",
- "body_text": "#1E1E2A",
- "muted": "#686877",
- "cover_pattern": "typographic",
- "mood": "scholarly",
- },
- "general": {
- # Dark slate; muted steel accent — neutral, no-nonsense
- "cover_bg": "#1F2329",
- "accent": "#4A6070",
- "accent_lt": "#E6EAEC",
- "text_light": "#EEEBE5",
- "page_bg": "#F8F6F2",
- "dark": "#1A1A1A",
- "body_text": "#2C2C2C",
- "muted": "#888888",
- "cover_pattern": "fullbleed",
- "mood": "neutral",
- },
- # ── Extended types — each uses a distinct new cover pattern ─────────────────
- "minimal": {
- # Warm off-white; dark neutral grey — truly restrained, no color signal
- "cover_bg": "#F7F6F4",
- "accent": "#4A4A4A",
- "accent_lt": "#EBEBEA",
- "text_light": "#F7F6F4",
- "page_bg": "#F7F6F4",
- "dark": "#111111",
- "body_text": "#222222",
- "muted": "#999999",
- "cover_pattern": "minimal",
- "mood": "restrained",
- },
- "stripe": {
- # Near-black; charcoal slate accent — structured, no-nonsense
- "cover_bg": "#1E222A",
- "accent": "#4A5568",
- "accent_lt": "#EAECEE",
- "text_light": "#FFFFFF",
- "page_bg": "#F8F8F7",
- "dark": "#0E1117",
- "body_text": "#262630",
- "muted": "#888898",
- "cover_pattern": "stripe",
- "mood": "bold",
- },
- "diagonal": {
- # Deep navy; muted slate-blue accent — dignified, controlled
- "cover_bg": "#1A2535",
- "accent": "#3D5A72",
- "accent_lt": "#E4EBF0",
- "text_light": "#EEF0F5",
- "page_bg": "#F8FAFC",
- "dark": "#0F1A2A",
- "body_text": "#1E2C3A",
- "muted": "#7A8A96",
- "cover_pattern": "diagonal",
- "mood": "dynamic",
- },
- "frame": {
- # Warm parchment; dark muted brown — classical, formal
- "cover_bg": "#F5F2EC",
- "accent": "#5C4A38",
- "accent_lt": "#EAE5DE",
- "text_light": "#F5F2EC",
- "page_bg": "#F5F2EC",
- "dark": "#2A1E14",
- "body_text": "#2C2018",
- "muted": "#9A8A78",
- "cover_pattern": "frame",
- "mood": "classical",
- },
- "editorial": {
- # White; deep burgundy accent — editorial weight without the shout
- "cover_bg": "#FFFFFF",
- "accent": "#7A2B36",
- "accent_lt": "#EEE4E5",
- "text_light": "#FFFFFF",
- "page_bg": "#FFFFFF",
- "dark": "#0A0A0A",
- "body_text": "#1A1A1A",
- "muted": "#777777",
- "cover_pattern": "editorial",
- "mood": "editorial",
- },
- # ── New patterns (v2) ────────────────────────────────────────────────────────
- "magazine": {
- # Warm linen; deep navy accent — formal publication standard
- "cover_bg": "#F0EEE9",
- "accent": "#1C3557",
- "accent_lt": "#E4EBF3",
- "text_light": "#FFFFFF",
- "page_bg": "#F0EEE9",
- "dark": "#0D1A2B",
- "body_text": "#2A2A2A",
- "muted": "#888888",
- "cover_pattern": "magazine",
- "mood": "magazine",
- },
- "darkroom": {
- # Deep navy; muted steel-blue accent — premium, controlled
- "cover_bg": "#151C27",
- "accent": "#3D5A7A",
- "accent_lt": "#E2EBF2",
- "text_light": "#EDE9E2",
- "page_bg": "#F7F7F5",
- "dark": "#0A1018",
- "body_text": "#2C2C2C",
- "muted": "#8A9AB0",
- "cover_pattern": "darkroom",
- "mood": "darkroom",
- },
- "terminal": {
- # Near-black; forest green accent — technical, serious (not neon)
- "cover_bg": "#0D1117",
- "accent": "#3D7A5C",
- "accent_lt": "#E2EEE8",
- "text_light": "#E6EDF3",
- "page_bg": "#F8F8F6",
- "dark": "#010409",
- "body_text": "#2C2C2C",
- "muted": "#5A7A6A",
- "cover_pattern": "terminal",
- "mood": "terminal",
- },
- "poster": {
- # White; near-black accent sidebar — stark, unambiguous
- "cover_bg": "#FFFFFF",
- "accent": "#0A0A0A",
- "accent_lt": "#EBEBEA",
- "text_light": "#FFFFFF",
- "page_bg": "#FFFFFF",
- "dark": "#0A0A0A",
- "body_text": "#1A1A1A",
- "muted": "#888888",
- "cover_pattern": "poster",
- "mood": "poster",
- },
- }
- # ── Font pairs — CSS names for cover HTML, ReportLab names for body ─────────────
- # cover uses Google Fonts via @import (no local disk caching needed)
- # body always uses system fonts via ReportLab
- FONT_PAIRS = {
- "authoritative": {
- "display_css": "Playfair Display",
- "body_css": "IBM Plex Sans",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=IBM+Plex+Sans:ital,wght@0,400;0,600;1,400&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "confident": {
- "display_css": "Syne",
- "body_css": "Nunito Sans",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=Syne:wght@600;800&family=Nunito+Sans:wght@400;600;700&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "clean": {
- "display_css": "DM Serif Display",
- "body_css": "DM Sans",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "expressive": {
- "display_css": "Fraunces",
- "body_css": "Inter",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,700;0,900;1,900&family=Inter:wght@300;400;500&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "scholarly": {
- "display_css": "EB Garamond",
- "body_css": "Source Sans 3",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,700;1,400&family=Source+Sans+3:wght@400;600&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "neutral": {
- "display_css": "Outfit",
- "body_css": "Outfit",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;700;900&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "restrained": {
- "display_css": "Cormorant Garamond",
- "body_css": "Jost",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,600;1,300&family=Jost:wght@300;400;500&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "bold": {
- "display_css": "Barlow Condensed",
- "body_css": "Barlow",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;900&family=Barlow:wght@400;500;600&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "dynamic": {
- "display_css": "Montserrat",
- "body_css": "Montserrat",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,700;0,900;1,400&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "classical": {
- "display_css": "Cormorant",
- "body_css": "Crimson Pro",
- "gfonts_import": "https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,400;0,700;1,400&family=Crimson+Pro:wght@400;600&display=swap",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "editorial": {
- "display_css": "Bebas Neue",
- "body_css": "Libre Franklin",
- "gfonts_import": (
- "https://fonts.googleapis.com/css2?family=Bebas+Neue"
- "&family=Libre+Franklin:ital,wght@0,400;0,700;1,400&display=swap"
- ),
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- # ── New moods (v2) ───────────────────────────────────────────────────────────
- "magazine": {
- "display_css": "Playfair Display",
- "body_css": "EB Garamond",
- "gfonts_import": (
- "https://fonts.googleapis.com/css2?family=Playfair+Display"
- ":ital,wght@0,700;0,900;1,700"
- "&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap"
- ),
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "darkroom": {
- "display_css": "Playfair Display",
- "body_css": "EB Garamond",
- "gfonts_import": (
- "https://fonts.googleapis.com/css2?family=Playfair+Display"
- ":ital,wght@0,700;0,900;1,700"
- "&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap"
- ),
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- },
- "terminal": {
- "display_css": "Space Mono",
- "body_css": "Space Mono",
- "gfonts_import": (
- "https://fonts.googleapis.com/css2?family=Space+Mono"
- ":ital,wght@0,400;0,700;1,400&display=swap"
- ),
- "display_rl": "Courier-Bold",
- "body_rl": "Courier",
- "body_b_rl": "Courier-Bold",
- },
- "poster": {
- "display_css": "Barlow Condensed",
- "body_css": "Courier Prime",
- "gfonts_import": (
- "https://fonts.googleapis.com/css2?family=Barlow+Condensed"
- ":wght@700;900"
- "&family=Courier+Prime:ital,wght@0,400;0,700;1,400&display=swap"
- ),
- "display_rl": "Times-Bold",
- "body_rl": "Courier",
- "body_b_rl": "Courier-Bold",
- },
- }
- SYSTEM_FALLBACK = {
- "display_css": "Georgia",
- "body_css": "Arial",
- "gfonts_import": "",
- "display_rl": "Times-Bold",
- "body_rl": "Helvetica",
- "body_b_rl": "Helvetica-Bold",
- }
- # ── Colour helpers ──────────────────────────────────────────────────────────────
- def _hex_to_rgb(h: str) -> tuple:
- h = h.lstrip("#")
- return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
- def _lighten(hex_color: str, factor: float = 0.09) -> str:
- """Blend hex_color toward white (factor = accent weight, 0=white, 1=full color)."""
- r, g, b = _hex_to_rgb(hex_color)
- return "#{:02X}{:02X}{:02X}".format(
- round(r * factor + 255 * (1 - factor)),
- round(g * factor + 255 * (1 - factor)),
- round(b * factor + 255 * (1 - factor)),
- )
- # ── Token assembly ─────────────────────────────────────────────────────────────
- def build_tokens(
- title: str,
- doc_type: str,
- author: str = "",
- date: str = "",
- accent_override: str = "",
- cover_bg_override: str = "",
- ) -> dict:
- palette = PALETTES.get(doc_type, PALETTES["general"]).copy()
- mood = palette["mood"]
- font_pair = FONT_PAIRS.get(mood, SYSTEM_FALLBACK)
- # Apply caller-supplied overrides before token assembly
- if accent_override:
- palette["accent"] = accent_override
- palette["accent_lt"] = _lighten(accent_override, 0.09)
- if cover_bg_override:
- palette["cover_bg"] = cover_bg_override
- tokens = {
- # Identity
- "title": title,
- "author": author,
- "date": date,
- "doc_type": doc_type,
- # Palette
- "cover_bg": palette["cover_bg"],
- "accent": palette["accent"],
- "accent_lt": palette["accent_lt"],
- "text_light": palette["text_light"],
- "page_bg": palette["page_bg"],
- "dark": palette["dark"],
- "body_text": palette["body_text"],
- "muted": palette["muted"],
- "cover_pattern": palette["cover_pattern"],
- "mood": mood,
- # Typography — CSS names for cover HTML (loaded via Google Fonts @import)
- "font_display": font_pair["display_css"],
- "font_body": font_pair["body_css"],
- "gfonts_import": font_pair["gfonts_import"],
- # Typography — ReportLab system font names for body pages
- "font_display_rl": font_pair["display_rl"],
- "font_body_rl": font_pair["body_rl"],
- "font_body_b_rl": font_pair["body_b_rl"],
- # Legacy keys (kept so render_body.py's register_fonts is a no-op)
- "font_heading": font_pair["display_rl"],
- "font_body_b": font_pair["body_b_rl"],
- "font_paths": {},
- # Type scale (pt)
- "size_display": 54,
- "size_h1": 22,
- "size_h2": 15,
- "size_h3": 11.5,
- "size_body": 10.5,
- "size_caption": 8.5,
- "size_meta": 8,
- # Layout (pt, 1cm ≈ 28.35pt)
- "margin_left": 79, # 2.8cm
- "margin_right": 79,
- "margin_top": 79,
- "margin_bottom": 71, # 2.5cm
- "section_gap": 26,
- "para_gap": 8,
- "line_gap": 17,
- }
- return tokens
- # ── CLI ───────────────────────────────────────────────────────────────────────
- def main():
- parser = argparse.ArgumentParser(description="Generate design tokens from document metadata")
- parser.add_argument("--title", default="Untitled Document")
- parser.add_argument("--type", default="general",
- choices=list(PALETTES.keys()),
- help="Document type: " + ", ".join(PALETTES.keys()))
- parser.add_argument("--author", default="")
- parser.add_argument("--date", default="")
- parser.add_argument("--meta", help="JSON file with title/type/author/date keys")
- parser.add_argument("--accent", default="",
- help="Override accent colour (hex, e.g. #2D6A8F). "
- "accent_lt is auto-derived by lightening toward white.")
- parser.add_argument("--cover-bg", default="",
- help="Override cover background colour (hex).")
- parser.add_argument("--out", default="tokens.json")
- args = parser.parse_args()
- if args.meta:
- try:
- with open(args.meta) as f:
- meta = json.load(f)
- args.title = meta.get("title", args.title)
- args.type = meta.get("type", args.type)
- args.author = meta.get("author", args.author)
- args.date = meta.get("date", args.date)
- except Exception as e:
- print(json.dumps({"status": "error", "error": str(e)}), file=sys.stderr)
- sys.exit(1)
- tokens = build_tokens(
- args.title, args.type, args.author, args.date,
- accent_override=args.accent,
- cover_bg_override=getattr(args, "cover_bg", ""),
- )
- try:
- with open(args.out, "w") as f:
- json.dump(tokens, f, indent=2)
- except Exception as e:
- print(json.dumps({"status": "error", "error": str(e)}), file=sys.stderr)
- sys.exit(3)
- print(json.dumps({
- "status": "ok",
- "out": args.out,
- "mood": tokens["mood"],
- "pattern": tokens["cover_pattern"],
- "fonts": f'{tokens["font_display"]} / {tokens["font_body"]}',
- }))
- if __name__ == "__main__":
- main()
|