Article technique

Maîtriser la substitution de police PDF dans Delphi avec PDFium

PDFium Component permet à une application Delphi de décider quels octets de police sont utilisés lorsqu'un PDF référence une police qu'il n'intègre pas. ConfigureSystemFontProvider installe une implémentation d'IPdfSystemFontProvider qui reçoit chaque demande de projection de police que PDFium effectue, avec le nom de la face, la graisse, l'indicateur italique, le jeu de caractères et la famille de chasse, et répond avec les octets TrueType, TrueType Collection ou OpenType à utiliser

Cela existe parce que les polices non intégrées sont une loterie de rendu. Un PDF qui nomme Arial et n'intègre rien se rend avec Arial sur un poste de travail, avec un substitut compatible en métriques sur un serveur Linux, et avec ce que le mappeur hôte trouve sur une image conteneur verrouillée. La même facture a un aspect différent sur chacun, les sauts de ligne se déplacent, et un client reçoit un document qui ne correspond pas à la copie archivée

Pourquoi ne pas simplement installer les polices sur le serveur ?

C'est parfois la bonne réponse, et quand c'est le cas, adoptez-la. Mais elle échoue dans trois situations courantes. Une licence peut interdire d'installer une police sur un serveur pour du rendu automatisé. Les images de conteneur sont reconstruites fréquemment et une police installée à la main disparaît au déploiement suivant. Et les flux de travail réglementés exigent que la pile de rendu soit reproductible à partir d'artefacts sous contrôle de version, ce qu'une installation de police à l'échelle de la machine n'est pas

Un fournisseur répond aux trois en déplaçant la décision dans votre application. Les polices sont livrées comme des ressources que vous contrôlez, la politique de projection est du code que vous pouvez relire, et le même binaire produit un rendu identique partout, car rien ne dépend de ce qui se trouve installé

Installer un fournisseur

La configuration doit avoir lieu avant le chargement de la bibliothèque. PDFium accepte une structure d'informations de police système à l'initialisation et conserve les descripteurs qu'il distribue ensuite, de sorte qu'échanger un fournisseur pendant que des documents sont ouverts invaliderait des descripteurs de police que PDFium détient toujours ; le composant rejette purement et simplement cela plutôt que de laisser corrompre un rendu :

uses
  PDFium;

type
  TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
  public
    function ResolveFont(const Request: TPdfSystemFontRequest;
      out Font: TPdfSystemFontData): Boolean;
  end;

function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
  out Font: TPdfSystemFontData): Boolean;
var
  Path: string;
begin
  // Projection déterministe : le nom de la face plus la graisse et l'italique décident
  // du fichier livré pour cette demande
  Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
    Request.Italic, Request.Charset);
  Result := Path <> '';
  if not Result then
    Exit;
  Font.FaceName := Request.FaceName;
  Font.FontData := LoadFileBytes(Path);   // octets sfnt ou TTC complets
  Font.Charset := Request.Charset;
  Font.TTCIndex := 0;                     // index à l'intérieur d'une collection
end;

var
  Policy: TPdfSystemFontPolicy;
begin
  Policy := TPdfSystemFontPolicy.Default;
  Policy.AllowDefaultFallback := False;   // l'hôte décide de tout
  Policy.AllowFaceSubstitution := False;  // rejeter un nom de face différent
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

  ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
  // Ce n'est qu'à présent que la bibliothèque est chargée et les documents ouverts
end;

Le démontage s'exécute dans l'ordre inverse : le fournisseur est d'abord détaché de PDFium, puis la bibliothèque est déchargée. Omettre le détachement laisse des descripteurs de police natifs pointant vers des objets Pascal sur le point d'être libérés, ce qui est la classique violation d'accès à l'arrêt dans un code qui mélange des interfaces à comptage de références avec une bibliothèque C

Ce que les indicateurs de politique décident réellement

AllowDefaultFallback est le commutateur entre deux modes de fonctionnement. Désactivé, une demande que le fournisseur décline échoue simplement, ce qui est ce que l'on souhaite tant qu'on prouve que chaque police d'un corpus est prise en compte : tout manque devient immédiatement visible au lieu d'être masqué. Activé, les demandes non résolues sont déléguées au mappeur renvoyé par FPDF_GetDefaultSystemFontInfo, tandis que le monde extérieur continue de voir un unique wrapper de descripteur uniforme, avec le nom de face, le jeu de caractères, les données de table et la suppression de police correctement acheminés selon l'origine

AllowFaceSubstitution régit si un fournisseur peut répondre avec un nom de face différent de celui demandé. La désactiver fait de la substitution une décision explicite plutôt qu'un accident, ce qui compte lorsqu'un document nomme une police dont les métriques diffèrent suffisamment pour changer la pagination

Le composant valide chaque réponse du fournisseur avant qu'elle n'atteigne PDFium : les données vides sont rejetées, les polices surdimensionnées sont rejetées face à MaxFontBytes, l'index TTC est vérifié, et les tables sfnt individuelles sont servies depuis le répertoire de polices lorsque PDFium demande une table plutôt que le fichier entier. Cette dernière capacité signifie qu'un fournisseur peut remettre un fichier de police complet et laisser le composant répondre aux requêtes au niveau des tables, au lieu d'exposer des objets Pascal bruts à travers l'ABI C

Mettre en cache sans données de police pendantes

Les demandes de projection de police se répètent constamment pendant le rendu, donc les réponses sont mises en cache avec une clé couvrant chaque paramètre de sélection de police, évincées selon un ordre borné du moins récemment utilisé. La subtilité concerne la durée de vie : PDFium peut encore être en train de lire les octets d'une police dont l'entrée de cache vient d'être évincée

Le cache stocke des tableaux dynamiques à comptage de références et chaque descripteur natif détient son propre instantané, de sorte que l'éviction abandonne une référence plutôt que de libérer une mémoire en cours d'utilisation. Le rappel de suppression libère le descripteur et maintient un compteur d'actifs. En pratique, cela signifie que MaxCacheEntries peut être ajusté pour la mémoire sans aucun risque de retirer des données sous un rendu en cours

Le fournisseur est-il appelé sur mon thread ?

Non, pas nécessairement. PDFium peut appeler le mappeur depuis ses propres threads de travail, donc une implémentation doit être thread-safe. Les compteurs partagés, le cache et l'observation de la configuration sont chacun protégés à l'intérieur du composant par leur propre section critique, mais le code à l'intérieur de ResolveFont vous revient de rendre sûr

La forme la plus sûre est un fournisseur qui ne touche à aucun état partagé mutable : lire depuis une table construite au démarrage, charger des octets depuis un fichier ou une ressource, renvoyer. Si une recherche nécessite un cache partagé qui vous appartient, protégez-le. Et gardez les exceptions à l'intérieur de votre implémentation, car une exception Pascal ne doit jamais se dérouler à travers la pile de PDFium ; le composant capture à la frontière de l'ABI C et convertit en échec ou en repli par défaut optionnel, mais s'appuyer là-dessus comme flux de contrôle normal coûte en performance et cache des bugs. Les règles de threading pour le reste du composant suivent les mêmes principes que celles décrites dans la discipline de verrou de rendu

Prouver la projection en production

Les statistiques transforment la substitution de police d'une supposition en quelque chose que l'on peut vérifier par assertion. GetSystemFontProviderStatistics indique si un fournisseur est configuré et installé, combien de demandes de projection ont été effectuées, et comment elles ont été satisfaites, réparties en succès de cache, succès de fournisseur et succès de repli par défaut, ainsi que les réponses rejetées, les demandes échouées, les descripteurs actifs et les polices mises en cache :

var
  Stats: TPdfSystemFontStatistics;
begin
  Stats := GetSystemFontProviderStatistics;
  Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
    [Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
     Stats.DefaultFallbackHits]));
  Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
    [Stats.RejectedProviderResponses, Stats.FailedRequests,
     Stats.ActiveHandles, Stats.CachedFonts]));

  // Dans une exécution de conformité avec le repli désactivé, tout succès de repli ou
  // demande échouée signifie qu'un document a référencé une police que nous ne livrons pas
  if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
    raise Exception.Create('unmapped font encountered - update the font set');
end;

Un compteur RejectedProviderResponses en hausse est le signal qu'un fournisseur répond avec des données que la politique refuse, généralement un fichier surdimensionné ou une face substituée, et cela mérite une alerte car ces demandes dégradent silencieusement vers un repli ou un échec. Pour diagnostiquer quelles polices un document nécessite réellement avant de construire la table de projection, la méthode d'inspection décrite dans l'analyse des propriétés de police PDF liste les polices intégrées et non intégrées par document

L'approvisionnement en polices, le rendu et l'extraction de texte partagent la même instance de bibliothèque dans Delphi, C++Builder et Lazarus ; les détails de déploiement sont décrits sur la page PDFium Component pour Delphi