Du libre, du code, des idées, du mélange d'ArraKISS…

De l'épice pour la pensée

Nous sommes aujourd'hui réunis suite à un terrible constat : notre visionneuse tkv craint un max.
C'est pourquoi nous allons ajouter une toute petite option nous permettant de choisir l'image que l'on souhaite visualiser sur notre ordinateur. Cela nous permettra par la même occasion de nous débarasser de certains morceaux qui ne sont plus utiles.

Profitons des dialogues déjà prêts dans la bibliothèque de tkinter en important celle qui nous intéresse :

from tkinter import filedialog

Nous pouvons à la place du téléchargement de l'image appeler une fenêtre de dialogue pour demander l'image à ouvrir :

# Ouverture de l'image
img_path = filedialog.askopenfilename(\
        initialdir=(os.path.expanduser("~")),\
        title="Image à ouvrir",\
        parent=w)

Notez ici plusieurs options :

  • initialdir : Le répertoire de départ où on propose des fichiers à ouvrir. Là, je me sert de os pour afficher le répartoire personnel de l'utilisateur repéré par ~ sous UNIX.
  • title : Le titre de notre fenêtre.
  • parent : La fenêtre dont dépend le dialogue.

Cela ne sufit pas, il faudrait proposer seulement les fichiers qui sont des images. On va donc indiquer les motifs qui nous intéressent (pattern). En gros, ce sont juste les extensions :

# Ouverture de l'image
img_path = filedialog.askopenfilename(\
        initialdir=(os.path.expanduser("~")),\
        filetypes=[('Images', ('.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF')), ('Tout', '.*')],\
        title="Image à ouvrir",\
        parent=w)

On ajoute quand même un petit test pour vérifier que on a bien sélectionné un fichier valide. On utilise la bibliothque "mimetypes" qui est livrée par défaut avec python, et on regarde si le mot "image" apparait dans le type du fichier.
Pour afficher l'erreur, on utilise une fenêtre de dialogue livré avec tkinter après avoir importé la partie qui nous intéresse :

mimtyp = mimetypes.guess_type(img_path)[0] # i.e 'image/jpeg'
if not mimtyp or "image" not in mimtyp :
    # Il n'y a pas le mot "image" dans le mimetype
    from tkinter import messagebox
    messagebox.showerror("Fichier invalide", "Le fichier demandé n'est pas une image.")

Et voilà !

Remarquez qu'il existe un tas de fenêtres de dialogue déjà toutes prêtes pour afficher des avertissements (showwarning) ou des informations (showinfo) ou de poser une simple question (askquestion). Regardez par ici pour plus de détails : http://effbot.org/tkinterbook/tkinter-standard-dialogs.htm.

Je vais rajouter quelques lignes pour afficher une image si elle est passée en argument (par exemple en faisant tkv /home/bibi/image.jpg) :


# Ouverture de l'image
if len(sys.argv) == 2:
    # On a une image en agument
    img_path = sys.argv[1]
else:
    # On va chercher une image sur le disque
    img_path = filedialog.askopenfilename(\
    # ...
    # ...

Il reste encore quelques points à améliorer, mais ça reste déjà plus pratique. ^^
Ça sera tout pour aujourd'hui.
La prochaine fois, nous ajouterons quelques boutons à notre visionneuse d'images :)

Voici le code final :


#!/usr/bin/env python
# -*- coding:Utf-8 -*- 

import os
import sys
import mimetypes
from tkinter import *
from tkinter import filedialog
from PIL import Image, ImageTk


# Notre fenêtre principale
w = Tk()
w.title("tkv : visionneuse d'images") # Un titre
w.configure(background='#000000')     # Fond noir

# Un conteneur dans la fenêtre
mainframe = Frame(w)
mainframe.pack(fill=BOTH,expand=True, padx=15, pady=15)

# Ouverture de l'image
img_path=""
if len(sys.argv) == 2:
    # On a une image en agument
    img_path = sys.argv[1]

if not os.path.isfile(img_path):
    # On va chercher une image sur le disque
    img_path = filedialog.askopenfilename(\
            initialdir=(os.path.expanduser("~")),\
            filetypes=[('Images', ('.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF')), ('Tout', '.*')],\
            title="Image à ouvrir",\
            parent=w)

# Est-ce un fichier valide ?
mimtyp = mimetypes.guess_type(img_path)[0] # i.e 'image/jpeg'
if not mimtyp or "image" not in mimtyp :
    # Il n'y a pas le mot "image" dans le mimetype
    from tkinter import messagebox
    messagebox.showerror("Fichier invalide", "Le fichier demandé n'est pas une image.")
    sys.exit(1)

# Ouverture de l'image
image = Image.open(img_path)
# Dimensions de l'écran : 
gap = 100 # marge par rapport aux bordes de l'écran
screen_width = w.winfo_screenwidth() - gap
screen_height = w.winfo_screenheight() - gap

if image.width > screen_width : 
    image = image.resize((screen_width, int(image.height * screen_width / image.width)), Image.ANTIALIAS)
if image.height > screen_height :   
    image = image.resize((int(image.width * screen_height / image.height), screen_height), Image.ANTIALIAS)

# Chargement de l'image en mémoire
img = ImageTk.PhotoImage(image)

# Insertion de l'image dans le conteneur.
img_widget = Label(mainframe, image=img)
img_widget.pack()

# Démarrage du programme
w.mainloop()

sys.exit(0)

Marre de tous ces tutos qui commencent avec un "Bonjour monde". J'aime pas le monde moi. À la place, je vous propose de faire une visionneuse d'images que nous appelerons "tkv" afin d'aller mater les dessins de Péhä.

Vous aurez besoin de :

  • tkinter avec python3 (python2 sapu, et puis même debian le lâche, il était temps ! ). Installez donc les paquets python3-tk sous debian ou python-tkinter-3.4.5 sous OpenBSD,
  • La bibliothèque de manipulation d'images PIL ou pillow (python3-pil et python3-pil.tkimage sous debian ou py3-Pillow sous OpenBSD),
  • Un éditeur de texte,
  • 20 minutes,
  • Un slip propre.

Voici le code qui va nous servir de point de départ, à enregistrer dans un fichier tkv.py :

#!/usr/bin/env python3
# -*- coding:Utf-8 -*- 

import urllib.request
from tkinter import *
from PIL import Image, ImageTk

Vous pouvez tester ce code avec la commande :

python3.4 tkv.py

Hé, mais c'est nul ton truc !

Patience ^^ . Il ne se passe rien pour l'instant, mis à part l'import des bibliothèques qui vont nous être utiles. S'il y a des erreurs, c'est qu'il vous manque tkinter ou la bibliothèque PIL.

Quelques commentaires maintenant, intégrés au code :

#!/usr/bin/env python3
# -*- coding:Utf-8 -*- 

import urllib.request     # On veut télécharger des trucs
from tkinter import *     # Là, on appelle tkinter
from PIL import Image, ImageTk     # On va afficher des images

On code en dessous de ces lignes. Pour commencer, on affiche la fenêtre avec :

w = Tk()
w.mainloop()

Si vous lancez le script, vous verrez un carré gris.

On est encore un peu loin de ce que l'on souhaite :)
La première ligne crée une instance de notre fenêtre, et l'appel de w.mainloop() la "démarre".
Avant cette dernière ligne, nous allons personnaliser notre application.

Afin d'afficher les images, on va ajouter dans la fenêtre un espace qui servira de conteneur. Il s'agit d'une "Frame" :

mainframe = Frame(w)
mainframe.pack(fill=BOTH, expand=True, padx=15, pady=15)

Remarquez qu'on passe à Frame(w) le widget dans laquelle elle est contenue. On parle de widget "parent".

Ensuite, on attache notre widget avec la fonction "pack". Nous indiquons quelques paramètres afin qu'elle remplisse bien toute la fenêtre (fill=BOTH, expand=True). De plus, on définit une marge, totalement facultative avec les paramètres padx et pady.

Si vous lancez le code, vous ne verrez aucun changement pour l'instant.

Maintenant, on va récupérer une image à afficher. On commence douvement avec une image qui est en ligne que l'on affichera dans mainframe.

Pour télécharger l'image, on utilise les lignes suivantes. C'est du python très bête, puisque ce n'est pas l'objet de l'article. Cette image sera enregistrée dans /tmp/image.jpg.

url="https://pbs.twimg.com/media/C1Zv9ZbXEAA4-W0.jpg"
urllib.request.urlretrieve(url, "/tmp/image.jpg")

Une fois cette image récupérée, nous la chargeons en mémoire :

image = Image.open(img_path)
img = ImageTk.PhotoImage(image)

Ne reste plus qu'à mettre cette image dans un widget. Ici, je vais la mettre dans un widget de type "Label", habituellement utilisé pour afficher du texte, mais qui peut aussi contenir une image :

img_widget = Label(mainframe, image=img)
img_widget.pack()

Remarquez que l'on met l'image dans le conteneur "mainframe", puis qu'on l'accroche dedans avec la fonction "pack".

Testez le code tel qu'il est actuellement :

#!/usr/bin/env python
# -*- coding:Utf-8 -*- 

import urllib.request
from tkinter import *
from PIL import Image, ImageTk

# Notre fenêtre principale
w = Tk()

# Un conteneur dans la fenêtre
mainframe = Frame(w)
mainframe.pack(fill=BOTH,expand=True, padx=15, pady=15)

# Téléchargement de l'image
url="https://pbs.twimg.com/media/C1Zv9ZbXEAA4-W0.jpg"
img_path="/tmp/image.jpg"
urllib.request.urlretrieve(url, img_path)

# Chargement de l'image
image = Image.open(img_path)
img = ImageTk.PhotoImage(image)

# Insertion de l'image dans le conteneur.
img_widget = Label(mainframe, image=img)
img_widget.pack()

# Démarrage du programme
w.mainloop()

Tout fonctionne comme prévu, sauf que l'image est trop grande. On va donc chercher à savoir s'il faut la redimensionner.
Déjà, on récupère les dimensions de l'écran :

gap = 100 # marge par rapport aux bords de l'écran
screen_width = w.winfo_screenwidth() - gap
screen_height = w.winfo_screenheight() - gap

On les compare aux dimensions de l'image. Si nécessaire, on la redimensionne avec un bête calcul de proportionnalité :

if image.width > screen_width : 
    image = image.resize((screen_width, int(image.height * screen_width / image.width)), Image.ANTIALIAS)
if image.height > screen_height :   
    image = image.resize((int(image.width * screen_height / image.height), screen_height), Image.ANTIALIAS)

Et voilà, notre image apparaît maintenant correctement :

Avant de se quitter à la fin de ce gros TP, on va améliorer un petit peu l'apparence de notre fenêtre.

On va déjà lui donner un titre :

w.title("tkv : visionneuse d'images")

Puis on définit un fond noir :

w.configure(background='#000000')

C'est la fenêtre qui est mise en noir. Et puisqu'on a laissé une marge lorsqu'on a ajouté le conteneur principal, ça nous permet de la voir.

C'est déjà un peu mieux :

Ouf, ça fait déjà beaucoup pour une première étape. Vous aurez remarqué que les parties les plus difficiles concernent le téléchargement de l'image et son redimensionnement. À côté de ça, l'utilisation de tkinter est d'une grande simplicité.

La prochaine fois, nous verrons comment choisir une image sur le disque de l'ordinateur et comment ajouter quelques boutons pour faire défiler les images.

À bientôt !

ps : voici le code final :


#!/usr/bin/env python
# -*- coding:Utf-8 -*- 

import urllib.request
from tkinter import *
from PIL import Image, ImageTk


# Notre fenêtre principale
w = Tk()
w.title("tkv : visionneuse d'images") # Un titre
w.configure(background='#000000')     # Fond noir

# Un conteneur dans la fenêtre
mainframe = Frame(w)
mainframe.pack(fill=BOTH,expand=True, padx=15, pady=15)

# Téléchargement de l'image
url="https://pbs.twimg.com/media/C1Zv9ZbXEAA4-W0.jpg"
img_path="/tmp/image.jpg"
urllib.request.urlretrieve(url, img_path)

# Ouverture de l'image
image = Image.open(img_path)
# Dimensions de l'écran : 
gap = 100 # marge par rapport aux bords de l'écran
screen_width = w.winfo_screenwidth() - gap
screen_height = w.winfo_screenheight() - gap

if image.width > screen_width : 
    image = image.resize((screen_width, int(image.height * screen_width / image.width)), Image.ANTIALIAS)
if image.height > screen_height :   
    image = image.resize((int(image.width * screen_height / image.height), screen_height), Image.ANTIALIAS)

# Chargement de l'image en mémoire
img = ImageTk.PhotoImage(image)

# Insertion de l'image dans le conteneur.
img_widget = Label(mainframe, image=img)
img_widget.pack()

# Démarrage du programme
w.mainloop()

On a récemment vu passer plusieurs articles mettant à mal l'auto-hébergement en soulevant un problème simple : "En cas de panne, c'est la panique" (voir l'article de Genma). Puisque je ne suis pas tout à fait d'accord, voici quelques idées à ce sujet.

Tout d'abord, la fameuse coupure de courant. Ça peut arriver à tout le monde, et c'est bien dommage. Dans ce cas, votre serveur est effectivement inaccessible. Cela veut dire que votre blog ne pourra pas être lu pendant quelques temps. Je laisse chacun juger du degré critique de cette panne...

Plus gênant peut-être, vous ne pourrez pas recevoir vos mails... En fait si, mais plus tard ! Il faut savoir que lorsqu'un serveur tente d'envoyer un mail et que le destinataire n'est pas prêt à le recevoir (il est en panne), alors l'expéditeur va tenter à nouveau de l'envoyer à intervalles réguliers. Alors bien sûr, au bout d'un moment, il va abandonner, mais ça vous laisse plusieurs jours avant que ce mail ne soit perdu dans le néant.

Si vous avez malgré tout peur de la panne de courant et si votre serveur ne peut se permettre quelques heures d'inaccessibilité, vous pouvez faire l'acquisition d'un onduleur (sorte de grosse batterie) sur lequel sera branché votre routeur et votre serveur.

Reste les plantages, bugs et autres accidents. À ce problème, il n'y a que les sauvegardes qui pourront restaurer votre serveur. Heureusement, ce n'est pas compliqué.

Plus important, je crois qu'il faut garder en tête que ces pannes peuvent aussi arriver à un hébergeur professionnel. Et lorsque cela arrive, vous êtes totalement dépendant du prestataire, et vous n'avez plus qu'à croiser les doigts qu'il a su faire des sauvegardes de vos données et que la panne sera vite réparée. Alors que si vous vous auto-hébergez, les sauvegardes, vous les avez déjà activées de façon journalière grâce au système de "altroot" d'OpenBSD et un rsync quotidien.

Je m'auto-héberge depuis plusieurs années maintenant. Des pannes, il y en a eu une grosse : lorsque j'ai déménagé, mon serveur était bien évidemment inaccessible. Mais il existe des astuces pour conserver ses mails, j'en avais parlé dans un précédent article. Les autres pannes étaient toutes de ma faute : je bidouillais sur le serveur et j'ai tenté des trucs, parce que ça m'amuse.

Tout ça pour dire que ce genre de choses ne devraient pas être un frein à l'auto-hébergement si vous voulez reprendre le contrôle de vos données.

Même si la ligne de commande est un outil surpuissant, une interface graphique (GUI) peut s'avérer très pratique pour certains. Puisque j'aime bien bidouiller de petits outils en python, j'en suis arrivé à la question : qu'utiliser pour réaliser un GUI ?
J'ai donc découvert pygtk et wxwidgets... Et m'y suis cassé les dents.

En effet, ces deux bibliothèques sont très et même trop complètes. Par ailleurs, installer de si grosses dépendances pour juste quelques petits outils, ça me cassait les pieds. J'ai définitivement laissé tombé l'idée de maîtriser GTK quand j'ai vu le code du 3hg-menu. C'était totalement différent dans les widgets que ce que j'avais pu découvrir en écrivant le handymenu. Quelle plaie de voir des "deprecated" dans ses programmes, et ne pas trouver de la documentation à jour !

Après avoir perdu du temps avec GTK, je me suis tourné vers la bibliothèque Tk que j'avais pu croiser auparavant. Et aujourd'hui, je suis convaincu que c'est la plus adaptée à mes besoin, à la fois simple et puissante. Pour apprendre à programmer, elle est à mon avis la plus pratique.

J'ai donc envie de publier quelques billets sur le sujet, à commencer par celui-ci.

Pourquoi TkInter me paraît être le meilleur choix ?

  • Il est disponible pour python, on profite donc de la syntaxe si agréable.
  • Il est léger, donc rapide.
  • Il est bien pensé et permet de rapidement créer une interface graphique.
  • Il dispose de tout ce dont on peut avoir besoin sans pour autant être trop fournit au risque de se perdre en chemin. Il manque peut-être le sypport de l'icône de notification (systray), mais on peut arranger ça (on y reviendra).
  • Il est stable, en particulier dans le sens où la sytaxe et la façon de l'utiliser n'est pas prête de changer. Ce n'est pas le cas de GTK par exemple, avec des modules très vite dépréciés... Car c'est très pénible de devoir réécrire une application parce que le toolkit graphique a mal évolué.
  • Il est multi-plateforme.

On peut tout de même noter que visuellement, tkinter n'est pas très beau. Cela peut toutefois s'améliorer. Je fais cependant passer la "beauté" d'une interface après son utilité.

En attendant le prochain article qui parlera de l'installation de tkinter, voici quelques références à avoir sous le coude :

ps : blague pourrie générée avec GéGé

Suite au message de Vincent qui me parlait de fail2ban, j'ai eu envie de reprendre ses idées . Il s'agit d'un petit programme qui reproduit le comportement de fail2ban mais pour OpenBSD.
Autrement dit, cet outil surveille les journaux du systèm et repère les erreurs d'identification. Si trop d'erreurs à la suite sont reprérées, on peut supposer que le serveur subit une attaque. Alors, on met sur liste noire l'IP.
Je l'ai modifié pour qu'il soit possible de surveiller plusieurs services et plusieurs journaux en même temps.

Il s'utilise ainsi :

  • Ajout de ces lignes dans le fichier /etc/pf.conf :
table <vilain_bruteforce> persist
block quick from <vilain_bruteforce>

On crée donc une table qui contiendra les IP à blacklister.

  • Création d'un fichier de configuration dans /etc/vilain.conf :
[DEFAULT]
# 24h + 5min
watch_while = 86700
maxtries = 3

#[name of the guardian]
#logfile = /file/to/watch
#regex = regex that return the bad guy IP

[ssh]
logfile = /var/log/authlog
regex = .* Failed .* from ([\S]+) .*

[ssh2]
logfile = /var/log/authlog
regex = .* Connection closed by ([\S]+) .*

La première section permet de définir pendant combien de temps une IP "mauvaise" est surveillée, et le nombre d'essais successifs qu'elle peut faire.
Ensuite, vous pouvez créez autant de sections que vous voulez. Il faut juste veiller à créer une expression régulière qui ressort l'IP du méchant.

  • Lancer vilain, qui ne nécessite que python3.

Tout se télécharge ici : http://git.yeuxdelibad.net/vilain.tgz ou http://git.yeuxdelibad.net/vilain/

J'aimerais avoir vos idées en plus des retours de bugs : quelles regexp sur quels fichiers vous auriez besoin ?

ps : les lecteurs du site obsd4a.net auront certainement remarqué une répétition dans cet article. J'ai besoin de retours d'expérience :) ).

;