Elk zichtbaar teken in een PDF draagt een verwijzing naar het lettertype dat het heeft getekend, en met de PDFium-component kunt u die verwijzing volgen naar het lettertype-object en uitlezen wat het weet. De toegangseenheid is het teken, niet het document: u kiest een teken op basis van zijn index in de paginatekst en vraagt naar de familienaam, de basisnaam, het gewicht (weight), de cursieve hoek (italic angle) en of het onderliggende lettertype daadwerkelijk in het bestand aanwezig is (embedded). Die laatste eigenschap is de eigenschap waar de meeste analyses echt naar op zoek zijn, want een ingesloten lettertype reist met het document mee, terwijl een niet-ingesloten lettertype een belofte is dat de machine van de lezer toevallig hetzelfde lettertype heeft geïnstalleerd
De component stelt deze beschikbaar via dezelfde TPdf- en TPdfView-objecten die u gebruikt voor het renderen en extraheren van tekst. Er is geen apart "font table"-object om te openen. Zodra de tekst van een pagina is geparseerd, hangen de lettertype-eigenschappen af van de tekenindex, en u leest ze één teken (glyph) tegelijk. Dat ontwerp sluit aan bij hoe PDF de informatie in de eerste plaats opslaat: één enkele pagina kan tientallen keren van lettertype wisselen, en het enige eerlijke antwoord op de vraag "in welk lettertype is dit document geschreven" is "dat hangt af van welk teken u bedoelt."
Het lettertype achter één teken lezen
De kleinste nuttige bewerking is om een tekenindex te nemen en alles te dumpen wat PDFium u kan vertellen over het lettertype ervan. Elke lettertype-eigenschap in TPdf en TPdfView is geïndexeerd op basis van tekenpositie, dus de index loopt als een rode draad door al deze eigenschappen. De pagina moet ook de huidige pagina zijn om de index af te zetten tegen de juiste tekst. Dit is belangrijk zodra u verdergaat dan pagina één
procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
Report: TStringList;
PtSize: Single;
begin
Report := TStringList.Create;
try
PtSize := Pdf.FontSize[CharIndex];
Report.Add('Character : ' + Pdf.Character[CharIndex]);
Report.Add('Family : ' + Pdf.FontFamilyName[CharIndex]);
Report.Add('Base name : ' + Pdf.FontBaseName[CharIndex]);
Report.Add('Weight : ' + IntToStr(Pdf.FontWeight[CharIndex]));
Report.Add('Italic : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
Report.Add('Size : ' + FormatFloat('0.0', PtSize) + ' pt');
Report.Add('Ascent : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
Report.Add('Descent : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
Report.Add('Embedded : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));
ShowMessage(Report.Text);
finally
Report.Free;
end;
end;
Een aantal van de handtekeningen (signatures) verrast mensen die afkomstig zijn van andere bibliotheken. FontAscent en FontDescent accepteren twee argumenten, de tekenindex en een puntgrootte (point size). PDFium rapporteert deze metrics namelijk in glyph-space eenheden die pas pixels worden zodra u ze schaalt met de grootte waarop de tekst is ingesteld. Geef de waarde door die u al hebt uitgelezen uit FontSize[CharIndex] en u krijgt ascent en descent in dezelfde punten (points) als de rest van de lay-out. Descent komt negatief terug, omdat dit onder de basislijn (baseline) wordt gemeten. De familienaam en de basisnaam zijn bewust afzonderlijke strings: de basisnaam is het onbewerkte /BaseFont-item uit de PDF, dat vaak een subset-voorvoegsel zoals ABCDEF+ bevat, terwijl de familienaam de opgeschoonde naam is waarnaar de renderer het herleidt
Een klik omzetten in een tekenindex
In een viewer kent u zelden vooraf de index. De gebruiker klikt op een teken en u moet de pixelcoördinaat vertalen naar het teken eronder. CharacterIndexAtPos doet precies dat: het neemt de muispositie en een tolerantie (tolerance) en retourneert de index van het dichtstbijzijnde teken, of een negatieve waarde wanneer de klik op witruimte of een lege pagina belandde
procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
Index: Integer;
begin
if not PdfView.Active then
Exit;
// 4 px of slack in each direction so a near-miss still hits the glyph.
Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
if Index < 0 then
Exit; // clicked between glyphs; leave the panel alone
PdfView.CurrentCharIndex := Index;
DescribeFontAt(PdfView.Pdf, Index);
end;
De tolerantie is de moeite waard om af te stellen. Te krap (tight) en gebruikers hebben het gevoel dat ze op de exacte stam (stem) van een letter moeten landen; te los en een klik in een marge springt (snaps) naar een verafgelegen teken dat niets te maken heeft met wat ze bedoelden. Drie tot vijf apparaatpixels is een redelijk startpunt voor schermweergave. De geretourneerde index bevindt zich in de geparseerde tekst van de huidige pagina, dezelfde indexruimte die elke lettertype-eigenschap verwacht, dus u kunt deze rechtstreeks aan de bovenstaande routine doorgeven. Het opslaan in CurrentCharIndex is optioneel maar handig: de weergave (view) bewaart dat als de notie van het gefocuste teken. Dat is handig als andere delen van de gebruikersinterface de selectie willen lezen zonder deze opnieuw af te leiden
Insluiting (Embedding) is de eigenschap die ertoe doet
Voor het meeste echte werk is de enige vraag die het beantwoorden waard is of elk lettertype is ingesloten (embedded). Een document waarvan alle lettertypen zich erin bevinden, rendert overal hetzelfde: op de RIP van een printservice, op de laptop van een collega en op een server zonder enige GUI (grafische gebruikersinterface). Een document dat leunt op een niet-ingesloten Helvetica gokt erop dat elk van die machines een bijpassend lettertype heeft. Wanneer de gok faalt, vervangt de reader het door iets vergelijkbaars, verschuiven de metrics en reflowt een zorgvuldig opgemaakt formulier net genoeg om stuk te gaan. Door de paginatekst te doorlopen en de lettertypen in te delen op basis van insluitingsstatus (embedding status), krijgt u op een goedkope manier het antwoord
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; // generated spaces and the like have no font
if Pdf.FontIsEmbedded[I] then
Embedded.Add(Name)
else
External.Add(Name);
end;
if External.Count > 0 then
ShowMessage(IntToStr(External.Count) +
' non-embedded font(s):' + sLineBreak + External.Text)
else
ShowMessage('All ' + IntToStr(Embedded.Count) +
' font(s) on this page are embedded.');
finally
Embedded.Free;
External.Free;
end;
end;
Twee details houden dit eerlijk. Ten eerste is CharacterCount per pagina, dus een audit van het hele document betekent dat Pdf.PageNumber voor elke pagina moet worden ingesteld en de loop (lus) opnieuw moet worden uitgevoerd, waarbij de resultaten worden samengevoegd. Ten tweede bevat de tekstlaag gegenereerde tekens zoals de spaties die een reader afleidt (infers) tussen woorden, en daarachter zit geen lettertype-object; de controle op een lege basisnaam (empty-base-name) slaat deze over in plaats van een fantoom te loggen. De basisnaam is hier de juiste sleutel voor ontdubbeling (de-duplication) omdat het subset-voorvoegsel (subset prefix) dat deze draagt, onderscheid maakt tussen twee verschillende subsets van dezelfde familie, wat meestal is wat u wilt weten
Het ingesloten lettertype eruit halen
Wanneer een lettertype is ingesloten, kunt u de bytes rechtstreeks lezen. FontData retourneert het onbewerkte (raw) lettertypeprogramma, dezelfde TrueType- of CFF-gegevens die de PDF bij zich draagt. Dit is voldoende om een stand-alone lettertypebestand te schrijven of om een 'vingerafdruk' van het lettertype te maken ten opzichte van een bekende bibliotheek. Het retourneert een lege array wanneer het lettertype niet is ingesloten, dus de controle op insluiting (embedding check) en de lengtecontrole bewaken samen de schrijfactie
procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
const OutputFile: string);
var
Data: TBytes;
Stream: TFileStream;
begin
if not Pdf.FontIsEmbedded[CharIndex] then
begin
ShowMessage('That glyph''s font is not embedded; nothing to extract.');
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('Wrote ' + IntToStr(Length(Data)) + ' bytes.');
end;
De bytes zijn de ingesloten subset, niet het oorspronkelijke commerciële lettertype. Wat u terugkrijgt, omvat dus meestal alleen de tekens (glyphs) die het document daadwerkelijk heeft gebruikt. Dat is precies goed voor forensisch onderzoek en verificatie, en een slechte keuze voor hergebruik; een subset van Times New Roman met dertig tekens is geen lettertype dat u kunt installeren en waarmee u kunt typen. Beschouw extractie als een manier om te inspecteren wat er is meegeleverd, niet als een tool voor het herstellen van lettertypen. Als u de overeenkomende basisnaam nodig hebt om de uitvoer te labelen, lees dan FontBaseName[CharIndex] in combinatie met de gegevens en strip de voorafgaande subset-tag (leading subset tag) als u de kale familie wilt hebben
De betekenis van het gewichtsgetal (weight number)
FontWeight retourneert de numerieke gewichtsklasse (weight class), dezelfde 100-tot-900-schaal die CSS gebruikt, waarbij 400 'regular' (normaal) is en 700 'bold' (vetgedrukt). PDFium rapporteert wat het lettertype declareert, en dat is niet altijd een rond honderdtal; een lettertype kan 350 of 650 aankondigen. Het behandelen van alles op of boven 600 als "vet genoeg om er toe te doen" (bold enough to matter) werkt beter dan testen op exact 700. De cursieve hoek (italic angle) is een begeleidend signaal: een waarde ongelijk aan nul, meestal negatief, betekent dat het gezicht (face) een schuin of echt cursief ontwerp is, en nul betekent rechtop (upright). Samen stellen ze u in staat om een vet-cursieve serie te onderscheiden van een normale, zonder iets te renderen. Dit is het soort controle dat een preflight-passage of een toegankelijkheidsaudit (accessibility audit) in bulk wil uitvoeren
Voor geen van deze leesacties is een gerenderde bitmap vereist. Ze komen uit de geparseerde tekstlaag, dus een open document op de juiste pagina is de enige opstelling (setup) die u nodig hebt. Hierdoor is lettertype-inspectie goedkoop (snel) uit te voeren over een heel archief. Als u dit combineert met tekstextractie, komen dezelfde tekenindexen overeen met de tekst die u eruit haalt. Hierdoor zijn het lettertype van een teken en de bijbehorende Unicode-waarde twee leesacties (reads) tegen één index. Het begeleidende artikel over het extraheren van tekst uit PDF-documenten met de PDFium-component behandelt die kant van de tekstlaag diepgaander
De hier getoonde lettertype-eigenschappen maken deel uit van de PDFium Delphi VCL-component