scripts

Build-time tooling for the Defence Ukraine site. None of these scripts run at request time — they generate assets that get committed to the repo.

Python tooling setup

The generate-event-cover.py script needs Pillow, fonttools, brotli, and cairosvg. They're not in package.json (which is JS-only), so they live in a Python venv at web/.venv/. One-time setup:

cd web python3 -m venv .venv .venv/bin/pip install Pillow fonttools brotli cairosvg numpy

After that, always run the Python scripts via .venv/bin/python3 rather than the system python3. The .venv/ directory is gitignored.

generate-tile.py

Generates "What We Do" style SVG tiles for the Defence Ukraine site — the small geometric tiles used on the homepage (Funding & Investment, Market Entry, Alliances, Tech Deployment) and on the program pages (Tech Pilot, Market Access, Funding Access).

All existing tiles have been reverse-engineered into the script as pixel-faithful clones, so you can re-emit any of them exactly. You can also compose new tiles either from named presets or by hand, using the same chunks and primitives the clones are built from.

What's in the script

Symbolism

Every existing tile has a specific meaning tied to the section it labels. When composing a new tile, the symbolism matters more than the geometry:

Tile / preset Source Means
funding-investment homepage media capital flowing down into a vehicle
market-entry homepage media1 two arrows converging at an entry point
alliances homepage media2 two overlapping squares = collaboration
tech-pilot program 1_1 tech being lifted from a base
market-access program 1_6 right chevron pushing through a market gate
funding-access program 1_4 key channeling funds into a vault
partnership alias → alliances standalone partnership symbol
partnership-funding new composition red alliance square + bold sky-pale F (Fundwiser)
for the Fundwiser × Defence Ukraine partnership page

Usage

# List all presets with their symbolic meaning python3 scripts/generate-tile.py --list-presets

# List the raw-geometry chunks (for custom compositions) python3 scripts/generate-tile.py --list-chunks

# Clone an existing tile (byte-identical to the source SVG) python3 scripts/generate-tile.py --preset alliances --out out.svg

# A program tile — transparent by default (for use on a colored hero) python3 scripts/generate-tile.py --preset tech-pilot --out out.svg

# Force a background fill on a program tile (rare) python3 scripts/generate-tile.py --preset tech-pilot --out out.svg # (tech-pilot is already transparent by default; pass --no-background # on the homepage clones if you need them transparent instead)

# Override the canvas size (default is the preset's native size — # 184 for homepage tiles, 200 for program tiles, 184 for new tiles) python3 scripts/generate-tile.py --preset alliances --size 240 --out big.svg

# Seeded random composition (deterministic per seed) python3 scripts/generate-tile.py --seed 42 --out out.svg

# Fresh random (no seed) — straight to stdout python3 scripts/generate-tile.py

# Drop the dark background fill (for tiles that should overlay a # colored hero section the way program thumbnails do) python3 scripts/generate-tile.py --preset alliances --no-background --out out.svg

Adding a new tile

Three paths, in increasing order of effort:

  1. Just use an existing clone. If one of the seven canonical tiles has the right meaning for your page, use its preset directly. This is the safest option because it guarantees brand consistency.

  2. Compose from chunks in a new preset function. Add a function in generate-tile.py that returns a list of stamped chunks (optionally with translate(...) / scale(...) transforms). This lets you build new compositions from proven visual elements. Look at preset_partnership_funding() for an example — it stamps the red alliance square and adds a bold letter_F on top with a hard-light blend. Register the new preset in the PRESETS dict, add an entry to DESCRIPTIONS with the symbolic meaning, and set the native size in NATIVE_SIZES.

  3. Use the primitives directly. If the shape doesn't exist in the chunk library, compose it from primitives (line, chevron, rect, triangle, letter_F, etc.). The primitives live between PRIMITIVES and COMPOSE in the script and each one has a docstring explaining its arguments and symbolic range.

When you add a new preset, generate it with --out web/images/<name>.svg and reference the committed SVG from your HTML — don't re-run the script at page-render time.

Constraints and style rules


generate-event-cover.py

Generates a Defence Ukraine event-cover composite image: a full-bleed background photograph with the Defence Ukraine logo in the top-left, the event title in a white card middle-left, a coloured location pill below it, and a date label in a white card bottom-left. This is the same layout as the canonical event covers shipped on the homepage and event pages (Lviv IT Arena 2025, Defence Tech Valley 2025, MSPO 2025).

The layout, paddings, font, colours, and per-card positions were reverse-engineered pixel-by-pixel from the existing covers. See the constants block at the top of generate-event-cover.py for the exact measurements and the rationale for each one.

What the script produces

Usage

# Basic — random pill colour (alternates between electric-blue and green # to match the existing event-cover inventory) .venv/bin/python3 scripts/generate-event-cover.py
--base source/your-photo.jpg
--title "Nordic Defence & Resilience Week 2026"
--location "Lviv, Ukraine"
--date "21–27 September"
--out images/your-event-cover.webp

# Force a specific pill colour .venv/bin/python3 scripts/generate-event-cover.py
--base source/photo.jpg
--title "..." --location "Lviv, Ukraine" --date "..."
--pill green
--out images/...webp

# Custom hex pill colour .venv/bin/python3 scripts/generate-event-cover.py
--base source/photo.jpg
--title "..." --location "..." --date "..."
--pill-color "#0F1B4A"
--out images/...webp

# Deterministic random — same seed always produces the same colour .venv/bin/python3 scripts/generate-event-cover.py
--base source/photo.jpg
--title "..." --location "..." --date "..."
--pill random --seed 2026
--out images/...webp

# Apply a dark overlay (0..1 strength) to a too-bright photo so the # white logo + cards have more contrast. ~0.2 is a good starting point # for a daytime exterior shot; the existing covers don't need overlay # because their source photos are already darker. .venv/bin/python3 scripts/generate-event-cover.py
--base source/bright-photo.jpg
--title "..." --location "..." --date "..."
--overlay 0.22
--out images/...webp

# Emit responsive width variants alongside the master output. # Generates -p-500, -p-800, -p-1080, -p-1600, -p-2000 width versions # so the events.html card can use srcset/sizes for responsive loading. .venv/bin/python3 scripts/generate-event-cover.py
--base source/photo.jpg
--title "..." --location "..." --date "..."
--responsive
--out images/your-event-cover.webp

# Most production runs use all of the above together .venv/bin/python3 scripts/generate-event-cover.py
--base source/photo.jpg
--title "..." --location "Lviv, Ukraine" --date "..."
--pill random --seed 2026
--overlay 0.22
--responsive
--out images/your-event-cover.webp

Pill presets:

Preset Hex Where used
electric-blue #0932D9 Lviv IT Arena 2025
green #017440 Defence Tech Valley 2025 (Funding Access program color)
midnight-navy #0F1B4A (available, not yet used on a cover)
red #FF3333 (available, not yet used on a cover)

--pill random picks from electric-blue or green only (the two "house" colors used by the canonical covers).

Picking a base photo

The composition expects:

Existing covers use interior conference shots (audience watching stage). Exterior architectural shots also work — see Nordic Week 2026 which uses a photo of Arena Lviv stadium.

Photo licensing: if your source is CC-licensed, add an attribution note to the page where the cover appears OR to a dedicated credits section. Don't ship Webflow stock or unattributed photos.

Pixel fidelity vs the originals

The script was calibrated against the canonical Lviv IT Arena 2025 and Defence Tech Valley 2025 covers. Verified:

Element Position (x, y range) Verified against
Logo x=60+, y=62..204 (h=142) both originals
Title card x=60+, y=566..747 (h=182) both originals
Pill y=1002..1145 (h=144) both originals
Date card x=60+, y=1146..1289 (h=144) both originals

Title text height ≈ 85 px, date text ≈ 51 px, pill text ≈ 47 px (all measured directly from the originals; the generator binary-searches the font size to match each target height).

Constants worth knowing about

If you need to tweak the layout, the constants live at the top of the script in a clearly-named block:

Where outputs go

By convention, generated event covers go in web/images/ with a clean filename like nordic-defence-resilience-week-2026-cover.webp. Reference them from _data/events.json in the cover_image.url field (use the /images/... path; the localImage filter in eleventy.config will pass it through unchanged because it doesn't match the Webflow CDN pattern).