] >
Paru dans GNU/Linux Magazine n°199 de décembre 2016.
Protégé par la licence CC BY-NC-ND 2.0.
Les articles précédents ([1], [2], [3], [4], [5], [6]) vous ont donné envie de créer vos propres codes QR ? Ce sera chose faite ici même, pour le moment on prépare les données binaires.
Dans cet article, on transforme le contenu d’un fichier en des données binaires directement collables dans un code QR.
Maintenant que nous savons lire le contenu des codes QR, même s’ils sont un peu entachés d’erreurs ou décorés d’un logo, nous allons faire l’inverse : créer un code QR avec un petit logo au milieu. N’en abusez pas, un code QR n’est lisible en pratique que par une application dédiée. Moi qui n’ai pas de téléphone portable, je déteste qu’on me propose un code QR tout nu sans explication comme au minimum l’adresse de la page ouèbe.
J’ai utilisé Wikiversity [7] et le QR-code tutorial [8] pour rédiger cet article. Les codes et fichiers sont sur le GitHub du magazine.
On va reprendre en sens inverse (à peu près), les étapes de la lecture d’un code QR. Les fonctions et constantes communes ou utiles sont factorisées dans qrcodeoutils.py et dans qrcodestandard.py, je n’expliquerai leur contenu que s’il n’a pas déjà été étudié précédemment.
On va reprendre la structure de la classe qrdecode.py qui lit une image, mais en partant d’un fichier quelconque.
01: #!/usr/bin/python3 03: from PIL import Image 04: from sys import exit,stderr 05: from qrcorps import * 06: from qrcodestandard import * 07: from argparse import *
On importe les mêmes bibliothèques que lors de la lecture.
09: class qrencode: 11: def __init__(self): 12: [self.arguments,self.nivcor,self.masque,self.message,self.mode,self.longclair, 13: self.version,self.longtoutclair,self.clair,self.longueur, 14: self.tout,self.entrelac,self.dim,self.tabmat,self.gris,self.bontab]=[None]*16
Les noms des attributs sont encore parlants, ils sont donnés dans l’ordre de leur apparition dans le code.
Je réutilise la bibliothèque
01: def options(self): 02: parser=ArgumentParser(description="Crée un code QR.") 03: parser.add_argument("-m",choices=["0","1","2","3","4","5","6","7"],help="Force le choix du masque.") 04: parser.add_argument("-i",required=True,metavar="fichier",help="Fichier texte d’entrée.") 05: parser.add_argument("-c",metavar="image",help="Imagette placée au milieu du code.") 06: parser.add_argument("-t",type=int,metavar="N",default=4,help="Taille d’un module.") 07: parser.add_argument("-o",required=True,metavar="image",help="Image de sortie.") 08: parser.add_argument("-n",choices=["L","M","Q","H"],default="L",help="Niveau de correction.") 09: parser.add_argument("-a",choices=["0","1"],default="0",help="Afficher l’image ou non.") 10: self.arguments=parser.parse_args()
Ici :
Voici ce qu’affiche le programme en cas d’absence d’entrées :
> ./qrencode.py usage: qrencode.py [-h] [-m {0,1,2,3,4,5,6,7}] -i fichier [-c image] [-t N] -o image [-n {L,M,Q,H}] [-a {0,1}] qrencode.py: error: the following arguments are required: -i, -o
Et l’aide :
> ./qrencode.py -h usage: qrencode.py [-h] [-m {0,1,2,3,4,5,6,7}] -i fichier [-c image] [-t N] -o image [-n {L,M,Q,H}] [-a {0,1}] Crée un code QR. optional arguments: -h, --help show this help message and exit -m {0,1,2,3,4,5,6,7} Force le choix du masque. -i fichier Fichier texte d’entrée. -c image Imagette placée au milieu du code. -t N Taille d’un module. -o image Image de sortie. -n {L,M,Q,H} Niveau de correction. -a {0,1} Afficher l’image ou non.
Tâchons maintenant d’ouvrir le fichier donné en entrée et de traduire les options dans nos attributs.
12: def entrees(self): 13: if self.arguments is None: 14: self.options() 16: if self.arguments.t<=0: 17: print("Il faut choisir une taille de module positive.") 18: exit(1) 19: self.nivcor=self.arguments.n 20: if self.arguments.m is None: 21: self.masque=None 22: else: 23: self.masque=int(self.arguments.m)
En fait,
J’omettrai systématiquement le préfixe
Si l'on cherche à entrer une taille de
24: self.message="" 25: try: 26: with open(self.arguments.i,"r") as f: 27: for l in f: 28: self.message=self.message+l 29: except IOError: 30: print("Fichier %s inaccessible."%self.arguments.i) 31: exit(1)
On teste si le fichier existe et est accessible (on lit alors le fichier ligne à ligne, on stocke son contenu binaire dans
Selon le contenu du fichier envoyé, on détermine s’il faut utiliser le mode numérique, alphanumérique ou binaire (en fait, utf-8 et je rappelle que le mode kanji est laissé de côté, il est inclus dans le mode binaire).
01: def carac(self): 02: if None in [self.nivcor,self.masque,self.message]: 03: self.entrees() 05: caracteres=set(self.message) 07: if self.message.isdecimal(): 08: self.mode=0 09: q,r=divmod(len(self.message),3) 10: self.longclair=10*q 11: if r==1: 12: self.longclair+=4 13: elif r==2: 14: self.longclair+=7
La méthode
Je rappelle qu’on utilise le fait que 10 bits suffisent à coder un nombre de trois chiffres, 7 pour un nombre à deux chiffres et 4 pour un nombre à un chiffre (voir les articles sur la lecture de codes QR [1], [2] et [3]).
15: elif caracteres.issubset(set(alphanum)): 16: self.mode=1 17: q,r=divmod(len(self.message),2) 18: self.longclair=11*q+r*6
Dans qrcodestandard.py,
19: else: 20: self.mode=2 21: self.message=self.message.encode("utf-8") 22: self.longclair=len(self.message)*8
Enfin, dans le cas binaire ou kanji, on convertit le message unicode en une liste d’octets. En effet,
On va déterminer la taille du code QR, c’est-à-dire le nombre de modules total du carré.
01: def detversion(self): 02: if None in [self.mode,self.longclair]: 03: self.carac() 04: for self.version in tableau: 05: self.longtoutclair=8*sum(blocs(tableau[self.version][self.nivcor])[1::2]) 06: s=self.longtoutclair-4-longbin(self.version,self.mode) 07: if s>self.longclair: 08: break
def blocs(l): return list(eval(l.replace("×","*").replace("),",")+")))
Elle transforme le contenu brut du tableau en un équivalent exploitable par Python. Ici, elle transforme par exemple
Tant que le message est trop long, on passe à la version supérieure.
10: if s<self.longclair: 11: print("Le message est trop long, veuillez le raccourcir ou choisir un niveau de correction moins fort.") 12: exit(1)
Si le message est toujours trop long (je n’ai pas codé la possibilité de créer un message découpé dans plusieurs codes QR consécutifs), on se plaint et on s’en va.
Il est temps de construire les données à écrire dans le code QR, tout est prêt.
Une fois la version choisie, on peut coder l’en-tête du code QR : le mode puis la longueur en caractères et non en bits ou en octets du message. Il est immédiatement suivi du message et complété éventuellement par un quadruplet de et d’octets de bourrage. La correction d’erreur suit.
01: def donnees(self): 02: if None in [self.longtoutclair,self.version]: 03: self.detversion() 04: self.clair=[0]*(3-self.mode)+[1]+[0]*self.mode 05: self.longueur=dec2bin(len(self.message),longbin(self.version,self.mode)) 06: self.clair+=self.longueur
Le
08: if self.mode==0: 09: q,r=divmod(len(self.message),3) 10: for i in range(q): 11: self.clair+=dec2bin(int(self.message[i*3:i*3+3]),10) 12: if r==1: 13: self.clair+=dec2bin(int(self.message[-1]),4) 14: elif r==2: 15: self.clair+=dec2bin(int(self.message[-2:]),7)
Dans le
16: elif self.mode==1: 17: q,r=divmod(len(self.message),2) 18: for i in range(q): 19: self.clair+=dec2bin(45*alphanum.index(self.message[2*i])\ 20: +alphanum.index(self.message[2*i+1]),11) 21: if r==1: 22: self.clair+=dec2bin(alphanum.index(self.message[-1]),6)
On lit chaque paire de caractères pour coder la somme pondérée de leurs indices dans
23: else: 24: for b in self.message: 25: self.clair+=dec2bin(b,8)
Enfin, dans le cas binaire, on code chaque octet en sa valeur binaire. Un message écrit en kanji utilisera ce mode.
27: def fin(self): 28: if None in [self.clair,self.longueur]: 29: self.donnees() 30: bourrage=dec2bin(0xec11,16) 31: self.clair+=[0,0,0,0] 32: # self.clair+=[0]*(8-len(self.clair)%8) 33: while len(self.clair)<self.longtoutclair: 34: self.clair+=bourrage 35: self.clair=self.clair[:self.longtoutclair]
On termine le message par quatre zéros consécutifs, puis selon les sources, par un bourrage de zéros pour que la longueur du message soit un multiple de huit ou non. Enfin, on complète jusqu’à la longueur finale par une suite de
Le message est prêt à être découpé en blocs pour calculer les codes correcteurs de Reed-Solomon.
01: def reedsolomon(self): 02: if None in [self.clair,self.longueur]: 03: self.fin() 04: posclair,posredondant=court2blocs(tableau[self.version][self.nivcor])
def court2blocs(l): l1=blocs(l) for i in range(0,len(l1),2): l1[i:i+2]=l1[i:i+2][::-1] l2,l3=[0],[0] for i in range(len(l1)//2): l2.append(l2[-1]+l1[2*i]) l3.append(l3[-1]+l1[2*i+1]-l1[2*i]) return l2,l3
Elle retourne les positions des octets de début de chaque bloc de la partie en clair puis de la partie redondante, le dernier octet de la première liste est la longueur totale de la partie en clair, de même pour la partie redondante. Dans notre cas, elle retourne
06: blocsclair=[] 07: for i in range(len(posclair)-1): 08: blocsclair.append(self.clair[8*posclair[i]:8*posclair[i+1]]) 09: blocsredondant=[]
Ici, on crée chaque bloc de données, le premier contient les 111 premiers octets donc les 888 premiers bits, son bloc correcteur contiendra 28 octets, soit 224 bits.
10: for i in range(len(blocsclair)): 11: bloc=blocsclair[i] 12: longredondant=8*(posredondant[i+1]-posredondant[i]) 13: polyclair=message2poly(bloc+[0]*longredondant) 14: modulo=Polynome.construction([1]) 15: for i in range(longredondant//8): 16: modulo*=Polynome.construction([1,F256.exp(i)]) 17: blocsredondant.append(poly2message(polyclair%modulo))
On calcule le complément correcteur de chaque bloc en clair qui n’est que le reste d’une division euclidienne de polynômes qu’on concatène aux données en clair, voir le détail dans la section sur la correction des erreurs [6].
19: self.tout=[] 20: for bloc in blocsclair: 21: self.tout+=bloc 22: for bloc in blocsredondant: 23: self.tout+=bloc
On concatène tout en une seule liste, les blocs en clair à la queue leu leu suivis (dans le même ordre) par les blocs correcteurs.
Pour qu’un petit dessin n’empêche pas la lecture du message d’un code QR, les données sont en fait entrelacées dès qu’un code QR est assez grand selon le tableau suivant :
Niveau de correction | Low | Medium | Quality | High |
---|---|---|---|---|
Entrelacement à partir de la version | 6 | 4 | 3 | 3 |
On le voit par exemple dans le dictionnaire
Les octets des blocs en clair puis correcteurs sont mélangés et donc distribués partout dans l’image. Dans notre cas, on a neuf blocs. On écrit les octets des neuf blocs dans un tableau comme ci-dessous :
Bloc | Octets | ||||||||
---|---|---|---|---|---|---|---|---|---|
n°0 | 0 | 1 | 2 | 3 | … | 108 | 109 | 110 | −1 |
n°1 | 111 | 112 | 113 | 114 | … | 219 | 220 | 221 | −1 |
n°2 | 222 | 223 | 224 | 225 | … | 330 | 331 | 332 | 333 |
… | … | … | … | … | … | … | … | … | … |
n°7 | 782 | 783 | 784 | 785 | … | 890 | 891 | 892 | 893 |
n°8 | 894 | 895 | 896 | 897 | … | 1002 | 1003 | 1004 | 1005 |
Le bloc n°0 contient les 111 premiers octets, soit les octets 0 à 110, le −1 signifie qu’il faut sauter ce rang et passer au bloc suivant. La partie en clair est construite orthogonalement : on lit le tableau de haut en bas d’abord puis de gauche à droite, il s’agit en quelque sorte du tableau transposé de celui-ci en sautant les valeurs négatives. Ainsi, la partie en clair entrelacée contient les octets 0, 111, 222, 334, 446, 558, 670, 782, 894, 1, 112, 223; … 892, 1004, 333, 445, 557, 669, 783, 893, 1005.
On fait la même chose pour les octets des blocs correcteurs qui ont tous la même taille.
01: def entrelacement(self): 02: if self.tout is None: 03: self.reedsolomon() 04: 05: posclair,posredondant=court2long(tableau[self.version][self.nivcor])
On récupère, dans le
Voici le début du contenu de
tableau={1:{"L":"(26,19)","M":"(26,16)","Q":"(26,13)","H":"(26,9)"}, 2:{"L":"(44,34)","M":"(44,28)","Q":"(44,22)","H":"(44,16)"}, 3:{"L":"(70,55)","M":"(70,44)","Q":"2×(35,17)","H":"2×(35,13)"}, 4:{"L":"(100,80)","M":"2×(50,32)","Q":"2×(50,24)","H":"4×(25,9)"}, 5:{"L":"(134,108)","M":"2×(67,43)","Q":"2×(33,15),2×(34,16)","H":"2×(33,11),2×(34,12)"}, 6:{"L":"2×(86,68)","M":"4×(43,27)","Q":"4×(43,19)","H":"4×(43,15)"}, […] 22:{"L":"2×(139,111),7×(140,112)" ,"M":"17×(74,46)","Q":"7×(54,24),16×(55,25)",[…]
Et ainsi de suite jusqu’à 40. J’ai graissé le niveau de correction illustré ci-dessus.
Plutôt que de transformer à la main les informations données dans [7], j’ai préféré créer des fonctions ad hoc qui le font à ma place, elles aussi dans qrcodestandard.py :
def blocs(l): return list(eval(l.replace("×","*").replace("),",")+")))
Dans notre cas, blocs retourne
Comme la manière initiale, relative, n’est pas très utilisable, je l’ai convertie, à l’aide de la fonction
def court2long(l): l1=blocs(l) for i in range(0,len(l1),2): l1[i:i+2]=l1[i:i+2][::-1]
On permute les valeurs deux à deux,
l2=[[-1]] for i in range(len(l1)//2): l2.append([1+j+l2[-1][-1] for j in range(l1[2*i])]) del l2[0]
On récupère la taille de chaque bloc de données en clair et on crée pour chaque valeur, une liste qui donne les positions des bits de chaque bloc. Par exemple,
l3=[] for i in range(max(len(l) for l in l2)): for j in range(len(l2)): try: l3.append(l2[j][i]) except IndexError: l3.append(-1)
Ici, on crée l’enchevêtrement des bits des blocs : on écrit les listes ci-dessus les unes sur les autres dans un tableau et on le lit verticalement. Il s’agit donc d’un
l4=[[max(l3)]] for i in range(len(l1)//2): l4.append([1+j+l4[-1][-1] for j in range(l1[2*i+1]-l1[2*i])]) del l4[0] l5=[] for i in range(max(len(l) for l in l4)): for j in range(len(l4)): l5.append(l4[j][i]) return l3,l5
Et on fait la même chose pour les blocs de la correction d’erreur sans vérification des tailles, ils ont toujours tous la même longueur.
06: pos=posclair+posredondant 07: self.entrelac=[0]*len(self.tout) 08: j=0 09: for i in range(len(self.entrelac)//8): 10: if pos[i]!=-1: 11: self.entrelac[8*j:8+8*j]=self.tout[8*pos[i]:8+8*pos[i]] 12: j+=1
Enfin, le
On a calculé les blocs correcteurs de Reed-Solomon, les blocs sont entrelacés et le message binaire brut est prêt à être déversé dans notre matrice. Ce sera l’objet de notre tout dernier article en page suivante où nous allons dessiner notre code QR avec une petite cerise sur le gâteau.