HotPDF Component вилучає текст Unicode з будь-кого завантаженого в Delphi PDF за допомогою двох викликів: 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 now holds the reading-flow text of the document
finally
Pdf.Free;
end;
end;
Розриви рядків у вихідних даних з'являються завдяки простому евристичному правилу: коли вертикальний початок гліфа зміщується на відстань, що перевищує половину поточного розміру шрифту — ознака кроку Td або T* у потоці вмісту — вставляється символ нового рядка. Символи, які декодер не може розпізнати, стають пробілами замість того, щоб зникнути, тому межі слів зберігаються, навіть коли окремі гліфи втрачаються. Чого цей режим не намагається робити, так це визначати порядок читання або виявляти кілька стовпців: двоколонкова сторінка виводиться з чергуванням у порядку потоку вмісту, що зазвичай, але не завжди, відповідає візуальному порядку
Коли замість цього слід використовувати вилучення зі збереженням макета?
ExtractLoadedPageTextLayout є правильним викликом, коли позиція має значення: таблиці, форми, лістинги коду, усе, що ви плануєте порівнювати, шукати за допомогою grep або розбирати по стовпцях. Замість згладжування гліфів у потік, він групує їх у базові лінії, сортує кожну базову лінію за віссю X та відтворює горизонтальні й вертикальні пробіли на моноширинній сітці символів, розмір якої визначається за медіанним зсувом гліфа та розміром шрифту. Великі проміжки між фрагментами на одній базовій лінії стають пробілами; великі проміжки між базовими лініями стають порожніми рядками. Результат читається так, як виглядає сторінка
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Columns, indentation and table alignment survive as
// spaces and blank lines on a character grid
end;
Обидва режими спільно використовують кожен байт механізму декодування і відрізняються лише тим, як вони розташовують декодовані гліфи, тому вибір нічого не коштує з точки зору точності. Вибирайте ExtractLoadedPageText, коли важливі лише слова, і ExtractLoadedPageTextLayout, коли важливо саме розташування. Визначення порядку читання у кількох стовпцях залишається поза межами обох режимів — сітковий рендеринг двоколонкової сторінки показує обидва стовпці поруч, що для порівняння є ідеальним варіантом, але не підходить для перетікання прози
Як HotPDF декодує коди символів у Unicode?
Як HotPDF декодує коди символів у Unicode?
HotPDF Component розпізнає кожен код символу за допомогою пріоритетного ланцюжка резервних варіантів: спочатку вбудований у шрифт CMap /ToUnicode, потім запис /Encoding (потік або іменований CMap), потім — для складених шрифтів — стандартні файли CMap від Adobe для колекцій символів, таких як Adobe-GB1, Adobe-CNS1, Adobe-Japan1 та Adobe-KR, і нарешті вбудовані таблиці WinAnsi та MacRoman для простих шрифтів. Стратегія, яка не може дати відповіді, мовчки переходить до наступної замість виклику винятку, а код, який вичерпує весь ланцюжок, повертає 0, щоб викликач міг порахувати пропуски замість ворожіння
CMap /ToUnicode (ISO 32000-1 §9.10.3) знаходиться на першому місці, оскільки це саме те відображення, яке генератор написав спеціально для вилучення. Шлях стандартного CMap від Adobe важливий для документів CJK, які використовують визначені CMaps, такі як UniGB-UTF16-H, замість вбудовування будь-чого: HotPDF постачає файли колекції у своїй директорії resources\CMap, знаходить їх відносно виконуваного файлу під час виконання та кешує кожну розібрану карту для кожного процесу — це варто знати, оскільки найбільша з них, карта Adobe-GB1, містить приблизно 2 МБ вихідного тексту, який ви не захочете розбирати заново на кожній сторінці. Якщо директорія відсутня, декодер просто пропускає дискові CMaps і працює з вбудованими таблицями та вбудованими кодуваннями. Це зворотний бік проблеми формування тексту, описаної в формуванні тексту складних сценаріїв з 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, який ви можете проходити крок за кроком, коли нетиповий файл потрапляє у вашу чергу