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
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
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
// 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