feat: optional device images in rack elevations, HTML preview in new tab

- New export option "Gerätebilder anzeigen": the SVG is rendered with
  include_images and the /media/ image links are replaced by embedded
  data URIs, since Word, WeasyPrint and SVG-as-image cannot load
  login-protected files. The Pillow PNG renderer draws the front/rear
  images too, with the device name on a dark label.
- Custom rack_svg_renderer adapters receive images=..., older adapters
  without that parameter keep working.
- The HTML preview now opens in a new browser tab.

Bump version to 0.3.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 13:56:39 +02:00
co-authored by Claude Opus 5.5
parent f24e42a516
commit 5b2d68356c
8 changed files with 97 additions and 17 deletions
+45 -5
View File
@@ -1,5 +1,7 @@
"""Rack-Ansichten offline (ohne HTTP-Aufruf der API) als SVG erzeugen."""
import base64
import logging
import mimetypes
from django.utils.module_loading import import_string
from netbox.plugins import get_plugin_config
@@ -13,17 +15,52 @@ LEGEND_WIDTH = 30
MARGIN_WIDTH = 15
def render_rack_svg(rack, face, user):
def device_image(device_type, face):
"""Bilddatei (FieldFile) des Gerätetyps für die Seite oder None."""
image = getattr(device_type, "front_image" if face == "front" else "rear_image", None)
return image if image else None
def _data_uri(image):
mime = mimetypes.guess_type(image.name)[0] or "image/png"
with image.open("rb") as f:
return f"data:{mime};base64,{base64.b64encode(f.read()).decode('ascii')}"
def _inline_images(svg, devices):
"""Ersetzt die /media/…-Links der Gerätebilder durch eingebettete Daten.
Word, WeasyPrint und <img src="data:image/svg+xml…"> laden keine externen (login-geschützten) Dateien nach.
"""
seen = set()
for d in devices:
for face in ("front", "rear"):
image = device_image(d.device_type, face)
if not image or image.name in seen:
continue
seen.add(image.name)
try:
svg = svg.replace(f'"{image.url}"', f'"{_data_uri(image)}"')
except OSError:
logger.warning("Gerätebild %s nicht lesbar", image.name)
return svg
def render_rack_svg(rack, face, user, images=False, devices=()):
"""Gibt (svg, fehler) zurück – genau einer der beiden Werte ist None.
Ist ``rack_svg_renderer`` gesetzt (z.B. der Offline-Renderer aus Netbox-Utilities, der mehrere
Geräte pro HE korrekt darstellt), wird dieser aufgerufen: ``fn(rack=..., face=..., user=...)``.
Geräte pro HE korrekt darstellt), wird dieser aufgerufen: ``fn(rack=..., face=..., user=..., images=...)``.
Andernfalls wird NetBox' eigener RackElevationSVG direkt in-process genutzt.
"""
try:
renderer = get_plugin_config("netbox_customer_export", "rack_svg_renderer")
if renderer:
svg = import_string(renderer)(rack=rack, face=face, user=user)
fn = import_string(renderer)
try:
svg = fn(rack=rack, face=face, user=user, images=images)
except TypeError: # ältere Adapter ohne images-Parameter
svg = fn(rack=rack, face=face, user=user)
else:
from dcim.svg import RackElevationSVG
@@ -36,10 +73,13 @@ def render_rack_svg(rack, face, user):
legend_width=LEGEND_WIDTH,
margin_width=MARGIN_WIDTH,
user=user,
include_images=False,
include_images=images,
base_url="",
).render(face)
return (svg.tostring() if hasattr(svg, "tostring") else str(svg)), None
svg = svg.tostring() if hasattr(svg, "tostring") else str(svg)
if images:
svg = _inline_images(svg, devices)
return svg, None
except Exception as e: # noqa: BLE001 – ein defektes Rack soll nicht den ganzen Export abbrechen
logger.exception("Rack-SVG für %s (%s) fehlgeschlagen", rack, face)
return None, f"{type(e).__name__}: {e}"