Chaque caractère visible dans un PDF porte une référence à la police qui l'a dessiné, et le composant PDFium vous permet de suivre cette référence jusqu'à l'objet police et de lire ce qu'il sait. L'unité d'accès est le caractère, pas le document : vous choisissez un caractère par son index dans le texte de la page et demandez le nom de famille, le nom de base, le poids, l'angle d'italique et si la police sous-jacente (underlying face) est réellement transportée à l'intérieur du fichier. Cette dernière propriété est celle que la plupart des analyses recherchent vraiment, car une police intégrée (embedded) voyage avec le document et une police non intégrée est une promesse que la machine du lecteur possède par hasard la même police de caractères installée
Le composant les expose à travers les mêmes objets TPdf et TPdfView que vous utilisez pour le rendu et l'extraction de texte. Il n'y a pas d'objet "table de polices" séparé à ouvrir. Une fois le texte d'une page analysé, les propriétés de police dépendent (hang off) de l'index de caractère, et vous les lisez un glyphe à la fois. Cette conception correspond à la façon dont le PDF stocke l'information en premier lieu : une seule page peut changer de police des dizaines de fois, et la seule réponse honnête à "dans quelle police ce document est-il" est "cela dépend du caractère dont vous parlez."
Lecture de la police derrière un caractère
La plus petite opération utile consiste à prendre un index de caractère et à vider (dump) tout ce que PDFium peut vous dire sur sa police. Chaque propriété de police sur TPdf et TPdfView est indexée par la position du caractère, de sorte que l'index les traverse toutes (threads through all of them). La page doit également être la page courante pour que l'index se résolve par rapport au bon texte, ce qui compte une fois que vous dépassez la première page
procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
Report: TStringList;
PtSize: Single;
begin
Report := TStringList.Create;
try
PtSize := Pdf.FontSize[CharIndex];
Report.Add('Caractère : ' + Pdf.Character[CharIndex]);
Report.Add('Famille : ' + Pdf.FontFamilyName[CharIndex]);
Report.Add('Nom de base : ' + Pdf.FontBaseName[CharIndex]);
Report.Add('Poids : ' + IntToStr(Pdf.FontWeight[CharIndex]));
Report.Add('Italique : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
Report.Add('Taille : ' + FormatFloat('0.0', PtSize) + ' pt');
Report.Add('Montée : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
Report.Add('Descente : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
Report.Add('Intégrée : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));
ShowMessage(Report.Text);
finally
Report.Free;
end;
end;
Quelques-unes des signatures surprennent les personnes venant d'autres bibliothèques. FontAscent et FontDescent prennent deux arguments, l'index du caractère et une taille en points, car PDFium rapporte ces métriques dans des unités d'espace de glyphes qui ne deviennent des pixels qu'une fois que vous les mettez à l'échelle (scale them) par la taille à laquelle le texte a été défini. Passez la valeur que vous avez déjà lue dans FontSize[CharIndex] et vous obtenez la montée (ascent) et la descente (descent) dans les mêmes points que le reste de la disposition. La descente revient négative, car elle mesure sous la ligne de base. Le nom de famille et le nom de base sont des chaînes séparées à dessein : le nom de base est l'entrée brute /BaseFont du PDF, portant souvent un préfixe de sous-ensemble (subset prefix) comme ABCDEF+, tandis que le nom de famille est le nom nettoyé vers lequel le moteur de rendu le résout
Transformer un clic en un index de caractère
Dans une visionneuse, vous connaissez rarement l'index à l'avance. L'utilisateur clique sur un glyphe et vous devez traduire la coordonnée de pixel en caractère qui se trouve en dessous. CharacterIndexAtPos fait exactement cela, en prenant la position de la souris et une tolérance et en renvoyant l'index du caractère le plus proche, ou une valeur négative lorsque le clic a atterri sur un espace blanc ou une page vide
procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
Index: Integer;
begin
if not PdfView.Active then
Exit;
// 4 px de jeu (slack) dans chaque direction pour qu'un clic manqué de peu touche quand même le glyphe.
Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
if Index < 0 then
Exit; // cliqué entre les glyphes ; laissez le panneau tranquille
PdfView.CurrentCharIndex := Index;
DescribeFontAt(PdfView.Pdf, Index);
end;
La tolérance vaut la peine d'être ajustée. Trop serrée, les utilisateurs ont l'impression de devoir atterrir sur la tige exacte (stem) d'une lettre ; trop lâche, et un clic dans une marge s'accroche (snaps) à un caractère lointain qui n'a rien à voir avec ce qu'ils voulaient dire. Trois à cinq pixels d'appareil est un point de départ raisonnable pour la visualisation à l'écran. L'index renvoyé est dans le texte analysé de la page courante, le même espace d'index que toutes les propriétés de police attendent, vous pouvez donc le passer directement à la routine ci-dessus. Le stocker dans CurrentCharIndex est facultatif mais pratique : la vue conserve cela comme sa notion du glyphe mis au point (focused glyph), ce qui est pratique si d'autres parties de l'interface utilisateur veulent lire la sélection sans la re-dériver
L'intégration (Embedding) est la propriété qui compte
Pour la plupart des travaux réels, la seule question à laquelle il vaut la peine de répondre est de savoir si chaque police est intégrée. Un document dont les polices voyagent toutes à l'intérieur s'affiche de la même manière sur le RIP d'un bureau d'impression, sur l'ordinateur portable d'un collègue et sur un serveur sans interface graphique du tout. Un document qui s'appuie sur un Helvetica non intégré fait le pari que chacune de ces machines possède une police correspondante, et lorsque le pari échoue, le lecteur la remplace par quelque chose de proche, les métriques changent (metrics shift), et un formulaire soigneusement mis en page (laid-out) se redistribue (reflows) juste assez pour se casser. Parcourir le texte de la page et regrouper (bucketing) les polices en fonction de leur statut d'intégration vous donne cette réponse à moindre coût
procedure ReportNonEmbeddedFonts(Pdf: TPdf);
var
Embedded, External: TStringList;
I: Integer;
Name: string;
begin
Embedded := TStringList.Create;
External := TStringList.Create;
try
Embedded.Sorted := True;
Embedded.Duplicates := dupIgnore;
External.Sorted := True;
External.Duplicates := dupIgnore;
for I := 0 to Pdf.CharacterCount - 1 do
begin
Name := Pdf.FontBaseName[I];
if Name = '' then
Continue; // les espaces générées et autres n'ont pas de police
if Pdf.FontIsEmbedded[I] then
Embedded.Add(Name)
else
External.Add(Name);
end;
if External.Count > 0 then
ShowMessage(IntToStr(External.Count) +
' police(s) non intégrée(s) :' + sLineBreak + External.Text)
else
ShowMessage('Toutes les ' + IntToStr(Embedded.Count) +
' police(s) de cette page sont intégrées.');
finally
Embedded.Free;
External.Free;
end;
end;
Deux détails gardent cela honnête. Premièrement, CharacterCount est par page, donc un audit de l'ensemble du document signifie définir Pdf.PageNumber sur chaque page à son tour et relancer la boucle, en fusionnant les résultats. Deuxièmement, la couche de texte contient des caractères générés tels que les espaces qu'un lecteur déduit entre les mots, et ceux-ci n'ont pas d'objet police derrière eux ; la vérification du nom de base vide les ignore plutôt que d'enregistrer un fantôme. Le nom de base est la bonne clé pour la déduplication ici, car le préfixe de sous-ensemble qu'il porte distingue deux sous-ensembles différents de la même famille, ce qui est généralement ce que vous voulez savoir
Extraction de la police intégrée
Lorsqu'une police est intégrée, vous pouvez lire ses octets directement. FontData renvoie le programme de police brut, les mêmes données TrueType ou CFF que le PDF transporte, ce qui est suffisant pour écrire un fichier de police autonome ou pour prendre l'empreinte digitale (fingerprint) de la police par rapport à une bibliothèque connue. Il renvoie un tableau vide lorsque la police n'est pas intégrée, de sorte que la vérification d'intégration et la vérification de longueur protègent ensemble l'écriture
procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
const OutputFile: string);
var
Data: TBytes;
Stream: TFileStream;
begin
if not Pdf.FontIsEmbedded[CharIndex] then
begin
ShowMessage('La police de ce glyphe n'est pas intégrée ; rien à extraire.');
Exit;
end;
Data := Pdf.FontData[CharIndex];
if Length(Data) = 0 then
Exit;
Stream := TFileStream.Create(OutputFile, fmCreate);
try
Stream.WriteBuffer(Data[0], Length(Data));
finally
Stream.Free;
end;
ShowMessage('A écrit ' + IntToStr(Length(Data)) + ' octets.');
end;
Les octets constituent le sous-ensemble intégré, et non la police de vente (retail font) d'origine, donc ce que vous récupérez ne couvre généralement que les glyphes réellement utilisés par le document. C'est exactement ce qu'il faut pour la criminalistique (forensics) et la vérification, et c'est inadapté (poor fit) pour la réutilisation ; un sous-ensemble de Times New Roman qui contient trente glyphes n'est pas une police que vous pouvez installer et utiliser pour taper. Considérez l'extraction comme un moyen d'inspecter ce qui a été expédié, et non comme un outil de récupération de polices. Si vous avez besoin du nom de base correspondant pour étiqueter la sortie, lisez FontBaseName[CharIndex] à côté des données et supprimez la balise de sous-ensemble de début si vous voulez la famille nue (bare family)
Comprendre le nombre de poids (weight number)
FontWeight renvoie la classe de poids numérique, la même échelle de 100 à 900 que celle utilisée par CSS, où 400 correspond à regular et 700 à bold (gras). PDFium signale ce que la police déclare, ce qui n'est pas toujours une centaine ronde ; une police peut annoncer 350 ou 650, et traiter tout ce qui est égal ou supérieur à 600 comme "assez gras pour avoir de l'importance (bold enough to matter)" tient mieux la route que de tester exactement 700. L'angle d'italique est un signal d'accompagnement : une valeur non nulle, généralement négative, signifie que la police a une conception oblique ou véritablement italique, et zéro signifie droite (upright). Ensemble, ils vous permettent de distinguer une exécution (run) en gras-italique d'une exécution régulière sans rien rendre, ce qui est le genre de vérification qu'une passe de contrôle en amont (preflight pass) ou un audit d'accessibilité veut faire en masse (in bulk)
Aucune de ces lectures ne nécessite un bitmap rendu. Elles proviennent de la couche de texte analysée, donc un document ouvert sur la bonne page est toute la configuration (setup) dont vous avez besoin, ce qui rend l'inspection des polices peu coûteuse (cheap) à exécuter sur une archive entière. Si vous associez cela à l'extraction de texte, les mêmes index de caractères s'alignent avec le texte que vous extrayez, de sorte que la police d'un glyphe et sa valeur Unicode sont deux lectures par rapport à un index. L'article complémentaire sur l'extraction de texte à partir de documents PDF avec le composant PDFium couvre cet aspect de la couche de texte plus en profondeur
Les propriétés de police présentées ici font partie du Composant Delphi VCL PDFium