Files
geointel/scripts/create_portfolio_case_study.py
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

390 lines
12 KiB
Python

from __future__ import annotations
from pathlib import Path
from reportlab.lib.colors import HexColor, white
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfgen import canvas
from reportlab.platypus import Paragraph
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / "output" / "pdf" / "geointel-case-study.pdf"
ASSETS = ROOT / "docs" / "assets" / "portfolio"
PUBLIC = ROOT / "frontend" / "public" / "portfolio"
PAGE = landscape(A4)
W, H = PAGE
INK = HexColor("#082f2b")
TEAL = HexColor("#0d7f70")
MINT = HexColor("#7ce0c8")
PALE = HexColor("#edf8f5")
MUTED = HexColor("#667b76")
AMBER = HexColor("#d9a94f")
def register_fonts() -> tuple[str, str]:
font_root = ROOT / "frontend" / "node_modules" / "@fontsource" / "manrope" / "files"
regular = font_root / "manrope-latin-500-normal.woff"
bold = font_root / "manrope-latin-700-normal.woff"
# ReportLab does not load WOFF. Use stable system fonts when TTF is absent.
if regular.suffix.lower() == ".ttf" and regular.exists():
pdfmetrics.registerFont(TTFont("GeoIntel", str(regular)))
pdfmetrics.registerFont(TTFont("GeoIntelBold", str(bold)))
return "GeoIntel", "GeoIntelBold"
return "Helvetica", "Helvetica-Bold"
FONT, FONT_BOLD = register_fonts()
def image_cover(
c: canvas.Canvas, path: Path, x: float, y: float, w: float, h: float
) -> None:
with Image.open(path) as image:
iw, ih = image.size
scale = max(w / iw, h / ih)
draw_w, draw_h = iw * scale, ih * scale
c.saveState()
clip = c.beginPath()
clip.rect(x, y, w, h)
c.clipPath(clip, stroke=0, fill=0)
c.drawImage(
ImageReader(str(path)),
x + (w - draw_w) / 2,
y + (h - draw_h) / 2,
draw_w,
draw_h,
mask="auto",
)
c.restoreState()
def title(c: canvas.Canvas, kicker: str, heading: str, sub: str | None = None) -> None:
c.setFillColor(TEAL)
c.setFont(FONT_BOLD, 8)
c.drawString(42, H - 44, kicker.upper())
c.setFillColor(INK)
c.setFont(FONT_BOLD, 25)
c.drawString(42, H - 78, heading)
if sub:
c.setFillColor(MUTED)
c.setFont(FONT, 9.5)
c.drawString(42, H - 99, sub)
def footer(c: canvas.Canvas, number: int) -> None:
c.setStrokeColor(HexColor("#d7e8e3"))
c.line(42, 27, W - 42, 27)
c.setFillColor(MUTED)
c.setFont(FONT, 7)
c.drawString(
42, 14, "GeoIntel - evidence-first GeoAI for Belgium and the Belgian North Sea"
)
c.drawRightString(W - 42, 14, f"{number:02d}")
def paragraph(
c: canvas.Canvas,
text: str,
x: float,
y: float,
w: float,
size: float = 10,
color=INK,
leading: float | None = None,
) -> float:
style = ParagraphStyle(
"body",
fontName=FONT,
fontSize=size,
leading=leading or size * 1.45,
textColor=color,
alignment=TA_LEFT,
)
p = Paragraph(text, style)
_, ph = p.wrap(w, H)
p.drawOn(c, x, y - ph)
return y - ph
def metric(
c: canvas.Canvas, x: float, y: float, value: str, label: str, width: float
) -> None:
c.setFillColor(PALE)
c.roundRect(x, y, width, 55, 9, fill=1, stroke=0)
c.setFillColor(TEAL)
c.setFont(FONT_BOLD, 18)
c.drawString(x + 13, y + 27, value)
c.setFillColor(MUTED)
c.setFont(FONT_BOLD, 7)
c.drawString(x + 13, y + 12, label.upper())
def draw_cover(c: canvas.Canvas) -> None:
image_cover(c, PUBLIC / "geointel-dark-case-study-cover.png", 0, 0, W, H)
c.setFillColor(HexColor("#031916"))
c.rect(0, 0, W * 0.55, H, fill=1, stroke=0)
c.setFillColor(MINT)
c.setFont(FONT_BOLD, 9)
c.drawString(48, H - 72, "PORTFOLIO CASE STUDY / 2026")
c.setFillColor(white)
c.setFont(FONT_BOLD, 35)
c.drawString(48, H - 135, "GeoIntel")
c.setFont(FONT_BOLD, 21)
c.drawString(48, H - 171, "Van kaartlaag naar")
c.setFillColor(MINT)
c.drawString(48, H - 198, "aantoonbaar inzicht.")
paragraph(
c,
"Een kaartgerichte GeoAI-workbench waarin officiële bronnen, GIS, PyTorch en kwaliteitsbewijs één reproduceerbare keten vormen.",
48,
H - 228,
315,
11,
HexColor("#c6ddd7"),
17,
)
c.setFillColor(HexColor("#173b36"))
c.roundRect(48, 47, 290, 48, 9, fill=1, stroke=0)
c.setFillColor(white)
c.setFont(FONT_BOLD, 8)
c.drawString(62, 74, "BELGIË + BELGISCHE NOORDZEE")
c.setFillColor(HexColor("#9ec8bd"))
c.setFont(FONT, 7.5)
c.drawString(62, 59, "React · FastAPI · PostGIS · PyTorch · NVIDIA CUDA")
def draw_challenge(c: canvas.Canvas) -> None:
c.setFillColor(white)
c.rect(0, 0, W, H, fill=1, stroke=0)
title(c, "01 / Productvraag", "Niet alleen zien, maar kunnen verdedigen")
y = paragraph(
c,
"Geoportalen tonen vaak lagen en losse metingen. GeoIntel maakt van bron, gebied, analyse en bewijs één operationele workflow.",
42,
H - 128,
330,
13,
INK,
19,
)
cards = [
(
"01",
"Context behouden",
"Autoriteit, meetmoment, CRS en dekking blijven zichtbaar.",
),
("02", "AI begrenzen", "Modelversie, confidence en validatiescope reizen mee."),
(
"03",
"Kwaliteit bewijzen",
"Objectbewijs en fouten zijn vóór export inspecteerbaar.",
),
]
for index, (num, head, body) in enumerate(cards):
cy = y - 42 - index * 78
c.setFillColor(PALE)
c.roundRect(42, cy - 54, 330, 64, 10, fill=1, stroke=0)
c.setFillColor(TEAL)
c.setFont(FONT_BOLD, 8)
c.drawString(56, cy - 6, num)
c.setFillColor(INK)
c.setFont(FONT_BOLD, 11)
c.drawString(86, cy - 7, head)
paragraph(c, body, 86, cy - 18, 266, 8.2, MUTED, 12)
image_cover(c, ASSETS / "geointel-landing-hero.png", 410, 53, 385, 415)
footer(c, 2)
def draw_experience(c: canvas.Canvas) -> None:
c.setFillColor(HexColor("#f5faf8"))
c.rect(0, 0, W, H, fill=1, stroke=0)
title(
c,
"02 / Experience design",
"De kaart blijft het werkblad",
"Rustige hiërarchie, zichtbare herkomst en bewijs op het moment dat het nodig is.",
)
image_cover(c, ASSETS / "geointel-workbench-map.png", 42, 73, 485, 360)
image_cover(c, ASSETS / "geointel-workbench-quality.png", 547, 73, 248, 360)
metric(c, 42, 440, "1", "ruimtelijke context", 160)
metric(c, 214, 440, "7+", "bronfamilies", 160)
metric(c, 386, 440, "QA", "vóór export", 160)
footer(c, 3)
def draw_ai(c: canvas.Canvas) -> None:
c.setFillColor(INK)
c.rect(0, 0, W, H, fill=1, stroke=0)
c.setFillColor(MINT)
c.setFont(FONT_BOLD, 8)
c.drawString(42, H - 44, "03 / PYTORCH + NVIDIA")
c.setFillColor(white)
c.setFont(FONT_BOLD, 25)
c.drawString(42, H - 78, "Van bronpixel naar gecontroleerde detectie")
c.setFillColor(HexColor("#b8d4cd"))
c.setFont(FONT, 9)
c.drawString(
42,
H - 99,
"Elke schakel bewaart de context die nodig is om het resultaat opnieuw te beoordelen.",
)
stages = [
("01", "Orthofoto", "CRS + dekking"),
("02", "Beeldtegels", "overlap + tile-id"),
("03", "NVIDIA GPU", "CUDA + model"),
("04", "Detecties", "confidence + geometrie"),
("05", "QA-bewijs", "precision + IoU"),
]
x0, gap, bw = 42, 13, 139
c.setStrokeColor(HexColor("#4bb99f"))
c.setLineWidth(2)
c.line(95, 285, 746, 285)
for i, (num, head, body) in enumerate(stages):
x = x0 + i * (bw + gap)
c.setFillColor(HexColor("#12443d"))
c.roundRect(x, 205, bw, 160, 12, fill=1, stroke=0)
c.setFillColor(MINT)
c.setFont(FONT_BOLD, 8)
c.drawString(x + 14, 340, num)
c.setFillColor(white)
c.setFont(FONT_BOLD, 10)
c.drawString(x + 14, 295, head)
c.setFillColor(HexColor("#9fc9bf"))
c.setFont(FONT, 7.4)
c.drawString(x + 14, 275, body)
c.setFillColor(MINT if i < 3 else AMBER)
c.circle(x + bw / 2, 230, 6, fill=1, stroke=0)
paragraph(
c,
"Fail-closed runtime: wanneer CUDA vereist maar niet beschikbaar is, of wanneer een lokaal model ontbreekt, toont GeoIntel een expliciete niet-geconfigureerde toestand. Productie-AI wordt nooit gesimuleerd.",
42,
166,
735,
11,
HexColor("#d7ebe6"),
17,
)
footer(c, 4)
def draw_architecture(c: canvas.Canvas) -> None:
c.setFillColor(white)
c.rect(0, 0, W, H, fill=1, stroke=0)
title(c, "04 / Architectuur", "Eén keten, expliciete verantwoordelijkheden")
nodes = [
(55, 270, 130, 66, "React + MapLibre", "kaartwerkruimte"),
(220, 270, 130, 66, "FastAPI", "contractlaag"),
(385, 355, 150, 66, "GeoPandas / Rasterio", "ruimtelijke verwerking"),
(385, 185, 150, 66, "Redis + RQ", "hervatbare jobs"),
(570, 270, 150, 66, "PyTorch + CUDA", "beeldanalyse"),
(570, 90, 150, 66, "PostGIS + artifacts", "resultaat + provenance"),
]
for x, y, w, h, head, body in nodes:
c.setFillColor(PALE if "PyTorch" not in head else HexColor("#0d5047"))
c.roundRect(x, y, w, h, 10, fill=1, stroke=0)
c.setFillColor(white if "PyTorch" in head else INK)
c.setFont(FONT_BOLD, 9)
c.drawString(x + 12, y + 39, head)
c.setFillColor(HexColor("#b9d8d0") if "PyTorch" in head else MUTED)
c.setFont(FONT, 7.2)
c.drawString(x + 12, y + 21, body)
c.setStrokeColor(TEAL)
c.setLineWidth(1.5)
arrows = [
((185, 303), (220, 303)),
((350, 303), (385, 388)),
((350, 303), (385, 218)),
((535, 388), (645, 336)),
((535, 218), (570, 303)),
((645, 270), (645, 156)),
]
for (x1, y1), (x2, y2) in arrows:
c.line(x1, y1, x2, y2)
c.circle(x2, y2, 2.5, fill=1, stroke=0)
paragraph(
c,
"PostGIS bewaart querybare geometrie en lifecycle-records. Grote raster-, mask- en modelbestanden blijven versioned artifacts; de database bewaart hun identiteit, checksum en provenance.",
55,
150,
460,
9.5,
MUTED,
15,
)
footer(c, 5)
def draw_outcomes(c: canvas.Canvas) -> None:
image_cover(c, PUBLIC / "geointel-dark-case-study-cover.png", 0, 0, W, H)
c.setFillColor(HexColor("#041d19"))
c.rect(0, 0, W, H, fill=1, stroke=0)
c.setFillColor(MINT)
c.setFont(FONT_BOLD, 8)
c.drawString(42, H - 44, "05 / RESULTAAT")
c.setFillColor(white)
c.setFont(FONT_BOLD, 27)
c.drawString(42, H - 82, "Een professionele GeoAI-workbench")
c.setFillColor(HexColor("#c0dad4"))
c.setFont(FONT, 10)
c.drawString(
42, H - 104, "Nationaal/maritiem van scope, lokaal controleerbaar in bewijs."
)
outcomes = [
("Map-first", "De gebruiker blijft in dezelfde ruimtelijke context."),
("Evidence-first", "QA en provenance zijn onderdeel van het resultaat."),
("GPU-ready", "NVIDIA CUDA is expliciet geconfigureerd en gevalideerd."),
(
"Accessible motion",
"Animatie verduidelijkt toestand en respecteert reduced motion.",
),
]
for i, (head, body) in enumerate(outcomes):
x = 42 + (i % 2) * 375
y = 330 - (i // 2) * 105
c.setFillColor(HexColor("#123f38"))
c.roundRect(x, y, 350, 78, 11, fill=1, stroke=0)
c.setFillColor(MINT)
c.setFont(FONT_BOLD, 11)
c.drawString(x + 16, y + 48, head)
paragraph(c, body, x + 16, y + 38, 316, 8, HexColor("#b8d2cc"), 12)
c.setFillColor(white)
c.setFont(FONT_BOLD, 11)
c.drawString(42, 58, "GeoIntel · Jens / ITWorx.tech")
c.setFillColor(HexColor("#8fb9af"))
c.setFont(FONT, 8)
c.drawString(42, 42, "Belgium and the Belgian North Sea · v1.0.0")
def build() -> None:
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
c = canvas.Canvas(str(OUTPUT), pagesize=PAGE, pageCompression=1)
c.setTitle("GeoIntel - Evidence-first GeoAI case study")
c.setAuthor("Jens / ITWorx.tech")
c.setSubject(
"Portfolio case study for the GeoIntel Belgium and Belgian North Sea workbench"
)
for draw in (
draw_cover,
draw_challenge,
draw_experience,
draw_ai,
draw_architecture,
draw_outcomes,
):
draw(c)
c.showPage()
c.save()
print(OUTPUT)
if __name__ == "__main__":
build()