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 :

Python Windows installer : case à cocher pour ajouter le programme au PATH.

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) :

Capture d'écran du résultat dans CMD.exe

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 :

  1. Je me rends dans le dossier contenant mon script.
  2. 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).
  3. J'installe la bibliothèque "pywin32" comme expliqué précédemment.
  4. Je liste le contenu du dossier pour montrer que le script "list_scanners.py" est bien présent.
  5. 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" :

Capture d'écran de l'exécution de scan.py dans CMD.exe

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 ! 😉️