diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index 21a84bd0a7..04be70bed0 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -33,7 +33,6 @@ avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png) SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos """ import glob -import imghdr import os import re import shutil @@ -41,6 +40,7 @@ from pathlib import Path from flask import current_app, url_for from PIL import Image as PILImage +import puremagic from werkzeug.utils import secure_filename from app import log @@ -51,99 +51,6 @@ from app.scodoc.sco_exceptions import ScoValueError 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: /. - : répertoire de recherche (déduit du dept_id) - : le prefix (LOGO_FILE_PREFIX pour les logos) - : 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_. . 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: """Responsable des opérations (select, create), du calcul des chemins et url ainsi que de la récupération des informations sur un logo. @@ -212,7 +119,7 @@ class Logo: def create(self, stream): img_type = guess_image_type(stream) 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._ensure_directory_exists() filename = self.basepath + "." + self.suffix @@ -310,14 +217,118 @@ class Logo: ) old_path.rename(new_path) + def html(self) -> str: + "élément HTML img affichant ce logo" + return f"""""" + + +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: /. + : répertoire de recherche (déduit du dept_id) + : le prefix (LOGO_FILE_PREFIX pour les logos) + : 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_. . 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: "guess image type from header in stream" - header = stream.read(512) - stream.seek(0) - fmt = imghdr.what(None, header) - if not fmt: + ext = puremagic.from_stream(stream) + if not ext or not ext.startswith("."): return None + fmt = ext[1:] # remove leading . + if fmt == "jfif": + fmt = "jpg" return fmt if fmt != "jpeg" else "jpg" diff --git a/requirements-3.11.txt b/requirements-3.11.txt index 0e3c846c73..75b898bf50 100644 --- a/requirements-3.11.txt +++ b/requirements-3.11.txt @@ -59,6 +59,7 @@ Pillow==10.0.0 platformdirs==3.10.0 pluggy==1.2.0 psycopg2==2.9.6 +puremagic==1.15 py==1.11.0 pycparser==2.21 pydot==1.4.2