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.
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.
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.
media-down-arrow-red, media2-red-square-lower-right,
techpilot-up-arrow-red, fundacc-red-vault-difference, …). Each
chunk preserves the original's exact coordinates, stroke widths, rect
rotations, and mix-blend-mode groups. Chunks are how you get
byte-identical fidelity.line, chevron, arrow, l_corner, diagonal,
rect, triangle, stroked_rect, bracket, stairs, circle,
diamond, letter_F, background, blend.partnership-funding) used for the Fundwiser partnership page.difference, which produces ugly
magenta).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 |
# 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
Three paths, in increasing order of effort:
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.
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.
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.
BACKGROUNDS and ACCENTS at the
top of the script. Introducing new hex values breaks brand consistency.SAFE_COMBOS in the script for the validated list.
Rule of thumb:
luminosity — blue rect over red (as in Alliances, Funding &
Investment) → blue stays blue, slightly darker where it overlaps red.hard-light — sky pale over mixed (as in Market Entry, Tech Pilot,
and the Fundwiser mark) → soft tint.difference — only safe for cyan over red-ish rects (as in Market
Access and Funding Access on their green/navy heroes); produces
ugly magenta when applied over electric blue.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.
# 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).
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.
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).
If you need to tweak the layout, the constants live at the top of the script in a clearly-named block:
CANVAS = (2400, 1350)LOGO_X / LOGO_Y / LOGO_WIDTH — logo placement and sizeTITLE_Y / PILL_Y / DATE_Y — fixed card vertical rangesTITLE_PAD / DATE_PAD / PILL_PAD — per-card paddings (see comments
for which sides are asymmetric and why)PILL_DATE_GAP = 2 — the small gap between the date card's top-right
corner and the pill's bottom-left corner (preserves the visual
"corners-touching" relationship in the originals)TARGET_*_TEXT_HEIGHT — target text heights in pixels (used by the
binary-search font sizer)PILL_PRESETS — the canonical pill coloursRANDOM_POOL — the subset of pill colours used by --pill randomBy 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).