feat: narrower margins on landscape topology pages
- Landscape pages use 7 mm side margins (PDF) / 0.7 cm (Word) and compact headings, so the topology gets about 28 cm of width. - Portrait pages after a landscape page restore their original margins. - The topology PNG is cropped to its content, removing the blank border. Bump version to 0.6.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ class CustomerExportConfig(PluginConfig):
|
|||||||
name = "netbox_customer_export"
|
name = "netbox_customer_export"
|
||||||
verbose_name = "Kunden-Export"
|
verbose_name = "Kunden-Export"
|
||||||
description = "Kundendokumentation als PDF oder Word exportieren"
|
description = "Kundendokumentation als PDF oder Word exportieren"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
author = "Claude"
|
author = "Claude"
|
||||||
base_url = "customer-export"
|
base_url = "customer-export"
|
||||||
min_version = "4.6.8"
|
min_version = "4.6.8"
|
||||||
|
|||||||
@@ -123,11 +123,19 @@ def render_docx(doc):
|
|||||||
portrait_size = (portrait.page_width, portrait.page_height)
|
portrait_size = (portrait.page_width, portrait.page_height)
|
||||||
landscape_open = False
|
landscape_open = False
|
||||||
|
|
||||||
|
portrait_margins = (
|
||||||
|
portrait.left_margin, portrait.right_margin, portrait.top_margin, portrait.bottom_margin,
|
||||||
|
portrait.header_distance, portrait.footer_distance,
|
||||||
|
)
|
||||||
|
|
||||||
def new_section(landscape):
|
def new_section(landscape):
|
||||||
s = d.add_section(WD_SECTION.NEW_PAGE)
|
s = d.add_section(WD_SECTION.NEW_PAGE)
|
||||||
w, h = portrait_size
|
w, h = portrait_size
|
||||||
s.orientation = WD_ORIENT.LANDSCAPE if landscape else WD_ORIENT.PORTRAIT
|
s.orientation = WD_ORIENT.LANDSCAPE if landscape else WD_ORIENT.PORTRAIT
|
||||||
s.page_width, s.page_height = (h, w) if landscape else (w, h)
|
s.page_width, s.page_height = (h, w) if landscape else (w, h)
|
||||||
|
if not landscape: # Ränder der Hochformat-Seiten wiederherstellen
|
||||||
|
(s.left_margin, s.right_margin, s.top_margin, s.bottom_margin,
|
||||||
|
s.header_distance, s.footer_distance) = portrait_margins
|
||||||
return s
|
return s
|
||||||
|
|
||||||
for blk in doc.blocks:
|
for blk in doc.blocks:
|
||||||
@@ -137,15 +145,21 @@ def render_docx(doc):
|
|||||||
landscape_open = False
|
landscape_open = False
|
||||||
if blk.kind == "landscape":
|
if blk.kind == "landscape":
|
||||||
s = new_section(landscape=True)
|
s = new_section(landscape=True)
|
||||||
|
# schmale Ränder, damit die Topologie möglichst groß wird
|
||||||
|
s.left_margin = s.right_margin = Cm(0.7)
|
||||||
|
s.top_margin, s.bottom_margin = Cm(0.7), Cm(1.1)
|
||||||
|
s.header_distance, s.footer_distance = Cm(0.3), Cm(0.4)
|
||||||
landscape_open = True
|
landscape_open = True
|
||||||
for h in blk.rows:
|
for h in blk.rows:
|
||||||
d.add_heading(h.text, level=h.level)
|
heading = d.add_heading(h.text, level=h.level)
|
||||||
|
heading.paragraph_format.space_before = Pt(0)
|
||||||
|
heading.paragraph_format.space_after = Pt(2)
|
||||||
png = blk.images[0]["png"]
|
png = blk.images[0]["png"]
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
px_w, px_h = Image.open(BytesIO(png)).size
|
px_w, px_h = Image.open(BytesIO(png)).size
|
||||||
avail_w = s.page_width - s.left_margin - s.right_margin
|
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)
|
avail_h = s.page_height - s.top_margin - s.bottom_margin - Cm(1.1 * len(blk.rows) + 0.6)
|
||||||
para = d.add_paragraph()
|
para = d.add_paragraph()
|
||||||
para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
if px_w / px_h >= avail_w / avail_h:
|
if px_w / px_h >= avail_w / avail_h:
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
@page { size: A4; margin: 20mm 15mm 20mm 15mm;
|
@page { size: A4; margin: 20mm 15mm 20mm 15mm;
|
||||||
@bottom-left { content: element(footer); }
|
@bottom-left { content: element(footer); }
|
||||||
@bottom-right { content: "Seite " counter(page) " / " counter(pages); font-size: 7pt; color: #777; } }
|
@bottom-right { content: "Seite " counter(page) " / " counter(pages); font-size: 7pt; color: #777; } }
|
||||||
@page landscape { size: A4 landscape; margin: 12mm 15mm 16mm 15mm; }
|
/* Querformat: schmale Ränder, damit die Topologie möglichst groß wird */
|
||||||
|
@page landscape { size: A4 landscape; margin: 7mm 7mm 11mm 7mm; }
|
||||||
@page cover { @bottom-left { content: none } @bottom-right { content: none } }
|
@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; }
|
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; }
|
.cover { page: cover; padding-top: 60mm; page-break-after: always; }
|
||||||
@@ -42,9 +43,9 @@
|
|||||||
.footer { position: running(footer); font-size: 7pt; color: #777; }
|
.footer { position: running(footer); font-size: 7pt; color: #777; }
|
||||||
@media screen { .footer { display: none; } }
|
@media screen { .footer { display: none; } }
|
||||||
.landscape { page: landscape; page-break-before: always; page-break-after: always; }
|
.landscape { page: landscape; page-break-before: always; page-break-after: always; }
|
||||||
.landscape h1 { page-break-before: avoid; margin-top: 0; }
|
.landscape h1 { page-break-before: avoid; margin: 0 0 2pt 0; font-size: 15pt; padding-bottom: 1pt; }
|
||||||
.landscape h2 { margin-top: 4pt; }
|
.landscape h2 { margin: 2pt 0 3pt 0; font-size: 12pt; }
|
||||||
.landscape img { display: block; margin: 0 auto; max-width: 100%; max-height: 152mm; }
|
.landscape img { display: block; margin: 0 auto; max-width: 100%; max-height: 172mm; }
|
||||||
@media screen { .landscape img { max-height: none; width: 100%; } }
|
@media screen { .landscape img { max-height: none; width: 100%; } }
|
||||||
.pagebreak { page-break-after: always; }
|
.pagebreak { page-break-after: always; }
|
||||||
@media screen { body { max-width: 1100px; margin: auto; padding: 16px; } }
|
@media screen { body { max-width: 1100px; margin: auto; padding: 16px; } }
|
||||||
|
|||||||
@@ -438,6 +438,14 @@ def render_png(graph, show_ports=False, scale=2):
|
|||||||
for text, x, y, anchor in labels:
|
for text, x, y, anchor in labels:
|
||||||
tag(text, x, y, anchor)
|
tag(text, x, y, anchor)
|
||||||
|
|
||||||
|
# weißen Rand abschneiden: jeder Millimeter kommt der Größe im Dokument zugute
|
||||||
|
from PIL import ImageChops
|
||||||
|
|
||||||
|
bbox = ImageChops.difference(img, Image.new("RGB", img.size, (255, 255, 255))).getbbox()
|
||||||
|
if bbox:
|
||||||
|
pad = 6 * S
|
||||||
|
img = img.crop((max(bbox[0] - pad, 0), max(bbox[1] - pad, 0), min(bbox[2] + pad, img.width), min(bbox[3] + pad, img.height)))
|
||||||
|
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
img.save(out, "PNG", optimize=True)
|
img.save(out, "PNG", optimize=True)
|
||||||
return out.getvalue()
|
return out.getvalue()
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-customer-export"
|
name = "netbox-customer-export"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
description = "NetBox-Plugin: Kundendokumentation (PDF/Word) für Standorte, Lokationen, Mandanten und Mandantengruppen"
|
description = "NetBox-Plugin: Kundendokumentation (PDF/Word) für Standorte, Lokationen, Mandanten und Mandantengruppen"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
Reference in New Issue
Block a user