gestion des images et logos + adaptation des usages

This commit is contained in:
Jean-Marie Place 2021-11-06 20:26:21 +01:00
commit 08cf4de28d
8 changed files with 310 additions and 136 deletions

View File

@ -44,6 +44,7 @@ import unicodedata
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.sco_logos import find_logo
PE_DEBUG = 0
@ -201,11 +202,11 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
# Logos: (add to logos/ directory in zip)
logos_names = ["logo_header.jpg", "logo_footer.jpg"]
for f in logos_names:
logo = os.path.join(scu.SCODOC_LOGOS_DIR, f)
if os.path.isfile(logo):
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + f)
logos_names = ["header", "footer"]
for name in logos_names:
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
if logo is not None:
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename)
# ----------------------------------------------------------------------------------------

View File

@ -51,14 +51,13 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
"""
import io
import os
import re
import time
import traceback
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from flask import g, url_for, request
from flask import g, request
import app.scodoc.sco_utils as scu
from app import log
@ -68,6 +67,7 @@ from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
import sco_version
from app.scodoc.sco_logos import replace_logo
def pdfassemblebulletins(
@ -141,26 +141,20 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
return text
# --- PDF format:
# handle logos:
image_dir = scu.SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/"
if not os.path.exists(image_dir):
image_dir = scu.SCODOC_LOGOS_DIR + "/" # use global logos
if not os.path.exists(image_dir):
log(f"Warning: missing global logo directory ({image_dir})")
image_dir = None
# la protection contre des noms malveillants est assuré par l'utilisation de secure_filename dans la classe Logo
text = re.sub(
r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
) # remove forbidden src attribute
if image_dir is not None:
text = re.sub(
r'<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>',
r'<img\1src="%s/logo_\2.jpg"\3/>' % image_dir,
text,
)
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
# tentatives d'acceder à d'autres fichiers !
# log('field: %s' % (text))
text = re.sub(
r'<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>',
lambda m: r'<img %s src="%s"%s/>'
% (
m.group(1),
replace_logo(name=m.group(2), dept_id=g.scodoc_dept_id),
m.group(3),
),
text,
) # log('field: %s' % (text))
return sco_pdf.makeParas(text, style, suppress_empty=suppress_empty_pars)

View File

@ -34,30 +34,177 @@ SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos
"""
import imghdr
import os
import re
from pathlib import Path
from flask import abort, current_app
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
import PIL
from PIL import Image as PILImage
GLOBAL = "_SERVER" # category for server level logos
def get_logo_filename(logo_type: str, scodoc_dept: str) -> str:
"""return full filename for this logo, or "" if not found
an existing file with extension.
logo_type: "header" or "footer"
scodoc-dept: acronym
def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX):
"""
# Search logos in dept specific dir (/opt/scodoc-data/config/logos/logos_<dept>),
# then in config dir /opt/scodoc-data/config/logos/
for image_dir in (
scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept,
scu.SCODOC_LOGOS_DIR, # global logos
):
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}")
if os.path.isfile(filename) and os.access(filename, os.R_OK):
return filename
"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
quelquesoit 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
return ""
def write_logo(stream, name, dept_id=None):
Logo(logoname=name, dept_id=dept_id).create(stream)
def replace_logo(name, dept_id):
logo = find_logo(logoname=name, dept_id=dept_id)
if logo is not None:
return logo.filepath
def list_logos():
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):
"Inventorie tous les logos existants (retourne un dictionnaire de dictionnaires [dept_id][logoname]"
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 (read, create), du calcul des chemins et url
et de la récupération des informations sur un logp.
Usage:
logo existant: Logo(<name>, <dept_id>, ...).read()
logo en création: Logo(<name>, <dept_id>, ...).create(stream)
Les attributs filename, filepath, get_url() ne devraient pas être utilisés avant les opérations
read 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 (read) 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 (sinon -> Exception)
il doit exister un et un seul fichier image parmi les types autorisés
(sinon on considère le premier trouvé)
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):
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):
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:
@ -68,28 +215,3 @@ def guess_image_type(stream) -> str:
if not fmt:
return None
return fmt if fmt != "jpeg" else "jpg"
def _ensure_directory_exists(filename):
"create enclosing directory if necessary"
directory = os.path.split(filename)[0]
if not os.path.exists(directory):
current_app.logger.info(f"sco_logos creating directory %s", directory)
os.mkdir(directory)
def store_image(stream, basename):
img_type = guess_image_type(stream)
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
abort(400, "type d'image invalide")
filename = basename + "." + img_type
_ensure_directory_exists(filename)
with open(filename, "wb") as f:
f.write(stream.read())
current_app.logger.info(f"sco_logos.store_image %s", filename)
# erase other formats if they exists
for extension in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]):
try:
os.unlink(basename + "." + extension)
except IOError:
pass

View File

@ -60,11 +60,8 @@ from reportlab.lib.pagesizes import letter, A4, landscape
from flask import g
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import (
CONFIG,
SCODOC_LOGOS_DIR,
LOGOS_IMAGES_ALLOWED_TYPES,
)
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_utils import CONFIG
from app import log
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
import sco_version
@ -219,20 +216,17 @@ class ScolarsPageTemplate(PageTemplate):
)
PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
self.logo = None
# XXX COPIED from sco_pvpdf, to be refactored (no time now)
# Search background in dept specific dir, then in global config dir
for image_dir in (
SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/",
SCODOC_LOGOS_DIR + "/", # global logos
):
for suffix in LOGOS_IMAGES_ALLOWED_TYPES:
fn = image_dir + "/bul_pdf_background" + "." + suffix
if not self.background_image_filename and os.path.exists(fn):
self.background_image_filename = fn
# Also try to use PV background
fn = image_dir + "/letter_background" + "." + suffix
if not self.background_image_filename and os.path.exists(fn):
self.background_image_filename = fn
breakpoint()
logo = find_logo(
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None
)
if logo is None:
# Also try to use PV background
logo = find_logo(
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None
)
if logo is not None:
self.background_image_filename = logo.filepath
def beforeDrawPage(self, canvas, doc):
"""Draws (optional) background, logo and contribution message on each page.

View File

@ -52,6 +52,7 @@ from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
import sco_version
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_pdf import PDFLOCK
from app.scodoc.sco_pdf import SU
@ -201,33 +202,36 @@ class CourrierIndividuelTemplate(PageTemplate):
self.logo_footer = None
self.logo_header = None
# Search logos in dept specific dir, then in global scu.CONFIG dir
for image_dir in (
scu.SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept,
scu.SCODOC_LOGOS_DIR, # global logos
):
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
if template_name == "PVJuryTemplate":
fn = image_dir + "/pvjury_background" + "." + suffix
else:
fn = image_dir + "/letter_background" + "." + suffix
if not self.background_image_filename and os.path.exists(fn):
self.background_image_filename = fn
if template_name == "PVJuryTemplate":
background = find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
else:
background = find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
if not self.background_image_filename and background is not None:
self.background_image_filename = background.filepath
fn = image_dir + "/logo_footer" + "." + suffix
if not self.logo_footer and os.path.exists(fn):
self.logo_footer = Image(
fn,
height=LOGO_FOOTER_HEIGHT,
width=LOGO_FOOTER_WIDTH,
)
footer = find_logo(logoname="footer", dept_id=g.scodoc_dept_id)
if footer is not None:
self.logo_footer = Image(
footer.filepath,
height=LOGO_FOOTER_HEIGHT,
width=LOGO_FOOTER_WIDTH,
)
fn = image_dir + "/logo_header" + "." + suffix
if not self.logo_header and os.path.exists(fn):
self.logo_header = Image(
fn,
height=LOGO_HEADER_HEIGHT,
width=LOGO_HEADER_WIDTH,
)
header = find_logo(logoname="header", dept_id=g.scodoc_dept_id)
if header is not None:
self.logo_header = Image(
header.filepath,
height=LOGO_HEADER_HEIGHT,
width=LOGO_HEADER_WIDTH,
)
def beforeDrawPage(self, canvas, doc):
"""Draws a logo and an contribution message on each page."""

View File

@ -228,7 +228,12 @@ if not os.path.exists(SCO_TMP_DIR) and os.path.exists(Config.SCODOC_VAR_DIR):
# ----- Les logos: /opt/scodoc-data/config/logos
SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos")
LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "jpeg", "png") # remind that PIL does not read pdf
LOGOS_DIR_PREFIX = "logos_"
LOGO_FILE_PREFIX = "logo_"
# forme générale des noms des fichiers logos/background:
# SCODOC_LOGO_DIR/LOGO_FILE_PREFIX<name>.<suffix> (fichier global) ou
# SCODOC_LOGO_DIR/LOGOS_DIR_PREFIX<dept_id>/LOGO_FILE_PREFIX<name>.<suffix> (fichier départemental)
# ----- Les outils distribués
SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools")

View File

@ -36,12 +36,12 @@
<div class="configuration_logo">
<h3>Logo en-tête</h3>
<p class="help">image placée en haut de certains documents documents PDF. Image actuelle:</p>
<div class="img-container"><img src="{{ url_for('scodoc.logo_header', scodoc_dept=scodoc_dept) }}"
<div class="img-container"><img src="{{ url_for('scodoc.get_logo_small', name="header") }}"
alt="pas de logo chargé" /></div>
{{ render_field(form.logo_header) }}
<h3>Logo pied de page</h3>
<p class="help">image placée en pied de page de certains documents documents PDF. Image actuelle:</p>
<div class="img-container"><img src="{{ url_for('scodoc.logo_footer', scodoc_dept=g.scodoc_dept) }}"
<div class="img-container"><img src="{{ url_for('scodoc.get_logo_small', name="footer") }}"
alt="pas de logo chargé" /></div>
{{ render_field(form.logo_footer) }}
</div>

View File

@ -30,6 +30,8 @@ Module main: page d'accueil, avec liste des départements
Emmanuel Viennet, 2021
"""
import io
from app.auth.models import User
import os
@ -38,7 +40,7 @@ from flask import abort, flash, url_for, redirect, render_template, send_file
from flask import request
from flask.app import Flask
import flask_login
from flask_login.utils import login_required
from flask_login.utils import login_required, current_user
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from werkzeug.exceptions import BadRequest, NotFound
@ -61,10 +63,14 @@ from app.decorators import (
scodoc,
permission_required_compat_scodoc7,
)
from app.scodoc.imageresize import ImageScale
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.sco_logos import Logo
from app.scodoc.sco_permissions import Permission
from app.views import scodoc_bp as bp
from PIL import Image as PILImage
@bp.route("/")
@bp.route("/ScoDoc")
@ -240,13 +246,9 @@ def configuration():
if form.validate_on_submit():
ScoDocSiteConfig.set_bonus_sport_func(form.bonus_sport_func_name.data)
if form.logo_header.data:
sco_logos.store_image(
form.logo_header.data, os.path.join(scu.SCODOC_LOGOS_DIR, "logo_header")
)
sco_logos.write_logo(stream=form.logo_header.data, name="header")
if form.logo_footer.data:
sco_logos.store_image(
form.logo_footer.data, os.path.join(scu.SCODOC_LOGOS_DIR, "logo_footer")
)
sco_logos.write_logo(stream=form.logo_footer.data, name="footer")
app.clear_scodoc_cache()
flash(f"Configuration enregistrée")
return redirect(url_for("scodoc.index"))
@ -259,29 +261,81 @@ def configuration():
)
def _return_logo(logo_type="header", scodoc_dept=""):
SMALL_SIZE = (300, 300)
def _return_logo(name="header", dept_id="", small=False, strict: bool = True):
# stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici
filename = sco_logos.get_logo_filename(logo_type, scodoc_dept)
if filename:
extension = os.path.splitext(filename)[1]
return send_file(filename, mimetype=f"image/{extension}")
# génération d'une url
# url = url_for(
# "scodoc.get_logo_small",
# name=name,
# dept_id=dept_id,
# global_if_not_found=global_if_not_found,
# )
logo = sco_logos.find_logo(name, dept_id, strict)
if logo is not None:
suffix = logo.suffix
if small:
with PILImage.open(logo.filepath) as im:
im.thumbnail(SMALL_SIZE)
stream = io.BytesIO()
# on garde le même format (on pourrait plus simplement générer systématiquement du JPEG)
fmt = { # adapt suffix to be compliant with PIL save format
"PNG": "PNG",
"JPG": "JPEG",
"JPEG": "JPEG",
}[suffix.upper()]
im.save(stream, fmt)
stream.seek(0)
return send_file(stream, mimetype=f"image/{fmt}")
else:
return send_file(logo.filepath, mimetype=f"image/{suffix}")
else:
return ""
abort(404)
@bp.route("/ScoDoc/logo_header")
@bp.route("/ScoDoc/<scodoc_dept>/logo_header")
def logo_header(scodoc_dept=""):
"Image logo header"
# "/opt/scodoc-data/config/logos/logo_header")
return _return_logo(logo_type="header", scodoc_dept=scodoc_dept)
# small version (copy/paste from get_logo
@bp.route("/ScoDoc/logos/<name>/small", defaults={"dept_id": None})
@bp.route("/ScoDoc/<int:dept_id>/logos/<name>/small")
@admin_required
def get_logo_small(name: str, dept_id: int):
strict = request.args.get("strict", "False")
return _return_logo(
name,
dept_id=dept_id,
small=True,
strict=strict.upper() not in ["0", "FALSE"],
)
@bp.route("/ScoDoc/logo_footer")
@bp.route("/ScoDoc/<scodoc_dept>/logo_footer")
def logo_footer(scodoc_dept=""):
"Image logo footer"
return _return_logo(logo_type="footer", scodoc_dept=scodoc_dept)
@bp.route(
"/ScoDoc/logos/<name>", defaults={"dept_id": None}
) # if dept not specified, take global logo
@bp.route("/ScoDoc/<int:dept_id>/logos/<name>")
@admin_required
def get_logo(name: str, dept_id: int):
strict = request.args.get("strict", "False")
return _return_logo(
name,
dept_id=dept_id,
small=False,
strict=strict.upper() not in ["0", "FALSE"],
)
# @bp.route("/ScoDoc/logo_header")
# @bp.route("/ScoDoc/<scodoc_dept>/logo_header")
# def logo_header(scodoc_dept=""):
# "Image logo header"
# return _return_logo(name="header", scodoc_dept=scodoc_dept)
# @bp.route("/ScoDoc/logo_footer")
# @bp.route("/ScoDoc/<scodoc_dept>/logo_footer")
# def logo_footer(scodoc_dept=""):
# "Image logo footer"
# return _return_logo(name="footer", scodoc_dept=scodoc_dept)
# essais