add support for svg images in library
This commit is contained in:
parent
2c217939b0
commit
ff3a09d30c
7 changed files with 257 additions and 30 deletions
|
@ -72,6 +72,8 @@ class ImageLibraryAdmin(admin.ModelAdmin):
|
|||
Display small thumbnail in list view.
|
||||
"""
|
||||
if obj.image:
|
||||
# Use img tag for all images in list view to maintain clickability
|
||||
# SVG files will still display correctly with img tag
|
||||
return format_html(
|
||||
'<img src="{}" width="50" height="50" style="object-fit: cover; border-radius: 4px;" />',
|
||||
obj.image.url,
|
||||
|
@ -85,10 +87,23 @@ class ImageLibraryAdmin(admin.ModelAdmin):
|
|||
Display larger preview in detail view.
|
||||
"""
|
||||
if obj.image:
|
||||
return format_html(
|
||||
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 4px;" />',
|
||||
obj.image.url,
|
||||
)
|
||||
if obj.is_svg():
|
||||
# For SVG files in detail view, use object tag for better rendering
|
||||
# This is only for display, not for clickable elements
|
||||
return format_html(
|
||||
'<div style="pointer-events: none;">'
|
||||
'<object data="{}" type="image/svg+xml" style="max-width: 300px; max-height: 300px; border-radius: 4px; background: #f5f5f5;">'
|
||||
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 4px;" />'
|
||||
"</object>"
|
||||
"</div>",
|
||||
obj.image.url,
|
||||
obj.image.url,
|
||||
)
|
||||
else:
|
||||
return format_html(
|
||||
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 4px;" />',
|
||||
obj.image.url,
|
||||
)
|
||||
return "No Image"
|
||||
|
||||
image_preview.short_description = "Preview"
|
||||
|
|
|
@ -59,11 +59,17 @@ class ImageLibraryWidget(forms.Select):
|
|||
selected = "selected" if str(image.pk) == str(value) else ""
|
||||
image_url = image.image.url if image.image else ""
|
||||
|
||||
# Use img tag for all images in widget to maintain clickability
|
||||
# SVG files will still display correctly with img tag
|
||||
preview_html = (
|
||||
f'<img src="{image_url}" alt="{image.alt_text}" loading="lazy">'
|
||||
)
|
||||
|
||||
html_parts.append(
|
||||
f"""
|
||||
<div class="image-option {selected}" data-value="{image.pk}">
|
||||
<div class="image-preview">
|
||||
<img src="{image_url}" alt="{image.alt_text}" loading="lazy">
|
||||
{preview_html}
|
||||
</div>
|
||||
<div class="image-info">
|
||||
<span class="image-name">{image.name}</span>
|
||||
|
|
|
@ -40,6 +40,8 @@ class ImageLibraryWidget(forms.Select):
|
|||
for image in images:
|
||||
thumbnail_html = ""
|
||||
if self.show_thumbnails and image.image:
|
||||
# Use img tag for all images in dropdowns to maintain functionality
|
||||
# SVG files will still display correctly with img tag
|
||||
thumbnail_html = format_html(
|
||||
' <img src="{}" style="width: 20px; height: 20px; object-fit: cover; margin-left: 5px; vertical-align: middle;" />',
|
||||
image.image.url,
|
||||
|
@ -64,16 +66,21 @@ class ImageLibraryWidget(forms.Select):
|
|||
if value:
|
||||
try:
|
||||
image = ImageLibrary.objects.get(pk=value)
|
||||
|
||||
# Use img tag for all images in preview for consistency
|
||||
# Add SVG indicator in the text if it's an SVG file
|
||||
svg_indicator = " (SVG)" if image.is_svg() else ""
|
||||
preview_html = format_html(
|
||||
'<div class="image-preview" style="margin-top: 10px;">'
|
||||
'<img src="{}" style="max-width: 200px; max-height: 200px; border: 1px solid #ddd; border-radius: 4px;" />'
|
||||
'<p style="margin-top: 5px; font-size: 12px; color: #666;">{} - {}x{} - {}</p>'
|
||||
'<p style="margin-top: 5px; font-size: 12px; color: #666;">{} - {}x{} - {}{}</p>'
|
||||
"</div>",
|
||||
image.image.url,
|
||||
image.name,
|
||||
image.width or "?",
|
||||
image.height or "?",
|
||||
image.get_file_size_display(),
|
||||
svg_indicator,
|
||||
)
|
||||
except ImageLibrary.DoesNotExist:
|
||||
pass
|
||||
|
|
|
@ -2,6 +2,8 @@ from django.db import models
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.utils.text import slugify
|
||||
from django_prose_editor.fields import ProseEditorField
|
||||
import mimetypes
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def validate_image_size(value, mb=1):
|
||||
|
@ -10,6 +12,49 @@ def validate_image_size(value, mb=1):
|
|||
raise ValidationError(f"Maximum file size is {mb} MB")
|
||||
|
||||
|
||||
def validate_image_or_svg(value):
|
||||
"""
|
||||
Validate that the uploaded file is either a valid image or SVG file.
|
||||
"""
|
||||
# Check file size first
|
||||
validate_image_size(value)
|
||||
|
||||
# Get the file extension and MIME type
|
||||
filename = value.name.lower()
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
|
||||
# List of allowed image formats
|
||||
allowed_image_types = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
"image/svg+xml",
|
||||
]
|
||||
|
||||
# Check if it's an SVG file
|
||||
if filename.endswith(".svg") or mime_type == "image/svg+xml":
|
||||
try:
|
||||
# Reset file pointer and read content
|
||||
value.seek(0)
|
||||
content = value.read()
|
||||
value.seek(0) # Reset for later use
|
||||
|
||||
# Try to parse as XML to ensure it's valid SVG
|
||||
ET.fromstring(content)
|
||||
return # Valid SVG
|
||||
except ET.ParseError:
|
||||
raise ValidationError("Invalid SVG file format")
|
||||
|
||||
# For non-SVG files, check MIME type
|
||||
if mime_type not in allowed_image_types:
|
||||
raise ValidationError(
|
||||
f"Unsupported file type. Allowed types: JPEG, PNG, GIF, WebP, BMP, TIFF, SVG"
|
||||
)
|
||||
|
||||
|
||||
class Currency(models.TextChoices):
|
||||
CHF = "CHF", "Swiss Franc"
|
||||
EUR = "EUR", "Euro"
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import os
|
||||
|
||||
import mimetypes
|
||||
import xml.etree.ElementTree as ET
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.text import slugify
|
||||
from PIL import Image as PILImage
|
||||
from .base import validate_image_size
|
||||
from .base import validate_image_or_svg
|
||||
|
||||
|
||||
def get_image_upload_path(instance, filename):
|
||||
|
@ -35,10 +36,10 @@ class ImageLibrary(models.Model):
|
|||
)
|
||||
|
||||
# Image file
|
||||
image = models.ImageField(
|
||||
image = models.FileField(
|
||||
upload_to=get_image_upload_path,
|
||||
validators=[validate_image_size],
|
||||
help_text="Upload image file (max 1MB)",
|
||||
validators=[validate_image_or_svg],
|
||||
help_text="Upload image file (max 1MB) - supports JPEG, PNG, GIF, WebP, BMP, TIFF, and SVG",
|
||||
)
|
||||
|
||||
# Image properties (automatically populated)
|
||||
|
@ -122,14 +123,86 @@ class ImageLibrary(models.Model):
|
|||
Update image properties like width, height, and file size.
|
||||
"""
|
||||
try:
|
||||
# Get image dimensions
|
||||
with PILImage.open(self.image.path) as img:
|
||||
self.width = img.width
|
||||
self.height = img.height
|
||||
|
||||
# Get file size
|
||||
self.file_size = self.image.size
|
||||
|
||||
# Check if it's an SVG file
|
||||
filename = self.image.name.lower()
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
|
||||
if filename.endswith(".svg") or mime_type == "image/svg+xml":
|
||||
# For SVG files, try to extract dimensions from the SVG content
|
||||
try:
|
||||
with open(self.image.path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse the SVG to extract width and height
|
||||
root = ET.fromstring(content)
|
||||
|
||||
# Get width and height attributes
|
||||
width = root.get("width")
|
||||
height = root.get("height")
|
||||
|
||||
# Extract numeric values if they exist
|
||||
if width and height:
|
||||
# Remove units like 'px', 'em', etc. and convert to int
|
||||
try:
|
||||
width_val = int(
|
||||
float(
|
||||
width.replace("px", "")
|
||||
.replace("em", "")
|
||||
.replace("pt", "")
|
||||
)
|
||||
)
|
||||
height_val = int(
|
||||
float(
|
||||
height.replace("px", "")
|
||||
.replace("em", "")
|
||||
.replace("pt", "")
|
||||
)
|
||||
)
|
||||
self.width = width_val
|
||||
self.height = height_val
|
||||
except (ValueError, TypeError):
|
||||
# If we can't parse dimensions, try viewBox
|
||||
viewbox = root.get("viewBox")
|
||||
if viewbox:
|
||||
try:
|
||||
viewbox_parts = viewbox.split()
|
||||
if len(viewbox_parts) >= 4:
|
||||
self.width = int(float(viewbox_parts[2]))
|
||||
self.height = int(float(viewbox_parts[3]))
|
||||
except (ValueError, TypeError):
|
||||
# Default SVG dimensions if we can't parse
|
||||
self.width = 100
|
||||
self.height = 100
|
||||
else:
|
||||
# Check for viewBox if width/height attributes don't exist
|
||||
viewbox = root.get("viewBox")
|
||||
if viewbox:
|
||||
try:
|
||||
viewbox_parts = viewbox.split()
|
||||
if len(viewbox_parts) >= 4:
|
||||
self.width = int(float(viewbox_parts[2]))
|
||||
self.height = int(float(viewbox_parts[3]))
|
||||
except (ValueError, TypeError):
|
||||
self.width = 100
|
||||
self.height = 100
|
||||
else:
|
||||
# Default SVG dimensions
|
||||
self.width = 100
|
||||
self.height = 100
|
||||
|
||||
except (ET.ParseError, FileNotFoundError, UnicodeDecodeError):
|
||||
# If SVG parsing fails, set default dimensions
|
||||
self.width = 100
|
||||
self.height = 100
|
||||
else:
|
||||
# For raster images, use PIL
|
||||
with PILImage.open(self.image.path) as img:
|
||||
self.width = img.width
|
||||
self.height = img.height
|
||||
|
||||
# Save without calling the full save method to avoid recursion
|
||||
ImageLibrary.objects.filter(pk=self.pk).update(
|
||||
width=self.width, height=self.height, file_size=self.file_size
|
||||
|
@ -175,6 +248,28 @@ class ImageLibrary(models.Model):
|
|||
self.usage_count -= 1
|
||||
self.save(update_fields=["usage_count"])
|
||||
|
||||
def is_svg(self):
|
||||
"""
|
||||
Check if the uploaded file is an SVG.
|
||||
"""
|
||||
if not self.image:
|
||||
return False
|
||||
|
||||
filename = self.image.name.lower()
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
|
||||
return filename.endswith(".svg") or mime_type == "image/svg+xml"
|
||||
|
||||
def get_mime_type(self):
|
||||
"""
|
||||
Return the MIME type of the image file.
|
||||
"""
|
||||
if not self.image:
|
||||
return None
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(self.image.name)
|
||||
return mime_type
|
||||
|
||||
|
||||
class ImageReference(models.Model):
|
||||
"""
|
||||
|
|
|
@ -77,3 +77,33 @@
|
|||
background-color: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* SVG support */
|
||||
.svg-preview {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.svg-preview object {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* SVG thumbnails in admin */
|
||||
.image-thumbnail object {
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.image-preview object {
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Category badges */
|
||||
.category-badge.svg {
|
||||
background-color: #f3e8ff;
|
||||
color: #7c3aed;
|
||||
}
|
|
@ -10,6 +10,7 @@ register = template.Library()
|
|||
def image_library_img(slug_or_id, css_class="", alt_text="", width=None, height=None):
|
||||
"""
|
||||
Render an image from the image library by slug or ID.
|
||||
Automatically handles SVG files with proper rendering.
|
||||
|
||||
Usage:
|
||||
{% image_library_img "my-image-slug" css_class="img-fluid" %}
|
||||
|
@ -25,24 +26,52 @@ def image_library_img(slug_or_id, css_class="", alt_text="", width=None, height=
|
|||
# Use provided alt_text or fall back to image's alt_text
|
||||
final_alt_text = alt_text or image.alt_text
|
||||
|
||||
# Build HTML attributes
|
||||
attrs = {
|
||||
"src": image.image.url,
|
||||
"alt": final_alt_text,
|
||||
}
|
||||
# Check if it's an SVG file
|
||||
if image.is_svg():
|
||||
# For SVG files, use object tag for better rendering
|
||||
attrs = {
|
||||
"data": image.image.url,
|
||||
"type": "image/svg+xml",
|
||||
"alt": final_alt_text,
|
||||
}
|
||||
|
||||
if css_class:
|
||||
attrs["class"] = css_class
|
||||
if css_class:
|
||||
attrs["class"] = css_class
|
||||
|
||||
if width:
|
||||
attrs["width"] = width
|
||||
if width:
|
||||
attrs["width"] = width
|
||||
|
||||
if height:
|
||||
attrs["height"] = height
|
||||
if height:
|
||||
attrs["height"] = height
|
||||
|
||||
# Build the HTML
|
||||
attr_string = " ".join(f'{k}="{v}"' for k, v in attrs.items())
|
||||
return format_html("<img {}/>", attr_string)
|
||||
# Build the object tag with img fallback
|
||||
attr_string = " ".join(f'{k}="{v}"' for k, v in attrs.items())
|
||||
return format_html(
|
||||
'<object {}><img src="{}" alt="{}" class="{}"/></object>',
|
||||
attr_string,
|
||||
image.image.url,
|
||||
final_alt_text,
|
||||
css_class or "",
|
||||
)
|
||||
else:
|
||||
# For raster images, use img tag
|
||||
attrs = {
|
||||
"src": image.image.url,
|
||||
"alt": final_alt_text,
|
||||
}
|
||||
|
||||
if css_class:
|
||||
attrs["class"] = css_class
|
||||
|
||||
if width:
|
||||
attrs["width"] = width
|
||||
|
||||
if height:
|
||||
attrs["height"] = height
|
||||
|
||||
# Build the HTML
|
||||
attr_string = " ".join(f'{k}="{v}"' for k, v in attrs.items())
|
||||
return format_html("<img {}/>", attr_string)
|
||||
|
||||
except ImageLibrary.DoesNotExist:
|
||||
# Return empty string or placeholder if image not found
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue