living-pipeline-bprize2026/scribus-poster-script.py

510 lines
20 KiB
Python

#!/usr/bin/env python3
"""
Scribus script: Generate the B-Prize 2026 "Living Pipeline" A3 poster.
Creates a landscape A3 (420x297mm) document with all content.
Run via wrapper that sets sys.argv before exec.
"""
import sys
import os
def parse_args():
args = {}
argv = sys.argv[1:]
i = 0
while i < len(argv):
if argv[i] == "--output" and i + 1 < len(argv):
args["output"] = argv[i + 1]
i += 2
elif argv[i] == "--dpi" and i + 1 < len(argv):
args["dpi"] = int(argv[i + 1])
i += 2
else:
i += 1
return args
def define_colors(scribus):
scribus.defineColorRGB("DarkNavy", 12, 35, 64)
scribus.defineColorRGB("DeepTeal", 27, 79, 114)
scribus.defineColorRGB("ForestGreen", 26, 107, 74)
scribus.defineColorRGB("BrightGreen", 39, 174, 96)
scribus.defineColorRGB("LightGreen", 213, 245, 227)
scribus.defineColorRGB("AlertRed", 192, 57, 43)
scribus.defineColorRGB("BrightRed", 231, 76, 60)
scribus.defineColorRGB("AccentBlue", 52, 152, 219)
scribus.defineColorRGB("LightBlue", 212, 230, 241)
scribus.defineColorRGB("DarkText", 26, 35, 50)
scribus.defineColorRGB("MidGray", 100, 100, 100)
scribus.defineColorRGB("LightGray", 230, 230, 230)
scribus.defineColorRGB("VLightGray", 245, 245, 245)
scribus.defineColorRGB("PageBG", 250, 251, 247)
scribus.defineColorRGB("CardBG", 255, 255, 255)
scribus.defineColorRGB("EarthBrown", 139, 69, 19)
scribus.defineColorRGB("Purple", 142, 68, 173)
scribus.defineColorRGB("WarmBG", 253, 245, 237)
scribus.defineColorRGB("CoolBG", 236, 247, 236)
# Track unique name counter to avoid collisions
_name_counter = [0]
def uname(prefix="obj"):
_name_counter[0] += 1
return f"{prefix}_{_name_counter[0]}"
def rect(s, x, y, w, h, fill, line_color="None", line_w=0):
n = s.createRect(x, y, w, h, uname("r"))
s.setFillColor(fill, n)
if line_color != "None":
s.setLineColor(line_color, n)
s.setLineWidth(line_w, n)
else:
s.setLineColor("None", n)
s.setLineWidth(0, n)
return n
def txt(s, x, y, w, h, text, font="DejaVu Sans", size=10, color="DarkText", align=0):
n = s.createText(x, y, w, h, uname("t"))
s.setText(text, n)
try:
s.setFont(font, n)
except:
pass
s.setFontSize(size, n)
s.setTextColor(color, n)
s.setTextAlignment(align, n)
return n
def hline(s, x, y, length, color="LightGray", width=0.5):
n = s.createLine(x, y, x + length, y, uname("l"))
s.setLineColor(color, n)
s.setLineWidth(width, n)
return n
def vline(s, x1, y1, x2, y2, color="BrightGreen", width=0.75):
n = s.createLine(x1, y1, x2, y2, uname("vl"))
s.setLineColor(color, n)
s.setLineWidth(width, n)
return n
def main():
try:
import scribus
except ImportError:
print("ERROR: Must run inside Scribus")
sys.exit(1)
args = parse_args()
output_path = args.get("output", "/app/output/bprize-poster.pdf")
dpi = args.get("dpi", 300)
s = scribus # shorthand
# ═══ PAGE SETUP ═══
# A3 Landscape: pass dimensions as landscape directly
PW = 420.0
PH = 297.0
s.newDocument(
(PW, PH), # Already landscape dimensions
(6, 6, 6, 6),
s.PORTRAIT, # Don't double-swap with LANDSCAPE flag
1, s.UNIT_MILLIMETERS, s.PAGE_1, 0, 1,
)
define_colors(s)
# ═══ LAYOUT GRID ═══
HEADER_H = 26.0
GAP = 1.5
MARGIN = 4.0
COL_TOP = HEADER_H + GAP
COL_H = PH - COL_TOP - MARGIN
# Three columns with gaps
TOTAL_W = PW - MARGIN * 2
COL1_W = TOTAL_W * 0.30
COL2_W = TOTAL_W * 0.37
COL3_W = TOTAL_W * 0.33
COL1_X = MARGIN
COL2_X = COL1_X + COL1_W + GAP
COL3_X = COL2_X + COL2_W + GAP
B = "DejaVu Sans Bold"
R = "DejaVu Sans"
I = "DejaVu Sans Oblique"
# ═══════════════════════════════════════
# HEADER
# ═══════════════════════════════════════
rect(s, 0, 0, PW, HEADER_H, "DarkNavy")
rect(s, 0, HEADER_H - 0.8, PW, 0.8, "BrightGreen")
txt(s, 10, 2.5, 280, 11, "THE LIVING PIPELINE", B, 26, "White")
txt(s, 10, 13, 310, 5,
"A mycorrhizal network model for distributed water supply along the Collingwood\u2013Alliston corridor",
R, 8.5, "White")
txt(s, 10, 19, 310, 5,
"Instead of one $270M pipe, what if the landscape itself became the water system?",
I, 8, "White")
txt(s, PW - 80, 4, 74, 18,
"Biomimicry Commons\nB-Prize 2026\nCollingwood\u2013Alliston Corridor\nOntario, Canada",
R, 7, "White", 2)
# ═══════════════════════════════════════
# COLUMN BACKGROUNDS
# ═══════════════════════════════════════
rect(s, COL1_X, COL_TOP, COL1_W, COL_H, "PageBG")
rect(s, COL2_X, COL_TOP, COL2_W, COL_H, "CardBG")
rect(s, COL3_X, COL_TOP, COL3_W, COL_H, "PageBG")
# ═══════════════════════════════════════
# COLUMN 1 — THE PROBLEM
# ═══════════════════════════════════════
cx = COL1_X + 4
cw = COL1_W - 8
cy = COL_TOP + 3
txt(s, cx, cy, cw, 5, "THE CHALLENGE", B, 9.5, "AlertRed")
hline(s, cx, cy + 5, cw, "BrightRed", 0.6)
cy += 7
txt(s, cx, cy, cw, 24,
"In September 2023, the cost to expand Collingwood\u2019s Raymond A. Barker Water Treatment Plant doubled \u2014 from $121M to $270M \u2014 to pump Georgian Bay water 53 km uphill to Alliston via a single 600mm pipeline following the historic 1852 railway corridor.\n\nOne pipe. Five towns. Zero redundancy. A break at km 30 cuts off everyone downstream.",
R, 7.5, "DarkText")
cy += 26
# Stats row
sw = (cw - 4) / 3
stats = [("$270M", "Expansion cost"), ("53 km", "Single pipeline"), ("5", "Towns dependent")]
for i, (val, lab) in enumerate(stats):
sx = cx + i * (sw + 2)
rect(s, sx, cy, sw, 13, "CardBG", "LightGray", 0.25)
txt(s, sx, cy + 1, sw, 6, val, B, 13, "AlertRed", 1)
txt(s, sx, cy + 7.5, sw, 5, lab, R, 5, "MidGray", 1)
cy += 16
# Community table
txt(s, cx, cy, cw, 3.5, "COMMUNITY STATUS", B, 6.5, "MidGray")
cy += 4.5
rect(s, cx, cy, cw, 4, "DarkNavy")
txt(s, cx + 1, cy + 0.7, 30, 3, "Community", B, 5, "White")
txt(s, cx + 32, cy + 0.7, 38, 3, "Source", B, 5, "White")
txt(s, cx + 71, cy + 0.7, cw - 72, 3, "Status", B, 5, "White")
cy += 4.5
rows = [
("Collingwood", "Georgian Bay WTP", "$270M expansion", "MidGray"),
("Stayner", "4 groundwater wells", "AT CAPACITY", "AlertRed"),
("Angus", "6 wells + pipeline", "DEV FROZEN", "AlertRed"),
("Alliston", "Pipeline + wells", "6,400 HOMES REJECTED", "AlertRed"),
("Blue Mountains", "Pipeline only", "1,250 m\u00b3/day", "MidGray"),
]
for i, (c, src, st, sc) in enumerate(rows):
bg = "CardBG" if i % 2 == 0 else "VLightGray"
rect(s, cx, cy, cw, 3.8, bg)
txt(s, cx + 1, cy + 0.5, 30, 3, c, B, 5.5, "DarkText")
txt(s, cx + 32, cy + 0.5, 38, 3, src, R, 5, "MidGray")
txt(s, cx + 71, cy + 0.5, cw - 72, 3, st, B, 4.5, sc)
cy += 4
cy += 4
# Nature's Model
txt(s, cx, cy, cw, 4.5, "NATURE\u2019S MODEL", B, 9, "ForestGreen")
hline(s, cx, cy + 5, cw, "BrightGreen", 0.5)
cy += 7
bw = (cw - 3) / 2
# Problem box
rect(s, cx, cy, bw, 36, "WarmBG", "AlertRed", 0.3)
txt(s, cx + 2, cy + 2, bw - 4, 4, "CURRENT SYSTEM", B, 6.5, "AlertRed", 1)
txt(s, cx + 2, cy + 8, bw - 4, 26,
"One trunk, one root.\nCut the trunk \u2192 all die.\n\nSingle point of failure.\nNo redundancy.\nNo local capacity.\n\n53 km of pumping\nuphill from Georgian Bay.",
R, 6.5, "DarkText", 1)
# Solution box
sx = cx + bw + 3
rect(s, sx, cy, bw, 36, "CoolBG", "BrightGreen", 0.3)
txt(s, sx + 2, cy + 2, bw - 4, 4, "MYCORRHIZAL FOREST", B, 6.5, "ForestGreen", 1)
txt(s, sx + 2, cy + 8, bw - 4, 26,
"Many roots, connected\nunderground. Resources\nflow to where needed.\n\nHub trees share water\nvia fungal networks.\nModular = resilient.\n\nNo single point of failure.",
R, 6.5, "DarkText", 1)
cy += 39
# Principle box
rect(s, cx, cy, cw, 28, "CoolBG")
rect(s, cx, cy, 1, 28, "BrightGreen")
txt(s, cx + 3, cy + 2, cw - 5, 24,
"In Ontario forests, 90% of rainfall events produce zero runoff. Every point along water\u2019s journey is a collection point, a storage node, and a treatment system. There is no \u201cend of pipe.\u201d\n\nDecentralized modular networks improve infrastructure resilience by a minimum of 3\u00d7 (Springer, 2024). Hydraulic redistribution through fungal hyphae increases shallow soil water by 28\u2013102% (Egerton-Warburton et al., J. Exp. Botany).\n\nDesign principle: Distribute collection, treatment, and storage across the network. Every node both gives and receives. Use the landscape as infrastructure.",
I, 6.5, "DarkText")
# ═══════════════════════════════════════
# COLUMN 2 — THE SOLUTION
# ═══════════════════════════════════════
cx = COL2_X + 4
cw = COL2_W - 8
cy = COL_TOP + 3
txt(s, cx, cy, cw, 5, "THE LIVING PIPELINE", B, 9.5, "ForestGreen")
hline(s, cx, cy + 5, cw, "BrightGreen", 0.6)
cy += 7
txt(s, cx, cy, cw, 9,
"Instead of $270M for one bigger plant, distribute capacity across the corridor \u2014 turning the landscape into a living water system where each community both gives and receives.",
R, 7, "DarkText")
cy += 11
# Solution Map
map_h = 58
rect(s, cx, cy, cw, map_h, "LightBlue", "AccentBlue", 0.25)
# Bay label
txt(s, cx + 2, cy + 2, 30, 3.5, "Georgian Bay", B, 6, "DeepTeal")
# Backbone line
mx = cx + cw * 0.42
vline(s, mx, cy + 8, mx, cy + map_h - 6, "BrightGreen", 0.75)
# Nodes
nodes = [
(cy + 10, "Collingwood WTP", "(reduced expansion)", True),
(cy + 22, "Stayner Node", "3,000 m\u00b3/day", False),
(cy + 34, "Angus Node", "5,000 m\u00b3/day", True),
(cy + 46, "Alliston Node", "3,000 m\u00b3/day", False),
]
for ny, label, sub, right_side in nodes:
rect(s, mx - 2, ny - 2, 4, 4, "BrightGreen", "White", 0.3)
lx = mx + 6 if right_side else cx + 3
txt(s, lx, ny - 2, 55, 7, f"{label}\n{sub}", R, 5.5, "DarkText")
# Flow arrows
for ay in [cy + 17, cy + 29, cy + 41]:
txt(s, mx + 3, ay, 6, 4, "\u2195", B, 7, "BrightGreen")
# MAR zone
rect(s, cx + cw - 40, cy + map_h - 20, 37, 16, "LightBlue", "AccentBlue", 0.2)
txt(s, cx + cw - 39, cy + map_h - 18, 35, 12, "MAR Zone\nAlliston Sand Plain", B, 5.5, "DeepTeal", 1)
# Wetland patches
for wy in [cy + 18, cy + 30, cy + 42]:
rect(s, cx + 2, wy, 22, 7, "LightGreen", "BrightGreen", 0.2)
txt(s, cx + 3, wy + 1.5, 20, 4, "Wetland", R, 5, "ForestGreen", 1)
# Legend
txt(s, cx + 2, cy + map_h - 5, cw - 4, 3.5,
"\u25cf Treatment Node \u25a2 MAR Zone \u25a2 Constructed Wetland \u2195 Bidirectional Flow",
R, 4.5, "MidGray")
cy += map_h + 3
# Strategy Cards
strategies = [
("1", "SATELLITE TREATMENT NODES", "EarthBrown",
"Each tree\u2019s own root system",
"3\u20134 modular membrane + UV units at existing well sites. Canadian manufacturers (H2O Innovation, Trojan Technologies). $2\u20138M each, deployable in 12\u201324 months. Reduces pipeline demand by 30\u201350%. Capacity tracks demand \u2014 no $270M upfront commitment."),
("2", "MANAGED AQUIFER RECHARGE", "AccentBlue",
"Forest floor + beaver dam storage",
"Alliston Sand Plain \u2014 Ontario\u2019s best MAR candidate (CFB Borden, one of the world\u2019s most studied aquifer sites). Infiltration basins (May\u2013Nov) + ASR wells (year-round). 1 ha of basin = water for 15\u201320K people. Precedent: Turku, Finland serves 300K on identical glaciofluvial geology."),
("3", "CONSTRUCTED TREATMENT WETLANDS", "BrightGreen",
"Riparian buffer zones",
"Hybrid subsurface-flow wetlands proven in Ontario winters (Fleming College CAWT, Lindsay ON). O&M costs 75% cheaper than conventional mechanical treatment. Non-potable reuse of treated greywater cuts potable demand 30\u201340% per household. Creates habitat corridors along rail trail."),
("4", "MYCORRHIZAL BACKBONE", "Purple",
"Common mycorrhizal network",
"Existing 600mm pipeline becomes smart balancing network \u2014 SCADA/IoT sensors, bidirectional flow, real-time optimization. Any node can supply neighbours during shortage. Precedent: SEQ Water Grid (Australia) \u2014 12 dams + 5 plants managed as one distributed system."),
]
card_h = 27
for num, title, color, bio, desc in strategies:
rect(s, cx, cy, cw, card_h, "CardBG", "LightGray", 0.15)
rect(s, cx, cy, 1.2, card_h, color)
# Number
rect(s, cx + 3, cy + 1.5, 5, 5, color)
txt(s, cx + 3, cy + 1.8, 5, 4, num, B, 7, "White", 1)
# Title
txt(s, cx + 10, cy + 1.5, cw - 14, 4, title, B, 7, "DarkText")
# Bio
txt(s, cx + 10, cy + 5.5, cw - 14, 3, f"Biomimicry: {bio}", I, 5, "MidGray")
# Desc
txt(s, cx + 3, cy + 9.5, cw - 6, 13, desc, R, 6, "DarkText")
cy += card_h + 1.5
# ═══════════════════════════════════════
# COLUMN 3 — FEASIBILITY
# ═══════════════════════════════════════
cx = COL3_X + 4
cw = COL3_W - 8
cy = COL_TOP + 3
txt(s, cx, cy, cw, 5, "FEASIBILITY & IMPACT", B, 9.5, "DeepTeal")
hline(s, cx, cy + 5, cw, "AccentBlue", 0.6)
cy += 7.5
# ── Cost Bars ──
txt(s, cx, cy, cw, 3, "FINANCIAL COMPARISON", B, 6.5, "MidGray")
cy += 4
bar_w = (cw - 16) / 2
bar_base_y = cy + 32
# Red bar
bh_red = 28
rect(s, cx + 2, bar_base_y - bh_red, bar_w, bh_red, "BrightRed")
txt(s, cx + 2, bar_base_y - bh_red + 4, bar_w, 8, "$270M", B, 14, "White", 1)
txt(s, cx + 2, bar_base_y + 1, bar_w, 5, "Centralized\n(Status Quo)", B, 5.5, "MidGray", 1)
# Green bar
bh_green = 16
gx = cx + bar_w + 14
rect(s, gx, bar_base_y - bh_green, bar_w, bh_green, "BrightGreen")
txt(s, gx, bar_base_y - bh_green + 2, bar_w, 7, "$118\u2013170M", B, 10, "White", 1)
txt(s, gx, bar_base_y + 1, bar_w, 5, "Living Pipeline\n(Distributed)", B, 5.5, "MidGray", 1)
cy = bar_base_y + 8
# Cost breakdown
cost_items = [
("WTP expansion (smaller Phase 1)", "$80\u2013100M"),
("Satellite nodes (3\u20134)", "$15\u201330M"),
("MAR infrastructure", "$8\u201315M"),
("Constructed wetlands (4 sites)", "$12\u201320M"),
("Smart network integration", "$3\u20135M"),
]
rect(s, cx, cy, cw, 3.5, "DarkNavy")
txt(s, cx + 1, cy + 0.5, cw * 0.6, 2.5, "Component", B, 5, "White")
txt(s, cx + cw * 0.6, cy + 0.5, cw * 0.4 - 1, 2.5, "Cost (CAD)", B, 5, "White", 2)
cy += 4
for i, (comp, cost) in enumerate(cost_items):
bg = "CardBG" if i % 2 == 0 else "VLightGray"
rect(s, cx, cy, cw, 3.2, bg)
txt(s, cx + 1, cy + 0.4, cw * 0.6, 2.5, comp, R, 5, "DarkText")
txt(s, cx + cw * 0.6, cy + 0.4, cw * 0.4 - 1, 2.5, cost, R, 5, "DarkText", 2)
cy += 3.3
# Savings row
rect(s, cx, cy, cw, 3.8, "LightGreen")
txt(s, cx + 1, cy + 0.6, cw * 0.5, 3, "SAVINGS", B, 6, "ForestGreen")
txt(s, cx + cw * 0.4, cy + 0.6, cw * 0.6 - 1, 3, "$100\u2013150M (37\u201356%)", B, 6, "ForestGreen", 2)
cy += 6
# ── Timeline ──
txt(s, cx, cy, cw, 3, "TIMELINE ADVANTAGE", B, 6.5, "MidGray")
cy += 4
# Centralized bar
txt(s, cx, cy, 22, 3, "Centralized", B, 5, "MidGray")
tbar_x = cx + 23
tbar_w = cw - 23
rect(s, tbar_x, cy, tbar_w, 3.5, "LightGray")
rect(s, tbar_x + tbar_w * 0.5, cy, tbar_w * 0.5, 3.5, "BrightRed")
txt(s, tbar_x, cy + 0.4, tbar_w, 2.5, "First water: 2029", B, 5, "White", 2)
cy += 4.5
# Living Pipeline bar
txt(s, cx, cy, 22, 3, "Living Pipeline", B, 5, "MidGray")
rect(s, tbar_x, cy, tbar_w, 3.5, "LightGray")
rect(s, tbar_x + tbar_w * 0.15, cy, tbar_w * 0.45, 3.5, "BrightGreen")
txt(s, tbar_x, cy + 0.4, tbar_w, 2.5, "First water: 2027", B, 5, "White", 2)
cy += 4.5
txt(s, cx, cy, cw, 3,
"\u25b2 2 years faster \u2014 unblocks ~3,000\u20135,000 housing units sooner",
B, 5.5, "BrightGreen", 1)
cy += 5
# ── Resilience ──
txt(s, cx, cy, cw, 3, "RESILIENCE", B, 6.5, "MidGray")
cy += 4
rc = [cw * 0.24, cw * 0.38, cw * 0.38]
rect(s, cx, cy, cw, 3.5, "DarkNavy")
txt(s, cx + 1, cy + 0.5, rc[0], 2.5, "Risk", B, 4.5, "White")
txt(s, cx + rc[0], cy + 0.5, rc[1], 2.5, "Centralized", B, 4.5, "White")
txt(s, cx + rc[0] + rc[1], cy + 0.5, rc[2], 2.5, "Living Pipeline", B, 4.5, "White")
cy += 4
res = [
("WTP failure", "All towns lose supply", "One node; others compensate"),
("Pipeline break", "Downstream cut off", "Nodes self-sufficient"),
("Drought", "Entire system stressed", "Aquifers buffer demand"),
("Cost escalation", "$121M\u2192$270M (+123%)", "Phased, no mega-risk"),
]
for i, (risk, cent, liv) in enumerate(res):
bg = "CardBG" if i % 2 == 0 else "VLightGray"
rect(s, cx, cy, cw, 3.5, bg)
txt(s, cx + 1, cy + 0.4, rc[0] - 1, 2.5, risk, R, 4.5, "DarkText")
txt(s, cx + rc[0], cy + 0.4, rc[1] - 1, 2.5, cent, R, 4.5, "AlertRed")
txt(s, cx + rc[0] + rc[1], cy + 0.4, rc[2] - 1, 2.5, liv, R, 4.5, "BrightGreen")
cy += 3.8
cy += 3
# ── Co-Benefits ──
txt(s, cx, cy, cw, 3, "CO-BENEFITS", B, 6.5, "MidGray")
cy += 4
coben = [
("Ecological:", "10\u201320 ha new habitat along rail corridor, integrating with NVCA restoration (78K trees, 2024)"),
("Economic:", "Unblocks development 2+ years sooner. 3,000 homes \u00d7 $400K = $1.2B housing construction"),
("Indigenous:", "Working with the watershed aligns with Saugeen Ojibway Nation water stewardship principles"),
("Energy:", "Local treatment uses 40\u201355% less energy than pumping 53 km. Savings ~$90\u2013130K/node/year"),
]
for label, text in coben:
txt(s, cx, cy, cw, 5,
f"{label} {text}", R, 5, "DarkText")
cy += 5.5
cy += 2
# ── Biomimicry Spiral ──
txt(s, cx, cy, cw, 3, "BIOMIMICRY DESIGN SPIRAL", B, 6.5, "MidGray")
cy += 4
steps = [
("Define", "Supply 5 towns\ncost-effectively"),
("Biologize", "How does nature\ndistribute?"),
("Discover", "Mycorrhizal nets,\nbeavers, wetlands"),
("Abstract", "Distributed nodes,\nlandscape as infra"),
("Emulate", "Satellite plants,\nMAR, backbone"),
("Evaluate", "37\u201356% savings,\n3\u00d7 resilience"),
]
stw = (cw - 5) / 6
for i, (title, desc) in enumerate(steps):
sx = cx + i * (stw + 1)
rect(s, sx, cy, stw, 11, "CardBG", "LightGray", 0.15)
txt(s, sx, cy + 0.5, stw, 2.5, title, B, 4.5, "ForestGreen", 1)
txt(s, sx, cy + 3.5, stw, 7, desc, R, 4, "MidGray", 1)
cy += 14
# ── Sources ──
hline(s, cx, cy, cw, "LightGray", 0.3)
cy += 1.5
txt(s, cx, cy, cw, 12,
"Key Sources: Collingwood WTP Class EA (2022); NVCA IWMP (2019); New Tecumseth Master Plan (2016); CFB Borden aquifer studies (U of Waterloo); Region of Waterloo ASR; Fleming College CAWT; SEQ Water Grid (QLD, Australia); Turku Finland MAR; Egerton-Warburton et al., J. Exp. Botany (2007); Ontario Stormwater Mgmt Manual; Biomimicry Institute Design Spiral; BC Wildlife Federation 10,000 Wetlands.",
R, 4.5, "MidGray")
# ═══ EXPORT ═══
pdf = s.PDFfile()
pdf.file = output_path
pdf.quality = 0
pdf.resolution = dpi
pdf.version = 14
pdf.compress = True
pdf.compressmtd = 0
pdf.save()
s.closeDoc()
print(f"Exported: {output_path}")
if __name__ == "__main__":
main()