Répertorier et classer vos PDF avec Zend_PDF

Cet article est un vieil article que j’avais mis de côté suite à l’arrêt du Zend Framework version 1, et au fait que le composant Zend_PDF (utilisé dans cet article) n’avait pas été reconduit dans ZF2 (en tout cas pas officiellement). Néanmoins, on trouve de nombreuses copies du composant Zend_PDF sur Packagist.org, dont une est curieusement associée à ZF2. Elle a été installée près de 390 000 fois donc on peut supposer que ce composant est encore utilisé par pas mal de monde :

Zend_PDF sur ZF2 avec Packagist.org

Comme son nom l’indique, le composant Zend_PDF est dédié à la production de documents PDF… mais pas seulement. Car il fournit un jeu de méthodes intéressantes pour modifier des documents PDF existants, et en extraire les métadonnées. Or ces métadonnées (titres, auteurs, mots clés, etc…) sont très intéressantes dès lors que vous souhaitez répertorier et classer la masse impressionnante de documents PDF qui s’est entassée sur vos postes et serveurs bureautiques. L’extraction de ces métadonnées vous permettra par exemple de stocker la liste de vos documents dans une base de données, et de proposer aux utilisateurs des fonctions de recherches avancées et bien sûr de consultation de ces documents. Nous allons voir dans la suite de cet article comment extraire ces métadonnées, et les stocker dans un fichier XML. Nous verrons également comment, à partir de ce même fichier XML, remettre à jour les métadonnées de ces mêmes fichiers PDF. Tout cela grâce à Zend_PDF, et à quelques connaissances complémentaires en PHP.

Si vous ne savez pas de quoi je parle exactement à propos des métatags PDF, je vous invite à ouvrir un document PDF quelconque avec Acrobat Reader, puis à aller dans le menu « Fichier », option « Propriétés ». Vous verrez apparaître un formulaire non modifiable contenant les champs « titre », « auteur », « sujet » et « mots clés ». Ce sont les fameux métatags (ou métadonnées) du format PDF.

Si l’on regarde la documentation de Zend_PDF, on s’aperçoit qu’il est possible d’extraire d’autres métatags tels que :

– le « créateur » du PDF (champ généralement alimenté avec le nom de l’éditeur détenteur des droits sur le document),
– le « producteur » du PDF (champ généralement alimenté avec le nom du logiciel ayant servi à produire le PDF).

Certains logiciels de lecture de PDF s’appuient sur les métadonnées des documents PDF pour proposer différents types de classement. Si le contenu de ces métadonnées n’est pas pertinent, la galère commence… Et on s’aperçoit dans la pratique que beaucoup de documents PDF ont des métadonnées mal renseignées, voire pas du tout renseignées. C’est le cas de beaucoup de documents téléchargeables librement sur l’internet, mais c’est le cas aussi pour beaucoup de livres PDF que j’ai acquis tout à fait légalement auprès d’éditeurs, qu’ils soient français, américains ou autre… Je ne peux pas leur jeter la pierre, car j’avoue que moi même, je ne me souciais pas le moins du monde de ces métatags lorsque je générais jusqu’ici mes propres PDF.

Pour remédier à ce problème, nous allons utiliser Zend_PDF pour extraire, mais aussi pour mettre à jour ces métadonnées. Nous verrons cependant qu’il y a des cas où la mise à jour de documents PDF est impossible, mais j’y reviendrai tout à l’heure.

Premièrement, je souhaite parcourir un répertoire et tous ses sous-répertoires, pour trouver et répertorier tous les PDF qui s’y trouvent. Pour cela, pas besoin de Zend_PDF, on va simplement utiliser une boucle de balayage récursif des répertoires avec du code PHP pur jus :

// chemin d'accès servant de point de départ pour la recherche des PDF (à adapter selon vos besoins)
$path = 'C:/Users/gjarrige/Documents/meslivresPDF/';

// utilisation des méthodes de balayage récursif de PHP pour balayer tous les sous-répertoires se trouvant dans $path
$files = new RecursiveIteratorIterator(
     new RecursiveDirectoryIterator($path),
     RecursiveIteratorIterator::CHILD_FIRST
);

// parcours du contenu de $files et identification des PDF au moyen de leur extension
foreach ($files as $filename => $fileinfo) {
    if ($fileinfo->isFile() && $fileinfo->getExtension() == 'pdf') {
        $pdfPath = $fileinfo->getPathname();
        // on en profite pour récupérer quelques infos qui nous serviront ultérieurement 
        $filename = $fileinfo->getFilename();
        $size = $fileinfo->getSize();
        // la suite des festivités dans un instant après quelques explications...
     }
}

Ok, ça c’était pour la mise en jambe.
Maintenant que vous savez « balayer » les répertoires et identifier les PDF qui s’y trouvent, voyons comment extraire les fameux métatags PDF. C’est là que Zend_PDF entre en scène. Pour pouvoir l’utiliser à l’intérieur de notre boucle, sur chacun des PDF à analyser, il nous faut ajouter un peu de code préparatoire, juste avant la boucle, code que voici :

// chemin d'accès vers le Zend Framework, à adapter selon l'emplacement dans lequel vous l'avez installé
define('PATH_ZEND_FMW', 'C:\Program Files (x86)\Zend\Apache2\htdocs\library\ZendFramework\ZF-1.12.0');

// chargement en mémoire du composant Zend_PDF 
require_once PATH_ZEND_FMW . '/library/Zend/Pdf.php';

// instanciation d'un objet Zend_PDF
$pdf = new Zend_Pdf ();

Pour analyser le contenu d’un PDF existant avec Zend_PDF, nous utilisons tout simplement la méthode statique « load() », comme dans l’exemple ci-dessous.
Dans cet exemple, j’ai choisi d’initialiser un tableau que j’ai appelé $metatags, et que je remplis avec les métatags extraits via cette fameuse méthode « load() ». Je me suis rendu compte à l’usage que certains PDF ne respectaient pas strictement la norme PDF, et que certaines propriétés pouvaient faire défaut. Pour être tranquille, je teste donc l’existence de chaque propriété avant de la stocker dans mon tableau $metatags. Ce qui nous donne le code suivant :

$pdf = Zend_Pdf::load($pdfPath);
$metatags = array(
    'filename' => $filename,
    'filepath' => $pdfPath,
    'pdf_title' => (isset($pdf->properties ['Title']))?
       $pdf->properties ['Title'] : 'not_found',
       'pdf_author' => (isset($pdf->properties ['Author'])) ? 
      $pdf->properties ['Author'] : 'not_found',
    'pdf_subject' => (isset($pdf->properties ['Subject'])) ? $pdf->properties ['Subject'] : 'not_found',
    'pdf_keywords' => (isset($pdf->properties ['Keywords'])) ? $pdf->properties ['Keywords'] : 'not_found',
    'pdf_creator' => (isset($pdf->properties ['Creator'])) ? $pdf->properties ['Creator'] : 'not_found',
    'pdf_producer' => (isset($pdf->properties ['Producer'])) ? $pdf->properties ['Producer'] : 'not_found'
);

Pour un code plus robuste, j’utilise dans mon script final les directives PHP « Try Catch » (pour la gestion des erreurs), et je monitore 2 problèmes que j’ai rencontrés à l’usage, et qui sont les suivants :

– 1er problème : certains PDF ont une taille excessive, souvent du fait de la présence de nombreuses copies d’écran. Je me suis rendu compte que Zend_PDF ne savait pas travailler avec des PDF de plus 300 Mo, j’ai donc été obligé d’ajouter un test sur la taille des documents, de manière à retirer du circuit les documents trop volumineux. Ce cas est géré via un test sur la variable $size que j’avais prévu dans le code précédent.

– 2ème problème : certains PDF sont protégés par mot de passe, et utilisent un niveau de sécurité qui interdit toute possibilité d’en extraire les métatags. C’est le cas notamment avec certains livres que j’ai achetés à certains éditeurs que je ne citerai pas… Ce cas déclenche une erreur qui est monitorée dans le bloc Catch.

Le code final est donc le suivant :

if ($size > 300000000) {
   $metatags = array(
     'filename' => $filename,
     'filepath' => $pdfPath,
     'pdf_title' => 'too big',
     'pdf_author' => 'too big',
     'pdf_subject' => 'too big',
     'pdf_keywords' => 'too big',
     'pdf_creator' => 'too big',
     'pdf_producer' => 'too big'
   );
} else {
   try {
      $pdf = Zend_Pdf::load($pdfPath);
      $metatags = array(
        'filename' => $filename,
        'filepath' => $pdfPath,
        'pdf_title' => (isset($pdf->properties ['Title'])) ? 
$pdf->properties ['Title'] : 'not_found',
        'pdf_author' => (isset($pdf->properties ['Author'])) ? $pdf->properties ['Author'] : 'not_found',
        'pdf_subject' => (isset($pdf->properties ['Subject'])) ? $pdf->properties ['Subject'] : 'not_found',
        'pdf_keywords' => (isset($pdf->properties ['Keywords'])) ? $pdf->properties ['Keywords'] : 'not_found',
        'pdf_creator' => (isset($pdf->properties ['Creator'])) ? $pdf->properties ['Creator'] : 'not_found',
        'pdf_producer' => (isset($pdf->properties ['Producer'])) ? $pdf->properties ['Producer'] : 'not_found'
     );
     unset($pdf);
 } catch (Exception $e) {
     error_log($e->getCode() . '  ' . $e->getMessage());
     $metatags = array(
        'filename' => $filename,
        'filepath' => $pdfPath,
        'pdf_title' => 'not_readable',
        'pdf_author' => 'not_readable',
        'pdf_subject' => 'not_readable',
        'pdf_keywords' => 'not_readable',
        'pdf_creator' => 'not_readable',
        'pdf_producer' => 'not_readable'
     );
  }
}

Bon, c’est très bien tout ça, mais je rappelle que mon objectif de départ était de constituer un fichier XML contenant un catalogue exhaustif de mes documents PDF. Donc après avoir alimenté le tableau $metatags, je vais m’en servir pour générer un document XML. Attention, pour que ça fonctionne, il faut ajouter un peu de code avant la boucle itérant sur $files, un autre bout de code à l’intérieur de la boucle, et une dernière ligne de code après la boucle. Je commence par le code à ajouter avant la boucle sur $files :

$dom = new DomDocument('1.0', 'UTF-8');
$dom->formatOutput = true;

$root = $dom->createElement("books");
$dom->appendChild($root);

// identifiant unique pour chaque livre répertorié
$id = 0 ;

Voici maintenant le code à ajouter à l’intérieur de la boucle, après l’initialisation du tableau $metatags :

$id++ ;
$bn = $dom->createElement("book");
$bn->setAttribute('id', $id);

$element = $dom->createElement("id");
$element->appendChild($dom->createTextNode($id));
$bn->appendChild($element);

$element = $dom->createElement("filename");
$element->appendChild(
   $dom->createTextNode($metatags['filename']));
$bn->appendChild($element);

$element = $dom->createElement("filepath");
$element->appendChild(
   $dom->createTextNode($metatags['filepath']));
$bn->appendChild($element);

$element = $dom->createElement("title");
$element->appendChild(
   $dom->createTextNode($metatags['pdf_title']));
$bn->appendChild($element);

$element = $dom->createElement("author");
$element->appendChild(
   $dom->createTextNode($metatags['pdf_author']));
$bn->appendChild($element);

$element = $dom->createElement("subject");
$element->appendChild(
   $dom->createTextNode($metatags['pdf_subject']));
$bn->appendChild($element);

$element = $dom->createElement("keywords");
$element->appendChild(
   $dom->createTextNode($metatags['pdf_keywords']));
$bn->appendChild($element);

$element = $dom->createElement("creator");
$element->appendChild(
   $dom->createTextNode($metatags['pdf_creator']));
$bn->appendChild($element);

$element = $dom->createElement("producer");
$element->appendChild(
   $dom->createTextNode($metatags['pdf_producer']));
$bn->appendChild($element);

$element = $dom->createElement("size");
$element->appendChild($dom->createTextNode($size));
$bn->appendChild($element);

// booléen que j'utiliserai ultérieurement pour
// identifier les PDF dont je souhaite modifier les
// métatags
$element = $dom->createElement("changed");
$element->appendChild($dom->createTextNode('no'));
$bn->appendChild($element);

$root->appendChild($bn);

Voici enfin la dernière ligne à ajouter après la boucle, si vous l’oubliez, vous n’obtiendrez jamais le document XML souhaité :

echo $dom->save('mybooks.xml');

Maintenant, un petit réglage : je me suis rendu compte à l’usage que, pour que Zend_PDF puisse fonctionner avec des documents de très grande taille, il fallait lui donner un petit coup de main, en modifiant une option de configuration de l’environnement de PHP. Cette modification ne vaut que pour le script en cours d’exécution, donc la fonction ini_set() est parfaite pour ça. La ligne ci-dessous doit être ajoutée au début de votre script PHP :

ini_set("memory_limit", "300M");

Voilà, si vous faites « tourner » le script PHP que je viens de vous présenter, vous obtiendrez un beau fichier XML contenant la liste de vos documents PDF.

Maintenant que vous avez votre fichier XML, vous pouvez mesurer l’ampleur des dégâts :
– y a-t-il beaucoup de très gros fichiers PDF dans vos répertoires ?
– y a-t-il beaucoup de fichiers PDF verrouillés dont l’extraction des métatags est impossible ?
– y a-t-il beaucoup de fichiers PDF dont les métatags sont inexistants ou fantaisistes ?

C’est sur cette dernière catégorie que vous pourrez agir, en corrigeant au cas par cas les informations stockées dans le fichier XML. Pensez dans ce cas à mettre le booléen « changed » à « yes », cela vous permettra d’identifier les PDF pour lesquels vous souhaitez modifier les métatags.

Il ne reste plus qu’à écrire le script PHP qui va faire le travail inverse du précédent, à savoir la mise à jour des métatags dans les fichiers PDF.
Le script PHP est très simple, je vous le livre brut de fonderie :

// chemin d'accès vers le Zend Framework, à adapter selon l'emplacement dans lequel vous l'avez installé
define('PATH_ZEND_FMW', 'C:\Program Files (x86)\Zend\Apache2\htdocs\library\ZendFramework\ZF-1.12.0');

// chargement en mémoire du composant Zend_PDF 
require_once PATH_ZEND_FMW . '/library/Zend/Pdf.php';

// instanciation d'un objet Zend_PDF
$pdf = new Zend_Pdf ();

// parsing du fichier XML
$books = simplexml_load_file('mybooks.xml');

foreach ($books->book as $item) {
    if ($item->changed == 'yes') {
        if ($item->title != 'not_readable') {
            try {
                $pdf = Zend_Pdf::load($item->filepath);
                $pdf->properties ['Title'] = $item->title;
                $pdf->properties ['Author'] = $item->author;
                $pdf->properties ['Subject'] = $item->subject;
                $pdf->properties ['Keywords'] = $item->keywords;
                $pdf->properties ['Creator'] = $item->creator;
                $pdf->properties ['Producer'] = $item->producer;
                $pdf->save($item->filepath, true);
                unset($pdf);
            } catch (Exception $e) {
                error_log($e->getCode() . '  ' . $e->getMessage() . '  ' . $item->filepath);
            }
        }
    }
}

En conclusion :

Même si j’ai été gêné par les sécurités trop restrictives placées sur certains documents PDF par les éditeurs, j’ai pu malgré tout faire un gros ménage dans ma base documentaire. Cela me permet maintenant de consulter la grande majorité de mes documents PDF sur ma tablette tactile, avec des possibilités de tris et de classements intéressantes. Comme je l’évoquais au début de cet article, vous pouvez utiliser ce principe pour vous constituer une base de données documentaire avec les différentes documentations éparpillées sur vos serveurs.

J’avoue m’être bien amusé, en poussant Zend_PDF dans ses retranchements, et même s’il a déclaré forfait sur certains documents trop volumineux, je suis très content du résultat.

Il faut souligner que Zend_PDF est capable de modifier bien autre chose que de simples métatags. Vous pouvez par exemple l’utiliser pour réordonner les pages d’un document PDF existant, ou encore pour fusionner plusieurs PDF en un seul document.

A noter que pour ce petit développement, j’ai utilisé la version 1.12 du Zend Framework. C’est à ce jour la dernière version de la branche 1 du ZF. La version 2 du ZF, dont une version stabilisée a été livrée tout récemment, ne contient pas encore de composant Zend_PDF. Mais comme le ZF 2 est encore en cours de développement, on peut s’attendre à ce que la situation évolue rapidement.

A très bientôt.
Grégory

Lien vers la documentation officielle du composant Zend_PDF.