Trích xuất bảng của PDFium Component, kể từ phiên bản 3.117.0, coi một hình chữ nhật tô đầy mảnh là một ruling của bảng. Khi DetectFilledRulings được bật, tức mặc định, một hộp tô đầy thẳng trục dày không quá MaxRulingThickness (3 point) trở thành một ruling dọc theo trục dài của nó, một hộp tô đầy lớn hơn đóng góp bốn cạnh của nó, và mọi tọa độ ruling đều được snap trong RulingSnapTolerance (4 point) trước khi lưới được lắp. Nhờ đó các bảng export từ Word, Google Docs và trình duyệt đi tới bộ phát hiện bảng kẻ dòng như những lưới hoàn chỉnh thay vì rơi xuống phát hiện theo khoảng trắng dưới dạng các mảnh vụn
Bài trước về phát hiện và trích xuất bảng nói rằng phát hiện bảng kẻ dòng dùng các đường được vẽ và mỗi đoạn path được stroke đều được biến đổi sang tọa độ trang. Câu đó đúng và chưa đủ. Đếm các path object trên một tập 13 tài liệu mẫu thực tế cho thấy 9 trong số đó không chứa path nào được stroke, vậy mà mỗi trang của chúng mang hàng trăm hình chữ nhật tô đầy dày 0,5 tới 1 point. Bộ phát hiện chỉ nhìn vào stroke không thấy gì, mọi trang rơi xuống phát hiện theo khoảng trắng, và đầu ra là một mớ mảnh vụn nhỏ thay vì bảng. Preset compact-columns thêm ở 3.116.4 có làm dịu chuyện đó ở mức từng mảnh; nguyên nhân gốc là bộ phát hiện đang đọc sai toán tử vẽ
Vì sao một bảng export từ Word không có đường nào được stroke?
Trình soạn thảo không nghĩ về đường viền như một đường thẳng; nó nghĩ đó là một hộp có bề rộng, và nó tô cái hộp đó bằng một lệnh fill. ISO 32000-1 §8.5.2.1 định nghĩa toán tử re là nối thêm một subpath hình chữ nhật, còn §8.5.3 tách riêng các toán tử vẽ: S stroke path với bề rộng nét hiện hành, f tô phần bên trong. Một viền ô 0,5 point ra đời dưới dạng x y w 0.5 re f, và toàn bộ bộ máy stroke, kể cả bề rộng nét, join và dash pattern, không hề chạy. Tô nền ô cũng là cấu trúc y hệt với cái hộp to hơn. Một lưới được stroke bằng m, l và S là thứ bộ phát hiện ban đầu trông đợi, và cũng là thứ gần như không ứng dụng văn phòng nào sinh ra:
% một viền ô từ bản export của trình soạn thảo: một hộp tô đầy cao 0,5 pt
72 700 468 0.5 re f
% tô nền ô: một hộp tô đầy bằng kích thước ô
72 676 117 24 re f
% đường lưới được stroke mà bộ phát hiện ban đầu được viết cho
72 700 m 540 700 l S
Với một bộ phát hiện chỉ hỏi FPDFPath_GetDrawMode xem cờ stroke có được bật hay không, cả hai hộp tô đầy đều vô hình. Các từ bên trong ô sau đó đi tới phát hiện theo khoảng trắng, nơi các cột cách nhau một gutter 6 point nằm dưới MinColumnGap mặc định là 12 point, và thứ quay về là một tập con dòng nào đó tình cờ thẳng hàng đủ để qua MinRows. Đó là hành vi mảnh vụn, và có chỉnh tham số bao nhiêu cũng không biến nó thành cái lưới mà tác giả đã vẽ
PDFium Component biến một hộp tô đầy thành ruling như thế nào?
TableCollectObjectRulings soi từng path object, mỗi lần một subpath. Draw mode lấy từ FPDFPath_GetDrawMode; một path được tính là tô đầy khi DetectFilledRulings bật và fill mode không phải none. Mỗi điểm được biến đổi qua object matrix và được thu lại, tối đa MaxSubpathPoints (8) cho mỗi subpath, và bất kỳ đoạn cong nào cũng đánh dấu subpath đó là cong. Khi subpath đóng lại hoặc một MoveTo mới bắt đầu, FlushSubpath quyết định nó là gì: một subpath cong bị bỏ, và mọi đa giác kín có các điểm không cùng nằm trong sai số PointTolerance (0,05 point) tính từ các cạnh của bounding box trên ít nhất một trục cũng bị bỏ. Một tam giác, một mũi nhọn hay một tab bo góc không bao giờ trở thành ruling, và đó là thứ giữ hình trang trí ra khỏi lưới
Thứ sống sót là một hình chữ nhật thẳng trục, được phân loại theo bounding box. Bề rộng bằng hoặc dưới MaxRulingThickness với chiều cao lớn hơn cho ra một ruling dọc ở giữa theo chiều ngang, trải hết hộp từ dưới lên trên; trường hợp đối xứng cho ra một ruling ngang. Cả hai chiều đều trên ngưỡng nghĩa là một ô được tô nền, và cái hộp đóng góp bốn ruling, mỗi cạnh một cái. Cả hai chiều đều bằng hoặc dưới ngưỡng thì không đóng góp gì, nên một bullet vuông 2 point không bị nhầm thành một đường. Một path được stroke đi đường cũ qua AddLine, mỗi đoạn thẳng trục một ruling, nên một lưới vẽ bằng S được xử lý y như trước, còn một path vừa fill vừa stroke sinh ra các mảnh chồng lên nhau mà lượt merge sẽ gộp lại:
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; // tính từ 1
Options := TPdfTableExtractionOptions.Default;
// đây là các giá trị mặc định của 3.117.0, ghi ra cho rõ
Options.DetectFilledRulings := True; // hộp tô đầy mảnh trở thành ruling
Options.MaxRulingThickness := 3.0; // point; hộp dày hơn bị tính là tô nền
Options.RulingSnapTolerance := 4.0; // point; 0 là tắt snap
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 làm gì cho các bảng chỉ có tô nền?
RulingSnapTolerance chính là thứ khiến một bảng được dựng chỉ từ tô nền nối thành một lưới. Một số bản export không vẽ viền nào cả: mỗi ô là một hộp tô đầy theo màu của nó, và các hộp kề nhau cách nhau một gutter trắng 1 tới 3 point. Mỗi hộp cho ra bốn ruling ở bốn cạnh, nhưng cạnh phải của ô này và cạnh trái của ô kia cách nhau 2 point, còn phép kiểm tra liên thông dùng RulingTolerance với mặc định là 1 point. Không có snap, mỗi ô tạo thành một thành phần liên thông riêng gồm bốn ruling, không thành phần nào đạt MinRows, và trang báo không có gì. TableSnapRulings thu mọi tọa độ X đang tham gia (vị trí của từng ruling dọc cộng điểm đầu và điểm cuối của từng ruling ngang) và mọi tọa độ Y tương tự, sắp từng danh sách, gom cụm bằng cách nối chuỗi các giá trị mà giá trị kề nó lệch không quá sai số, thay mỗi cụm bằng giá trị trung bình của nó, rồi dịch mọi vị trí, điểm đầu và điểm cuối về tâm cụm gần nhất. Hai bên của một gutter trở thành cùng một đường, và tính liên thông được giữ
Snap chạy trước TableMergeRulings, thứ sắp các ruling rồi nối các mảnh đồng tuyến chạm nhau hoặc chồng nhau trong RulingTolerance, và cả hai đều chạy trước khi TableDetectRuled nhìn thấy dữ liệu, nên phép kiểm tra liên thông theo từng cặp tỉ lệ với số đường lưới chứ không phải số mảnh của từng ô. Trên một lưới đã được stroke, các lượt này vô hại, vì những tọa độ vốn đã trùng nhau thì snap về chính nó. Một điều cần nhớ là việc gom cụm theo chuỗi không có giới hạn bề rộng của riêng nó: một dãy tọa độ cách nhau 3 point sẽ sụp thành một tâm duy nhất. Ở giá trị mặc định 4 point, điều đó chỉ ảnh hưởng tới các cột hẹp hơn một ký tự, nhưng nếu một tài liệu có gutter 3 point thật sự phải tách rời, hãy hạ sai số xuống hoặc đặt bằng 0 để tắt snap:
// Cô lập chiến lược kẻ dòng và so từng thiết lập nhìn thấy gì trên một trang
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;
// Một bản export từ Word thường báo 0, N rồi ít hơn N:
// chỉ stroke thì không thấy gì, snap nối các ô tô nền lại,
// và tắt snap thì mỗi ô tô nền thành một hòn đảo riêng
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));
Ruling bên trong form XObject
Các công cụ dàn trang thường gói một bảng, hoặc cả phần thân trang, vào một form XObject rồi vẽ nó bằng Do. ISO 32000-1 §8.10.1 quy định rằng form matrix được ghép nối với ma trận biến đổi hiện hành khi form được vẽ, nên một hình chữ nhật bên trong form sống trong không gian form và chỉ đáp xuống trang sau hai hay nhiều phép biến đổi. TableCollectObjectRulings đệ quy vào các form object khi IncludeFormXObjects được bật: nó đọc object matrix, kết hợp với ma trận cha qua TableMultiplyMatrix, với thứ tự tham số nghĩa là “ánh xạ qua ma trận thứ nhất, rồi qua ma trận thứ hai”, và liệt kê các con bằng FPDFFormObj_CountObjects cùng FPDFFormObj_GetObject, truyền ma trận đã kết hợp xuống dưới. Việc lồng sâu hơn MaxFormDepth (8) bị bỏ qua âm thầm, đó là một biện pháp phòng những tệp bệnh hoạn chứ không phải một giới hạn mà bản export thật nào tới gần. Lý do thứ tự nhân quan trọng cũng là lý do đã bàn trong prepend và append ma trận: đổi chỗ hai toán hạng sẽ dịch số hạng translation, và một ruling lẽ ra nằm ở đỉnh trang lại nằm ở gốc tọa độ
Vì sao ngân sách ruling tăng gấp bốn?
MaxRulingSegments mặc định tăng từ 4096 lên 16384 ở 3.117.0 vì viền theo từng ô đến với số lượng lớn hơn hẳn các đường lưới được stroke. Một bảng 30 dòng, 6 cột vẽ bằng stroke là 38 đoạn thẳng. Cùng bảng đó export thành các hộp tô đầy thì lên tới bốn viền mỗi ô, 720 mảnh trước khi merge, và một biểu mẫu có ô tô nền thì gấp đôi con số đó. Hai bảng như vậy trên một trang sẽ đốt hết ngân sách cũ. Ngân sách được cưỡng chế trong TableAppendRuling qua Check, thứ ném EPdfError với thông báo “Table ruling-segment budget exceeded”; không có kết quả suy giảm, không có lưới một phần, và lượt phát hiện theo khoảng trắng cũng không chạy. Nếu bạn tự đặt một ngân sách chặt hơn cho đầu vào không đáng tin, hãy bắt exception rồi tự quyết, thay vì đọc một kết quả rỗng thành “không có bảng nào”:
Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048; // cố tình chặt cho đầu vào không đáng tin
try
Tables := Pdf.ExtractTables(Options);
except
on E: EPdfError do
begin
Log(E.Message); // 'Table ruling-segment budget exceeded'
Options.MaxRulingSegments := 16384; // mặc định của 3.117.0
Tables := Pdf.ExtractTables(Options);
end;
end;
Kết quả đo được và chỗ mà cách làm này dừng
Trên cùng 13 tài liệu mẫu đó, việc trích xuất đi từ 43 bảng, trong đó 9 bảng được phát hiện theo kẻ dòng và 34 là mảnh vụn theo khoảng trắng hay dương tính giả, xuống còn 41 bảng kẻ dòng và không còn dương tính giả nào theo khoảng trắng. Một phần của việc dọn dẹp đó thuộc về hai thay đổi đi kèm ở 3.117.0: các word đã được một lưới kẻ dòng nhận trước sẽ bị loại bỏ trước khi lượt phát hiện theo khoảng trắng chạy, nên một bảng không bao giờ bị báo hai lần, và biên cột theo khoảng trắng giờ phải là một hành lang không có text xuyên suốt mọi dòng mà nó phân tách, đó là thứ ngăn các đoạn văn căn đều hai bên bị tính thành bảng 5x4. Còn bộ đọc hình chữ nhật tô đầy mới là thứ chuyển chính các bảng từ cột mảnh vụn sang cột kẻ dòng
Nói thẳng các ranh giới cũng đáng. Một trang không có lớp text vẫn cho ra bộ khung lưới với mọi ô rỗng, vì ruling đến từ hình học còn text đến từ text page; trang scan cần OCR trước. Các hình tô đầy có đường cong, góc bo tròn hay đường bao không phải hình chữ nhật đều bị bỏ hoàn toàn, nên một bảng có viền được vẽ dưới dạng đường bao chữ nhật bo góc vẫn cần phát hiện theo khoảng trắng như trước. Một bảng không có cả viền lẫn tô nền thì không bị gì trong chuyện này thay đổi và vẫn thuộc về chiến lược khoảng trắng được trình bày trong bài về trích xuất bảng; khi ngay cả cách đó cũng chưa đủ, các word box và block từ structured text và thứ tự đọc là nguyên liệu thô cho một bộ đọc riêng theo miền nghiệp vụ. Demo TableExtractionLab đi kèm component có phơi DetectFilledRulings trong panel options của nó, đó là cách nhanh nhất để xem một bản export cho ra sao khi bật và khi tắt tùy chọn này; toàn bộ API được trình bày trên trang PDFium Component cho Delphi