Техническа статия

Извличане на текст от PDF файлове с PDFium Component в Delphi

Извличането на текст от PDF изглежда лесно, докато не се сблъскате с документ, при който текстовият слой липсва, повреден е или е разделен на десетки малки части без ясна последователност. PDFium Component предоставя две възможности: масива Character[] за директен достъп по индекс до всеки символ на страницата, и метода ReadablePageContent за структуриран изглед, който пресъздава абзаци и заглавия от структурата на таговете в PDF файла или чрез евристичен анализ. Нито един от тях не е универсален избор, затова е важно да се знае какво предлага всеки

Отваряне на документа и капанът с безшумен отказ

Компонентът TPdf отваря файл чрез задаване на FileName и промяна на Active := True. Важна подробност: задаването Active := True никога не генерира изключение. Ако файлът липсва, защитен е с парола или е повреден, PDFium обработва грешката вътрешно и Active остава на стойност False. Това означава, че всеки процес на извличане трябва да съдържа следната проверка:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Could not open PDF (damaged or wrong password)');
    Exit;
  end;
  // extraction follows here
finally
  Pdf.Active := False;
  Pdf.Free;
end;

Защитените с парола файлове изискват задаване на Pdf.Password := '...' преди Active := True. Няма втори шанс: ако Active върне грешка, трябва да затворите файла и да го отворите отново с правилната парола

Извличане страница по страница чрез Character[]

Най-ниско ниво на достъп се осигурява чрез обхождане на всеки символ на страницата. Задайте Pdf.PageNumber, за да заредите текстовия слой за тази страница, след което обходете елементите до CharacterCount чрез свойството Character[]. Струва си да се проверяват два флага за всеки елемент: CharacterGenerated[i] маркира генерирани символи, въведени от рендиращия модул (например тирета за пренасяне), които нямат реална Unicode стойност, а CharacterMapError[i] показва, че PDFium не е успял да напасне символа към Unicode код, което се случва при кодиране на шрифтове без ToUnicode таблица

procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
  Page, I: Integer;
  Line: string;
  Ch: WideChar;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    Line := '';
    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
        Continue;
      Ch := Pdf.Character[I];
      if Ch = #13 then
        Ch := #10;   // normalize CR to LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

Резултатът е обикновен низ от Unicode символи в реда, в който PDFium ги извлича (реда на появата им в потока от съдържание), което невинаги съвпада с реда за четене отляво надясно. За повечето документи на латиница, създадени със стандартни офис пакети, това е напълно достатъчно. При сканирани PDF файлове с разпознаване на текст (OCR) или при текст отдясно наляво, редът може да бъде грешен. В тези случаи методът ReadablePageContent е по-полезен

Структурирано извличане с ReadablePageContent

Методът ReadablePageContent работи на по-високо ниво: той връща запис TPdfReadableContent, чийто масив Fragments съдържа фрагменти с определен тип Kind, който идентифицира абзаци, заглавия, елементи от списъци, клетки на таблици и други. Когато PDF файлът съдържа структурирано дърво (проверете Pdf.IsTagged), източникът е rosStructure и редът на четене е гарантиран. При неструктурирани файлове PDFium се връща към rosHeuristic – евристичен анализ, който групира символите в логически единици въз основа на техните размери, но не гарантира пълна точност

procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
  Page: Integer;
  Content: TPdfReadableContent;
  Fragment: TPdfContentFragment;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Content := Pdf.ReadablePageContent(Page);
    for Fragment in Content.Fragments do
    begin
      case Fragment.Kind of
        cfHeading   : Output.Add('# ' + Fragment.Text);
        cfParagraph : Output.Add(Fragment.Text);
        cfListItem  : Output.Add('- ' + Fragment.Text);
      else
        Output.Add(Fragment.Text);
      end;
    end;
  end;
end;

Ако Content.Source = rosHeuristic и резултатът изглежда разбъркан, текстовият слой на документа вероятно не е записан с оглед на реда за четене. В този случай единственото сигурно решение е нов експорт от изходното приложение с коректни тагове или допълнителна обработка, която сортира координатите на символите първо по Y, а след това по X

Какво предоставят CharacterOrigin и CharacterRectangle

И двете свойства връщат позицията на символа в координатите на страницата (в точки, като началото е в долния ляв ъгъл, а Y нараства нагоре). CharacterOrigin[i] е базовата точка на символа; CharacterRectangle[i] е неговият пълен обхващащ правоъгълник. Това са градивните елементи за всяка обработка отвъд чистия текст: откриване на колони, групиране на символи в редове по Y координати или изграждане на селекция за четец. Ако трябва да разберете кой символ се намира под курсора на мишката, методът CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) извършва това търсене директно, без да е необходимо да обхождате правоъгълниците ръчно

Инсталиране на DLL библиотеката

Компонентът PDFium Component прехвърля целия анализ на PDF към нативна DLL библиотека – pdfium32.dll или pdfium64.dll, в зависимост от целевата платформа. Комплектът включва скрипт CopyDlls.bat, който копира правилния файл в системната директория на Windows. Изпълнението му като администратор веднъж на компютъра за разработка е достатъчно; за разпространение копирайте DLL файла в папката на самото приложение. Версиите с поддръжка на V8 (pdfium32v8.dll, pdfium64v8.dll) са много по-големи и са необходими само ако вашите PDF документи съдържат JavaScript код. За просто извличане на текст стандартната версия е най-добрият избор

Ако DLL файлът липсва по време на работа, задаването на Active := True ще приключи с безшумен отказ, точно както при липсващ файл, тъй като компонентът улавя грешката при зареждане вътрешно. Винаги тествайте на чиста машина преди разпространение

Използване на FontSize[] заедно с Character[] за анализ на оформлението

Отвъд чистия текст, програмният интерфейс предоставя свойството FontSize[i], което връща размера на всеки символ в точки. Комбинирано с CharacterOrigin[i] и CharacterRectangle[i], това ви позволява да разграничавате основния текст от заглавията, без да разчитате на структурираното дърво. Поредица от символи, при която размерът на шрифта се увеличава над определен праг, почти сигурно представлява заглавие. Същият метод може да се използва за откриване на подписи под картинки (малък текст под обхвата на изображението) или бележки под линия (малък текст в долната част на страницата). Нищо от това не изисква рендиране; и трите свойства четат данни директно от текстовия слой, който PDFium изгражда при Active := True

Един нюанс: FontSize[i] отразява размера след прилагане на матрицата на трансформация на страницата (CTM), така че документ, чиито страници са били мащабарини при създаването, ще върне съответно променени размери. Ако сравнявате размери между страници с различни размери, ги нормализирайте спрямо височината на MediaBox на страницата, преди да вземате решения въз основа на прагови стойности

Записване на резултата във файл

Класът TStringList в Delphi поддържа UTF-8 изходни данни коректно от XE насам. Задайте WriteBOM := False, ако имате нужда от файл без BOM маркер (много софтуерни инструменти не поддържат водещ BOM маркер):

var
  Lines: TStringList;
begin
  Lines := TStringList.Create;
  try
    ExtractAllText(Pdf, Lines);
    Lines.WriteBOM := False;
    Lines.SaveToFile('output.txt', TEncoding.UTF8);
  finally
    Lines.Free;
  end;
end;

За много големи документи, където паметта е ограничена, пишете директно в TStreamWriter с TEncoding.UTF8 в рамките на цикъла за страници, вместо първо да натрупвате целия текст в списък

Програмните интерфейси Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent и CharacterIndexAtPos, показани тук, са част от PDFium Component за Delphi и C++Builder