Un rapport veraPDF affirmant qu’une largeur de glyphe contredit le programme de police embarqué ne vous dit presque rien sur quel glyphe, ni pourquoi. PDFlibPas répond à cette question en résolvant chaque code de caractère à travers la cmap embarquée vers un index de glyphe, en normalisant la métrique du programme à 1000 unités par em, et en comparant là
Pourquoi les largeurs de glyphes sont-elles en désaccord ?
Parce que les deux nombres comparés vivent dans des systèmes de coordonnées différents, et rien dans le dictionnaire PDF ne vous donne la conversion. Un dictionnaire de police écrit /Widths en espace glyphe, que le PDF fixe au millième de l’em (ISO 32000-1 §9.2.4). La table hmtx à l’intérieur du programme TrueType embarqué écrit les avances en unités de conception de la police, et la table head décide combien de ces unités font un em : 2048 pour la plupart des fontes TrueType, 1000 pour celles dérivées de CFF, parfois tout autre chose. Comparez les valeurs brutes et chaque police 2048-upem de votre corpus paraît cassée. C’est le piège que l’ISO 14289-1 §7.21.5 tend à quiconque tente d’auditer les largeurs en lisant les champs du dictionnaire
PDFlibPas normalise au chargement. TPDFTrueTypeParser stocke Advance * 1000 div unitsPerEm dans son tableau de largeurs, si bien que Parser.GetWidth(GID) répond déjà en millièmes d’em comme le PDF, et GetRawWidth reste disponible quand vous avez besoin des unités de conception. Reste la moitié la plus difficile : passer d’un code de caractère à un index de glyphe. Pour une police TrueType simple, la route dépend du drapeau Symbolic dans le FontDescriptor, bit 3 de /Flags
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// Les fontes symboliques sont adressées directement via la cmap du programme,
// avec la convention (3,0) octet haut en repli
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// Non symbolique : code -> nom de glyphe via l'encodage, nom -> Unicode
// via l'Adobe Glyph List, Unicode -> GID via la cmap du programme
UnicodeValue := GetGlyphUnicode(EncodingNames[Code and $FF]);
if UnicodeValue = 0 then
Continue;
GID := Parser.GetGlyphIndex(UnicodeValue);
end;
if (GID > 0) and (GID < Parser.GlyphCount) then
if Abs(PDFWidth - Parser.GetWidth(GID)) > 1 then
Inc(MismatchCount);
finally
Parser.Free;
end;
Deux détails dans ce fragment portent du poids. La tolérance est d’une unité, pas zéro, car la normalisation est une division entière et un fichier légitimement produit peut atterrir à une unité près ; c’est exactement la formulation « within one thousandth of an em » que rapporte le diagnostic 10036. Et la garde GID < Parser.GlyphCount n’est pas une décoration. GetWidth est écrit pour être indulgent envers les appelants de rendu, ramenant un index hors plage à la dernière entrée de hmtx et retombant sur 750 quand la table est absente. L’indulgence est juste pour le rendu et fausse pour l’audit, donc l’audit rejette l’index avant de demander une largeur au lieu de faire confiance au clamp
CIDFontType2 ajoute une indirection de plus
PDFlibPas parcourt les polices composites de la même façon, avec /CIDToGIDMap inséré entre le CID et le glyphe. Les largeurs arrivent dans le tableau /W, auquel l’ISO 32000-1 §9.7.4.3 donne deux formes qui alternent librement dans un même tableau : un CID de départ suivi d’un tableau de largeurs consécutives, ou un premier CID, un dernier CID, et une seule largeur appliquée sur la plage. L’audit parse les deux, puis remet chaque paire obtenue à la même comparaison, et rapporte le total sous le diagnostic 10037. L’étape de mappage est là où les polices composites diffèrent, et c’est pourquoi le diagnostic 10021 de carte manquante compte avant même de lire une largeur — une /CIDToGIDMap absente ou malformée ne fait pas que violer §7.21.3.2, elle rend la question de la largeur sans réponse
// /CIDToGIDMap est le nom /Identity ou un flux d'indices de glyphes 16 bits
// gros boutien, un par CID (ISO 32000-1 section 9.7.4.2)
Obj := DerefIndRef(FDoc, CIDFont.FindValueByKeyName('CIDToGIDMap'));
if (Obj is TPDFName) and (TPDFName(Obj).Name = 'Identity') then
begin
GID := CID;
Result := True;
end
else if Obj is TPDFStream then
begin
Data := TPDFStream(Obj).GetDecodedStream;
P := CID * 2 + 1; // les chaînes Pascal sont à base 1
if (P >= 1) and (P + 1 <= Length(Data)) then
begin
GID := (Integer(Byte(Data[P])) shl 8) or Integer(Byte(Data[P + 1]));
Result := True;
end;
end;
Que doit faire un auditeur quand le programme de police refuse de se décoder ?
Ne rien dire. Les contrôles de complétude /CharSet et /CIDSet qu’exige l’ISO 14289-1 §7.21.4.2 — diagnostics 10038 et 10039 — sont l’endroit où un validateur trop zélé devient un passif, car un rapport « votre CharSet est incomplet » est indiscernable, pour celui qui le lit, de « notre décodeur Type 1 a abandonné ». PDFlibPas ne signale donc une entrée manquante que quand trois choses réussissent ensemble : le programme de police se décode, le mappage code-vers-glyphe se résout, et l’ensemble lui-même se décode. TPDFType1Decoder.LoadPFBFromString doit renvoyer True et produire un compte de charstrings avant qu’un nom de glyphe soit vérifié contre la chaîne /CharSet ; le chemin /CIDSet exige que le flux se décompresse et que le compte de glyphes revienne positif avant qu’un seul bit soit testé. Toute exception en route se réduit à « aucune constatation », pas à un défaut
C’est un biais délibéré vers les faux négatifs, et il vaut la peine d’être énoncé clairement plutôt qu’enfoui. Une table CFF corrompue, une variante Type 1 non prise en charge, ou une /CIDSet plus courte que la plage de glyphes produisent tous du silence au lieu d’un diagnostic. Le raisonnement est que les audits PDF/UA sont transmis à des auteurs qui n’ont pas construit l’outillage, et une fausse accusation coûte plus cher qu’une constatation manquée : l’auteur brûle une journée à prouver qu’un fichier conforme est conforme, et cesse de faire confiance à tout le rapport. Le Matterhorn Protocol fait la même distinction sous une autre forme quand il sépare les contrôles qu’une machine peut décider de ceux qu’un humain doit, et son point de contrôle Fonts (31) est là où ceux-ci vivent. Si vous avez besoin de la lecture plus stricte, faites tourner PDFlibPas comme barrière rapide et un validateur dédié comme second avis — ce jumelage est celui décrit dans la visite guidée du preflight PDF/A et PDF/UA
Page /Contents est une liste, pas un flux
L’erreur la plus coûteuse en audit de flux de contenu est de traiter /Contents comme un seul flux. L’ISO 32000-1 §7.7.3.3 laisse une page détenir un tableau de flux dont la concaténation, avec des blancs entre les parties, est le programme de la page ; les producteurs coupent à des points arbitraires, et un BT peut se trouver dans un membre avec son ET appariant dans le suivant. Un processeur de contenu garde un état — la profondeur d’imbrication du contenu marqué, la police sélectionnée par le dernier Tf, le drapeau d’objet texte — et Process remet cet état à zéro à l’entrée. Appelez-le une fois par membre du tableau et chaque flux après le premier démarre sans police courante, si bien que du texte parfaitement balisé se lit comme du bruit non balisé et sans police. PDFlibPas concatène d’abord et traite une fois
function ContentObjectData(FDoc: TSmartPDFDocument; Obj: TPDFObject): AnsiString;
var
I: Integer;
begin
Result := '';
Obj := DerefIndRef(FDoc, Obj);
if Obj is TPDFStream then
Result := TPDFStream(Obj).GetDecodedStream
else if Obj is TPDFArray then
for I := 0 to TPDFArray(Obj).Count - 1 do
Result := Result + ContentObjectData(FDoc, TPDFArray(Obj).Item[I]) + #10;
end;
// Un appel Process sur toute la concaténation, jamais un appel par membre
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
Quels Form XObjects comptent réellement comme non structurés ?
Seuls ceux qu’une page invoque réellement, depuis un site d’appel hors contenu marqué, et dont le propre contenu montre du texte. Le diagnostic 10040 fait respecter l’ISO 14289-1 §7.20 en enregistrant trois faits indépendants par numéro d’objet — contient du texte, a été invoqué, a été invoqué à l’intérieur d’un contenu marqué — et ne rapporte que l’intersection des deux premiers moins le troisième. Chacun des deux raccourcis est faux d’une façon que vous livreriez : signaler chaque Form portant du texte dans /Resources punit une bibliothèque de gabarits que personne ne dessine, et signaler chaque Form invoqué punit des logos vectoriels qui ne portent aucun texte et n’ont pas besoin de balisage. Le site d’invocation est résolu par numéro d’objet plutôt que par nom de ressource, puisque la même Form est régulièrement atteinte sous des noms différents sur des pages différentes. Le diagnostic compagnon 10041 parcourt le même programme concaténé pour §7.21.8, résolvant chaque opérateur d’affichage de texte à travers la police en vigueur et comptant les codes qui atterrissent sur .notdef, ce qui est interdit quel que soit le mode de rendu du texte — y compris le mode invisible utilisé derrière des images scannées. La façon dont les Forms survivantes devraient être enveloppées est une question d’arbre de structure, traitée dans l’article sur la construction de structure PDF balisée
Des polices sans aucun FontDescriptor
Une police non embarquée est une entrée légitime de cet audit, pas un état d’erreur, et chaque aide sous le contrôle d’embarquement doit y survivre. Quand PDFlibPas ne trouve aucun /FontDescriptor, ou un descripteur sans FontFile, FontFile2 ni FontFile3, il enregistre le diagnostic 10020 — ou 10022 quand le nom fait partie des Standard 14, que §7.21.4 NOTE 5 refuse pointilleusement d’exempter — puis continue à travers le reste du fichier. C’est tout l’intérêt d’un rapport : un auteur veut chaque constatation en une passe, pas une constatation par exécution. Donc la référence de descripteur remise aux aides largeur, cmap, CharSet et CIDSet peut être Nil, et chacune la teste à l’entrée au lieu de supposer qu’un contrôle antérieur a interrompu l’audit. Si le correctif consiste à embarquer ce qui manque, la mécanique est dans la note sur l’embarquement des polices manquantes dans un PDF existant
Exécuter l’audit
Un appel, sur un fichier que vous n’avez pas nécessairement produit. TPDFlib.CheckFileCompliance prend un sélecteur de test de conformité — 2 pour PDF/UA-1 sous ISO 14289-1:2014 — et renvoie soit zéro, soit un handle de liste de chaînes dont les entrées sont un code numérique, un deux-points, et un message lisible. Les constatations police et flux de contenu discutées ici occupent 10020 à 10041 dans cette plage, tenues à l’écart numériquement des codes PDF/A 00xxx pour qu’un journal mixte reste lisible. Passer 1 dans Options court-circuite à la première constatation, ce que vous voulez dans une barrière de build plutôt que dans un outil d’auteur. Pour un document encore ouvert en mémoire, GetPDFUADiagnostics exécute l’inspection équivalente sans aller-retour par le disque
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2 sélectionne PDF/UA-1 ; Options = 0 rapporte chaque constatation
Issues := PDF.CheckFileCompliance('delivery.pdf', '', 2, 0);
if Issues = 0 then
WriteLn('delivery.pdf: PDF/UA-1 conformant')
else
begin
Count := PDF.GetStringListCount(Issues);
for I := 1 to Count do
WriteLn(' ', PDF.GetStringListItem(Issues, I)); // ex. 10037 CIDFontType2 ...
end;
end;
Rien de tout cela n’exige un binaire validateur externe sur la machine, ce qui est la différence entre un contrôle qui tourne à chaque build et un contrôle qui tourne quand quelqu’un y pense. Les API de conformité et de diagnostic décrites ici arrivent dans la PDFlibPas Delphi PDF Library standard, dont la page produit porte la table complète des codes de diagnostic PDF/UA-1 à côté des suites de tests PDF/A, PDF/X et PDF/E