Compare commits

...
1 Commits
Author SHA1 Message Date
MrBlakeandClaude Opus 5.5 cbb8128385 fix: downscale oversized bulk uploads so NetBox can render thumbnails
NetBox renders image attachment thumbnails under its global 25 MP Pillow
limit, so 50 MP photos uploaded via the bulk form showed no preview.
Scale images above that limit down (applying EXIF orientation) before
saving. Also resolve leftover merge conflict markers in the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:19:19 +02:00
4 changed files with 50 additions and 18 deletions
+6 -10
View File
@@ -348,6 +348,12 @@ none are saved. An optional shared description can be applied; the original
file name is kept as the display name. Requires permission to add image
attachments and to view the target object.
NetBox caps Pillow at 25 megapixels, which breaks current 50 MP smartphone
photos (e.g. Google Pixel, 8160×6144) and their thumbnails. The bulk upload
therefore scales larger images down to NetBox' limit (EXIF orientation is
applied) before saving. If an image is rejected, the form also shows Pillow's
exact error message.
### Installing multiple modules
Under **Plugins > NetBox Utilities > Install multiple modules** (or via the
@@ -359,17 +365,7 @@ The operation is atomic: if a bay is taken meanwhile or a naming conflict
occurs, nothing is saved. Requires the permission to add modules; object
permissions still apply.
<<<<<<< HEAD
NetBox begrenzt Pillow global auf 25 Megapixel und lehnt Bilder ab 50
Megapixeln ab. Damit aktuelle 50-MP-Smartphone-Fotos (z. B. Google Pixel,
8160×6144) hochgeladen werden können, hebt der Mehrfach-Upload dieses Limit
während der Verarbeitung auf 100 Megapixel an. Wird ein Bild abgelehnt, zeigt
das Formular zusätzlich die genaue Fehlermeldung von Pillow an.
### Module mehrfach einbauen
=======
### Personalised navigation
>>>>>>> a85e4d6375a673a333d601fa8867e00a96423fec
Under **Plugins > NetBox Utilities > Customize navigation** users see every menu
they have access to. Arrow buttons change the order; a toggle hides a menu.
+1 -1
View File
@@ -1,6 +1,6 @@
from netbox.plugins import PluginConfig, get_plugin_config
__version__ = "0.14.1"
__version__ = "0.14.2"
class NetBoxUtilitiesConfig(PluginConfig):
+42 -6
View File
@@ -6,6 +6,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import SuspiciousFileOperation, ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import DatabaseError, transaction
from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
@@ -66,11 +67,45 @@ def raised_image_pixel_limit():
if original_limit is not None and original_limit < BULK_IMAGE_MAX_PIXELS:
Image.MAX_IMAGE_PIXELS = BULK_IMAGE_MAX_PIXELS
try:
yield
yield original_limit
finally:
Image.MAX_IMAGE_PIXELS = original_limit
def downscale_image(upload, max_pixels):
"""Shrink an upload to NetBox' pixel limit so NetBox can render thumbnails later.
Returns the original upload when it is small enough or cannot be processed;
NetBox' own validation reports broken files afterwards.
"""
from PIL import Image, ImageOps
if not max_pixels:
return upload
try:
upload.seek(0)
with Image.open(upload) as image:
width, height = image.size
if width * height <= max_pixels:
return upload
image_format = image.format
image = ImageOps.exif_transpose(image)
scale = (max_pixels / (width * height)) ** 0.5
size = (max(1, int(image.width * scale)), max(1, int(image.height * scale)))
image = image.resize(size, Image.Resampling.LANCZOS)
if image_format == "JPEG" and image.mode not in ("RGB", "L"):
image = image.convert("RGB")
content = BytesIO()
save_options = {"quality": 90} if image_format in ("JPEG", "WEBP") else {}
image.save(content, format=image_format, **save_options)
except Exception: # noqa: BLE001 - leave validation to NetBox
return upload
finally:
if hasattr(upload, "seek"):
upload.seek(0)
return SimpleUploadedFile(upload.name, content.getvalue(), content_type=getattr(upload, "content_type", None))
class BulkImageUploadView(ContentTypePermissionRequiredMixin, View):
template_name = "netbox_utilities/bulk_image_upload.html"
@@ -86,12 +121,12 @@ class BulkImageUploadView(ContentTypePermissionRequiredMixin, View):
parent = self._get_parent(request, request.POST)
form = BulkImageUploadForm(request.POST, request.FILES)
if form.is_valid():
with raised_image_pixel_limit():
return self._process_upload(request, form, parent)
with raised_image_pixel_limit() as netbox_pixel_limit:
return self._process_upload(request, form, parent, netbox_pixel_limit)
return self._render(request, form, parent)
def _process_upload(self, request, form, parent):
image_forms = self._build_image_forms(parent, form.cleaned_data)
def _process_upload(self, request, form, parent, max_pixels):
image_forms = self._build_image_forms(parent, form.cleaned_data, max_pixels)
invalid = [image_form for image_form in image_forms if not image_form.is_valid()]
if invalid:
for image_form in invalid:
@@ -161,9 +196,10 @@ class BulkImageUploadView(ContentTypePermissionRequiredMixin, View):
return None
@staticmethod
def _build_image_forms(parent, cleaned_data):
def _build_image_forms(parent, cleaned_data, max_pixels=None):
image_forms = []
for image in cleaned_data["images"]:
image = downscale_image(image, max_pixels)
if hasattr(image, "seek"):
image.seek(0)
image_forms.append(
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "netbox-utilities"
version = "0.14.1"
version = "0.14.2"
description = "Navigation, tenant utilities, connection VLANs, partial-width racks, and bulk operations for NetBox 4.6/4.7"
readme = "README.md"
requires-python = ">=3.12"