Artykuł techniczny

Odczytywanie właściwości czcionek PDF za pomocą komponentu PDFium w Delphi

Każdy widoczny znak w dokumencie PDF zawiera odwołanie do czcionki, która go narysowała, a komponent PDFium pozwala podążać za tym odwołaniem do obiektu czcionki i odczytywać to, co o nim wie. Jednostką dostępu jest znak, a nie dokument: wybierasz znak na podstawie jego indeksu w tekście strony i pytasz o nazwę rodziny, nazwę bazową, grubość, kąt pochylenia oraz o to, czy podstawowy krój pisma jest faktycznie zawarty w pliku. Ta ostatnia właściwość to coś, czego w rzeczywistości poszukuje większość analiz, ponieważ osadzona czcionka podróżuje wraz z dokumentem, a nieosadzona to tylko obietnica, że na komputerze czytelnika zainstalowany jest ten sam krój pisma

Komponent udostępnia je poprzez te same obiekty TPdf i TPdfView, których używasz do renderowania i ekstrakcji tekstu. Nie ma oddzielnego obiektu "tabeli czcionek" do otwarcia. Po przeanalizowaniu tekstu strony właściwości czcionki są powiązane z indeksem znaku, a ty odczytujesz je jeden glif na raz. Taki projekt odpowiada sposobowi, w jaki PDF przechowuje informacje w pierwszej kolejności: pojedyncza strona może zmieniać czcionki dziesiątki razy, a jedyna uczciwa odpowiedź na pytanie "jaką czcionką jest napisany ten dokument", to "zależy, który znak masz na myśli"

Odczytywanie czcionki za jednym znakiem

Najmniejszą użyteczną operacją jest pobranie indeksu znaku i zrzucenie wszystkiego, co PDFium może ci powiedzieć o jego czcionce. Każda właściwość czcionki w TPdf i TPdfView jest indeksowana według pozycji znaku, więc indeks przewija się przez nie wszystkie. Strona musi być również stroną bieżącą, aby indeks odnosił się do właściwego tekstu, co ma znaczenie, gdy przejdziesz dalej niż pierwsza strona

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;

Kilka sygnatur zaskakuje ludzi przechodzących z innych bibliotek. FontAscent i FontDescent przyjmują dwa argumenty, indeks znaku i rozmiar punktowy, ponieważ PDFium zgłasza te metryki w jednostkach przestrzeni glifów, które stają się pikselami dopiero po przeskalowaniu ich przez rozmiar, w jakim ustawiono tekst. Przekaż wartość, którą już odczytałeś z FontSize[CharIndex], a otrzymasz wydłużenie górne (ascent) i dolne (descent) w tych samych punktach co reszta układu. Wydłużenie dolne wraca ujemne, ponieważ mierzy się je poniżej linii bazowej. Nazwa rodziny i nazwa bazowa to celowo oddzielne ciągi znaków: nazwa bazowa to surowy wpis /BaseFont z pliku PDF, często zawierający prefiks podzbioru, taki jak ABCDEF+, podczas gdy nazwa rodziny to oczyszczona nazwa, na którą renderer ją rozwiązuje

Zamiana kliknięcia w indeks znaku

W przeglądarce rzadko znasz indeks z góry. Użytkownik klika glif, a ty musisz przetłumaczyć współrzędną piksela na znak znajdujący się pod nim. CharacterIndexAtPos robi dokładnie to, biorąc pozycję myszy oraz tolerancję i zwracając indeks najbliższego znaku lub wartość ujemną, gdy kliknięcie trafiło na biały znak lub pustą stronę

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;

Warto dostosować tolerancję. Zbyt wąska sprawia, że użytkownicy mają wrażenie, iż muszą trafić w dokładny rdzeń litery; zbyt luźna, a kliknięcie na marginesie przyciąga do jakiegoś odległego znaku, który nie ma nic wspólnego z tym, o co im chodziło. Od trzech do pięciu pikseli urządzenia to rozsądny punkt wyjścia do oglądania na ekranie. Zwrócony indeks odnosi się do przetworzonego tekstu bieżącej strony, tej samej przestrzeni indeksów, jakiej oczekuje każda właściwość czcionki, więc możesz przekazać go prosto do powyższej procedury. Przechowywanie go w CurrentCharIndex jest opcjonalne, ale wygodne: widok zachowuje go jako swoje pojęcie zogniskowanego glifu, co jest przydatne, jeśli inne części interfejsu użytkownika chcą odczytać zaznaczenie bez ponownego jego wyliczania

Osadzanie to właściwość, która ma znaczenie

W przypadku większości prawdziwych prac, jedynym pytaniem wartym odpowiedzi jest to, czy każda czcionka jest osadzona. Dokument, którego wszystkie czcionki są w nim zawarte, renderuje się tak samo na RIP-ie biura druku, laptopie kolegi i serwerze bez GUI w ogóle. Dokument, który opiera się na nieosadzonej czcionce Helvetica, gra w ciemno, że każda z tych maszyn ma pasujący krój, a kiedy ta próba się nie powiedzie, czytnik podstawia coś zbliżonego, metryki się przesuwają, a starannie ułożony formularz przestawia się na tyle, by ulec zniszczeniu. Przejście przez tekst strony i pogrupowanie czcionek według statusu osadzenia daje tę odpowiedź niewielkim kosztem

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;

Dwa szczegóły sprawiają, że to rozwiązanie jest uczciwe. Po pierwsze, CharacterCount przypada na stronę, więc audyt całego dokumentu oznacza ustawienie Pdf.PageNumber na każdą stronę po kolei, ponowne uruchomienie pętli i scalenie wyników. Po drugie, warstwa tekstu zawiera wygenerowane znaki, takie jak spacje, które czytnik wywnioskowuje między słowami, i za nimi nie kryje się żaden obiekt czcionki; sprawdzenie puste nazwy bazowej pomija je, zamiast logować jako fantom. Nazwa bazowa jest tu właściwym kluczem do deduplikacji, ponieważ prefiks podzbioru, który przenosi, odróżnia dwa różne podzbiory tej samej rodziny, a to zwykle chcesz wiedzieć

Wyciąganie osadzonego kroju

Gdy czcionka jest osadzona, możesz odczytać jej bajty bezpośrednio. FontData zwraca surowy program czcionki, te same dane TrueType lub CFF, które przenosi PDF, co wystarcza do napisania samodzielnego pliku czcionki lub do identyfikacji kroju na podstawie znanej biblioteki. Zwraca pustą tablicę, gdy czcionka nie jest osadzona, więc sprawdzenie osadzenia i sprawdzenie długości razem chronią operację zapisu

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;

Bajty to osadzony podzbiór, a nie oryginalna czcionka detaliczna, więc to, co otrzymujesz z powrotem, zwykle obejmuje tylko te glify, których dokument faktycznie użył. Jest to dokładnie to, czego potrzeba do celów kryminalistycznych i weryfikacji, ale słabo nadaje się do ponownego wykorzystania; podzbiór czcionki Times New Roman zawierający trzydzieści glifów nie jest czcionką, którą można zainstalować i nią pisać. Traktuj wyodrębnianie jako sposób na zbadanie tego, co zostało dostarczone, a nie jako narzędzie do odzyskiwania czcionek. Jeśli potrzebujesz pasującej nazwy bazowej do etykietowania wyjścia, odczytaj FontBaseName[CharIndex] obok danych, i usuń wiodący znacznik podzbioru, jeśli chcesz otrzymać samą rodzinę

Zrozumienie liczby określającej grubość

FontWeight zwraca liczbową klasę grubości, tę samą skalę od 100 do 900, której używa CSS, gdzie 400 to normalna czcionka (regular), a 700 to pogrubiona (bold). PDFium zgłasza wszystko, co deklaruje czcionka, a to nie zawsze jest okrągła setka; krój może komunikować 350 lub 650, a traktowanie wszystkiego od 600 wzwyż jako "na tyle pogrubione, że ma to znaczenie" sprawdza się lepiej niż testowanie na dokładnie 700. Kąt pochylenia jest sygnałem towarzyszącym: niezerowa wartość, zwykle ujemna, oznacza, że krój ma pochyły lub prawdziwie pochylony wygląd (italic), a zero oznacza projekt pionowy (upright). Razem pozwalają odróżnić fragment pogrubiony z kursywą od zwykłego bez konieczności renderowania czegokolwiek, co jest rodzajem sprawdzania, jakie przeprowadzają wsadowo narzędzia typu preflight lub audyty dostępności

Żaden z tych odczytów nie wymaga wyrenderowanej mapy bitowej. Pochodzą one z przeanalizowanej warstwy tekstu, więc otwarty dokument na odpowiedniej stronie to cała konfiguracja, jakiej potrzebujesz, co sprawia, że inspekcja czcionek jest tania w wykonaniu dla całego archiwum. Jeśli łączysz to z wyodrębnianiem tekstu, te same indeksy znaków pasują do tekstu, który pobierasz, więc czcionka glifu i jego wartość Unicode to dwa odczyty dla jednego indeksu. Towarzyszący artykuł na temat ekstrakcji tekstu z dokumentów PDF za pomocą komponentu PDFium obejmuje tę stronę warstwy tekstu bardziej szczegółowo

Właściwości czcionek przedstawione tutaj są częścią Komponentu PDFium Delphi VCL