Article technique

Détecter les glyphes PDF manquants lors du dessin en Delphi

Un glyphe manquant dans un PDF n'est pas une erreur. Le producteur demande un caractère que la police sélectionnée ne peut pas mapper, la police renvoie l'index de glyphe zéro, et le fichier qui sort est structurellement valide, s'ouvre partout, et montre une boîte vide là où un nom ou un montant devrait être. Personne dans le pipeline de génération ne l'apprend. Le destinataire, si. HotPDF ferme cette boucle avec TrackUnresolvedGlyphs : activez-la et la voie de dessin de texte enregistre chaque point de code dont la recherche de glyphe se résout à l'index zéro, déclenchant OnUnresolvedGlyph une fois par constat unique avec le point de code, la police sur laquelle il a échoué, l'écriture à laquelle il appartient, et une suggestion de polices qui le couvriraient

La détection est la moitié de la réponse. L'autre moitié est SetFontFallbackChain, qui enregistre une liste ordonnée de polices par écriture, pour que les cas courants se résolvent eux-mêmes et que seuls les vrais manques atteignent votre gestionnaire. Ensemble, elles transforment une classe de défaut autrefois rapportée par les clients en vérification au moment de la construction

Pourquoi un glyphe manquant ne lève-t-il rien ?

Parce que l'ISO 32000 n'impose aucune obligation à un producteur de vérifier la couverture, et l'index de glyphe zéro est un glyphe légitime. C'est .notdef, dont le concepteur de police choisit le contour : d'habitude un rectangle vide ou creux, parfois rien du tout. Un visionneur qui le dessine se comporte correctement. L'extraction de texte peut même renvoyer les bons caractères, car le mappage /ToUnicode est écrit depuis le texte source plutôt que depuis les contours, donc une vérification aller-retour automatisée passera volontiers un document dont le texte visible a des trous

Diagramme de pourquoi un glyphe PDF manquant reste silencieux tandis que le glyphe zéro dessine une boîte vide et que l'extraction ToUnicode passe les vérifications aller-retour
Le glyphe zéro est une réponse .notdef légitime et /ToUnicode est écrit depuis le texte source, donc rien dans le pipeline n'est informé du manque

La conséquence pratique est que la couverture doit être vérifiée au moment du dessin, quand la bibliothèque sait encore quel point de code a été demandé et quel glyphe la police a réellement offert. Après, l'information est perdue

Le détecteur doit surveiller l'état de sous-ensemble, pas le contexte de périphérique

C'est là que la première implémentation s'est trompée, et la raison vaut la peine d'être comprise car elle s'applique à toute vérification de couverture greffée sur un pipeline de texte. HotPDF a deux voies de texte. L'une émet à travers une police TrueType Unicode enregistrée avec une table de caractères en mémoire construite à l'enregistrement. L'autre est une voie GDI héritée qui crée un contexte de périphérique et un handle de police frais par exécution de caractères

Juger la couverture depuis la voie GDI est sans espoir. Son mappage n'est pas le mappage qui finit dans le flux de contenu émis, et les deux ne sont pas synchronisés, donc un détecteur lisant les résultats GDI signale toute la plage ASCII imprimable comme non résolue. La réponse faisant autorité vit dans la police enregistrée : la table de caractères que RegisterUnicodeTTF analyse, interrogée par GetUnicodeGlyphForCodepoint. Le détecteur est donc verrouillé sur l'état prêt au sous-ensemble, pas sur une condition GDI quelconque, et il ne tourne simplement pas sur les documents qui n'ont jamais enregistré de police Unicode, ce qui est correct car ces documents sont de toute façon limités aux encodages standards

Un second piège se trouve juste à côté. Le nom de famille GDI d'une police et le nom PostScript extrait du binaire de la police à l'enregistrement sont des chaînes différentes, et pas d'une manière que vous pouvez normaliser : une famille appelée Arial Unicode MS porte le nom PostScript ArialMT. Tout verrou écrit « la police actuellement sélectionnée est-elle celle que nous avons enregistrée », comparé par nom, est du code mort qui ne se déclenche jamais. Verrouillez sur l'état, jamais sur les noms de polices

Flux de détection de glyphes non résolus HotPDF montrant le verrou sur l'état de sous-ensemble, la recherche GetUnicodeGlyphForCodepoint et le câblage de l'événement OnUnresolvedGlyph
La couverture est jugée depuis la table de la police Unicode enregistrée plutôt que depuis GDI, et chaque point de code unique déclenche un événement avec une suggestion de police

Ne testez pas un détecteur de glyphes avec des émojis

Le cas de test évident est une figure souriante, et il vous convaincra que le détecteur est cassé. Les points de code émoji courants dans les plans astraux se résolvent par une voie de synthèse à usage privé qui les mappe directement à un index de glyphe, donc ils n'atteignent jamais la branche de couverture générale. Le détecteur se comporte correctement et le test mesure la mauvaise voie

Utilisez plutôt un point de code non attribué. U+0378 est à jamais non alloué dans Unicode, donc aucune police ne peut légitimement le mapper, et il exerce exactement la branche que vous voulez vérifier. Cette distinction entre « la fonctionnalité est cassée » et « le test a choisi une entrée qui contourne la fonctionnalité » coûte de vraies heures, et les points de code non attribués sont la façon la moins chère de l'éviter

type
  TCoverageAudit = class
  private
    FFindings: TStringList;
  public
    procedure Handle(Sender: TObject;
      const Info: THPDFUnresolvedGlyphInfo);
    property Findings: TStringList read FFindings;
  end;

procedure TCoverageAudit.Handle(Sender: TObject;
  const Info: THPDFUnresolvedGlyphInfo);
begin
  // Se déclenche une fois par point de code unique, pas une fois par occurrence
  FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
    [Info.CodePoint, String(Info.FontName), Ord(Info.Script),
     String(Info.SuggestedFonts)]));
end;

// Le câbler dans un travail de génération
Pdf := THotPDF.Create(nil);
try
  Pdf.TrackUnresolvedGlyphs := True;
  Pdf.OnUnresolvedGlyph := Audit.Handle;
  Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
  Pdf.BeginDoc;
  Pdf.CurrentPage.SetFont('Arial', [], 11);
  Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
  Pdf.EndDoc;
  if Audit.Findings.Count > 0 then
    // Faire échouer le travail plutôt que de livrer une page avec des boîtes
    raise Exception.Create(Audit.Findings.Text);
finally
  Pdf.Free;
end;

Les chaînes de repli sont par écriture, pas par police

La raison pour laquelle le repli est délimité par écriture plutôt que par police source est que les manques de couverture se regroupent par système d'écriture. Une police de texte latine manque de devanagari, de thaï, de han et d'émoji, tout à la fois, et le substitut de chacun est une police différente. Déclarer une chaîne par écriture décrit donc le déploiement réel : une police latine pour le corps de texte, une police CJK, une police émoji, une police attrape-tout

Diagramme de repli de polices par écriture mappant les écritures hfsCJK, hfsArabic, hfsEmoji et hfsOther à des chaînes de polices de substitution ordonnées dans HotPDF
Chaque écriture obtient sa propre chaîne ordonnée, donc une police latine de corps manquant de han, d'arabe ou d'émoji tombe sur une police qui la couvre
// THPDFFontScript couvre hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji et hfsOther
Pdf.SetFontFallbackChain(hfsCJK,
  ['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);

Le repli et la détection sont complémentaires plutôt qu'alternatifs. Les chaînes gèrent la couverture que vous avez anticipée ; le détecteur signale la couverture que vous n'avez pas anticipée, ce qui sur un système traitant des données clients arbitraires est la moitié intéressante. Notez que substituer une police change les métriques, donc un paragraphe qui replie peut se recomposer ; si la mise en page compte, le comportement de fermeture et de sous-ensemblisation de la police substituée mérite d'être approfondi dans l'article sur la fermeture de sous-ensemble de police, et les écritures qui exigent un réordonnancement ou une liaison sont gérées par l'étape de shaping décrite dans le shaping du texte en écritures complexes

Comment adapter un comportement sans risquer la voie existante

La même version a ajouté un repli de table kern héritée pour l'espacement des paires, et la façon dont il a été délimité est un schéma qui vaut d'être copié. Plutôt que d'ajouter un nouveau point de décision à la logique de crénage, le repli se suspend à la branche de sortie anticipée qui existait déjà pour les polices sans table GPOS. Une police moderne avec GPOS ne l'atteint jamais, donc son comportement est inchangé par construction plutôt que par test. Les voies qui n'enregistrent pas de police Unicode produisent deux décalages nuls, donc elles sont inchangées elles aussi

C'est la forme générale d'un adaptage à faible risque dans une bibliothèque de rendu mature : trouver la branche qui ne produit actuellement rien et y mettre le nouveau comportement. Cela transforme « nous croyons que cela n'a rien fait régresser » en « cela ne peut rien avoir fait régresser », ce qui est une bien meilleure chose à dire sur un moteur de texte traversé par les factures des autres

Faites-en un verrou, pas un journal

Les constats de couverture ne sont utiles que si quelque chose échoue dessus. Dans un service de génération de documents, l'arrangement productif est de garder le suivi actif dans le travail de régression nocturne contre un corpus de vrais noms de clients, adresses et descriptions de produits, et de faire échouer le travail sur tout constat. Comme l'événement se déclenche une fois par point de code unique plutôt qu'une fois par occurrence, la sortie reste assez petite pour être lue même quand toute une écriture manque

En production, le même gestionnaire vaut mieux comme télémétrie : enregistrez le point de code et la police, continuez de servir le document, et laissez l'agrégat vous dire quelle écriture ajouter ensuite à l'ensemble de polices de déploiement. Le comportement de rendu pour les polices intégrées et substituées est couvert plus loin dans le rendu des glyphes de polices intégrées, et la liste complète des propriétés y compris TrackUnresolvedGlyphs est documentée sur la page produit du HotPDF Delphi PDF component