Repository: netbox-community/netbox

Issue:
Images in rack view not formatted, right hand side is cropped
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reproducible bugs. If you need assistance with
    NetBox installation, or if you have a general question, DO NOT open an
    issue. Instead, post to our mailing list:

        https://groups.google.com/forum/#!forum/netbox-discuss

    Please describe the environment in which you are running NetBox. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report.
-->
### Environment
* Python version: 3.6.8 <!-- Example: 3.6.9 -->
* NetBox version: 2.7.7 <!-- Example: 2.7.3 -->

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of NetBox (or the current beta
    release where applicable). Begin with the creation of any necessary
    database objects and call out every operation being performed explicitly.
    If reporting a bug in the REST API, be sure to reconstruct the raw HTTP
    request(s) being made: Don't rely on a wrapper like pynetbox.
-->
### Steps to Reproduce
1. Upload an image formatted to the correct aspect ratio (226x19 was suggested) to a front or rear image of a device type. Save.
2. Go to a rack view containing device which has had an image uploaded.

<!-- What did you expect to happen? -->
### Expected Behavior
Image is shown correctly, scaled so that the entirety of it fits the width (and appropriate number of RU height).

<!-- What happened instead? -->
### Observed Behavior
No matter what I try, the right hand side of the image is always cut off. This happens regardless of the image size / aspect ratio.

Searching into this a bit more, I think the issue is related to the CSS code which enforces a width of 260px when the rectangle box representing the rack is 229px wide.

`<image class="device-image" height="20" preserveAspectRatio="none" width="260" x="30" xlink:href="/media/deicetype-images/<filename>" t="0"></image>`

Upon further experimenting, if you set the preserveAspectRatio to "yes", then the image's aspect ratio is respected and squeezed to fit inside the width of 230 / 30x number of RU of device *and* it is also centered. This might be a good way of working around issue of certain images having the rack ears in them (19") or only the "opening" (circa 17.5") width.


Discussion:

Resolving patch:
diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py
--- a/netbox/dcim/constants.py
+++ b/netbox/dcim/constants.py
@@ -9,10 +9,10 @@
 
 RACK_U_HEIGHT_DEFAULT = 42
 
+RACK_ELEVATION_BORDER_WIDTH = 2
 RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
-
-RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
-RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20
+RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 220
+RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 22
 
 
 #
diff --git a/netbox/dcim/elevations.py b/netbox/dcim/elevations.py
--- a/netbox/dcim/elevations.py
+++ b/netbox/dcim/elevations.py
@@ -6,6 +6,7 @@
 
 from utilities.utils import foreground_color
 from .choices import DeviceFaceChoices
+from .constants import RACK_ELEVATION_BORDER_WIDTH
 
 
 class RackElevationSVG:
@@ -75,7 +76,7 @@ def _draw_device_front(self, drawing, device, start, end, text):
         if self.include_images and device.device_type.front_image:
             url = device.device_type.front_image.url
             image = drawing.image(href=url, insert=start, size=end, class_='device-image')
-            image.stretch()
+            image.fit(scale='slice')
             link.add(image)
 
     def _draw_device_rear(self, drawing, device, start, end, text):
@@ -91,7 +92,7 @@ def _draw_device_rear(self, drawing, device, start, end, text):
         if self.include_images and device.device_type.rear_image:
             url = device.device_type.rear_image.url
             image = drawing.image(href=url, insert=start, size=end, class_='device-image')
-            image.stretch()
+            image.fit(scale='slice')
             drawing.add(image)
 
     @staticmethod
@@ -134,13 +135,16 @@ def render(self, face, unit_width, unit_height, legend_width):
         """
         Return an SVG document representing a rack elevation.
         """
-        drawing = self._setup_drawing(unit_width + legend_width, unit_height * self.rack.u_height)
+        drawing = self._setup_drawing(
+            unit_width + legend_width + RACK_ELEVATION_BORDER_WIDTH * 2,
+            unit_height * self.rack.u_height + RACK_ELEVATION_BORDER_WIDTH * 2
+        )
         reserved_units = self.rack.get_reserved_units()
 
         unit_cursor = 0
         for ru in range(0, self.rack.u_height):
             start_y = ru * unit_height
-            position_coordinates = (legend_width / 2, start_y + unit_height / 2 + 2)
+            position_coordinates = (legend_width / 2, start_y + unit_height / 2 + RACK_ELEVATION_BORDER_WIDTH)
             unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru
             drawing.add(
                 drawing.text(str(unit), position_coordinates, class_="unit")
@@ -153,11 +157,12 @@ def render(self, face, unit_width, unit_height, legend_width):
             height = unit.get('height', 1)
 
             # Setup drawing coordinates
-            start_y = unit_cursor * unit_height
+            x_offset = legend_width + RACK_ELEVATION_BORDER_WIDTH
+            y_offset = unit_cursor * unit_height + RACK_ELEVATION_BORDER_WIDTH
             end_y = unit_height * height
-            start_cordinates = (legend_width, start_y)
-            end_cordinates = (legend_width + unit_width, end_y)
-            text_cordinates = (legend_width + (unit_width / 2), start_y + end_y / 2)
+            start_cordinates = (x_offset, y_offset)
+            end_cordinates = (unit_width, end_y)
+            text_cordinates = (x_offset + (unit_width / 2), y_offset + end_y / 2)
 
             # Draw the device
             if device and device.face == face:
@@ -187,6 +192,13 @@ def render(self, face, unit_width, unit_height, legend_width):
             unit_cursor += height
 
         # Wrap the drawing with a border
-        drawing.add(drawing.rect((legend_width, 1), (unit_width - 1, self.rack.u_height * unit_height - 2), class_='rack'))
+        border_width = RACK_ELEVATION_BORDER_WIDTH
+        border_offset = RACK_ELEVATION_BORDER_WIDTH / 2
+        frame = drawing.rect(
+            insert=(legend_width + border_offset, border_offset),
+            size=(unit_width + border_width, self.rack.u_height * unit_height + border_width),
+            class_='rack'
+        )
+        drawing.add(frame)
 
         return drawing


Test patch: