Génération de bargraphes en langage C

Génération d'une série de bargraphes

Le fichier gen-serie-bargraphes.c utilise la définition de quelques constantes :
// constantes
#include "bargraphe.h"
      
qui définissent des dimensions des bargraphes en pixels ainsi que le nombre de couleurs qu'ils peuvent contenir.
// dimensions des bargraphes en pixels
#define larg_bgraph  500
#define haut_bgraph  50

// nombre de couleurs de la palette
#define coul_bgraph  4
      
Il fait aussi appel aux deux bibliothèques de fonctions décrites ci-dessus et à la fonction qui permettra de générer l'un des bargraphes.
// bibliothèques de fonctions
#include "trtchamps.h"
#include "entete-bmp.h"
#include "genbargraphe.h"
      
Pour fonctionner, le programme gen-serie-bargraphes qui est lancé depuis PHP a besoin de 2 paramètres :
Il commence donc par vérifier s'il dispose bien de 2 paramètres et dans ce cas, tente d'ouvrir le fichier CSV en lecture.
// programme principal : récupère les données d'un fichier csv et crée les images bmp

void main (int narg, char *varg [])
{
    FILE *desccsv;    // descripteur du fichier csv local
    char champ [6];   // champ récupéré dans le fichier csv
    char imgbmp [30]; // nom d'une image bmp
    int  i;           // compteur
    float taux [coul_bgraph - 1]; // taux de vaccination pour chaque dose


    // si appel avec le bon nombre de paramètres
    if (--narg == 2)
    {
        // ouvrir le fichier csv en lecture
        desccsv = fopen (varg [1], "r");

        // si l'ouverture s'est bien passée
        if (desccsv)
        {
            // traitement pour générer les bargraphes
            (...)

            // terminé avec le fichier csv
            fclose (desccsv);
        }
        // sinon
        else
            // message d'erreur
            fprintf (stderr, "Fichier %s inaccessible ou inexistant\n", varg [1]);
    }
    // sinon
    else
        // autre message d'erreur
        fprintf (stderr, "Syntaxe : %s fichier_csv préfixe_images_bmp\n", *varg);
}
      
Si tout s'est bien passé jusque là :
    // initialiser l'entête pour les fichier bmp
    gen_entete_bmp (larg_bgraph, haut_bgraph, coul_bgraph);

    // sauter la première ligne du fichier csv
    finligne (desccsv);

    // récupérer le début du premier champ de la ligne suivante
    recupchamp (desccsv, champ);

    // tant que non fin de fichier csv
    while (*champ)
    {
        // traitement de la ligne lue
        (...)

        // récupérer le début du premier champ de la ligne suivante
        recupchamp (desccsv, champ);
    }
      
Pour les fichiers CSV locaux relatifs à l'âge des vaccinés, le premier champ récupéré est utilisable tel quel. En revanche, pour les bargraphes par région ou par département d'outre-mer, le nom de l'endroit concerné est exprimé en clair dans le fichier CSV. On préfère toutefois condenser ce nom en ne gardant que 3 caractères minuscules.

Le premier caractère du nom de région ou de DOM étant en majuscule, on le passe en minuscule par une opération sur les bits du caractère :
    // si champ alphabétique
    if (*champ > '9')
    {
        // passer le premier caractère en minuscules
        champ [0] = champ [0] | 0x20;
      
On aurait pu aussi utiliser la fonction tolower () pour ce traitement.

Ensuite, il y a 2 cas particuliers à traiter :

Si le nom d'un DOM commence par st (Saint-...), à la place des caractères 2 et 3, soit t- , on met les caractères 4 et 5 en passant le 4ème caractère en minuscules. Ainsi, par exemple, le nom St-Martin va donner le code sma .
    // si le nom du champ commence par "st"
    if (champ [0] == 's' && champ [1] == 't')
    {
        // prendre 2 autres caractères significatifs plus loin
        champ [1] = champ [3] | 0x20;
        champ [2] = champ [4];
    }
      
Autre cas particulier, le deuxième caractère du nom de région est un é . Comme ce site web utilise l'encodage de caractères UTF-8 qui est devenu un standard mondial, les caractères accentués des langues européennes sont codés sur 2 octets. On remplacera ce caractère par un e et il faudra récupérer le caractère qui le suit encore un octet plus loin.
    // sinon si le 2ème caractère est accentué
    else if (champ [1] & 0x80)
    {
        // le mettre en minuscules
        champ [1] = 'e';

        // et récupérer le caractère suivant
        champ [2] = champ [3];
    }
      
Enfin, on va tronquer ce champ à 3 caractères en mettant le caractère de fin de chaine (du langage C, mais pas que lui) juste après.
    // champ alphabétique limité à 3 caractères
    champ [3] = '\0';
      
À présent, entre le champ 1 du fichier CSV que l'on vient de traiter et le préfixe des noms de bargraphes passé en paramètre au lancement du programme (varg [2]), on a toutes les informations nécessaires pour définir le nom du bargraphe qui représentera les taux de vaccination pour le ligne courante du fichier CSV.
    // fabriquer le chemin d'accès au fichier image
    sprintf (imgbmp, "images/%s-%s.bmp", varg [2], champ);
      
Dans la ligne courante du fichier CSV, les champs qui suivent concernent l'effectif de population qui répond au critère du champ 1 et pour chacune des doses de vaccin, le nombre de doses injectées. Au total, autant de champs dont on n'a pas besoin que de couleurs dans le bargraphe.

On choisit d'ignorer ces champs.
    // sauter les champs relatifs à la population totale
    // et au nombre d'injections de chaque dose
    // soit autant de champs que de couleurs dans le bargraphe
    for (i = 0; i < coul_bgraph; i++)
        sautechamp (desccsv);
      
Les derniers champs de la ligne du fichier CSV concernent les taux de vaccination pour chacune des doses (soit un champ de moins que de couleurs dans le bargraphe, la dernière couleur étant pour les non vaccinés).

On va récupérer et mémoriser ces taux. Toutefois, c'est pour la dernière dose qu'il y aura eu le moins d'injections, suivie de l'avant dernière dose, et ainsi de suite jusqu'à la première dose qui est celle qu'aura reçue le plus grand nombre de personnes.

Pour pouvoir construire le bargraphe, on va mémoriser les taux de vaccination par valeurs croissantes, c'est à dire dans l'ordre décroissant des numéros de dose.

Comme en langage C (et dans beaucoup de langage de programmation), le numéro de la première case d'un tableau (qui correspondra à la dernière dose) est 0, le numéro pour la première dose sera le nombre de doses - 1.
    // une dose de vaccin de moins que de couleurs dans le bargraphe
    i = coul_bgraph - 1;

    // on va récupérer les taux de vaccination et les mémoriser
    // de la dernière dos à la première (ordre croissant des taux)

    // tant qu'on n'a pas récupéré tous les taux de vaccination
    while (i-- > 0)
    {
        // récupérer un taux de vaccination
        recupchamp (desccsv, champ);

        // le mémoriser
        taux [i] = atof (champ);
    }
      
À présent, on peut demander la création du bargraphe.
    // générer l'image du barregraphe
    cree_bargraphe (imgbmp, taux);
      
Maintenant, on lit le premier champ de la ligne suivante du fichier CSV local
    // récupérer le début du premier champ de la ligne suivante
    recupchamp (desccsv, champ);
      
et un reprend le même traitement jusqu'à ce que toutes les lignes du fichier CSV local aient été exploitées.

Génération d'une image bmp représentant un bargraphe

La fonction qui permet de générer un bargraphe utilise un grand nombre de variables locales :
    // position du dernier pixel pour chaque couleur dans la ligne
    int pix_dose [coul_bgraph];

    char *ligne;    // une ligne de l'image (2 pixels par octet)
    int  lg_ligne;  // le nombre d'octets qu'elle nécessite
    int  manque;    // pour compléter la ligne à un multiple de 4 octets
    int  pos_ligne; // position dans la ligne en cous de remplissage

    int  fin_coul;  // pour compter par groupe de 2 pixels
    float coefpix;  // nombre de pixels pour 1 %
    int  couleur;   // numéro de la couleur qu'on rajoute
    char d_pix;     // octet avec les valeurs de 2 couleurs consécutives
    int  num_ligne; // compteur de lignes
    FILE *descfic;  // descripteur du fichier bmp
      
En plus, pour les images bmp comportant jusqu'à 256 couleurs, il faut définir quelles couleurs sont utilisée parmi 2563 = 224 couleurs possibles. Ainsi, pour chaque pixel de l'image, il suffira d'indiquer un numéro de couleur.

Dans le fichier BMP, chaque élément de la palette de couleurs est composé de 4 octets : Une valeur sur 4 octets peut être mémorisée dans un entier 32 bits.

Toutefois, avec les microprocesseurs de type Intel, qui équipent les PC, les valeurs numérique sont stockées en mémoire de l'octet de poids le plus faible, à l'octet de poids le plus fort.

Ainsi, pour exprimer une couleur qui sera mémorisée dans un entier de 4 octets, on peut écrire un nombre hexadécimal de 6 caractères dans lequel les composantes colorées apparaîtront dans l'ordre rouge, vert, bleu.

Cet ordre est aussi celui du codage hexadécimal des couleurs dans les fichiers html.

Pour les bargraphes, on utilisera la palette de couleur suivante :
// palette des couleurs (violet, bleu, bleu vert, vert, jaune d'or, orange, rouge)
static long palette [] = {0x6000FF, 0x2020FF, 0x0080FF, 0x00E000,
                          0xFFE000, 0xFF8000, 0xFF0000};
      
Comme toutes les lignes du bargraphe seront identiques, on va en préparer une et la dupliquer sur toute la hauteur du bargraphe.

On commence par déterminer combien d'octets seront nécessaires pour mémoriser une ligne, sachant que pour une image comportant de 3 à 16 couleurs, on peut mémoriser les numéros de couleur dans la palette à raison de 2 pixels consécutifs par octet. Par contre, une ligne de l'image devra occuper un nombre d'octets multiple de 4.

Le nombre d'octets de la ligne étant calculé, on peut allouer en mémoire un tableau d'octets pour mémoriser son contenu.
    // calculer le nombre d'octets de la ligne mémorisée
    lg_ligne = larg_bgraph / 2;

    manque = lg_ligne % 4;

    if (manque)
        lg_ligne = lg_ligne + 4 - manque;

    // allouer la mémoire pour une ligne de l'image
    ligne = malloc (lg_ligne);
      
Ensuite, pour chaque couleur du bargraphe, on va calculer la position du pixel le plus à droite.

On a besoin de calculer le coefficient de conversion entre les taux de vaccination et le nombre de pixels dans le bargraphe. Or, un taux de 100 % correspond à la largeur du bargraphe

La dernière couleur (correspondant aux non vaccinés) permettra d'atteindre ces 100 % dans le bargraphe.

Pour les autres couleurs du bargraphe, on part des taux de vaccination que la fonction cree_bargraphe (...) a récupérés.
    // calcul du nombre de pixels pour 1 % dans le bargraphe
    coefpix = larg_bgraph / 100.0;

    // dernière couleur correspondant à "non vacciné"
    couleur = coul_bgraph - 1;

    // le dernier pixel pour cette couleur correspondra à 100 %
    pix_dose [couleur] = larg_bgraph;

    // tant qu'on n'a pas traité toutes les couleurs du bargraphe
    while (couleur)
    {
        // remonter d'une couleur (ce qui revient à traiter la dose suivante)
        couleur --;

        // dernier pixel de la couleur pour la dose concernée
        pix_dose [couleur] = (coefpix * taux [couleur]) + 0.5;
    }
      
À présent, on va remplir le tableau d'octets qui correspond au contenu d'une ligne du bargraphe. La difficulté est que chaque octet mémorisé va correspondre à 2 pixels successifs.

On démarre dans la partie gauche de la ligne avec la première couleur qui correspond à la dernière dose de vaccin et on va faire le même traitement pour toutes les couleurs du bargraphe.
    // initialisations
    couleur   = 0;  // couleur de la dernière dose
    pos_ligne = 0;  // début de la ligne

    // tant qu'on n'a pas traité toute les couleurs du bargraphe
    while (couleur < coul_bgraph)
    {
        (...)
    }
      
Dans un bargraphe, le plus probable est que 2 pixels consécutifs soient de la même couleur. On va donc remplir la ligne en traitant les pixels deux par deux.

On commence par préparer un octet d_pix qui va contenir les codes de couleur pour 2 pixels de couleur identique. En numérotation hexadécimale, le contenu de d_pix pour les différentes couleurs possibles sera 00h puis 11h, 22h, 33h, ... (si c'était utile, on pourrait aller jusqu'à FFh).

On va remplir la ligne avec l'octet préparé tant que 2 pixels consécutifs sont de cette couleur.
    // code de couleur pour 2 pixels consécutifs de la couleur courante
    d_pix = couleur * 0x11;

    // position du dernier couple de pixels de cette couleur
    fin_coul = pix_dose [couleur] / 2;

    // tant qu'on n'a pas traité tous les couples de pixels de cette couleur
    while (pos_ligne < fin_coul)
        // colorer 2 pixels dans la ligne
        ligne [pos_ligne ++] = d_pix;
      
Lorsqu'on a fini, deux cas peuvent se présenter :
  1. Les 2 pixels qui suivent ont 2 couleurs différentes.
  2. Les 2 pixels qui suivent sont de la même couleur.
On distingue ces 2 cas en cherchant si le rang du dernier pixel de la couleur qu'on est en train de traiter est impair ou pair.

Dans le premier cas, le plus souvent, le pixel qui suit est de la couleur suivante dans la liste des couleurs de la palette. On pourrait traiter simplement ce cas en rajoutant 1 à la variable d_pix.

Mais il arrive qu'une couleur du bargraphe doive être sautée. Par exemple, à partir de février 2022, le nombre de personnes de 70 à 74 ans ayant reçu au moins une dose de vaccin est tellement proche du nombre de personnes qui en ont reçu exactement deux doses, qu'il n'est pas possible de les différencier dans un bargraphe de 500 pixels de large sans tricher.

On va donc chercher dans une boucle la prochaine couleur pour laquelle il y a des pixels plus à droite dans le bargraphe que ceux de la dernière couleur traitée.

Cette couleur trouvée, rajouter les 2 pixels de couleur différente dans la ligne.

L'autre cas (les 2 pixels qui suivent sont de la même couleur), est traité beaucoup plus simplement : il suffit de passer à la couleur suivante et c'est au prochain tour de boucle que cette couleur sera traitée.
    // si le dernier pixel de cette couleur a une valeur impaire
    if (pix_dose [couleur] & 1)
    {
        // sauter les couleurs correspondant au même nombre de pixels
        do
            couleur ++;
        while (pix_dose [couleur - 1] == pix_dose [couleur]);

        // colorer ce pixels et le suivant dans les bonnes couleurs
        ligne [pos_ligne ++] = (d_pix & 0xF0) | couleur;
    }
    // sinon
    else
        // passer à la couleur correspondant à la dose précédente
        couleur ++;
      
Quand on a rajouté tous les couples de pixels dans la ligne, il y reste une opération (pas indispensable) qui consiste à initialiser la fin de la ligne, puisque celle-ci doit occuper un nombre d'octets multiple de 4.
    // initialiser la fin de ligne
    while (pos_ligne < lg_ligne)
        ligne [pos_ligne ++] = 0;
      
On dispose à présent de toutes les information nécessaires pour remplir le fichier qui contiendra le bargraphe.

On commence par ouvrir ce fichier en écriture et début de fichier (mode
w). Si ce fichier n'existait pas encore, il est créé vierge. Dans le cas contraire, son ancien contenu est effacé.

On recopie alors dans ce fichier :
  1. l'entête de l'image BMP,
  2. la palette des couleurs,
  3. le contenu de l'image.
Pour le contenu de l'image, comme toutes les lignes sont identiques, il suffit de recopier le contenu de la variable ligne autant de fois que nécessaire.
    // c'est parti pour générer le fichier image

    // ouvrir le fichier en écriture
    descfic = fopen (fichier, "w");

    // si l'ouverture s'est bien passée
    if (descfic)
    {
        // copier l'entête bmp
        fwrite (entete, sizeof (entete), 1, descfic);

        // copier la palette
        fwrite (palette, sizeof (palette), 1, descfic);

        // générer les lignes de l'image
        for (num_ligne = 0; num_ligne < haut_bgraph; num_ligne++)
            fwrite (ligne, lg_ligne, 1, descfic);

        // le fichier bmp est prêt
        fclose (descfic);
    }
    // sinon
    else
        // message d'erreur
        fprintf (stderr, "Impossible de créer le fichier %s\n", fichier);
      
Le fichier bmp est prêt. Le programme gen-serie-bargraphes pourra éventuellement demande d'en créer d'autres en traitant les lignes qui suivent dans le fichier CSV local.