Technical Article

Analýza vlastností písiem v PDF pomocou PDFium VCL v Delphi

Každý viditeľný znak v PDF nesie odkaz na písmo, ktoré ho vykreslilo, a PDFium VCL vám umožňuje tento odkaz sledovať späť k objektu písma a prečítať to, čo o sebe vie. Jednotkou prístupu je znak, nie dokument: vyberiete si znak podľa jeho indexu v texte stránky a zistíte názov rodiny písiem, základný názov, hrúbku, uhol sklonu kurzívy a to, či je samotný font skutočne prenášaný vo vnútri súboru. Práve táto posledná vlastnosť je tým, o čo pri väčšine analýz naozaj ide. Vložené (embedded) písmo totiž cestuje s dokumentom, kým nevložené písmo je iba prísľubom, že počítač čitateľa má náhodou nainštalovaný rovnaký typ písma.

Komponent tieto vlastnosti sprístupňuje prostredníctvom rovnakých objektov TPdf a TPdfView, ktoré používate na vykresľovanie a extrakciu textu. Neotvára sa žiadny samostatný objekt s „tabuľkou písiem“. Po analýze textu stránky sú vlastnosti písma naviazané na index znaku a vy ich čítate po jednotlivých glyfoch. Tento návrh zodpovedá spôsobu, akým PDF tieto informácie ukladá: jedna stránka môže zmeniť písmo desiatky ráz a jedinou pravdivou odpoveďou na otázku „v akom písme je tento dokument“ je „záleží na tom, ktorý znak máte na mysli“.

Čítanie písma za jedným znakom

Najmenšou užitočnou operáciou je vziať index znaku a vypísať všetko, čo vám PDFium dokáže o jeho písme povedať. Každá vlastnosť písma v triedach TPdf a TPdfView je indexovaná podľa pozície znaku, takže index prechádza všetkými týmito vlastnosťami. Stránka musí byť zároveň nastavená ako aktuálna stránka, aby sa index vyriešil voči správnemu textu, čo je dôležité najmä vtedy, keď sa presuniete za prvú stránku.

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;

Niektoré deklarácie prekvapia vývojárov prichádzajúcich z iných knižníc. FontAscent a FontDescent berú dva parametre, index znaku a veľkosť v bodoch (point size), pretože PDFium hlási tieto metriky v jednotkách priestoru glyfov, ktoré sa stanú pixelmi až po ich prepočítaní veľkosťou, na ktorú bol text nastavený. Odovzdajte hodnotu, ktorú ste už prečítali z FontSize[CharIndex], a získate ascent (výšku horného preťahu) a descent (hĺbku dolného preťahu) v rovnakých bodoch ako zvyšok rozloženia. Descent sa vracia ako záporná hodnota, pretože sa meria pod účaří (baseline). Názov rodiny písiem a základný názov sú samostatné reťazce zámerne: základný názov je surový záznam /BaseFont z PDF, ktorý často nesie prefix podskupiny ako napríklad ABCDEF+, zatiaľ čo názov rodiny je vyčistený názov, na ktorý ho vykresľovacie jadro nakoniec preloží.

Preklad kliknutia na index znaku

V prehliadači zriedkakedy poznáte index znaku vopred. Používateľ klikne na glyf a vy musíte preložiť pixelovú súradnicu na znak pod ňou. Metóda CharacterIndexAtPos robí presne to: berie pozíciu myši a toleranciu a vracia index najbližšieho znaku alebo zápornú hodnotu, ak kliknutie dopadlo na biele miesto alebo prázdnu časť stránky.

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;

Toleranciu je dobré vyladiť. Ak je príliš tesná, používatelia majú pocit, že musia kliknúť na presnú líniu písmena; ak je príliš voľná, kliknutie na okraj sa prichytí k vzdialenému znaku, ktorý nemá nič spoločné s tým, čo chceli. Tri až päť hardvérových pixelov je rozumným východiskovým bodom pre prezeranie na obrazovke. Vrátený index smeruje do analyzovaného textu aktuálnej stránky, čo je rovnaký indexový priestor, aký očakáva každá vlastnosť písma, takže ho môžete odovzdáť priamo do vyššie uvedenej rutiny. Uloženie do CurrentCharIndex je voliteľné, ale pohodlné: zobrazenie si ho uchováva ako svoju predstavu o zameranom glyfe, čo je praktické, ak iné časti používateľského rozhrania chcú čítať výber bez opätovného odvodzovania.

Vloženie písma je kľúčová vlastnosť

Pri väčšine reálnych úloh je jedinou otázkou, na ktorú stojí za to odpovedať, či je každé písmo vložené. Dokument, ktorého všetky písma sa nachádzajú priamo v ňom, sa vykreslí rovnako na tlačiarenskom RIPe, na notebooku kolegu aj na serveri úplne bez grafického rozhrania. Dokument, ktorý sa spolieha na nevložený font Helvetica, riskuje a vsádza na to, že každý z týchto strojov má nainštalované zhodné písmo. Ak táto stávka nevyjde, čítačka nahradí písmo niečím podobným, čím sa posunú metriky a starostlivo navrhnuté rozloženie formulára sa zmení práve toľko, aby sa rozsypalo. Prejdenie textu stránky a roztriedenie písiem podľa stavu vloženia vám poskytne túto odpoveď rýchlo a efektívne.

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 detaily udržiavajú tento prístup presný. Po prvé, CharacterCount sa vzťahuje na konkrétnu stránku, takže audit celého dokumentu znamená postupné prepínanie Pdf.PageNumber na každú stránku a opätovné spustenie cyklu s následným zlúčením výsledkov. Po druhé, textová vrstva obsahuje generované znaky, ako sú medzery, ktoré čítačka predpokladá medzi slovami, a tie nemajú priradený žiadny objekt písma; kontrola prázdneho základného názvu ich preskočí namiesto zaznamenávania fantómových položiek. Základný názov je tu správnym kľúčom na odstránenie duplicít, pretože prefix podskupiny, ktorý nesie, odlišuje dve rôzne podskupiny rovnakej rodiny písiem, čo je zvyčajne to, čo potrebujete vedieť.

Extrahovanie vloženého písma

Keď je písmo vložené, môžete priamo čítať jeho bajty. Vlastnosť FontData vracia surový program písma, teda rovnaké dáta TrueType alebo CFF, aké prenáša PDF, čo stačí na zápis samostatného súboru písma alebo na overenie vzorky písma voči známej knižnici. Ak písmo nie je vložené, vracia prázdne pole, takže kontrola vloženia a dĺžky dát spoločne chránia zápis.

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;

Získané bajty predstavujú vloženú podskupinu znakov, nie pôvodné komerčné písmo, takže to, čo dostanete späť, zvyčajne pokrýva iba tie glyfy, ktoré dokument skutočne použil. To je presne to, čo potrebujete na forenznú analýzu a verifikáciu, ale nehodí sa to na opätovné použitie; podskupina písma Times New Roman, ktorá obsahuje tridsať glyfov, nie je písmom, ktoré by ste si mohli nainštalovať a písať s ním. Extrakciu vnímajte ako spôsob kontroly toho, čo bolo s dokumentom dodané, nie ako nástroj na obnovu plnohodnotných písiem. Ak potrebujete zodpovedajúci základný názov na označenie výstupu, prečítajte si vlastnosť FontBaseName[CharIndex] spolu s dátami a v prípade potreby odstráňte prefix podskupiny, ak chcete čistý názov rodiny.

Porozumenie hodnote hrúbky písma

Metóda FontWeight vracia číselnú triedu hrúbky písma, čo je rovnaká stupnica od 100 do 900, akú používa CSS, kde 400 znamená bežné písmo (regular) a 700 je tučné (bold). PDFium hlási to, čo písmo deklaruje, čo nemusí byť vždy zaokrúhlená stovka. Písmo môže deklarovať hodnotu 350 alebo 650, pričom posudzovanie akejkoľvek hodnoty od 600 vyššie ako „dostatočne tučnej“ funguje v praxi lepšie než testovanie presnej hodnoty 700. Uhol sklonu kurzívy (italic angle) je sprievodným signálom: nenulová hodnota, zvyčajne záporná, znamená, že písmo je šikmé alebo skutočná kurzíva, zatiaľ čo nula znamená vzpriamené písmo. Spoločne vám umožňujú odlíšiť tučnú kurzívu od bežného textu bez potreby akéhokoľvek vykresľovania, čo je presne typ kontroly, akú chce hromadne vykonávať predbežná kontrola (preflight) alebo audit prístupnosti.

Žiadne z týchto čítaní nevyžaduje vykreslenú bitmapu. Pochádzajú z analyzovanej textovej vrstvy, takže otvorený dokument na správnej stránke je celým nastavením, ktoré potrebujete, vďaka čomu je kontrola písiem lacná na prevádzku naprieč celým archívom. Ak to kombinujete s extrakciou textu, rovnaké indexy znakov sa zhodujú s textom, ktorý vyťahujete, takže font glyfu a jeho hodnota v Unicode sú dvoma čítaniami voči jednému indexu. Sprievodný článok o extrahovaní textu z PDF dokumentov pomocou PDFium VCL sa tejto téme textovej vrstvy venuje podrobnejšie.

Vlastnosti písiem uvedené v tomto článku sú súčasťou komponentu PDFium Delphi VCL Component.