Technical Article

Čitanje svojstava fonta PDF-a pomoću PDFium VCL-a u Delphiju

Svaki vidljivi znak u PDF-u nosi referencu na font koji ga je nacrtao, a PDFium VCL vam omogućuje da pratite tu referencu natrag do objekta fonta i pročitate što on zna. Jedinica pristupa je znak, a ne dokument: odabirete znak prema njegovom indeksu u tekstu stranice i tražite naziv obitelji (family name), osnovni naziv (base name), težinu, kut ukošenja (italic angle) i podatak o tome je li font doista sadržan unutar datoteke. To posljednje svojstvo je ono što većina analiza zapravo traži, cuz ugrađeni font putuje s dokumentom, dok je neugrađeni font samo obećanje da računalo čitatelja slučajno ima instaliran isti font.

Komponenta ih izlaže kroz iste objekte TPdf i TPdfView koje koristite za renderiranje i ekstrakciju teksta. Ne postoji zaseban objekt "tablice fontova" koji treba otvoriti. Nakon što se tekst stranice analizira, svojstva fonta su povezana s indeksom znaka te ih čitate glif po glif. Taj dizajn odgovara načinu na koji PDF uopće pohranjuje te informacije: jedna stranica može promijeniti fontove desecima puta, a jedini iskreni odgovor na pitanje "u kojem je fontu ovaj dokument" glasi "ovisi o kojem znaku govorite".

Čitanje fonta iza pojedinog znaka

Najmanja korisna operacija je uzimanje indeksa znaka i ispis svega što vam PDFium može reći o njegovom fontu. Svako svojstvo fonta na TPdf i TPdfView indeksirano je pozicijom znaka, pa se indeks provlači kroz sva njih. Stranica također mora biti trenutna stranica kako bi se indeks ispravno razriješio u odnosu na odgovarajući tekst, što postaje važno čim prijeđ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 moglo bi iznenaditi programere koji dolaze iz drugih knjižnica. FontAscent i FontDescent primaju dva argumenta: indeks znaka i veličinu u točkama (point size), jer PDFium prijavljuje te metričke vrijednosti u jedinicama prostora glifova koje postaju pikseli tek kada ih skalirate prema veličini na koju je tekst postavljen. Proslijedite vrijednost koju ste već pročitali iz FontSize[CharIndex] i dobit ćete gornju duljinu (ascent) i donju duljinu (descent) u istim točkama kao i ostatak izgleda. Descent se vraća kao negativna vrijednost jer se mjeri ispod osnovne linije. Naziv obitelji (family name) i osnovni naziv (base name) namjerno su odvojeni nizovi znakova: osnovni naziv je sirovi unos /BaseFont iz PDF-a, koji često nosi prefiks podskupa poput ABCDEF+, dok je naziv obitelji očišćeni naziv koji preglednik razrješava.

Pretvaranje klika u indeks znaka

U pregledniku rijetko unaprijed znate indeks. Korisnik klikne na glif, a vi morate prevesti koordinatu piksela u znak ispod njega. Metoda CharacterIndexAtPos radi upravo to: prima poziciju miša i toleranciju te vraća indeks najbližeg znaka, ili negativnu vrijednost ako je klik pao 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 vrijedi prilagoditi. Ako je preuska, korisnici će imati osjećaj da moraju kliknuti točno na liniju slova; ako je preširoka, klik na marginu privući će neki daleki znak koji nema nikakve veze s onim što su željeli. Tri do pet piksela uređaja razumna su polazna točka za prikaz na zaslonu. Vraćeni indeks odnosi se na analizirani tekst trenutne stranice, što je isti indeksni prostor koji očekuje svako svojstvo fonta, pa ga možete proslijediti izravno u gornju rutinu. Pohranjivanje u CurrentCharIndex je opcionalno, ali prikladno: prikaz ga zadržava kao fokusirani glif, što je korisno ako drugi dijelovi sučelja žele pročitati odabir bez ponovnog izračunavanja.

Ugradnja je svojstvo koje je doista važno

Za većinu stvarnih zadataka, jedino pitanje na koje vrijedi odgovoriti jest je li svaki font ugrađen. A dokument čiji su svi fontovi sadržani unutar njega prikazat će se isto na RIP-u tiskare, na prijenosnom računalu kolege i na poslužitelju bez ikakvog grafičkog sučelja. Dokument koji se oslanja na neugrađenu Helvetica-u riskira da svako od tih računala ima odgovarajući font, a kada taj rizik ne uspije, čitač ga zamjenjuje nečim sličnim, metričke vrijednosti se mijenjaju, a pažljivo oblikovan obrazac se pomiče dovoljno da se pokvari. Prolazak kroz tekst stranice i grupiranje fontova prema statusu ugradnje pruža vam taj odgovor na brz i jednostavan 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 osiguravaju točnost ove analize. Prvo, CharacterCount se odnosi na pojedinu stranicu, pa revizija cijelog dokumenta znači postavljanje svojstva Pdf.PageNumber na svaku stranicu redom, ponovno pokretanje petlje i spajanje rezultata. Drugo, tekstualni sloj sadrži generirane znakove, poput razmaka koje čitač pretpostavlja između riječi, a oni nemaju objekt fonta iza sebe; provjera praznog osnovnog naziva ih preskače umjesto da bilježi fantomske fontove. Osnovni naziv je pravi ključ za uklanjanje duplikata jer prefiks podskupa koji nosi razlikuje dva različita podskupa iste obitelji fontova, što je obično informacija koju želite saznati.

Izvlačenje ugrađenog fonta

Kada je font ugrađen, možete izravno pročitati njegove bajtove. Svojstvo FontData vraća sirovi program fonta, iste TrueType ili CFF podatke koje PDF nosi, što je dovoljno za pisanje samostalne datoteke fonta ili za provjeru otiska fonta u odnosu na poznatu knjižicu. Vraća prazno polje kada font nije ugrađen, pa provjera ugradnje i provjera duljine zajedno osiguravaju siguran upis.

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 predstavljaju ugrađeni podskup (subset), a ne originalni komercijalni font, tako da ono što dobijete obično pokriva samo glifove koje je dokument doista koristio. To je savršeno za forenziku i provjeru, ali loše za ponovnu upotrebu; podskup fonta Times New Roman koji sadrži samo trideset glifova nije font koji možete instalirati i koristiti za pisanje. Tretirajte ekstrakciju kao način pregledavanja onoga što je isporučeno, a ne kao alat za oporavak fontova. Ako trebate odgovarajući osnovni naziv za označavanje izlaza, pročitajte FontBaseName[CharIndex] uz podatke i uklonite vodeću oznaku podskupa ako želite samo čisti naziv obitelji.

Razumijevanje broja težine fonta

Svojstvo FontWeight vraća numeričku klasu težine, istu ljestvicu od 100 do 900 koju koristi CSS, gdje je 400 uobičajeni (regular), a 700 podebljani (bold) font. PDFium javlja ono što font deklarira, što nije uvijek okrugla stotica; font može prijaviti 350 ili 650, a tretiranje bilo čega na ili iznad 600 kao "dovoljno podebljano" funkcionira bolje od provjere točne vrijednosti 700. Kut ukošenja je prateći signal: ne-nula vrijednost, obično negativna, znači da je font kosi (oblique) ili pravi kurziv (italic), dok nula označava uspravan font. Zajedno vam omogućuju da razlikujete podebljani kurziv od običnog teksta bez renderiranja ičega, što je upravo provjera kakvu priprema za tisak (preflight) ili revizija pristupačnosti žele raditi masovno.

Nijedno od ovih čitanja ne zahtijeva renderiranu bitmapu. Ona dolaze iz analiziranog tekstualnog sloja, pa je otvoren dokument na ispravnoj stranici sva priprema koja vam je potrebna, što inspekciju fontova čini vrlo učinkovitom za pokretanje na cijeloj arhivi. Ako ovo kombinirate s ekstrakcijom teksta, isti indeksi znakova usklađeni su s tekstom koji izvlačite, tako da su font glifa i njegova Unicode vrijednost zapravo dva čitanja s istim indeksom. Prateći članak o ekstrakciji teksta iz PDF dokumenata pomoću PDFium VCL-a detaljnije pokriva tu stranu tekstualnog sloja.

Svojstva fonta ovdje prikazana dio su komponente PDFium Delphi VCL Component.