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

519 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Scribus script: Generate the B-Prize 2026 "Living Pipeline" A3 poster.
Creates a landscape A3 (420x297mm) document with improved design.
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("Navy", 11, 29, 51)
scribus.defineColorRGB("DeepTeal", 20, 83, 105)
scribus.defineColorRGB("Forest", 27, 107, 74)
scribus.defineColorRGB("Green", 34, 168, 97)
scribus.defineColorRGB("LightGreen", 232, 245, 233)
scribus.defineColorRGB("Alert", 198, 40, 40)
scribus.defineColorRGB("AlertLight", 255, 235, 238)
scribus.defineColorRGB("Blue", 46, 134, 193)
scribus.defineColorRGB("LightBlue", 227, 242, 253)
scribus.defineColorRGB("DarkText", 26, 35, 50)
scribus.defineColorRGB("MidText", 84, 110, 122)
scribus.defineColorRGB("LightText", 144, 164, 174)
scribus.defineColorRGB("Border", 224, 224, 224)
scribus.defineColorRGB("Surface", 255, 255, 255)
scribus.defineColorRGB("BG", 248, 249, 250)
scribus.defineColorRGB("Purple", 123, 31, 162)
scribus.defineColorRGB("Earth", 121, 85, 72)
scribus.defineColorRGB("MapBG1", 214, 234, 248)
scribus.defineColorRGB("MapBG2", 212, 239, 223)
scribus.defineColorRGB("BarRed", 239, 83, 80)
scribus.defineColorRGB("BarGreen", 102, 187, 106)
_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, radius=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)
if radius > 0:
s.setCornerRadius(int(radius), 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="Border", 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="Green", 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
# ═══ PAGE SETUP ═══
PW = 420.0
PH = 297.0
s.newDocument(
(PW, PH),
(6, 6, 6, 6),
s.PORTRAIT,
1, s.UNIT_MILLIMETERS, s.PAGE_1, 0, 1,
)
define_colors(s)
# ═══ LAYOUT GRID ═══
HEADER_H = 22.0
FOOTER_H = 6.0
MARGIN = 0.0
COL_TOP = HEADER_H
COL_H = PH - HEADER_H - FOOTER_H
COL1_W = 110.0
COL2_W = 180.0
COL3_W = 130.0
COL1_X = 0
COL2_X = COL1_W
COL3_X = COL1_W + COL2_W
B = "DejaVu Sans Bold"
R = "DejaVu Sans"
I = "DejaVu Sans Oblique"
# ═══════════════════════════════════════
# HEADER
# ═══════════════════════════════════════
rect(s, 0, 0, PW, HEADER_H, "Navy")
# Gradient accent line
rect(s, 0, HEADER_H - 0.7, PW * 0.5, 0.7, "Blue")
rect(s, PW * 0.5, HEADER_H - 0.7, PW * 0.5, 0.7, "Green")
txt(s, 14, 2.5, 250, 8, "The Living Pipeline", B, 22, "White")
txt(s, 14, 10, 270, 4.5,
"A biomimicry-inspired distributed water system for the Collingwood\u2013Alliston corridor",
R, 7.5, "White")
txt(s, 14, 15, 270, 4,
"Instead of one $270M pipe, what if the landscape itself became the water system?",
I, 7, "White")
# Badge
rect(s, PW - 62, 3, 50, 16, "Navy", "White", 0.3)
txt(s, PW - 62, 4, 50, 3, "B-PRIZE 2026", B, 5.5, "White", 1)
txt(s, PW - 62, 8, 50, 4, "Biomimicry Commons", R, 6.5, "White", 1)
txt(s, PW - 62, 13, 50, 3, "Simcoe County, Ontario", R, 4.5, "White", 1)
# ═══════════════════════════════════════
# COLUMN BACKGROUNDS
# ═══════════════════════════════════════
rect(s, COL1_X, COL_TOP, COL1_W, COL_H, "Surface")
rect(s, COL2_X, COL_TOP, COL2_W, COL_H, "BG")
rect(s, COL3_X, COL_TOP, COL3_W, COL_H, "Surface")
# Column separators
vline(s, COL2_X, COL_TOP, COL2_X, PH - FOOTER_H, "Border", 0.25)
vline(s, COL3_X, COL_TOP, COL3_X, PH - FOOTER_H, "Border", 0.25)
# ═══════════════════════════════════════
# COLUMN 1 — THE CHALLENGE
# ═══════════════════════════════════════
cx = COL1_X + 8
cw = COL1_W - 16
cy = COL_TOP + 8
# Section title
txt(s, cx, cy, cw, 4, "THE CHALLENGE", B, 7.5, "Alert")
hline(s, cx, cy + 5, cw, "Alert", 0.5)
cy += 8
# Intro text
txt(s, cx, cy, cw, 18,
"Collingwood\u2019s water treatment plant expansion doubled from $121M to $270M to serve five municipalities along a single 53 km pipeline.\n\nOne pipe. Five towns. Zero redundancy.",
R, 7, "MidText")
cy += 20
# Hero stats
sw = (cw - 6) / 3
stats = [("$270M", "Expansion cost"), ("53 km", "Single pipeline"), ("5", "Towns at risk")]
for i, (val, lab) in enumerate(stats):
sx = cx + i * (sw + 3)
rect(s, sx, cy, sw, 14, "AlertLight", "None", 0, 1.5)
txt(s, sx, cy + 1.5, sw, 7, val, B, 14, "Alert", 1)
txt(s, sx, cy + 9, sw, 4, lab, R, 4.5, "LightText", 1)
cy += 17
# Community table
rect(s, cx, cy, cw, 3.5, "Navy")
txt(s, cx + 1.5, cy + 0.5, 26, 2.5, "Community", B, 4.5, "White")
txt(s, cx + 28, cy + 0.5, 32, 2.5, "Source", B, 4.5, "White")
txt(s, cx + 62, cy + 0.5, cw - 63, 2.5, "Status", B, 4.5, "White")
cy += 4
rows = [
("Collingwood", "Georgian Bay WTP", "$270M expansion", "MidText"),
("Stayner", "4 groundwater wells", "At capacity", "Alert"),
("Angus", "6 wells + pipeline", "Dev frozen", "Alert"),
("Alliston", "Pipeline + wells", "6,400 homes rejected", "Alert"),
("Blue Mountains", "Pipeline", "1,250 m\u00b3/day", "MidText"),
]
for i, (c, src, st, sc) in enumerate(rows):
bg = "Surface" if i % 2 == 0 else "BG"
rect(s, cx, cy, cw, 3.5, bg)
txt(s, cx + 1.5, cy + 0.5, 26, 2.5, c, B, 5, "DarkText")
txt(s, cx + 28, cy + 0.5, 32, 2.5, src, R, 4.5, "MidText")
txt(s, cx + 62, cy + 0.5, cw - 63, 2.5, st, B, 4.5, sc)
cy += 3.7
cy += 5
# Nature's Model header
txt(s, cx, cy, cw, 3, "NATURE\u2019S MODEL", B, 6, "LightText")
cy += 5
# Comparison boxes
bw = (cw - 4) / 2
# Problem box
rect(s, cx, cy, bw, 32, "AlertLight", "None", 0, 2)
txt(s, cx + 2, cy + 2, bw - 4, 3.5, "CURRENT SYSTEM", B, 5.5, "Alert", 1)
txt(s, cx + 2, cy + 8, bw - 4, 22,
"One trunk. One root.\nCut it \u2014 everything dies.\n\nSingle point of failure.\n53 km of pumping uphill.",
R, 6, "MidText", 1)
# Solution box
sx = cx + bw + 4
rect(s, sx, cy, bw, 32, "LightGreen", "None", 0, 2)
txt(s, sx + 2, cy + 2, bw - 4, 3.5, "MYCORRHIZAL FOREST", B, 5.5, "Forest", 1)
txt(s, sx + 2, cy + 8, bw - 4, 22,
"Many roots, connected\nunderground. Resources\nflow where needed.\n\nNo single point of failure.",
R, 6, "MidText", 1)
cy += 35
# Principle callout
rect(s, cx, cy, cw, 22, "LightGreen", "None", 0, 1.5)
rect(s, cx, cy, 0.8, 22, "Green")
txt(s, cx + 3, cy + 2, cw - 5, 16,
"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.\n\nDecentralized networks improve resilience by 3\u00d7 minimum (Springer, 2024).",
I, 6, "DarkText")
# ═══════════════════════════════════════
# COLUMN 2 — THE SOLUTION
# ═══════════════════════════════════════
cx = COL2_X + 10
cw = COL2_W - 20
cy = COL_TOP + 8
txt(s, cx, cy, cw, 4, "THE LIVING PIPELINE", B, 7.5, "Forest")
hline(s, cx, cy + 5, cw, "Green", 0.5)
cy += 8
txt(s, cx, cy, cw, 7,
"Distribute capacity across the corridor \u2014 the landscape becomes a living water system where each community both gives and receives.",
R, 7.5, "DarkText")
cy += 10
# Solution Map
map_h = 68
rect(s, cx, cy, cw, map_h, "LightBlue", "Blue", 0.2, 2)
# Bay label
txt(s, cx + cw/2 - 15, cy + 1.5, 30, 3, "Georgian Bay", B, 5.5, "DeepTeal", 1)
# Backbone line
mx = cx + cw * 0.45
vline(s, mx, cy + 8, mx, cy + map_h - 8, "Green", 0.6)
# Nodes
nodes = [
(cy + 9, "Collingwood WTP", "(reduced expansion)", True),
(cy + 23, "Stayner Node", "3,000 m\u00b3/day", False),
(cy + 39, "Angus Node", "5,000 m\u00b3/day", True),
(cy + 55, "Alliston Node", "3,000 m\u00b3/day", False),
]
for ny, label, sub, right_side in nodes:
rect(s, mx - 2.5, ny - 2.5, 5, 5, "Green", "Surface", 0.3, 2.5)
lx = mx + 6 if right_side else cx + 3
txt(s, lx, ny - 2, 55, 6, f"{label}\n{sub}", R, 5, "DarkText")
# Flow arrows
for ay in [cy + 17, cy + 32, cy + 48]:
txt(s, mx + 3.5, ay, 6, 4, "\u2195", B, 6.5, "Green")
# MAR zone
rect(s, cx + cw - 42, cy + map_h - 22, 38, 17, "LightBlue", "Blue", 0.2, 3)
txt(s, cx + cw - 41, cy + map_h - 20, 36, 12, "MAR Zone\nAlliston Sand Plain", B, 5, "DeepTeal", 1)
# Wetland patches
for wy in [cy + 20, cy + 38]:
rect(s, cx + 2, wy, 24, 8, "LightGreen", "Green", 0.15, 2)
txt(s, cx + 3, wy + 2, 22, 4, "Wetland", R, 4.5, "Forest", 1)
# Legend
txt(s, cx + 2, cy + map_h - 4, cw - 4, 3,
"\u25cf Node \u25a2 MAR Zone \u25a2 Wetland \u2195 Bidirectional",
R, 4, "LightText")
cy += map_h + 4
# Strategy Cards
strategies = [
("1", "Satellite Treatment Nodes", "Earth",
"Each tree\u2019s own root system",
"3\u20134 modular membrane + UV units at existing wells. $2\u20138M each, online in 12\u201324 months. Reduces pipeline demand 30\u201350%."),
("2", "Managed Aquifer Recharge", "Blue",
"Forest floor + beaver dam storage",
"Alliston Sand Plain \u2014 Ontario\u2019s best MAR candidate. 1 ha basin = water for 15\u201320K people. Precedent: Turku, Finland serves 300K on identical geology."),
("3", "Constructed Treatment Wetlands", "Green",
"Riparian buffer zones",
"Subsurface-flow wetlands proven in Ontario winters (Fleming College CAWT). O&M 75% cheaper. Greywater reuse cuts demand 30\u201340%."),
("4", "Mycorrhizal Backbone", "Purple",
"Common mycorrhizal network",
"Existing pipeline becomes smart bidirectional grid \u2014 SCADA/IoT, adaptive routing. Precedent: SEQ Water Grid (Australia) \u2014 12 dams + 5 plants as one system."),
]
card_h = 24
for num, title, color, bio, desc in strategies:
rect(s, cx, cy, cw, card_h, "Surface", "Border", 0.15, 2)
rect(s, cx, cy, 1, card_h, color)
# Number badge
rect(s, cx + 3, cy + 2, 6.5, 6.5, color, "None", 0, 1.5)
txt(s, cx + 3, cy + 2.3, 6.5, 5, num, B, 8, "White", 1)
# Title
txt(s, cx + 12, cy + 2, cw - 16, 4, title, B, 7, "DarkText")
# Bio
txt(s, cx + 12, cy + 6, cw - 16, 3, f"Biomimicry: {bio}", I, 5, "LightText")
# Desc
txt(s, cx + 4, cy + 10, cw - 8, 12, desc, R, 5.5, "MidText")
cy += card_h + 2
# ═══════════════════════════════════════
# COLUMN 3 — FEASIBILITY
# ═══════════════════════════════════════
cx = COL3_X + 8
cw = COL3_W - 16
cy = COL_TOP + 8
txt(s, cx, cy, cw, 4, "FEASIBILITY & IMPACT", B, 7.5, "DeepTeal")
hline(s, cx, cy + 5, cw, "Blue", 0.5)
cy += 8
# ── Cost section ──
txt(s, cx, cy, cw, 3, "CAPITAL COST", B, 5.5, "LightText")
cy += 4
bar_w = (cw - 20) / 2
bar_base_y = cy + 28
# Red bar
bh_red = 24
rect(s, cx + 4, bar_base_y - bh_red, bar_w, bh_red, "BarRed", "None", 0, 1.5)
txt(s, cx + 4, bar_base_y - bh_red + 5, bar_w, 8, "$270M", B, 13, "White", 1)
txt(s, cx + 4, bar_base_y + 1.5, bar_w, 4, "Centralized", B, 5, "LightText", 1)
# Green bar
bh_green = 14
gx = cx + bar_w + 16
rect(s, gx, bar_base_y - bh_green, bar_w, bh_green, "BarGreen", "None", 0, 1.5)
txt(s, gx, bar_base_y - bh_green + 2, bar_w, 6, "$118\u2013170M", B, 9, "White", 1)
txt(s, gx, bar_base_y + 1.5, bar_w, 4, "Living Pipeline", B, 5, "LightText", 1)
cy = bar_base_y + 7
# Savings badge
rect(s, cx + cw/2 - 24, cy, 48, 5.5, "LightGreen", "Green", 0.2, 1)
txt(s, cx + cw/2 - 24, cy + 0.8, 48, 4, "Save $100\u2013150M (37\u201356%)", B, 6, "Forest", 1)
cy += 8
# Cost breakdown table
cost_items = [
("WTP expansion (Phase 1)", "$80\u2013100M"),
("Satellite nodes (3\u20134)", "$15\u201330M"),
("MAR infrastructure", "$8\u201315M"),
("Constructed wetlands (4)", "$12\u201320M"),
("Smart network integration", "$3\u20135M"),
]
rect(s, cx, cy, cw, 3, "Navy")
txt(s, cx + 1, cy + 0.3, cw * 0.6, 2.5, "Component", B, 4.5, "White")
txt(s, cx + cw * 0.6, cy + 0.3, cw * 0.4 - 1, 2.5, "Cost (CAD)", B, 4.5, "White", 2)
cy += 3.5
for i, (comp, cost) in enumerate(cost_items):
bg = "Surface" if i % 2 == 0 else "BG"
rect(s, cx, cy, cw, 3, bg)
txt(s, cx + 1, cy + 0.3, cw * 0.6, 2.5, comp, R, 4.5, "MidText")
txt(s, cx + cw * 0.6, cy + 0.3, cw * 0.4 - 1, 2.5, cost, R, 4.5, "DarkText", 2)
cy += 3.2
cy += 4
# ── Timeline ──
txt(s, cx, cy, cw, 3, "TIMELINE", B, 5.5, "LightText")
cy += 4
txt(s, cx, cy, 20, 3, "Centralized", B, 4.5, "LightText")
tbar_x = cx + 22
tbar_w = cw - 22
rect(s, tbar_x, cy, tbar_w * 0.55, 3.5, "Border")
rect(s, tbar_x + tbar_w * 0.55, cy, tbar_w * 0.45, 3.5, "BarRed", "None", 0, 1)
txt(s, tbar_x, cy + 0.5, tbar_w, 2.5, "First water: 2029", B, 4.5, "White", 2)
cy += 5
txt(s, cx, cy, 20, 3, "Living Pipeline", B, 4.5, "LightText")
rect(s, tbar_x, cy, tbar_w * 0.18, 3.5, "Border")
rect(s, tbar_x + tbar_w * 0.18, cy, tbar_w * 0.42, 3.5, "BarGreen", "None", 0, 1)
rect(s, tbar_x + tbar_w * 0.6, cy, tbar_w * 0.4, 3.5, "Border")
txt(s, tbar_x, cy + 0.5, tbar_w * 0.6, 2.5, "First water: 2027", B, 4.5, "White", 1)
cy += 4.5
txt(s, cx, cy, cw, 3, "2 years faster \u2014 unblocks ~3\u20135K homes sooner", B, 5, "Green", 1)
cy += 5.5
# ── Resilience ──
txt(s, cx, cy, cw, 3, "RESILIENCE", B, 5.5, "LightText")
cy += 4
rc = [cw * 0.25, cw * 0.375, cw * 0.375]
rect(s, cx, cy, cw, 3, "Navy")
txt(s, cx + 1, cy + 0.3, rc[0], 2.5, "Risk", B, 4, "White")
txt(s, cx + rc[0], cy + 0.3, rc[1], 2.5, "Centralized", B, 4, "White")
txt(s, cx + rc[0] + rc[1], cy + 0.3, rc[2], 2.5, "Living Pipeline", B, 4, "White")
cy += 3.5
res = [
("WTP failure", "All towns lose supply", "Others compensate"),
("Pipeline break", "Downstream cut off", "Nodes self-sufficient"),
("Drought", "System-wide stress", "Aquifers buffer"),
("Cost escalation", "$121M\u2192$270M", "Phased, no mega-risk"),
]
for i, (risk, cent, liv) in enumerate(res):
bg = "Surface" if i % 2 == 0 else "BG"
rect(s, cx, cy, cw, 3.2, bg)
txt(s, cx + 1, cy + 0.3, rc[0] - 1, 2.5, risk, R, 4, "DarkText")
txt(s, cx + rc[0], cy + 0.3, rc[1] - 1, 2.5, cent, R, 4, "Alert")
txt(s, cx + rc[0] + rc[1], cy + 0.3, rc[2] - 1, 2.5, liv, R, 4, "Green")
cy += 3.4
cy += 4
# ── Co-Benefits ──
txt(s, cx, cy, cw, 3, "CO-BENEFITS", B, 5.5, "LightText")
cy += 4
coben = [
("Ecological:", "10\u201320 ha new habitat along rail corridor"),
("Economic:", "3,000+ homes unlocked = $1.2B construction"),
("Indigenous:", "Working with the watershed, not against it"),
("Energy:", "40\u201355% less than pumping 53 km"),
]
for label, text in coben:
txt(s, cx, cy, cw, 4, f"{label} {text}", R, 4.5, "MidText")
cy += 4.5
cy += 3
# ── Design Spiral ──
txt(s, cx, cy, cw, 3, "DESIGN METHODOLOGY", B, 5.5, "LightText")
cy += 4
steps = [
("Define", "Supply 5 towns\ncost-effectively"),
("Biologize", "How does nature\ndistribute?"),
("Discover", "Mycorrhizal nets,\nwetlands"),
("Abstract", "Nodes + landscape\nas infra"),
("Emulate", "MAR, satellites,\nsmart grid"),
("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, 10, "Surface", "Border", 0.1, 1)
txt(s, sx, cy + 0.5, stw, 2.5, title, B, 4, "Forest", 1)
txt(s, sx, cy + 3.5, stw, 6, desc, R, 3.5, "LightText", 1)
cy += 13
# ── Sources ──
hline(s, cx, cy, cw, "Border", 0.25)
cy += 1.5
txt(s, cx, cy, cw, 12,
"Sources: Collingwood WTP Class EA (2022) \u2022 NVCA IWMP (2019) \u2022 New Tecumseth Master Plan (2016) \u2022 CFB Borden (U of Waterloo) \u2022 Region of Waterloo ASR \u2022 Fleming College CAWT \u2022 SEQ Water Grid (Australia) \u2022 Turku Finland MAR \u2022 Egerton-Warburton et al., J. Exp. Botany (2007) \u2022 Biomimicry Institute Design Spiral",
R, 4, "LightText")
# ═══ FOOTER ═══
rect(s, 0, PH - FOOTER_H, PW, FOOTER_H, "Navy")
txt(s, 14, PH - FOOTER_H + 1.5, 200, 3, "Jeff Emmett \u2022 The Living Pipeline \u2022 B-Prize 2026", R, 5, "White")
txt(s, PW - 200, PH - FOOTER_H + 1.5, 186, 3,
"Biomimicry Commons \u2022 Collingwood\u2013Alliston Corridor, Simcoe County, Ontario",
R, 5, "White", 2)
# ═══ 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()