Tehnički članak

Čitanje svojstava PDF fontova pomoću PDFium Component u Delphi-ju

Svaki vidljivi karakter u PDF-u nosi referencu na font koji ga je nacrtao, a PDFium Component vam omogućava da pratite tu referencu nazad do objekta fonta i pročitate šta on zna. Jedinica pristupa je karakter, a ne dokument: birate karakter prema njegovom indeksu u tekstu stranice i tražite naziv porodice (family name), osnovno ime (base name), težinu (weight), ugao kurziva (italic angle) i da li je sam font zapravo prenesen unutar datoteke. Ovo poslednje svojstvo je ono što većina analiza zapravo traži, jer ugrađeni font putuje sa dokumentom, dok je neugrađeni samo obećanje da mašina čitaoca slučajno ima instaliran isti font

Komponenta ih izlaže preko istih TPdf i TPdfView objekata koje koristite za renderovanje i ekstrakciju teksta. Ne postoji poseban objekat "tabela fontova" koji treba otvoriti. Nakon što se tekst stranice parsira, svojstva fonta vise sa indeksa karaktera i čitate ih glif po glif. Taj dizajn odgovara načinu na koji PDF uopšte čuva informacije: jedna stranica može da promeni fontove desetinama puta, a jedini iskren odgovor na pitanje "u kom fontu je ovaj dokument" jeste "zavisi na koji karakter mislite"

Čitanje fonta iza jednog karaktera

Najmanja korisna operacija je uzimanje indeksa karaktera i izbacivanje svega što vam PDFium može reći o njegovom fontu. Svako svojstvo fonta na TPdf i TPdfView je indeksirano pozicijom karaktera, tako da se indeks provlači kroz sve njih. Stranica takođe mora biti trenutna stranica da bi se indeks razrešio u odnosu na tačan tekst, što postaje važno kada pređete prvu stranicu

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;

Nekoliko deklaracija može iznenaditi ljude koji dolaze iz drugih biblioteka. FontAscent i FontDescent primaju dva argumenta, indeks karaktera i veličinu u tačkama (point size), jer PDFium prijavljuje te metrike u jedinicama prostora glifova koje postaju pikseli tek kada ih skalirate veličinom na koju je tekst postavljen. Prosledite vrednost koju ste već pročitali iz FontSize[CharIndex] i dobićete ascent i descent u istim tačkama kao i ostatak rasporeda. Descent se vraća kao negativan, pošto meri ispod osnovne linije (baseline). Naziv porodice i osnovno ime su namerno odvojeni stringovi: osnovno ime je sirovi unos /BaseFont iz PDF-a, koji često nosi prefiks podskupa kao što je ABCDEF+, dok je naziv porodice očišćeno ime koje renderer na kraju razrešava

Pretvaranje klika u indeks karaktera

U čitaču retko unapred znate indeks. Korisnik klikne na glif i vi morate da prevedete koordinatu piksela u karakter ispod njega. CharacterIndexAtPos radi upravo to, uzimajući poziciju miša i toleranciju i vraćajući indeks najbližeg karaktera, ili negativnu vrednost kada klik padne na prazan prostor ili praznu stranicu

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;

Toleranciju vredi podesiti. Ako je previše uska, korisnici će imati osećaj da moraju da kliknu na tačno stablo slova; ako je previše labava, klik na marginu može da se zakači za neki udaljeni karakter koji nema nikakve veze sa onim što su želeli. Tri do pet piksela na ekranu je razumna polazna tačka. Vraćeni indeks je u parsiranom tekstu trenutne stranice, što je isti indeksni prostor koji očekuje svako svojstvo fonta, tako da ga možete direktno proslediti gornjoj rutini. Čuvanje u CurrentCharIndex je opciono, ali zgodno: prikaz to drži kao svoj fokusirani glif, što je korisno ako drugi delovi korisničkog interfejsa žele da pročitaju selekciju bez ponovnog izračunavanja

Ugradnja je svojstvo koje je najvažnije

Za većinu stvarnog rada, jedino pitanje na koje vredi odgovoriti jeste da li je svaki font ugrađen. Dokument čiji se svi fontovi nalaze unutar njega renderuje se isto na RIP-u štamparske službe, na laptopu kolege i na serveru bez ikakvog grafičkog interfejsa (GUI). Dokument koji se oslanja na neugrađeni font Helvetica rizikuje da svaka od tih mašina ima odgovarajući font, a kada taj rizik propadne, čitač ga zamenjuje nečim sličnim, metrika se menja, i pažljivo raspoređen obrazac se pomera taman toliko da se pokvari. Prolazak kroz tekst stranice i grupisanje fontova prema statusu ugradnje daje vam taj odgovor na jeftin način

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;

Dva detalja održavaju ovo tačnim. Prvo, CharacterCount je po stranici, tako da provera celog dokumenta znači postavljanje Pdf.PageNumber na svaku stranicu redom i ponovno pokretanje petlje, spajajući rezultate. Drugo, tekstualni sloj sadrži generisane karaktere kao što su razmaci koje čitalac zaključuje između reči, a oni nemaju objekat fonta iza sebe; provera praznog osnovnog imena ih preskače umesto da beleži fantomski unos. Osnovno ime je ispravan ključ za uklanjanje duplikata ovde jer prefiks podskupa koji nosi razlikuje dva različita podskupa iste porodice, što je obično ono što želite da znate

Izvlačenje ugrađenog fonta

Kada je font ugrađen, možete direktno čitati njegove bajtove. FontData vraća sirovi program fonta, iste TrueType ili CFF podatke koje PDF nosi, što je dovoljno za pisanje samostalne datoteke fonta ili za proveru otiska fonta u odnosu na poznatu biblioteku. Vraća prazan niz kada font nije ugrađen, tako da provera ugradnje i provera dužine zajedno štite upisivanje

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;

Bajtovi su ugrađeni podskup (subset), a ne originalni komercijalni font, tako da ono što dobijete nazad obično pokriva samo glifove koje je dokument stvarno koristio. To je upravo ono što je potrebno za forenziku i verifikaciju, a loše rešenje za ponovnu upotrebu; podskup fonta Times New Roman koji sadrži trideset glifova nije font koji možete instalirati i koristiti za kucanje. Tretirajte ekstrakciju kao način da pregledate šta je isporučeno, a ne kao alat za oporavak fonta. Ako vam treba odgovarajuće osnovno ime za označavanje izlaza, pročitajte FontBaseName[CharIndex] zajedno sa podacima, i uklonite vodeću oznaku podskupa ako želite čistu porodicu

Razumevanje broja težine

FontWeight vraća numeričku klasu težine, istu skalu od 100 do 900 koju koristi CSS, gde je 400 regular (normalno), a 700 bold (boldovano). PDFium prijavljuje šta god font deklariše, što nije uvek zaokružena stotina; font može prijaviti 350 ili 650, i tretiranje bilo čega na ili iznad 600 kao "dovoljno podebljano" funkcioniše bolje nego testiranje tačno za 700. Ugao kurziva (italic angle) je prateći signal: ne-nula vrednost, obično negativna, znači da je font ukošen ili pravi kurzivni dizajn, a nula znači uspravan. Zajedno vam omogućavaju da razlikujete bold-italic deo od običnog bez renderovanja bilo čega, što je vrsta provere koju preflight prolaz ili revizija pristupačnosti žele da obave grupno

Nijedno od ovih čitanja ne zahteva renderovanu bitmapu. Dolaze iz parsiranog tekstualnog sloja, tako da je otvoren dokument na ispravnoj stranici sve što vam treba od podešavanja, što pregled fontova čini jeftinim za pokretanje na celoj arhivi. Ako ovo uparujete sa ekstrakcijom teksta, isti indeksi karaktera se poklapaju sa tekstom koji izvlačite, tako da su font glifa i njegova Unicode vrednost dva čitanja u odnosu na jedan indeks. Prateći članak o ekstrakciji teksta iz PDF dokumenata pomoću PDFium Component detaljnije pokriva tu stranu tekstualnog sloja

Svojstva fonta prikazana ovde su deo PDFium Delphi VCL komponente