Ajout Logo.html() + remplace deprecated imghdr par puremagic

This commit is contained in:
Emmanuel Viennet 2023-08-01 14:47:22 +02:00
parent 14aa70fdc5
commit 606eaf6d14
2 changed files with 111 additions and 99 deletions

View File

@ -33,7 +33,6 @@ avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png)
SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos
""" """
import glob import glob
import imghdr
import os import os
import re import re
import shutil import shutil
@ -41,6 +40,7 @@ from pathlib import Path
from flask import current_app, url_for from flask import current_app, url_for
from PIL import Image as PILImage from PIL import Image as PILImage
import puremagic
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from app import log from app import log
@ -51,99 +51,6 @@ from app.scodoc.sco_exceptions import ScoValueError
GLOBAL = "_" # category for server level logos GLOBAL = "_" # category for server level logos
def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX):
"""
"Recherche un logo 'name' existant.
Deux strategies:
si strict:
reherche uniquement dans le département puis si non trouvé au niveau global
sinon
On recherche en local au dept d'abord puis si pas trouvé recherche globale
quelque soit la stratégie, retourne None si pas trouvé
:param logoname: le nom recherche
:param dept_id: l'id du département dans lequel se fait la recherche (None si global)
:param strict: stratégie de recherche (strict = False => dept ou global)
:param prefix: le prefix utilisé (parmi scu.LOGO_FILE_PREFIX / scu.BACKGROUND_FILE_PREFIX)
:return: un objet Logo désignant le fichier image trouvé (ou None)
"""
logo = Logo(logoname, dept_id, prefix).select()
if logo is None and not strict:
logo = Logo(logoname=logoname, dept_id=None, prefix=prefix).select()
return logo
def delete_logo(name, dept_id=None):
"""Delete all files matching logo (dept_id, name) (including all allowed extensions)
Args:
name: The name of the logo
dept_id: the dept_id (if local). Use None to destroy globals logos
"""
logo = find_logo(logoname=name, dept_id=dept_id)
while logo is not None:
os.unlink(logo.select().filepath)
logo = find_logo(logoname=name, dept_id=dept_id)
def write_logo(stream, name, dept_id=None):
"""Crée le fichier logo sur le serveur.
Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream"""
Logo(logoname=name, dept_id=dept_id).create(stream)
def rename_logo(old_name, new_name, dept_id):
logo = find_logo(old_name, dept_id, True)
logo.rename(new_name)
def list_logos():
"""Crée l'inventaire de tous les logos existants.
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
[None][name] pour les logos globaux
[dept_id][name] pour les logos propres à un département (attention id numérique du dept)
Les départements sans logos sont absents du résultat
"""
inventory = {None: _list_dept_logos()} # logos globaux (header / footer)
for dept in Departement.query.filter_by(visible=True).all():
logos_dept = _list_dept_logos(dept_id=dept.id)
if logos_dept:
inventory[dept.id] = _list_dept_logos(dept.id)
return inventory
def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
"""Inventorie toutes les images existantes pour un niveau (GLOBAL ou un département).
retourne un dictionnaire de Logo [logoname] -> Logo
les noms des fichiers concernés doivent être de la forme: <rep>/<prefix><name>.<suffixe>
<rep> : répertoire de recherche (déduit du dept_id)
<prefix>: le prefix (LOGO_FILE_PREFIX pour les logos)
<suffix>: un des suffixes autorisés
:param dept_id: l'id du departement concerné (si None -> global)
:param prefix: le préfixe utilisé
:return: le résultat de la recherche ou None si aucune image trouvée
"""
allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
# parse filename 'logo_<logoname>.<ext> . be carefull: logoname may include '.'
filename_parser = re.compile(f"{prefix}(([^.]*.)+)({allowed_ext})")
logos = {}
path_dir = Path(scu.SCODOC_LOGOS_DIR)
if dept_id:
path_dir = Path(
os.path.sep.join(
[scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + str(dept_id)]
)
)
if path_dir.exists():
for entry in path_dir.iterdir():
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
result = filename_parser.match(entry.name)
if result:
logoname = result.group(1)[
:-1
] # retreive logoname from filename (less final dot)
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
return logos if len(logos.keys()) > 0 else None
class Logo: class Logo:
"""Responsable des opérations (select, create), du calcul des chemins et url """Responsable des opérations (select, create), du calcul des chemins et url
ainsi que de la récupération des informations sur un logo. ainsi que de la récupération des informations sur un logo.
@ -212,7 +119,7 @@ class Logo:
def create(self, stream): def create(self, stream):
img_type = guess_image_type(stream) img_type = guess_image_type(stream)
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES: if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
raise ScoValueError("type d'image invalide") raise ScoValueError(f"type d'image invalide ({img_type})")
self._set_format(img_type) self._set_format(img_type)
self._ensure_directory_exists() self._ensure_directory_exists()
filename = self.basepath + "." + self.suffix filename = self.basepath + "." + self.suffix
@ -310,14 +217,118 @@ class Logo:
) )
old_path.rename(new_path) old_path.rename(new_path)
def html(self) -> str:
"élément HTML img affichant ce logo"
return f"""<img class="sco_logo" src="{self.get_url()}" alt="Logo {self.logoname}">"""
def find_logo(
logoname: str,
dept_id: int | None = None,
strict: bool = False,
prefix: str = scu.LOGO_FILE_PREFIX,
) -> Logo | None:
"""
"Recherche un logo 'name' existant.
Deux strategies:
si strict:
recherche uniquement dans le département puis si non trouvé au niveau global
sinon
On recherche en local au dept d'abord puis si pas trouvé recherche globale
quelque soit la stratégie, retourne None si pas trouvé
:param logoname: le nom recherche
:param dept_id: l'id du département dans lequel se fait la recherche (None si global)
:param strict: stratégie de recherche (strict = False => dept ou global)
:param prefix: le prefix utilisé (parmi scu.LOGO_FILE_PREFIX / scu.BACKGROUND_FILE_PREFIX)
:return: un objet Logo désignant le fichier image trouvé (ou None)
"""
logo = Logo(logoname, dept_id, prefix).select()
if logo is None and not strict:
logo = Logo(logoname=logoname, dept_id=None, prefix=prefix).select()
return logo
def delete_logo(name, dept_id=None):
"""Delete all files matching logo (dept_id, name) (including all allowed extensions)
Args:
name: The name of the logo
dept_id: the dept_id (if local). Use None to destroy globals logos
"""
logo = find_logo(logoname=name, dept_id=dept_id)
while logo is not None:
os.unlink(logo.select().filepath)
logo = find_logo(logoname=name, dept_id=dept_id)
def write_logo(stream, name, dept_id=None):
"""Crée le fichier logo sur le serveur.
Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream
"""
Logo(logoname=name, dept_id=dept_id).create(stream)
def rename_logo(old_name, new_name, dept_id):
logo = find_logo(old_name, dept_id, True)
logo.rename(new_name)
def list_logos():
"""Crée l'inventaire de tous les logos existants.
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
[None][name] pour les logos globaux
[dept_id][name] pour les logos propres à un département (attention id numérique du dept)
Les départements sans logos sont absents du résultat
"""
inventory = {None: _list_dept_logos()} # logos globaux (header / footer)
for dept in Departement.query.filter_by(visible=True).all():
logos_dept = _list_dept_logos(dept_id=dept.id)
if logos_dept:
inventory[dept.id] = _list_dept_logos(dept.id)
return inventory
def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
"""Inventorie toutes les images existantes pour un niveau (GLOBAL ou un département).
retourne un dictionnaire de Logo [logoname] -> Logo
les noms des fichiers concernés doivent être de la forme: <rep>/<prefix><name>.<suffixe>
<rep> : répertoire de recherche (déduit du dept_id)
<prefix>: le prefix (LOGO_FILE_PREFIX pour les logos)
<suffix>: un des suffixes autorisés
:param dept_id: l'id du departement concerné (si None -> global)
:param prefix: le préfixe utilisé
:return: le résultat de la recherche ou None si aucune image trouvée
"""
allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
# parse filename 'logo_<logoname>.<ext> . be carefull: logoname may include '.'
filename_parser = re.compile(f"{prefix}(([^.]*.)+)({allowed_ext})")
logos = {}
path_dir = Path(scu.SCODOC_LOGOS_DIR)
if dept_id:
path_dir = Path(
os.path.sep.join(
[scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + str(dept_id)]
)
)
if path_dir.exists():
for entry in path_dir.iterdir():
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
result = filename_parser.match(entry.name)
if result:
logoname = result.group(1)[
:-1
] # retreive logoname from filename (less final dot)
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
return logos if len(logos.keys()) > 0 else None
def guess_image_type(stream) -> str: def guess_image_type(stream) -> str:
"guess image type from header in stream" "guess image type from header in stream"
header = stream.read(512) ext = puremagic.from_stream(stream)
stream.seek(0) if not ext or not ext.startswith("."):
fmt = imghdr.what(None, header)
if not fmt:
return None return None
fmt = ext[1:] # remove leading .
if fmt == "jfif":
fmt = "jpg"
return fmt if fmt != "jpeg" else "jpg" return fmt if fmt != "jpeg" else "jpg"

View File

@ -59,6 +59,7 @@ Pillow==10.0.0
platformdirs==3.10.0 platformdirs==3.10.0
pluggy==1.2.0 pluggy==1.2.0
psycopg2==2.9.6 psycopg2==2.9.6
puremagic==1.15
py==1.11.0 py==1.11.0
pycparser==2.21 pycparser==2.21
pydot==1.4.2 pydot==1.4.2