Technical Article

Извличане на текст, изображения и шрифтове от PDF в Delphi с PDFlibPas

Извличането на текст, изображения и шрифтове от съществуващ PDF изглежда лесна задача, докато не се сблъскате с реални документи. Пуснете модул за търсене върху четиридесет хиляди клиентски файла и грешките ще се разпределят в няколко добре познати групи. Думите се сливат, тъй като никой не е съобщил на модула колко голямо разстояние се брои за интервал. Други страници се връщат като нечетливи символи, защото подмножеството от шрифтове (subsetted font) не съдържа съпоставяне от кодовете на глифовете към реалните символи. А „логотÐ?на компаниятаâ€?се оказва девет отделни обекта на изображения, подредени зад маска. Нищо от това не е грешка в библиотеката, това е разликата между обикновеното извикване на функция за извличане и разбирането какво точно може и какво не може да се възстанови от байтовете на диска.

losLab PDF Library за Pascal предоставя на Delphi и C++Builder разработчиците повече от един начин за четене на тези три потока, като нивата се различават по своите гаранции. Ключът е в съпоставянето на нивото към задачата: индекс за търсене, рецензиране за цензуриране (redaction) и проверка за PDF/A съвместимост изискват различни неща от една и съща страница. Използването на грешно извикване води до излишни усилия или ненадеждни резултати.

Нива на извличане на текст и техните гаранции

GetPageText приема стойност на опциите от 0 до 8, като това число определя машината (engine) за обработка, а не формата. Стойности от 0 до 2 изпълняват бърз проход, подходящ за предварителен преглед. Стойности от 3 до 8 се насочват към машина, съобразена с оформлението на страницата, която възстановява редовете и интервалите въз основа на реалната позиция на глифовете. В този диапазон разликите са съществени: 4 и 6 разделят текста на думи, 5 и 6 връщат ширината на всеки глиф, а 7 извлича чист текст без шрифтове, цветове и метаданни. Опция 7 е изходът за индексиране при търсене, тъй като то се нуждае само от думи.

Нито една опция не може да спаси документ, който изначално не съдържа нужната информация. PDF съпоставя кодовете на символите с формите на глифовете, и единственото нещо, което превежда тези кодове обратно в четим текст, е таблицата ToUnicode CMap на шрифта (ISO 32000-1 §9.10). Когато даден шрифт е вграден частично (subsetted) без такава таблица, извличането е невъзможно. Тази библиотека, функцията за копиране в четеца или други инструменти, всички те се ограничават до отгатване по имената на глифовете или не връщат нищо. Правилното решение е разпознаване, а не сложни опити за корекция. Маркирайте страницата с ниска степен на надеждност и я изпратете към OCR, тъй като тихото индексиране на нечетливи символи е по-лошо от признаването, че файлът не може да бъде прочетен.

За случаите, които стандартните опции не покриват, като персонализирана токенизация, анализ на потока от съдържание или обработка по ваши правила, декодерът е наличен едно ниво по-долу. Класът TPDFExtractor се изгражда върху речника с ресурси на страницата и колекцията от шрифтове. Неговият метод ExtractTextW превежда операциите с текст от потока през механизмите на шрифта за възстановяване на Unicode, а събитието OnFindObject ви предава всеки обект в процеса на обработка. Повечето приложения нямат нужда от такъв детайлен достъп, но тези, които имат, оценяват наличието на този публичен слой.

Позиционирани блокове: основа за търсене и цензуриране

Чистият текст ви казва какво пише на страницата. Рано или късно обаче даден продукт трябва да знае и къде точно се намира този текст, за маркиране при търсене, очертаване на зони за цензуриране или поставяне на анотации. ExtractPageTextBlocks връща дескриптор към списък с текстови сегменти, всеки от които съдържа текст, ограничаващ правоъгълник (bounding box), име и размер на използвания шрифт:

var

  Pdf: TPDFlib;

  Blocks, I: Integer;

begin

  Pdf := TPDFlib.Create;

  try

    if Pdf.LoadFromFile('contract.pdf', '') <> 1 then

      raise Exception.Create('load failed');

    Pdf.SelectPage(1);

    Blocks := Pdf.ExtractPageTextBlocks(0);

    for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do

      Writeln(Format('%s  [%s %.1f pt at %.0f,%.0f]',

        [Pdf.GetTextBlockText(Blocks, I),

         Pdf.GetTextBlockFontName(Blocks, I),

         Pdf.GetTextBlockFontSize(Blocks, I),

         Pdf.GetTextBlockBound(Blocks, I, 0),

         Pdf.GetTextBlockBound(Blocks, I, 1)]));

    Pdf.ReleaseTextBlocks(Blocks);

  finally

    Pdf.Free;

  end;

end;

Ена подробност в тази област води до най-много грешки при интеграцията. Параметрите SetTextExtractionArea, SetTextExtractionWordGap и SetTextExtractionOptions представляват състояния на ниво документ, които се запазват, а не аргументи за всяко извикване. Ако ограничите областта за четене на заглавната лента за класифициране на документ, това ограничение тихомълком ще засегне всички последващи извличания за същия дескриптор, включително повикванията на GetPageText. Препоръчително е да нулирате състоянието за извличане между отделните задачи или да използвате отделен дескриптор за всяка изходна сесия.

Прагът за разстояние между думите е инструментът срещу първия тип грешки, слетите думи. SetTextExtractionWordGap указва на машината за оформление колко хоризонтално пространство (спрямо собствения интервал на глифовете) разделя думите. Една гъста таблица изисква по-малък интервал от маркетингова страница, така че настройването на този праг за конкретния клас документи е по-добро решение от една глобална константа. Това състояние се запазва за документа, така че го настройвайте съзнателно.

Изображения: оригинални потоци, а не екранни снимки

Грешният начин за извличане на изображения от PDF е рендирането и изрязването на страницата. Това променя разделителната способност на пикселите, прилага ротация и унищожава оригиналния формат. Методът GetPageImageList извлича реалните ресурси от изображения, реферирани от страницата, като всеки елемент връща своите свойства и оригинални данни без промяна:

var

  ImgList, I: Integer;

begin

  Pdf.SelectPage(1);

  ImgList := Pdf.GetPageImageList(0);

  for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do

  begin

    Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));

    Pdf.SaveImageListItemDataToFile(ImgList, I, 0,

      Format('page1-img%.2d.bin', [I]));

  end;

  Pdf.ReleaseImageList(ImgList);

end;

Проверявайте GetImageListItemFormatDesc, преди да правите предположения за даден елемент, тъй като ресурсите на страницата рядко са просто едно изображение. Алфа маската (soft mask) се показва като отделен запис. Един и същ XObject често се повтаря на много страници, така че дедупликирайте по съдържание на хеша преди архив, за да не записвате едно и също лого стотици пъти. Изображенията в режим CMYK JPEG изискват управление на цветовете при последваща обработка, иначе се визуализират с обърнати цветове. Когато искате регистър на ниво документ, FindImages заедно с SetFindImagesMode сканира целия файл наведнъж.

Има една граница, която си струва да се обсъди предварително: извличането на изображения връща само растерни ресурси. Лого или графика, начертани с векторни линии, не са изображение в контекста на ресурсите и няма да се появят в списъка с изображения, независимо колко ясно се виждат на екрана. Ако е необходимо да извлечете такава графика, трябва да рендирате съответната област в битмап, което е различна операция. Двата типа резултати не трябва да се смесват без ясно обозначение.

Шрифтове: одит, а не експорт

Интерфейсът за шрифтове отговаря на въпроси за самите шрифтове. Той не извлича самите файлове на шрифтовете и това определя начина му на използване. След като FindFonts сканира документа, преброяването преминава през шрифтовете по техния ID, а повикванията за свойства се отнасят за текущо избрания шрифт:

var

  I: Integer;

begin

  Pdf.FindFonts;

  for I := 1 to Pdf.FontCount do        // font indexes start at 1, not 0

    if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then

      Writeln(Format('%s  type=%d  embedded=%d  subset=%d',

        [Pdf.FontName, Pdf.FontType,

         Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));

end;

Внимавайте с границите на цикъла. Индексите на шрифтовете са от 1 до FontCount, докато текстовите блокове и списъците с изображения са с базирани на нула индекси. Смесването на двата подхода води до грешка с единица, която или пропуска първия елемент, или надхвърля границите на масива. Освен това, този API не предлага байтов експорт на шрифтове, никое повикване не връща вградения шрифт като TTF или OTF файл. Моделът е проектиран за анализ и проверка на метаданни. Това е напълно достатъчно за реалните нужди: откриване на частично вградени шрифтове, проверка на вграждането преди архивиране (неинтегрираният шрифт блокира PDF/A съвместимостта, както е описано в PDF/A и PDF/UA проверка в Delphi) и диагностика при проблеми с кодирането.

Последното повикване е изключително полезно при първоначална проверка. Извикайте GetFontEncoding за всеки шрифт и го анализирайте заедно с флага за частично вграждане (subset), за да предвидите качеството на извличане на текст. Страница, чиито шрифтове са частично вградени с нестандартни кодирания, е идеален кандидат за OCR, което спестява време от излишни опити за текстово извличане.

Мащабно извличане без зареждане на документи

При пакетна обработка зареждането на цял документ само за четене на една страница хаби излишни ресурси. Вариантите с едно извикване ExtractFilePageText and ExtractFilePageTextBlocks приемат име на файл, парола и номер на страница директно, като избягват пълното зареждане. За изключително големи файлове е наличен още по-ефективен метод. Директният достъп отваря файл чрез четене на xref таблицата, така че DAOpenFileReadOnly, последвано от DAExtractPageText, зарежда само обектите, необходими за конкретната страница. Това изисква промяна в подхода: функциите за директен достъп адресират страниците чрез PageRef (дескриптор за референция към обект), получен от DAFindPage, а не чрез чист номер на страница. Подаването на номер на страница вместо дескриптор води до грешни резултати без съобщение за грешка. Повече за инструментите за директен достъп вижте в сливане, разделяне и директен достъп до големи PDF файлове.

За да пишете стабилен код за извличане, винаги третирайте страницата като ненадежден източник на данни. Несъответствието между извлечения текст и визуализираното съдържание почти винаги се дължи на проблеми с кодирането, слети знаци или липсващи таблици ToUnicode в шрифтовете. Решението е да оценявате надеждността и да пренасочвате лошите страници към OCR. Настройте правилно процесите си за анализ на шрифтове и следете състоянието на параметрите за извличане (като ограничаващия правоъгълник) през целия жизнен цикъл на дескриптора.

Демонстрационни проекти, пробни версии и пълното описание на API са налични на продуктовата страница на losLab PDF Library за Delphi.