diff --git a/README.md b/README.md index f5d7351..d771451 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,8 @@ Der Abschnitt **Netzwerk-Topologie** zeichnet je Standort ein Diagramm mit Symbo Die Reihenfolge innerhalb einer Ebene wird so optimiert, dass sich möglichst wenige Leitungen kreuzen. * Leitungen werden **rechtwinklig** über eigene Sammelschienen geführt, farbig je übergeordnetem Gerät. Portnamen stehen direkt am Zielgerät, Mehrfach-Uplinks werden dicker gezeichnet und als „2ד markiert. +* Jede Topologie steht auf einer **eigenen Seite im Querformat** (PDF und Word) und wird so groß wie möglich skaliert. + Danach geht das Dokument im Hochformat weiter. * Provider-Leitungen erscheinen als Wolke, Geräte anderer Standorte gestrichelt (abschaltbar). * Beschriftung wählbar: Management-IP, Modell, Ports an den Verbindungen. diff --git a/netbox_customer_export/__init__.py b/netbox_customer_export/__init__.py index 276965b..8d03ddd 100644 --- a/netbox_customer_export/__init__.py +++ b/netbox_customer_export/__init__.py @@ -5,7 +5,7 @@ class CustomerExportConfig(PluginConfig): name = "netbox_customer_export" verbose_name = "Kunden-Export" description = "Kundendokumentation als PDF oder Word exportieren" - version = "0.5.0" + version = "0.6.0" author = "Claude" base_url = "customer-export" min_version = "4.6.8" diff --git a/netbox_customer_export/blocks.py b/netbox_customer_export/blocks.py index 94bd6e4..e231df7 100644 --- a/netbox_customer_export/blocks.py +++ b/netbox_customer_export/blocks.py @@ -28,7 +28,11 @@ class Document: @property def toc(self): - return [b for b in self.blocks if b.kind == "heading" and b.level <= 2] + out = [] + for b in self.blocks: + headings = b.rows if b.kind == "landscape" else [b] + out += [h for h in headings if h.kind == "heading" and h.level <= 2] + return out _anchor_counter = {"n": 0} @@ -66,6 +70,12 @@ def IMAGES(images, wide=False): return Block("images", images=out, level=1 if wide else 0) # level=1: volle Seitenbreite +def LANDSCAPE(headings, png): + """Eigene Seite im Querformat: Überschriften + Bild so groß wie möglich (z.B. Topologie).""" + data_uri = "data:image/png;base64," + base64.b64encode(png).decode("ascii") + return Block("landscape", rows=list(headings), images=[{"svg": None, "png": png, "caption": "", "data_uri": data_uri}]) + + def PAGEBREAK(): return Block("pagebreak") diff --git a/netbox_customer_export/renderers/docx.py b/netbox_customer_export/renderers/docx.py index b14d400..6243ba2 100644 --- a/netbox_customer_export/renderers/docx.py +++ b/netbox_customer_export/renderers/docx.py @@ -61,6 +61,7 @@ def render_docx(doc): # Ohne Vorlage: Standarddokument mit Farben/Schrift aus dem Branding d = Docx() sec = d.sections[0] + sec.page_width, sec.page_height = Cm(21), Cm(29.7) # A4 statt US-Letter aus der python-docx-Vorlage sec.left_margin = sec.right_margin = Cm(2) sec.top_margin = sec.bottom_margin = Cm(2) d.styles["Normal"].font.name = b["font_family"] @@ -116,8 +117,42 @@ def render_docx(doc): d.settings.element.append(upd) d.add_page_break() + from docx.enum.section import WD_ORIENT, WD_SECTION + + portrait = d.sections[-1] + portrait_size = (portrait.page_width, portrait.page_height) + landscape_open = False + + def new_section(landscape): + s = d.add_section(WD_SECTION.NEW_PAGE) + w, h = portrait_size + s.orientation = WD_ORIENT.LANDSCAPE if landscape else WD_ORIENT.PORTRAIT + s.page_width, s.page_height = (h, w) if landscape else (w, h) + return s + for blk in doc.blocks: - if blk.kind == "heading": + if landscape_open and blk.kind != "landscape": + # erst beim nächsten Inhalt zurück ins Hochformat – vermeidet leere Seiten + new_section(landscape=False) + landscape_open = False + if blk.kind == "landscape": + s = new_section(landscape=True) + landscape_open = True + for h in blk.rows: + d.add_heading(h.text, level=h.level) + png = blk.images[0]["png"] + from PIL import Image + + px_w, px_h = Image.open(BytesIO(png)).size + avail_w = s.page_width - s.left_margin - s.right_margin + avail_h = s.page_height - s.top_margin - s.bottom_margin - Cm(1.4 * len(blk.rows) + 0.8) + para = d.add_paragraph() + para.alignment = WD_ALIGN_PARAGRAPH.CENTER + if px_w / px_h >= avail_w / avail_h: + para.add_run().add_picture(BytesIO(png), width=avail_w) + else: + para.add_run().add_picture(BytesIO(png), height=avail_h) + elif blk.kind == "heading": d.add_heading(blk.text, level=min(blk.level, 4)) elif blk.kind == "paragraph": d.add_paragraph(blk.text) diff --git a/netbox_customer_export/sections.py b/netbox_customer_export/sections.py index 37d0247..24f2b1d 100644 --- a/netbox_customer_export/sections.py +++ b/netbox_customer_export/sections.py @@ -7,7 +7,7 @@ from django.apps import apps from django.contrib.contenttypes.models import ContentType from netbox.plugins import get_plugin_config -from .blocks import EMPTY, H, IMAGES, KV, P, PAGEBREAK, T, get +from .blocks import EMPTY, H, IMAGES, KV, LANDSCAPE, P, PAGEBREAK, T, get from .rack_png import render_rack_png from .rack_svg import render_rack_svg @@ -412,12 +412,13 @@ def licenses(scope, opts): def topology(scope, opts): from .topology import build_graphs, render_png - blocks = [H(1, "Netzwerk-Topologie")] + title = H(1, "Netzwerk-Topologie") graphs = build_graphs(scope, opts) if not graphs: - return _empty(opts, blocks, "Keine verkabelten Geräte gefunden.") + return _empty(opts, [title], "Keine verkabelten Geräte gefunden.") ports = "ports" in (opts.get("topo_details") or []) - for g in graphs: - blocks.append(H(2, g.title)) - blocks.append(IMAGES([(None, "", render_png(g, show_ports=ports))], wide=True)) - return blocks + # Je Standort eine Querformat-Seite; die Abschnittsüberschrift steht mit auf der ersten + return [ + LANDSCAPE(([title] if i == 0 else []) + [H(2, g.title)], render_png(g, show_ports=ports)) + for i, g in enumerate(graphs) + ] diff --git a/netbox_customer_export/templates/netbox_customer_export/document.html b/netbox_customer_export/templates/netbox_customer_export/document.html index 78781e4..c93f692 100644 --- a/netbox_customer_export/templates/netbox_customer_export/document.html +++ b/netbox_customer_export/templates/netbox_customer_export/document.html @@ -8,6 +8,7 @@ @page { size: A4; margin: 20mm 15mm 20mm 15mm; @bottom-left { content: element(footer); } @bottom-right { content: "Seite " counter(page) " / " counter(pages); font-size: 7pt; color: #777; } } + @page landscape { size: A4 landscape; margin: 12mm 15mm 16mm 15mm; } @page cover { @bottom-left { content: none } @bottom-right { content: none } } body { font-family: "{{ b.font_family }}", "Segoe UI", "DejaVu Sans", Arial, sans-serif; font-size: {{ b.font_size }}pt; color: #222; } .cover { page: cover; padding-top: 60mm; page-break-after: always; } @@ -40,6 +41,11 @@ figcaption { font-style: italic; color: #555; } .footer { position: running(footer); font-size: 7pt; color: #777; } @media screen { .footer { display: none; } } + .landscape { page: landscape; page-break-before: always; page-break-after: always; } + .landscape h1 { page-break-before: avoid; margin-top: 0; } + .landscape h2 { margin-top: 4pt; } + .landscape img { display: block; margin: 0 auto; max-width: 100%; max-height: 152mm; } + @media screen { .landscape img { max-height: none; width: 100%; } } .pagebreak { page-break-after: always; } @media screen { body { max-width: 1100px; margin: auto; padding: 16px; } } {{ b.extra_css|safe }} @@ -73,6 +79,11 @@ {% elif b.kind == 'images' %}