Article technique

Automatiser le contrôle preflight PDF en Delphi avec HotPDF

Le fichier s'ouvre sans problème sur votre machine. Acrobat l'affiche, l'aperçu avant impression paraît correct, toutes les pages sont là. Puis il part chez l'imprimeur, ou dans le système d'archivage qui ingère votre lot mensuel, et il revient rejeté : images RVB dans un travail CMJN, pas de clé /Trapped, un output intent qui ne correspond pas à la presse. Rien dans le document ne clochait pour qui l'ouvrait. Il était fautif face à un profil, et ce profil a été vérifié quelque part où vous n'étiez pas. Le preflight est le nom que la prépresse donne à ce contrôle, et la vraie question est de savoir où il doit se placer quand les PDF sortent de votre propre code Delphi plutôt que du poste d'un graphiste

HotPDF ne vous propose pas de fonction de preflight à appeler. Le composant embarque une fenêtre de rapport preflight dans sa démo graphique, mais aucune API ne se trouve derrière, rien qu'un service ou un script de build puisse invoquer, et prétendre le contraire vous enverrait chercher une méthode qui n'existe pas. Cela ressemble à un manque jusqu'à ce que vous remarquiez que, pour des fichiers que vous générez vous-même, appeler un validateur sur votre propre sortie est de toute façon la mauvaise approche. Vous contrôlez déjà chaque propriété qu'un validateur irait inspecter. Le découpage utile consiste à rendre le générateur incapable d'émettre un mauvais fichier, puis à le prouver avec un outil que vous n'avez pas écrit

Schéma d'un pipeline preflight Delphi où les réglages de conformité HotPDF empêchent les PDF non conformes dès la génération pendant que veraPDF et Acrobat Preflight prouvent le résultat de l'extérieur
La prévention inscrit les règles PDF/A et PDF/X dans la génération HotPDF elle-même pendant que des validateurs externes rendent le verdict que le générateur ne peut pas s'attribuer

Pourquoi vous contrôlez votre propre sortie autrement

Le preflight traditionnel suppose le fichier d'un inconnu. Un graphiste quelconque, une autre application, une chaîne de retouches indéterminée l'ont produit, et vous l'inspectez parce que vous n'avez aucune idée de son contenu. Un document produit par votre code n'est pas un inconnu. Incorporation des polices, espace colorimétrique, output intent, bloc de métadonnées : votre programme a décidé de tout cela quelques millisecondes avant que le fichier n'atteigne le disque. L'inspecter ensuite pour découvrir des choix que vous venez de poser est du travail pour rien. Le geste le moins coûteux est de contraindre ces choix pour qu'un fichier non conforme n'existe jamais et n'ait pas à être intercepté

Il existe aussi une raison de crédibilité à garder la vérification à l'extérieur. Une bibliothèque qui bénit sa propre sortie corrige sa propre copie. Quand le système d'archivage d'un client ou le RIP d'un imprimeur rejette votre fichier, « notre composant dit que tout va bien » ne pèse rien. Un verdict de veraPDF ou d'Acrobat, lui, pèse, parce que la partie adverse utilise les mêmes outils

Faire de la conformité un réglage, pas une liste de contrôle

La couche de prévention n'est que de la configuration. Posez PDFACompliance ou PDFXCompliance avant BeginDoc et HotPDF maintient les règles correspondantes pendant toute la passe de génération : il incorpore les polices, surveille l'usage de DeviceRGB et DeviceCMYK par rapport à l'output intent que vous avez déclaré, et refuse les fonctionnalités que le profil interdit. Les contradictions remontent à EndDoc, où les garde-fous de conformité lèvent une erreur au lieu de livrer discrètement quelque chose qui échouera plus loin. Une fois le fichier enregistré, les mêmes propriétés relisent ce qui a réellement été appliqué, et c'est le fait dont le journal de votre pipeline a le plus besoin :

// Après EndDoc : consigner les profils appliqués avec les métadonnées du traitement
if Pdf.PDFACompliance <> '' then
  Log('Generated as PDF/A level ' + Pdf.PDFACompliance);
if Pdf.PDFXCompliance <> '' then
  Log('Generated as PDF/X profile ' + Pdf.PDFXCompliance);

Mettez ces indicateurs sur la même ligne de journal que l'empreinte des données d'entrée et la version de HotPDF. Le jour où un validateur et votre générateur seront en désaccord sur un fichier, cette ligne vous dira quel modèle l'a produit et quelle build de la bibliothèque était chargée, et la discussion qui aurait autrement dévoré un après-midi devient un simple grep. Les output intents, profils ICC et balisage qui se tiennent derrière ces indicateurs sont détaillés dans le guide de la sortie PDF/A, PDF/X et PDF/UA avec HotPDF

Un premier filtre économique pour les fichiers que vous n'avez pas générés

Tous les pipelines ne sont pas purement génératifs. Des clients déposent des PDF, des scanners en déversent dans un dossier, des partenaires en joignent à leurs courriels. Faire passer chacun d'eux dans un validateur structurel complet gaspille du temps de file d'attente sur des fichiers qui ne s'ouvriront même pas. La Direct File API de HotPDF lit assez de la structure d'un fichier pour répondre à « est-ce seulement un PDF exploitable » sans charger tout l'arbre des objets, ce qui en fait un bon endroit pour échouer vite :

function TriagePdf(Pdf: THotPDF; const FileName: string): Boolean;
var
  Handle, Pages: Integer;
begin
  Result := False;
  Handle := Pdf.DAOpenFileReadOnly(FileName, '');
  if Handle <= 0 then
    Exit;  // structurellement illisible : mise en quarantaine, pas de validation
  try
    Pages := Pdf.DAGetPageCount(Handle);
    Result := Pages > 0;
  finally
    Pdf.DACloseFile(Handle);
  end;
end;

Deux faits au sujet de cette API décident de la façon de l'envelopper. Le raccourci en mémoire plate ne vaut que pour une entrée non chiffrée ; passez un mot de passe à DAOpenFileReadOnly et il retombe silencieusement sur une analyse complète, de sorte qu'un fichier que vous savez chiffré devrait passer par DecryptFile vers une copie de travail en clair avant le tri. Et DAGetPageCount ne signifie rien sur un handle qui ne s'est pas ouvert proprement, donc le contrôle du handle reste strict et un résultat non positif est un rejet, pas une nouvelle tentative. D'autres modèles de ce genre figurent dans l'article sur la Direct File API pour les flux de gros PDF

veraPDF, exécuté dans le cadre du build

Pour tout ce que vous annoncez comme PDF/A ou PDF/UA, veraPDF est le validateur à câbler. Il fonctionne sans interface, accepte un lot, émet du XML ou du JSON, et nomme chaque échec par sa clause ISO, si bien qu'une règle en échec face à la clause 6.2.2 de la norme ISO 19005-1 renvoie directement à un réglage du générateur au lieu de vous laisser deviner. Le piloter depuis Delphi relève du simple contrôle de processus :

function RunVeraPdf(const PdfFile, ReportFile: string): Cardinal;
var
  Cmd: string;
  SI: TStartupInfo;
  PI: TProcessInformation;
begin
  Cmd := Format('cmd /c verapdf.bat --format xml "%s" > "%s"',
    [PdfFile, ReportFile]);
  FillChar(SI, SizeOf(SI), 0);
  SI.cb := SizeOf(SI);
  if not CreateProcess(nil, PChar(Cmd), nil, nil, False,
      CREATE_NO_WINDOW, nil, nil, SI, PI) then
    RaiseLastOSError;
  try
    WaitForSingleObject(PI.hProcess, 120000);  // borner l'attente pour chaque fichier
    GetExitCodeProcess(PI.hProcess, Result);
  finally
    CloseHandle(PI.hThread);
    CloseHandle(PI.hProcess);
  end;
end;

Ce délai d'attente justifie sa présence. Un fichier malformé peut entraîner n'importe quel analyseur dans un coin dont il ne ressort jamais, et une attente sans limite au sein d'un worker de file d'attente entraîne le reste de la file avec lui. Bornez l'attente, donnez au dépassement son propre code d'échec, et mettez le fichier de côté pour un humain. Quand vous lisez le résultat, analysez le XML à la recherche des identifiants de règles, pas du texte lisible. Les identifiants de règles survivent aux montées de version du validateur ; la formulation des messages, non, et un code stable est quelque chose qu'un ingénieur du support peut rechercher dans les anciens tickets

La manière dont vous exécutez le lot compte autant que la réussite de chaque fichier. Un processus par fichier, et non un par lot, pour qu'une entrée toxique ne vous coûte que le délai de ce fichier et rien de plus. Plafonnez le nombre de processus du validateur au nombre de cœurs, car la construction du rapport XML est limitée par le processeur et la surcharge ne fait que provoquer du thrashing. Et posez un plafond de taille à l'admission, parce qu'un livre numérisé de deux gigaoctets accaparera la file quelle que soit la patience de l'analyseur. Rien de tout cela n'est du preflight au sens strict. C'est la différence entre un filtre qui survit au volume de fin de mois et un filtre que l'on désactive la première nuit où il bloque le pipeline à 2 h du matin

Schéma d'un filtre de lot Delphi qui lance un processus veraPDF par PDF avec un délai borné, extrait les identifiants de règles XML plutôt que les messages, et archive chaque rapport à côté de son fichier
Un garde-fou à l'admission plafonne la charge de la file pendant qu'un worker veraPDF neuf par fichier empêche une entrée empoisonnée de bloquer le build

PDF/X est le point où l'approche montre ses limites. veraPDF ne le valide pas, donc le contrôle qui fonctionne reste le Preflight d'Acrobat avec le profil ISO 15930 nommé par votre imprimeur. Acrobat réclame un humain, ce qui implique un échantillonnage plutôt qu'une couverture totale : le premier fichier issu d'un nouveau modèle, plus un petit tirage aléatoire dans chaque lot, pendant que le filtre automatisé traite tout ce qui peut l'être sans intervention. Un contrôle par échantillon qui tourne vraiment vaut mieux qu'une automatisation complète qui reste éternellement à moitié faite

Un rapport que vous voudrez encore dans un an

Un filtre preflight rapporte deux fois. Une première fois lorsqu'il arrête un mauvais fichier à la porte, et une seconde bien plus tard quand quelqu'un demande pourquoi tel fichier est passé. Ce second moment est celui qui devrait dicter le format, car c'est celui où un rapport trop maigre vous laisse démuni. Pour chaque fichier contrôlé, conservez l'empreinte de l'entrée, les indicateurs de conformité du générateur et la version de la bibliothèque tirés de la ligne de journal ci-dessus, le nom et la version du validateur, le profil auquel il a été confronté, le succès ou l'échec, et les identifiants des règles en échec avec les numéros de page partout où le validateur les fournit. Rangez ce rapport à côté du fichier qu'il décrit. Placez-le dans un système distinct et ce système sera mis hors service avant l'archive qu'il documente

Les exceptions doivent elles aussi être consignées. Quand un client insiste pour livrer un fichier que le filtre n'aime pas, la réponse n'est pas d'assouplir la règle pour tout le monde. Notez qui a approuvé ce fichier, sur quels motifs, et jusqu'à quelle date, puis attachez cette dérogation à son rapport. Une dérogation avec un nom et une échéance est une décision assumée par quelqu'un. Un contrôle mis en commentaire « temporairement » est un incident qui attend sa date

Une dernière habitude se rembourse toute seule : quand un fichier échoue, copiez-le dans un dossier de régression nommé avant que quiconque y touche. Presque tous les problèmes de preflight qui méritent un débogage remontent à une entrée précise, et les équipes qui conservent ces entrées corrigent la récidive en une heure au lieu d'attendre qu'elle ressurgisse en production. Les propriétés de conformité et la Direct File API présentées ici font partie du composant Delphi HotPDF pour Delphi et C++Builder, dont la documentation couvre chaque appel en détail