Technický článek

Čtení vlastností písem PDF pomocí komponenty PDFium v Delphi

Každý viditelný znak v PDF nese odkaz na písmo, které ho nakreslilo, a komponenta PDFium vám umožňuje sledovat tento odkaz zpět na objekt písma a přečíst si, co o něm ví. Jednotkou přístupu je znak, nikoliv dokument: vyberete znak podle jeho indexu v textu stránky a zeptáte se na název rodiny, základní název, tloušťku, úhel kurzívy a na to, zda je podkladový řez písma ve skutečnosti obsažen uvnitř souboru. Ta poslední vlastnost je tím, o co jde většině analýz, protože vložené písmo cestuje s dokumentem a to nevložené je příslibem, že stroj čtenáře má náhodou nainstalován stejný typ písma

Komponenta je zpřístupňuje prostřednictvím stejných objektů TPdf a TPdfView, jaké používáte pro vykreslování a extrakci textu. Neexistuje žádný samostatný objekt "tabulka písem", který by se dal otevřít. Jakmile byl text stránky analyzován, vlastnosti písma visí na indexu znaku a vy je čtete jeden glyf po druhém. Tento návrh se hodí k tomu, jak PDF ukládá informace na prvním místě: na jediné stránce se může změnit písmo desítekkrát a jediná upřímná odpověď na otázku "v jakém písmu je tento dokument" zní "záleží, který znak myslíte."

Čtení písma za jedním znakem

Nejmenší užitečnou operací je převzít index znaku a vypsat vše, co vám PDFium může sdělit o jeho písmu. Každá vlastnost písma v TPdf a TPdfView je indexována pozicí znaku, takže index se táhne všemi. Aby se index vyhodnotil proti správnému textu, musí to být také aktuální stránka, na čemž záleží ve chvíli, kdy se přesunete za stranu jedna

procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
  Report: TStringList;
  PtSize: Single;
begin
  Report := TStringList.Create;
  try
    PtSize := Pdf.FontSize[CharIndex];

    Report.Add('Znak      : ' + Pdf.Character[CharIndex]);
    Report.Add('Rodina    : ' + Pdf.FontFamilyName[CharIndex]);
    Report.Add('Základní  : ' + Pdf.FontBaseName[CharIndex]);
    Report.Add('Tloušťka  : ' + IntToStr(Pdf.FontWeight[CharIndex]));
    Report.Add('Kurzíva   : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' stupňů');
    Report.Add('Velikost  : ' + FormatFloat('0.0', PtSize) + ' bodů');
    Report.Add('Vzestup   : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
    Report.Add('Sestup    : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
    Report.Add('Vloženo   : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));

    ShowMessage(Report.Text);
  finally
    Report.Free;
  end;
end;

Několik definic překvapí lidi přicházející z jiných knihoven. FontAscent a FontDescent přebírají dva argumenty, index znaku a velikost v bodech, protože PDFium reportuje tyto metriky v jednotkách prostoru glyfů, které se stanou pixely až poté, co je škálujete na velikost, na jakou byl text nastaven. Předejte hodnotu, kterou jste již přečetli z FontSize[CharIndex], a dostanete vzestup a sestup ve stejných bodech jako zbytek rozložení. Sestup se vrací jako záporný, protože měří pod účařím (baseline). Název rodiny a základní název jsou schválně oddělené řetězce: základní název (base name) je nezpracovaný záznam /BaseFont z PDF, často nesoucí prefix podsady jako ABCDEF+, zatímco název rodiny (family name) je očištěný název, na který ho renderer vyřeší

Změna kliknutí na index znaku

V prohlížeči málokdy znáte index předem. Uživatel klikne na glyf a vy musíte převést souřadnici pixelu na znak pod ním. CharacterIndexAtPos dělá přesně to: vezme pozici myši a toleranci a vrátí index nejbližšího znaku, nebo zápornou hodnotu, když kliknutí dopadlo na prázdné místo nebo prázdnou stránku

procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Index: Integer;
begin
  if not PdfView.Active then
    Exit;

  // 4 px vůle v každém směru, takže těsné minutí stále zasáhne glyf.
  Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
  if Index < 0 then
    Exit;                      // kliknuto mezi glyfy; nechte panel na pokoji

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

Toleranci se vyplatí vyladit. Je-li příliš přísná, uživatelé mají pocit, že musí přistát přesně na dříku písmene; je-li příliš volná a kliknutí v okraji přeskočí k nějakému vzdálenému znaku, nemá to nic společného s tím, co zamýšleli. Tři až pět pixelů zařízení je rozumný výchozí bod pro prohlížení na obrazovce. Vrácený index ukazuje do analyzovaného textu aktuální stránky, tedy do stejného prostoru indexů, který každá vlastnost písma očekává, takže jej můžete předat přímo do výše uvedené rutiny. Jeho uložení do CurrentCharIndex je volitelné, ale pohodlné: pohled to zachová jako svůj pojem zaostřeného glyfu, což se hodí, pokud jiné části uživatelského rozhraní chtějí číst výběr bez jeho nového odvozování

Vložení je vlastnost, na které záleží

Pro většinu reálné práce jedinou otázkou, na kterou stojí za to odpovědět, je, zda je každé písmo vloženo. Dokument, jehož všechna písma putují uvnitř něj, se na tiskárně (RIP), na notebooku kolegy a na serveru bez jakéhokoli GUI vůbec vykreslí stejně. Dokument, který spoléhá na nevloženou Helvetica, hraje hazard s tím, že na každém z těchto strojů je shodný řez, a když sázka selže, čtečka ho nahradí něčím blízkým, metriky se posunou a pečlivě rozvržený formulář přeteče zrovna tak, aby se pokazil. Procházení textu stránky a ukládání písem do skupin podle stavu vložení vám poskytne odpověď levně

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;              // vygenerované mezery a podobně nemají písmo
      if Pdf.FontIsEmbedded[I] then
        Embedded.Add(Name)
      else
        External.Add(Name);
    end;

    if External.Count > 0 then
      ShowMessage(IntToStr(External.Count) +
        ' nevložené(á) písmo(a):' + sLineBreak + External.Text)
    else
      ShowMessage('Všech ' + IntToStr(Embedded.Count) +
        ' písem na této stránce je vloženo.');
  finally
    Embedded.Free;
    External.Free;
  end;
end;

Dva detaily udržují tuto rutinu upřímnou. Za prvé, CharacterCount se vztahuje na jednu stránku, takže audit celého dokumentu znamená postupně nastavit Pdf.PageNumber na každou stránku a spustit smyčku znovu, s tím že spojíte výsledky. Za druhé, textová vrstva obsahuje vygenerované znaky, jako jsou mezery, které si čtečka odvodí mezi slovy, a za nimi není žádný objekt písma; kontrola prázdného základního názvu je přeskočí, aby nelogovala fantoma. Základní název je tady tím správným klíčem pro odstranění duplikátů, protože předpona podsady, kterou nese, odlišuje dvě různé podsady ze stejné rodiny, což je obvykle to, co chcete vědět

Vytažení vloženého řezu ven

Když je písmo vloženo, můžete přímo číst jeho bajty. FontData vrací surový program písma, tedy stejná data TrueType nebo CFF, která PDF nese, což stačí na zápis samostatného souboru s písmem nebo na otisk (fingerprinting) řezu oproti známé knihovně. Vrací prázdné pole, pokud písmo není vloženo, takže kontrola vložení a kontrola délky společně chrání 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('Písmo pro tento glyf není vloženo; není co extrahovat.');
    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('Zapsáno ' + IntToStr(Length(Data)) + ' bajtů.');
end;

Bajty představují vloženou podsadu, ne původní maloobchodní písmo, takže to, co dostanete nazpět, obvykle pokrývá jen ty glyfy, které dokument skutečně použil. To je naprosto v pořádku pro forenzní analýzu a ověřování a chabě to vyhovuje k opakovanému použití; podsada Times New Roman, která obsahuje třicet glyfů, není písmo, které si můžete nainstalovat a začít v něm psát. Extrakci berte jako způsob, kterým můžete prozkoumat, co bylo dodáno, nikoli jako nástroj pro obnovu písma. Pokud potřebujete odpovídající základní název k označení výstupu, přečtěte si FontBaseName[CharIndex] spolu s daty, a pokud chcete prázdnou rodinu, odstraňte počáteční značku podsady

Porozumění číslu tloušťky

FontWeight vrací číselnou třídu tloušťky na stejné stupnici od 100 do 900, kterou používají CSS, kde 400 je běžné (regular) a 700 je tučné (bold). PDFium reportuje cokoliv, co písmo deklaruje, což nemusí být vždy celá stovka; řez může ohlásit 350 nebo 650 a zacházení se vším na úrovni 600 nebo vyšší jako s "dostatečně tučným na to, aby na tom záleželo" obstojí lépe, než testování na přesných 700. Úhel kurzívy je doprovodným signálem: nenulová hodnota, zpravidla negativní, znamená, že daný řez je obliques nebo pravý italic (kurzíva) design a nula značí upright (kolmé) písmo. Tyto dva společně vám dovolí rozpoznat průběh tučnou kurzívou od běžného bez toho, abyste něco vykreslovali, což je právě ten typ kontroly, kterou by validace předtiskové přípravy nebo audit přístupnosti chtěl provádět hromadně

Žádné z těchto čtení nevyžaduje vyrendrovanou bitmapu. Vycházejí z analyzované textové vrstvy, takže k nastavení postačuje otevřený dokument na správné stránce, díky čemuž je prozkoumání písma ve funkčním archivu levné na běh. Pokud toto spárujete s extrakcí textu, stejné indexy znaků budou lícovat s textem, který vytáhnete, takže písmo glyfu a jeho hodnota Unicode jsou dvě načtení vůči jednomu indexu. Doprovodný článek o extrahování textu z dokumentů PDF s komponentou PDFium pokrývá tu druhou stranu textové vrstvy podrobněji

Vlastnosti písem, které jsou zde zobrazené, jsou součástí komponenty PDFium Delphi VCL Component