L’objectif de cet article est de voir une manière de créer un calendrier d’une année donnée à l’aide de Python et de LaTeX (pour l’export en PDF).

Créer un calendrier à l’aide de Python et LaTeX: préliminaires

Ce n’est pas la première chose que j’ai faite (pour être honnête, c’est même la dernière chose), mais ce que l’on peut faire avant tout, si l’on n’aime pas réfléchir, c’est de demander à une IA de faire le job… Mais il faut tout de même un bon prompt!

Comme je suis flemmard sur la recherche de prompts, j’ai juste demandé à chatGPT:

Ecrit un programme en langage python permettant d'exporter au format pdf un calendrier d'une année donnée

Le prompt n’est pas d’une grande rigueur par rapport à ce que je veux faire, mais c’est un premier test. Voici ce qu’il propose:

import calendar
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas

def export_calendar(year):
    # Créer un nouveau fichier PDF
    filename = f"calendrier_{year}.pdf"
    c = canvas.Canvas(filename, pagesize=letter)

    # Définir les marges et la taille de la page
    margin_x = 50
    margin_y = 50
    page_width, page_height = letter
    usable_width = page_width - 2 * margin_x
    usable_height = page_height - 2 * margin_y

    # Obtenir le calendrier pour l'année donnée
    cal = calendar.calendar(year)

    # Calculer la hauteur de chaque mois
    month_height = usable_height / 4

    # Diviser le calendrier en 4 colonnes
    col_width = usable_width / 4

    # Coordonnées de départ pour le premier mois
    x = margin_x
    y = page_height - margin_y

    # Parcourir tous les mois de l'année
    for month in cal.split("\n"):
        # Rendre les mois centrés horizontalement dans leur colonne
        month_width = c.stringWidth(month, "Helvetica", 12)
        x_offset = (col_width - month_width) / 2

        # Dessiner le mois sur le canvas
        c.setFont("Helvetica", 12)
        c.drawString(x + x_offset, y - 20, month)

        # Descendre d'une hauteur équivalente à celle d'un mois
        y -= month_height

        # Si la hauteur atteint la limite de la page, passer à la colonne suivante
        if y <= margin_y:
            y = page_height - margin_y - month_height
            x += col_width

    # Enregistrer le canvas au format PDF
    c.save()

    print(f"Calendrier pour l'année {year} exporté au format PDF dans le fichier '{filename}'.")

# Exemple d'utilisation
export_calendar(2023)

Ce programme est loin d’être satisfaisant. Voici ce que cela produit:

C’est bien pourri quand-même… Et ce n’est pas étonnant vu la motivation que j’ai eu pour écrire le prompt. Il est donc temps de mettre en route les méninges!

Calendrier à l’aide de Python et LaTeX: le programme

Pour créer un magnifique calendrier au format PDF, il nous faudra:

  • avoir installé une distribution \(\LaTeX\) sur sa machine
  • avoir installé Python (of course!)
  • éventuellement, Stable Diffusion (pur générer des en-têtes de mois par IA)

Tout ceci est gratuit (car je suis un gros radin).

Cahier des charges

Il faudra que chaque mois tienne sur une page.

Sur chaque page, il faudra qu’il y ait une en-tête (une image représentant le mois) sur laquelle sont écrits le mois et l’année.

Je veux que les jours soient écrits sous l’en-tête, et que le calendrier sus sous la forme d’une grille. Dans chaque case de cette grille, il faut qu’il y ait la date (le numéro du jour).

Je veux aussi que l’utilisateur ou l’utilisatrice ait la possibilité de choisir des options: position de la date dans la case, couleur, taille, couleur des trait de la grille, jours écrits en entier, ou en abrégé, ou uniquement avec les initiales.

L’implémentation en Python

Je décide d’utiliser la Programmation Orientée Objet (POO) pour ce projet.

Je veux une classe Calendrier qui admet l’année en attribut. Par exemple,

>>> Calendrier(2023)

devra désigner l’ensemble du calendrier de l’année 2023.

Comme mentionné plus haut, il faut aussi d’autres attributs à la classe pour l’export au format PDF.

Il faut aussi une méthode pour exporter au format PDF. Cette méthode va avoir des arguments (nom du fichier, booléen pour supprimer ou non les fichiers auxiliaires pour générer le PDF et liste des mois qui nous intéressent).

L’exportation du calendrier à l’aide de Python et LaTeX

Je souhaite donc passer par \(\LaTeX\), et utiliser plus particulièrement TiKZ: c’est une solution graphique avec laquelle je me sens à l’aise.

Les attributs de la classe et les arguments de la méthode d’exportation

Attributs de la classe:
---------------------
* year 							: année du calendrier

* dayname 						: style d'écriture des jours
--> trois valeurs possibles :
----- 'long' (par défaut),
----- 'short' (Initiales des jours)
----- 'abr' (trois premières lettres des jours)

* colorday 						: couleur avec laquelle les jours sont écrits
---> Valeur par défaut : 'black'

* position 						: position du nombre indiquant le jour dans les cases
---> Valeurs possibles :
----- 'center' (par défaut),
----- 'bottom left',
----- 'bottom right',
----- 'top left',
----- 'top right'

* scale 						: échelle du nombre
---> Si position = 'center', scale=4 par défaut
---> Sinon scale=2 par défaut

* opacity 						: opacité avec laquelle est écrit le nombre du jour dans les cases
---> Valeur par défaut : 0.2

* textcolor 					: couleur TiKZ avec laquelle est écrite le jour dans les cases
---> Valeur par défaut : 'gray'

* linecolor 					: couleur des traits du calendrier
---> Valeur par défaut : 'black'

* linewidth 					: épaisseur des traits de la grille
---> Valeur par défaut : '1pt'


Arguments de la méthode 'export()':
---------------------------------
* L : liste des mois à exporter.
---> Par exemple, L = [7,8] pour juillet et août.
---> Valeur par défaut : L = range(1,13)

* erase : booléen qui indique si les fichiers auxiliaires, y compris le fichier '.tex', doivent être supprimés.
---> Valeur par défaut : False

* filename : nom du calendrier
---> Valeur par défaut : filename = 'calendrier-<année>'

Les images

Je suis passé par Stable Diffusion pour générer douze en-têtes au format 175 mm x 50mm, mais ça, c’est un choix personnel. En plus, pour l’exemple que je vais mettre ici, je en me suis pas trop cassé la tête et n’ai pas cherché à avoir les plus belles images du monde… loin de là!

Le programme

from calendar import monthcalendar, day_name, month_name
# Pour afficher les jours et les mois en français
import locale
locale.setlocale(locale.LC_TIME,'')
import os

"""
Attributs de la classe:
---------------------
* year 							: année du calendrier

* dayname 						: style d'écriture des jours
--> trois valeurs possibles :
----- 'long' (par défaut),
----- 'short' (Initiales des jours)
----- 'abr' (trois premières lettres des jours)

* colorday 						: couleur avec laquelle les jours sont écrits
---> Valeur par défaut : 'black'

* position 						: position du nombre indiquant le jour dans les cases
---> Valeurs possibles :
----- 'center' (par défaut),
----- 'bottom left',
----- 'bottom right',
----- 'top left',
----- 'top right'

* scale 						: échelle du nombre
---> Si position = 'center', scale=4 par défaut
---> Sinon scale=2 par défaut

* opacity 						: opacité avec laquelle est écrit le nombre du jour dans les cases
---> Valeur par défaut : 0.2

* textcolor 					: couleur TiKZ avec laquelle est écrite le jour dans les cases
---> Valeur par défaut : 'gray'

* linecolor 					: couleur des traits du calendrier
---> Valeur par défaut : 'black'

* linewidth 					: épaisseur des traits de la grille
---> Valeur par défaut : '1pt'


Arguments de la méthode 'export()':
---------------------------------
* L : liste des mois à exporter.
---> Par exemple, L = [7,8] pour juillet et août.
---> Valeur par défaut : L = range(1,13)

* erase : booléen qui indique si les fichiers auxiliaires, y compris le fichier '.tex', doivent être supprimés.
---> Valeur par défaut : False

* filename : nom du calendrier
---> Valeur par défaut : filename = 'calendrier-<année>'

"""

class Calendrier:
    def __init__(self, year, dayname = 'long', colorday = 'black', position = 'center', scale = None, opacity = 0.2 , textcolor = 'gray', linecolor = 'black', linewidth = '1pt'):
        self.year = year
        self.colorday = colorday
        self.position = position
        self.scale = scale
        self.opacity = opacity
        self.textcolor = textcolor
        self.linecolor = linecolor
        self.linewidth = linewidth
                
        if dayname == 'long':
            self.days = [ i[0].upper() + i[1:] for i in day_name ]
        elif dayname == 'short':
            self.days = [ i[0].upper() for i in day_name ] # jours sous la forme 'L', 'M', ...
        elif dayname == 'abr':
            self.days = [ i[0].upper()+i[1:3] for i in day_name ] # jours sous la forme abrégée : 'Lun', 'Mar', ...
        
    def export_month(self, month):
        self.matrix = monthcalendar(self.year,month)
        
        self.tex_month = '\\begin{tikzpicture}[line width='+self.linewidth+',color='+self.linecolor+']\n\\node[below right] (head) at (0,0) { \\includegraphics[width=175mm, height=50mm]{images/"' + month_name[month] +'.png"} };\n\\node[scale=5, text=gray] at ($(head.center)+(0.1,-0.1)$) {\\bfseries ' + month_name[month] + ' ' + str(self.year) + '};\n\\node[scale=5, text=white] at (head.center) {\\bfseries ' + month_name[month] + ' ' + str(self.year) + '};\n'
        for j in range(7):
            self.tex_month += '\\node[below,outer sep=5mm, text='+self.colorday+'] at ($(head.south)+(' + str((j-3)*2.5-0.125) + ',0)$) {' + self.days[j] +'};\n'
            
            
        self.tex_month += '\\begin{scope}[shift={(0,-7)}]\n\\draw (0,0) grid[xstep = 25mm, ystep = 30mm] (7*2.5cm,-' + str(len(self.matrix)) + '*3cm);\n\\end{scope}\n'
            
        if self.position == 'center':
            if self.scale == None:
                self.scale = 4
            for line in self.matrix:
                y = -self.matrix.index( line )*3 - 8.5
                for col in line:
                    if col != 0:
                        self.tex_month += '\\node['+self.textcolor+',opacity='+str(self.opacity)+',scale='+str(self.scale)+'] at (' + str(1.25 + line.index(col)*2.5) + ',' + str(y) + ') {' + str(col) + '};\n'
                        
        elif self.position == 'top right':
            if self.scale == None:
                self.scale = 2
            for line in self.matrix:
                y = -7 - self.matrix.index( line )*3
                for col in line:
                    if col != 0:
                        self.tex_month += '\\node[below left,'+self.textcolor+',opacity='+str(self.opacity)+',scale='+str(self.scale)+'] at (' + str(2.5 + line.index(col)*2.5) + ',' + str(y) + ') {' + str(col) + '};\n'
        
        elif self.position == 'top left':
            if self.scale == None:
                self.scale = 2
            for line in self.matrix:
                y = -7 - self.matrix.index( line )*3
                for col in line:
                    if col != 0:
                        self.tex_month += '\\node[below right,'+self.textcolor+',opacity='+str(self.opacity)+',scale='+str(self.scale)+'] at (' + str(line.index(col)*2.5) + ',' + str(y) + ') {' + str(col) + '};\n'
                        
        elif self.position == 'bottom right':
            if self.scale == None:
                self.scale = 2
            for line in self.matrix:
                y = -10 - self.matrix.index( line )*3
                for col in line:
                    if col != 0:
                        self.tex_month += '\\node[above left,'+self.textcolor+',opacity='+str(self.opacity)+',scale='+str(self.scale)+'] at (' + str(2.5 + line.index(col)*2.5) + ',' + str(y) + ') {' + str(col) + '};\n'
        
        elif self.position == 'bottom left':
            if self.scale == None:
                self.scale = 2
            for line in self.matrix:
                y = -10 - self.matrix.index( line )*3
                for col in line:
                    if col != 0:
                        self.tex_month += '\\node[above right,'+self.textcolor+',opacity='+str(self.opacity)+',scale='+str(self.scale)+'] at (' + str(line.index(col)*2.5) + ',' + str(y) + ') {' + str(col) + '};\n'
        
        self.tex_month += '\\end{tikzpicture}'
        
        return self.tex_month
    
   def export(self, L = range(1,13), filename = 'calendrier-', erase = False):
        filename = filename+str(self.year)
        tex = '\\documentclass[12pt,a4paper]{article}\n\\usepackage{nopageno}\n\\usepackage[hmargin=1.75cm, vmargin=1cm]{geometry}\n\\setlength{\\parindent}{0pt}\n\\usepackage{tikz}\n\\usetikzlibrary{calc}\n'
        tex += '\\begin{document}\n'
        for month in L:
            tex += self.export_month(month)
            if month != L[-1]:
                tex += '\n\\newpage\n'
                
        tex += '\n\\end{document}'
        
        if os.path.isfile(filename+".tex"):
            os.remove(filename+".tex")
            
        fichier = open(filename+".tex","x" , encoding='utf-8')
        fichier.write(tex)
        fichier.close()
        cmd = "pdflatex  --shell-escape -synctex=1 -interaction=nonstopmode "+filename+".tex"
        os.system(cmd)
        
        if erase:
            os.remove(filename+".tex")
            os.remove(filename+".aux")
            os.remove(filename+".log")
            os.remove(filename+".synctex.gz")
        
        cmd = "START "+filename+".pdf"
        os.system(cmd)
        

C = Calendrier(2023)
"""
C.export([7]) # exporte le mois de juillet
C.export([7,8]) # exporte les mois de juillet et août
"""
C.export(erase=True) # exporte le calendrier entier de l'année

La commande suivante:

>>> C = Calendrier(2023)
>>> C.export(erase=True)

donne le résultat ci-dessous:


0 0 votes
Évaluation de l'article
S’abonner
Notification pour
guest
0 Commentaires
Le plus ancien
Le plus récent Le plus populaire
Commentaires en ligne
Afficher tous les commentaires
0
Nous aimerions avoir votre avis, veuillez laisser un commentaire.x