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: