HotPDF Component извлича Unicode текст от всеки PDF, който заредите в Delphi, чрез две извиквания: ExtractLoadedPageText връща текста на страницата според потока на четене, а ExtractLoadedPageTextLayout (добавен във v2.263.0) възстановява визуалното оформление на страницата като чист текст, така че колоните, отстъпите и подравняването на таблиците се запазват в изходния резултат; И двете работят върху документи, които HotPDF не е създал, което всъщност е важната ситуация: фактурата, която клиент ви е изпратил по имейл, отчетът, доставен от сканиращо бюро, договорът, генериран от софтуер, чието име вече никой не си спомня
Постигането на това изисква повече механизми, отколкото двете декларации предполагат, тъй като PDF не съхранява текст по начин, по който го прави текстов файл; Тази статия разглежда и двата режима на извличане, след което надниква под капака на трите компонента под тях – четеца на CMap, интерпретатора на потоци от съдържание и резервната верига за декодиране на шрифтове – защото разбирането на начина, по който работи съпоставянето, определя разликата между това просто да вдигнете рамене пред нечетлив изход и да го диагностицирате
Защо извличането на текст е по-трудно от простото четене на низове от файла?
Потокът от съдържание на PDF записва кодове на символи, а не самите символи; Операторите Tj и TJ (ISO 32000-1 §9.4.3) носят низове от байтове, чието значение зависи изцяло от шрифта, избран от предходния Tf: байт 0x41 може да бъде буквата A под WinAnsi, произволен глиф във вградено подмножество от шрифт или половината от двубайтов CID в композитен CJK шрифт; ISO 32000-1 §9.10 дефинира извличането на текст именно като този проблем на декодиране – съпоставяне на всеки код обратно към Unicode с помощта на всяка информация, предоставена от речника на шрифта – и стандартът изрично посочва, че от съответстващия файл не се изисква да предоставя достатъчно информация за това
Тази последна клауза обяснява всеки отчет за грешка от типа „защо копирането и поставянето от този PDF дава нечетливи символи“, който някога сте виждали; Създател на документи, който вгражда подмножество от шрифт без таблица /ToUnicode, е записал файл, който се изобразява перфектно, но се извлича като безсмислица, тъй като съпоставянето код-към-глиф съществува, но съпоставянето код-към-Unicode никога не е било доставено; Следователно всеки честен API за извличане е верига от резервни варианти, базирана на най-добрите усилия, и полезният въпрос е колко дълбока е тази верига
Извличане според потока на четене с ExtractLoadedPageText
За индексиране на търсене, съпоставяне на ключови думи или подаване на текст към конвейер за анализ, ExtractLoadedPageText е извикването, от което се нуждаете; Сигнатурата е function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean – индексите на страниците са с база нула, резултатът се получава как вграден в Delphi UnicodeString и функцията връща False, когато страницата няма четлив поток от съдържание, вместо да предизвиква изключение
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// AllText вече съдържа текста на документа според потока на четене
finally
Pdf.Free;
end;
end;
Новите редове в изходния резултат идват от умишлено проста евристика: когато вертикалното начало на даден глиф се премести с повече от половината от текущия размер на шрифта – белег за стъпка Td или T* в потока от съдържание – се вмъква нов ред; Символите, които декодерът не може да разчете, се превръщат в интервали, вместо да изчезват, така че границите на думите се запазват дори когато отделни глифове не могат да бъдат разчетени; Това, което този режим не се опитва да направи, е групиране по реда на четене или откриване на множество колони: страница с две колони излиза преплетена по реда на потока от съдържание, което обикновено, но не винаги, е визуалният ред
Кога вместо това трябва да използвате извличане със запазване на оформлението?
ExtractLoadedPageTextLayout е правилното извикване винаги, когато позицията носи смисъл: таблици, формуляри, списъци с код, всичко, което възнамерявате да сравнявате с diff, grep или да анализирате по колони; Вместо да свива глифовете в поток, той ги групира в базови линии, сортира всяка базова линия по X и възпроизвежда хоризонталното и вертикалното празно пространство върху мрежа от символи с фиксирана ширина, оразмерена според средното отместване на глифа и размера на шрифта; Широките разстояния между текстовите секции на една и съща базова линия се превръщат в интервали; големите разстояния между базовите линии се превръщат в празни редове; Резултатът се чете така, както изглежда страницата
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Колоните, отстъпите и подравняването на таблиците се запазват като
// интервали и празни редове върху символна мрежа
end;
Двата режима споделят всеки байт от механизма за декодиране и се различават само по това как подреждат декодираните глифове, така че изборът не струва нищо по отношение на точността; Изберете ExtractLoadedPageText, когато само думите имат значение, и ExtractLoadedPageTextLayout, когато оформлението е важно; Откриването на реда на четене за множество колони остава извън обхвата и за двата режима – мрежово изобразяване на страница с две колони ви показва и двете колони една до друга, което за сравнение с diff е точно това, което трябва, но за пренареждане на проза не е
Как HotPDF декодира кодовете на символите към Unicode?
HotPDF Component декодира всеки код на символ чрез подредена по приоритет верига от резервни варианти: първо вградената в шрифта /ToUnicode CMap, след това записа /Encoding (поток или именована CMap), след това — за композитни шрифтове — стандартните CMap файлове на Adobe за колекции от символи като Adobe-GB1, Adobe-CNS1, Adobe-Japan1 и Adobe-KR, и накрая вградените таблици WinAnsi and MacRoman за прости шрифтове; Стратегия, която не може да даде отговор, преминава тихо към следващата, вместо да предизвиква изключение, а код, който изчерпва цялата верига, се декодира до 0, за да може извикващият да преброи пропуските, вместо да гадае
Таблицата /ToUnicode CMap (ISO 32000-1 §9.10.3) е на първо място, защото тя е съпоставянето, което създателят е записал специално за извличане; Пътят на стандартните CMap файлове на Adobe е важен за CJK документи, които използват предефинирани CMaps като UniGB-UTF16-H, вместо да вграждат нещо: HotPDF доставя файловете от колекцията в своята директория resources\CMap, локализира ги спрямо изпълнимия файл по време на работа и кешира всяка анализирана карта за целия процес — това е добре да се знае, тъй като най-голямата от тях, картата Adobe-GB1, е приблизително 2 MB изходен текст, който не искате да анализирате повторно за всяка страница; Ако директорията липсва, декодерът просто пропуска CMap файловете на диска и работи с вградените таблици плюс вградените кодирания; Това е огледалният образ от страната на четене на проблема с оформянето, разгледан в оформяне на текст със сложни писмености с HotPDF, където същата разлика между код и глиф се среща при записването
Два капана в синтаксиса на CMap, които си струва да знаете
CMap файловете изглеждат лесни за анализиране, но не са, и две подробности са причина за повечето неуспехи при първия опит за изграждане на анализатор; Първата е, че броят на записите идва преди ключовата дума за раздела: даден раздел се чете като 2 beginbfchar, а не като beginbfchar 2; Анализатор, който очаква броя след ключовата дума, консумира числото като излишен маркер и след това намира нула записи във всеки раздел; Стабилният подход — този, на който се спря четецът на HotPDF — е да игнорира напълно броя и да върти цикъл до съответната ключова дума endbfchar / endbfrange, което има предимството да толерира реални файлове, чиито бройки са просто грешни
Вторият капан е, че целите на bfchar и bfrange са UTF-16BE низове, а не цели числа; Дестинацията <D83DDE00> означава U+1F600 — сурогатна двойка, която трябва да бъде рекомбинирана в една кодова точка — и четенето на тези четири байта като голямо-ендиан цяло число дава безсмислена стойност за всяка кодова точка извън Основната многоезична равнина; Емотиконите в PDF файловете вече не са екзотика, така че декодер, който пропуска рекомбинацията на сурогатни двойки, се проваля с файлове, които вашите потребители действително притежават; HotPDF анализира първо шестнадесетичния литерал до необработени байтове, след което рекомбинира UTF-16BE кодови единици, което също обхваща целите с множество знаци, произвеждани от съпоставянията на лигатури
Слизане до ниво глифове с ExtractLoadedPageGlyphs
И двете текстови извиквания са изградени върху ExtractLoadedPageGlyphs, а базовият THPDFGlyphArray е достъпен и за вашия код; Всеки THPDFGlyphRecord носи декодираната кодова точка Unicode заедно с необработения код на символа, ширината на кода в байтове (1, 2 или 4, определена от codespacerange на CMap), активния ключ и размер на ресурса на шрифта, началото по X и Y в потребителското пространство и хоризонталното отместване; Това е достатъчно за изграждане на откриване на граници на думи, позиционирано маркиране или персонализиран алгоритъм за оформление, без сами да се налага да докосвате потока от съдържание
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
[Unresolved, Length(Glyphs)]);
end;
end;
Преброяването на записи с Unicode = 0, както е по-горе, е честният начин за измерване на качеството на извличане на даден документ, преди да се доверите на текста по-нататък; Записите на глифове също така свързват всеки символ с изходния операнд в потока от съдържание, което прави възможно търсенето и замяната на текст в зареден документ на HotPDF върху същата основа
Кои PDF файлове няма да предадат своя текст?
Някои файлове побеждават всеки екстрактор и е по-добре да ги откриете, отколкото да доставите техния изходен резултат; Сканираните документи са най-очевидният случай: страница, която е едно голямо изображение, изобщо не съдържа оператори за текст, така че извличането правилно връща празен низ – решението е OCR, а извличането на изображенията на страниците от заредения PDF е първата стъпка от този конвейер; Подмножествата от шрифтове без таблица /ToUnicode са по-трудният случай: ако пътят на /Encoding и стандартните CMaps също се окажат празни, тези глифове се декодират до 0 и се появяват като интервали в текстовите извиквания; Шифрованите документи се извличат нормално, при условие че ги заредите с тяхната парола чрез претоварването на LoadFromFile, така че потоците се дешифрират, преди интерпретаторът изобщо да ги види
Едно по-тясно ограничение си струва да се каже ясно: веригата за декодиране чете CMap и потоци от съдържание през Flate пътя на HotPDF, така че шрифт, чийто ToUnicode поток използва необичаен филтър, преминава към следващата стратегия, вместо да провали страницата; На практика FlateDecode обхваща почти всичко, произведено през последните две десетилетия, и преминаването е тихо по дизайн – получавате най-добрия текст, който файлът позволява, вместо изключение; Същият механизъм за обекти от страната на четене, който разрешава речниците на шрифтовете тук, захранва и редактирането на метаданни в заредени документи, така че конвейер за въвеждане на документи може да извлича, инспектира и анотира с едно преминаване
Извличането на текст, рендерирането със запазване на оформлението, достъпът на ниво глифове и функциите за търсене и замяна, изградени върху тях, са част от стандартния HotPDF Component за Delphi и C++Builder — без външни DLL файлове, без текстови услуги на операционната система, просто Object Pascal, през който можете да преминете стъпка по стъпка, когато странен файл попадне във вашата опашка