Générez un rapport, intégrez une police TrueType, et la sortie s'ouvre correctement dans toutes les visionneuses que vous essayez. Les glyphes sont corrects, le texte est sélectionnable, le fichier est valide. La seule chose qui ne va pas, c'est la taille. Un document qui a utilisé quelques dizaines de caractères latins transporte l'intégralité de la police de 350 Ko. Un document qui a imprimé un paragraphe en chinois transporte une police CJK de 14 Mo au lieu de la tranche d'un demi-mégaoctet dont il devrait avoir besoin. Aucune exception n'a été levée, aucun avertissement n'a été consigné et le fichier a passé la validation. Voici à quoi ressemble une étape de finalisation mal ordonnée de l'extérieur : rien n'échoue, et la seule preuve est un nombre trop grand
Le bug qui l'a produit a vécu dans HotPDF le temps d'une ligne de version et a depuis été corrigé. Il vaut la peine d'être décrit non pas comme un avis de défaut, mais comme une leçon, car la forme de l'erreur est d'ordre général. Tout moteur de document comporte une étape de finalisation qui mute les objets juste avant de les écrire, et la justesse de cette étape dépend entièrement de l'ordre de ses étapes par rapport à la sérialisation. Mettez une étape du mauvais côté de l'écriture et elle ne fait rien, discrètement
Ce que le sous-ensemblage de polices est censé faire
Une police de sous-ensemble est la partie d'un fichier TrueType qu'un document utilise réellement. La norme ISO 32000-1 §9.9 décrit comment un programme de police intégré navigue dans un flux référencé par le descripteur de police, et pour un programme TrueType, ce flux est /FontFile2 avec un /Length1 donnant le nombre d'octets non compressés. Le sous-ensemblage réécrit les tables glyf et loca pour qu'elles ne contiennent que les glyphes que le document référence, renumérote les identifiants de glyphes et préfixe le nom de /BaseFont avec une balise de six lettres telle que ABCDEF+ pour marquer la police comme étant un sous-ensemble, exactement comme l'exige la spécification. Une police latine qui se sous-ensemble à dix ou quinze kilo-octets fait la différence entre un PDF allégé et un PDF qui embarque une police de caractères entière pour l'amour d'un seul titre
Le moment où cela se produit a de l'importance. Le sous-ensemblage n'est pas une transformation que vous appliquez aux octets déjà sur le disque. Il modifie le graphe d'objets en mémoire : il réduit le contenu du flux /FontFile2, corrige /Length1 et réécrit la chaîne /BaseFont. Tout cela doit être en place lorsque le sérialiseur parcourt le graphe et émet des octets. Si les modifications surviennent après l'écriture des octets, elles mettent à jour des objets que personne ne lira jamais
Le symptôme, et pourquoi rien ne s'est plaint
Le comportement signalé consistait en des polices complètes dans la sortie sans aucun diagnostic. Un utilisateur qui enregistrait une police TrueType Unicode et produisait un document normal a constaté que l'objet de police intégré avait la même longueur que le fichier .ttf source, et que le nom /BaseFont ne portait aucun préfixe de sous-ensemble à six lettres. La sortie ne rétrécissait jamais entre les exécutions qui utilisaient dix glyphes et celles qui en utilisaient dix mille
L'absence de toute erreur est la partie qui rend cette classe de bugs coûteuse. Une routine de sous-ensemblage qui s'exécute au mauvais moment s'exécute tout de même. Elle parcourt l'utilisation accumulée des points de code, construit un sous-ensemble parfaitement correct et l'applique au graphe d'objets en mémoire. En interne, le travail est fait et l'appel revient proprement. La seule chose qui ne va pas, c'est que le graphe d'objets qu'elle a modifié n'est plus la chose qui est écrite, car l'enregistreur a déjà terminé. Du point de vue de l'appelant, le document a été produit et enregistré sans incident, ce qui est précisément l'impression que donne une défaillance silencieuse
La cause fondamentale était l'ordre de finalisation
Dans HotPDF, le travail de fermeture se produit dans EndDoc. L'étape de sous-ensemblage est une routine interne nommée BuildAndApplyUnicodeFontSubset. Elle lit l'ensemble de points de code utilisés par document, conservés dans un bitmap que le chemin d'émission de texte remplit au fur et à mesure que les glyphes sont affichés, mappe chaque point de code utilisé via la table point-de-code-vers-glyphe en cache vers un identifiant de glyphe réel, et réécrit le programme de police autour de cette fermeture. Lorsqu'une police TrueType Unicode est enregistrée, le chemin d'émission définit un bit dans l'ensemble de points de code utilisés pour chaque caractère qu'il dessine, de sorte qu'au moment où le document se ferme, le moteur sait exactement quels glyphes le sous-ensemble doit conserver
Le défaut résidait dans le fait que BuildAndApplyUnicodeFontSubset était invoquée après que SaveToStream ou SaveToFile ait déjà sérialisé le document. Les modifications du sous-ensembleur à /FontFile2, son /Length1 corrigé et le préfixe /BaseFont de six lettres ont tous été calculés par rapport à un graphe d'objets qui avait déjà été transformé en octets. La solution a été une réorganisation en une ligne : déplacer l'appel de sous-ensemblage avant la sérialisation, de sorte que l'enregistreur émette la police sous-ensemblée plutôt que l'original. La séquence corrigée exécute d'abord le sous-ensembleur et sérialise ensuite
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
Pdf.EndDoc; // subsetting runs here, before the write
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Une fois l'ordre corrigé, rien ne change dans le code appelant. Le sous-ensemblage est activé par défaut une fois qu'une police TrueType Unicode a été enregistrée. Vous enregistrez la police, commencez le document, dessinez et le terminez, et le sous-ensemble est construit à partir des glyphes que vous avez utilisés avant que les octets ne quittent la mémoire
Pourquoi une étape mal placée constitue une catégorie entière
La raison pour laquelle cela vaut une leçon plutôt qu'une note de bas de page est que EndDoc émet une liste d'étapes de fermeture, et chacune d'elles est sensible à sa position par rapport à l'écriture. Le sous-ensemblage de polices en est une. La sortie PDF/A requiert un flux /CIDSet qui énumère exactement les identifiants de glyphes présents dans le sous-ensemble, une contrainte imposée par la norme ISO 19005 pour qu'un validateur puisse confirmer que le programme intégré correspond à ce que le descripteur de police prétend ; ce flux est émis dans la même fenêtre de finalisation et dépend de la construction préalable du sous-ensemble. Le PDF/UA-1 exige, selon la norme ISO 14289-1 §7.18.3, que chaque page portant une annotation déclare /Tabs avec la valeur /S, et une routine interne nommée EnsurePDFUATabsOnAnnotatedPages appose cette clé au cours de la même étape. Les vérifications de l'intention de sortie s'y exécutent également
La même erreur d'ordre qui a désactivé le sous-ensemblage a également supprimé la clé d'ordre de tabulation PDF/UA sur les pages annotées, parce que cette étape se trouvait du même mauvais côté de l'écriture. veraPDF et PAC signalent un /Tabs /S manquant comme une violation du point de contrôle 21-001 du protocole Matterhorn. Ainsi, un seul appel mal placé n'a pas seulement gonflé la taille du fichier ; il a silencieusement enfreint une exigence de conformité d'accessibilité en même temps, avec le même manque de la moindre erreur. C'est le danger d'une étape de finalisation : ses étapes partagent une condition préalable, et une seule erreur d'ordre peut en éliminer plusieurs à la fois alors que chaque appel renvoie toujours le succès
Comment une défaillance silencieuse d'émission est réellement capturée
Un bug qui ne lève aucune exception n'est pas détecté en exécutant le programme. Il est capturé en inspectant la sortie et en la comparant à ce que l'entrée aurait dû produire. Pour le sous-ensemblage de polices, les vérifications sont concrètes. Comparez la taille du fichier de sortie à une attente approximative : un document qui a touché à une poignée de glyphes ne devrait pas avoir la taille d'une police de caractères complète. Ouvrez l'objet de police intégré et lisez sa longueur en octets ; un /FontFile2 sous-ensemblé pour une police latine représente une petite fraction du fichier source. Lisez le nom de /BaseFont et confirmez que le préfixe de six lettres est présent, car son absence est un signal direct qu'aucun sous-ensemble n'a été appliqué
var
Pdf: THotPDF;
Output: TMemoryStream;
begin
Output := TMemoryStream.Create;
try
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
Pdf.EndDoc;
Pdf.SaveToStream(Output);
finally
Pdf.Free;
end;
// A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
if Output.Size > 100 * 1024 then
raise Exception.Create('Font subset did not shrink the output');
finally
Output.Free;
end;
end;
Pour la sortie PDF/A, la vérification est encore plus nette, car un validateur fait le travail pour vous. Définissez le niveau de conformité et faites passer le résultat dans veraPDF : un /CIDSet manquant, ou un sous-ensemble qui ne correspond pas au descripteur, est signalé comme une clause défaillante plutôt que d'être laissé à votre appréciation visuelle. Les commutateurs de conformité qui pilotent ce travail de finalisation sont des propriétés du document. PDFACompliance prend une chaîne de caractères telle que '2B' pour le PDF/A-2 Niveau B, et PDFUACompliance est un booléen qui active les exigences en matière de PDF balisé et d'ordre de tabulation
Pdf := THotPDF.Create(nil);
try
Pdf.PDFACompliance := '2B'; // PDF/A-2 Level B, drives /CIDSet emission
Pdf.PDFUACompliance := True; // stamps /Tabs /S on annotated pages
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
Pdf.EndDoc;
Pdf.SaveToFile('Report_PDFA.pdf');
finally
Pdf.Free;
end;
La leçon d'ingénierie
Deux règles en découlent. La première est que toute étape de finalisation qui mute des objets doit s'exécuter avant que ces objets ne soient sérialisés, et la phase de fermeture d'un moteur de document doit être lue comme un pipeline ordonné où la sérialisation est la dernière action, et non une action parmi d'autres. La seconde est celle qui a coûté le plus de temps ici : pour une étape d'émission, l'absence d'erreur n'est pas une preuve de succès. Une routine qui construit le bon sous-ensemble et l'applique au mauvais graphe, déjà écrit, ne signale rien d'anormal, car de son propre point de vue, il n'y avait rien. La vérification doit se pencher sur l'artefact, et non sur le code de retour. Vérifiez la taille de la sortie, lisez la longueur en octets de la police intégrée et son préfixe /BaseFont, et laissez veraPDF juger de la sortie PDF/A où un /CIDSet manquant transforme une insuffisance silencieuse en un échec nommé
Le côté producteur de la gestion des polices, la manière dont les polices de caractères sont enregistrées et intégrées pour la sortie de rapports, est couvert dans notre article sur les polices et les images dans la sortie de rapports. Le côté validation, où ces étapes de finalisation sont vérifiées par rapport aux normes, est couvert dans le guide sur la validation PDF/A et PDF/UA. Les deux vont de pair avec le travail de sous-ensemblage et de conformité décrit ici, qui est fourni avec le Composant HotPDF pour Delphi et C++Builder aux côtés des API de chargement, d'édition, de chiffrement et de signature couvertes ailleurs sur ce blog