Transformation de Burrows-Wheeler


22 juillet 2007

J'avais besoin d'un programme qui réalisait la transformation de Burrows-Wheeler (TBW) sur de simples lignes de texte et non pas sur des énormes fichiers binaires comme c'est le cas d'habitude. J'ai donc pondu le code qui suit :

#include <iostream>

int LineLen;

int Cmp(const void* a,const void*b)
{
    return strncmp(*(const char**)a,*(const char**)b,LineLen);
}

int main()
{
    int i;
    char    *Line=new char[1000], **Rows=new char*[1000];
    while(gets(Line)){
        LineLen = strlen(Line);
        //On utilise un double buffer pour la ligne courante
        memcpy(Line+LineLen, Line, LineLen);
        //Les permutations en utilisant de simples pointeurs
        for(i=0; i<LineLen; i++)        Rows[i]=Line+i;
        //On trie les pointeurs en fonction de leurs données
        qsort(Rows, LineLen, 4, Cmp);
        //On cherche la ligne originale dans la liste triée.
        i=0;
        while (i<LineLen && strncmp(Rows[i], Line, LineLen))    i++;
        int OriginalIndex = i;
        //
        printf("%d\t",OriginalIndex);
        for(i=0; i<LineLen; i++)    printf("%c", Rows[i][LineLen-1]);
        printf("\n");
    }
    delete []Line;
    delete []Rows;
}

pour les données d'entrée suivante :

ABCABC
abracadabra
BurrowsWheelerTransform
gaattcctgggcctggggctgtggcagctgcctcgtccct
tcacctcctggcttattctctccctccatatcttagcaat
ctcatgcctgtaatcccagcattttggtaggccaaggcgg
gtcggatcacctgaggtcaggagttcgagggccagcctga
tgaccatggtgaaaccccatctctactaaaaatacaaaat
taatcgggcatggtggcacatgcctgtaatcccagctact
ctgaggcaggagaatcgcttaaacccaggaagtggaggtt
tcagtgagctgagattgtgccattgcactccagcctgggt
aacaagagcaaaactccatcaaaaaaaataatatatgtat
atatatattacaattttatatatatatacacattatgtaa
taccattttatatatatacattacgtaatggtaaatgttt
gatcgtctccctggagaataatccccaatgtgaaattact
ctaagtggtgggattacaggcgtgtgcccaacttttcctg
agccttttgaggctgacaccagaggtagaagcccagcctc
tccccactggccatgtggggagaggctccagcctgcagca
accagggatctggcctcaagtgatgccccaacagtgggcg
1001110010001111110101011101101110010100
1100111011000100110110010000011011100111
1000100001000001010101101000010110000011
0000000000111111000100100001011010100011
0011011000001111001010010001001011101111
0010110110101011100100010011000000010000
1101011110010100001000111001000100011001
0011001010001001001110100000101001001101
1110110100001001100110001010101001100100
1110001011101110110000110100011011100001

le programme renvoie :

0   CCAABB
2   rdarcaaaabb
0   mrsrhelsWerafreToruwnBo
16  gcagtgctgtccgccgtgtgagtggtgtctgtcccgccca
28  cctctatgtcttcatctctctgagttattcccctacccaa
17  ctctaacccctggctgggcagtggaacttgggcaatctta
31  cccgggggtctgagttcccttgggaacgaagaaagtgccg
37  tacgaaaaatgataccaacccacaatttttgcaccagtaa
31  ttctcaaccgcagctgtgcaggtatgcttggtcaaagacc
17  taggagccggaacgcattggagtggcaataaagtatcgcg
29  cgcgccggctctggagcttattaatgatgtccgtgctgaa
8   caaacaaatacataaagtaatcttgactaaataagaaaca
11  tacttctttttattttcaaaatgtaaaaataataaatata
25  ttatttttttaaccacaatgctggattaaataatatttga
25  ggtcatgaagaacccttcttactagttctatacaggccaa
13  cttacagcacgtggcagtgtgatttgaccttgcgggattc
7   ggctcaccggacctacgggcgcattaaaagaaggcctttc
35  cgccgcccgcgcctgctggcagatagaggtagttcaccga
3   ccagcacggctcacacgggctcgttgggtgataacagacg
21  1111100100111011001010010110111100010110
30  1010101011001100100011011111001110010010
30  1110100100000000001010111001001100000100
0   1000001010101000001010010101000100101110
8   1010101110010001010110010101010110111000
13  1010000010010010001010111100101010001000
35  1101110010011000010001000011101011010100
9   1010011101111110100000000000100010010100
39  1010101111110011000010011100010001000010
36  1110100000101001110111011110011010101000

Je voulais un code qui soit rapide à implémenter mais :

  • la limite de 1000 octets n'est pas terrible pour une routine généraliste.
  • j'utilise qsort qui n'est pas optimal par rapport à mon radix sort pour des données de taille importante.
  • J'utilise gets ce qui est déconseillé !
  • On n'a pas à dupliquer la ligne de données, en compliquant un peu le code ça devrait pouvoir marcher.
  • Le buffer Rows est de trop il faut trouver une astuce pour ne pas l'utiliser il prend une mémoire folle.

Tout ceci sera fixé plus loin, pour l'instant ce qui nous intéresse c'est d'avoir un outil de test à utiliser en conjonction avec d'autres compresseurs maison.

La transformation inverse

Maintenant que l'exécutable précédent (que j'ai appelé bwt) est fait et fonctionne, comment retrouver à partir de sa sortie les données originales ?

Le LZ* le plus intéressant : le LZSS


19 juillet 2007

J'apprécie depuis longtemps l'algorithme de compression sans perte LZSS et j'ai décidé de lui dédier une page de ce site pour l'illustrer et l'implémenter du mieux possible.

Pour faire vite et simple, l'algorithme compresse les données d'entrée en remplaçant des segments de caractères par des références vers des segments identiques déjà rencontrés. Ainsi Fulbert à la position 100 sera remplacé par Ful<25,4> si bert a déjà été rencontré à la position 25. Nous pouvons encoder la référence <position, taille> sur deux caractères, on verra plus tard comment et ainsi éliminer 4 - 2 = 2 caractères, d'où la compression. C'est, on s'en doute quelque chose qui fonctionne très bien pour le texte.

Voici venu le moment d'un exemple de code qui réalise cette compression dans des conditions idéales : on alloue un buffer suffisamment grand pour ne pas se poser la question du bouclage et on ne doit ni sauver les données ni les relire. Le but étant de tester le concept (qui marche depuis 1982 on ne fait donc pas là une grande découverte) et de fournir quelques statistiques :

getMatch(cursor)
    ... recherche ce qui se trouve à partir de la position <cursor> dans le dictionnaire.

store(position, len)
    ...stocke dans le dictionnaire <len> octets des données d'entrées à partir de <position>.

emitSegment(match)      output <match.offset, match.len>
emitChar(c)             output c

main()
{
    cursor = 0
    while (cursor != EOF)
        if (Match = getMatch(cursor))
            emitSegment(Match)
            store(cursor, Match.len)
            cursor += segment.length
        else
            emitChar(cursor)
            store(cursor, 1)
}

Le principe : on lit chaque caractère l'un après l'autre et soit on a déjà rencontré ce qui suit auquel cas on émet un segment, soit on n'a encore jamais rencontré ça et on émet le caractère tel quel. C'est extrèment simple et le code l'est également.

Tout ceci donne, une fois traduit, en C :

char*   Dic = NULL;
int     DicCursor = 0, DicLength = 0, DicStoredLength = 0;

void    addToDictionary(char c)
{
    Dic[DicCursor++] = c;
    DicStoredLength++;
}

bool    searchMatch(const char* strToCode, int cursor, int bufLn, int& longestMatchOffset, int& longestMatchLength)
{
    longestMatchLength = longestMatchOffset = 0;
    int DicPos = 0, i = 0;
    while (DicPos < DicStoredLength)        {
        while (DicPos+i<DicStoredLength && cursor+i<bufLn && strToCode[cursor+i] == Dic[DicPos+i])      i++;
        if (longestMatchLength<i)       {
            longestMatchOffset = DicPos;
            longestMatchLength = i;
        }
        DicPos++;
        i = 0;
    }
    return longestMatchLength != 0;
}


void    outputChar(char c)              {   printf("%c", c);    }
void    outputCode(int offset, int ln)  {   printf("<%d, %d>", offset, ln);     }

int main(int argc, char *argv[])
{
    DicLength = 2048;
    FILE*   FileToCode = fopen("../../config.h.in", "r");
    if (!FileToCode)        return EXIT_FAILURE;
    fseek(FileToCode, 0, SEEK_END);
    long    FileSize = ftell(FileToCode);
    char*   ToCode = new char[FileSize];
    rewind(FileToCode);
    fread(ToCode, 1, FileSize, FileToCode);
    fclose(FileToCode);
    //
    Dic = new char[DicLength];
    int Ln = FileSize;
    int i = 0, EncodedSize = 0;
    int NbRawChars = 0, NbBlocks = 0;
    int HeaderSize = 3, j;
    while (i<Ln)        {
        int MatchOffset, MatchLength;
        char    c = ToCode[i];
        if (searchMatch(ToCode, i, Ln, MatchOffset, MatchLength) && MatchLength>2)      {
            outputCode(MatchOffset, MatchLength);
            for(j=i; j<i+MatchLength; j++)      addToDictionary(ToCode[j]);
            i += MatchLength;
            EncodedSize += 2;
            NbBlocks++;
        }   else    {
            outputChar(c);
            i++;
            EncodedSize++;
            NbRawChars++;
            addToDictionary(c);
        }
    }
    delete []Dic;
    printf("\n");
    printf("Taille originale : %d\n", Ln);
    printf("Taille compressée hors header et flags : %d\n", EncodedSize);
    printf("Nombre de caractères émis de façon brute : %d\n", NbRawChars);
    printf("Nombre de blocs <offset,ln> : %d\n", NbBlocks);
    int TotalPackedSize = EncodedSize + HeaderSize + (NbRawChars>>3) + 1;
    printf("Taille totale compressée : %d\n", TotalPackedSize);
    printf("Ratio : %.3f %%\n", 100.0f * TotalPackedSize / Ln);
    return EXIT_SUCCESS;
}

Voyons ce que ça donne pour plusieurs sources d'entrée :

Taille originale : 35147
Taille compressée hors header et flags : 11621
Nombre de caractères émis de façon brute : 1933
Nombre de blocs <offset,ln> : 4844
Taille totale compressée : 11866
Ratio : 33.761 %", "Source : <a href='http://www.gnu.org/licenses/gpl-3.0.txt'>GPL v3</a>
Taille originale : 702748
Taille compressée hors header et flags : 174968
Nombre de caractères émis de façon brute : 3628
Nombre de blocs <offset,ln> : 85670
Taille totale compressée : 175425
Ratio : 24.963 %

Vous remarquerez que la taille compressée est inférieure de 80ko au fichier Zip référencé sur le site du Projet Gutenberg. C'est un bon début :)

Bien sûr tout ça n'est pas très sérieux : ça prend un temps fou (la méthode de comparaison est risible, au moins 3 minutes pour Pride and Prejudice...), le buffer s'adapte aux données d'entrées (je préfèrerai qu'il soit fixe) et il n'y a toujours pas de sauvegarde des données pour vérifier que ça fonctionne dans les deux sens. Mais ça n'est pas grave, c'est la première version et on a vérifié que le code est faisable (ouf c'était dur) (je plaisantais dans la dernière parenthèse, on est d'accord hein ?).

La première optimisation

En rajoutant trois petites lignes à la recherche on divise par 3 le temps passé dans la routine. Ces lignes sont là pour passer sur un caractère s'il n'est pas égal au premier recherché :

bool    searchMatch(const char* strToCode, int cursor, int bufLn, int& longestMatchOffset, int& longestMatchLength)
{
    longestMatchLength = longestMatchOffset = 0;
    //Recherche naïve
    int DicPos = 0, i = 0;
    while (DicPos < DicStoredLength)        {
        char    CharToLookFor = strToCode[cursor];
        while (DicPos<DicStoredLength &&  Dic[DicPos] != CharToLookFor)     DicPos++;
        i=1;
        while (DicPos+i<DicStoredLength && cursor+i<bufLn && strToCode[cursor+i] == Dic[DicPos+i])      i++;
        if (longestMatchLength<i)       {
            longestMatchOffset = DicPos;
            longestMatchLength = i;
        }
        DicPos++;
        i = 0;
    }
    return longestMatchLength != 0;
}

Deuxième optimisation : hash table et listes chaînées

Tout le monde connait le concept et l'intérêt des Hash Table, et bien pour le LZSS c'est impressionnant d'intérêt. Si on cherche une chaîne dans le dictionnaire on peut parcourir ce dernier comme précédemment ou bien demander à une fonction "donne-moi les emplacements du dictionnaire qui contiennent telle lettre". Plus de parcours ! On va directement au bon endroit. La lettre en question s'appelle la clé (de la requête).

Bien. Mais quelle est la structure de donnée qui permet de poser cette question efficacement, comprendre rapidement (en O(1) si possible) ? Et bien c'est une Hash Table, dont les clés sont les premières lettres hashées, combinée avec des listes chaînées contenant des pointeurs vers les emplacement dans le dictionnaire.

Tout ça économise énormément de temps sur la recherche d'un segment du dictionnaire intéressant mais c'est optimisable : il suffit non plus d'utiliser la première lettre mais plusieurs :) En fait je suis arrivé à une version qui utilise les 4 premières lettres des données d'entrées, les combine en un nombre qui représente un index dans ma Hash Table pour parcourir une liste de cellules chaînées pour trouver mon segment le plus intéressant. Les résultats ?

Un résultat intermédiaire m'a donné pour Pride & Prejudice 102 secondes pour une recherche naïve (qui utilise tout de même la première optimisation) et 3.37sec pour la version "Hash Table" avec un buffer illimité.

Si on est maintenant prêt à oublier un peu le taux de compression pour privilégier la vitesse d'encodage, en limitant la taille du dictionnaire à 32768 octets on passe à 13.7 secs pour la version naïve et à 0.27 secs pour la version utilisant une hash table.

Rien à dire ça commence à être praticable...

Reste à présent à implémenter la sérialisation (la sauvegarde et la relecture) et à améliorer l'occupation mémoire parce que pour arriver à presque 1/4 de seconde j'ai créé plein de choses dans la RAM du PC...

La moralité : quand on constate que j'ai un facteur 1000 entre cette optimisation et la recherche naïve on se dit que l'algo le plus intéressant de recherche de chaîne de caratères ne pourra jamais battre une indexation à 4 caractères ! Dommage, parce que ça veut dire que Jalut écrase Dāwūd, mais c'est logique. D'un autre côté si quelqu'un est arrivé à faire quelque chose avec KMP ou Boyer-Moore ça m'intéresse !

La suite...

on décode des données sauvées On compare toujours les temps des différentes méthodes pour savoir où l'on en est. L'usage de l'algo ? On utilise des fichiers de différents types : wav, dwg, txt & cpp, images raw. Les améliorations : - taille variable du stockage des segments (1, 2 ou 3 octets). - on sauve séparément les flux de caractères, de bits de contrôle et de segments (position et taille séparément également). - on compresse à nouveau les flux différemment en fonction de leurs contenus - stockage définitifs des segments à partir d'un certain seuil (ils ne sont plus supprimés du dico mais déplacés ailleurs)

Les exécutables réduits - C'est possible !


10 juin 2007

Petite page temporaire de prise de notes pour un futur point complet sur l'analyse et la réduction des exécutables.

#include <stdio.h>
#include <string.h>

int pgcd(int u, int v, int verbose)
{
    int M = 1;
    while (u && v)      {
        if (verbose)        printf("pgcd(%d, %d)\n", u, v);
        if (!(u&1) && !(v&1))       { M <<= 1; u>>=1, v>>=1; }
        else    if (!(u&1) && v&1)      u>>=1;
        else    if (u&1 && !(v&1))      v>>=1;
        else    {
            //Les deux nombres sont impairs
            if (u>v)    u=(u-v)>>1;
            else        v=(v-u)>>1;
        }
    }
    return M * (u ? u : v);
}

int printSyntax(char* progName, int retVal)
{
    printf("Syntaxe pour calculer le PGCD de u et v :\n\n\t%s [--verbose] <u> <v>\n\n", progName);
    return retVal;
}

int main(int argc, char** argv)
{
    int Verbose;
    int N[2], i, j;
    for(i=1, j=0; i<argc && j<2; i++)    {
        if (!strcmp(argv[i], "--verbose"))  Verbose = 1;
        if (!strcmp(argv[i], "--help"))     return printSyntax(*argv, 0);
        if (!sscanf(argv[i], "%d", &N[j++]))    j--;
    }
    if (argc<=2 || j<2) {
        printf("J'ai besoin de deux pour calculer leur PGCD.\n");
        return printSyntax(argv[0], 1);
    }
    //
    printf("%d\n", pgcd(N[0], N[1], Verbose));
    return 0;
}

Si on compile le source ci-dessus avec les commandes suivantes on arrive à un exécutable de 2764 octets (sur ma machine) :

gcc -Os -Wall -L`gcc -print-file-name=` /usr/lib/crt1.o /usr/lib/crti.o -nostdlib -o pgcd pgcd.c /usr/lib/crtn.o -lc
strip --strip-unneeded -R .comment -R .gnu.version -R .note.ABI-tag pgcd

Questions :

  • le code (le segment .text) ne fait que 557 octets et les segments .init et .fini ensembles 36 octets. Total = 593 octets. Pourquoi 2171 octets en plus ?
  • Optimiser le fichier ELF résultant.
  • Combler le fossé (500 octets je crois) entre les versions C et C++.
  • Déduire les meilleures pratiques (code et compil) pour avoir un code le plus petit possible.

OpenGL : la base


9 avril 2007

OpengL - généralités

Effacement d'un écran

glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);

Effacement de l'écran et du z-buffer

glClearColor(0.0, 0.0, 0.0, 0.0);
    glClearDepth(1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Efface l'écran en noir et met chaque pixel du z-buffer à la valeur 1.0.

Initialisation d'une fenêtre en GLUT

glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
glutInitWindowPosition(100, 100);
glutInitWindowSize(400, 400);
glutCreateWindow("Titre de ma fenêtre");

Exemple de callback appelée par glutReshapeFunc

glutReshapeFunc(animReshape);

void animReshape(int w, int h)
{
    glViewport(0, 0, (GLsizei) w, (GLsizei) h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-50, 50, -50, 50, -1, 1);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

Comment dessiner un carré à l'écran ?

//Première méthode : on utilise un glRect pour faire simple
glColor3f(1,0,1);
glBegin(GL_POLYGON);
glRectf(0.25, 0.25, 0.75, 0.75);
glEnd();
glFlush();

//Deuxième méthode : on utilise plusieurs appels à glVertex3f pour décomposer le polygone
glColor3f(1,0,1);
glBegin(GL_POLYGON);
    glVertex3f(0.25, 0.25, 0.0);
    glVertex3f(0.75, 0.25, 0.0);
    glVertex3f(0.75, 0.75, 0.0);
    glVertex3f(0.25, 0.75, 0.0);
glEnd();
    glFlush();

Exemples d'appels valides à glVertex*

glVertex2s(2, 3);
glVertex3d(0.0, 0.0, 3.1415926535898);
glVertex4f(2.3, 1.0, -2.2, 2.0);

GLdouble    dvect[3] = {5.0, 9.0, 1992.0};
glVertex3dv(dvect);
Vérifier si il y a un avantage en temps machine à utiliser la version pointeur de glVertex*() ou pas.

Affichage d'un cercle généré

glBegin(GL_POLYGON);
//Le cercle est blanc
glColor3d(1,1,1);
//Début des déclarations des points composant le cercle
#define     PI      3.141592654f
int i = 0, NbPoints = 30;
float   Radius = 25.0f;
for(i=0; i>NbPoints; i++)       {
    float   Angle = 2.0f*PI*i/NbPoints;
    glVertex2f(Radius*cos(Angle), Radius*sin(Angle));
}
//Tous les points ont été calculés, on peut provoquer le rendu
glEnd();
Adapter le vieux code des meta-balls.

Exemple d'utilisation de glPolygonMode

glPolygonMode(GL_FRONT, GL_FILL);   //Faces avant remplies
        glPolygonMode(GL_BACK, GL_LINE);    //Faces arrières outlined
Faire des routines de générations d'objets procéduraux. Ajouter les caps (de l'appendice B du bouquin) et toutes les fonctions de requêtes. Ajouter au Snippets la possibilités de faire des liens avec des fichiers externes, sous la forme de dock OSX ça serait pas mal : ça s'ouvre au bas du bloc orangé et contient les icônes des images ou des fichiers. Bien sûr ça peut se refermer.

Réglages préliminaires : viewport, projection & caméra

    //Réglages de la projection
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0, 1.0, 0.5, 100.0);      //Un glFrustum bien paramétré est équivalent à cet appel GLU
    //Réglages du système objet
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    //Définition de l'espace de dessin
    glViewport(0, 0, (GLsizei) 400, (GLsizei) 400);
    //Position de la caméra, point de visée, vecteur Up de la caméra
    gluLookAt(0.0, 0.0, 0.0,  0.0, 0.0, 100.0,  0.0, 1.0, 0.0);

Les différentes transformations

gluLookAt(pos, target, up) : visualisation
glScalef(scalev) : modélisation
glMatrixMode(GL_PROJECTION) puis glFrustum() : projection

Première application

On affiche ici un cube en fil de fer face à la caméra en rafraîchissant la fenètre et la redimensionnant au besoin.

#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glut.h>

void    display(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glColor3f(1,1,1);
    glLoadIdentity();
    gluLookAt(0,0,5,0,0,0,0,1,0);
    glScalef(1,2,1);
    glutWireCube(1);
    glFlush();
}

void reshape(int w, int h)
{
    glViewport(0,0,(GLsizei)w,(GLsizei)h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glFrustum(-1,1,-1,1,1.5,20);
    glMatrixMode(GL_MODELVIEW);
}

int main(int argc, char *argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(400, 400);
    glutCreateWindow(\"Titre\");
    //
    glClearColor(0,0,0,0);
    glShadeModel(GL_FLAT);
    //
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutMainLoop();

    return EXIT_SUCCESS;
        }
Fichier first.cpp

A compiler avec la commande suivante :

g++ -Wall -lglut first.cpp -o first

Convertisseur HTML vers PDF


8 avril 2007

Convertisseur de fichier HTML en PDF

Objectif : réaliser une combinaison logicielle permettant, quelque soit la page HTML, d'en déduire un fichier PDF le plus fidèle possible. Piste : l'ensemble contiendra une application C++ (aka binaire) rapide qu'on nourrira de la page HTML qui, utilisant PDFLib, générera le fichier PDF. Note : il ne faut pas oublier la gestion des fontes à insérer dans les PDFs produits. Gabarit :

//On récupère le flux d'entrée
    HTMLFile = stdin();
    //On vérifie que l'entrée suit bien les specs XHTML
    if (!(ParsedDatas = parseXML(HTMLFile)))        return EXIT_FAILURE;
    //On crée le document PDF
    Doc = new PDFDocument();
    if (!Doc->generatePDF(ParsedDatas))     return EXIT_FAILURE;
    //On envoie le PDF produit sur la sortie standard
    Doc->echo();
    return EXIT_SUCCESS;
        

Bien sûr les fonctions/méthodes utilisées (stdin(), parseXML() etc) n'existent pas encore, mais le but de ce document est d'y remédier. Le choix est de recevoir des données depuis stdin et d'émettre le produit sur stdout pour rendre l'usage de l'application multiple : aujourd'hui, nous avons besoin de transformer des pages web depuis une application serveur PHP, mais demain les spécifications peuvent évoluer (Java, transformation PDF en batch etc) il faut donc qu'elles puissent s'adapter à ce genre de situations. Je veux pouvoir être capable d'écrire les lignes de scripts suivantes :

    cat test.html | pdfconverter > test.pdf
    wget "http://new.google.com/" | pdfconverter > new.google.com.pdf
    wget "http://new.google.com/" | pdfconverter > test.pdf && echo "Texte du mail" | mutt -a test.pdf -t "Titre" 42@flubb.net
    pdfconverter --input-file test.html > test.pdf
    pdfconverter --input-file test.html --output-file test.pdf', "Insertion du convertisseur de HTML en PDF dans n'importe quelle chaîne de production

Il faut à présent détailler toutes ces étapes : 1- interception du flux d'entrée depuis l'entrée standard 2- parsing XML utilisant libxml2 3- implémentation d'une classe PDFDocument 4- émission sur la sortie standard Le point 3 est le plus problématique mais les trois autres ne sont pas à négliger : elles peuvent en effet poser problèmes plus tard si elles ne sont pas bien affutées maintenant. On va donc les traiter dans le désordre :

1- interception du flux d'entrée depuis l'entrée standard

?a peut être fait de façon simpl(e|iste) mais si on veut essayer de minimiser les new (en gros n'en faire qu'un seul), la façon ci-dessous correspond :

void    getSTDIn(char** ptr, int& nbLines, int& totalLength)
{
    int     LineLength = 1000;
    nbLines = 0;
    totalLength = 0;
    char*   Line = new char[LineLength];
    char*   FGetsStatus = NULL;
    FILE*   STDInCopy = tmpfile();
    //
    do  if (FGetsStatus = fgets(Line, LineLength, stdin))       {
        int Ln = strlen(Line);
        fwrite(Line, Ln, 1, STDInCopy);     //###CHECK
        nbLines++;
        totalLength += Ln;
    } while(FGetsStatus != NULL);
    delete []Line;
    //
    rewind(STDInCopy);
    //
    *ptr = new char[totalLength+1];
    (*ptr)[totalLength] = 0;
    fread((*ptr), totalLength, 1, STDInCopy);       //###CHECK
    fclose(STDInCopy);      //On efface le fichier temporaire
        }
Lire stdin en deux passes pour minimiser les new

2- parsing XML utilisant libxml2

void    displayNode(xmlNodePtr node, int level)
{
    if (!node)      return;
    while (node)        {
        int i;
        for(i=0; i<level; i++)      printf(\"  \");
        printf(\"%s, type %d, Contenu : %s\\n\", node->name, node->type, node->content);
        if (node->children)     displayNode(node->children, level+1);
        node = node->next;
    }
}

//Affiche l'arbre de parsing de libXML2
void showXMLTree(xmlDocPtr doc)
{
    xmlNodePtr  HTMLNode;
    printf(\"Document : %s\\n\", doc->name);
    xmlNodePtr  CurNode = doc->children;
    displayNode(CurNode, 1);
}


int main(int argc, char *argv[])
{
    // Buffer contient le texte à parser
    xmlDocPtr   HTMLDoc;
    HTMLDoc = xmlParseDoc((xmlChar*)Buffer);
    showXMLTree(HTMLDoc);
    //...
        }

Ce qui produit :

Pour l'entrée HTML suivante on obtient l'output au-dessous :

<html>
    <head>
        <title>Titre de ma page</title>
    </head>
    <body>
        <p>Premier paragraphe</p>
    </body>
</html>

-------------------------------------

Document : (null)
    html, type 1, Contenu : (null)
        text, type 3, Contenu :

        head, type 1, Contenu : (null)
            text, type 3, Contenu :

            title, type 1, Contenu : (null)
                text, type 3, Contenu : Titre de ma page
            text, type 3, Contenu :

        text, type 3, Contenu :

        body, type 1, Contenu : (null)
            text, type 3, Contenu :

            p, type 1, Contenu : (null)
                text, type 3, Contenu : Premier paragraphe
            text, type 3, Contenu :

   text, type 3, Contenu :

4- émission sur la sortie standard

On utilise simplement la fonction puts pour envoyer un contenu sur le terminal (ou même un - encore plus simple - printf). On peut donc passer aux choses sérieuses...

3- implémentation d'une classe PDFDocument

On reprend le code de PDFDocument.php et html2pdf.php pour commencer le travail. Question de debug : comment débugguer un programme que l'on veut nourrir par un pipe ? Je ne vois pas comment faire ça sous KDevelop. C'est ça qui m'empêchait de debugguer le fgets qui était pending (les io étaient bloquantes). Deux choses : essayer stdio sans lock et voir comment envoyer des données à un programme qui tourne (un pipe est activé une fois que l'applicatino est lancée donc).

Le programme de développement

Pour la version 0.1 : - générer un PDF simple pour tester les bindings de PDFLib6. Pour la version 0.2 : - Gérer plusieurs formats de pages (a4, a3 etc). - Récupérer le code du texteur PHP pour afficher du texte multi-ligne. Pour la version 0.3 : - faire que les scripts du tableau 2 fonctionnent. - gérer les éléments HTML suivants : p, b, i, u, a, div. Pour la version 0.4 : - gérer les styles suivants : font-style, color, background-color Pour la version 0.5 : - gérer les images (essentiel mais très utile également pour le débug des tableaux de la phase suivante). Pour la version 0.6 : - gérer les tableaux Pour la version 0.7 : - gérer la pagination - gérer les headers et les footers - ajouter la gestion d'autres styles. Pour la version 0.8 : - afficher correctement la fiche contact de Linda - ajouter la gestion d'autres styles. Pour la version 0.9 : - afficher correctement un rapport NCA de Linda - ajouter la gestion d'autres styles. Pour la version 1.0 : - afficher correctement deux synthereports de GED-Pro : hopital-couple-enfant et chu-rennes. - finir la gestion des styles.

Accueil1 2 3 4 5