Le compression JPEG

Jusqu’à là, nous avons vu des techniques de compression qui sont appliquées aux formats d’images dits sans pertes, comme PNG ou GIF (avec lesquels la qualité de l’image n’est pas dégradée, sauf si on utilise une palette de couleurs restreinte).

Je vais maintenant vous parler de JPEG, un des formats de compression d’images avec pertes (beurk, les artéfacts !) les plus répandus. Son grand argument ? Il compresse mieux (surtout les photos). Sachez aussi que des techniques semblables sont utilisées dans des formats et codecs comme MP3 (pour le son) ou comme MPEG, HEVC, etc. (pour la vidéo), comme nous le verrons plus tard.

La compression : les différents traitements opérés par JPEG

L’espace de couleurs YCbCr

Jusqu’à là, vous êtes habitué à voir des images où chaque pixel est composé de trois valeurs de couleurs : rouge, bleu, vert (RGB).

RGB, cependant, n’est pas ce qu’on utilise historiquement à la télévision, et ce n’est pas non plus ce qu’on utilise dans JPEG. Dans JPEG, on utilise un autre espace de couleurs, appelé YCbCr, qui utilise trois composantes : l’intensité, le bleu et le rouge. Quand on ne garde « que » l’intensité, on a notre image en noir et blanc, tout simplement : le bleu et le rouge ne servent qu’à la coloriser comme il faut.

Où est passé le vert ? L’intensité est en fait un savant mélange entre le rouge, le vert et le bleu (dont l’importance de chacune a été pondérée, puisque toutes ne sont pas aussi criardes pour nos yeux), et le bleu et le rouge sont exprimés comme leur différence avec cette intensité (toutes proportions gardées, il faut en effet les faire rentrer dans un nombre de 0 à 255). Le vert peut donc être « deviné » en enlevant ce qu’il reste de l’intensité après avoir corrigé le bleu et le rouge.

Mais pourquoi ne pas « juste » utiliser RGB ? Eh bien il y a plusieurs raisons, toutes très pertinentes :

  • Les contours ont tendance à être précis et à varier brusquement dans les images, alors que les teintes de couleurs, beaucoup moins. Elles sont donc plus compressibles.
  • Les humains font très attention aux contours des formes mais moins aux couleurs : du coup, ça nous permet de recourir à une technique grossière, qu’on va découvrir tout de suite…
Illustration de la conversion de RGB à YCbCr (source : CC BY-NC-SA Antho59290)

Vous rencontrerez peut-être aussi le terme « YUV ». Il s’agit ni plus ni moins de la version analogique de YCbCr, les deux termes sont donc assez semblables.

Le chroma subsampling (sous-échantillonage chromatique)

Avec JPEG, dans un premier temps, si les contours seront gardés tels quels (ou presque), eh bien les teintes de couleurs, on va leur faire quelque chose de limite : on va les étaler comme du Nutella. Choquant, hein ? Si, dans les images fortement compressés avec JPEG, vous avez déjà eu l’impression que les couleurs débordaient légèrement de leurs contours, eh bien ce n’était pas qu’une impression.

On va faire trois images séparées (exactement comme je l’ai monté plus haut : une pour l’intensité, une pour le rouge, une pour le bleu) et on séparer le bleu et le rouge l’une de l’autre, et l’intensité, on n’y touchera pas à cette étape, mais le bleu et le rouge, on va les rétrécir (par exemple les rendre deux fois plus petites). Je dis par exemple, car il y a plusieurs réglages. Ces réglages s’expriment comme il suit :

La notation la plus courante, appelée « notation J:a:b », est la suivante : trois nombres séparés par des deux-points, où :

  • Le premier nombre est le nombre de pixels d’intensité conservés pour 4 pixels affichés, sur chaque ligne ;
  • Le second nombre est le nombre de pixels de couleurs conservés pour 4 pixels affichés, sur les lignes paires ;
  • Le troisième nombre est le nombre de pixels de couleurs conservés pour 4 pixels affichés, sur les lignes impaires.
Les différents réglages du chroma subsampling avec JPEG.
Les différents réglages du chroma subsampling avec JPEG.

Cette grossière astuce s’appelle le chroma subsampling.

Notez qu’elle n’est pas toujours utilisée, le réglage par défaut de GIMP par exemple est d’encoder les images en 4:4:4. Le plus important se situe ensuite.

Le découpage en blocs

Sur JPEG, néanmoins on ne travaille pas sur une image entière : on travaille sur des blocs de 8x8 pixels (séparément en ce qui concerne l’intensité, le bleu et le rouge, donc).

Dans les faits, si on étale nos couleurs sur du 4:2:2, on travaillera en fait sur 8x8 valeurs, qui correspondent ainsi à 16x8 pixels affichés.

Dans les faits, si la taille d’une image n’est pas exactement un multiple de 8 dans un axe donné, et que la compression est forte, de légers défauts de compression pourraient apparaître. C’est un des soucis de JPEG.

Dans le jargon de la vidéo, ces blocs sur lesquels s’effectuent la compression s’appellent « macroblocs ». Avec JPEG, ce sont des « MCU » (Minimum Coded Units).

Le codage DCT

DCT (Discrete Cosine Transform) est une fonction mathématique qui transforme notre tableau de 8x8 valeurs de couleurs, ou d’intensité, en tableau de 8x8 nouvelles valeurs (qui n’ont rien à voir mais permettent aussi de reconstituer l’image).

En gros, avec DCT, on va décomposer notre bloc de pixels en rayures de différentes tailles, des rayures les plus amples (un aplat complet) aux plus rapprochées (des stries d’un pixel sur deux). Chaque ensemble de stries aura sa propre intensité (ce sera un nombre de notre nouvelle table). En combinant toutes nos stries, on retrouve l’image d’origine.

Voici à quoi ressemblent les différentes stries :

Les stries de DCT, sur lesquelles n'ont pas encore été appliquées d'intensités particulières.
Les stries de DCT, sur lesquelles n'ont pas encore été appliquées d'intensités particulières.

Les plus amples sont dans le coin en haut à gauche, elles le deviennent moins au fur et à mesure qu’on s’en éloigne. Celles tout à gauche sont horizontales, celles tout en haut sont verticales, celles en diagonale sont en damier, les autres sont plus ou moins en biais.

Les stries se combinent en se superposant. Vous en trouverez une illustration interactive ici (cliquez sur les boutons et les cases) : http://sorciersdesalem.math.cnrs.fr/Vulgarisation/JPEG/jpeg-DCT.html

Après les avoir toutes combinées, à leurs intensités respectives, on retrouve notre image finale.

DCT peut s’implémenter de la façon suivante en Python :

from math import cos, pi, sqrt
from copy import deepcopy
from typing import List

"""
    On part d'un tableau de valeurs y, x (ligne, colonne) et on en
    renvoie un nouveau.
"""

def encoder_dct(ancien_tableau : List[List[int]]) -> List[List[int]]:
    
    # Le nouveau tableau aura le même nombre d'entrées (et de sous-
    # entrées) que le tableau d'origine, mais pas les mêmes valeurs,
    # c'est pourquoi on commence à copier le tableau d'origine (et
    # ses tableaux imbriqués) pour en créer un distinct.
    
    nouveau_tableau : List[List[int]] = deepcopy(ancien_tableau)
    
    for nouveau_y in range(8):
        for nouveau_x in range(8):
            
            nouvelle_valeur = 0
            
            for ancien_y in range(8):
                for ancien_x in range(8):
                    
                    # Le cosinus retourne un facteur qui pondère
                    # la valeur selon si on est dans la strie ou non.

                    nouvelle_valeur += (
                        ancien_tableau[ancien_y][ancien_x] *
                        cos(((2 * ancien_y + 1) * nouveau_y * pi) / 16) *
                        cos(((2 * ancien_x + 1) * nouveau_x * pi) / 16)
                    )
            
            # Si on est au bord du tableau (aplat complet),
            # cosinus retournera toujours 1 et le nombre
            # pourrait être très gros : on va donc le
            # réduire un peu
            
            if nouveau_y == 0:
                nouvelle_valeur /= sqrt(2)
            if nouveau_x == 0:
                nouvelle_valeur /= sqrt(2)
            nouvelle_valeur /= 4
            
            nouveau_tableau[nouveau_y][nouveau_x] = nouvelle_valeur
    
    return nouveau_tableau

Pour que vous compreniez bien : on utilise la fonction cosinus pour voir si, à un endroit donné de l’ancien tableau, on se trouve dans une des stries qu’on a montré plus haut, puis on fait en gros la somme (ou la moyenne) des pixels qui se trouvent à l’origine sur cette strie. Puis on range une intensité dans notre nouveau tableau, qu’on va associer au motif de strie donné.

Jusqu’ici, DCT a transformé la façon d’exprimer notre image, mais sa taille n’a pas diminué : on l’a juste organisée différemment (c’est donc, comme BWT, un pré-traitement). En quoi DCT peut-il nous aider à compresser ?

La réponse est simple : les stries les plus fines représentent les plus petits détails. Les plus grosses stries représentent les détails les plus épais et visibles (aplats, semi-aplats, etc.). On peut donc ne que garder précises les valeurs des plus grosses stries, et arrondir, de plus en plus brutalement, les petites, sans trop dégrader la qualité de l’image (on dégradera essentiellement les « détails »). C’est ce qui se passera à l’étape suivante : la quantification.

La quantification

Vient donc ensuite le moment où on perd (intelligemment) en qualité : les différentes composantes de la table DCT générée, pour chaque canal (intensité, bleu, rouge), seront arrondies à des valeurs plus petites (on fera une simple division entière + arrondi sur chaque valeur). Les valeurs qui contiennent le plus de détail (par exemple celle tout en haut à gauche) seront moins arrondies que les autres, et perdront moins de précision.

L’objet de cette étape, vous l’avez deviné, c’est de faire des nombres plus petits et donc qui prendront moins de place par la suite.

Reprenons : après avoir converti notre image en trois canaux YCbCr (intensité, bleu, rouge), nous avons appliqué le chroma subsampling (rétréci les canaux de couleur) et pour chaque bloc de 8x8, nous avons appliqué la transformation DCT (qu’il faudra refaire dans l’autre sens au décodage). Nous avons maintenant des blocs de 8x8 valeurs DCT à traiter.

Par défaut, nous allons utiliser une table de valeurs, dite matrice de quantification, qui nous donnera le nombre par lequel on divisera chacune des valeurs de notre tableau DCT de manière raisonnable. Il y a une matrice de quantification par défaut fournie par le standard JPEG, mais attention, Photoshop utilise sa propre matrice par exemple. Ce qui fait que la matrice de quantification est généralement incluse dans le fichier à décoder.

La matrice par défaut, fournie par le standard JPEG, est la suivante :

Q=[1611101624405161121214192658605514131624405769561417222951878062182237566810910377243555648110411392496478871031211201017292959811210010399].Q= \begin{bmatrix} 16 & 11 & 10 & 16 & 24 & 40 & 51 & 61 \\ 12 & 12 & 14 & 19 & 26 & 58 & 60 & 55 \\ 14 & 13 & 16 & 24 & 40 & 57 & 69 & 56 \\ 14 & 17 & 22 & 29 & 51 & 87 & 80 & 62 \\ 18 & 22 & 37 & 56 & 68 & 109 & 103 & 77 \\ 24 & 35 & 55 & 64 & 81 & 104 & 113 & 92 \\ 49 & 64 & 78 & 87 & 103 & 121 & 120 & 101 \\ 72 & 92 & 95 & 98 & 112 & 100 & 103 & 99 \end{bmatrix}.

Notez comment les valeurs DCT proches du coin en haut à gauche seront divisées par des nombres moins gros, et seront donc mieux préservées.

Notre matrice va avoir des chiffres plus ou moins gros en fonction de la qualité que l’on souhaite préserver sur l’image. Les chiffres ci-dessus s’appliquent pour une qualité de 50 %. Si on veut utiliser une qualité plus ou moins élevée, il va falloir effectuer ce qui suit sur chaque nombre de la matrice:

  • Calculateur un multiplicateur (qui va nous permettre d’avoir des diviseurs de valeurs DCT plus élevés pour une qualité plus basse) :
    • Si la qualité est inférieure à 50 %, alors notre multiplicateur sera de 5000 % divisé par la qualité (par exemple, 5000÷40=1255000 \div 40 = 125, 5000÷10=5005000 \div 10 = 500)
    • Si la qualité est supérieure ou égale à 50 %, alors notre multiplicateur sera de 200 % moins deux fois la qualité (par exemple, 20050×2=100200 - 50 \times 2 = 100 : logique, notre matrice est pour une qualité de 50 % à la base, 20075×2=50200 - 75 \times 2 = 50, 20095×2=10200 - 95 \times 2 = 10)
  • Appliquer notre multiplicateur sur chaque valeur de la matrice de quantification, puis effectuer un arrondi (par exemple, si notre multiplicateur est de 125 %, alors on va faire round(valeur * 1.25) sur chaque entrée de la matrice)
    • Attention, si jamais on obtient 0 après avoir effectué notre arrondi, on met 1 à la place. Pour éviter de faire des divisions par zéro.

Pour illustration, voici ce que l’on obtiendrait en faisant varier graduellement la qualité d’une image. La qualité la plus faible est à gauche et la qualité la plus élevée est à droite ; vers le milieu, vous aurez typiquement les artéfacts que l’on obtient en appliquant une compression JPEG de qualité moyenne.

Maintenant, vous savez ce qui produit ces artéfacts ! Notez comment les blocs de couleurs font 8x8 pixels. (Illustration CC-BY Michel Gäbler / Azatoth)

Le codage entropique

Pour coder les valeurs de l’image, JPEG utilise en alternance un symbole Huffman qui contient un nombre de valeurs nulles à sauter et la taille en bits de la prochaine valeur non-nulle, suivi de la prochaine valeur non-nulle (sans Huffman).

Grâce à la quantification, nos valeurs sont déjà « petites », et du fait des propriétés de DCT, elles ne devraient pas avoir beaucoup tendance à se répéter, c’est pourquoi il ne serait pas forcément intéressant d’utiliser Huffman dessus.

On a donc des tuples de trois informations qui sont codées en boucle dans le fichier :

Taille Valeur
4 bits hauts de la valeur Huffman Nombre de valeurs à zéro à sauter
4 bits bas de la valeur Huffman Taille de la prochaine valeur n’étant pas à zéro (ou 15 lorsque 15 valeurs successives à zéro se suivent)
Variable, voir ci-dessus Prochaine valeur DCT quantifiée (ou rien lorsque 15 valeurs successives à zéro se suivent)

Lorsqu’il n’y a plus de valeurs non-nulles à lire, on peut rencontrer le symbole Huffman spécial « EOB » (End of Block - codé par un « 0 »).

Dans quel ordre les valeurs DCT sont-elles encodées ? Étonnamment, pas tout droit, mais en zig-zag : les valeurs les plus proches du bord en haut à gauche de l’image seront encodées en premier. En effet, étant donné qu’elles ont tenance à être plus élevées, cela veut dire qu’il y aura de plus en plus de chances d’avoir des valeurs à « 0 » qui se suivent en s’approchant du bord opposé de l’image.

L’ordre de codage des valeurs DCT en zig-zag avec JPEG (domaine public, Alex Khristov).

Cela vaut pour le mode de codage par défaut de JPEG, l’« encodage séquentiel ». Il existe aussi un autre mode d’encodage, le mode d’« encodage progressif » où au lieu de parcourir un bloc donné à la fois en zig-zag, vous encodez d’abord la valeur située dans le coin supérieur gauche de chaque bloc, puis la 2ème valeur de chaque bloc, puis la 3ème valeur de chaque bloc, etc. jusqu’aux coins inférieurs droits.

Le mode progressif peut être un peu plus efficace à la compression (plus de zéros cumulés), mais est moins répandu.

À noter que le premier symbole Huffman ne contient qu’une taille, correspondant à une valeur codée sur 8 bits. Il existera donc des tables Huffman différentes pour les tailles du coin gauche supérieur de chaque bloc (la première valeur de la DCT, appelée « DC », qui correspond donc à la teinte moyenne de tout le bloc et est plus élevée que les autres) et celles du reste des valeurs DCT (celles qui ne sont pas un aplat, elles sont appelées « AC »).

Les valeurs DC sont aussi codées en tant qu’une différence avec la précédente, afin de prendre moins de bits.

Les valeurs DCT en elles-mêmes, qui sont donc de tailles de bits variables, sont codées comme il suit : si le premier bit est un « 1 », alors il s’agit d’une valeur positive à lire telle quelle. Si le premier bit est un « 0 », implicitement il s’agit d’une valeur négative, et il faut inverser les bits pour retrouver celle d’origine.

Pour inverser les bits, en Python, on peut utiliser l’opération XOR (Exclusive OR) avec un masque de taille équivalente à la valeur. Si notre valeur codée sur 4 bits est 0001, alors on va faire : 0b0001 ^ 0b1111, ce qui va donner 1110, soit 14 en décimal, la valeur est donc -14. Cette opération s’appelle complément à un.

Récapitulatif des étapes

Voici les étapes, dans l’ordre, qui seront suivies pour effectuer la compression JPEG.

Ordre dans la compression Ordre dans la décompression Nom de l’étape Description
1 8 Codage YCbCr Remplacer les tuples de pixels rouge, vert, bleu, par des tuples intensité, différence de bleu, différence de rouge. Semblable au codage YUV utilisé historiquement à la télévision, permet d’appliquer un traitement différent sur les couleurs qui sont moins visibles par l’œil humain.
2 7 Chroma subsampling Optionnel : étaler les pixels de couleurs (donc artificiellement, « rétricir » les masques de couleurs à superposer à l’intensité, pour les « agrandir » quand l’image devra être vue) afin de gagner en taille et perdre en qualité, en partant du postulat que l’œil humain prête moins attention aux nuances de couleurs qu’aux contours). Très utilisé dans la vidéo en général.
3 6 Découpage en blocs L’image est découpée (par défaut) en blocs de 8 x 8 pixels, appelés « MCU », afin d’appliquer la DCT individuellement (voir ci-dessous).
4 5 DCT L’étape la plus importante de la compression. L’image est découpée en stries de différentes tailles, et on fait la moyenne des pixels se trouvant sur chacune de ces stries (on doit pouvoir retrouver l’image d’origine en re-superposant ces stries par la suite). À l’étape suivante, on fera perdre de la qualité aux stries les plus rapprochées (donc les moins visibles), c’est là qu’on gagnera en taille.
5 4 La quantification On va diviser chaque intensité de strie sortie par DCT par un nombre fixe (pour l’image, fourni dans l’image et variable selon la qualité de la compression fournie par son auteur) afin de perdre en qualité et gagner en taille. Les artéfacts apparaissent ici.
6 3 L’encodage en zig-zag Afin d’optimiser la probabilité de la présence de longue suites de « 0 », nous allons encoder les valeurs de notre tableau DCT, une fois la quantification effectuée, nous pas de gauche à droite puis de haut en bas mais en zig-zag, afin d’avoir les plus faibles (les plus petits niveaux de détails) qui apparaissent à la fin, les uns après les autres. Avec un JPEG en « mode progressif » (ils son répandus aussi), c’est différent.
7 2 Le codage entropique (Huffman + RLE de zéros + complément à un) Les nombres de zéros successifs à répéter dans l’image, ainsi que les tailles en bits servant à exprimer les valeurs qui ne sont pas à « zéro », sont codées avec Huffman (et un dictionnaire Huffman différent pour le bord en haut à gauche de l’image qui ne peut être précédé par des zéros). Les dictionnaires Huffman sont inclus dans le fichier. Les valeurs DCT quantifiés en elles-mêmes sont exprimées en faisant un complément à un avec extension de signe implicite (voir ci-dessous : si le premier bit de la séquence de bits de taille variable est un « 0 », notre nombre est négatif et les bits de sa valeur doivent être inversés pour être retrouvés).
8 1 Byte stuffing On ne l’a pas encore vu jusqu’ici, et ça ne fait pas partie de la compression à proprement parler, mais afin de permettre à un décodeur qui ne connaît pas notre format de compression de pouvoir sauter les données de l’image (dont la taille n’est pas indiquée au préalable) sans effectuer le décodage entropique, on va remplacer tous les FF par des FF 00 (ce qui permettra de trouver de manière sûre le marqueur de fin d’image qui fait partie du format et vient ensuite). Retenez-le bien pour ne pas vous retrouver avec des données complètement folles 🤪 !

Le format JPEG (JFIF)

Si on appelle couramment « JPEG » le format de fichiers idoine, à l’origine « JPEG » désigne le « Joint Photographic Experts Group » : c’est un groupe de travail formé par des membres de trois organisations :

  • L'ITU-T (c’est le gros organisme de normalisation « historique » qui a défini les normes derrière la téléphonie numérique, le Minitel, le réseau SS7 qui permet d’interconnecter les différents opérateurs téléphoniques entre eux, et encore d’autres choses avant ça) ;
  • L'ISO, un organisme de normalisation très général. Comme l’ITU-T, il s’agit d’une ramifications de l’ONU, basées à Genève ;
  • L'IEC, une autre association d’ingénieurs qui collabore souvent avec l’ISO.

Le groupe JPEG (cousin du groupe MPEG, qui a créé les standards de compression vidéo du même nom) a créé non seulement la compression JPEG, dont je vous ai parlé jusqu’ici, mais aussi le format JPEG (.jpg ou .jpeg), dont le vrai nom est en fait « JFIF » - « JPEG File Interchange Format ». C’est pourquoi vous voyez « JFIF » inscrit au début des fichiers JPEG.

JFIF est en fait une extension d’un autre format nommé « JIF », dont il gomme les incertitudes. Ce format est entièrement MSB (Most-Significant Bit first) et big-endian.

L’agencement en segments

Avec JPEG, par rapport à PNG, on ne parle pas pas de chunks mais des segments (dans l’idée c’est la même chose). Un fichier JPEG est composé d’un suite de segments, qui sont des blocs de données composés comme il suit :

Taille Nom du champ Description
1 octet Début de marqueur Juste l’octet 0xff
1 octet Type de marqueur Un champ entre 0x00 et 0xfe qui définit le type de données qui suivent.
2 octets Taille du segment (Optionnel, selon le type de marqueur) La taille du segment, incluant les deux octets du champ « taille » mais pas les deux octets du marqueur. Écrit en big-endian.
Variable Données du segment (Optionnel, selon le type de marqueur) Les données du segment

Un fichier se commence par un marqueur « SOI » (« Start of Image »), codé FF D8 (sans taille ni données) et se termine par un marqueur « EOI », FF D9 (même chose). Juste après le « SOI », il y a un marqueur « JFIF-APP0 » qui contient quelques informations basiques (à minima le fait que votre image dispose bien de base de pixels carrés (dans la télévision ils ont des pixels qui ne le sont pas, si si !), à maxima le nombre de pixels par centimère de votre image et plusieurs vignettes).

Pêle-mêle, les segments que vous retrouvez couramment sont SOF0 (« Start of Frame » - qui contient des informations importantes comme la taille de l’image, le nombre de pixels, le chroma subsampling ; ou sa variante SOF2 lorsqu’on est en mode progressif à l’étape du codage entropique), DHT et DQT (« Define Huffman Tables » and « Define Quantization Tables », leurs noms parlent d’eux-mêmes), « EXIF-APP1 » (qui peut contenir des informations comme le modèle de votre appareil photo, la date et l’heure, voire la position GPS de la prise de vue - vous êtes fichés !).

Mais le plus important de tous, c’est le segment « SOS » (Start of Scan), qui contient les données de l’image en elle-même. Seule la taille de base de son en-tête est indiquée, les données encodées avec Huffman sont à lire progressivement.

Notez que la norme JPEG est très (trop) complète, comme c’est souvent le cas avec les normes de l’ITU-T, et qu’elle définit d’autres techniques de codages que le codage Huffman « classique » : notamment, le fait d’utiliser du codage arithmétique (semblable au range coding que nous avons vus avec LZMA, seule la terminologie change) à la place du codage Huffman, même si les décodeurs ne le supportent pas. Par contre, le codage arithmétique est utilisé dans d’autres normes comme MPEG, que nous verrons plus tard.

Le segment SOF0 : la « vraie » en-tête d’un fichier JPEG baseline

Le segment SOF0 est composé de son marqueur, FF C0, suivi de sa taille sur 16 bits, suivi de :

Taille Description
8 bits Le nombre de bits par composante (par exemple l’intensité, la différence de bleu ou la différence de rouge), en général 8 bits
16 bits Hauteur de l’image en pixels (de 0 à 65 535)
16 bits Largeur de l’image en pixels (de 0 à 65 535)
8 bits Le nombre de composantes (3 pour YCbCr, 1 pour des niveaux de gris)

Puis, pour chaque composante :

Taille Description
8 bits L’index de la composante avec 1, 2 et 3 pour Y, Cb et Cr - indique l’ordre dans lequel elles seront présentes au sein des données du fichier
4 bits En cas de chroma subsampling, le nombre de pixels horizontaux dans un ratio horizontal:vertical (de 1 à 4)
4 bits En cas de chroma subsampling, le nombre de pixels verticaux dans un ratio horizontal:vertical (de 1 à 4)
8 bits Lorsque plusieurs tables de quantification sont présentes dans le fichier, indique l’index de la table de quantification à utiliser (de 0 à 3)

Le segment DHT : Define Huffman Tables

En boucle, et pour autant de tables de Huffman que nous allons retrouver dans le fichier, nous allons encoder les données suivantes :

Taille Description
4 bits « 0 » ou « 1 » : définit si cette table servira à décoder la taille en bits de la valeur DC, le bord gauche supérieur de la matrice DCT (valeur « 0 »). Sinon ce sera une des autres valeurs, les valeurs AC (valeur « 1 »).
4 bits Identifiant unique de la table Huffman qui suit, qu’on utilisera plus tard dans le fichier pour y faire référence. En principe, de « 0 » à « 3 ».
8 bits x 16 Pour chacune des 16 tailles en bits de codes Huffman possibles admises par JPEG (de 0 à 16), le nombre de codes à y inscrire. Pour deviner les valeurs des codes à partir de leurs tailles, cela se passera comme avec DEFLATE.
8 bits x nombre total de codes dans la table À partir de l’information précédente, on obtient la valeur correspondant à chaque code qui pourra être encodée dans notre table Huffman. Les codes de la taille la plus petite en premier et ceux de la taille la plus grande en dernier, en toute logique (ils sont codés par fréquence d’utilisation).

Le segment DQT : Define Quantization Tables

Il est encodé comme il suit, pour autant de tables de quantification qu’il y en a à déclarer :

Taille Description
4 bits Identifiant unique de la table de quantification qui suit, qu’on utilisera plus tard dans le fichier pour y faire référence. En principe, de « 0 » à « 3 ».
4 bits « 0 » ou « 1 » : définit si cette table contiendra des multiplicateurs DCT codés sur 16 bits (valeur « 1 ») ou 8 bits (valeur « 0 »).
8 ou 16 bits x (8 x 8) Les multiplicateurs à appliquer sur chaque valeur DCT quantifié pour refaire la quantification dans l’auteur sens (après décodage zig-zag).

Le segment SOS : Start Of Scan, le vrai début des données

Si comme les autres segments contenant des données, « SOS » est préfixé par une taille de 16 bits, sauf qu’elle n’en couvre que l’en-tête, elle ne couvre pas les données compressées de l’image en elle-mêmes : les données compressées sont inscrites directement dans le flux d’octets de l’image, ce qui évite à l’encodeur de devoir tout stocker en mémoire pour en connaître la taille avant de sortir les données (il s’agit d’un nombre calculable de valeurs Huffman).

Pour qu’on puisse « quand même » facilement sauter les données compressées, il y a un petit détail : l’octet « FF » (caractère de début de marqueur) » est remplacé par « FF 00 » partout où il est présent au sein des données compressées, sachant qu’il n’y a pas de marqueur « FF 00 ». Cela permet donc de faire la différence avec un « vrai » marqueur qui marquerait la fin des données compressées, sans forcément les interpréter. Le fait d’échapper des octets spéciaux par un autre octet se retrouve dans d’autres formats et protocoles (en général plutôt anciens), on l’appelle « byte stuffing ».

Commençons par l’en-tête du segment « SOS », qui précède les données Huffman et nous vient en aide pour les comprendre.

Il est composé comme il suit :

Taille Description
8 bits Nombre de composantes présentes au sein de l’image (1 pour les niveaux de gris, 3 pour YCbCr, 2 ou 4 dans certains cas spécifiques à Adobe/au format PDF (avoir 4 composantes vous permet de faire du CMYK, cyan-magenta-yellow-black - comme lorsque vous imprimez !). Ici, nous ne nous intéresserons qu’à YCbCr, qui est de loin le plus courant.
Voir ci-dessous Les informations propres à chaque composante présente dans l’image.
3 x 8 bits Des paramètres spécifiques au JPEG progressif, que nous n’évoquerons pas ici.

Les données propres à chaque composante sont composées comme il suit :

Taille Description
8 bits Identifiant numérique « standard » de la composante, permettant de faire le lien avec leur position dans la boucle de données à décompresser (en général, elles se retrouvent dans l’ordre). « 1 » pour Y, « 2 » pour Cb, « 3 » pour Cr.
4 bits L’identifiant de la table Huffman (voir plus haut) que nous utiliserons pour décoder les tailles de valeurs « AC » + le nombre de répétitions de zéros pour cette composante.
4 bits L’identifiant de la table Huffman (voir plus haut) que nous utiliserons pour décoder les tailles de valeurs « DC » pour cette composante.

Les données encodées en Huffman et en complément à un viennent juste après. L’alignement sur un octet n’est retrouvé « qu’à » la fin du flux de bits de données compressées. Les données sont écrites, quand on n’est pas en mode « progressif », ligne de MCU par ligne de MCU, puis colonne de MCU colonne par colonne de MCU, puis composante par composante, puis ligne de pixel par ligne de pixel, puis colonne de pixel par colonne de pixel (il peut y avoir des cas encore plus compliqués dans le cas où on ne travaille pas sur du 8x8 :D ).

La valeur « DC » de chaque bloc (la première du bloc, même après le zig-zag) est à additioner à la dernière valeur « DC » utilisée pour la même composante.

Un décodeur JPEG en Python

Nous allons écrire un lecteur de JPEG très basique, qui ne gérera ni les JPEG progressifs, ni le subsampling. Si vous souhaitez lire ou écrire des fichiers JPEG plus compliqués, vous pourrez l’améliorer ou bien le réécrire dans le langage de votre choix !

Comme dans le chapitre précédent, le résultat de l’image à décoder s’affichera dans le terminal. Voici la riante image que nous allons décoder : (lien direct)

Pour écrire notre décodeur JPEG en Python, nous allons commence par prendre la classe DecodeurHuffman que nous avons écrite lors du chapitre sur DEFLATE, ainsi que la classe LecteurDeBitsMSB que nous avons écrite lors du chapitre sur BZip2. Vous l’aurez compris, elles seront utilisables telles quelles.

Voici d’abord une table des différents types de marqueurs JPEG. Elle nous permettra au préalable de connaître les différents types de marqueurs présents au sein de l’image :

segment_names = {
    0xD8: 'Start Of Image',
    0xC0: 'Start Of Frame (baseline DCT)',
    0xC2: 'Start Of Frame (progressive DCT)',
    0xC4: 'Define Huffman Table(s)',
    0xDB: 'Define Quantization Table(s)',
    0xDD: 'Define Restart Interval',
    0xDA: 'Start Of Scan',
    0xD0: 'Restart 0',
    0xD1: 'Restart 1',
    0xD2: 'Restart 2',
    0xD3: 'Restart 3',
    0xD4: 'Restart 4',
    0xD5: 'Restart 5',
    0xD6: 'Restart 6',
    0xD7: 'Restart 7',
    0xE0: 'JFIF-APP0',
    0xE1: 'EXIF-APP1',
    0xFE: 'Comment',
    0xD9: 'End Of Image'
}

Ensuite, on va faire la fonction qui va implémenter le complément à un dont on a parlé plus haut (la fonction qui nous permettra de lire des valeurs DCT depuis le flux d’un nombre fixe de bits, où le fait que le premier bit soit « 0 » ou « 1 » définit s’il faut inverser le signe et les bits de la valeur concernée).

def complement_a_un_jpeg(taille_bits_ac, valeur_ac):
    
    plus_haut_bit = valeur_ac >> (taille_bits_ac - 1)
    
    if plus_haut_bit == 0:
        
        masque_de_taille_bits_ac = (1 << taille_bits_ac) - 1
        
        valeur_ac ^= masque_de_taille_bits_ac
        
        valeur_ac *= -1 # Changer le signe de la valeur
        
    return valeur_ac

Ensuite, on va faire la fonction qui va décoder un tableau de valeurs DCT (grosso modo l’inverse de la fonction pour encoder DCT qu’on a vue plus haut).

def decoder_dct(ancien_tableau : List[List[int]]) -> List[List[int]]:
    
    # Le nouveau tableau aura le même nombre d'entrées (et de sous-
    # entrées) que le tableau d'origine, mais pas les mêmes valeurs,
    # c'est pourquoi on commence à copier le tableau d'origine (et
    # ses tableaux imbriqués) pour en créer un distinct.
    
    nouveau_tableau : List[List[int]] = deepcopy(ancien_tableau)
    
    # Ici, on va faire le transformation inverse de DCT (qui sert à
    # décoder vers des pixels au lieu d'encoder vers des valeurs DCT),
    # assez sembable à celle d'origine.
    
    for nouveau_y in range(8):
        for nouveau_x in range(8):
            
            nouvelle_valeur = 0
            
            for ancien_y in range(8):
                for ancien_x in range(8):
                    
                    # Le cosinus retourne un facteur qui pondère
                    # la valeur selon si on est dans la strie ou non.

                    valeur_a_ajouter = (
                        ancien_tableau[ancien_y][ancien_x] *
                        cos(((2 * nouveau_y + 1) * ancien_y * pi) / 16) *
                        cos(((2 * nouveau_x + 1) * ancien_x * pi) / 16)
                    )
            
                    # Si on est au bord du tableau (aplat complet),
                    # cosinus retournera toujours 1 et le nombre
                    # pourrait être très gros : on va donc le
                    # réduire un peu
                    
                    if ancien_y == 0:
                        valeur_a_ajouter /= sqrt(2)
                    if ancien_x == 0:
                        valeur_a_ajouter /= sqrt(2)
                    
                    nouvelle_valeur += valeur_a_ajouter
                
            nouvelle_valeur /= 4
            
            nouvelle_valeur = 128 + int(ceil(nouvelle_valeur))
            nouvelle_valeur = min(max(nouvelle_valeur, 0), 255)
            
            nouveau_tableau[nouveau_y][nouveau_x] = nouvelle_valeur
    
    return nouveau_tableau

Ensuite, on va générer une table de conversion qui nous pemettra de prendre nos valeurs DCT quantifiés ayant été mises en ziz-zag, pour, à partir de la position de chaque valeur, remettre celle-ci dans l’ordre d’origine.

# Générer une liste permettant, successivement, de prendre
# une position "y * 8 + x" dans un tableau et d'effectuer
# la transformation zig-zag, dans le sens du décodage

table_decoder_zig_zag : List[int] = []

position_y = 0
position_x = 0

for taille_diagonale in [*range(1, 8), *range(8, 0, -1)]:
    
    # Pour faire des zig-zags, on fait des diagonales en
    # alternance vers le haut et vers le bas (une fois
    # sur deux, donc on peut regarder si la taille de notre
    # diagonale est paire ou impaire)
    
    diagonale_vers_le_bas : bool = bool(taille_diagonale % 2 == 0)
    
    for position_dans_la_diagonale in range(taille_diagonale):
        
        # Le premier point à écrire sera 0,0 (on y est déjà à l'origine)
        
        table_decoder_zig_zag.append(position_y * 8 + position_x)
        
        # Si on n'a pas fini de parcourir notre diagonale, on calcule
        # la position suivante
        
        if position_dans_la_diagonale + 1 < taille_diagonale:
            
            if diagonale_vers_le_bas:
                
                position_y += 1
                position_x -= 1
            
            else:
                
                position_y -= 1
                position_x += 1
        
    # Quand on a fini de parcourir la diagonale, on va se décaler d'un cran
      # pour laisser place à la diagonale suivante
    
    if diagonale_vers_le_bas:
        if position_y < 7:
            position_y += 1
        else:
            position_x += 1
    else:
        if position_x < 7:
            position_x += 1
        else:
            position_y += 1


On va attaquer le vif du sujet : la fonction qui contient la boucle qui va lie chaque segment de notre image JPEG, puis afficher l’image dans le terminal, en utilisant les séquences de codes couleurs vues dans le précédent chapitre.

def mon_decodeur_jpeg(entree : bytes):
    
    lecteur_de_bits = LecteurDeBitsMSB(entree)

    position_vers_table_huffman : Dict[Tuple[int, bool], DecodeurHuffman] = {}
    
    position_vers_table_de_quantification : Dict[int, List[int]] = {}
    
    while True:
        
        # Lecture de l'en-tête de segment
        
        marker_start = lecteur_de_bits.lire_bits(8)
        if not marker_start: # Fin du fichier atteinte
            break
        assert marker_start == 0xff
        
        marker_type = lecteur_de_bits.lire_bits(8)
        
        # Lecture de la valeur du segment
        
        print()
        
        if not (0xd0 <= marker_type <= 0xd9): # Est-ce qu'il s'agit d'un marqueur de taille variable ?
            
            segment_size = lecteur_de_bits.lire_bits(16)
        
            segment_data = lecteur_de_bits.lire_octets(segment_size - 2)
            
            print('[DEBUG] Read segment %s (FF %02X) of size %d: %s' % (segment_names[marker_type], marker_type, segment_size, segment_data[:100]))
            
            lecteur_de_bits_segment = LecteurDeBitsMSB(segment_data)
        
            if marker_type == 0xc0: # SOF0 - Start of Frame (baseline sequential DCT with Huffman)
                
                bits_par_composante = lecteur_de_bits_segment.lire_bits(8)
                
                assert bits_par_composante == 8
                
                hauteur_image_pixels = lecteur_de_bits_segment.lire_bits(16)
                largeur_image_pixels = lecteur_de_bits_segment.lire_bits(16)
                
                nombre_de_composantes = lecteur_de_bits_segment.lire_bits(8)
                
                index_composante_boucle_vers_ratio_subsampling_horizontal : Dict[int, int] = {}
                index_composante_boucle_vers_ratio_subsampling_vertical : Dict[int, int] = {}

                index_composante_boucle_vers_index_table_quantification : Dict[int, int] = {}
                
                for index_composante_boucle in range(nombre_de_composantes):
                    
                    index_composante_standard = lecteur_de_bits_segment.lire_bits(8)
                    
                    ratio_subsampling_horizontal = lecteur_de_bits_segment.lire_bits(4)
                    ratio_subsampling_vertical = lecteur_de_bits_segment.lire_bits(4)
                    
                    index_table_quantification_composante = lecteur_de_bits_segment.lire_bits(8)
                    
                    index_composante_boucle_vers_ratio_subsampling_horizontal[index_composante_boucle] = ratio_subsampling_horizontal
                    index_composante_boucle_vers_ratio_subsampling_vertical[index_composante_boucle] = ratio_subsampling_vertical
                    
                    index_composante_boucle_vers_index_table_quantification[index_composante_boucle] = index_table_quantification_composante
                    
            elif marker_type == 0xc2: # SOF2 - Start of Frame (progressive sequential DCT with Huffman)
                
                raise NotImplementedError("DCT progressif n'est pas supporté")
                
            
            elif marker_type == 0xc4: # DHT - Define Huffman Table(s)
                
                while lecteur_de_bits_segment.octets.tell() < len(segment_data):
                    
                    # Est-ce une table de Huffman qui doit encoder le maqueur
                    # de taille pour le bord inférieur gauche de chaque bloc ?
                    
                    est_table_ac_sinon_dc = bool(lecteur_de_bits_segment.lire_bits(4))
                    
                    # Quelle est la position de cette table dans la liste des
                    # tables Huffman ? (valeurs possibles : de 0 à 3)
                    
                    position_de_table_huffman = lecteur_de_bits_segment.lire_bits(4)
                    
                    assert 0 <= position_de_table_huffman <= 3
                    
                    # Quelle est le nombre de symboles pour chaque taille de
                    # code Huffman possible ?
                    
                    taille_de_code_vers_nombre_symboles : Dict[int, int] = {
                        position_taille_de_code + 1: lecteur_de_bits_segment.lire_bits(8)
                        for position_taille_de_code in range(16)
                    }
                    
                    # Quels sont les symboles définis dans notre table, pour
                    # chaque taille en bits utilisée ?
                    
                    valeur_vers_taille_de_code = {
                        lecteur_de_bits_segment.lire_bits(8): taille_de_code
                        for taille_de_code, nombre_symboles in sorted(taille_de_code_vers_nombre_symboles.items())
                        for index_symbole in range(nombre_symboles)
                    }
                    
                    # On a toutes les information, on stocke notre décodeur
                    # Huffman pour lorsqu'on devra décoder les données de l'image.
                    
                    position_vers_table_huffman[(position_de_table_huffman, est_table_ac_sinon_dc)] = DecodeurHuffman(
                        valeur_vers_taille_de_code = valeur_vers_taille_de_code,
                        lecteur_de_bits = lecteur_de_bits
                    )
                    
            
            elif marker_type == 0xdb: # DQT - Define Quantization Table
                
                while lecteur_de_bits_segment.octets.tell() < len(segment_data):
                                        
                    # Les valeurs de notre table de quantification sont-elles
                    # codées sur 8 ou 16 bits ?
                    
                    is_16_bits_precision_quantization_table = bool(lecteur_de_bits_segment.lire_bits(4))
                    
                    # On lit le numéro qui identifiera notre table de quantification
                    
                    position_de_table_de_quantification = lecteur_de_bits_segment.lire_bits(4)
                    
                    assert 0 <= position_de_table_de_quantification <= 3
                    
                    # On lit notre table de quantification
                    
                    position_vers_table_de_quantification[position_de_table_de_quantification] = [
                        lecteur_de_bits_segment.lire_bits(16 if is_16_bits_precision_quantization_table else 8)
                        for position_valeur in range(8 * 8)
                    ]
                    

            # SOS - Start of Scan : seule la taille de l'en-tête est donnée
            # explicitement, les données de l'image qui suivent sont de taille
            # variable
            
            elif marker_type == 0xda:

                class ComposanteYCbCr(IntEnum):
                    intensite = 1
                    difference_bleu = 2
                    difference_rouge = 3


                index_composante_boucle_vers_index_composante_standard : Dict[int, ComposanteYCbCr] = {}

                index_composante_boucle_vers_decodeur_huffman_ac : Dict[int, DecodeurHuffman] = {}
                index_composante_boucle_vers_decodeur_huffman_dc : Dict[int, DecodeurHuffman] = {}

                # On commence par lire l'en-tête des données, dont la taille a
                # été indiquée explicitement
                
                nombre_de_composantes = lecteur_de_bits_segment.lire_bits(8)
                
                for index_composante_boucle in range(nombre_de_composantes):
                    
                    index_composante_standard = lecteur_de_bits_segment.lire_bits(8)
                    
                    index_composante_boucle_vers_index_composante_standard[index_composante_boucle] = ComposanteYCbCr(index_composante_standard)
                    
                    position_composante_dans_table_huffman_ac = lecteur_de_bits_segment.lire_bits(4)
                    position_composante_dans_table_huffman_dc = lecteur_de_bits_segment.lire_bits(4)
                    
                    # Stocker les informations lues pour plus tard
                    
                    index_composante_boucle_vers_index_composante_standard[index_composante_boucle] = ComposanteYCbCr(
                        index_composante_standard)
                    
                    index_composante_boucle_vers_decodeur_huffman_ac[index_composante_boucle] = position_vers_table_huffman[
                        (position_composante_dans_table_huffman_ac, True)]
                    
                    index_composante_boucle_vers_decodeur_huffman_dc[index_composante_boucle] = position_vers_table_huffman[
                        (position_composante_dans_table_huffman_dc, False)]
                    
                donnees_specifiques_au_jpeg_progressif = lecteur_de_bits_segment.lire_octets(3)
                
                # On calcule au passage le nombre de MCU (blocs de 8x8 pixels,
                # macroblocs) qu'il nous faudra lire
                
                hauteur_image_mcu = int(ceil(hauteur_image_pixels / 8))
                largeur_image_mcu = int(ceil(largeur_image_pixels / 8))
                
                # Après avoir lu l'en-tête, on continue à lire des bits
                # directement sur le flux de données.
                #
                # On va extraire le flux de données avec une regex, vu
                # qu'on sait qu'il ne contient jamais l'octet "FF", sauf
                # suivi d'un "00" ce qui signifie qu'il s'agit d'un "FF"
                # échappé. Au passage on défait cet échappement.
                
                octets_restant_a_lire = lecteur_de_bits.octets.read()
                
                octets_de_donnees, autres_segments = match(b'^(.+?)(\xff[\x01-\xff].*)$', octets_restant_a_lire, flags = DOTALL).groups()
                
                lecteur_de_bits = LecteurDeBitsMSB(octets_de_donnees.replace(b'\xff\x00', b'\xff') + autres_segments)
                
                for decodeur_huffman in index_composante_boucle_vers_decodeur_huffman_dc.values():
                    decodeur_huffman.lecteur_de_bits = lecteur_de_bits
                for decodeur_huffman in index_composante_boucle_vers_decodeur_huffman_ac.values():
                    decodeur_huffman.lecteur_de_bits = lecteur_de_bits
                
                # On a défait l'échappement des "FF".
                
                index_composante_standard_vers_y_vers_x_vers_donnees : Dict[ComposanteYCbCr, Dict[int, Dict[int, int]]] = defaultdict(lambda: defaultdict(dict))
                
                index_composante_standard_vers_precedente_valeur_dc : Dict[ComposanteYCbCr, int] = defaultdict(int)
                

                for base_position_pixels_y in range(0, hauteur_image_mcu * 8, 8):
                    for base_position_pixels_x in range(0, largeur_image_mcu * 8, 8):
                
                        for (
                            (index_composante_boucle, index_composante_standard),
                            (index_composante_boucle, decodeur_huffman_ac),
                            (index_composante_boucle, decodeur_huffman_dc)
                        ) in zip(
                            sorted(index_composante_boucle_vers_index_composante_standard.items()),
                            sorted(index_composante_boucle_vers_decodeur_huffman_ac.items()),
                            sorted(index_composante_boucle_vers_decodeur_huffman_dc.items())
                        ):
                            
                            
                            # Lire les données de la composante encodées partiellement avec Huffman
                            
                            taille_bits_dc = decodeur_huffman_dc.lire_prochaine_valeur()
                            
                            if taille_bits_dc:
                                valeur_dc = complement_a_un_jpeg(taille_bits_dc, lecteur_de_bits.lire_bits(taille_bits_dc))
                            else:
                                valeur_dc = 0
                            
                            valeur_dc += index_composante_standard_vers_precedente_valeur_dc[index_composante_standard]
                            index_composante_standard_vers_precedente_valeur_dc[index_composante_standard] = valeur_dc
                            
                            # Après la valeur DC (coin en haut à gauche), lire les valeurs AC
                            
                            valeurs_dct : List[int] = [valeur_dc]
                            
                            while len(valeurs_dct) < 8 * 8:
                                
                                nombre_repetitions_et_taille_bits_ac = decodeur_huffman_ac.lire_prochaine_valeur()
                            
                                nombre_repetitions = nombre_repetitions_et_taille_bits_ac >> 4 # Nombre de répétitions de valeurs à zéro
                                taille_bits_ac = nombre_repetitions_et_taille_bits_ac & 0b00001111

                                # if nombre_repetitions_et_taille_bits_ac == 0:
                                if nombre_repetitions < 15 and taille_bits_ac == 0:
                                    valeurs_dct += [0] * (8 * 8 - len(valeurs_dct))
                                    break
                            

                                valeurs_dct += [0] * nombre_repetitions
                                                            
                                if taille_bits_ac:
                                    valeur_ac = complement_a_un_jpeg(taille_bits_ac, lecteur_de_bits.lire_bits(taille_bits_ac))
                                    
                                    valeurs_dct.append(valeur_ac)
                                
                                assert len(valeurs_dct) <= 8 * 8
                            
                            # Effectuer la déquantification (augmenter la
                            # valeur des entrées de la matrice DCT)
                            
                            index_table_quantification_composante = index_composante_boucle_vers_index_table_quantification[index_composante_boucle]
                            
                            table_de_quantification = position_vers_table_de_quantification[index_table_quantification_composante]
                            
                            for position in range(len(valeurs_dct)):
                                
                                valeurs_dct[position] *= table_de_quantification[position]  & 0xff
                            
                            # Effectuer le réagencement zig-zag
                            
                            valeurs_zig_zag : List[int] = [0] * (8 * 8)
                            for position in range(len(valeurs_dct)):
                                valeurs_zig_zag[table_decoder_zig_zag[position]] = valeurs_dct[position]
                            
                            y_vers_x_vers_valeur_zig_zag : List[List[int]] = [[0] * 8 for i in range(8)]
                            
                            for position_lineaire, valeur_zig_zag in enumerate(valeurs_zig_zag):
                                
                                position_y = position_lineaire // 8
                                position_x = position_lineaire % 8
                                
                                y_vers_x_vers_valeur_zig_zag[position_y][position_x] = valeur_zig_zag
                                    
                            
                            # Effectuer la DCT inverse (décodage DCT)
                            
                            valeurs_dct : List[List[int]] = decoder_dct(y_vers_x_vers_valeur_zig_zag)
                            
                            # Recoller les pixels avec ceux des autres MCU
                            
                            for y in range(8):
                                for x in range(8):
                            
                                    index_composante_standard_vers_y_vers_x_vers_donnees[index_composante_standard][base_position_pixels_y + y][base_position_pixels_x + x] = valeurs_dct[y][x]
                    
                # On a décodé notre image, on peut passer à l'affichage
                
                sequences_de_terminal_rgb : str = ''
                
                for position_y in range(hauteur_image_pixels):
                    
                    for position_x in range(largeur_image_pixels):
                
                        # Effectuer la conversion YCbCr vers RGB
                
                        intensite : int = index_composante_standard_vers_y_vers_x_vers_donnees[ComposanteYCbCr.intensite][position_y][position_x]
                        
                        difference_bleu : int = index_composante_standard_vers_y_vers_x_vers_donnees[ComposanteYCbCr.difference_bleu][position_y][position_x]

                        difference_rouge : int = index_composante_standard_vers_y_vers_x_vers_donnees[ComposanteYCbCr.difference_rouge][position_y][position_x]
                                                
                        # Les valeurs RGB :
                        
                        rouge = intensite + 1.402 * (difference_rouge - 128)

                        vert = intensite - 0.344136 * (difference_bleu - 128) - 0.714136 * (difference_rouge - 128)
                        
                        bleu = intensite + 1.772 * (difference_bleu - 128)
                        
                        rouge = max(0, min(255, rouge))
                        vert = max(0, min(255, vert))
                        bleu = max(0, min(255, bleu))
                        
                        # Transformer nos pixels en espaces colorés à afficher
                        # dans notre terminal, comme nous l'avons vus dans le
                        # chapitre sur PNG
                        
                        sequences_de_terminal_rgb += '\x1B[48;2;%d;%d;%dm' % (rouge, vert, bleu) + '  ' + '\x1B[0m'
                
                    sequences_de_terminal_rgb += '\n'
                
                # Afficher l'image décodée dans le terminal
                
                print(sequences_de_terminal_rgb.strip())
                
                lecteur_de_bits.aligner_bits_sur_octet()

            
        # Est-ce qu'il s'agit d'un marqueur non suivi de données ?
        
        elif 0xd0 <= marker_type <= 0xd9:
            print('[DEBUG] Read segment %s (FF %02X) with no data' % (segment_names[marker_type], marker_type))
            
            if marker_type == 0xd9: # EOI - End of Image
                
                break
        
                    
    

Enfin, on va encore ajouter un peu de code pour prendre propremement le chemin de l’image à décoder en entrée, en utilisant le module argparse :

# Prendre un chemin de la part de l'utilisateur en entrée.

args = ArgumentParser(description = "Affiche une image JPEG de petite taille " +
    "dans le terminal, en utilisant les caractères d'échappement ANSI.")

args.add_argument('fichier_entree', help = "Chemin de l'image à décoder sur le disque.")

args = args.parse_args()

with open(args.fichier_entree, 'rb') as objet_fichier:
    mon_decodeur_jpeg(objet_fichier.read())

Voici le code en entier, avec les imports, que nous exécutons :

#!/usr/bin/python3
#-*- encoding: Utf-8 -*-

from ctypes import BigEndianStructure, c_char, c_uint8, c_uint32
from typing import List, Tuple, Set, Dict, Union, Optional
from math import ceil, cos, pi, sqrt
from argparse import ArgumentParser
from collections import defaultdict
from re import match, DOTALL
from zlib import decompress
from decimal import Decimal
from binascii import crc32
from copy import deepcopy
from enum import IntEnum
from io import BytesIO


"""
    Variante de notre classe LecteurDeBits qui lit les valeurs encodées dans
    chaque octet d'un flux de bits de gauche à droite (MSB), plutôt que de
    droite à gauche (LSB).
"""

class LecteurDeBitsMSB:
    
    def __init__(self, entree : bytes):
        
        self.octets = BytesIO(entree)
        
        # On initialise nos attributs.
        
        self.bits_non_lus = 0
        self.taille_bits_non_lus = 0
        
    """
        Lire un entier composé d'un certain nombre de bits,
        en commençant par les bits les plus à gauche
    """
    
    def lire_bits(self, nombre_bits : int) -> int:

        while self.taille_bits_non_lus < nombre_bits:
            
            prochain_octet = self.octets.read(1)
            
            """
                Si notre lecture du BytesIO a retourné une chaîne vide, alors
                ça veut dire qu'il n'y a plus d'octets de disponibles, on va
                déclencher une erreur
            """
            
            if not prochain_octet:
                raise EOFError
            
            """
                On n'oublie pas de convertir notre chaîne d'un octet en
                entier en ne prenant explicitement que le premier octet: "[0]"
                
                On l'ajoute à nos bits non lus
            """
            
            self.bits_non_lus <<= 8
            self.bits_non_lus |= prochain_octet[0]
            self.taille_bits_non_lus += 8
        
        # Les bits à lire sont les bits encore non lus les
        # plus à gauche (MSB = Most significant bits)
        
        bits_lus = self.bits_non_lus >> (self.taille_bits_non_lus - nombre_bits)
        
        self.taille_bits_non_lus -= nombre_bits
        
        masque_bits_non_lus = (1 << self.taille_bits_non_lus) - 1
        self.bits_non_lus &= masque_bits_non_lus
        
        return bits_lus
    
    """
        Lire simplement des octets
    """
    
    def lire_octets(self, nombre_octets : int) -> bytes:
        
        self.aligner_bits_sur_octet()
        
        octets_lus = self.octets.read(nombre_octets)
        
        if len(octets_lus) < nombre_octets:
            raise EOFError # Pas assez d'octets, déclencher une erreur, EOF = End of file = Fin de fichier
        
        return octets_lus
    
    """
        Se rendre au début de l'octet suivant
    """
    
    def aligner_bits_sur_octet(self):
        
        self.bits_non_lus = 0
        self.taille_bits_non_lus = 0
    



# Notre décodeur de Huffman, repris de DEFLATE.

class DecodeurHuffman:
    
    """
        La méthode "__init__" (en jargon technique, on l'appelle « notre
        constructeur » prendra comme premier argument une association (un
        dictionnaire Python) entre des valeurs (des entiers), et leurs tailles 
        de codes respectives d'autres entiers). À partir de ça, les codes
        Huffman seront calculés automatiquement.
    """
    
    def __init__(self, valeur_vers_taille_de_code : Dict[int, int], lecteur_de_bits : LecteurDeBitsMSB):
        
        # Chaîne de code = chaîne représentant un code écrit en chiffres binaires
        
        self.chaine_de_code_vers_valeur : Dict[str, int] = {}
        
        self.lecteur_de_bits : LecteurDeBitsMSB = lecteur_de_bits
        
        derniere_taille_de_code : int = None
        dernier_code : int = None
        
        """
            Comme expliqué plus haut dans le tutoriel, on va numéroter nos
            codes selon leurs tailles de codes respectives, puis selon leurs
            valeurs.
            
            Du coup, il va falloir passer sur les valeurs dans l'ordre, en
            prenent d'abord en compte la taille de code.
            
            Cette petite fonction, que l'on va passer à la fonction de tri de
            Python ("sorted"), devrait nous être très utile pour trier d'abord
            par tailles de codes, et ensuite par valeurs.
        """
        
        def tri_valeurs(valeur_et_taille_de_code):
            valeur, taille_de_code = valeur_et_taille_de_code # On prend la tuple...
            return (taille_de_code, valeur) # ... et on la change de sens pour dire qu'on trie par tailles de code d'abord
        
        for valeur, taille_de_code in sorted(valeur_vers_taille_de_code.items(), key = tri_valeurs):
            
            if not taille_de_code:
                continue # Taille de code de 0 ! On passe !
            
            if dernier_code is None:
                code = 0
                
            else:
                if taille_de_code < derniere_taille_de_code:
                    # Vu qu'on passe sur les tailles de code dans l'ordre, de
                    # la plus petite à la plus grande, ça n'arrive très
                    # logiquement jamais.
                    
                    raise Exception
                
                elif taille_de_code > derniere_taille_de_code:
                    code = (dernier_code + 1) << (taille_de_code - derniere_taille_de_code)
                
                else:
                    code = dernier_code + 1
            
            # On transforme notre code en chaîne contenant des chiffres
            # binaires : on utilise "format" pour créer la chaîne (qui
            # sera la plus courte possible), puis "zfill" qui va nous
            # ajouter le bon nombre de zéros à gauche
            
            chaine_de_code = format(code, 'b').zfill(taille_de_code)
            
            self.chaine_de_code_vers_valeur[chaine_de_code] = valeur
            
            dernier_code = code
            derniere_taille_de_code = taille_de_code
        
        """
            Maintenant qu'on a constitué tous nos codes dans 
            "chaine_de_code_vers_valeur", il va falloir créer la structure
            représentant notre arbre.
            
            Notre arbre sera fait de dictionnaires Python imbriqués entre eux,
            avec pour chacun au plus deux clefs, "0" et "1", et dont la valeur
            correspond soit à notre valeur Huffman (un entier), soit à un
            autre dictionnaire s'il faut continuer dans l'arbre.
        """
        
        # L'anotation de type "Union" signifie « une de ces valeurs » (comme
        # toutes les annotations de type, elle a uniquement une valeur
        # d'information)
        
        self.mon_arbre : Dict[int, Union[dict, int]] = {}
        
        for chaine_de_code, valeur in self.chaine_de_code_vers_valeur.items():
            
            # Pour chaque code, on va descendre dans l'arbre, en créant au
            # passage les dictionaires imbiqués successifs pour chaque bit
            # (sauf le dernier), et assigner le dernier bit à la valeur
            # associée au code
            
            noeud_actuel_dans_l_arbre = self.mon_arbre
            
            # Descendre dans l'arbre...
            
            for chiffre_bit in chaine_de_code[:-1]:
                bit = int(chiffre_bit) # Conversion de chaîne en entier
                
                if bit not in noeud_actuel_dans_l_arbre:
                    
                    noeud_actuel_dans_l_arbre[bit] = {} # Création de dictionnaire
                
                noeud_actuel_dans_l_arbre = noeud_actuel_dans_l_arbre[bit]
            
            # ...on est au bout
            
            dernier_bit = int(chaine_de_code[-1])
            
            noeud_actuel_dans_l_arbre[dernier_bit] = valeur
        
        # On a créé notre arbre, c'est tout bon !
    
    """
        Maintenant : voici la fonction qu'on va appeler tout le temps pour
        faire le décodage : elle va lire le code à partir du lecteur de bits.
        
        Elle commence en haut de l'arbre, puis elle avance d'un bit à la fois,
        jusqu'à avoir trouvé la valeur.
    """
    
    def lire_prochaine_valeur(self) -> int:
        
        noeud_actuel_dans_l_arbre = self.mon_arbre
        
        while True:
            prochain_bit : int = self.lecteur_de_bits.lire_bits(1)
            
            noeud_actuel_dans_l_arbre = noeud_actuel_dans_l_arbre[prochain_bit]
            
            # On est tombé sur un entier ? Chouette, on a trouvé notre valeur !
            
            if type(noeud_actuel_dans_l_arbre) == int:
                
                return noeud_actuel_dans_l_arbre


segment_names = {
    0xD8: 'Start Of Image',
    0xC0: 'Start Of Frame (baseline DCT)',
    0xC2: 'Start Of Frame (progressive DCT)',
    0xC4: 'Define Huffman Table(s)',
    0xDB: 'Define Quantization Table(s)',
    0xDD: 'Define Restart Interval',
    0xDA: 'Start Of Scan',
    0xD0: 'Restart 0',
    0xD1: 'Restart 1',
    0xD2: 'Restart 2',
    0xD3: 'Restart 3',
    0xD4: 'Restart 4',
    0xD5: 'Restart 5',
    0xD6: 'Restart 6',
    0xD7: 'Restart 7',
    0xE0: 'JFIF-APP0',
    0xE1: 'EXIF-APP1',
    0xFE: 'Comment',
    0xD9: 'End Of Image'
}


def complement_a_un_jpeg(taille_bits_ac, valeur_ac):
    
    plus_haut_bit = valeur_ac >> (taille_bits_ac - 1)
    
    if plus_haut_bit == 0:
        
        masque_de_taille_bits_ac = (1 << taille_bits_ac) - 1
        
        valeur_ac ^= masque_de_taille_bits_ac
        
        valeur_ac *= -1 # Changer le signe de la valeur
        
    return valeur_ac

def decoder_dct(ancien_tableau : List[List[int]]) -> List[List[int]]:
    
    # Le nouveau tableau aura le même nombre d'entrées (et de sous-
    # entrées) que le tableau d'origine, mais pas les mêmes valeurs,
    # c'est pourquoi on commence à copier le tableau d'origine (et
    # ses tableaux imbriqués) pour en créer un distinct.
    
    nouveau_tableau : List[List[int]] = deepcopy(ancien_tableau)
    
    # Ici, on va faire le transformation inverse de DCT (qui sert à
    # décoder vers des pixels au lieu d'encoder vers des valeurs DCT),
    # assez sembable à celle d'origine.
    
    for nouveau_y in range(8):
        for nouveau_x in range(8):
            
            nouvelle_valeur = 0
            
            for ancien_y in range(8):
                for ancien_x in range(8):
                    
                    # Le cosinus retourne un facteur qui pondère
                    # la valeur selon si on est dans la strie ou non.

                    valeur_a_ajouter = (
                        ancien_tableau[ancien_y][ancien_x] *
                        cos(((2 * nouveau_y + 1) * ancien_y * pi) / 16) *
                        cos(((2 * nouveau_x + 1) * ancien_x * pi) / 16)
                    )
            
                    # Si on est au bord du tableau (aplat complet),
                    # cosinus retournera toujours 1 et le nombre
                    # pourrait être très gros : on va donc le
                    # réduire un peu
                    
                    if ancien_y == 0:
                        valeur_a_ajouter /= sqrt(2)
                    if ancien_x == 0:
                        valeur_a_ajouter /= sqrt(2)
                    
                    nouvelle_valeur += valeur_a_ajouter
                
            nouvelle_valeur /= 4
            
            nouvelle_valeur = 128 + int(ceil(nouvelle_valeur))
            nouvelle_valeur = min(max(nouvelle_valeur, 0), 255)
            
            nouveau_tableau[nouveau_y][nouveau_x] = nouvelle_valeur
    
    return nouveau_tableau

# Générer une liste permettant, successivement, de prendre
# une position "y * 8 + x" dans un tableau et d'effectuer
# la transformation zig-zag, dans le sens du décodage

table_decoder_zig_zag : List[int] = []

position_y = 0
position_x = 0

for taille_diagonale in [*range(1, 8), *range(8, 0, -1)]:
    
    # Pour faire des zig-zags, on fait des diagonales en
    # alternance vers le haut et vers le bas (une fois
    # sur deux, donc on peut regarder si la taille de notre
    # diagonale est paire ou impaire)
    
    diagonale_vers_le_bas : bool = bool(taille_diagonale % 2 == 0)
    
    for position_dans_la_diagonale in range(taille_diagonale):
        
        # Le premier point à écrire sera 0,0 (on y est déjà à l'origine)
        
        table_decoder_zig_zag.append(position_y * 8 + position_x)
        
        # Si on n'a pas fini de parcourir notre diagonale, on calcule
        # la position suivante
        
        if position_dans_la_diagonale + 1 < taille_diagonale:
            
            if diagonale_vers_le_bas:
                
                position_y += 1
                position_x -= 1
            
            else:
                
                position_y -= 1
                position_x += 1
        
    # Quand on a fini de parcourir la diagonale, on va se décaler d'un cran
    # pour laisser place à la diagonale suivante
    
    if diagonale_vers_le_bas:
        if position_y < 7:
            position_y += 1
        else:
            position_x += 1
    else:
        if position_x < 7:
            position_x += 1
        else:
            position_y += 1


def mon_decodeur_jpeg(entree : bytes):
    
    lecteur_de_bits = LecteurDeBitsMSB(entree)

    position_vers_table_huffman : Dict[Tuple[int, bool], DecodeurHuffman] = {}
    
    position_vers_table_de_quantification : Dict[int, List[int]] = {}
    
    while True:
        
        # Lecture de l'en-tête de segment
        
        marker_start = lecteur_de_bits.lire_bits(8)
        if not marker_start: # Fin du fichier atteinte
            break
        assert marker_start == 0xff
        
        marker_type = lecteur_de_bits.lire_bits(8)
        
        # Lecture de la valeur du segment
        
        print()
        
        if not (0xd0 <= marker_type <= 0xd9): # Est-ce qu'il s'agit d'un marqueur de taille variable ?
            
            segment_size = lecteur_de_bits.lire_bits(16)
        
            segment_data = lecteur_de_bits.lire_octets(segment_size - 2)
            
            print('[DEBUG] Read segment %s (FF %02X) of size %d: %s' % (segment_names[marker_type], marker_type, segment_size, segment_data[:100]))
            
            lecteur_de_bits_segment = LecteurDeBitsMSB(segment_data)
        
            if marker_type == 0xc0: # SOF0 - Start of Frame (baseline sequential DCT with Huffman)
                
                bits_par_composante = lecteur_de_bits_segment.lire_bits(8)
                
                assert bits_par_composante == 8
                
                hauteur_image_pixels = lecteur_de_bits_segment.lire_bits(16)
                largeur_image_pixels = lecteur_de_bits_segment.lire_bits(16)
                
                nombre_de_composantes = lecteur_de_bits_segment.lire_bits(8)
                
                index_composante_boucle_vers_ratio_subsampling_horizontal : Dict[int, int] = {}
                index_composante_boucle_vers_ratio_subsampling_vertical : Dict[int, int] = {}

                index_composante_boucle_vers_index_table_quantification : Dict[int, int] = {}
                
                for index_composante_boucle in range(nombre_de_composantes):
                    
                    index_composante_standard = lecteur_de_bits_segment.lire_bits(8)
                    
                    ratio_subsampling_horizontal = lecteur_de_bits_segment.lire_bits(4)
                    ratio_subsampling_vertical = lecteur_de_bits_segment.lire_bits(4)
                    
                    index_table_quantification_composante = lecteur_de_bits_segment.lire_bits(8)
                    
                    index_composante_boucle_vers_ratio_subsampling_horizontal[index_composante_boucle] = ratio_subsampling_horizontal
                    index_composante_boucle_vers_ratio_subsampling_vertical[index_composante_boucle] = ratio_subsampling_vertical
                    
                    index_composante_boucle_vers_index_table_quantification[index_composante_boucle] = index_table_quantification_composante
                    
            elif marker_type == 0xc2: # SOF2 - Start of Frame (progressive sequential DCT with Huffman)
                
                raise NotImplementedError("DCT progressif n'est pas supporté")
                
            
            elif marker_type == 0xc4: # DHT - Define Huffman Table(s)
                
                while lecteur_de_bits_segment.octets.tell() < len(segment_data):
                    
                    # Est-ce une table de Huffman qui doit encoder le maqueur
                    # de taille pour le bord inférieur gauche de chaque bloc ?
                    
                    est_table_ac_sinon_dc = bool(lecteur_de_bits_segment.lire_bits(4))
                    
                    # Quelle est la position de cette table dans la liste des
                    # tables Huffman ? (valeurs possibles : de 0 à 3)
                    
                    position_de_table_huffman = lecteur_de_bits_segment.lire_bits(4)
                    
                    assert 0 <= position_de_table_huffman <= 3
                    
                    # Quelle est le nombre de symboles pour chaque taille de
                    # code Huffman possible ?
                    
                    taille_de_code_vers_nombre_symboles : Dict[int, int] = {
                        position_taille_de_code + 1: lecteur_de_bits_segment.lire_bits(8)
                        for position_taille_de_code in range(16)
                    }
                    
                    # Quels sont les symboles définis dans notre table, pour
                    # chaque taille en bits utilisée ?
                    
                    valeur_vers_taille_de_code = {
                        lecteur_de_bits_segment.lire_bits(8): taille_de_code
                        for taille_de_code, nombre_symboles in sorted(taille_de_code_vers_nombre_symboles.items())
                        for index_symbole in range(nombre_symboles)
                    }
                    
                    # On a toutes les information, on stocke notre décodeur
                    # Huffman pour lorsqu'on devra décoder les données de l'image.
                    
                    position_vers_table_huffman[(position_de_table_huffman, est_table_ac_sinon_dc)] = DecodeurHuffman(
                        valeur_vers_taille_de_code = valeur_vers_taille_de_code,
                        lecteur_de_bits = lecteur_de_bits
                    )
                    
            
            elif marker_type == 0xdb: # DQT - Define Quantization Table
                
                while lecteur_de_bits_segment.octets.tell() < len(segment_data):
                                        
                    # Les valeurs de notre table de quantification sont-elles
                    # codées sur 8 ou 16 bits ?
                    
                    is_16_bits_precision_quantization_table = bool(lecteur_de_bits_segment.lire_bits(4))
                    
                    # On lit le numéro qui identifiera notre table de quantification
                    
                    position_de_table_de_quantification = lecteur_de_bits_segment.lire_bits(4)
                    
                    assert 0 <= position_de_table_de_quantification <= 3
                    
                    # On lit notre table de quantification
                    
                    position_vers_table_de_quantification[position_de_table_de_quantification] = [
                        lecteur_de_bits_segment.lire_bits(16 if is_16_bits_precision_quantization_table else 8)
                        for position_valeur in range(8 * 8)
                    ]
                    

            # SOS - Start of Scan : seule la taille de l'en-tête est donnée
            # explicitement, les données de l'image qui suivent sont de taille
            # variable
            
            elif marker_type == 0xda:

                class ComposanteYCbCr(IntEnum):
                    intensite = 1
                    difference_bleu = 2
                    difference_rouge = 3


                index_composante_boucle_vers_index_composante_standard : Dict[int, ComposanteYCbCr] = {}

                index_composante_boucle_vers_decodeur_huffman_ac : Dict[int, DecodeurHuffman] = {}
                index_composante_boucle_vers_decodeur_huffman_dc : Dict[int, DecodeurHuffman] = {}

                # On commence par lire l'en-tête des données, dont la taille a
                # été indiquée explicitement
                
                nombre_de_composantes = lecteur_de_bits_segment.lire_bits(8)
                
                for index_composante_boucle in range(nombre_de_composantes):
                    
                    index_composante_standard = lecteur_de_bits_segment.lire_bits(8)
                    
                    index_composante_boucle_vers_index_composante_standard[index_composante_boucle] = ComposanteYCbCr(index_composante_standard)
                    
                    position_composante_dans_table_huffman_ac = lecteur_de_bits_segment.lire_bits(4)
                    position_composante_dans_table_huffman_dc = lecteur_de_bits_segment.lire_bits(4)
                    
                    # Stocker les informations lues pour plus tard
                    
                    index_composante_boucle_vers_index_composante_standard[index_composante_boucle] = ComposanteYCbCr(
                        index_composante_standard)
                    
                    index_composante_boucle_vers_decodeur_huffman_ac[index_composante_boucle] = position_vers_table_huffman[
                        (position_composante_dans_table_huffman_ac, True)]
                    
                    index_composante_boucle_vers_decodeur_huffman_dc[index_composante_boucle] = position_vers_table_huffman[
                        (position_composante_dans_table_huffman_dc, False)]
                    
                donnees_specifiques_au_jpeg_progressif = lecteur_de_bits_segment.lire_octets(3)
                
                # On calcule au passage le nombre de MCU (blocs de 8x8 pixels,
                # macroblocs) qu'il nous faudra lire
                
                hauteur_image_mcu = int(ceil(hauteur_image_pixels / 8))
                largeur_image_mcu = int(ceil(largeur_image_pixels / 8))
                
                # Après avoir lu l'en-tête, on continue à lire des bits
                # directement sur le flux de données.
                #
                # On va extraire le flux de données avec une regex, vu
                # qu'on sait qu'il ne contient jamais l'octet "FF", sauf
                # suivi d'un "00" ce qui signifie qu'il s'agit d'un "FF"
                # échappé. Au passage on défait cet échappement.
                
                octets_restant_a_lire = lecteur_de_bits.octets.read()
                
                octets_de_donnees, autres_segments = match(b'^(.+?)(\xff[\x01-\xff].*)$', octets_restant_a_lire, flags = DOTALL).groups()
                
                lecteur_de_bits = LecteurDeBitsMSB(octets_de_donnees.replace(b'\xff\x00', b'\xff') + autres_segments)
                
                for decodeur_huffman in index_composante_boucle_vers_decodeur_huffman_dc.values():
                    decodeur_huffman.lecteur_de_bits = lecteur_de_bits
                for decodeur_huffman in index_composante_boucle_vers_decodeur_huffman_ac.values():
                    decodeur_huffman.lecteur_de_bits = lecteur_de_bits
                
                # On a défait l'échappement des "FF".
                
                index_composante_standard_vers_y_vers_x_vers_donnees : Dict[ComposanteYCbCr, Dict[int, Dict[int, int]]] = defaultdict(lambda: defaultdict(dict))
                
                index_composante_standard_vers_precedente_valeur_dc : Dict[ComposanteYCbCr, int] = defaultdict(int)
                

                for base_position_pixels_y in range(0, hauteur_image_mcu * 8, 8):
                    for base_position_pixels_x in range(0, largeur_image_mcu * 8, 8):
                
                        for (
                            (index_composante_boucle, index_composante_standard),
                            (index_composante_boucle, decodeur_huffman_ac),
                            (index_composante_boucle, decodeur_huffman_dc)
                        ) in zip(
                            sorted(index_composante_boucle_vers_index_composante_standard.items()),
                            sorted(index_composante_boucle_vers_decodeur_huffman_ac.items()),
                            sorted(index_composante_boucle_vers_decodeur_huffman_dc.items())
                        ):
                            
                            
                            # Lire les données de la composante encodées partiellement avec Huffman
                            
                            taille_bits_dc = decodeur_huffman_dc.lire_prochaine_valeur()
                            
                            if taille_bits_dc:
                                valeur_dc = complement_a_un_jpeg(taille_bits_dc, lecteur_de_bits.lire_bits(taille_bits_dc))
                            else:
                                valeur_dc = 0
                            
                            valeur_dc += index_composante_standard_vers_precedente_valeur_dc[index_composante_standard]
                            index_composante_standard_vers_precedente_valeur_dc[index_composante_standard] = valeur_dc
                            
                            # Après la valeur DC (coin en haut à gauche), lire les valeurs AC
                            
                            valeurs_dct : List[int] = [valeur_dc]
                            
                            while len(valeurs_dct) < 8 * 8:
                                
                                nombre_repetitions_et_taille_bits_ac = decodeur_huffman_ac.lire_prochaine_valeur()
                            
                                nombre_repetitions = nombre_repetitions_et_taille_bits_ac >> 4 # Nombre de répétitions de valeurs à zéro
                                taille_bits_ac = nombre_repetitions_et_taille_bits_ac & 0b00001111

                                # if nombre_repetitions_et_taille_bits_ac == 0:
                                if nombre_repetitions < 15 and taille_bits_ac == 0:
                                    valeurs_dct += [0] * (8 * 8 - len(valeurs_dct))
                                    break
                            

                                valeurs_dct += [0] * nombre_repetitions
                                                            
                                if taille_bits_ac:
                                    valeur_ac = complement_a_un_jpeg(taille_bits_ac, lecteur_de_bits.lire_bits(taille_bits_ac))
                                    
                                    valeurs_dct.append(valeur_ac)
                                
                                assert len(valeurs_dct) <= 8 * 8
                            
                            # Effectuer la déquantification (augmenter la
                            # valeur des entrées de la matrice DCT)
                            
                            index_table_quantification_composante = index_composante_boucle_vers_index_table_quantification[index_composante_boucle]
                            
                            table_de_quantification = position_vers_table_de_quantification[index_table_quantification_composante]
                            
                            for position in range(len(valeurs_dct)):
                                
                                valeurs_dct[position] *= table_de_quantification[position]  & 0xff
                            
                            # Effectuer le réagencement zig-zag
                            
                            valeurs_zig_zag : List[int] = [0] * (8 * 8)
                            for position in range(len(valeurs_dct)):
                                valeurs_zig_zag[table_decoder_zig_zag[position]] = valeurs_dct[position]
                            
                            y_vers_x_vers_valeur_zig_zag : List[List[int]] = [[0] * 8 for i in range(8)]
                            
                            for position_lineaire, valeur_zig_zag in enumerate(valeurs_zig_zag):
                                
                                position_y = position_lineaire // 8
                                position_x = position_lineaire % 8
                                
                                y_vers_x_vers_valeur_zig_zag[position_y][position_x] = valeur_zig_zag
                                    
                            
                            # Effectuer la DCT inverse (décodage DCT)
                            
                            valeurs_dct : List[List[int]] = decoder_dct(y_vers_x_vers_valeur_zig_zag)
                            
                            # Recoller les pixels avec ceux des autres MCU
                            
                            for y in range(8):
                                for x in range(8):
                            
                                    index_composante_standard_vers_y_vers_x_vers_donnees[index_composante_standard][base_position_pixels_y + y][base_position_pixels_x + x] = valeurs_dct[y][x]
                    
                # On a décodé notre image, on peut passer à l'affichage
                
                sequences_de_terminal_rgb : str = ''
                
                for position_y in range(hauteur_image_pixels):
                    
                    for position_x in range(largeur_image_pixels):
                
                        # Effectuer la conversion YCbCr vers RGB
                
                        intensite : int = index_composante_standard_vers_y_vers_x_vers_donnees[ComposanteYCbCr.intensite][position_y][position_x]
                        
                        difference_bleu : int = index_composante_standard_vers_y_vers_x_vers_donnees[ComposanteYCbCr.difference_bleu][position_y][position_x]

                        difference_rouge : int = index_composante_standard_vers_y_vers_x_vers_donnees[ComposanteYCbCr.difference_rouge][position_y][position_x]
                                                
                        # Les valeurs RGB :
                        
                        rouge = intensite + 1.402 * (difference_rouge - 128)

                        vert = intensite - 0.344136 * (difference_bleu - 128) - 0.714136 * (difference_rouge - 128)
                        
                        bleu = intensite + 1.772 * (difference_bleu - 128)
                        
                        rouge = max(0, min(255, rouge))
                        vert = max(0, min(255, vert))
                        bleu = max(0, min(255, bleu))
                        
                        # Transformer nos pixels en espaces colorés à afficher
                        # dans notre terminal, comme nous l'avons vus dans le
                        # chapitre sur PNG
                        
                        sequences_de_terminal_rgb += '\x1B[48;2;%d;%d;%dm' % (rouge, vert, bleu) + '  ' + '\x1B[0m'
                
                    sequences_de_terminal_rgb += '\n'
                
                # Afficher l'image décodée dans le terminal
                
                print(sequences_de_terminal_rgb.strip())
                
                lecteur_de_bits.aligner_bits_sur_octet()

            
        # Est-ce qu'il s'agit d'un marqueur non suivi de données ?
        
        elif 0xd0 <= marker_type <= 0xd9:
            print('[DEBUG] Read segment %s (FF %02X) with no data' % (segment_names[marker_type], marker_type))
            
            if marker_type == 0xd9: # EOI - End of Image
                
                break
        
                    
    
# Prendre un chemin de la part de l'utilisateur en entrée.

args = ArgumentParser(description = "Affiche une image JPEG de petite taille " +
    "dans le terminal, en utilisant les caractères d'échappement ANSI.")

args.add_argument('fichier_entree', help = "Chemin de l'image à décoder sur le disque.")

args = args.parse_args()

with open(args.fichier_entree, 'rb') as objet_fichier:
    mon_decodeur_jpeg(objet_fichier.read())

Notre .JPEG décodé s’affiche, avec quelques informations de débogage à propos de chaque segment savamment incluses :

Clem, la plus belle, avec des artéfacts !
Clem, la plus belle, avec des artéfacts !

Liens utiles :