145 lines
5.4 KiB
Python
145 lines
5.4 KiB
Python
import re
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
|
|
from django.core.files.base import ContentFile
|
|
from django.db import models
|
|
|
|
from core.models import ClubEvent as CoreClubEvent
|
|
# Pillow
|
|
from PIL import ExifTags, Image, ImageOps
|
|
|
|
def _safe_segment(value: str) -> str:
|
|
return re.sub(r"[^A-Za-z0-9._-]", "", (value or ""))
|
|
|
|
|
|
def club_event_picture_original_upload_to(instance, filename: str) -> str:
|
|
ext = Path(filename or "").suffix.lower() or ".bin"
|
|
date_ref = _safe_segment(getattr(instance.event, "date_ref", "")) or "event"
|
|
person = _safe_segment(instance.person_name) or "persona"
|
|
stem = _safe_segment(Path(filename or "image").stem) or "image"
|
|
return f"club/events/{date_ref}/{person}/original/{stem}{ext}"
|
|
|
|
|
|
def club_event_picture_compressed_upload_to(instance, filename: str) -> str:
|
|
date_ref = _safe_segment(getattr(instance.event, "date_ref", "")) or "event"
|
|
person = _safe_segment(instance.person_name) or "persona"
|
|
stem = _safe_segment(Path(filename or "image").stem) or "image"
|
|
return f"club/events/{date_ref}/{person}/compressed/{stem}.jpg"
|
|
|
|
|
|
def _to_degrees(value):
|
|
if isinstance(value, tuple) and len(value) == 2:
|
|
return float(value[0]) / float(value[1] or 1)
|
|
return float(value)
|
|
|
|
|
|
def _dms_to_decimal(dms, ref):
|
|
try:
|
|
deg = _to_degrees(dms[0])
|
|
minute = _to_degrees(dms[1])
|
|
second = _to_degrees(dms[2])
|
|
decimal = deg + (minute / 60.0) + (second / 3600.0)
|
|
if ref in {"S", "W"}:
|
|
decimal = -decimal
|
|
return decimal
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
class ClubEventPicture(models.Model):
|
|
event = models.ForeignKey(CoreClubEvent, on_delete=models.CASCADE, related_name="pictures")
|
|
person_name = models.TextField(default="")
|
|
location = models.TextField(default="", blank=True)
|
|
title = models.TextField(default="", blank=True)
|
|
hidden = models.BooleanField(default=False)
|
|
original_image = models.FileField(upload_to=club_event_picture_original_upload_to)
|
|
compressed_image = models.FileField(upload_to=club_event_picture_compressed_upload_to, blank=True, null=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = "club_event_pictures"
|
|
|
|
def __str__(self):
|
|
return f"{self.event.date_ref} - {self.title or self.original_image.name}"
|
|
|
|
def _extract_exif_location(self):
|
|
try:
|
|
if not self.original_image:
|
|
return ""
|
|
self.original_image.seek(0)
|
|
image = Image.open(self.original_image)
|
|
exif = image.getexif()
|
|
gps_info = {}
|
|
gps_tag_id = None
|
|
for key, value in ExifTags.TAGS.items():
|
|
if value == "GPSInfo":
|
|
gps_tag_id = key
|
|
break
|
|
if gps_tag_id is None or gps_tag_id not in exif:
|
|
return ""
|
|
|
|
raw_gps = exif.get(gps_tag_id, {})
|
|
for key, val in raw_gps.items():
|
|
tag_name = ExifTags.GPSTAGS.get(key, key)
|
|
gps_info[tag_name] = val
|
|
|
|
lat = _dms_to_decimal(gps_info.get("GPSLatitude"), gps_info.get("GPSLatitudeRef"))
|
|
lon = _dms_to_decimal(gps_info.get("GPSLongitude"), gps_info.get("GPSLongitudeRef"))
|
|
if lat is None or lon is None:
|
|
return ""
|
|
return f"{lat:.6f}, {lon:.6f}"
|
|
except Exception:
|
|
return ""
|
|
|
|
def _build_compressed_content(self):
|
|
self.original_image.seek(0)
|
|
image = Image.open(self.original_image)
|
|
image = ImageOps.exif_transpose(image)
|
|
# JPEG no soporta alpha; cuando hay transparencia la aplanamos en blanco.
|
|
if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info):
|
|
alpha_image = image.convert("RGBA")
|
|
background = Image.new("RGB", alpha_image.size, (255, 255, 255))
|
|
background.paste(alpha_image, mask=alpha_image.getchannel("A"))
|
|
image = background
|
|
elif image.mode != "RGB":
|
|
image = image.convert("RGB")
|
|
image.thumbnail((800, 800))
|
|
|
|
buffer = BytesIO()
|
|
image.save(buffer, format="JPEG", quality=60, optimize=True)
|
|
return ContentFile(buffer.getvalue())
|
|
|
|
def _original_image_changed(self) -> bool:
|
|
if not self.original_image:
|
|
return False
|
|
if not self.pk:
|
|
return True
|
|
previous = type(self).objects.filter(pk=self.pk).values_list("original_image", flat=True).first()
|
|
return str(previous or "") != str(self.original_image.name or "")
|
|
|
|
def save(self, *args, **kwargs):
|
|
should_refresh_assets = self._original_image_changed()
|
|
|
|
if not self.title and self.original_image:
|
|
self.title = Path(self.original_image.name).stem
|
|
|
|
if should_refresh_assets and self.original_image:
|
|
if not self.location:
|
|
self.location = self._extract_exif_location()
|
|
|
|
compressed_content = self._build_compressed_content()
|
|
compressed_name = Path(self.original_image.name).stem + ".jpg"
|
|
self.compressed_image.save(compressed_name, compressed_content, save=False)
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class ClubEvent(CoreClubEvent):
|
|
class Meta:
|
|
proxy = True
|
|
app_label = "club"
|
|
verbose_name = "Evento"
|
|
verbose_name_plural = "Eventos"
|