Files
AI_portal/dwg-counting/pdf_export.py
Ondřej Glaser 48cef99257 Initial portal commit: landing + 9 AI-powered apps
Apps:
- dwg-rooms: extract room numbers from DWG/DXF
- dwg-counting: count symbols in PDF drawings (OpenCV template matching)
- contract-check: review PDF contracts against a checklist (Claude vision + Tesseract OCR fallback)
- email-drafter: bullet notes → polished Czech/English business emails
- invoice-extractor: PDF/image invoice → structured data → Excel
- translator: Czech-first translator across 19 languages with tone control
- vv-check: find inconsistent unit prices across VV sheets in one workbook
- vv-compare: diff original vs new VV files (changes / added / removed)
- feature-request: portal users submit ideas + sample files

Infrastructure:
- LiteLLM gateway with per-app virtual keys + budgets
- Langfuse observability
- Geist font, shared theme, cross-subdomain back link + theme sync via cookie/URL
- Caddy reverse proxy on *.klas.chat
2026-05-13 15:25:04 +02:00

94 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Render annotated PDF with color-coded symbol match highlights."""
import logging
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
logger = logging.getLogger(__name__)
# Distinct visually-separable colors (RGB) for up to 12 symbol types
PALETTE = [
(228, 26, 28), # red
(55, 126, 184), # blue
(77, 175, 74), # green
(152, 78, 163), # purple
(255, 127, 0), # orange
(0, 184, 184), # teal
(247, 129, 191), # pink
(153, 153, 0), # olive
(166, 86, 40), # brown
(102, 102, 102), # grey
(0, 0, 0), # black
(190, 81, 51), # rust
]
def _load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
for path in (
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
):
try:
return ImageFont.truetype(path, size)
except OSError:
continue
return ImageFont.load_default()
def render_annotated_pdf(
drawing_path: Path,
results: list[dict],
out_path: Path,
filename: str = "drawing",
) -> Path:
"""Build a multi-page PDF: page 1 = legend, page 2 = annotated drawing.
results: [{id, description, count, matches: [{x,y,w,h,score,rot}, ...]}, ...]
"""
base = Image.open(drawing_path).convert("RGB")
annotated = base.copy()
draw = ImageDraw.Draw(annotated)
# Scale stroke width by image size so boxes are visible at any zoom
stroke = max(2, min(annotated.size) // 800)
label_font = _load_font(max(14, min(annotated.size) // 200))
for idx, r in enumerate(results):
color = PALETTE[idx % len(PALETTE)]
for m in r.get("matches", []):
x, y, w, h = m["x"], m["y"], m["w"], m["h"]
draw.rectangle([x, y, x + w, y + h], outline=color, width=stroke)
# Legend page
legend_w = annotated.size[0]
row_h = max(36, legend_w // 30)
legend_h = row_h * (len(results) + 4)
legend = Image.new("RGB", (legend_w, legend_h), "white")
ldraw = ImageDraw.Draw(legend)
title_font = _load_font(max(28, legend_w // 50))
ldraw.text((40, 30), f"Počítání symbolů — {filename}", font=title_font, fill="black")
ldraw.text(
(40, 30 + row_h),
f"Nalezeno {sum(r['count'] for r in results)} symbolů v {len(results)} kategoriích.",
font=label_font, fill="black",
)
y = 30 + 3 * row_h
swatch = row_h - 10
for idx, r in enumerate(results):
color = PALETTE[idx % len(PALETTE)]
ldraw.rectangle([40, y, 40 + swatch, y + swatch], fill=color, outline="black", width=2)
text = f"{r.get('description', r['id'])}{r['count']}×"
ldraw.text((40 + swatch + 16, y), text, font=label_font, fill="black")
y += row_h
# Save as multi-page PDF
legend.save(
out_path,
"PDF",
resolution=150.0,
save_all=True,
append_images=[annotated],
)
return out_path