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
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
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
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
/FontFileet CFF ou OpenType/FontFile3, rapportés commeOnly TrueType FontFile2 programs are supported - Les CIDFonts Identity sans
/W,/W2ni/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
CIDFontType2niTrueTypesimple - Les polices dont le subset manque
SubsetOptions.MinimumSavingPercent, et toutes les polices quand le fichier reconstruit manqueMinimumDocumentSavingPercent; dans ce casReport.AppliedvautFalseet 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