Run-Length Encoding
A propos du code
Introduction à la méthode
#include <stdio.h>
#include "main.h"
#include <string.h>
typedef unsigned char u8;
int main(int argc, char* argv[])
{
Voici notre chaîne stockée dans un tableau. Et on stocke sa longueur dans une variable:
u8 input[] = "AAAAAEEEFFFFAAAHHHHJJJJJHHHH";
int inputSize = strlen((char*)input);
On va stocker le résultat de l'encodage dans une autre tableau. Dans le pire des cas, il utilisera 2 fois plus
u8 output[inputSize * 2];
int outputSize = 0;
Ces 2 lignes vont afficher la chaîne de départ à l'écran:
printf("input: %s\n", input);
printf("input size: %d\n", inputSize);
On va traiter un par un les caractères de la chaine en entrée.
int runStart = 0;
int currentByte = 1;
Ensuite, on démarre la boucle sur chaque caractère, et on calcule la longueur du run courant.
while (true)
{
int runLength = currentByte - runStart;
Si l'octet courant est différent du premier du run.
if (input[currentByte] != input[runStart] ||
currentByte == inputSize ||
runLength == 255)
{
... alors on écrit le run dans le tableau de sortie, et on en démarre un nouveau.
output[outputSize++] = runLength;
output[outputSize++] = input[runStart];
runStart = currentByte;
}
Si on atteint la fin de la chaine d'entrée, on sort de la boucle.
if (currentByte == inputSize)
break;
Sinon, on incrémente l'index de l'octet courant, et on reboucle au début
currentByte++;
}
Finalement on affiche le résultat:
printf("output: ");
for (int i = 0; i < outputSize; i += 2)
{
printf("(%d)", output[i]);
printf("%c ", output[i + 1]);
}
printf("\n");
printf("output size: %d\n", outputSize);
return 0;
}
Télécharger le code source
Télécharger l'exécutable pour Windows
Comme on s'y attendait, le résultat de ce programme est:
input: AAAAAEEEFFFFAAAHHHHJJJJJHHHH
input size: 28
output: (5)A (3)E (4)F (3)A (4)H (5)J (4)H
output size: 14
Maintenant, cet exemple marche, mais qu'est-ce qu'il se passe si on essaye avec une chaine comme "AEFAHJHABGHOZ" où
input: AEFAHJHABGHOZ
input size: 13
output: (1)A (1)E (1)F (1)A (1)H (1)J (1)H (1)A (1)B (1)G (1)H (1)O (1)Z
output size: 26
Télécharger le code source
Télécharger l'exécutable pour Windows
La sortie est 2 fois plus longue que l'entrée. Ca n'est pas une compression très efficace...
Codes, compteurs, runs et chaînes
#include <stdio.h>
#include "main.h"
#include <string.h>
typedef unsigned char u8;
#define MIN_RUN 3
#define MAX_RUN 127
#define MIN_STRING 1
#define MAX_STRING 127
J'ai mis la routine de compression dans une fonction séparée pour plus de clarté:
void compress(u8* input, int& inputSize, u8* output, int& outputSize)
{
Comme avant, on va utiliser un index pour le début du run courant, et pour le caractère courant.
int stringStart = 0;
int runStart = 0;
int currentByte = 1;
bool isInRun = false;
On commence la boucle de traitement.
while (true)
{
Si on est dans l'état "chaîne"...
if (isInRun == false)
{
// string mode
Au départ on va mettre à jour l'index runStart si on a trouvé un caractère qui n'est pas le même que le run courant.
if (input[currentByte] != input[runStart])
runStart = currentByte;
Ensuite j'ai essayé d'écrire le code de la même manière que celui qu'on a vu dans le premier chapitre.
int stringLength = runStart - stringStart;
int runLength = currentByte + 1 - runStart;
Puis on teste les conditions pour terminer la chaîne:
if (currentByte == inputSize ||
runLength == MIN_RUN ||
stringLength == MAX_STRING)
{
Si une de ces conditions est remplie on écrit la "chaîne" dans la sortie.
// emit string
if (stringLength >= MIN_STRING)
{
output[outputSize++] = stringLength;
for (int i = 0; i < stringLength; i++)
output[outputSize++] = input[stringStart++];
}
Puis, seulement si la condition de longueur minimale de run est remplie, on bascule dans l'état "run".
// go to run mode
if (runLength == MIN_RUN)
isInRun = true;
}
}
L'état "run" est pratiquement identique au code qu'on a vu dans le premier chapitre. La principale différence est
else
{
// run mode
stringStart = currentByte;
int runLength = currentByte - runStart;
if (input[currentByte] != input[runStart] ||
currentByte == inputSize ||
runLength == MAX_RUN)
{
// emit run
output[outputSize++] = 128 + runLength;
output[outputSize++] = input[runStart];
runStart = currentByte;
// go to string mode
isInRun = false;
}
}
Enfin la fin de la boucle est la même que dans le premier chapitre.
if (currentByte == inputSize)
break;
currentByte++;
}
}
La fonction principale ne fait qu'entrer la chaîne de départ dans la fonction de compression et affiche le
int main(int argc, char* argv[])
{
u8 input[] = "ABCDAAAABBCDDDDEEEEE";
int inputSize = strlen((char*)input);
u8 output[inputSize * 2];
int outputSize = 0;
printf("input: %s\n", input);
printf("input size: %d\n", inputSize);
compress(input, inputSize, output, outputSize);
printf("output: ");
int i = 0;
while (i != outputSize)
{
printf("(%d)", output[i]);
if (output[i] < 128)
{
int length = output[i++];
for (int j = 0; j < length; j++)
printf("%c", output[i++]);
}
else
{
i++;
printf("%c", output[i++]);
}
printf(" ");
}
printf("\n");
printf("output size: %d\n", outputSize);
return 0;
}
Télécharger le code source
Télécharger l'exécutable pour Windows
La sortie de ce programme nous montre un vrai encodage RLE.
input: ABCDAAAABBCDDDDEEEEE
input size: 20
output: (4)ABCD (132)A (3)BBC (132)D (133)E
output size: 15
Petites optimisations
#define MIN_RUN 3
#define MAX_RUN 127
#define MIN_STRING 1
#define MAX_STRING 127
La longueur minimale d'un run est 3. Donc le compteur ne sera jamais 128, 129 ou 130.
// emit run
output[outputSize++] = 128 + runLength - MIN_RUN;
De même on soustrait MIN_STRING au compteur d'une "chaîne".
output[outputSize++] = stringLength - MIN_STRING;
Et maintenant les compteurs peuvent prendre des valeurs maximales plus grandes
#define MIN_RUN 3
#define MAX_RUN (127 + MIN_RUN)
#define MIN_STRING 1
#define MAX_STRING (127 + MIN_STRING)
Télécharger le code source
Télécharger l'exécutable pour Windows
Il a aussi fallu faire une petite modification quand on affiche le résultat à l'écran mais ce n'est pas la partie
input: ABCDAAAABBCDDDDEEEEE
input size: 20
output: (3)ABCD (129)A (2)BBC (129)D (130)E
output size: 15
Remarquez que le premier code (3)ABCD veut dire qu'on va copier (3 + 1) caractères.
Décompression
void uncompress(u8* input, int& inputSize, u8* output, int& outputSize)
{
int i = 0;
outputSize = 0;
Ensuite, on va boucler jusqu'à ce qu'on atteigne la fin de l'entrée:
while (i != inputSize)
{
Le premier octet qu'on "lit" est le compteur. S'il est inférieur à 128, on a une chaîne, et on l'écrit dans la
if (input[i] < 128)
{
int length = input[i++] + MIN_STRING;
for (int j = 0; j < length; j++)
output[outputSize++] = input[i++];
}
Si le compteur était supérieur ou égal à 128, on a un run et on l'écrit.
else
{
int length = input[i++] - 128 + MIN_RUN;
for (int j = 0; j < length; j++)
output[outputSize++] = input[i];
i++;
}
}
}
Alors on va simplement appeler cette fonction après qu'on a compressé notre chaîne et voir ce qu'on obtient:
uncompress(output, outputSize, input, inputSize);
printf("uncomp: %s\n", input);
printf("uncompSize: %d\n", inputSize);
Télécharger le code source
Télécharger l'exécutable pour Windows
Et voici le résultat de ce programme. Comme on s'y attendait, on retrouve notre chaîne de départ.
input: ABCDAAAABBCDDDDEEEEE
input size: 20
output: (3)ABCD (129)A (2)BBC (129)D (130)E
output size: 15
uncomp: ABCDAAAABBCDDDDEEEEE
uncompSize: 20
Compresser des fichiers
int fileGetSize(char* fileName)
{
int fileSize;
FILE* f = fopen(fileName, "rb");
fseek(f, 0, SEEK_END);
fileSize = ftell(f);
fclose(f);
return fileSize;
}
La fonction fileLoad alloue de la mémoire et charge un fichier dedans.
int fileLoad(char* fileName, u8** mem)
{
int fileSize = fileGetSize(fileName);
*mem = (u8*)malloc(fileSize);
FILE* f = fopen(fileName, "rb");
fread(*mem, 1, fileSize, f);
fclose(f);
return fileSize;
}
La fonction fileSave sera utilisée pour sauvegarder soit un fichier compressé soit un fichier décompressé.
void fileSave(char* fileName, u8* mem, int fileSize, int originalSize)
{
FILE* f = fopen(fileName, "wb");
if (originalSize != 0)
fwrite(&originalSize, sizeof(int), 1, f);
fwrite(mem, 1, fileSize, f);
fclose(f);
}
Maintenant en utilisant ces fonctions utilitaires on peut écrire les fonctions qui vont compresser ou décompresser
void fileCompress(char* srcName, char* destName)
{
u8* input;
int inputSize;
u8* output;
int outputSize = 0;
inputSize = fileLoad(srcName, &input);
printf("original size: %d\n", inputSize);
output = (u8*)malloc(inputSize * 2);
compress(input, inputSize, output, outputSize);
fileSave(destName, output, outputSize, inputSize);
printf("compressed size: %d\n", outputSize);
free(input);
free(output);
}
Ensuite la fonction fileUncompress.
void fileUncompress(char* srcName, char* destName)
{
u8* input;
int inputSize;
u8* output;
int outputSize = 0;
inputSize = fileLoad(srcName, &input);
outputSize = *((int*)input);
output = (u8*)malloc(outputSize);
inputSize -= sizeof(int);
uncompress(input + sizeof(int), inputSize, output, outputSize);
fileSave(destName, output, outputSize, 0);
printf("uncompressed size: %d\n", outputSize);
free(input);
free(output);
}
Pour tester ces fonctions, j'ai choisi de compresser une image Targa non compressée de 16 couleurs.
int main(int argc, char* argv[])
{
fileCompress("image.tga", "comp.rle");
fileUncompress("comp.rle", "uncomp.tga");
return 0;
}
Télécharger le code source
Télécharger l'exécutable pour Windows
Et voilà la sortie de ce programme.
original size: 459986
compressed size: 170209
uncompressed size: 459986
Variantes