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.

Accueil