Tehnički članak

Čitanje svojstava PDF fontova pomoću PDFium Component-a u Delphiju

Svaki vidljivi znak u PDF-u nosi referencu na font koji ga je nacrtao, a PDFium Component vam omogućuje da slijedite tu referencu natrag do objekta fonta i pročitate što on zna. Jedinica pristupa je znak, a ne dokument: odaberete znak prema njegovom indeksu u tekstu stranice i pitate za ime obitelji (family name), osnovno ime (base name), težinu, kut kurziva (italic angle) i prenosi li se zapravo sam font (face) unutar datoteke. To zadnje svojstvo je ono za čim najviše analiza zapravo traga, jer ugrađeni font putuje s dokumentom, a neugrađeni font je obećanje da se dogodi da čitačev stroj (machine) ima instaliran isti font (typeface)

Komponenta ih izlaže kroz iste objekte TPdf i TPdfView koje koristite za renderiranje i izdvajanje teksta. Ne postoji poseban objekt "tablice fontova" koji bi se trebao otvoriti. Nakon što je tekst stranice parsiran, svojstva fonta vise na indeksu znakova, a vi ih čitate jedan po jedan glif (glyph). Taj dizajn odgovara načinu na koji PDF uopće pohranjuje informacije: jedna stranica može promijeniti fontove desetke puta, a jedini iskren odgovor na pitanje "u kojem je fontu ovaj dokument" jest "ovisi na koji znak mislite"

Čitanje fonta iza jednog znaka

Najmanja korisna operacija je uzeti indeks znaka i ispisati (dump) sve što vam PDFium može reći o njegovom fontu. Svako svojstvo fonta na TPdf i TPdfView indeksirano je prema poziciji znaka, tako da se indeks provlači kroz sve njih. Stranica također mora biti trenutna stranica kako bi se indeks riješio (resolve) naspram pravog teksta, što je važno kada prođ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 se potpisa (signatures) iznenadi ljude koji dolaze iz drugih biblioteka. FontAscent i FontDescent uzimaju dva argumenta, indeks znaka i veličinu točke (point size), jer PDFium izvješćuje o tim metrikama u jedinicama prostora glifa (glyph-space units) koje postaju pikseli tek kada ih skalirate veličinom na koju je tekst bio postavljen. Proslijedite vrijednost koju ste već pročitali iz FontSize[CharIndex] i dobivate uspon (ascent) i silazak (descent) u istim točkama kao i ostatak rasporeda (layout). Silazak se vraća negativan, budući da mjeri ispod osnovne linije (baseline). Ime obitelji i osnovno ime namjerno su zasebni nizovi: osnovno ime je čisti (raw) /BaseFont unos iz PDF-a, koji često nosi prefiks podskupa (subset prefix) kao što je ABCDEF+, dok je ime obitelji očišćeno ime u koje ga renderer rješava

Pretvaranje klika u indeks znaka

U pregledniku (viewer) rijetko unaprijed znate indeks. Korisnik klikne na glif, a vi morate prevesti (translate) koordinatu piksela u znak ispod nje. CharacterIndexAtPos radi točno to, uzimajući poziciju miša i toleranciju te vraćajući indeks najbližeg znaka, ili negativnu vrijednost kada klik padne na prazan prostor (whitespace) 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 podesiti. Ako je previše stroga (tight), korisnici će osjećati kao da moraju sletjeti na točan stup slova; ako je previše labava, klik na marginu će preskočiti (snap) na neki udaljeni znak koji nema nikakve veze s onim što su mislili. Tri do pet piksela uređaja (device pixels) je razumna početna točka za pregledavanje na zaslonu. Vraćeni indeks je u parsiranom tekstu trenutne stranice, istom prostoru indeksa koji svako svojstvo fonta očekuje, tako da ga možete predati (hand) izravno gornjoj rutini. Pohranjivanje u CurrentCharIndex nije obavezno, ali je prikladno: pogled to zadržava kao svoj pojam fokusiranog glifa, što je zgodno ako drugi dijelovi korisničkog sučelja žele čitati odabir (selection) bez ponovnog izvođenja (re-deriving)

Ugrađivanje je svojstvo koje je bitno

Za većinu stvarnog rada, jedino pitanje na koje vrijedi odgovoriti jest jesu li svi fontovi ugrađeni. Dokument čiji se fontovi svi nalaze unutar njega renderira se isto na RIP-u tiskare, na koleginom prijenosnom računalu i na poslužitelju bez ikakvog GUI-ja. Dokument koji se oslanja na neugrađenu Helvetica-u se kocka (gambling) da svaki od tih strojeva ima odgovarajući font, a kada se ta kocka ne isplati, čitač zamjenjuje nečim sličnim, metrika se mijenja, a pažljivo postavljen obrazac prepravlja se (reflows) točno toliko da pukne. Obilaženje teksta stranice i stavljanje fontova u košare (bucketing) prema statusu ugrađivanja daje vam taj odgovor vrlo jeftino (cheaply)

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 poštenim (honest). Prvo, CharacterCount je po stranici, tako da revizija cijelog dokumenta (whole-document audit) znači postavljanje Pdf.PageNumber na svaku stranicu redom i ponovno pokretanje petlje, spajajući rezultate. Drugo, tekstualni sloj (text layer) sadrži generirane znakove kao što su razmaci (spaces) koje čitatelj pretpostavlja (infers) između riječi, a oni nemaju objekt fonta iza sebe; provjera praznog osnovnog imena (empty-base-name check) preskače ih umjesto da bilježi fantoma. Osnovno ime je ovdje pravi ključ za deduplikaciju (de-duplication) jer prefiks podskupa (subset prefix) koji nosi razlikuje dva različita podskupa (subsets) iste obitelji, što je obično ono što želite znati

Izvlačenje ugrađenog fonta

Kada je font ugrađen možete izravno pročitati njegove bajtove. FontData vraća sirovi (raw) program fonta, iste TrueType ili CFF podatke koje PDF nosi, što je dovoljno za pisanje samostalne (standalone) datoteke fonta ili za uzimanje otiska prsta (fingerprint) fonta u poznatoj biblioteci. Vraća prazan niz kada font nije ugrađen, tako da provjera ugrađivanja i provjera duljine zajedno čuvaju zapis

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, a ne originalni maloprodajni font (retail font), tako da ono što dobijete obično pokriva samo glifove (glyphs) koje je dokument stvarno koristio. To je sasvim točno (exactly right) za forenziku i provjeru, a loše pristaje za ponovnu upotrebu; podskup Times New Roman-a koji sadrži trideset glifova nije font koji možete instalirati i s kojim možete tipkati. Tretirajte izdvajanje (extraction) kao način da pregledate što je isporučeno, a ne kao alat za oporavak (recovery) fonta. Ako vam je potrebno odgovarajuće osnovno ime (base name) za označavanje (label) izlaza, pročitajte FontBaseName[CharIndex] uz podatke i uklonite (strip) vodeću oznaku podskupa (subset tag) ako želite čistu (bare) obitelj

Shvaćanje broja težine

FontWeight vraća numeričku klasu težine, istu ljestvicu od 100 do 900 koju koristi CSS, gdje je 400 obično (regular), a 700 je podebljano (bold). PDFium izvješćuje o onome što font deklarira, što nije uvijek okrugla stotica (round hundred); font može objaviti 350 ili 650, a tretiranje bilo čega na ili iznad 600 kao "dovoljno podebljanog da bude važno (bold enough to matter)" drži se bolje nego testiranje za točno 700. Kut kurziva (italic angle) je prateći signal: vrijednost različita od nule (non-zero value), obično negativna, znači da je font ukošen (oblique) ili istinskog (true) dizajna kurziva, a nula znači uspravan (upright). Zajedno vam omogućuju da razlikujete podebljani kurzivni prolaz (run) od onog običnog bez renderiranja ičega, što je vrsta provjere (check) koju propusnica prije leta (preflight pass) ili revizija pristupačnosti (accessibility audit) žele obaviti na veliko

Nijedno od ovih čitanja ne zahtijeva renderiranu bitmapu. Ona dolaze iz parsiranog (parsed) sloja teksta, tako da je otvoren dokument na pravoj stranici sva postava (setup) koja vam treba, što pregled (inspection) fonta čini jeftinim za pokretanje preko cijele arhive. Ako ovo uparujete (pairing) s izdvajanjem (extraction) teksta, isti indeksi znakova usklađuju se (line up) s tekstom koji izvučete, tako da su font glifa i njegova Unicode vrijednost dva čitanja s obzirom na (against) jedan indeks. Popratni (companion) članak o izdvajanju teksta iz PDF dokumenata s PDFium Component-om detaljnije obrađuje tu stranu tekstualnog sloja

Svojstva fonta prikazana ovdje dio su PDFium Delphi VCL Component