Python : Scanner un document sous Windows avec l'API WIA
Pour les besoins d'un projet sur lequel je travaille actuellement, je dois accéder à des scanners pour numériser des documents depuis un script Python sur différentes plateformes (principalement Windows et macOS).
Aujourd'hui je vais donc vous parler de la numérisation de document sous Windows via l'API WIA (Windows Image Acquisition), à l'aide de la bibliothèque pywin32.
API Win32, COM et WIA
Pour scanner des documents sous Windows, la solution la plus simple est de passer par l'API WIA (Windows Image Acquisition) qui est disponible de base sur le système. Il existe 2 versions de cette API :
- la version 1.0, présente sur Windows XP (et Windows ME, mais on va faire comme si cette version n'avait jamais existé 😛️),
- et la version 2.0, disponible à partir de Windows Vista.
Étant donné que Windows XP n'est plus supporté depuis 10 ans (8 avril 2014), on va s'intéresser ici à la seconde version de l'API.
WIA fait partie de l'API Win32. Sous ce nom on retrouve les API qui permettent d'accéder et de contrôler divers aspects du système d'exploitation Windows. Pour son fonctionnement, WIA utilise le modèle COM (Component Object Model), qui est une abstraction permettant à différentes briques logiciels, écrites à l'aide de divers langages de programmation, de communiquer entre elles via un protocole commun.
Pour accéder à cette API depuis Python, on va donc devoir passer par le module pythoncom de la bibliothèque pywin32.
Installation des dépendances
Avant de pouvoir se lancer dans le code, il faut bien entendu installer quelques petits trucs sur la machine, à commencer par une version récente de Python 3 que vous pouvez télécharger depuis python.org. Lorsque vous installez Python, pensez à cocher la case « Add python.exe to PATH » histoire que le programme soit accessible depuis le terminal :
Il peut être nécessaire de redémarrer la session pour que la mise à jour du PATH soit prise en compte.
Ensuite, il va nous falloir installer pywin32. Pour ce faire ouvrez votre terminal (faites "<Touche Windows>+R", puis tapez "CMD.EXE"), puis entrez la commande suivante :
pip install pywin32
Note
NOTE : Si vous maitrisez un peu Python, il est préférable d'installer les dépendances dans un virtualenv, mais on va rester sur la procédure la plus simple pour cet article. 😉️
Voilà, avec ça vous êtes parés !
Lister les scanners
Pour commencer, on va voir comment lister les différents scanners disponibles sur la machine.
Pour ce faire, on va créer un petit script Python que je vais, pour ma part, nommer "list_scanners.py" et que je vais placer dans le dossier "ScannerExamples" qui se trouve sur mon bureau. Voici le contenu du script, avec les commentaires qui vont bien :
#!/usr/bin/env python3
# On charge la bibliothèque cliente pour les API Win32/COM
import win32com.client
# On charge et on instancie la classe qui permet de lister et manipuler les
# scanners
device_manager = win32com.client.Dispatch("WIA.DeviceManager")
# On parcourt tous les scanners dispo. Le "DeviceManager" va nous retourner
# des objets "DeviceInfo" qui nous permettront de récupérer les propriétés
# des périphériques et de nous y connecter par la suite.
for device_info in device_manager.DeviceInfos:
print("-------------------------------------")
# On affiche une à une les propriétés du scanner
for prop in device_info.Properties:
print("%s: %s" % (prop.Name, str(prop.Value)))
Pour lancer ce script,
- on ouvre un terminal : "<Touche Windows> + R",
- on se rend dans le dossier contenant le script : "cd C:\Users\Fabien\Desktop\ScannerExamples" (1),
- puis on lance le script Python : "python list_scanners.py" (5).
Si tout s'est bien passé, on devrait voir la liste des scanners s'afficher avec l'ensemble de leurs propriétés (5) :
Sur cette capture on voit que j'ai fait un peu plus de choses que ce que je vous ai dit précédemment, mais pas de panique, je vous explique tout ça rapidement :
- Je me rends dans le dossier contenant mon script.
- Je crée et j'active un virtualenv car je ne souhaite pas installer mes bibliothèques Python directement sur le système, je préfère que ça reste confiné (mais vous n'êtes pas obligés de faire pareil).
- J'installe la bibliothèque "pywin32" comme expliqué précédemment.
- Je liste le contenu du dossier pour montrer que le script "list_scanners.py" est bien présent.
- Je lance mon script comme indiqué ci-dessus. On peut voir qu'un scanner est connecté à ma machine (j'ai censuré les informations qui permettaient d'identifier le modèle exact).
Les seules étapes réellement nécessaires pour que tout fonctionne et pouvoir lancer le script sont la 1, la 3 et la 5.
Scanner un document
Maintenant qu'on sait lister les scanners, voyons comment scanner un document et sauvegarder le résultat dans un fichier.
Voici mon script "scan.py" commenté comme il se doit :
#!/usr/bin/env python3
import win32com.client
device_manager = win32com.client.Dispatch("WIA.DeviceManager")
# On prend le premier scanner disponible
#
# ATTENTION : contrairement aux listes Python, les listes Win32/COM
# commencent à 1 et non à 0.
device_info = device_manager.DeviceInfos(1)
# On se connecte au scanner
device = device_info.Connect()
# On récupère le premier "élément" du périphérique (le scanner à plat pour
# moi). Il est possible qu'il y en ait plusieurs sur un scanner plus
# complexe, mais tous les scanners que j'ai utilisés n'en avait qu'un...
scan_source = device.Items(1)
# On numérise le document en sélectionnant au passage le format de l'image
# souhaité.
#
# NOTE¹ : À priori WIA semble supporter du BMP, du PNG, du JPEG etc. mais
# je n'ai jamais réussi à obtenir autre chose que du BMP (c'est
# peut-être dépendant des drivers ?). Je vous recommande donc de
# choisir BMP et de convertir vous-même l'image dans un autre
# format après coup, en utilisant la bibliothèque PIL par exemple.
#
# NOTE² : la constante « win32com.client.constants.wiaFormatBMP » n'existe
# qu'après avoir exécuté « win32com.client.Dispatch("WIA.DeviceManager") »
# dans le script.
#
# Si jamais cette constante n'était pas disponible pour une raison
# ou pour une autre, sachez que vous pouvez la remplacer par la
# string suivante : "{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}".
wia_image = scan_source.Transfer(win32com.client.constants.wiaFormatBMP)
# On sauvegarde l'image dans le fichier "out.bmp" dans le dossier courant
wia_image.SaveFile("out.bmp")
On peut lancer le script de la même manière que tout à l'heure, avec la commande "python scan.py" :
Sur la capture d'écran ci-dessus, on peut voir que le fichier "out.bmp" a bien été créé suite à l'appel de notre commande. 😃️
Modifier les paramètres du scan
L'API WIA nous permet de configurer quelques paramètres sur le scanner, comme la définition, le contraste, la luminosité, etc.
Voici un petit script permettant d'afficher les options disponibles :
#!/usr/bin/env python3
import win32com.client
device_manager = win32com.client.Dispatch("WIA.DeviceManager")
device_info = device_manager.DeviceInfos(1)
device = device_info.Connect()
scan_source = device.Items(1)
# On liste les différents réglages du scanner
for prop in scan_source.Properties:
print("%s: %s" % (prop.Name, str(prop.Value)))
Et voilà le résultat que ça me donne avec mon scanner une fois le script ci-dessus exécuté :
Item Name: Scan Full Item Name: 0000\Root\Scan Item Flags: 532483 Color Profile Name: C:\Windows\system32\spool\drivers\color\sRGB Color Space Profile.icm Access Rights: 1 Lamp Warm up Time: 120 Current Intent: 0 Horizontal Resolution: 200 Vertical Resolution: 200 Horizontal Start Position: 0 Vertical Start Position: 0 Horizontal Extent: 1700 Vertical Extent: 2340 Rotation: 0 Brightness: 0 Contrast: 0 Item Size: 0 Data Type: 3 Bits Per Pixel: 24 Compression: 0 Channels Per Pixel: 3 Bits Per Channel: 8 Photometric Interpretation: 0 Planar: 0 Buffer Size: 27262976 Threshold: 128 Filename extension: BMP Media Type: 2 Preferred Format: {B96B3CAB-0728-11D3-9D7B-0000F81EF32E} # <= GUID correspondant à BMP Format: {B96B3CAB-0728-11D3-9D7B-0000F81EF32E} # <= GUID correspondant à BMP Pixels Per Line: 1700 Bytes Per Line: 5100 Number of Lines: 2340
Ici on peut voir que l'API nous donne accès à un nombre assez important de paramètres. On peut également constater que le nom des paramètres peut contenir des espaces, il faudra donc y faire attention par la suite... 🙃️
Note
NOTE : Dans les exemples suivants, je vais utiliser la syntaxe du REPL (interface en ligne de commande) de Python :
- les lignes qui commencent par trois chevrons (>>>) sont des instructions Python, tel qu'on pourrait les écrire dans un script,
- et les autres lignes affichent le résultat de l'instruction précédente.
Maintenant qu'on a listé les paramètres disponibles, on va voir comment les modifier. Prenons par exemple le paramètre "Contrast".
Pour lire la valeur de ce paramètre on peut interroger l'API de la manière suivante :
>>> scan_source.Properties("Contrast").Value
0
Si on veut changer cette valeur, il suffit d'en assigner une nouvelle :
>>> scan_source.Properties("Contrast").Value = 1000 # +100% de contraste
On peut vérifier que notre changement a bien été pris en compte :
>>> scan_source.Properties("Contrast").Value
1000
Ici on peut être surpris qu'il faille définir ce paramètre à 1000 pour augmenter le contraste de 100%... C'est un choix arbitraire de l'API (et peut être dépendant du driver pour certains paramètres). Il est heureusement possible de connaitre les valeurs minimales et maximales de chaque paramètre, ainsi que leur « granularité » :
>>> scan_source.Properties("Contrast").SubTypeMin
-1000
>>> scan_source.Properties("Contrast").SubTypeMax
1000
>>> scan_source.Properties("Contrast").SubTypeStep
1
Histoire de recoller un peu les morceaux, voici un script qui scanne un document avec un contraste de +50% sur le premier scanner disponible :
#!/usr/bin/env python3
import win32com.client
device_manager = win32com.client.Dispatch("WIA.DeviceManager")
device_info = device_manager.DeviceInfos(1)
device = device_info.Connect()
scan_source = device.Items(1)
scan_source.Properties("Contrast").Value = scan_source.Properties("Contrast").SubTypeMax // 2
wia_image = scan_source.Transfer(win32com.client.constants.wiaFormatBMP)
wia_image.SaveFile("out.bmp")
C'était pas si compliqué
Au final on peut voir que l'API est très simple d'utilisation, les seules difficultés viennent de la documentation de Microsoft qui est bien trop théorique et bavarde, et de la documentation de pywin32 / pythoncom qui manque peut être d'exemples ciblés.
Si vous voulez creuser le sujet, je vous mets ci-dessous les liens vers les différentes documentations utiles :
À bientôt pour de prochains articles ! 😉️