Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions src/Filee.Engines/Pdf/PdfDocumentReader.Tables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
// page (lines, paragraphs, alignment within the cell), and cell borders, shading and row heights come from the PDF.
//
// Conservative on purpose: a grid needs 2 rows and 2 columns, merged cells must be rectangles, text must sit in the
// cells (a rule through a word means a chart or a strike-through, not a table) and at least two cells hold text.
// cells (a rule through a word means a chart or a strike-through, not a table; only a few words may run on past
// their cell's right rule, as long spreadsheet text does) and at least two cells hold text.
// Anything else stays text in reading order as before. Tables without rules (aligned by white space only) are not
// detected; a table running over several pages becomes one table per page.

Expand Down Expand Up @@ -373,17 +374,27 @@ private static List<double> Edges(IEnumerable<double> positions)
if (cells.Count < 4 && (cells.Select(c => c.Row).Distinct().Count() < 2 || cells.Select(c => c.Column).Distinct().Count() < 2))
return null;

// Words by their centre; a rule through the middle of a word means this is no table around text.
// Words by their centre; a rule through the middle of a word means this is no table around text. Except a word
// that runs on past its cell's right rule: a spreadsheet cuts long text off at the cell (the PDF only clips the
// rest) or lets it run into the empty cell next to it. A few of those stay in the cell they start in.
var bounds = new PdfRectangle(xs[0], ys[^1], xs[^1], ys[0]);
bool Inside(double x, double y) => x > bounds.Left && x < bounds.Right && y > bounds.Bottom && y < bounds.Top;
var overflows = Math.Max(1, words.Count(w => Inside((w.Left + w.Right) / 2, (w.Top + w.Bottom) / 2)) / 20);
foreach (var word in words)
{
var (x, y) = ((word.Left + word.Right) / 2, (word.Top + word.Bottom) / 2);
if (x <= bounds.Left || x >= bounds.Right || y <= bounds.Bottom || y >= bounds.Top)
if (!Inside(x, y))
continue;
var band = (word.Top - word.Bottom) * 0.3;
if (vertical.Any(v => v.Position > word.Left + 1 && v.Position < word.Right - 1 && v.Start < y && v.End > y)
|| horizontal.Any(h => h.Position > word.Bottom + band && h.Position < word.Top - band && h.Start < word.Right - 1 && h.End > word.Left + 1))
if (horizontal.Any(h => h.Position > word.Bottom + band && h.Position < word.Top - band && h.Start < word.Right - 1 && h.End > word.Left + 1))
return null;
var through = vertical.Where(v => v.Position > word.Left + 1 && v.Position < word.Right - 1 && v.Start < y && v.End > y).ToList();
if (through.Count > 0)
{
if (--overflows < 0)
return null;
x = (word.Left + through.Min(v => v.Position)) / 2;
}
var column = Math.Clamp(xs.FindLastIndex(e => e <= x), 0, columns - 1);
var row = Math.Clamp(ys.FindLastIndex(e => e >= y), 0, rows - 1);
cells[owner[row, column]].Words.Add(word);
Expand Down
38 changes: 38 additions & 0 deletions tests/Filee.Engines.Tests/PdfTableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,44 @@ public async Task Rules_drawn_as_bars_keep_their_weight_and_shading()
Assert.Null(inner.Fill);
}

[Fact]
public async Task Long_text_running_past_its_cell_stays_in_that_cell()
{
// A spreadsheet's measurement sheet (#138): one label is longer than its column, so its last word runs on past
// the cell's right rule, where the spreadsheet cut it off (the PDF only clips it).
var dir = fx.NewFolder();
var pdf = new PdfBuilder().NewPage();
double[] xs = [50, 200, 260, 400];
double[] ys = [100, 120, 140, 160, 180];
foreach (var y in ys)
pdf.Rule(xs[0], y, xs[^1], y);
foreach (var x in xs)
pdf.Rule(x, ys[0], x, ys[^1]);
string[][] cells =
[
["Point of measurement", "Tol.", "M"],
["Shoulder seam forward", "1/8", "1/2"],
["Front neck drop at the collar notch", "1/8", "3 1/2"],
["Collar point", "1/8", "3.5"],
];
for (var r = 0; r < 4; r++)
{
for (var c = 0; c < 3; c++)
Cell(pdf, cells[r][c], c == 1 ? xs[c] + 16 : xs[c], ys[r], bold: r == 0); // tolerances sit mid-column
}
var label = pdf.Width(cells[2][0], 10, pdf.Font());
Assert.InRange(xs[0] + 4 + label, xs[1] + 2, xs[1] + 16); // its last word straddles the rule, short of the next text
var docx = await ConvertAsync(pdf.Save(Path.Combine(dir, "overflow.pdf")), "docx");

ValidPackage(docx);
var table = Assert.Single(ReadBack(docx).Sections.SelectMany(s => s.Blocks).OfType<HTable>());
var texts = Texts(table);
Assert.Equal(4, texts.Length);
Assert.Equal("Front neck drop at the collar notch", texts[2][0]);
Assert.Equal(["1/8", "3 1/2"], texts[2][1..]);
Assert.Equal(cells[3], texts[3]);
}

[Fact]
public async Task Boxes_underlines_and_empty_grids_stay_text()
{
Expand Down
Loading