Un atelier de réception des PDF est un petit programme avec une seule mission : regarder chaque fichier avant qu'aucun traitement en aval n'ait le droit d'y toucher. Pour tenir cette mission, il doit réunir une poignée de capacités en une seule passe. Il ouvre le fichier (sans lui faire confiance), lit ce que le fichier prétend de lui-même, cherche du contenu qui trompera un extracteur naïf ou portera une attaque, décide s'il existe le moindre texte extractible, puis dirige le document vers une file d'attente selon ce qu'il a trouvé. Sautez l'inspection et les échecs seront silencieux : un PDF chiffré par mot de passe propriétaire qui enveloppe un formulaire XFA traverse un extracteur de texte sous forme de chaînes vides, se fait indexer comme document vierge, et personne ne s'en aperçoit avant que quelqu'un en aval aille chercher un contenu qui n'a jamais été lu. PDFium Component est un visualiseur et une bibliothèque d'inspection VCL/LCL livrés en code source pour Delphi, C++Builder et Lazarus, et il expose les appels d'introspection dont cet atelier a besoin. Les sections ci-dessous parcourent quel appel répond à quelle question, et les deux endroits où l'appel évident vous donne une réponse fausse avec assurance
Cinq questions à trancher avant de router un fichier
Retirez la grille et la bande de vignettes, et le tri de réception se réduit à cinq questions :
- Le fichier peut-il seulement être ouvert, et sous quel mot de passe ?
- Que prétend-il être : titre, auteur, date de création ?
- Porte-t-il du contenu actif ou risqué comme du JavaScript, un formulaire XFA ou des fichiers incorporés ?
- Y a-t-il du texte extractible, ou est-ce un scan en route pour l'OCR ?
- Compte tenu de tout cela, quelle file le reçoit : traitement direct, revue manuelle ou quarantaine ?
Chaque question se projette sur un ou deux appels de PDFium Component. Deux de ces projections ont des arêtes vives qui expliquent la plupart des fichiers mal routés que j'ai eu à déboguer en production. Les métadonnées du document vivent à deux endroits différents qui peuvent se contredire, et le chiffrement n'empêche pas nécessairement un document de s'ouvrir
Ouvrir à bas coût : remplissage de formulaire coupé, zéro page rendue
Le tri doit être l'ouverture la moins coûteuse possible. Poser FormFill := False avant Active := True demande au composant de sauter entièrement l'environnement de remplissage de formulaires. Cela raccourcit le temps de chargement et, tout aussi important pour des fichiers d'origine inconnue, cela empêche tout JavaScript au niveau du document de s'initialiser. Aucune des propriétés d'inspection utilisées ci-dessous n'exige de rendre une page : une passe de tri n'a donc jamais à produire le moindre bitmap
procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := IncomingPath;
Pdf.FormFill := False; // pas d'environnement de formulaire, pas d'init JavaScript
Pdf.Active := True; // l'échec est silencieux : Active reste simplement à False
if not Pdf.Active then
begin
Rec.OpenFailed := True; // fichier endommagé ou verrou par mot de passe utilisateur
Exit; // le bloc finally s'exécute quand même
end;
Rec.PageCount := Pdf.PageCount;
CollectIdentity(Pdf, IncomingPath, Rec);
CollectRiskSignals(Pdf, Rec);
finally
Pdf.Active := False;
Pdf.Free; // ne jamais laisser fuir l'instance sur un fichier malformé
end;
end;
Le contrôle qui suit l'affectation n'est pas facultatif, et c'est un contrôle plutôt qu'un gestionnaire d'exception pour une raison. Quand le moteur ne peut pas charger le fichier, le composant avale l'EPdfError interne et laisse Active à False au lieu de la propager. Du code qui attend une exception lira joyeusement PageCount depuis un document qui ne s'est jamais ouvert. Si le flux de rejet a besoin du texte d'erreur réel du moteur, lisez le fichier dans un tableau d'octets et appelez la surcharge LoadDocument qui prend un TBytes ; ce chemin-là lève bien une EPdfError avec le message, y compris pour le cas du mot de passe. Le try..finally garde sa place. Les services de réception tournent sans surveillance pendant des semaines, et aucune exception ultérieure ne doit laisser fuir l'instance TPdf ni tenir un verrou sur lequel la passe de reprise trébucherait
Le débit devient rarement le goulot. Avec le remplissage de formulaires désactivé et aucun rendu, une ouverture de tri est dominée par les entrées-sorties, et un seul worker inspecte confortablement plusieurs fichiers par seconde depuis un disque local. Si le volume de réception finit par dépasser un worker, partitionnez le travail par fichier plutôt que par contrôle. Les cinq questions partagent une seule ouverture, et les répartir entre plusieurs processus multiplierait l'étape la plus chère au lieu de l'amortir
Les métadonnées vivent à deux endroits, et ils se contredisent
ISO 32000-1 définit deux domiciles pour les métadonnées de document : le dictionnaire d'information du document (clause 14.3.3) et un paquet XMP attaché au catalogue (clause 14.3.2). Les propriétés Title, Author, Subject et CreationDate lisent le dictionnaire Info, avec MetaText[] pour toute autre clé et DecodeDate pour analyser la chaîne de date D:YYYYMMDD.... Le piège est que les producteurs modernes écrivent de plus en plus du XMP uniquement, une direction qu'ISO 32000-2 officialise en dépréciant la plupart des clés du dictionnaire Info dans PDF 2.0. Le symptôme dans un outil de réception est concret. Votre atelier affiche un titre vide alors qu'Adobe Acrobat en montre un, parce qu'Acrobat s'est rabattu sur dc:title dans le paquet XMP, que les propriétés du dictionnaire Info ne touchent jamais
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
var Rec: TIntakeRecord);
begin
Rec.Title := Pdf.Title; // valeur du dictionnaire Info
Rec.Author := Pdf.Author;
Rec.CreatedAt := Pdf.CreationDate; // chaîne de date PDF brute ("D:2026...")
// Un titre Info vide ne veut pas dire que le document est sans titre. Le
// composant n'expose pas le paquet XMP : sondez donc les octets bruts du
// fichier à la recherche de l'élément dc:title avant de croire ce vide.
if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
Include(Rec.Flags, ifTitleInXmpOnly);
end;
Même la sonde grossière par sous-chaîne ci-dessus gagne sa place : « métadonnées présentes, mais pas là où les outils historiques regardent » est un fait pertinent pour le routage dans toute chaîne d'archivage qui indexe sur le titre ou l'auteur. Si votre index en aval ne lit que le dictionnaire Info, les fichiers marqués ainsi deviendront silencieusement introuvables
Des fichiers chiffrés qui s'ouvrent quand même
Un document chiffré n'échoue pas nécessairement à s'ouvrir. Le gestionnaire de sécurité standard (ISO 32000-1 clause 7.6.3) distingue un mot de passe utilisateur, exigé pour ouvrir le document, d'un mot de passe propriétaire qui ne fait que verrouiller des permissions comme l'impression et la copie. Une large part des documents professionnels « protégés » sont chiffrés avec un mot de passe propriétaire et un mot de passe utilisateur vide. Ils s'ouvrent sans invite, se déchiffrent entièrement, et comptent sur la bonne volonté des visualiseurs pour honorer les indicateurs de permission. C'est de la politique, pas de la protection, et vos états de réception doivent refléter la différence
Détecter le chiffrement après une ouverture réussie prend un appel moteur plus un repli. FPDF_GetSecurityHandlerRevision(Pdf.Document) renvoie -1 pour les fichiers non protégés et la révision du gestionnaire sinon, et Pdf.Permissions qui renvoie autre chose que le masque tous bits à un $FFFFFFFF est le signal corroborant. Pour les fichiers réellement verrouillés par mot de passe utilisateur, affectez Password avant de poser Active := True ; si l'ouverture échoue encore, dirigez le fichier vers un état bloqué qui demande les identifiants à l'expéditeur par un canal sûr plutôt que de réessayer à l'aveugle. Et résistez à la tentation de traiter « chiffré » comme une mise en quarantaine automatique. Dans la plupart des secteurs riches en documents, les fichiers chiffrés mais ouvrables sont le cas normal, pas le cas suspect
Contenu actif : JavaScript, XFA et fichiers incorporés
Trois constats doivent toujours atteindre la décision de routage. D'abord le JavaScript : l'événement OnUnsupportedFeature signale les caractéristiques structurelles comme le XFA ou le contenu 3D à mesure que le moteur les rencontre, mais il ne détecte pas le JavaScript. Contrôlez plutôt JavaScriptActionCount et traitez un résultat non nul comme du contenu actif. Ensuite le XFA : quand FormType renvoie ftXfaFull, les pages visibles ne sont souvent guère plus qu'un rendu du modèle XFA, et l'extraction de texte classique verra du texte type plutôt que les valeurs saisies. Enfin les pièces jointes : un PDF est un format conteneur, et AttachmentCount vous dit si celui-ci transporte des passagers
procedure CollectRiskSignals(Pdf: TPdf; var Rec: TIntakeRecord);
var
i, PageNo: Integer;
Ext: string;
begin
Rec.IsEncrypted := Assigned(FPDF_GetSecurityHandlerRevision) and
(FPDF_GetSecurityHandlerRevision(Pdf.Document) <> -1);
Rec.HasForms := Pdf.FormType <> ftNone;
Rec.IsXfa := Pdf.FormType = ftXfaFull;
Rec.HasJavaScript := Pdf.JavaScriptActionCount > 0;
// AnnotationCount est une propriété par page ; parcourez les pages pour
// la totaliser. Charger un objet page ne rend rien, cela reste bon marché.
Rec.Annotations := 0;
for PageNo := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := PageNo;
Inc(Rec.Annotations, Pdf.AnnotationCount);
end;
Rec.Attachments := Pdf.AttachmentCount;
for i := 0 to Rec.Attachments - 1 do
begin
Ext := LowerCase(ExtractFileExt(string(Pdf.AttachmentName[i])));
if (Ext = '.exe') or (Ext = '.js') or (Ext = '.vbs') or (Ext = '.dll') then
Include(Rec.Flags, ifDangerousAttachment);
end;
end;
Deux détails de cette boucle méritent l'attention. Le nom de la pièce jointe vient de l'intérieur du document : ne le réutilisez jamais comme chemin de sortie sans l'assainir d'abord ; un nom incorporé tel que ..\..\start.exe est une traversée de répertoires qui attend un appel de sauvegarde négligent. Et une liste noire d'extensions est un fil-piège, pas une garantie. Son rôle est de forcer une décision humaine, pas de certifier le fichier propre
Transformer les signaux en états de routage
Un modèle d'états praticable demande moins d'états que la plupart des équipes ne le pensent : prêt (aucun bloqueur, texte présent), revue (ouverture réussie mais quelque chose demande un regard, comme un formulaire XFA, du JavaScript, une couche de texte vide ou un titre présent seulement en XMP), bloqué (mot de passe utilisateur requis) et endommagé (ouverture échouée). Consignez les preuves à côté de l'état. L'empreinte du fichier, le nombre de pages, les indicateurs exacts et le message d'erreur du moteur pour les fichiers endommagés comptent tous, car la personne qui contestera une décision de routage le fera des semaines plus tard, contre un fichier qui aura peut-être été remplacé ou modifié depuis
Quand un opérateur doit bel et bien regarder un fichier en quarantaine, ne le confiez pas au visualiseur par défaut du système. Rendez-le dans un volet durci, scripts et gestion des liens désactivés, l'approche décrite dans la construction d'une surface de prévisualisation PDF sûre en Delphi. Et si votre réception alimente une archive soumise à des exigences de conformité, la passe de tri est l'endroit naturel pour programmer un contrôle plus poussé ; la validation préflight par lot contre les profils PDF/A et PDF/UA reprend exactement là où cette inspection s'arrête
La page produit du composant couvre les licences, l'API complète d'inspection et les démonstrations livrées, dont un inspecteur de documents de style réception : PDFium Component