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
94 lines
2.9 KiB
Python
94 lines
2.9 KiB
Python
"""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
|