390 lines
12 KiB
Python
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()
|