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.
|
Display small thumbnail in list view.
|
||||||
"""
|
"""
|
||||||
if obj.image:
|
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(
|
return format_html(
|
||||||
'<img src="{}" width="50" height="50" style="object-fit: cover; border-radius: 4px;" />',
|
'<img src="{}" width="50" height="50" style="object-fit: cover; border-radius: 4px;" />',
|
||||||
obj.image.url,
|
obj.image.url,
|
||||||
|
@ -85,6 +87,19 @@ class ImageLibraryAdmin(admin.ModelAdmin):
|
||||||
Display larger preview in detail view.
|
Display larger preview in detail view.
|
||||||
"""
|
"""
|
||||||
if obj.image:
|
if obj.image:
|
||||||
|
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(
|
return format_html(
|
||||||
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 4px;" />',
|
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 4px;" />',
|
||||||
obj.image.url,
|
obj.image.url,
|
||||||
|
|
|
@ -59,11 +59,17 @@ class ImageLibraryWidget(forms.Select):
|
||||||
selected = "selected" if str(image.pk) == str(value) else ""
|
selected = "selected" if str(image.pk) == str(value) else ""
|
||||||
image_url = image.image.url if image.image 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(
|
html_parts.append(
|
||||||
f"""
|
f"""
|
||||||
<div class="image-option {selected}" data-value="{image.pk}">
|
<div class="image-option {selected}" data-value="{image.pk}">
|
||||||
<div class="image-preview">
|
<div class="image-preview">
|
||||||
<img src="{image_url}" alt="{image.alt_text}" loading="lazy">
|
{preview_html}
|
||||||
</div>
|
</div>
|
||||||
<div class="image-info">
|
<div class="image-info">
|
||||||
<span class="image-name">{image.name}</span>
|
<span class="image-name">{image.name}</span>
|
||||||
|
|
|
@ -40,6 +40,8 @@ class ImageLibraryWidget(forms.Select):
|
||||||
for image in images:
|
for image in images:
|
||||||
thumbnail_html = ""
|
thumbnail_html = ""
|
||||||
if self.show_thumbnails and image.image:
|
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(
|
thumbnail_html = format_html(
|
||||||
' <img src="{}" style="width: 20px; height: 20px; object-fit: cover; margin-left: 5px; vertical-align: middle;" />',
|
' <img src="{}" style="width: 20px; height: 20px; object-fit: cover; margin-left: 5px; vertical-align: middle;" />',
|
||||||
image.image.url,
|
image.image.url,
|
||||||
|
@ -64,16 +66,21 @@ class ImageLibraryWidget(forms.Select):
|
||||||
if value:
|
if value:
|
||||||
try:
|
try:
|
||||||
image = ImageLibrary.objects.get(pk=value)
|
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(
|
preview_html = format_html(
|
||||||
'<div class="image-preview" style="margin-top: 10px;">'
|
'<div class="image-preview" style="margin-top: 10px;">'
|
||||||
'<img src="{}" style="max-width: 200px; max-height: 200px; border: 1px solid #ddd; border-radius: 4px;" />'
|
'<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>",
|
"</div>",
|
||||||
image.image.url,
|
image.image.url,
|
||||||
image.name,
|
image.name,
|
||||||
image.width or "?",
|
image.width or "?",
|
||||||
image.height or "?",
|
image.height or "?",
|
||||||
image.get_file_size_display(),
|
image.get_file_size_display(),
|
||||||
|
svg_indicator,
|
||||||
)
|
)
|
||||||
except ImageLibrary.DoesNotExist:
|
except ImageLibrary.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -2,6 +2,8 @@ from django.db import models
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django_prose_editor.fields import ProseEditorField
|
from django_prose_editor.fields import ProseEditorField
|
||||||
|
import mimetypes
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
def validate_image_size(value, mb=1):
|
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")
|
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):
|
class Currency(models.TextChoices):
|
||||||
CHF = "CHF", "Swiss Franc"
|
CHF = "CHF", "Swiss Franc"
|
||||||
EUR = "EUR", "Euro"
|
EUR = "EUR", "Euro"
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import os
|
import os
|
||||||
|
import mimetypes
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from PIL import Image as PILImage
|
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):
|
def get_image_upload_path(instance, filename):
|
||||||
|
@ -35,10 +36,10 @@ class ImageLibrary(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Image file
|
# Image file
|
||||||
image = models.ImageField(
|
image = models.FileField(
|
||||||
upload_to=get_image_upload_path,
|
upload_to=get_image_upload_path,
|
||||||
validators=[validate_image_size],
|
validators=[validate_image_or_svg],
|
||||||
help_text="Upload image file (max 1MB)",
|
help_text="Upload image file (max 1MB) - supports JPEG, PNG, GIF, WebP, BMP, TIFF, and SVG",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Image properties (automatically populated)
|
# Image properties (automatically populated)
|
||||||
|
@ -122,14 +123,86 @@ class ImageLibrary(models.Model):
|
||||||
Update image properties like width, height, and file size.
|
Update image properties like width, height, and file size.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get image dimensions
|
# 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:
|
with PILImage.open(self.image.path) as img:
|
||||||
self.width = img.width
|
self.width = img.width
|
||||||
self.height = img.height
|
self.height = img.height
|
||||||
|
|
||||||
# Get file size
|
|
||||||
self.file_size = self.image.size
|
|
||||||
|
|
||||||
# Save without calling the full save method to avoid recursion
|
# Save without calling the full save method to avoid recursion
|
||||||
ImageLibrary.objects.filter(pk=self.pk).update(
|
ImageLibrary.objects.filter(pk=self.pk).update(
|
||||||
width=self.width, height=self.height, file_size=self.file_size
|
width=self.width, height=self.height, file_size=self.file_size
|
||||||
|
@ -175,6 +248,28 @@ class ImageLibrary(models.Model):
|
||||||
self.usage_count -= 1
|
self.usage_count -= 1
|
||||||
self.save(update_fields=["usage_count"])
|
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):
|
class ImageReference(models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -77,3 +77,33 @@
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
color: #666;
|
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):
|
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.
|
Render an image from the image library by slug or ID.
|
||||||
|
Automatically handles SVG files with proper rendering.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
{% image_library_img "my-image-slug" css_class="img-fluid" %}
|
{% image_library_img "my-image-slug" css_class="img-fluid" %}
|
||||||
|
@ -25,7 +26,35 @@ 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
|
# Use provided alt_text or fall back to image's alt_text
|
||||||
final_alt_text = alt_text or image.alt_text
|
final_alt_text = alt_text or image.alt_text
|
||||||
|
|
||||||
# Build HTML attributes
|
# 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 width:
|
||||||
|
attrs["width"] = width
|
||||||
|
|
||||||
|
if height:
|
||||||
|
attrs["height"] = height
|
||||||
|
|
||||||
|
# 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 = {
|
attrs = {
|
||||||
"src": image.image.url,
|
"src": image.image.url,
|
||||||
"alt": final_alt_text,
|
"alt": final_alt_text,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue