Decompiled NetBox core's own bundled JS (netbox.js) to find the real toggle logic: its "images-only"/"labels-only" control only ever hides image.device-image and text.device-image-label - it never touches the plain text.label, relying on an opaque device image to visually cover it. A device without a photo therefore still shows its name live, even in "images-only". apply_rack_view_mode() previously removed .label for images-only and .device-image/.device-image-label for labels-only, which not only targeted the wrong element for images-only but also left device- image-label (the name overlaid on the image) around in labels-only. For a static, non-interactive export there's no image left to cover stray text, so per explicit request this deliberately does not mirror NetBox's live fallback: images-only now always removes every label (.label and .device-image-label) for a pure image view, and labels-only removes the image layer (.device-image and .device-image-label) to reproduce NetBox's own box-with-text view. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
104 lines
3.8 KiB
Python
104 lines
3.8 KiB
Python
import xml.etree.ElementTree as ET
|
|
|
|
SVG_NS = "http://www.w3.org/2000/svg"
|
|
XLINK_NS = "http://www.w3.org/1999/xlink"
|
|
|
|
ET.register_namespace("", SVG_NS)
|
|
ET.register_namespace("xlink", XLINK_NS)
|
|
|
|
_ANCHOR_TAGS = (f"{{{SVG_NS}}}a", "a")
|
|
_IMAGE_TAGS = (f"{{{SVG_NS}}}image", "image")
|
|
_HREF_ATTRS = ("href", f"{{{XLINK_NS}}}href")
|
|
|
|
RACK_VIEW_MODES = {"images-and-labels", "images-only", "labels-only"}
|
|
_PLAIN_LABEL_CLASS = "label"
|
|
_IMAGE_CLASS = "device-image"
|
|
_IMAGE_LABEL_CLASS = "device-image-label"
|
|
|
|
|
|
def apply_rack_view_mode(svg_source, mode):
|
|
"""Filter the rack elevation SVG down to images-only or labels-only content.
|
|
|
|
NetBox's own "Bilder"/"Beschriftungen" toggle never actually removes the
|
|
plain .label text: it just leaves an opaque .device-image sitting on top
|
|
of it, so a device without a photo still shows its name in "images-only"
|
|
live. A downloaded, static export has no such fallback text to hide
|
|
behind an image, so here "images-only" removes every label outright
|
|
(.label and .device-image-label) to always produce a pure image view,
|
|
and "labels-only" removes the image layer to reproduce the same
|
|
box-with-text view NetBox itself already shows for that mode.
|
|
"""
|
|
if mode not in RACK_VIEW_MODES or mode == "images-and-labels":
|
|
return svg_source
|
|
root = ET.fromstring(svg_source)
|
|
if mode == "images-only":
|
|
_remove_elements_with_class(root, {_PLAIN_LABEL_CLASS, _IMAGE_LABEL_CLASS})
|
|
elif mode == "labels-only":
|
|
_remove_elements_with_class(root, {_IMAGE_CLASS, _IMAGE_LABEL_CLASS})
|
|
return ET.tostring(root, encoding="unicode")
|
|
|
|
|
|
def _remove_elements_with_class(parent, class_names):
|
|
for child in list(parent):
|
|
classes = set(child.attrib.get("class", "").split())
|
|
if classes & class_names:
|
|
parent.remove(child)
|
|
else:
|
|
_remove_elements_with_class(child, class_names)
|
|
|
|
|
|
def strip_svg_hyperlinks(svg_source, image_loader=None):
|
|
"""Return svg_source with every <a> wrapper unwrapped and dead links removed.
|
|
|
|
NetBox wraps rack elevation devices in hyperlinks pointing at internal,
|
|
relative object URLs. Those links are dead weight (and broken paths) once
|
|
the SVG is saved and opened outside of a NetBox session, so this unwraps
|
|
them while keeping their contents.
|
|
|
|
<image> elements reference device photos by the same kind of internal,
|
|
relative URL. Simply stripping their href would leave the device blank,
|
|
so image_loader (href -> "data:...;base64,..." or None) is used to embed
|
|
the image data directly instead. Without an image_loader, image hrefs are
|
|
left untouched.
|
|
"""
|
|
root = ET.fromstring(svg_source)
|
|
_unwrap_anchors(root)
|
|
_resolve_hrefs(root, image_loader)
|
|
return ET.tostring(root, encoding="unicode")
|
|
|
|
|
|
def _unwrap_anchors(parent):
|
|
replaced = False
|
|
new_children = []
|
|
for child in list(parent):
|
|
_unwrap_anchors(child)
|
|
if child.tag in _ANCHOR_TAGS:
|
|
replaced = True
|
|
new_children.extend(list(child))
|
|
else:
|
|
new_children.append(child)
|
|
if replaced:
|
|
for child in list(parent):
|
|
parent.remove(child)
|
|
for child in new_children:
|
|
parent.append(child)
|
|
|
|
|
|
def _resolve_hrefs(element, image_loader):
|
|
if element.tag in _IMAGE_TAGS:
|
|
if image_loader is not None:
|
|
for attr in _HREF_ATTRS:
|
|
href = element.attrib.get(attr)
|
|
if not href:
|
|
continue
|
|
data_uri = image_loader(href)
|
|
if data_uri:
|
|
element.attrib[attr] = data_uri
|
|
else:
|
|
element.attrib.pop(attr, None)
|
|
else:
|
|
for attr in _HREF_ATTRS:
|
|
element.attrib.pop(attr, None)
|
|
for child in element:
|
|
_resolve_hrefs(child, image_loader)
|