Articol tehnic

Citirea proprietăților fonturilor PDF cu PDFium în Delphi

Fiecare caracter vizibil dintr-un PDF poartă o referință spre fontul care l-a desenat, iar PDFium Component vă lasă să urmăriți acea referință înapoi până la obiectul font și să citiți ce știe el. Unitatea de acces este caracterul, nu documentul: alegeți un caracter după indexul lui din textul paginii și cereți numele familiei, numele de bază, greutatea, unghiul de italic și dacă fața de bază este într-adevăr purtată în interiorul fișierului. Ultima proprietate este cea pe care o urmăresc de fapt majoritatea analizelor, pentru că un font încorporat călătorește împreună cu documentul, în timp ce unul neîncorporat este o promisiune că mașina cititorului se întâmplă să aibă instalat același caracter tipografic

Componenta le expune prin aceleași obiecte TPdf și TPdfView pe care le folosiți pentru randare și pentru extragerea textului. Nu există un obiect separat de tip „tabel de fonturi” care să fie deschis. Odată ce textul unei pagini a fost parsat, proprietățile fontului atârnă de indexul caracterului, iar dumneavoastră le citiți glifă cu glifă. Designul acesta se potrivește cu felul în care PDF stochează informația de la bun început: o singură pagină poate schimba fonturile de zeci de ori, iar singurul răspuns onest la întrebarea „în ce font este documentul acesta” este „depinde la ce caracter vă referiți”

Citirea fontului din spatele unui caracter

Cea mai mică operație utilă este să luați un index de caracter și să afișați tot ce vă poate spune PDFium despre fontul lui. Fiecare proprietate de font din TPdf și TPdfView este indexată după poziția caracterului, așa că indexul le străbate pe toate. Pagina trebuie să fie și pagina curentă pentru ca indexul să se rezolve față de textul corect, ceea ce contează odată ce treceți dincolo de prima pagină

Diagramă a PDFium Component în Delphi citind numele familiei, numele de bază, greutatea, unghiul de italic, corpul, ascendenta și descendenta, precum și încorporarea, pentru un index de caracter din textul parsat al paginii
PDFium Component atârnă fiecare proprietate de font de un index de caracter, așa că o singură citire pentru o glifă acoperă nume, greutate, metrici și încorporare
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;

Câteva dintre semnături îi surprind pe cei care vin de la alte biblioteci. FontAscent și FontDescent primesc două argumente, indexul caracterului și un corp de literă, pentru că PDFium raportează acele metrici în unități din spațiul glifelor, care devin pixeli abia după ce le scalați cu dimensiunea la care a fost așezat textul. Transmiteți valoarea pe care ați citit-o deja din FontSize[CharIndex] și obțineți ascendenta și descendenta în aceleași puncte ca restul aranjării. Descendenta vine negativă, pentru că se măsoară sub linia de bază. Numele familiei și numele de bază sunt șiruri separate în mod intenționat: numele de bază este intrarea brută /BaseFont din PDF, care poartă adesea un prefix de subset precum ABCDEF+, în timp ce numele familiei este numele curățat la care îl rezolvă motorul de randare

Transformarea unui clic într-un index de caracter

Într-un vizualizator rareori știți indexul dinainte. Utilizatorul dă clic pe o glifă, iar dumneavoastră trebuie să traduceți coordonata în pixeli în caracterul de sub ea. CharacterIndexAtPos face exact asta: primește poziția mouse-ului și o toleranță și returnează indexul celui mai apropiat caracter sau o valoare negativă când clicul a nimerit în spațiu alb ori în pagină goală

Diagramă de flux a unui vizualizator Delphi cu PDFium Component care transformă un clic de mouse într-un index de caracter cu CharacterIndexAtPos și o toleranță în pixeli, nimerind o glifă sau aterizând în spațiu alb
CharacterIndexAtPos transformă un clic în același index de caracter pe care îl așteaptă fiecare proprietate de font, cu o toleranță care merită ținută între trei și cinci pixeli
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 joc în fiecare direcție, ca un clic ratat la limită să prindă totuși glifa.
  Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
  if Index < 0 then
    Exit;                      // clic între glife; lasă panoul în pace

  PdfView.CurrentCharIndex := Index;
  DescribeFontAt(PdfView.Pdf, Index);
end;

Toleranța merită reglată. Prea strânsă și utilizatorii simt că trebuie să nimerească exact bara unei litere; prea largă și un clic într-o margine se lipește de vreun caracter îndepărtat, care nu are nicio legătură cu ce voiau. Trei până la cinci pixeli de dispozitiv este un punct de plecare rezonabil pentru vizionarea pe ecran. Indexul returnat trimite în textul parsat al paginii curente, același spațiu de indici pe care îl așteaptă orice proprietate de font, așa că îl puteți da direct rutinei de mai sus. Păstrarea lui în CurrentCharIndex este opțională, dar comodă: vizualizarea îl ține drept noțiune proprie de glifă focalizată, ceea ce ajută dacă alte părți ale interfeței vor să citească selecția fără să o deducă din nou

Încorporarea este proprietatea care contează

Pentru majoritatea lucrărilor reale, singura întrebare care merită un răspuns este dacă fiecare font este încorporat. Un document ale cărui fonturi călătoresc toate în interiorul lui se randează la fel pe RIP-ul unei tipografii, pe laptopul unui coleg și pe un server fără nicio interfață grafică. Un document care se sprijină pe un Helvetica neîncorporat pariază că fiecare dintre acele mașini are o față potrivită, iar când pariul eșuează, cititorul substituie ceva apropiat, metricile se deplasează, iar un formular aranjat cu grijă se reformatează exact cât să se strice. Parcurgerea textului paginii și gruparea fonturilor după starea de încorporare vă dă ieftin acest răspuns

Diagramă a unui audit Delphi cu PDFium Component care parcurge caracterele paginii și grupează fonturile după FontIsEmbedded pentru a separa fețele încorporate de cele neîncorporate și a le raporta pe acestea din urmă
Gruparea caracterelor după FontIsEmbedded scoate la iveală fonturile neîncorporate pe care o tipografie sau un server ar trebui să le substituie
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;              // spațiile generate și altele asemenea nu au 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;

Două detalii mențin lucrurile oneste. Întâi, CharacterCount este per pagină, așa că un audit pe întregul document înseamnă setarea lui Pdf.PageNumber pe fiecare pagină pe rând și rularea buclei din nou, cu îmbinarea rezultatelor. Apoi, stratul de text conține caractere generate, precum spațiile pe care un cititor le deduce între cuvinte, iar acestea nu au niciun obiect font în spate; verificarea numelui de bază gol le sare, în loc să înregistreze o fantomă. Numele de bază este cheia potrivită pentru deduplicare aici, pentru că prefixul de subset pe care îl poartă distinge două subseturi diferite ale aceleiași familii, iar de obicei exact asta vreți să aflați

Extragerea feței încorporate

Când un font este încorporat, îi puteți citi octeții direct. FontData returnează programul brut al fontului, aceleași date TrueType sau CFF pe care le poartă PDF-ul, ceea ce ajunge pentru a scrie un fișier de font de sine stătător sau pentru a lua amprenta feței față de o bibliotecă cunoscută. Returnează un tablou gol când fontul nu este încorporat, așa că verificarea încorporării și verificarea lungimii păzesc împreună scrierea

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;

Octeții sunt subsetul încorporat, nu fontul comercial original, așa că ce primiți înapoi acoperă de obicei doar glifele pe care documentul le-a folosit efectiv. Asta este exact ce trebuie pentru expertiză și verificare și se potrivește prost cu reutilizarea; un subset din Times New Roman care conține treizeci de glife nu este un font pe care să îl instalați și cu care să scrieți. Tratați extragerea ca pe un mod de a inspecta ce a fost livrat, nu ca pe un instrument de recuperare a fonturilor. Dacă aveți nevoie de numele de bază potrivit pentru a eticheta rezultatul, citiți FontBaseName[CharIndex] alături de date și îndepărtați eticheta de subset de la început dacă vreți familia goală

Cum se citește numărul de greutate

FontWeight returnează clasa numerică de greutate, aceeași scală de la 100 la 900 pe care o folosește CSS, unde 400 este normal, iar 700 este aldin. PDFium raportează ce declară fontul, ceea ce nu este întotdeauna o sută rotundă; o față poate anunța 350 sau 650, iar tratarea oricărei valori de la 600 în sus drept „suficient de aldină cât să conteze” rezistă mai bine decât testarea exactă pentru 700. Unghiul de italic este un semnal însoțitor: o valoare nenulă, de obicei negativă, înseamnă că fața este un design oblic sau un italic veritabil, iar zero înseamnă drept. Împreună, ele vă permit să deosebiți o secvență aldină-italică de una normală fără să randați nimic, adică genul de verificare pe care o trecere de preflight sau un audit de accesibilitate vrea să o facă în masă

Niciuna dintre aceste citiri nu are nevoie de o imagine randată. Ele vin din stratul de text parsat, așa că un document deschis pe pagina potrivită este toată pregătirea de care aveți nevoie, ceea ce face inspecția fonturilor ieftin de rulat pe o arhivă întreagă. Dacă împerecheați asta cu extragerea textului, aceiași indici de caracter se aliniază cu textul pe care îl scoateți, așa că fontul unei glife și valoarea ei Unicode sunt două citiri pe același index. Articolul însoțitor despre extragerea textului din documente PDF cu PDFium Component acoperă mai în detaliu acea latură a stratului de text

Proprietățile de font arătate aici fac parte din PDFium Delphi VCL Component