Article technique

Intégrer les polices manquantes dans les PDF existants pour le PDF/A en Delphi

losLab PDF Library peut intégrer les programmes de polices manquants d'un PDF déjà chargé en un seul appel : EmbedMissingFonts parcourt chaque dictionnaire de polices du document, localise la police système installée correspondante par son nom BaseFont et réécrit le programme de polices dans le fichier. Pour les équipes qui réparent des documents tiers dont la validation PDF/A échoue en raison de l'intégration des polices, c'est le correctif qui fait disparaître l'erreur de contrôle en amont 00030

Le scénario n'est que trop fréquent. Un pipeline d'intégration d'archives reçoit des PDF de fournisseurs, de clients ou d'un bureau de numérisation ; les documents s'affichent correctement sur tous les postes de travail du bâtiment ; puis le validateur PDF/A rejette l'ensemble du lot avec la même plainte répétée une fois par fichier : au moins une police n'est pas intégrée. Personne en amont ne régénérera les fichiers, le pipeline doit donc les réparer. Cet article traite de cette méthode de réparation. Il accompagne l'article sur le contrôle en amont, qui traite de la détection des violations PDF/A et PDF/UA : cette partie vous indique quels documents sont corrompus, celle-ci corrige le problème le plus fréquent

Pourquoi le PDF/A exige-t-il que chaque police soit intégrée ?

ISO 19005-1 §6.3.4 exige que chaque police utilisée par un document conforme contienne son programme de polices à l'intérieur du fichier, car toute la promesse du PDF/A est la reproductibilité : le document doit s'afficher de manière identique sur une machine dans cinquante ans qui ne partage aucune police avec la machine qui l'a produit. Une police non intégrée est une instruction d'aller chercher Arial quelque part sur le système de visualisation, et la position de la norme est que « quelque part sur le système de visualisation » n'est pas une garantie d'archivage. Quels que soient les glyphes, les métriques et la couverture de la police de substitution, c'est ce que le lecteur obtient, et cela peut ne pas correspondre à ce que l'auteur a vu

Le coupable historique est la convention Standard 14. PDF 1.0 promettait que chaque visionneuse intégrait Helvetica, Times, Courier, Symbol et ZapfDingbats, de sorte que les générateurs ont appris à référencer ces polices par leur nom sans rien intégrer, et trente ans d'outils font toujours exactement la même chose. losLab PDF Library prend cette exigence suffisamment au sérieux pour que, en mode de création PDF/A, AddStandardFont soit délibérément inopérant : la bibliothèque ne fournit pas les programmes de polices Standard 14, ne peut pas intégrer ce qu'elle n'a pas et refuse d'écrire une référence non intégrée dans un document qui prétend être conforme. Elle renvoie 0 sans sélectionner de police, de sorte qu'un document PDF/A doit plutôt utiliser AddTrueTypeFont avec intégration, et toute requête Embed=0 est silencieusement convertie en Embed=1 lorsque le mode PDF/A est actif. C'est le côté écriture. Le problème le plus difficile est le côté lecture : un document que quelqu'un d'autre a déjà écrit, rempli de dictionnaires de polices que vous n'avez pas créés

Comment EmbedMissingFonts répare-t-il un document chargé ?

losLab PDF Library répare les polices sur place plutôt que de les reconstruire. Lorsqu'un générateur PDF écrit une police TrueType non intégrée, le dictionnaire FontDescriptor qu'il produit est déjà complet : FontName, FontBBox, Flags, Ascent, Descent, StemV, tous présents. La seule chose qui la sépare d'une police intégrée est l'absence d'une entrée, la référence du flux /FontFile2 contenant le programme de polices réel. Ainsi, EmbedMissingFonts ne touche pas au dictionnaire de polices, à l'encodage, au tableau des largeurs ou à tout flux de contenu qui référence la police par son nom de ressource. Il lit le programme de polices correspondant à partir du système, le compresse dans un nouvel objet flux et ajoute une seule référence /FontFile2 (ou /FontFile3 pour les polices CIDFontType0) au FontDescriptor déjà présent. Tout ce vers quoi pointent les pages du document reste exactement là où il se trouvait, ce qui rend l'opération sûre pour les fichiers que vous ne contrôlez pas

La prise en charge comprend les deux architectures de polices que vous rencontrerez en pratique : les polices TrueType simples et les polices composites Type0/CID, le type produit pour le texte CJK et la sortie Unicode moderne. Le parcours énumère délibérément chaque dictionnaire Font dans l'arbre d'objets du document plutôt que de s'appuyer sur un parcours de ressources page par page, de sorte que les polices référencées à partir d'annotations ou partagées entre les pages soient également récupérées. L'API est un appel unique sur le document chargé

var
  PDF: TPDFlib;
  Repaired: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('supplier-invoice.pdf', '') <> 1 then
      raise Exception.Create('Could not load PDF');

    // Walks every Font dictionary; returns how many fonts
    // gained a font program. Fonts whose program cannot be
    // found on the system are skipped, not failed.
    Repaired := PDF.EmbedMissingFonts;
    Writeln(Format('%d font program(s) embedded', [Repaired]));

    PDF.SaveToFile('supplier-invoice-repaired.pdf');
  finally
    PDF.Free;
  end;
end;

Un détail bon à savoir car il explique pourquoi la correspondance des noms fonctionne mieux qu'une simple comparaison de chaînes : la bibliothèque normalise les noms BaseFont avant de les rechercher. Les préfixes de sous-ensemble (le modèle ABCDEF+ de six lettres majuscules et un signe plus) sont supprimés, les suffixes de style PostScript tels que ArialMT sont résolus en Arial, et les fichiers TrueType Collection sont détectés et décompressés afin qu'une police résidant dans un fichier .ttc s'intègre toujours correctement

Vérifier la réparation avec un rapport de contrôle en amont

CreatePreflightReport est l'étape de vérification, et la boucle est délibérément fermée : le même audit qui a condamné le fichier doit être celui qui l'approuve. Le code d'erreur 00030 est la conclusion de l'audit approfondi PDF/A qui indique « Au moins une police n'est pas intégrée (FontFile/FontFile2/FontFile3 manquant) », et elle est signalée pour le fichier dans son ensemble, de sorte qu'une seule police oubliée le maintient actif. Exécutez le rapport sur le fichier source, réparez, enregistrez et exécutez-le à nouveau sur le fichier de sortie

function HasFontEmbeddingViolation(PDF: TPDFlib;
  const FileName: string): Boolean;
var
  Report: string;
begin
  // ComplianceTests = 1 selects the PDF/A checks
  Report := PDF.CreatePreflightReport(FileName, '', 1, 0);
  Result := Pos('00030', Report) > 0;
end;

Pour une vue par police plutôt qu'un verdict par fichier, chargez à nouveau le document réparé et énumérez : FindFonts suivi de SelectFont et GetFontIsEmbedded signale l'état d'intégration police par police, ce qui est l'outil approprié lorsqu'un traitement par lots doit enregistrer précisément quelle police de quel fichier n'a pas pu être réparée. Le même modèle d'énumération apparaît dans l'article sur l'extraction de texte, d'images et de polices à partir de PDF chargés, où il alimente l'extraction au lieu de la réparation

Que se passe-t-il lorsque la police n'est pas installée sur le système ?

EmbedMissingFonts ignore toute police dont elle ne trouve pas le programme, et signale cet oubli via sa valeur de retour : si le décompte est inférieur au nombre de polices non intégrées que vous avez comptées, la différence correspond aux polices que le système ne possède pas. Il s'agit du mode d'échec normal, et il est préférable aux alternatives, car inventer un programme de substitution pour une police nommée dans le document modifierait le rendu, ce qu'une réparation d'archivage ne doit absolument jamais faire. Pour ces cas, losLab PDF Library fournit EmbedFontProgramFromFile, qui intègre un fichier .ttf ou .otf fourni par l'appelant dans la police nommée, afin qu'un pipeline puisse distribuer les polices d'entreprise qu'il s'attend à rencontrer et s'y rabattre délibérément

var
  I, FontID: Integer;
begin
  PDF.FindFonts;
  for I := 1 to PDF.FontCount do
  begin
    FontID := PDF.GetFontID(I);
    if (FontID > 0) and (PDF.SelectFont(FontID) = 1) then
      if PDF.GetFontIsEmbedded = 0 then
        // Try the installed system font first, then fall back
        // to a font file shipped alongside the application
        if PDF.EmbedFontProgram(PDF.FontName) = 0 then
          PDF.EmbedFontProgramFromFile(PDF.FontName,
            'fonts\CorporateSans.ttf');
  end;
end;

Deux limites méritent d'être énoncées clairement. Premièrement, les polices Type1 ne sont pas réparées dans l'implémentation actuelle : leur entrée /FontFile requiert la structure PFB à trois segments avec des clés de longueur explicites, et la bibliothèque les ignore plutôt que d'écrire un flux malformé ; elles sont rares dans les documents modernes mais elles apparaissent dans les archives anciennes. Deuxièmement, l'intégration d'une polices est un acte soumis à licence. Les autorisations d'intégration d'une police TrueType appartiennent à sa fonderie, et un pipeline de réparation qui insère des programmes de polices sous licence dans des documents quittant l'organisation devrait faire confirmer par quelqu'un que les licences de polices le permettent réellement. La bibliothèque fera ce que vous lui demandez ; savoir si vous pouvez le demander est une question pour votre service juridique, pas pour votre compilateur

L'intégration est nécessaire, mais pas suffisante

La réparation des polices élimine l'erreur 00030, et rien d'autre. Un document qui échoue au contrôle PDF/A sur le chiffrement, les métadonnées XMP manquantes, un espace colorimétrique dépendant du périphérique sans OutputIntent, ou des tables ToUnicode absentes échouera toujours après l'intégration de chaque police, c'est pourquoi la réparation fait partie d'une boucle guidée par le contrôle en amont plutôt que de la remplacer. Exécutez le rapport complet, corrigez ce qu'il nomme, et laissez le rapport vous dire quand vous avez terminé. Il y a aussi une dimension de coût : un programme complet de police CJK représente plusieurs mégaoctets, donc l'intégration de plusieurs d'entre eux peut gonfler considérablement un petit document. Le contrepoids est le sous-ensemblement (subsetting), abordé dans l'article sur l'optimisation de la taille du fichier PDF et le sous-ensemblement des polices, qui réduit chaque programme intégré aux glyphes que le document affiche réellement

Empêcher les nouveaux documents de régresser

SetEmbedAllFonts est la moitié préventive de la même fonctionnalité : une protection côté écriture qui empêche votre propre code de produire les documents que cet article répare. Lorsque SetEmbedAllFonts(1) est actif, tout appel ultérieur à AddTrueTypeFont demandant Embed=0 est converti en référence intégrée, ce qui étend à chaque document la garantie que le mode PDF/A applique déjà. Cela affecte les polices ajoutées après l'appel, pas les polices déjà présentes dans un fichier chargé, la division du travail est donc claire : SetEmbedAllFonts pour les documents que vous créez, EmbedMissingFonts pour les documents dont vous héritez

PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// From here on, AddTrueTypeFont(Name, 0) behaves
// like AddTrueTypeFont(Name, 1): no non-embedded
// reference can reach the output file

Les deux moitiés, la protection côté écriture et le chemin chargement-réparation-sauvegarde, font partie de losLab PDF Library pour Delphi, C# et VB.NET, aux côtés du moteur de contrôle en amont qui vérifie le résultat ; la page du produit contient la référence complète de l'API de polices, y compris les appels d'intégration et de sous-ensemblement par police