# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """Gestion des images logos (nouveau ScoDoc 9) Les logos sont `logo_header.` et `logo_footer.` avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png) SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos """ import imghdr import os import re from pathlib import Path from flask import abort, current_app, url_for from werkzeug.utils import secure_filename from app import Departement, ScoValueError from app.scodoc import sco_utils as scu from PIL import Image as PILImage GLOBAL = "_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 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 list_logos(): """Crée l'inventaire de tous les logos existants. L'inventaire se présente comme un dictionnaire de dictionnaire de Logo: [GLOBAL][name] pour les logos globaux [dept_id][name] pour les logos propres à un département (attention id numérique du dept) """ inventory = {GLOBAL: _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.acronym] = _list_dept_logos(dept.id) return inventory def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX): """nventorie 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) 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) 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 logp. Usage: logo existant: Logo(, , ...).select() (retourne None si fichier non trouvé) logo en création: Logo(, , ...).create(stream) Les attributs filename, filepath, get_url() ne devraient pas être utilisés avant les opérations select ou save (le format n'est pas encore connu à ce moement là) """ def __init__(self, logoname, dept_id=None, prefix=scu.LOGO_FILE_PREFIX): """Initialisation des noms et département des logos. if prefix = None on recherche simplement une image 'logoname.*' Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet """ self.logoname = secure_filename(logoname) self.scodoc_dept_id = dept_id self.prefix = prefix or "" self.suffix = None self.dimensions = None if self.scodoc_dept_id: self.dirpath = os.path.sep.join( [ scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + secure_filename(str(dept_id)), ] ) else: self.dirpath = scu.SCODOC_LOGOS_DIR self.basepath = os.path.sep.join( [self.dirpath, self.prefix + secure_filename(self.logoname)] ) self.filepath = None self.filename = None def _set_format(self, fmt): self.suffix = fmt self.filepath = self.basepath + "." + fmt self.filename = self.logoname + "." + fmt def _ensure_directory_exists(self): "create enclosing directory if necessary" if not Path(self.dirpath).exists(): current_app.logger.info(f"sco_logos creating directory %s", self.dirpath) os.mkdir(self.dirpath) def create(self, stream): img_type = guess_image_type(stream) if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES: abort(400, "type d'image invalide") self._set_format(img_type) self._ensure_directory_exists() filename = self.basepath + "." + self.suffix with open(filename, "wb") as f: f.write(stream.read()) current_app.logger.info(f"sco_logos.store_image %s", self.filename) # erase other formats if they exists for suffix in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]): try: os.unlink(self.basepath + "." + suffix) except IOError: pass def select(self): """ Récupération des données pour un logo existant il doit exister un et un seul fichier image parmi de suffixe/types autorisés (sinon on prend le premier trouvé) cette opération permet d'affiner le format d'un logo de format inconnu """ for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES: path = Path(self.basepath + "." + suffix) if path.exists(): self._set_format(suffix) with open(self.filepath, "rb") as f: img = PILImage.open(f) self.dimensions = img.size return self return None def get_url(self): """Retourne l'URL permettant d'obtenir l'image du logo""" return url_for( "scodoc.get_logo", scodoc_dept=self.scodoc_dept_id, name=self.logoname, global_if_not_found=False, ) def get_url_small(self): """Retourne l'URL permettant d'obtenir l'image du logo sous forme de miniature""" return url_for( "scodoc.get_logo_small", scodoc_dept=self.scodoc_dept_id, name=self.logoname, global_if_not_found=False, ) 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: return None return fmt if fmt != "jpeg" else "jpg"