From cbb8128385730d737f3d854c536bf19e3e1532c1 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 2 Oct 2026 10:19:19 +0200 Subject: [PATCH] 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 --- README.md | 16 +++++------- netbox_utilities/__init__.py | 2 +- netbox_utilities/views.py | 48 +++++++++++++++++++++++++++++++----- pyproject.toml | 2 +- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4206411..0c8692d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/netbox_utilities/__init__.py b/netbox_utilities/__init__.py index d36b83a..2bc9573 100644 --- a/netbox_utilities/__init__.py +++ b/netbox_utilities/__init__.py @@ -1,6 +1,6 @@ from netbox.plugins import PluginConfig, get_plugin_config -__version__ = "0.14.1" +__version__ = "0.14.2" class NetBoxUtilitiesConfig(PluginConfig): diff --git a/netbox_utilities/views.py b/netbox_utilities/views.py index cdff733..734bd6e 100644 --- a/netbox_utilities/views.py +++ b/netbox_utilities/views.py @@ -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( diff --git a/pyproject.toml b/pyproject.toml index bba0f81..054c35c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"