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
This commit is contained in:
93
dwg-counting/pdf_export.py
Normal file
93
dwg-counting/pdf_export.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user