Article technique

Subset des polices déjà incorporées dun PDF en Delphi

PDFium Component fait un subset des polices déjà incorporées dans un PDF existant via SubsetPdfDocumentFonts dans lunité FPdfDocumentFontSubset. La fonction remplace un programme TrueType /FontFile2 incorporé par un plus petit seulement quand les ressources de police elles-mêmes prouvent chaque glyphe que le document peut référencer ; une police dont le domaine de glyphes ne peut pas être prouvé est laissée exactement comme elle était

La raison habituelle de vouloir cela est une archive que vous navez pas produite. Des factures, relevés et rapports de générateurs plus anciens portent souvent une Arial complète ou une fonte CJK entière dans chaque fichier, et quelques centaines de kilooctets de police par document de deux pages saccumulent vite sur quelques millions de documents. Rendre à nouveau depuis la source est rarement une option. Ce que vous avez, cest le PDF, et la seule question sûre est de savoir quelle part de chaque police incorporée peut partir sans quune seule page ne perde un caractère

Pourquoi faire un subset dun PDF existant risque-t-il de perdre des glyphes ?

Faire un subset dun PDF existant risque de perdre des glyphes parce que le post-processeur ne sait pas quels glyphes le rédacteur dorigine a employés. Le producteur qui a incorporé la police connaissait son propre texte ; un outil qui tourne ensuite doit reconstruire ce savoir, et tout glyphe quil rate devient une boîte vide ou une forme .notdef sur une page que personne ne regarde, jusquà ce quun client le fasse

Lapproche évidente est danalyser chaque flux de contenu, chaque Form XObject, chaque apparence dannotation et chaque apparence par défaut de champ, de collecter les codes montrés avec chaque police, et de garder ces glyphes. Ça marche jusquà ce que ça rencontre une ressource de police partagée entre pages et formes, un champ de formulaire dont une visionneuse régénère lapparence depuis /DA, ou un flux de contenu avec une séquence dopérateurs que le balayeur ne modélise pas. SubsetPdfDocumentFonts ne balaye délibérément pas les flux de contenu. Il dérive le domaine de glyphes des métadonnées que le PDF doit de toute façon porter sur chaque police : le /CIDToGIDMap, les tableaux de largeurs /W et /W2, le bitmap /CIDSet, la CMap /ToUnicode, le tableau /Widths de police simple et le /Encoding. Quand ces métadonnées ne bornent pas le domaine, la police reste intacte

Comment PDFium Component prouve le domaine de glyphes en faisant un subset des polices TrueType incorporées depuis Delphi : /CIDToGIDMap, largeurs /W et /W2, /CIDSet, /ToUnicode, /Widths et /Encoding bornent le domaine sans balayer les flux de contenu, et une police improuvable est préservée intacte
SubsetPdfDocumentFonts transforme les métadonnées de police en un domaine de glyphes prouvé avant de couper quoi que ce soit, et une police dont les ressources ne peuvent pas borner le domaine est rapportée et laissée intacte

Garder les IDs de glyphes dorigine pour que les flux de contenu restent intacts

Le backend de subset derrière SubsetPdfDocumentFonts, TPdfTrueTypeSubsetBackend, garde chaque logement dID de glyphe dorigine, si bien quaucun flux de contenu, CID ni /CIDToGIDMap na jamais à être réécrit. Il reconstruit les tables glyf et loca, donne à chaque glyphe inutilisé une plage loca de longueur nulle et laisse maxp.numGlyphs inchangé. Le glyphe 4711 reste le glyphe 4711 après le subset ; il a juste des contours seulement si le document peut latteindre

Ce choix retire la partie la plus risquée du subset dun document fini. Renuméroter les glyphes voudrait dire réécrire chaque chaîne de chaque flux de contenu qui emploie la police, plus le flux de mappage, plus tout ce qui met en cache des IDs de glyphes. Les IDs retenus coûtent un peu de taille, parce que loca garde une entrée par glyphe dorigine, mais lessentiel dun programme TrueType est données de contours, et cest ça qui part. Les glyphes composites sont suivis vers leurs composantes, si bien quune lettre accentuée garde la lettre de base et laccent dont elle est bâtie ; des références composites cycliques ou trop profondes font échouer la police au lieu de produire une fermeture cassée

IDs de glyphes retenus après un subset des polices TrueType incorporées avec PDFium Component en Delphi : le glyphe 4711 garde son numéro et ses contours, les glyphes inutilisés reçoivent des plages loca de longueur nulle tandis que maxp.numGlyphs reste inchangé, si bien que flux de contenu, CIDs et /CIDToGIDMap ne sont jamais réécrits
Comme les IDs de glyphes survivent au subset, flux de contenu, CIDs et le /CIDToGIDMap nexigent jamais de réécriture - le gain de taille vient des contours abandonnés, pas dune renumérotation

La police renommée suit aussi ISO 32000-1 §9.6.4, qui exige quun nom de police subset commence par six lettres majuscules et un signe plus. Le composant dérive les six lettres dun digest SHA-256 du nouveau programme de police, retire dabord tout préfixe ABCDEF+ existant, résout les collisions entre polices du même document, et écrit le nouveau nom à la fois dans le /FontName du descripteur et le /BaseFont du dictionnaire de police, y compris le parent Type0 dun CIDFont

uses
  System.SysUtils, System.Classes, FPdfDocumentFontSubset;

procedure SubsetEmbeddedFonts(const SourceName, TargetName: string);
var
  Source, Target: TFileStream;
  Options: TPdfDocumentFontSubsetOptions;
  Report: TPdfDocumentFontSubsetReport;
  Output: TBytes;
begin
  Options := TPdfDocumentFontSubsetOptions.Default;
  Source := TFileStream.Create(SourceName, fmOpenRead or fmShareDenyWrite);
  try
    if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
      raise Exception.Create('Font subsetting failed: ' + Report.ErrorMessage);
  finally
    Source.Free;
  end;
  // La sortie est toujours un PDF complet : le fichier reconstruit quand Report.Applied
  // vaut True, sinon une copie octet pour octet de la source
  Target := TFileStream.Create(TargetName, fmCreate);
  try
    if Length(Output) > 0 then
      Target.WriteBuffer(Output[0], Length(Output));
  finally
    Target.Free;
  end;
end;

Comment SubsetPdfDocumentFonts prouve-t-il le domaine de glyphes dun CIDFont ?

SubsetPdfDocumentFonts prouve le domaine de glyphes dune police CIDFontType2 depuis son mappage CID-vers-glyphe, et le mappage décide de la preuve supplémentaire nécessaire. Un flux /CIDToGIDMap explicite (ISO 32000-1 §9.7.4.2) est déjà une liste de chaque glyphe que la police peut adresser, si bien que chaque GID du flux rejoint le domaine. Un flux de mappage de plus de 65536 entrées, ou avec un compte doctets impair, échoue au profil borné et la police est préservée

/CIDToGIDMap /Identity est le cas difficile, et cest aussi le plus courant dans les fichiers modernes. Identity dit que CID égale GID, ce qui ne vous dit rien des CIDs que le document emploie. Le composant exige donc une preuve primaire : un tableau de largeurs /W ou /W2 explicite (§9.7.4.3), dont les entrées CID marquent les glyphes pour lesquels le producteur a déclaré des métriques, ou un flux /CIDSet dans le descripteur de police (§9.8.3), un bitmap dun bit par CID, bit de poids fort en premier. Une police Identity nue sans ni lun ni lautre est préservée avec la raison Identity CIDToGIDMap requires an explicit W array or CIDSet stream

Le piège à employer /W comme preuve est /DW. Un producteur a le droit de laisser de côté chaque CID dont la largeur égale la largeur par défaut, et un rédacteur bien optimisé fait exactement cela pour les largeurs de glyphes les plus courantes. Si le domaine venait de /W seul, ces glyphes disparaîtraient silencieusement du subset. Pour combler ce trou, le composant parcourt chaque police Type0 dont /DescendantFonts pointe le CIDFont, exige que son /Encoding soit /Identity-H ou /Identity-V pour quun code de 2 octets soit le CID, et fusionne dans le domaine les codes sources de chaque entrée beginbfchar et beginbfrange de sa CMap /ToUnicode (§9.10.3). Tout autre encodage, ou un flux /ToUnicode qui ne peut pas être décodé dans le budget, préserve la police

Décision de preuves pour un CIDToGIDMap Identity dans le subset de polices PDFium Component pour Delphi : un flux de mappage explicite liste déjà chaque glyphe, Identity exige un tableau de largeurs /W ou /W2 ou un bitmap /CIDSet, et les codes sources /ToUnicode comblent langle mort de largeur par défaut /DW
Un CIDToGIDMap Identity ne dit rien de lusage de lui-même, si bien que le sous-ensembleur exige des preuves /W ou /CIDSet et fusionne les codes /ToUnicode pour couvrir les glyphes que /W omet à /DW

Polices TrueType simples : des Widths aux IDs de glyphes

Les polices /Subtype /TrueType à octet simple sont prouvées par leur plage de codes et leur encodage, puis résolues vers des IDs de glyphes au travers du cmap propre du programme de police. Le composant exige 0 <= FirstChar <= LastChar <= 255 et un tableau /Widths dont la longueur vaut exactement LastChar - FirstChar + 1 ; un tableau court, long ou non numérique signifie que le domaine de codes nest pas prouvé, et la police reste telle quelle

Chaque code de cette plage, plus tout code nommé dans /Differences ou couvert par /ToUnicode, est ensuite mappé comme le décrit ISO 32000-1 §9.6.6. Les codes sous /WinAnsiEncoding, /MacRomanEncoding ou /StandardEncoding passent par la table correspondante vers Unicode. Les noms de glyphes de /Differences sont résolus au travers de lAdobe Glyph List, des noms uniXXXX et uXXXX, des 258 noms de glyphes Macintosh standards et de la table de noms post format 2.0 propre de la police. Les valeurs résultantes sont cherchées dans des sous-tables cmap de formats 0, 4, 6 et 12, y compris Unicode (3,1), (3,10) et (0,*), Mac Roman (1,0), et Windows Symbol (3,0) à F000 plus le code

Quand une police na aucun encodage de base du tout, le composant marque lunion de toute interprétation plausible au lieu den deviner une. Cela garde quelques glyphes probablement inutilisés, la bonne direction dans laquelle se tromper. Le cas opposé est strict : un nom /Differences qui ne mappe vers aucun ID de glyphe prouvé rejette la police entière, parce quun nom non résolu signifie que le domaine est incomplet

procedure ReportFontSubset(Source: TStream; Log: TStrings);
var
  Options: TPdfDocumentFontSubsetOptions;
  Report: TPdfDocumentFontSubsetReport;
  Output: TBytes;
  I: Integer;
begin
  Options := TPdfDocumentFontSubsetOptions.Default;
  Options.SubsetOptions.MinimumSavingPercent := 10;  // par police
  Options.MinimumDocumentSavingPercent := 5;         // fichier entier
  if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
  begin
    Log.Add('Rejected: ' + Report.ErrorMessage);
    Exit;
  end;
  Log.Add(Format('Fonts %d, subset %d, preserved %d, glyphs %d',
    [Report.FontCount, Report.SubsetFontCount, Report.PreservedFontCount,
     Report.GlyphCount]));
  Log.Add(Format('Font bytes %d -> %d, file bytes %d -> %d',
    [Report.OriginalFontBytes, Report.SubsetFontBytes,
     Report.SourceByteCount, Report.OutputByteCount]));
  for I := 0 to High(Report.Issues) do
    Log.Add(Format('  object %d kept: %s',
      [Report.Issues[I].ObjectNumber, Report.Issues[I].Reason]));
end;

Que laisse SubsetPdfDocumentFonts de côté ?

SubsetPdfDocumentFonts laisse de côté chaque police quil ne peut pas prouver, et il rapporte chacune dans Report.Issues avec le numéro dobjets et une raison au lieu de faire échouer le document. Report.Complete ne vaut True que quand aucune police na été préservée. Les cas préservés valent dêtre connus avant de promettre une réduction de taille :

  • Les programmes Type 1 /FontFile et CFF ou OpenType /FontFile3, rapportés comme Only TrueType FontFile2 programs are supported
  • Les CIDFonts Identity sans /W, /W2 ni /CIDSet, et les CIDFonts sans flux de mappage ni /Identity
  • Les programmes de police référencés de quelque part autre que leurs descripteurs de police, et les descripteurs partagés avec un consommateur qui nest pas CIDFontType2 ni TrueType simple
  • Les polices dont le subset manque SubsetOptions.MinimumSavingPercent, et toutes les polices quand le fichier reconstruit manque MinimumDocumentSavingPercent ; dans ce cas Report.Applied vaut False et la sortie est les octets dorigine

Deux entrées échouent carrément au lieu dêtre préservées. Les documents signés et les documents avec champs de signature sont rejetés, parce que remplacer les objets de police invaliderait lintervalle doctets de chaque signature, et les fichiers chiffrés exigent une source déchiffrée dabord. Une exécution réussie écrit les objets de remplacement comme une mise à jour creuse puis reconstruit le fichier au travers de la même passe daccessibilité décrite dans le guide délaguation dobjets orphelins et de déduplication de ressources, si bien que les vieux programmes de police ne survivent pas comme objets inaccessibles et le résultat est une seule révision propre

Faire un subset du nouveau texte à la sauvegarde avec saSubsetNewFonts

Les polices que PDFium Component incorpore pendant la session courante sont un cas séparé, géré par loption de sauvegarde saSubsetNewFonts plutôt que par SubsetPdfDocumentFonts. saSubsetNewFonts correspond au drapeau PDFium FPDF_SUBSET_NEW_FONTS, qui fait tourner le sous-ensembleur HarfBuzz natif sur les polices que des appels natifs tels que TPdf.AddText ont chargées du système, y compris les fontes OpenType CFF. PDFium en amont étiquette toujours le drapeau expérimental. Il ne touche pas les polices qui étaient déjà dans le fichier

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    Pdf.AddPage(0, 595, 842);
    Pdf.AddText('Quarterly statement', 'Arial', 14, 72, 770, clBlack, $FF, 0.0);
    if not Pdf.SaveAs('statement.pdf', saSubsetNewFonts) then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

Dans la sonde du composant, un document avec du texte Arial est tombé de 590 Ko à 22 Ko sous saSubsetNewFonts, et lextraction de texte du fichier sauvegardé correspondait toujours exactement. Une police couleur COLR v0, Segoe UI Emoji à 7,0 Mo, est passée par le même chemin pour 10,5 Ko, rendue comme ses contours de base puisque le PDF na aucune notion de couches COLR. Les polices variables sont lexception, et le bloc se tient devant le sous-ensembleur : GDI GetFontData a renvoyé un flux de substitution de 18,3 Mo pour le fichier Segoe UI Variable de 1,85 Mo, et la création dobjets de texte a échoué avant que le moindre subset commence. Le parcours de lincorporation de polices CID Type 2 depuis Delphi couvre le chemin dincorporation que ces polices empruntent dabord

Où la preuve sarrête

La preuve à lintérieur de SubsetPdfDocumentFonts est exactement aussi forte que les métadonnées de police que le producteur a écrites. Un CIDFont Identity dont la preuve est /W seul garde encore un angle mort : un glyphe dont la largeur égale /DW, qui na pas dentrée /ToUnicode, et que le descripteur ne liste pas dans un /CIDSet. Un tel glyphe nest pas prouvable depuis les ressources, et le composant ne peut pas savoir quil existe sans le balayage de flux de contenu quil évite exprès. Les producteurs qui omettent à la fois /ToUnicode et /CIDSet tout en compressant /W sont rares, mais ils existent, donc pour une archive venue dun générateur inconnu, rendez un échantillon avant et après et comparez pixels et texte extrait

Avant de lancer un lot, cela aide aussi de savoir à quoi vous avez affaire. Lapproche de lecture des propriétés de polices PDF avec PDFium Component vous dit quelles polices sont incorporées du tout, et Report.Issues dune passe à blanc vous dit lesquelles seront préservées et pourquoi. Les options par défaut bornent aussi le travail : 1024 polices, environ un million de glyphes au total, 128 Mo de source et un budget de travail fixe, avec un CancellationToken optionnel pour les longs travaux par lots

FAQ

SubsetPdfDocumentFonts change-t-il le texte ou la mise en page du PDF ?

Non. Les IDs de glyphes dorigine, CIDs, largeurs et flux de contenu sont gardés, si bien que lextraction de texte et le rendu emploient les mêmes glyphes quavant ; seuls les contours inaccessibles sont retirés et les noms de police reçoivent un nouveau préfixe de subset

Pourquoi une police incorporée na-t-elle pas été mise en subset ?

Lisez Report.Issues. Chaque police préservée porte une raison telle quun /W ou /CIDSet manquant sur un CIDFont Identity, un programme non TrueType, un tableau /Widths mal formé ou une économie sous le seuil

saSubsetNewFonts est-il la même chose que SubsetPdfDocumentFonts ?

Non. saSubsetNewFonts fait un subset des polices incorporées dans la session courante quand vous sauvegardez, tandis que SubsetPdfDocumentFonts travaille sur un flux PDF existant et remplace les polices déjà incorporées par un autre producteur

Le subset de polices de document, le subset de polices à la sauvegarde et la passe délaguation dobjets orphelins derrière eux sont tous livrés avec le PDFium Component for Delphi and C++Builder, qui enveloppe PDFium pour des projets Delphi, C++Builder et Lazarus