Je reviens sur l’article « Solution générique d’impression de pages web » que j’avais rédigé il y a quelque temps. Je proposais dans le billet initial de traiter une problématique récurrente des applications web : comment générer un fichier PDF à partir d’une page html ? La démarche était expliquée et la dernière piste de solutions évoquée consistait à utiliser un client web côté serveur, qui permettait de générer le fichier PDF. En l’occurrence il s’agissait de CutyCapt.

Cette solution a ses avantages :

  • L’utilisateur final n’a pas à installer un module dans son navigateur : PDFCreator, DoPDF, etc.,
  • Le prestataire n’a pas à développer deux fois chaque écran « imprimable » : pour une présentation HTML côté client et pour une présentation PDF côté serveur,
  • Le client est satisfait car le prestataire ne développe pas deux fois !

Concrètement, la mise en oeuvre de cette solution a quand même un coût : celui de la mise en place initiale. Une fois le système en place, toutes les ressources adressables de l’application en question seront « imprimables ».

En conclusion du billet initial, vous avez pu lire que le principal inconvénient de CutyCapt est son manque de souplesse. Par exemple, le délai nécessaire pour lancer la capture est gênant :

  • si on le fixe trop court, on n’est pas certain du résultat en sortie. C’est le cas lorsque le chargement du site n’est pas totalement terminé,
  • si on le fixe trop long, on perd du temps alors que le serveur reste a terminé son travail.

Depuis l’écriture du billet et pour une mise en oeuvre concrète, j’ai pu mettre en oeuvre le même principe avec une librairie bien plus intéressante : PhantomJS.

# PhantomJS

Cette librairie est plus souple que CutyCapt car elle met à disposition une API javascript qui permet de piloter l’impression :

  • créer une « page web » en chargeant son contenu ou en utilisant une URL,
  • injecter de nouveaux fichiers javascript dans la page,
  • envoyer des événements souris : clic, etc.,
  • accéder au système de fichiers,
  • définir la taille de la partie visible, le format A4, etc.,
  • lancer la capture vers un fichier sur disque.

# Installation de PhantomJS

Pour une installation sous CentOS ou Debian, comptez un peu de temps pour configurer un serveur X (ou utiliser Xvfb) et installer QT ainsi que GIT avant de récupérer le code source de PhantomJS et de le compiler. Vous pouvez également récupérer la version compilée. Tout est ici.

# Utilisation de PhantomJS

L’utilisation de cette librairie est triviale : on se contente d’écrire un fichier javascript dont le chemin est passé à PhantomJS pour la réalisation de la capture. C’est dans ce fichier javascript qu’on place les instructions d’impression.

L’exemple qui suit reste très simple :

 

Pour tester, j’exécute l’instruction suivante sur le serveur :

 Le résultat est conforme aux attentes :

# Intégration de PhantomJS

Au sein d’une application web, PhantomJS n’est qu’une composante du système global. Pour avoir une vision complète du système, on procède de la manière suivante pour chaque ressource « imprimable ».

Pour imprimer une ressource, l’utilisateur clique sur le bouton « Imprimer ».

Une fenêtre « HTML » (une Ext Window, un Closure Dialog, etc.) d’aperçu avant impression est ouverte, qui embarque une iframe référençant la ressource à imprimer. Dans cette vue, on présente la ressource de manière à ce qu’elle soit imprimée correctement : on masque les bandeaux de navigation, etc.

Dans la fenêtre d’aperçu avant impression, deux boutons permettent de lancer une impression via le navigateur : « Imprimer » ou de lancer une impression via le serveur : « Télécharger ».

Dans le cas d’une impression via le navigateur, seul le contenu de l’iFrame est envoyé à l’imprimante système : le cadre et les boutons ne seront pas visibles. Il s’agit d’une impression classique qui donne dans le navigateur Google Chrome :

Dans le cas d’une impression via le serveur, on utilise une URL qui permet de contacter le serveur d’impressions et qui comporte un paramètre qui décrit la ressource à imprimer. Au plus simple, la valeur du paramètre est une URL. Par exemple, l’URL de contact du serveur d’impression sera :

http://www.testphantomjs.atolcd.com/servimp?url=http://www.atolcd.com

Le serveur d’impressions réceptionne l’URL à imprimer, lance l’impression en utilisant PhantomJS et renvoie le flux du fichier PDF. Un extrait en PHP :

// Récupération de l’URL à utiliser
$url = $request->getParameter(‘url’);
$cible = ‘/tmp/phantomjs-test.pdf’;

// Génération du fichier sur disque
$phantomOutput = shell_exec(‘DISPLAY=:0 /usr/local/phantomjs/bin/phantomjs /tmp/testsimple.js ‘.$url.’ ‘.$cible);

// Renvoi du contenu du fichier
$this->getResponse()->setContentType(‘application/pdf’);
$this->getResponse()->setHttpHeader(‘Content-Disposition’, ‘attachment; filename= »‘.basename($cible).' »‘);
$this->getResponse()->setContent(readfile($cible));

Le navigateur récupère la réponse sous la forme d’un flux PDF et propose l’enregistrement du fichier sur disque. Le résultat est le suivant :

Pour aller plus loin, on pensera à sécuriser le serveur d’impressions car il pourrait être utilisé pour imprimer n’importe quelle ressource adressable sur internet. Par exemple : vérifier la provenance, vérifier que l’URL cible est une URL de l’application, utiliser des clés plutôt que des URLs complètes, etc.

# Impression de ressources privées

A ce niveau de développement, l’impression fonctionnera pour toutes les ressources qui ne requièrent pas d’authentification.

Le serveur d’impression utilise le moteur WebKit si bien qu’il ouvre une nouvelle session lorsqu’il lance une impression. En conséquence, il doit être en mesure de transmettre les informations d’authentification lors des impressions.

Pour imprimer une ressource privée, plusieurs possibilités s’offrent à nous :

  • le serveur fait confiance aux requêtes qui proviennent de lui même (localhost). Ceci permet un accès à la ressource et si la présentation dépend de l’utilisateur, on pourra passer une information identifiant l’utilisateur (au plus simple, son identifiant).
  • le serveur propose une authentification basée sur le mode opératoire SSO. Par exemple, il procède en trois étapes :
    • Etape 1 : [Handler : serveur impression] réception de la requête d’un utilisateur authentifié :
      • Généréation d’un jeton à durée de vie courte dans une structure de données en mémoire ou en base
      • Requête du serveur sur lui-même en utilisant le jeton
    • Etape 2 : [Handler : système d’authenficiation] réception d’une requête avec jeton
      • Vérification du jeton (et invalidation)
      • Authentification de l’utilisateur
      • Redirection vers l’URL associée
    • Etape 3 : [Handler : page classique] réception d’une requête
      • L’utilisateur est authentifié, le serveur laisse passer la requête

Cette solution a l’avantage de proposer la souplesse d’une connexion sur le mode opératoire « SSO » : elle fonctionne également dans le cas où le serveur cible n’est pas le même que le serveur d’impression (sur lequel l’utilisateur est initialement authentifié).

Ce type de sécurisation a été retenu pour un projet qui nécessitait une impression de ressources privées.

Au niveau du passage des informations d’authentification, on remarquera à la lecture de l’API de PhantomJS, la présence de deux paramètres HTTP : « userName » et « password ». En conséquence, PhantomJS laisse le choix d’une transmission des informations d’authentification au niveau du protocole HTTP ou des paramètres de l’URL.

# Exemple de traitement avancé

Pour aller encore un peu plus loin avec PhantomJS et qualifier sa souplesse, j’ai décidé de le stresser un peu : imprimer une page dans laquelle sont utilisés OpenLayers et l’API minimale de l’IGN pour afficher les fonds de plans.

On se retrouve avec la problématique initiale : quand réaliser la capture pour avoir un résultat sûr ?

En effet, l’arrivée des fonds de l’IGN n’est pas immédiate dans la mesure où l’IGN effectue une vérification par le nom de domaine avant de laisser l’accès aux fonds et où les images peuvent représenter un volume important de données à télécharger. D’autant plus que le temps nécessaire dépend de facteurs externes : la qualité de la connexion, le chargement des serveur de cartes, etc.

Pour traiter ce cas de figure, j’ai utilisé un compteur de requêtes vers l’IGN. Quand une requête concerne une image de l’IGN, j’incrémente le compteur ; lorsque la réponse arrive, je le décrémente. La capture est réalisée lorsque qu’il n’y a plus aucune requête en attente.

Un extrait simplifié du script PhantomJS :

// A l’émission d’une requête

page.onResourceRequested = function (request) {

if (request.url.search(‘wxs.ign.fr’)>=0) {

++nbPendingReq;

}

};

// A la réception de la réponse

page.onResourceReceived = function (response) {

if (response.url.search(‘wxs.ign.fr’)>=0 && response.stage==’end’) {

–nbPendingReq;

if (nbPendingReq<1) {

page.render(target);

phantom.exit();

}

}

};

Le résultat est concluant puisqu’il s’agit d’un exemple en production (voir la capture plus haut dans le billet).

# Bilan

Pour conclure, PhantomJS est la librairie que j’attendais pour ce type de traitement. Le rendu est fidèle dans la mesure où le moteur WebKit est utilisé et les performances sont acceptables.

Les impressions multi-pages fonctionnent, bien qu’il arrive qu’une ligne de texte soit coupée en deux à l’horizontale pour se retrouver sur deux pages.

La force de la librairie réside dans la souplesse induite par son mode de fonctionnement qui permet d’adapter l’impression au cas de figure qui se présente. De plus, le format de sortie peut être imposé ce qui est un autre point d’amélioration par rapport à CutyCapt.