การดึงตารางของ PDFium Component ตั้งแต่เวอร์ชัน 3.117.0 มอง filled rectangle บาง ๆ เป็นเส้นตารางของตาราง เมื่อเปิด DetectFilledRulings ซึ่งเป็นค่าเริ่มต้น กล่องทึบที่เรียงตามแกนและหนาไม่เกิน MaxRulingThickness (3 point) จะกลายเป็นเส้นตารางหนึ่งเส้นตามแกนยาวของมัน กล่องทึบที่ใหญ่กว่านั้นให้เส้นขอบสี่ด้าน และพิกัดของเส้นตารางทุกเส้นจะถูก snap ภายใน RulingSnapTolerance (4 point) ก่อนที่กริดจะถูกประกอบขึ้น ตารางที่ export จาก Word, Google Docs และเบราว์เซอร์จึงไปถึงตัวตรวจจับแบบมีเส้นตารางในฐานะกริดที่ครบถ้วน แทนที่จะตกไปที่การตรวจจับแบบ whitespace ในฐานะชิ้นส่วนกระจัดกระจาย
บทความก่อนหน้าเรื่องการตรวจจับและดึงตารางระบุว่าการตรวจจับแบบมีเส้นตารางใช้เส้นที่วาดไว้ และแต่ละ segment ของ path ที่ถูก stroke จะถูก transform เป็นพิกัดหน้า ประโยคนั้นจริงแต่ไม่ครบ การนับ path object ในชุดเอกสารตัวอย่างจริง 13 ฉบับแสดงว่า 9 ฉบับไม่มี stroked path เลย แต่ทุกหน้าของพวกมันมี filled rectangle หนา 0.5 ถึง 1 point เป็นร้อย ๆ ตัว ตัวตรวจจับที่ดูแค่ stroke จึงไม่เห็นอะไร ทุกหน้าตกลงไปที่การตรวจจับแบบ whitespace และผลลัพธ์ก็เป็นเศษชิ้นเล็ก ๆ กระจัดกระจายแทนที่จะเป็นตาราง preset compact-columns ที่เพิ่มใน 3.116.4 บรรเทาเรื่องนี้ในระดับชิ้นส่วน แต่ต้นตอคือตัวตรวจจับอ่าน painting operator ผิดตัว
ทำไมตารางที่ export จาก Word ถึงไม่มีเส้นที่ถูก stroke
โปรแกรมประมวลคำไม่ได้คิดว่าขอบคือเส้น มันคิดว่าขอบคือกล่องที่มีความกว้าง แล้วทากล่องนั้นด้วยการ fill ISO 32000-1 §8.5.2.1 นิยามตัวดำเนินการ re ว่าต่อ rectangle subpath เข้าไป และ §8.5.3 แยกตัวดำเนินการทาออกเป็น: S stroke path ด้วยความกว้างเส้นปัจจุบัน ส่วน f fill พื้นที่ภายใน ขอบเซลล์หนา 0.5 point จึงออกมาเป็น x y w 0.5 re f และกลไกการ stroke ทั้งความกว้างเส้น, join และ dash pattern ไม่เคยถูกเรียกเลย การแรเงาเซลล์ก็เป็นโครงสร้างเดียวกันแต่กล่องใหญ่กว่า กริดที่ stroke ด้วย m, l และ S คือสิ่งที่ตัวตรวจจับเดิมคาดหวัง และมันคือสิ่งที่แทบไม่มีอะไรที่ export จากแอปพลิเคชันสำนักงานผลิตออกมา:
% ขอบเซลล์หนึ่งเส้นจากไฟล์ที่ export จากโปรแกรมประมวลคำ: กล่องทึบสูง 0.5 pt
72 700 468 0.5 re f
% การแรเงาเซลล์: กล่องทึบขนาดเท่าเซลล์
72 676 117 24 re f
% เส้นกริดแบบ stroke ที่ตัวตรวจจับเดิมถูกเขียนขึ้นมารองรับ
72 700 m 540 700 l S
สำหรับตัวตรวจจับที่ถาม FPDFPath_GetDrawMode แค่ว่าธง stroke ถูกตั้งไหม กล่องทึบทั้งสองแบบก็มองไม่เห็น ข้อความในเซลล์ก็ไปถึงการตรวจจับแบบ whitespace ซึ่งคอลัมน์ที่คั่นด้วยช่องว่าง 6 point ต่ำกว่าค่าเริ่มต้นของ MinColumnGap ที่ 12 point และสิ่งที่ได้กลับมาคือแถวชุดย่อยใดก็ตามที่บังเอิญเรียงตรงกันพอจะผ่าน MinRows นั่นคือพฤติกรรมแบบชิ้นส่วน และการจูนพารามิเตอร์เท่าไรก็ไม่ทำให้มันกลายเป็นกริดที่คนเขียนเอกสารวาดไว้
PDFium Component เปลี่ยนกล่องทึบให้เป็นเส้นตารางได้อย่างไร
TableCollectObjectRulings ตรวจ path object ทุกตัวทีละ subpath โหมดการทามาจาก FPDFPath_GetDrawMode; path จะนับว่าเป็นแบบ fill เมื่อเปิด DetectFilledRulings และโหมด fill ไม่ใช่ none จุดทุกจุดถูก transform ผ่าน matrix ของออบเจ็กต์และเก็บไว้ สูงสุด MaxSubpathPoints (8) ต่อ subpath และ segment ที่เป็นเส้นโค้งใด ๆ จะทำให้ subpath นั้นถูกทำเครื่องหมายว่าโค้ง เมื่อ subpath ปิดหรือมี MoveTo ใหม่เริ่มขึ้น FlushSubpath จะตัดสินว่ามันคืออะไร: subpath ที่โค้งถูกทิ้ง และรูปหลายเหลี่ยมปิดที่มีจุดไม่ได้อยู่ภายใน PointTolerance (0.05 point) จากขอบของ bounding box บนอย่างน้อยหนึ่งแกนก็ถูกทิ้งด้วย สามเหลี่ยม, chevron หรือแท็บมุมโค้งไม่เคยกลายเป็นเส้นตาราง นั่นคือสิ่งที่กันงานศิลป์ประดับไม่ให้เข้าไปในกริด
สิ่งที่รอดมาคือสี่เหลี่ยมที่เรียงตามแกน โดยจำแนกจาก bounding box ของมัน ความกว้างที่เท่ากับหรือต่ำกว่า MaxRulingThickness ขณะที่ความสูงสูงกว่านั้น ให้เส้นตารางแนวตั้งหนึ่งเส้นที่กึ่งกลางแนวนอน ครอบกล่องจากล่างขึ้นบน; เคสกลับกันให้เส้นตารางแนวนอนหนึ่งเส้น ถ้ามิติทั้งสองเกินเกณฑ์ก็หมายถึงเซลล์ที่ถูกแรเงา และกล่องนั้นให้เส้นตารางสี่เส้น หนึ่งเส้นต่อหนึ่งขอบ ถ้ามิติทั้งสองอยู่ที่หรือต่ำกว่าเกณฑ์ก็ไม่ให้อะไรเลย สัญลักษณ์หัวข้อย่อยสี่เหลี่ยมขนาด 2 point จึงไม่ถูกเข้าใจผิดว่าเป็นเส้น path ที่ stroke ใช้เส้นทางเดิมผ่าน AddLine หนึ่งเส้นตารางต่อหนึ่ง segment ที่เรียงตามแกน กริดที่วาดด้วย S จึงถูกจัดการเหมือนเดิมทุกประการ และ path ที่ทาทั้ง fill และ stroke จะให้ชิ้นส่วนที่ทับกันซึ่งรอบการ merge จะยุบรวมให้:
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; // point; กล่องที่หนากว่านับเป็นการแรเงา
Options.RulingSnapTolerance := 4.0; // point; 0 ปิดการ 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 ทำอะไรให้ตารางที่ใช้การแรเงาเซลล์
RulingSnapTolerance คือสิ่งที่ทำให้ตารางที่สร้างจากการแรเงาเพียงอย่างเดียวเชื่อมกันเป็นกริดเดียว ไฟล์ export บางตัวไม่วาดขอบเลย: ทุกเซลล์เป็นกล่องทึบในสีของตัวเอง และกล่องที่อยู่ข้างกันคั่นด้วยช่องว่างสีขาว 1 ถึง 3 point กล่องแต่ละใบให้เส้นตารางสี่เส้นตามขอบ แต่ขอบขวาของเซลล์หนึ่งกับขอบซ้ายของเซลล์ถัดไปห่างกัน 2 point และการทดสอบการเชื่อมต่อใช้ RulingTolerance ซึ่งมีค่าเริ่มต้นที่ 1 point ถ้าไม่ snap ทุกเซลล์จะกลายเป็น connected component ของเส้นตารางสี่เส้นของตัวเอง ไม่มี component ใดไปถึง MinRows และหน้านั้นก็รายงานอะไรไม่ออก TableSnapRulings รวบรวมพิกัด X ทุกตัวที่เกี่ยวข้อง (ตำแหน่งของเส้นตารางแนวตั้งแต่ละเส้น บวกจุดเริ่มและจุดจบของเส้นแนวนอนแต่ละเส้น) และพิกัด Y ในทำนองเดียวกัน เรียงแต่ละลิสต์ จับกลุ่มด้วยการเชื่อมค่าที่เพื่อนบ้านต่างกันไม่เกิน tolerance แทนแต่ละกลุ่มด้วยค่าเฉลี่ยของมัน แล้วเลื่อนทุกตำแหน่ง จุดเริ่ม และจุดจบไปยังศูนย์กลางกลุ่มที่ใกล้ที่สุด สองฝั่งของช่องว่างจึงกลายเป็นเส้นเดียวกัน และการเชื่อมต่อก็คงอยู่
การ snap รันก่อน TableMergeRulings ซึ่งเรียงเส้นตารางและเชื่อมชิ้นที่เป็นแนวเส้นเดียวกันซึ่งแตะหรือทับกันภายใน RulingTolerance และทั้งสองอย่างรันก่อนที่ TableDetectRuled จะได้เห็นข้อมูล การตรวจการเชื่อมต่อแบบเป็นคู่จึงเป็นสัดส่วนกับจำนวนเส้นกริด ไม่ใช่จำนวนชิ้นส่วนต่อเซลล์ บนกริดที่ stroke รอบเหล่านี้ไม่เป็นอันตราย เพราะพิกัดที่เหมือนกันอยู่แล้วก็ snap เข้าหาตัวเอง สิ่งเดียวที่ควรระวังคือการจับกลุ่มแบบเชื่อมโซ่ไม่มีความจำกัดความกว้างของตัวเอง: พิกัดที่เรียงกันห่างกันละ 3 point จะยุบรวมเป็นศูนย์กลางเดียว ที่ค่าเริ่มต้น 4 point เรื่องนี้กระทบแค่คอลัมน์ที่แคบกว่าตัวอักษร แต่ถ้าเอกสารมีช่องว่าง 3 point จริง ๆ ที่ต้องแยกจากกัน ให้ลด tolerance ลงหรือตั้งเป็น 0 เพื่อปิดการ snap:
// แยกกลยุทธ์แบบมีเส้นตารางออกมา แล้วเทียบว่าค่าแต่ละแบบเห็นอะไรบนหน้าเดียว
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;
// ไฟล์ที่ export จาก Word ปกติจะรายงาน 0, N แล้วก็น้อยกว่า N:
// แบบดูแค่ stroke ไม่เห็นอะไร การ snap เชื่อมเซลล์ที่ถูกแรเงาเข้าด้วยกัน
// และการปิด snap ก็ทิ้งให้แต่ละเซลล์ที่แรเงาเป็นเกาะของตัวเอง
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 ระบุว่า form matrix ถูก concatenate กับ current transformation matrix เมื่อ form ถูกทา สี่เหลี่ยมที่อยู่ภายใน form จึงอยู่ใน form space และลงมาบนหน้าได้ก็ต่อเมื่อผ่าน transform ตั้งแต่สองชั้นขึ้นไป TableCollectObjectRulings ลงไปข้างใน form object แบบเรียกซ้ำเมื่อตั้ง IncludeFormXObjects: มันอ่าน matrix ของออบเจ็กต์ รวมมันกับ matrix ของ parent ผ่าน TableMultiplyMatrix ซึ่งลำดับอาร์กิวเมนต์หมายความว่า "map ผ่าน matrix ตัวแรก แล้วจึงตัวที่สอง" และแจกแจงลูกด้วย FPDFFormObj_CountObjects กับ FPDFFormObj_GetObject โดยส่ง matrix ที่รวมแล้วลงไป การซ้อนลึกกว่า MaxFormDepth (8) จะถูกข้ามไปเงียบ ๆ ซึ่งเป็นเกราะกันไฟล์ที่ผิดปกติ ไม่ใช่ขีดจำกัดที่ไฟล์ export จริงเข้าใกล้ เหตุผลที่ลำดับการคูณสำคัญก็เป็นเหตุผลเดียวกับที่พูดถึงในการ prepend กับการ append matrix: การสลับ operand ทำให้พจน์ translation ขยับ แล้วเส้นตารางที่ควรลงบนหัวหน้าก็ไปลงที่จุดกำเนิดแทน
ทำไมงบประมาณเส้นตารางถึงเพิ่มเป็นสี่เท่า
ค่าเริ่มต้นของ MaxRulingSegments เพิ่มจาก 4096 เป็น 16384 ใน 3.117.0 เพราะขอบรายเซลล์มามากกว่าเส้นกริดแบบ stroke หลายเท่า ตารางขนาด 30 แถว 6 คอลัมน์ที่ stroke คือ 38 segment ตารางเดียวกันที่ export ออกมาเป็นกล่องทึบคือขอบได้ถึงสี่ด้านต่อเซลล์ รวมเป็น 720 ชิ้นก่อน merge และฟอร์มที่มีเซลล์แรเงาก็เพิ่มเป็นสองเท่าของนั้น ตารางแบบนี้สองตัวบนหน้าเดียวก็จะใช้งบเดิมจนหมดแล้ว งบประมาณนี้ถูกบังคับใช้ใน TableAppendRuling ผ่าน Check ซึ่ง raise EPdfError ด้วยข้อความ "Table ruling-segment budget exceeded"; ไม่มีผลลัพธ์แบบเสื่อมคุณภาพ ไม่มีกริดบางส่วน และรอบ whitespace ก็ไม่รันด้วย ถ้าคุณตั้งงบที่แน่นกว่าเองสำหรับ input ที่ไม่น่าเชื่อถือ ให้ดัก exception แล้วตัดสินใจ แทนที่จะอ่านผลลัพธ์ว่างเปล่าว่า "ไม่มีตาราง":
Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048; // ตั้งให้แน่นโดยตั้งใจสำหรับ input ที่ไม่น่าเชื่อถือ
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 ตัวเป็นชิ้นส่วน whitespace หรือ false positive มาเป็น 41 ตารางแบบมีเส้นตารางและไม่มี false positive จาก whitespace เลย ส่วนหนึ่งของการเก็บกวาดนั้นมาจากการเปลี่ยนแปลงควบคู่สองอย่างใน 3.117.0: คำที่ถูกกริดแบบมีเส้นตารางจับไปแล้วจะถูกลบออกก่อนที่การตรวจจับแบบ whitespace จะรัน ตารางจึงไม่ถูกรายงานซ้ำสองครั้ง และขอบเขตคอลัมน์แบบ whitespace ตอนนี้ต้องเป็นทางเดินที่ไม่มีข้อความตลอดทุกแถวที่มันคั่น ซึ่งเป็นสิ่งที่หยุดไม่ให้ย่อหน้าแบบ justified ถูกให้คะแนนเป็นตาราง 5x4 ตัวอ่าน filled rectangle คือสิ่งที่ย้ายตัวตารางเองจากคอลัมน์ชิ้นส่วนไปยังคอลัมน์แบบมีเส้นตาราง
ขอบเขตของมันคุ้มที่จะบอกให้ชัด หน้าที่ไม่มี text layer ก็ยังให้โครงกริดออกมา โดยทุกเซลล์ว่างเปล่า เพราะเส้นตารางมาจากเรขาคณิตและข้อความมาจาก text page; หน้าที่สแกนมาต้องผ่าน OCR ก่อน รูปทรงทึบที่มีเส้นโค้ง, มุมโค้ง หรือโครงร่างที่ไม่ใช่สี่เหลี่ยมจะถูกทิ้งทั้งหมด ตารางที่ขอบของมันถูกวาดเป็นโครงร่างสี่เหลี่ยมมุมโค้งจึงยังต้องใช้การตรวจจับแบบ whitespace เหมือนเดิม ตารางที่ไม่มีทั้งขอบและการแรเงาไม่ถูกกระทบโดยเรื่องทั้งหมดนี้ และยังเป็นอาณาเขตของกลยุทธ์ whitespace ที่อธิบายไว้ในบทความเรื่องการดึงตาราง; เมื่อแค่นั้นยังไม่พอ word box กับ block จากstructured text และลำดับการอ่านก็เป็นวัตถุดิบสำหรับตัวอ่านเฉพาะทาง TableExtractionLab demo ที่ออกมาพร้อมคอมโพเนนต์เปิดให้ใช้ DetectFilledRulings ในแผงออปชันของมัน ซึ่งเป็นวิธีเร็วที่สุดที่จะเห็นว่าไฟล์ export ตัวหนึ่งหน้าตาเป็นอย่างไรเมื่อเปิดและเมื่อปิดมัน; API แบบเต็มอธิบายไว้ที่หน้า PDFium Component for Delphi