// ===========================================================================
// ©2013-2022 WebSupergoo. All rights reserved.
//
// This source code is for use exclusively with the ABCpdf product with
// which it is distributed, under the terms of the license for that
// product. Details can be found at
//
// http://www.websupergoo.com/
//
// This copyright notice must not be deleted and must be reproduced alongside
// any sections of code extracted from this module.
// ===========================================================================
using System;
using System.Drawing;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using WebSupergoo.ABCpdf14;
using WebSupergoo.ABCpdf14.Objects;
using WebSupergoo.ABCpdf14.Atoms;
using WebSupergoo.ABCpdf14.Operations;
using WebSupergoo.ABCpdf14.Elements;
using Structure = WebSupergoo.ABCpdf14.Operations.Accessibility.Structure;
using ParentTree = WebSupergoo.ABCpdf14.Operations.Accessibility.ParentTree;
using KidArranger = WebSupergoo.ABCpdf14.Operations.Accessibility.KidArranger;
namespace WebSupergoo.TableDetection {
public class Tables {
private List<Table> _tables = null;
public bool FontSensitive { get; set; } = false;
public IList<Table> Items { get { return _tables; } }
public void Add(Structure tags, List<StructureElementElement> elements) {
_tables = new List<Table>();
_tables.Add(new Table(this));
Dictionary<int, Rows> pageRows = new Dictionary<int, Rows>();
foreach (StructureElementElement element in elements) {
Rows rows = null;
if (!pageRows.TryGetValue(element.GetPage(tags).ID, out rows)) {
rows = new Rows(this, tags);
pageRows[element.GetPage(tags).ID] = rows;
}
rows.Add(element);
}
foreach (KeyValuePair<int, Rows> pair in pageRows) {
int page = tags.GetPageNumber(pair.Key);
Rows rows = pair.Value;
rows.SortTopToBottom();
for (int i = 0; i < rows.Items.Count; i++) {
Row row = rows.Items[i];
Add(row);
}
}
}
private void Add(Row row) {
if (row.Items.Count > 1) {
int count = Math.Min(_tables.Count, 3); // try last few tables
for (int i = 0; i < count; i++) {
if (_tables[_tables.Count - 1 - i].TryAdd(row))
return;
}
}
Table last = new Table(this);
_tables.Add(last);
last.TryAdd(row);
}
}
public class Table {
private Tables _tables;
private List<Row> _rows = null;
private List<XRect> _columns = null;
private Table() {}
public Table(Tables tables) {
_tables = tables;
_rows = new List<Row>();
_columns = new List<XRect>();
}
public void Clear() {
_rows.Clear();
_columns.Clear();
}
public IList<Row> Items { get { return _rows; } }
public Page Page { get { return Items.Count > 0 ? Items[0].Page : null; } }
public bool TryAdd(Row row) {
if (_rows.Count == 0) {
_rows.Add(row);
// assume that first row defines the column widths
// if it doesn't then we would need to adjust the bounding
// boxes as we saw items or indeed perhaps even add new columns
foreach (StructureElementElement element in row.Items)
_columns.Add(element.GetBBox());
return true;
}
List<int> blanks = null;
int i = 0;
foreach (StructureElementElement element in row.Items) {
XRect bbox = element.GetBBox();
if (i >= _columns.Count) {
// items left but no columns left to put them into
return false;
}
for (int j = i; j < _columns.Count; j++) {
XRect col_bbox = _columns[j];
if (bbox.Left > col_bbox.Right) {
// perhaps this column is blank, see if we fit under the next one
if (blanks == null)
blanks = new List<int>();
blanks.Add(i);
i++;
continue;
}
if (bbox.Right < col_bbox.Left)
return false;
// this matches
i++;
break;
}
}
if (blanks != null) {
foreach (int index in blanks) {
row.Items.Insert(index, null);
}
}
_rows.Add(row);
return true;
}
private void UpdateColumns() {
if (_columns == null) {
_columns = new List<XRect>();
}
}
}
public class Rows {
private Tables _tables;
private Structure _tags;
private List<Row> _rows = null;
private Rows() {}
public Rows(Tables tables, Structure tags) {
_tables = tables;
_tags = tags;
_rows = new List<Row>();
_rows.Add(new Row(_tables, _tags));
}
public IList<Row> Items { get { return _rows; } }
public Page Page { get { return Items.Count > 0 ? Items[0].Page : null; } }
public void Add(StructureElementElement element) {
Row last = _rows[_rows.Count - 1];
if (!last.TryAdd(element)) {
last = new Row(_tables, _tags);
_rows.Add(last);
last.TryAdd(element);
}
}
public void SortTopToBottom() { // and left to right if top == bottom
_rows.Sort((i1, i2) =>
i1.BBox.Top != i2.BBox.Top ? i2.BBox.Top.CompareTo(i1.BBox.Top) : i1.BBox.Left.CompareTo(i2.BBox.Left)
);
}
}
[DebuggerDisplay("\\{ BBox = {BBox} Page = {Page.ID} Text = {Text} \\}")]
public class Row {
private Tables _tables;
private Structure _tags;
private List<StructureElementElement> _items = null;
private XRect _bbox = null;
private Page _page = null;
private Dictionary<string, string> _style = null;
private Row() { }
public Row(Tables tables, Structure tags) {
_tables = tables;
_tags = tags;
_items = new List<StructureElementElement>();
}
public IList<StructureElementElement> Items { get { return _items; } }
public Page Page { get { return Items.Count > 0 ? Items[0].GetPage(_tags) : null; } }
public XRect BBox { get { return _bbox; } }
public string Text { get { StringBuilder sb = new StringBuilder(); foreach (var item in _items) sb.Append(item.EntryActualText); return sb.ToString(); } }
public bool TryAdd(StructureElementElement element) {
if (_items.Count == 0) {
_items.Add(element);
_bbox = element.GetBBox();
_page = element.GetPage(_tags);
_style = element.GetStyle();
return true;
}
// elements need to be on the same page
if (_page.ID != element.GetPage(_tags).ID)
return false;
// elements in rows need to line up horizontally
XRect bbox = element.GetBBox();
if ((bbox.Top < _bbox.Bottom) || (bbox.Bottom > _bbox.Top))
return false;
if ((_bbox.Top < bbox.Bottom) || (_bbox.Bottom > bbox.Top))
return false;
if ((_tables.FontSensitive) && (!StylesCompatible(_style, element.GetStyle())))
return false;
_items.Add(element);
_bbox.Union(bbox);
return true;
}
public static bool StylesCompatible(Dictionary<string, string> style1, Dictionary<string, string> style2) {
if ((style1 == null) || (style2 == null))
return true;
string family1 = null, family2 = null;
style1.TryGetValue("font-family", out family1);
style2.TryGetValue("font-family", out family2);
if ((family1 == null) || (family2 == null))
return true;
return family1 == family2;
}
}
}