diff --git a/README.md b/README.md index 1b2260a2..75da23d2 100755 --- a/README.md +++ b/README.md @@ -362,7 +362,7 @@ review does not stop while we work that out. This tool is the result of a collaborative effort of many people in the DFIR community. -DLEAPP logo artwork courtesy of Johann Polewczyk, with the per-OS window +The earlier DLEAPP logo artwork was by Johann Polewczyk, with the per-OS window controls (Linux, Windows, macOS) suggested by James Habben. DLEAPP is built on the RLEAPP framework by Alexis Brignoni and contributors. diff --git a/admin/scripts/generate_logo.py b/admin/scripts/generate_logo.py index f791e29a..dd6a2b0c 100644 --- a/admin/scripts/generate_logo.py +++ b/admin/scripts/generate_logo.py @@ -1,173 +1,294 @@ -"""Generate DLEAPP logo/banner assets from the source vector artwork. - -The DLEAPP icon is artwork by Johann Polewczyk: three stacked application -windows with a pointer. James Habben's idea gave each window the window -controls of a different desktop OS, so the icon says "desktop apps, any -platform" at a glance: - - back amber - Linux : circular controls on the left - middle blue - Windows: line glyphs on the right - front light - macOS : traffic lights on the left - -``assets/source/DLEAPP_art.svg`` is the working master (plum tile, OS controls, -dimensional shading). Johann's original flat artwork is kept untouched beside it -as ``DLEAPP_art_original.svg``. This script renders every asset used by the app, -the HTML report and the repo straight from the master vector. - -DLEAPP brand palette (taken from the artwork itself): - tile #5F3A5C plum / aubergine (unclaimed in the LEAPP family) - ink #2A1710 dark outlines - gold #F2B035 primary accent - blue #2A7FD4 secondary accent - grey #D5D9DE neutral / inputs - cream #F7F0E0 light text - -Requires ``rsvg-convert`` (librsvg) for SVG rendering and ``iconutil`` for the -macOS .icns. Run: python admin/scripts/generate_logo.py +"""Render a LEAPP tool's logo assets from its vector logo. + +The same file sits in iLEAPP, ALEAPP, RLEAPP, VLEAPP, DLEAPP, GLEAPP and LAVA. +It finds the repository it runs in, reads that tool's vector master (``svg`` in +TOOLS below) and rewrites every raster the app, its reports and its README use: + + square the tile at a fixed size (README, app window, report, web UI) + banner the tile extended into a rounded plate with the tool name beside + it (GUI header and HTML report header), rendered once at 640 px + tall and scaled to each height + icns the macOS app icon, every size rendered from the vector + ico the Windows icon, every size rendered from the vector + +The plate color is the tile's own fill and its corner radius is the tile's, so +the banner reads as the icon with the name written on it. The name is set in +black to match the artwork's outlines. + +To take a new logo from the designer, import it first: + + python --import path/to/NEW.svg + +An Inkscape export stores the tile as ``rx="0" ry="45.43"``. Inkscape draws +that rounded, but the SVG specification (and so every browser and librsvg) +treats a zero rx as square corners, so the import copies ry into rx. It also +drops images hidden with ``display:none`` (tracing layers), which never render. + +Requires rsvg-convert (librsvg), Pillow, Arial Bold or Helvetica for the name, +and iconutil for the .icns (macOS only). Then run with no arguments: + + python """ +import argparse import os +import re import shutil import subprocess +import sys import tempfile from PIL import Image, ImageDraw, ImageFont -DL = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) -SOURCE_SVG = os.path.join(DL, "assets", "source", "DLEAPP_art.svg") +TOOLS = { + "iLEAPP": { + "marker": "ileappGUI.py", + "svg": "assets/iLEAPP_logo.svg", + "square": {"assets/icon.png": 256, "scripts/_elements/logo.png": 512}, + "banner": {"assets/iLEAPP_logo.png": 51, "scripts/_elements/iLEAPP_banner.png": 80}, + "icns": ["assets/icon.icns"], + "ico": [], + }, + "ALEAPP": { + "marker": "aleappGUI.py", + "svg": "assets/ALEAPP_logo.svg", + "square": {"assets/icon.png": 256, "scripts/_elements/logo.png": 512}, + "banner": {"assets/ALEAPP_logo.png": 51, "scripts/_elements/ALEAPP_banner.png": 80}, + "icns": ["assets/icon.icns"], + "ico": [], + }, + "RLEAPP": { + "marker": "rleappGUI.py", + "svg": "assets/RLEAPP_logo.svg", + "square": {"assets/icon.png": 256, "scripts/_elements/logo.png": 512}, + "banner": {"assets/RLEAPP_logo.png": 51, "scripts/_elements/RLEAPP_banner.png": 70}, + "icns": ["assets/icon.icns"], + "ico": [], + }, + "VLEAPP": { + "marker": "vleappGUI.py", + "svg": "assets/VLEAPP_logo.svg", + "square": {"assets/icon.png": 256, "scripts/_elements/logo.png": 512}, + "banner": {"assets/VLEAPP_logo.png": 51, "scripts/_elements/VLEAPP_banner.png": 80}, + "icns": ["assets/icon.icns"], + "ico": [], + }, + "DLEAPP": { + "marker": "dleappGUI.py", + "svg": "assets/DLEAPP_logo.svg", + "square": {"assets/DLEAPP_logo.png": 1024, "assets/icon.png": 256, + "scripts/_elements/logo.png": 512}, + "banner": {"assets/DLEAPP_banner.png": 640, "scripts/_elements/DLEAPP_banner.png": 640}, + "icns": ["assets/icon.icns"], + "ico": [], + }, + "GLEAPP": { + "marker": "gleapp/__init__.py", + "svg": "docs/images/gleapp-logo.svg", + "square": {"docs/images/gleapp-logo.png": 256, "gleapp/web/static/gleapp-logo.png": 256, + "gleapp/web/static/favicon.png": 64}, + "banner": {}, + "icns": ["packaging/gleapp.icns"], + "ico": ["packaging/gleapp.ico"], + }, + "LAVA": { + "marker": "src/renderer/App.jsx", + "svg": "assets/LAVA_logo.svg", + "square": {"assets/LAVA_icon_1024.png": 1024, "public/LAVA_icon_1024.png": 1024, + "src/renderer/public/LAVA_icon_1024.png": 1024}, + "banner": {}, + "icns": ["assets/lava.icns"], + "ico": ["assets/lava.ico"], + }, +} + +ICNS_SIZES = [16, 32, 128, 256, 512] # each also rendered @2x +ICO_SIZES = [16, 24, 32, 48, 64, 128, 256] +BANNER_DESIGN_H = 640 +INK = (0, 0, 0) +FONTS = ("/System/Library/Fonts/Supplemental/Arial Bold.ttf", + "/Library/Fonts/Arial Bold.ttf", + "/System/Library/Fonts/HelveticaNeue.ttc") + +RECT_RE = re.compile(r"]*>") +IMAGE_RE = re.compile(r"]*?/>", re.S) + + +def repo_root(): + """The first directory above this script that holds a .git entry.""" + here = os.path.dirname(os.path.abspath(__file__)) + while not os.path.exists(os.path.join(here, ".git")): + parent = os.path.dirname(here) + if parent == here: + raise SystemExit("not inside a git checkout") + here = parent + return here + + +def this_tool(root): + """The TOOLS entry whose marker file exists in root.""" + found = [name for name, spec in TOOLS.items() + if os.path.exists(os.path.join(root, spec["marker"]))] + if len(found) != 1: + raise SystemExit(f"cannot tell which tool this repository is: {found or 'none'}") + return found[0] + + +def _attr(tag, name): + m = re.search(rf'\b{name}="([^"]*)"', tag) + return m.group(1) if m else None + + +def view_width(svg): + """The width of the SVG viewBox, in user units.""" + m = re.search(r'viewBox="\s*[-\d.]+\s+[-\d.]+\s+([\d.]+)\s+[\d.]+"', svg) + if not m: + raise SystemExit("the SVG has no viewBox") + return float(m.group(1)) + + +def tile_rect(svg): + """The background tile: the one rect as wide as the viewBox.""" + width = view_width(svg) + tiles = [t for t in RECT_RE.findall(svg) + if _attr(t, "width") and abs(float(_attr(t, "width")) - width) < 0.01] + if len(tiles) != 1: + raise SystemExit(f"expected one full-width tile rect, found {len(tiles)}") + return tiles[0], width + + +def tile_style(svg): + """(fill color, corner radius as a fraction of the side) of the tile.""" + tile, width = tile_rect(svg) + fill = re.search(r"fill:(#[0-9a-fA-F]{6})", tile) + rx, ry = float(_attr(tile, "rx") or 0), float(_attr(tile, "ry") or 0) + if not fill: + raise SystemExit("the tile rect has no fill color") + if rx == 0 or ry == 0: + raise SystemExit("the tile has a zero corner radius and renders square outside " + "Inkscape; bring the file in with --import") + color = tuple(int(fill.group(1)[i:i + 2], 16) for i in (1, 3, 5)) + return color, ry / width + + +def import_svg(src, dest): + """Copy a designer SVG to the master path, rounding the tile and dropping + images hidden with display:none.""" + with open(src, "r", encoding="utf-8") as fh: + svg = fh.read() + tile, _ = tile_rect(svg) + ry = _attr(tile, "ry") + if (_attr(tile, "rx") or "0") in ("0", "0.0") and ry: + fixed = re.sub(r'\brx="[^"]*"', f'rx="{ry}"', tile) if _attr(tile, "rx") \ + else tile.replace("