147 lines
6.5 KiB
Python
147 lines
6.5 KiB
Python
"""Logo-beheer met kwaliteitscheck.
|
||
|
||
GET /api/huisstijl/ -> huidig logo (alle ingelogde gebruikers).
|
||
PUT /api/huisstijl/ -> upload logo (alleen beheerders); voert een
|
||
kwaliteitscheck uit en geeft waarschuwingen terug.
|
||
DELETE /api/huisstijl/ -> logo verwijderen (alleen beheerders).
|
||
"""
|
||
from rest_framework.decorators import api_view, parser_classes
|
||
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
||
from rest_framework.response import Response
|
||
|
||
from .models import Huisstijl
|
||
from .serializers import HuisstijlSerializer
|
||
|
||
TOEGESTAAN = {"png", "jpg", "jpeg", "gif", "webp", "svg"}
|
||
MAX_BYTES = 3 * 1024 * 1024 # 3 MB
|
||
|
||
|
||
def _hex(rgb):
|
||
return "#%02x%02x%02x" % (rgb[0], rgb[1], rgb[2])
|
||
|
||
|
||
def analyseer_logo(f):
|
||
"""Beoordeel een geüpload logo. Geeft {ok, fouten, waarschuwingen, info}.
|
||
|
||
'ok' is False bij harde problemen (verkeerd bestandstype, onleesbaar).
|
||
Anders wordt het bestand opgeslagen, maar mogelijk met waarschuwingen
|
||
(bv. een niet-transparante / gevulde achtergrond, of lage resolutie).
|
||
"""
|
||
naam = (f.name or "").lower()
|
||
ext = naam.rsplit(".", 1)[-1] if "." in naam else ""
|
||
fouten, waarsch = [], []
|
||
if ext not in TOEGESTAAN:
|
||
fouten.append(f"Bestandstype '.{ext}' wordt niet ondersteund. Gebruik PNG, SVG, JPG, GIF of WebP.")
|
||
return {"ok": False, "fouten": fouten, "waarschuwingen": waarsch, "info": {}}
|
||
if getattr(f, "size", 0) and f.size > MAX_BYTES:
|
||
waarsch.append(f"Het bestand is groot ({f.size // 1024} KB); overweeg een kleiner logo.")
|
||
|
||
if ext == "svg":
|
||
return {"ok": True, "fouten": [], "waarschuwingen": waarsch,
|
||
"info": {"formaat": "SVG", "transparant": True, "breedte": None, "hoogte": None, "achtergrond": None}}
|
||
|
||
try:
|
||
from PIL import Image
|
||
except Exception:
|
||
# Zonder Pillow slaan we toch op, maar zonder beeldcontrole.
|
||
return {"ok": True, "fouten": [], "waarschuwingen": waarsch + ["Beeldcontrole niet beschikbaar."], "info": {"formaat": ext.upper()}}
|
||
|
||
try:
|
||
f.seek(0)
|
||
img = Image.open(f)
|
||
img.load()
|
||
except Exception:
|
||
return {"ok": False, "fouten": ["Kan de afbeelding niet lezen of het bestand is beschadigd."], "waarschuwingen": [], "info": {}}
|
||
|
||
breedte, hoogte = img.size
|
||
transparant = False
|
||
achtergrond = None
|
||
heeft_alpha = img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info)
|
||
|
||
if heeft_alpha:
|
||
rgba = img.convert("RGBA")
|
||
hoeken = [rgba.getpixel((0, 0)), rgba.getpixel((breedte - 1, 0)),
|
||
rgba.getpixel((0, hoogte - 1)), rgba.getpixel((breedte - 1, hoogte - 1))]
|
||
if all(p[3] < 16 for p in hoeken):
|
||
transparant = True
|
||
else:
|
||
achtergrond = hoeken[0][:3]
|
||
waarsch.append("De hoeken zijn niet doorzichtig; het logo lijkt een gevulde achtergrond te hebben. "
|
||
"Op een gekleurde balk valt dat lelijk op — gebruik bij voorkeur een transparante PNG of SVG.")
|
||
else:
|
||
rgb = img.convert("RGB")
|
||
hoeken = [rgb.getpixel((0, 0)), rgb.getpixel((breedte - 1, 0)),
|
||
rgb.getpixel((0, hoogte - 1)), rgb.getpixel((breedte - 1, hoogte - 1))]
|
||
achtergrond = hoeken[0]
|
||
eensgezind = len(set(hoeken)) == 1
|
||
if eensgezind and min(achtergrond) > 245:
|
||
waarsch.append("Effen witte achtergrond zonder transparantie. Op een gekleurd vlak valt het witte blok op; "
|
||
"een transparante PNG of SVG is netter.")
|
||
elif eensgezind:
|
||
waarsch.append(f"Effen achtergrondkleur gedetecteerd ({_hex(achtergrond)}). Gebruik bij voorkeur een transparante PNG of SVG.")
|
||
else:
|
||
waarsch.append("Geen transparantie (bv. JPG). Voor een nette weergave op gekleurde vlakken is een transparante PNG of SVG beter.")
|
||
|
||
if breedte < 80 or hoogte < 80:
|
||
waarsch.append(f"Lage resolutie ({breedte}×{hoogte}px); dit kan korrelig printen.")
|
||
|
||
info = {
|
||
"formaat": img.format or ext.upper(),
|
||
"breedte": breedte,
|
||
"hoogte": hoogte,
|
||
"transparant": transparant,
|
||
"achtergrond": _hex(achtergrond) if achtergrond else None,
|
||
}
|
||
return {"ok": True, "fouten": [], "waarschuwingen": waarsch, "info": info}
|
||
|
||
|
||
@api_view(["GET", "PUT", "POST", "DELETE"])
|
||
@parser_classes([MultiPartParser, FormParser, JSONParser])
|
||
def huisstijl(request):
|
||
obj = Huisstijl.actueel()
|
||
schrijven = request.method in ("PUT", "POST", "DELETE")
|
||
if schrijven and not (request.user and request.user.is_authenticated and request.user.is_staff):
|
||
return Response({"detail": "Alleen beheerders (is_staff) kunnen het logo wijzigen. Vraag een beheerder om het logo in te stellen."}, status=403)
|
||
|
||
if request.method == "DELETE":
|
||
if obj.logo:
|
||
obj.logo.delete(save=True)
|
||
return Response(HuisstijlSerializer(obj, context={"request": request}).data)
|
||
|
||
if request.method in ("PUT", "POST"):
|
||
analyse = None
|
||
# Thema-tekstvelden (kleuren/naam) — uit JSON of form-data.
|
||
for veld in ("schoolnaam", "primaire_kleur", "accent_kleur"):
|
||
if veld in request.data:
|
||
waarde = (request.data.get(veld) or "").strip()
|
||
if veld.endswith("_kleur") and waarde and not _is_kleur(waarde):
|
||
return Response({"detail": f"Ongeldige kleur voor {veld}: gebruik een hex-code zoals #2563eb."}, status=400)
|
||
setattr(obj, veld, waarde)
|
||
# Optioneel een nieuw logo.
|
||
f = request.FILES.get("logo")
|
||
if f:
|
||
try:
|
||
analyse = analyseer_logo(f)
|
||
except Exception as e: # noqa: BLE001 — geef een leesbare reden i.p.v. 500.
|
||
return Response({"detail": f"Kon de afbeelding niet verwerken: {e}"}, status=400)
|
||
if not analyse["ok"]:
|
||
return Response({"detail": " ".join(analyse["fouten"]), "kwaliteit": analyse}, status=400)
|
||
f.seek(0)
|
||
if obj.logo:
|
||
obj.logo.delete(save=False)
|
||
obj.logo = f
|
||
try:
|
||
obj.save()
|
||
except Exception as e: # noqa: BLE001
|
||
return Response({"detail": f"Opslaan mislukte: {e}"}, status=400)
|
||
data = HuisstijlSerializer(obj, context={"request": request}).data
|
||
if analyse is not None:
|
||
data["kwaliteit"] = analyse
|
||
return Response(data)
|
||
|
||
return Response(HuisstijlSerializer(obj, context={"request": request}).data)
|
||
|
||
|
||
def _is_kleur(s):
|
||
import re
|
||
return bool(re.fullmatch(r"#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})", s or ""))
|