Вилучення таблиць у PDFium Component, від версії 3.117.0, трактує тонкий залитий прямокутник як лінійку таблиці. З увімкненим DetectFilledRulings, а це типове значення, залитий вирівняний по осях прямокутник завтовшки не більше за MaxRulingThickness (3 пункти) стає однією лінійкою вздовж своєї довгої осі, більший залитий прямокутник дає свої чотири краї, а кожну координату лінійки притягують у межах RulingSnapTolerance (4 пункти) перш ніж сітка збирається. Тож таблиці, експортовані з Word, Google Docs і браузерів, доходять до детектора розлінованих таблиць як повні сітки, а не провалюються у виявлення за пробілами як фрагменти
Раніша стаття про виявлення й вилучення таблиць стверджувала, що виявлення розлінованих таблиць користується намальованими лініями й що кожен обведений сегмент шляху перетворюється в координати сторінки. Це речення було правдиве й неповне. Підрахунок об'єктів шляхів на наборі з 13 реальних зразків документів показав, що 9 із них не містять жодного обведеного шляху взагалі, але кожна з їхніх сторінок несе сотні залитих прямокутників завтовшки від 0.5 до 1 пункта. Детектор, який знав лише обведення, не бачив нічого, кожна сторінка провалювалася у виявлення за пробілами, і виводом був розсип дрібних фрагментів замість таблиць. Пресет компактних колонок, доданий у 3.116.4, пом'якшував це на рівні фрагментів; першопричиною було те, що детектор читав не той оператор малювання
Чому таблиця, експортована з Word, не має обведених ліній?
Текстовий процесор не думає про рамку як про лінію; він думає про неї як про прямокутник зі шириною й малює цей прямокутник заливкою. ISO 32000-1 §8.5.2.1 визначає оператор re як додавання підшляху прямокутника, а §8.5.3 розділяє оператори малювання: S обводить шлях поточною товщиною лінії, f заливає його нутро. Рамка клітинки завтовшки 0.5 пункта виходить як x y w 0.5 re f, і механізм обведення — з товщиною лінії, з'єднаннями й штрихуванням — не запускається взагалі. Заливка клітинки — та сама конструкція з більшим прямокутником. Обведена сітка, намальована через m, l і S, — це те, чого очікував початковий детектор, і це те, чого не робить майже ніщо з експортованого офісними застосунками:
% одна рамка клітинки з експорту текстового процесора: залитий прямокутник заввишки 0.5 pt
72 700 468 0.5 re f
% заливка клітинки: залитий прямокутник розміром із клітинку
72 676 117 24 re f
% обведена лінія сітки, під яку писався початковий детектор
72 700 m 540 700 l S
Для детектора, який питає FPDFPath_GetDrawMode лише про те, чи виставлено прапорець обведення, обидва залитих прямокутники невидимі. Слова всередині клітинок тоді доходять до виявлення за пробілами, де колонки, розділені проміжком у 6 пунктів, сидять нижче типового MinColumnGap у 12 пунктів, і повертається та підмножина рядків, яка випадково вирівняна достатньо, щоб пройти MinRows. Це і є поведінка фрагментів, і жодне налаштування параметрів не перетворює її на ту сітку, яку автор намалював
Як PDFium Component перетворює залитий прямокутник на лінійку?
TableCollectObjectRulings оглядає кожен об'єкт шляху по одному підшляху за раз. Режим малювання береться з FPDFPath_GetDrawMode; шлях рахується залитим, коли DetectFilledRulings увімкнено й режим заливки не «none». Кожну точку перетворюють через матрицю об'єкта й збирають, до MaxSubpathPoints (8) на підшлях, і будь-який сегмент кривої позначає підшлях як кривий. Коли підшлях закривається або починається новий MoveTo, FlushSubpath вирішує, чим він був: кривий підшлях відкидається, як і будь-який замкнений багатокутник, чиї точки не лежать усі в межах PointTolerance (0.05 пункта) від країв обмежувального прямокутника хоча б по одній осі. Трикутник, шеврон чи заокруглений язичок ніколи не стає лінійкою — саме це й тримає декоративну графіку поза сіткою
Те, що виживає, — це вирівняний по осях прямокутник, класифікований за його обмежувальним прямокутником. Ширина на рівні MaxRulingThickness або менша при більшій висоті дає одну вертикальну лінійку на горизонтальному центрі, що охоплює прямокутник знизу вгору; дзеркальний випадок дає одну горизонтальну лінійку. Обидва виміри вище порога означають залиту клітинку, і прямокутник дає чотири лінійки, по одній на край. Обидва виміри на порозі або нижче не дають нічого, тож квадратний маркер на 2 пункти не сплутати з лінією. Обведений шлях іде старішим маршрутом через AddLine, по одній лінійці на вирівняний по осях сегмент, тож сітка, намальована через S, обробляється точно як раніше, а шлях, намальований і заливкою, і обведенням, дає частини, що перекриваються, які згортає прохід злиття:
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
Mode: string;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'itinerary-from-word.pdf';
Pdf.LoadDocument;
Pdf.PageNumber := 1; // з відліком від 1
Options := TPdfTableExtractionOptions.Default;
// це типові значення 3.117.0, виписані для ясності
Options.DetectFilledRulings := True; // тонкі залиті прямокутники стають лінійками
Options.MaxRulingThickness := 3.0; // пункти; товщі прямокутники рахуються як заливка
Options.RulingSnapTolerance := 4.0; // пункти; 0 вимикає притягування
Options.IncludeFormXObjects := True;
Tables := Pdf.ExtractTables(Options);
for I := 0 to High(Tables) do
begin
if Tables[I].DetectionMode = ptdmRuled then
Mode := 'ruled'
else
Mode := 'whitespace';
Writeln(Format('%dx%d %s, confidence %.2f',
[Tables[I].RowCount, Tables[I].ColumnCount, Mode,
Tables[I].Confidence]));
end;
finally
Pdf.Free;
end;
end;
Що RulingSnapTolerance дає таблицям із залитих клітинок?
RulingSnapTolerance — це те, що змушує таблицю, збудовану з самих лише заливок, зчепитися в одну сітку. Деякі експорти не малюють рамок взагалі: кожна клітинка — залитий прямокутник свого кольору, а сусідні прямокутники розділені проміжком у 1–3 пункти білого. Кожен прямокутник дає чотири лінійки-краї, але правий край однієї клітинки й лівий край наступної стоять за 2 пункти один від одного, а перевірка зв'язності використовує RulingTolerance, який типово дорівнює 1 пункту. Без притягування кожна клітинка утворює власну зв'язну компоненту з чотирьох лінійок, жодна компонента не дістає до MinRows, і сторінка не повідомляє нічого. TableSnapRulings збирає кожну X-координату, що задіяна (позицію кожної вертикальної лінійки плюс початок і кінець кожної горизонтальної), і так само кожну Y-координату, сортує кожен список, кластеризує його, зчіплюючи значення, сусіди яких різняться не більше ніж на допуск, замінює кожен кластер його середнім, а тоді зсуває кожну позицію, початок і кінець до найближчого центра кластера. Два боки проміжку стають однією лінією, і зв'язність тримається
Притягування виконується до TableMergeRulings, який сортує лінійки й з'єднує колінеарні частини, що торкаються або перекриваються в межах RulingTolerance, і обидва проходи виконуються до того, як TableDetectRuled узагалі побачить дані, тож попарна перевірка зв'язності пропорційна кількості ліній сітки, а не кількості фрагментів на клітинку. На обведеній сітці ці проходи нешкідливі, бо координати, які вже були ідентичні, притягуються самі до себе. Єдине, що варто мати на увазі: кластеризація ланцюжком не має власної межі ширини — послідовність координат із кроком 3 пункти згортається в один центр. За типового значення 4 пункти це зачіпає лише колонки вужчі за символ, але якщо в документі є справжні 3-пунктові проміжки, які мусять лишитися окремими, знизьте допуск або виставте 0, щоб вимкнути притягування:
// Ізолювати стратегію розлінованих таблиць і порівняти, що кожне налаштування бачить на одній сторінці
function CountRuledTables(Pdf: TPdf; FilledRulings: Boolean;
SnapTolerance: Double): Integer;
var
Options: TPdfTableExtractionOptions;
begin
Options := TPdfTableExtractionOptions.Default;
Options.DetectWhitespaceTables := False;
Options.DetectFilledRulings := FilledRulings;
Options.RulingSnapTolerance := SnapTolerance;
Result := Length(Pdf.ExtractTables(Options));
end;
// Експорт із Word типово дає 0, N, а потім менше за N:
// лише обведення не бачить нічого, притягування з'єднує залиті клітинки,
// а вимкнене притягування лишає кожну залиту клітинку окремим островом
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));
Лінійки всередині form XObject
Інструменти верстки часто загортають таблицю, або й усе тіло сторінки, у form XObject і малюють його через Do. ISO 32000-1 §8.10.1 визначає, що матриця форми конкатенується з поточною матрицею перетворення, коли форму малюють, тож прямокутник усередині форми живе в просторі форми й потрапляє на сторінку лише після двох або більше перетворень. TableCollectObjectRulings рекурсивно заходить у form-об'єкти, коли виставлено IncludeFormXObjects: він читає матрицю об'єкта, поєднує її з батьківською матрицею через TableMultiplyMatrix, чий порядок аргументів означає «відобразити через першу матрицю, потім через другу», і перелічує дочірні об'єкти через FPDFFormObj_CountObjects та FPDFFormObj_GetObject, передаючи поєднану матрицю вниз. Вкладеність глибша за MaxFormDepth (8) мовчки пропускається — це запобіжник проти патологічних файлів, а не межа, до якої наближається якийсь реальний експорт. Порядок множення важливий з тієї ж причини, що обговорюється в множенні матриць «спереду чи ззаду»: перестановка операндів зсуває доданок перенесення, і лінійка, яка мала приземлитися вгорі сторінки, приземляється в початок координат
Чому бюджет лінійок зріс учетверо?
Типове MaxRulingSegments зросло з 4096 до 16384 у 3.117.0, бо рамки на кожну клітинку приходять у куди більшій кількості, ніж обведені лінії сітки. Обведена таблиця на 30 рядків і 6 колонок — це 38 сегментів ліній. Та сама таблиця, експортована залитими прямокутниками, — це до чотирьох рамок на клітинку, 720 частин перед злиттям, а форма із залитими клітинками подвоює це. Дві такі таблиці на сторінці вичерпали б старий бюджет. Бюджет забезпечується в TableAppendRuling через Check, який кидає EPdfError з повідомленням «Table ruling-segment budget exceeded»; жодного деградованого результату, жодної часткової сітки немає, і прохід за пробілами теж не виконується. Якщо ви виставите тугіший бюджет для недовіреного входу, ловіть виняток і вирішуйте, а не читайте порожній результат як «таблиць немає»:
Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048; // навмисно тісний для недовіреного входу
try
Tables := Pdf.ExtractTables(Options);
except
on E: EPdfError do
begin
Log(E.Message); // 'Table ruling-segment budget exceeded'
Options.MaxRulingSegments := 16384; // типове значення 3.117.0
Tables := Pdf.ExtractTables(Options);
end;
end;
Виміряні результати й де підхід спиняється
На тих самих 13 зразках документів вилучення пішло з 43 таблиць, з яких 9 розлінованих і 34 фрагменти за пробілами або хибні спрацювання, до 41 розлінованої таблиці й жодного хибного спрацювання за пробілами. Частина цього прибирання належить двом супутнім змінам у 3.117.0: слова, які вже забрала розлінована сітка, видаляються перед запуском виявлення за пробілами, тож таблиця ніколи не повідомляється двічі, а межа колонки за пробілами тепер мусить бути вільним від тексту коридором крізь кожен рядок, який вона розділяє, — саме це й спинило вирівняні абзаци від того, щоб оцінюватися як таблиці 5x4. Читач залитих прямокутників — це те, що пересунуло самі таблиці з колонки фрагментів до колонки розлінованих
Межі варто назвати прямо. Сторінка без текстового шару все одно дає кістяк сітки, кожна клітинка порожня, бо лінійки приходять із геометрії, а текст — зі сторінки тексту; скановані сторінки спершу потребують OCR. Залиті фігури з кривими, заокругленими кутами або непрямокутними контурами відкидаються повністю, тож таблиця, чиї рамки намальовані контурами заокруглених прямокутників, як і раніше потребує виявлення за пробілами. Таблиця без рамок і без заливок цим усім не зачеплена й лишається вотчиною стратегії пробілів, описаної в статті про вилучення таблиць; коли й цього не досить, прямокутники слів і блоки з структурованого тексту й порядку читання — це сировина для читача під конкретну предметну область. Демо TableExtractionLab, яке постачається з компонентом, виставляє DetectFilledRulings у своїй панелі опцій — це найшвидший спосіб побачити, як виглядає конкретний експорт із ним і без нього; повний API описано на сторінці PDFium Component for Delphi