Article technique

Intégrer des polices CID Type 2 dans un PDF depuis Delphi

PDFium Component écrit du texte dans une page PDF au niveau du glyphe avec AddCidType2Text, en intégrant des données TrueType que vous fournissez sous forme de police CID Type 2. Il attribue à chaque instance de glyphe un CID séquentiel, génère la correspondance CID-vers-GID explicite et la CMap ToUnicode, et fusionne les glyphes sur une même ligne de base en un seul objet texte natif. Le sous-ensemblage optionnel garde le fichier léger, avec une politique explicite pour ce qui se passe quand le sous-ensemblage échoue

L'écriture au niveau du glyphe est ce dont vous avez besoin quand le texte a déjà été façonné. L'arabe, le devanagari et tout script à formes contextuelles produisent une séquence d'identifiants de glyphe qui ne se mappe plus un pour un sur des caractères, si bien qu'une API prenant une chaîne et un nom de police ne peut pas exprimer le résultat. Remettre directement les glyphes et les positions est le seul moyen de placer du texte de script complexe correctement façonné dans un PDF

AddCidType2Text en Delphi attribue à chaque instance de glyphe façonné un CID séquentiel, écrit le CIDToGIDMap et le CMap ToUnicode, et fusionne la séquence en un objet texte de page qui s'extrait proprement
Chaque instance de glyphe prend son propre CID, le dictionnaire de la police mappe les CIDs vers des identifiants de glyphes et retour vers Unicode, et la séquence atterrit sur la page comme un objet texte qui s'extrait en retour vers le texte voulu

Pourquoi chaque instance de glyphe reçoit son propre CID

L'optimisation tentante consiste à dédupliquer : un CID par identifiant de glyphe distinct, réutilisé partout où ce glyphe apparaît. Cela produit une police plus petite et cela casse l'extraction de texte, car le même glyphe peut légitimement correspondre à un contenu Unicode différent selon les endroits

Un glyphe de ligature est le cas évident. Le même glyphe « ffi » peut représenter les trois caractères dans un mot et, après une décision de façonnage différente, quelque chose d'autre ailleurs. La correspondance ToUnicode est indexée par CID, si bien qu'un CID partagé ne peut porter qu'une seule correspondance, et quel que soit le texte qui perd cette compétition, il devient inextractible

Chaque instance de glyphe prend donc son propre CID, et chaque correspondance est libre de représenter plusieurs unités de code UTF-16. Un glyphe sans texte significatif — un élément décoratif, un glyphe de positionnement pur — se mappe explicitement vers U+200B, une espace de largeur nulle, si bien que chaque CID possède un résultat d'extraction observable plutôt qu'un vide

uses
  PDFium;

var
  Pdf: TPdf;
  Glyphs: TPdfCidGlyphs;
  Options: TPdfCidFontOptions;
  Report: TPdfCidFontReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'label.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;

    // Une entrée par glyphe façonné : l'id de glyphe, le texte qu'il représente,
    // et son avance et ses décalages dans l'espace texte
    SetLength(Glyphs, 3);
    Glyphs[0].GlyphID := 402;  Glyphs[0].UnicodeText := 'ffi';
    Glyphs[0].Advance := 18.4;
    Glyphs[1].GlyphID := 71;  Glyphs[1].UnicodeText := 'c';
    Glyphs[1].Advance := 9.8;
    Glyphs[2].GlyphID := 74;   Glyphs[2].UnicodeText := 'e';
    Glyphs[2].Advance := 9.2;

    Options := TPdfCidFontOptions.Default;   // sous-ensemble préféré
    Options.VerifyExtraction := True;

    if Pdf.AddCidType2Text(LoadFileBytes('NotoSans.ttf'), Glyphs,
         11, 72, 700, Options, Report) then
      Writeln(Format('%d glyphs, %d unique, font %d -> %d bytes, match=%s',
        [Report.GlyphCount, Report.UniqueGlyphCount,
         Report.OriginalFontBytes, Report.EmbeddedFontBytes,
         BoolToStr(Report.ExtractionMatches, True)]));
  finally
    Pdf.Free;
  end;
end;

Le sous-ensemblage, et les vérifications qu'un code de retour ne vous donne pas

Réduire une police en sous-ensemble aux glyphes réellement utilisés fait la différence entre intégrer 300 Ko et intégrer 12 Ko, et sur un document comportant plusieurs polices, cela décide si le fichier est envoyable par e-mail. La voie de sous-ensemblage de la plateforme accepte directement une liste de glyphes, ce qui convient parfaitement à cette API puisque l'appelant connaît déjà chaque identifiant de glyphe utilisé

Ce que cela ne vous donne pas, c'est la confiance. Un appel de sous-ensemblage peut rapporter un succès et retourner une sortie inutilisable, si bien que le composant valide trois propriétés avant d'accepter le résultat : la sortie doit être plus petite que l'originale, elle doit se réanalyser comme un sfnt valide avec une table maxp lisible, et elle doit conserver les glyphes jusqu'à l'identifiant original le plus élevé demandé. Tout échec signifie que le sous-ensemble est rejeté

Ce qui se passe ensuite relève de la politique de l'appelant. Sous pcfemSubsetPreferred, le réglage par défaut, un sous-ensemble rejeté revient à intégrer la police complète, si bien que la page est correcte et simplement plus volumineuse. Sous pcfemSubsetRequired, l'opération échoue avant que la page ne soit modifiée, ce que veut un pipeline contraint en taille. pcfemFull saute entièrement le sous-ensemblage. Le rapport indique quelle voie a été prise via SubsetAttempted, SubsetApplied, UsedFullFontFallback et SubsetErrorCode

Le subsetting de police CID Type 2 en Delphi passe trois vérifications de validation — taille, intégrité sfnt et couverture des glyphes — avant acceptation, et un sous-ensemble rejeté suit la politique pcfem de repli sur la police complète ou d'échec dur
Un sous-ensemble n'est accepté qu'après le passage des contrôles de taille, de sfnt et de couverture de glyphes, et un sous-ensemble rejeté suit ensuite la politique de l'appelant — retomber sur la police complète ou échouer avant que la page ne soit modifiée

Une vérification qui ne peut pas produire de faux positif

Avec VerifyExtraction activé, le composant confirme que ce qu'il a écrit peut être relu. La façon naïve de le faire est d'extraire toute la page et de chercher la chaîne attendue, et c'est erroné : une page qui contenait déjà ce texte réussit le test même quand le nouveau texte a été écrit incorrectement

Au lieu de cela, la page de texte est reconstruite et les handles d'objet insérés par cet appel sont lus individuellement, dans l'ordre d'insertion, puis concaténés. Le résultat est comparé au texte attendu, et le rapport expose les deux chaînes aux côtés du booléen, si bien qu'une divergence peut être diagnostiquée plutôt que simplement détectée

Activez-le pendant le développement et dans tout pipeline où l'extractibilité est une exigence — archives consultables, conformité d'accessibilité, exploration de texte en aval. Le coût est une reconstruction de page de texte par appel, ce qui explique qu'il ne soit pas activé par défaut dans les boucles serrées

Budgets et échec atomique

Octets de police, instances de glyphes et unités de code Unicode sont chacun plafonnés avant l'allocation, et le format de police, l'index TTC, la plage d'identifiants de glyphe et les valeurs de géométrie sont validés avant toute écriture. La géométrie doit être finie, ce qui semble évident jusqu'à ce qu'un moteur de façonnage vous remette une avance NaN issue d'une police malformée

L'échec est atomique au niveau de la page. Si le chargement de police, l'écriture d'objets, la génération de contenu ou la vérification d'extraction échoue, chaque objet inséré par cet appel est retiré dans l'ordre inverse et le contenu de la page est régénéré. Un appel échoué laisse la page telle qu'elle était, pas avec un demi-fragment de texte dessus

Où cela s'inscrit dans un pipeline de texte

La répartition du travail mérite d'être énoncée clairement. Le façonnage — transformer des caractères en glyphes positionnés — n'est pas le travail de cette API ; il revient à un moteur de façonnage, et la propre prise en charge bidirectionnelle et des scripts complexes du composant est traitée dans la gestion des emoji, du CJK et des paires suppléantes. Cette API est ce que vous appelez ensuite, avec la série de glyphes que le moteur de façonnage a produite

Pour du texte latin ordinaire sans façonnage contextuel, les API texte basées sur des chaînes plus simples sont le bon outil et produisent un code plus compact. Recourez à l'écriture CID Type 2 quand vous disposez d'une sortie façonnée, quand vous avez besoin d'un contrôle exact des identifiants de glyphe, ou quand la police doit être intégrée à partir d'octets que vous détenez plutôt que résolue par nom — l'alternative de résolution de police étant le mécanisme fournisseur décrit dans contrôler la substitution de police PDF

Une remarque de déploiement : intégrer une police est autant une question de licence qu'une question technique. Les polices varient selon que l'intégration est autorisée du tout, autorisée pour l'affichage seulement, ou autorisée pour l'édition. La bibliothèque intégrera n'importe quels octets que vous lui donnez, et vérifier la licence relève de votre responsabilité, pas de celle du format de fichier

L'écriture de texte au niveau du glyphe, la fourniture de polices et l'extraction de texte partagent le même modèle de page en Delphi, C++Builder et Lazarus ; l'API complète est décrite sur la page du composant PDFium pour Delphi