fix: inline device images as data URIs in offline SVG export

strip_svg_hyperlinks() removed every href, including on <image>
elements that reference device-type photos, leaving devices with
images blank in the exported file. Non-anchor hrefs (currently only
<image>) are now resolved through an optional image_loader callback
and embedded as base64 data URIs instead of being stripped; anchor
unwrapping is unchanged. RackElevationOfflineSVGView wires this up to
NetBox's MEDIA storage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 15:20:43 +02:00
co-authored by Claude Sonnet 5
parent 23acebcf17
commit ead171d546
5 changed files with 102 additions and 14 deletions
+32 -1
View File
@@ -368,6 +368,37 @@ class SelectTenantView(LoginRequiredMixin, View):
return redirect(_safe_return_url(request))
def _media_data_uri_loader():
import base64
import mimetypes
from urllib.parse import urlparse
from django.conf import settings
from django.core.files.storage import default_storage
media_url_path = urlparse(settings.MEDIA_URL).path
def load(href):
storage_path = urlparse(href).path
if media_url_path and storage_path.startswith(media_url_path):
storage_path = storage_path[len(media_url_path) :]
storage_path = storage_path.lstrip("/")
try:
if not storage_path or not default_storage.exists(storage_path):
return None
with default_storage.open(storage_path, "rb") as image_file:
data = image_file.read()
except OSError:
return None
mime_type, _ = mimetypes.guess_type(storage_path)
mime_type = mime_type or "application/octet-stream"
encoded = base64.b64encode(data).decode("ascii")
return f"data:{mime_type};base64,{encoded}"
return load
class RackElevationOfflineSVGView(LoginRequiredMixin, View):
"""Serve a rack elevation SVG stripped of hyperlinks, for offline use."""
@@ -394,7 +425,7 @@ class RackElevationOfflineSVGView(LoginRequiredMixin, View):
if hasattr(response, "render"):
response.render()
offline_svg = strip_svg_hyperlinks(response.content)
offline_svg = strip_svg_hyperlinks(response.content, image_loader=_media_data_uri_loader())
filename = f"{rack.name}-{face}-offline.svg".replace(" ", "_")
http_response = HttpResponse(offline_svg, content_type="image/svg+xml")
http_response["Content-Disposition"] = f'attachment; filename="{filename}"'