From 33f726c689229c18994c0a956cd20f78252078e1 Mon Sep 17 00:00:00 2001 From: Maverick Studer Date: Wed, 28 Feb 2024 16:13:56 +0100 Subject: [PATCH] RED-8550: Faulty table recognition and text duplication leads to huge sections (cherry picked from commit 74f55a5cbf905d0f869d7aa2c12c80a6d9c42e36) --- .../processor/model/AbstractPageBlock.java | 4 +- .../model/graph/nodes/TableCell.java | 6 +- .../processor/model/table/Cell.java | 17 +- .../processor/model/table/Ruling.java | 19 +- .../processor/model/table/TablePageBlock.java | 26 +- .../services/RulingCleaningService.java | 286 ++++++++-------- .../services/TableExtractionService.java | 312 ++---------------- .../factory/DocumentGraphFactory.java | 4 +- .../visualization/LayoutGridService.java | 39 ++- .../processor/utils/DrawingOptions.java | 28 ++ .../processor/utils/GeometricComparators.java | 88 +++++ .../utils/PdfVisualisationUtility.java | 41 +-- .../utils/RectangleTransformations.java | 16 + .../utils/RectangularIntersectionFinder.java | 77 +++++ .../processor/utils/SpreadsheetFinder.java | 172 ++++++++++ .../processor/utils/UnionFind.java | 44 +++ .../layoutparser/server/BdrJsonBuildTest.java | 3 +- .../graph/DocumentGraphVisualizationTest.java | 3 +- .../server/graph/ViewerDocumentTest.java | 2 +- .../PdfSegmentationServiceTest.java | 5 +- .../services/RulingCleaningServiceTest.java | 33 +- .../server/utils/visualizations/PdfDraw.java | 70 ++-- .../DontMergeNonConsecutiveTables.pdf | Bin 0 -> 16012 bytes .../files/SinglePages/Meto_vol2_Page10.pdf | Bin 0 -> 88319 bytes 24 files changed, 763 insertions(+), 532 deletions(-) create mode 100644 layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/DrawingOptions.java create mode 100644 layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/GeometricComparators.java create mode 100644 layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/RectangularIntersectionFinder.java create mode 100644 layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/SpreadsheetFinder.java create mode 100644 layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/UnionFind.java create mode 100644 layoutparser-service/layoutparser-service-server/src/test/resources/files/SinglePages/DontMergeNonConsecutiveTables.pdf create mode 100644 layoutparser-service/layoutparser-service-server/src/test/resources/files/SinglePages/Meto_vol2_Page10.pdf diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/AbstractPageBlock.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/AbstractPageBlock.java index 16a468e..d113dfa 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/AbstractPageBlock.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/AbstractPageBlock.java @@ -6,12 +6,14 @@ import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageB import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public abstract class AbstractPageBlock { +@EqualsAndHashCode(callSuper = true) +public abstract class AbstractPageBlock extends Rectangle { @JsonIgnore protected float minX; diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/graph/nodes/TableCell.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/graph/nodes/TableCell.java index 3036d94..3d726d8 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/graph/nodes/TableCell.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/graph/nodes/TableCell.java @@ -84,14 +84,16 @@ public class TableCell implements GenericSemanticNode { private TextBlock buildTextBlock() { - return streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector()); + return streamAllSubNodes().filter(SemanticNode::isLeaf) + .map(SemanticNode::getLeafTextBlock) + .collect(new TextBlockCollector()); } @Override public String toString() { - return treeId + ": " + NodeType.TABLE_CELL + ": " + this.buildTextBlock().buildSummary(); + return treeId + ": " + NodeType.TABLE_CELL + ": " + this.getTextBlock().buildSummary(); } } diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/Cell.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/Cell.java index 7dfce70..2f0de29 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/Cell.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/Cell.java @@ -1,6 +1,7 @@ package com.knecon.fforesight.service.layoutparser.processor.model.table; import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -36,6 +37,12 @@ public class Cell extends Rectangle { } + public Cell(Rectangle2D r) { + + super((float) r.getY(), (float) r.getX(), (float) r.getWidth(), (float) r.getHeight()); + } + + public void addTextBlock(TextPageBlock textBlock) { textBlocks.add(textBlock); @@ -76,14 +83,4 @@ public class Cell extends Rectangle { return this.getHeight() >= MIN_SIZE && this.getWidth() >= MIN_SIZE; } - public boolean nearlyIntersects(Cell other) { - - if (this.getHeight() <= 0 || other.getHeight() <= 0) { - return false; - } - double x0 = this.getX() + 2; - double y0 = this.getY() + 2; - return (other.x + other.width > x0 && other.y + other.height > y0 && other.x < x0 + this.getWidth() - 2 && other.y < y0 + this.getHeight() - 2); - } - } diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/Ruling.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/Ruling.java index 6acf5e1..7586258 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/Ruling.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/Ruling.java @@ -20,7 +20,8 @@ import lombok.extern.slf4j.Slf4j; @SuppressWarnings("all") public class Ruling extends Line2D.Float { - private static int PERPENDICULAR_PIXEL_EXPAND_AMOUNT = 2; + public static final int PERPENDICULAR_UNIT_EXPAND_AMOUNT = 2; + public static final int COLINEAR_OR_PARALLEL_UNIT_EXPAND_AMOUNT = 2; public Ruling(Point2D p1, Point2D p2) { @@ -110,8 +111,8 @@ public class Ruling extends Line2D.Float { }); for (Ruling h : horizontals) { - sos.add(new SortObject(SOType.HLEFT, h.getLeft() - PERPENDICULAR_PIXEL_EXPAND_AMOUNT, h)); - sos.add(new SortObject(SOType.HRIGHT, h.getRight() + PERPENDICULAR_PIXEL_EXPAND_AMOUNT, h)); + sos.add(new SortObject(SOType.HLEFT, h.getLeft() - PERPENDICULAR_UNIT_EXPAND_AMOUNT, h)); + sos.add(new SortObject(SOType.HRIGHT, h.getRight() + PERPENDICULAR_UNIT_EXPAND_AMOUNT, h)); } for (Ruling v : verticals) { @@ -151,7 +152,7 @@ public class Ruling extends Line2D.Float { if (i == null) { continue; } - rv.put(i, new Ruling[]{h.getKey().expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT), so.ruling.expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT)}); + rv.put(i, new Ruling[]{h.getKey().expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT), so.ruling.expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT)}); } catch (UnsupportedOperationException e) { log.info("Some line are oblique, ignoring..."); continue; @@ -267,7 +268,7 @@ public class Ruling extends Line2D.Float { } - public boolean nearlyIntersects(Ruling another, int colinearOrParallelExpandAmount) { + public boolean nearlyIntersects(Ruling another) { if (this.intersectsLine(another)) { return true; @@ -276,9 +277,9 @@ public class Ruling extends Line2D.Float { boolean rv = false; if (this.perpendicularTo(another)) { - rv = this.expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT).intersectsLine(another); + rv = this.expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT).intersectsLine(another); } else { - rv = this.expand(colinearOrParallelExpandAmount).intersectsLine(another.expand(colinearOrParallelExpandAmount)); + rv = this.expand(COLINEAR_OR_PARALLEL_UNIT_EXPAND_AMOUNT).intersectsLine(another.expand(COLINEAR_OR_PARALLEL_UNIT_EXPAND_AMOUNT)); } return rv; @@ -319,8 +320,8 @@ public class Ruling extends Line2D.Float { public Point2D intersectionPoint(Ruling other) { - Ruling this_l = this.expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT); - Ruling other_l = other.expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT); + Ruling this_l = this.expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT); + Ruling other_l = other.expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT); Ruling horizontal, vertical; if (!this_l.intersectsLine(other_l)) { diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/TablePageBlock.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/TablePageBlock.java index 8e91dae..020dca6 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/TablePageBlock.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/model/table/TablePageBlock.java @@ -3,6 +3,7 @@ package com.knecon.fforesight.service.layoutparser.processor.model.table; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -11,6 +12,7 @@ import java.util.TreeMap; import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock; import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType; import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock; +import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations; import lombok.Getter; import lombok.Setter; @@ -19,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class TablePageBlock extends AbstractPageBlock { + public static final double CELL_AREA_CONTAINED_THRESHOLD = 0.98; private final TreeMap cellTreeMap = new TreeMap<>(); private final int rotation; @@ -93,7 +96,7 @@ public class TablePageBlock extends AbstractPageBlock { /** * Detect header cells (either first row or first column): - * Column is marked as header if cell text is bold and row cell text is not bold. + * Column is marked as header if originalCell text is bold and row originalCell text is not bold. * Defaults to row. */ private void computeHeaders() { @@ -101,7 +104,7 @@ public class TablePageBlock extends AbstractPageBlock { if (rows == null) { rows = computeRows(); } - // A bold cell is a header cell as long as every cell to the left/top is bold, too + // A bold originalCell is a header originalCell as long as every originalCell to the left/top is bold, too // we move from left to right and top to bottom for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) { List rowCells = rows.get(rowIndex); @@ -257,15 +260,19 @@ public class TablePageBlock extends AbstractPageBlock { for (Float x : sortedUniqueX) { if (prevY != null && prevX != null) { - var cell = new Cell(new Point2D.Float(prevX, prevY), new Point2D.Float(x, y)); + var cellFromGridStructure = new Cell(new Point2D.Float(prevX, prevY), new Point2D.Float(x, y)); - if (cell.hasMinimumSize()) { + if (cellFromGridStructure.hasMinimumSize()) { cells.stream() - .filter(cell::nearlyIntersects) - .forEach(intersectingCell -> cell.getTextBlocks().addAll(intersectingCell.getTextBlocks())); + .map(originalCell -> new CellWithIntersection(originalCell, RectangleTransformations.calculateIntersectedArea(cellFromGridStructure, originalCell))) + .filter(cellWithIntersection -> cellWithIntersection.intersectedArea > 0) + .filter(cellWithIntersection -> cellWithIntersection.originalCell.getArea() > cellWithIntersection.intersectedArea * CELL_AREA_CONTAINED_THRESHOLD) + .max(Comparator.comparing(CellWithIntersection::intersectedArea)) + .map(CellWithIntersection::originalCell) + .ifPresent(matchingCell -> cellFromGridStructure.getTextBlocks().addAll(matchingCell.getTextBlocks())); - row.add(cell); + row.add(cellFromGridStructure); } } prevX = x; @@ -405,4 +412,9 @@ public class TablePageBlock extends AbstractPageBlock { return sb.toString(); } + + record CellWithIntersection(Cell originalCell, double intersectedArea) { + + } + } \ No newline at end of file diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/RulingCleaningService.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/RulingCleaningService.java index 2a18dc0..c51c90b 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/RulingCleaningService.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/RulingCleaningService.java @@ -1,21 +1,21 @@ package com.knecon.fforesight.service.layoutparser.processor.services; -import java.awt.geom.Line2D; +import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.X_FIRST_RULING_COMPARATOR; + import java.awt.geom.Point2D; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings; +import com.knecon.fforesight.service.layoutparser.processor.model.table.Rectangle; import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling; import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.TableCells; -import com.knecon.fforesight.service.layoutparser.processor.utils.DoubleComparisons; +import com.knecon.fforesight.service.layoutparser.processor.utils.UnionFind; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,26 +25,145 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class RulingCleaningService { - private static final float THRESHOLD_Y = 6; - private static final float THRESHOLD_X = 2; + private static final float THRESHOLD_X_VERTICAL = 1; + private static final float THRESHOLD_Y_VERTICAL = 2; + private static final float THRESHOLD_X_HORIZONTAL = 2; + private static final float THRESHOLD_Y_HORIZONTAL = 3; public CleanRulings getCleanRulings(List tableCells, List rulings) { + Rulings verticalAndHorizontalRulingLines; + if (!rulings.isEmpty()) { - snapPoints(rulings); + verticalAndHorizontalRulingLines = extractVerticalAndHorizontalRulingLines(rulings); + } else { + verticalAndHorizontalRulingLines = getRulingsFromParsedCells(tableCells); } + verticalAndHorizontalRulingLines.verticalLines.sort(X_FIRST_RULING_COMPARATOR); + verticalAndHorizontalRulingLines.horizontalLines.sort(X_FIRST_RULING_COMPARATOR); + verticalAndHorizontalRulingLines = cleanRulings(verticalAndHorizontalRulingLines); + + return CleanRulings.builder().vertical(verticalAndHorizontalRulingLines.verticalLines()).horizontal(verticalAndHorizontalRulingLines.horizontalLines()).build(); + } + + + private Rulings cleanRulings(Rulings rulings) { + + List> groupedOverlappingVerticalRectangles = groupOverlappingRectangles(rulings.verticalLines.stream() + .map(RulingCleaningService::getOverlapRectangle) + .distinct() + .toList()); + List cleanedVerticalRulings = groupedOverlappingVerticalRectangles.stream() + .map(rectList -> getXCenteredRuling(Rectangle.boundingBoxOf(rectList))) + .toList(); + + List> groupedOverlappingHorizontalRectangles = groupOverlappingRectangles(rulings.horizontalLines.stream() + .map(RulingCleaningService::getOverlapRectangle) + .distinct() + .toList()); + + List cleanedHorizontalRulings = groupedOverlappingHorizontalRectangles.stream() + .map(rectList -> getYCenteredRuling(Rectangle.boundingBoxOf(rectList))) + .collect(Collectors.toList()); + + return new Rulings(cleanedVerticalRulings, cleanedHorizontalRulings); + } + + + private List> groupOverlappingRectangles(List rectangles) { + + UnionFind unionFind = new UnionFind<>(); + for (int i = 0; i < rectangles.size(); i++) { + for (int j = i + 1; j < rectangles.size(); j++) { + Rectangle rectangle1 = rectangles.get(i); + Rectangle rectangle2 = rectangles.get(j); + + // we can stop early when we are too far off because of x-y-sorting + if(rectangle1.getRight() < rectangle2.getLeft() && rectangle1.getBottom() < rectangle2.getTop()) { + break; + } + + if (rectangle1.intersects(rectangle2)) { + unionFind.union(rectangle1, rectangle2); + } + } + } + + Map> groups = new HashMap<>(); + for (Rectangle rectangle : rectangles) { + Rectangle root = unionFind.find(rectangle); + groups.computeIfAbsent(root, k -> new ArrayList<>()).add(rectangle); + } + return new ArrayList<>(groups.values()); + } + + + private static Rectangle getOverlapRectangle(Ruling ruling) { + + float top; + float left; + float w; + float h; + + if (ruling.x1 < ruling.x2) { + left = ruling.x1; + w = ruling.x2 - ruling.x1; + } else { + left = ruling.x2; + w = ruling.x1 - ruling.x2; + } + if (ruling.y1 < ruling.y2) { + top = ruling.y1; + h = ruling.y2 - ruling.y1; + } else { + top = ruling.y2; + h = ruling.y1 - ruling.y2; + } + + if (ruling.horizontal()) { + return new Rectangle(top - THRESHOLD_Y_HORIZONTAL, left - THRESHOLD_X_HORIZONTAL, w + 2 * THRESHOLD_X_HORIZONTAL, h + 2 * THRESHOLD_Y_HORIZONTAL); + } else { + return new Rectangle(top - THRESHOLD_Y_VERTICAL, left - THRESHOLD_X_VERTICAL, w + 2 * THRESHOLD_X_VERTICAL, h + 2 * THRESHOLD_Y_VERTICAL); + } + } + + + public static Ruling getXCenteredRuling(Rectangle rectangle) { + + float x = (float) rectangle.getCenterX(); + float y1 = rectangle.getTop(); + float y2 = rectangle.getBottom(); + + Point2D point1 = new Point2D.Float(x, y1 + THRESHOLD_Y_VERTICAL); + Point2D point2 = new Point2D.Float(x, y2 - THRESHOLD_Y_VERTICAL); + + return new Ruling(point1, point2); + } + + + public static Ruling getYCenteredRuling(Rectangle rectangle) { + + float x1 = rectangle.getLeft(); + float x2 = rectangle.getRight(); + float y = (float) rectangle.getCenterY(); + + Point2D point1 = new Point2D.Float(x1 + THRESHOLD_X_HORIZONTAL, y); + Point2D point2 = new Point2D.Float(x2 - THRESHOLD_X_HORIZONTAL, y); + + return new Ruling(point1, point2); + } + + + private Rulings extractVerticalAndHorizontalRulingLines(List rulings) { + List vrs = new ArrayList<>(); for (Ruling vr : rulings) { if (vr.vertical()) { vrs.add(vr); } } - if (vrs.isEmpty()) { - vrs.addAll(extractVerticalRulings(tableCells)); - } - List verticalRulingLines = collapseOrientedRulings(vrs); List hrs = new ArrayList<>(); for (Ruling hr : rulings) { @@ -52,98 +171,26 @@ public class RulingCleaningService { hrs.add(hr); } } - if (hrs.isEmpty()) { - hrs.addAll(extractHorizontalRulings(tableCells)); - } - List horizontalRulingLines = collapseOrientedRulings(hrs); - - return CleanRulings.builder().vertical(verticalRulingLines).horizontal(horizontalRulingLines).build(); + return new Rulings(vrs, hrs); } - public void snapPoints(List rulings) { + private Rulings getRulingsFromParsedCells(List tableCells) { - // collect points and keep a Line -> p1,p2 map - Map linesToPoints = new HashMap<>(); - List points = new ArrayList<>(); - for (Line2D.Float r : rulings) { - Point2D p1 = r.getP1(); - Point2D p2 = r.getP2(); - linesToPoints.put(r, new Point2D[]{p1, p2}); - points.add(p1); - points.add(p2); - } - - // snap by X - points.sort(Comparator.comparingDouble(Point2D::getX)); - - List> groupedPoints = new ArrayList<>(); - groupedPoints.add(new ArrayList<>(Collections.singletonList(points.get(0)))); - - for (Point2D p : points.subList(1, points.size() - 1)) { - List last = groupedPoints.get(groupedPoints.size() - 1); - if (Math.abs(p.getX() - last.get(0).getX()) < THRESHOLD_X) { - groupedPoints.get(groupedPoints.size() - 1).add(p); - } else { - groupedPoints.add(new ArrayList<>(Collections.singletonList(p))); - } - } - - for (List group : groupedPoints) { - float avgLoc = 0; - for (Point2D p : group) { - avgLoc += p.getX(); - } - avgLoc /= group.size(); - for (Point2D p : group) { - p.setLocation(avgLoc, p.getY()); - } - } - // --- - - // snap by Y - points.sort(Comparator.comparingDouble(Point2D::getY)); - - groupedPoints = new ArrayList<>(); - groupedPoints.add(new ArrayList<>(Collections.singletonList(points.get(0)))); - - for (Point2D p : points.subList(1, points.size() - 1)) { - List last = groupedPoints.get(groupedPoints.size() - 1); - if (Math.abs(p.getY() - last.get(0).getY()) < THRESHOLD_Y) { - groupedPoints.get(groupedPoints.size() - 1).add(p); - } else { - groupedPoints.add(new ArrayList<>(Collections.singletonList(p))); - } - } - - for (List group : groupedPoints) { - float avgLoc = 0; - for (Point2D p : group) { - avgLoc += p.getY(); - } - avgLoc /= group.size(); - for (Point2D p : group) { - p.setLocation(p.getX(), avgLoc); - } - } - // --- - - // finally, modify lines - for (Map.Entry ltp : linesToPoints.entrySet()) { - Point2D[] p = ltp.getValue(); - ltp.getKey().setLine(p[0], p[1]); - } + List vrs = extractVerticalRulingsFromParsedCells(tableCells); + List hrs = extractHorizontalRulingsFromParsedCells(tableCells); + return new Rulings(vrs, hrs); } - private Collection extractVerticalRulings(List cvParsedTableCells) { + private List extractVerticalRulingsFromParsedCells(List tableCells) { List vrs = new ArrayList<>(); - if (cvParsedTableCells != null) { - for (TableCells cvParsedTableCell : cvParsedTableCells) { - Ruling leftLine = createRuling(cvParsedTableCell.getX0(), cvParsedTableCell.getX0(), cvParsedTableCell.getY0(), cvParsedTableCell.getY1()); - Ruling rightLine = createRuling(cvParsedTableCell.getX1(), cvParsedTableCell.getX1(), cvParsedTableCell.getY0(), cvParsedTableCell.getY1()); + if (tableCells != null) { + for (TableCells tableCell : tableCells) { + Ruling leftLine = createRuling(tableCell.getX0(), tableCell.getX0(), tableCell.getY0(), tableCell.getY1()); + Ruling rightLine = createRuling(tableCell.getX1(), tableCell.getX1(), tableCell.getY0(), tableCell.getY1()); vrs.add(leftLine); vrs.add(rightLine); } @@ -152,19 +199,18 @@ public class RulingCleaningService { } - private Collection extractHorizontalRulings(List cvParsedTableCells) { + private List extractHorizontalRulingsFromParsedCells(List tableCells) { List hrs = new ArrayList<>(); - if (cvParsedTableCells != null) { - for (TableCells cvParsedTableCell : cvParsedTableCells) { - Ruling topLine = createRuling(cvParsedTableCell.getX0(), cvParsedTableCell.getX1(), cvParsedTableCell.getY1(), cvParsedTableCell.getY1()); - Ruling baseLine = createRuling(cvParsedTableCell.getX0(), cvParsedTableCell.getX1(), cvParsedTableCell.getY0(), cvParsedTableCell.getY0()); + if (tableCells != null) { + for (TableCells tableCell : tableCells) { + Ruling topLine = createRuling(tableCell.getX0(), tableCell.getX1(), tableCell.getY1(), tableCell.getY1()); + Ruling baseLine = createRuling(tableCell.getX0(), tableCell.getX1(), tableCell.getY0(), tableCell.getY0()); hrs.add(topLine); hrs.add(baseLine); } } - return hrs; } @@ -190,46 +236,8 @@ public class RulingCleaningService { } - private List collapseOrientedRulings(List lines) { + private record Rulings(List verticalLines, List horizontalLines) { - int COLINEAR_OR_PARALLEL_PIXEL_EXPAND_AMOUNT = 1; - return collapseOrientedRulings(lines, COLINEAR_OR_PARALLEL_PIXEL_EXPAND_AMOUNT); - } - - - private List collapseOrientedRulings(List lines, int expandAmount) { - - ArrayList rv = new ArrayList<>(); - lines.sort((a, b) -> { - final float diff = a.getPosition() - b.getPosition(); - return Float.compare(diff == 0 ? a.getStart() - b.getStart() : diff, 0f); - }); - - for (Ruling next_line : lines) { - Ruling last = rv.isEmpty() ? null : rv.get(rv.size() - 1); - // if current line colinear with next, and are "close enough": expand current line - if (last != null && DoubleComparisons.feq(next_line.getPosition(), last.getPosition()) && last.nearlyIntersects(next_line, expandAmount)) { - final float lastStart = last.getStart(); - final float lastEnd = last.getEnd(); - - final boolean lastFlipped = lastStart > lastEnd; - final boolean nextFlipped = next_line.getStart() > next_line.getEnd(); - - boolean differentDirections = nextFlipped != lastFlipped; - float nextS = differentDirections ? next_line.getEnd() : next_line.getStart(); - float nextE = differentDirections ? next_line.getStart() : next_line.getEnd(); - - final float newStart = lastFlipped ? Math.max(nextS, lastStart) : Math.min(nextS, lastStart); - final float newEnd = lastFlipped ? Math.min(nextE, lastEnd) : Math.max(nextE, lastEnd); - last.setStartEnd(newStart, newEnd); - assert !last.oblique(); - } else if (next_line.length() == 0) { - continue; - } else { - rv.add(next_line); - } - } - return rv; } } diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/TableExtractionService.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/TableExtractionService.java index 8dd639d..2827153 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/TableExtractionService.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/TableExtractionService.java @@ -1,13 +1,13 @@ package com.knecon.fforesight.service.layoutparser.processor.services; -import java.awt.geom.Point2D; +import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.CELL_SIZE_COMPARATOR; +import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.RECTANGLE_SIZE_COMPARATOR; + import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -20,66 +20,15 @@ import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling; import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock; import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock; import com.knecon.fforesight.service.layoutparser.processor.utils.DoubleComparisons; +import com.knecon.fforesight.service.layoutparser.processor.utils.RectangularIntersectionFinder; +import com.knecon.fforesight.service.layoutparser.processor.utils.SpreadsheetFinder; @Service public class TableExtractionService { - private static final int MAX_TABLE_OUTER_POINT_TOLERANCE = 10; private static final int MAX_TABLE_CONTAINED_CELLS_WITH_TEXT = 1; - private static final float SPREADSHEET_AREA_TOLERANCE = 0.001f; - - private static final Comparator X_FIRST_POINT_COMPARATOR = (point1, point2) -> { - - int rv = 0; - float point1X = DoubleComparisons.round(point1.getX(), 2); - float point1Y = DoubleComparisons.round(point1.getY(), 2); - float point2X = DoubleComparisons.round(point2.getX(), 2); - float point2Y = DoubleComparisons.round(point2.getY(), 2); - - if (point1X > point2X) { - rv = 1; - } else if (point1X < point2X) { - rv = -1; - } else if (point1Y > point2Y) { - rv = 1; - } else if (point1Y < point2Y) { - rv = -1; - } - return rv; - }; - private static final Comparator Y_FIRST_POINT_COMPARATOR = (point1, point2) -> { - - int rv = 0; - float point1X = DoubleComparisons.round(point1.getX(), 2); - float point1Y = DoubleComparisons.round(point1.getY(), 2); - float point2X = DoubleComparisons.round(point2.getX(), 2); - float point2Y = DoubleComparisons.round(point2.getY(), 2); - - if (point1Y > point2Y) { - rv = 1; - } else if (point1Y < point2Y) { - rv = -1; - } else if (point1X > point2X) { - rv = 1; - } else if (point1X < point2X) { - rv = -1; - } - return rv; - }; - - private static final Comparator CELL_SIZE_COMPARATOR = (cell1, cell2) -> { - - Double cell1Size = cell1.getHeight() * cell1.getWidth(); - Double cell2Size = cell2.getHeight() * cell2.getWidth(); - return cell1Size.compareTo(cell2Size); - }; - - private static final Comparator RECTANGLE_SIZE_COMPARATOR = (rect1, rect2) -> { - - Double rect1Size = rect1.getHeight() * rect1.getWidth(); - Double rect2Size = rect2.getHeight() * rect2.getWidth(); - return rect1Size.compareTo(rect2Size); - }; + private static final int TEXT_BLOCK_CONTAINMENT_TOLERANCE = 2; + private static final double TABLE_UNIFORMITY_THRESHOLD = 0.7; /** @@ -115,7 +64,7 @@ public class TableExtractionService { cells = new ArrayList<>(new HashSet<>(cells)); DoubleComparisons.sort(cells, Rectangle.ILL_DEFINED_ORDER); - List spreadsheetAreas = findSpreadsheetsFromCells(cells); + List spreadsheetAreas = SpreadsheetFinder.findSpreadsheetsFromCells(cells); // sort spreadsheetAreas by size (height * width) ascending so that cells are placed in the smallest tables first // this way no cell duplication occurs when tables are contained in other tables and only the most inner table contains the cells spreadsheetAreas.sort(RECTANGLE_SIZE_COMPARATOR); @@ -132,10 +81,10 @@ public class TableExtractionService { var containedCellsWithText = containedCells.stream() .filter(cell -> !cell.getTextBlocks().isEmpty()) - .count(); + .toList(); // verify if table would contain fewer cells with text than the threshold allows - if (containedCellsWithText >= MAX_TABLE_CONTAINED_CELLS_WITH_TEXT) { + if (containedCellsWithText.size() >= MAX_TABLE_CONTAINED_CELLS_WITH_TEXT && checkIfTableCellsAreUniform(containedCells)) { tables.add(new TablePageBlock(containedCells, area, page.getRotation())); cells.removeAll(containedCells); } @@ -164,6 +113,21 @@ public class TableExtractionService { } + private boolean checkIfTableCellsAreUniform(List containedCells) { + + if(containedCells.size() <= 2) { + return true; + } + + Map> cellsGroupedByRoundedWidth = containedCells.stream() + .map(Rectangle::getWidth) + .map(size -> Math.round(size / 10.0) * 10) + .collect(Collectors.groupingBy(Long::longValue)); + + return (double) cellsGroupedByRoundedWidth.size() / containedCells.size() <= TABLE_UNIFORMITY_THRESHOLD; + } + + private boolean doesCellContainTextBlock(Cell cell, TextPageBlock textBlock) { double x = textBlock.getPdfMinX(); @@ -175,225 +139,19 @@ public class TableExtractionService { } double x0 = cell.getX(); double y0 = cell.getY(); - return (x >= x0 - 2 && y >= y0 - 2 && (x + w) <= x0 + cell.getWidth() + 2 && (y + h) <= y0 + cell.getHeight() + 2); + return (x >= x0 - TEXT_BLOCK_CONTAINMENT_TOLERANCE + && y >= y0 - TEXT_BLOCK_CONTAINMENT_TOLERANCE + && (x + w) <= x0 + cell.getWidth() + 2 * TEXT_BLOCK_CONTAINMENT_TOLERANCE + && (y + h) <= y0 + cell.getHeight() + 2 * TEXT_BLOCK_CONTAINMENT_TOLERANCE); } - private List findCells(List horizontalRulingLines, List verticalRulingLines) { - - // Fix for 211.pdf - for (Ruling r : horizontalRulingLines) { - if (r.getX2() < r.getX1()) { - double a = r.getX2(); - r.x2 = (float) r.getX1(); - r.x1 = (float) a; - } - } - - List cellsFound = new ArrayList<>(); - Map intersectionPoints = Ruling.findIntersections(horizontalRulingLines, verticalRulingLines); - List intersectionPointsList = new ArrayList<>(intersectionPoints.keySet()); - intersectionPointsList.sort(Y_FIRST_POINT_COMPARATOR); - - for (int i = 0; i < intersectionPointsList.size(); i++) { - Point2D topLeft = intersectionPointsList.get(i); - Ruling[] hv = intersectionPoints.get(topLeft); - - // CrossingPointsDirectlyBelow( topLeft ); - List xPoints = new ArrayList<>(); - // CrossingPointsDirectlyToTheRight( topLeft ); - List yPoints = new ArrayList<>(); - - for (Point2D p : intersectionPointsList.subList(i, intersectionPointsList.size())) { - if (p.getX() == topLeft.getX() && p.getY() > topLeft.getY()) { - xPoints.add(p); - } - if (p.getY() == topLeft.getY() && p.getX() > topLeft.getX()) { - yPoints.add(p); - } - } - outer: - for (Point2D xPoint : xPoints) { - // is there a vertical edge b/w topLeft and xPoint? - if (!hv[1].equals(intersectionPoints.get(xPoint)[1])) { - continue; - } - for (Point2D yPoint : yPoints) { - // is there a horizontal edge b/w topLeft and yPoint ? - if (!hv[0].equals(intersectionPoints.get(yPoint)[0])) { - continue; - } - Point2D btmRight = new Point2D.Float((float) yPoint.getX(), (float) xPoint.getY()); - if (intersectionPoints.containsKey(btmRight) - && intersectionPoints.get(btmRight)[0].equals(intersectionPoints.get(xPoint)[0]) - && intersectionPoints.get(btmRight)[1].equals(intersectionPoints.get(yPoint)[1])) { - cellsFound.add(new Cell(topLeft, btmRight)); - break outer; - } - } - } - } - - // TODO create cells for vertical ruling lines with aligned endpoints at the top/bottom of a grid - // that aren't connected with an horizontal ruler? - // see: https://github.com/jazzido/tabula-extractor/issues/78#issuecomment-41481207 - - return cellsFound; - } - - - private List findSpreadsheetsFromCells(List cells) { - // via: http://stackoverflow.com/questions/13746284/merging-multiple-adjacent-rectangles-into-one-polygon - List rectangles = new ArrayList<>(); - Set pointSet = new HashSet<>(); - Map edgesH = new HashMap<>(); - Map edgesV = new HashMap<>(); - - for (Rectangle cell : cells) { - for (Point2D pt : cell.getPoints()) { - if (pointSet.contains(pt)) { // shared vertex, remove it - pointSet.remove(pt); - } else { - pointSet.add(pt); - } - } - } - - // X first sort - List pointsSortX = new ArrayList<>(pointSet); - pointsSortX.sort(X_FIRST_POINT_COMPARATOR); - // Y first sort - List pointsSortY = new ArrayList<>(pointSet); - pointsSortY.sort(Y_FIRST_POINT_COMPARATOR); - - int i = 0; - while (i < pointSet.size()) { - float currY = (float) pointsSortY.get(i).getY(); - while (i < pointSet.size() && DoubleComparisons.feq(pointsSortY.get(i).getY(), currY)) { - edgesH.put(pointsSortY.get(i), pointsSortY.get(i + 1)); - edgesH.put(pointsSortY.get(i + 1), pointsSortY.get(i)); - i += 2; - } - } - - i = 0; - while (i < pointSet.size()) { - float currX = (float) pointsSortX.get(i).getX(); - while (i < pointSet.size() && DoubleComparisons.feq(pointsSortX.get(i).getX(), currX)) { - edgesV.put(pointsSortX.get(i), pointsSortX.get(i + 1)); - edgesV.put(pointsSortX.get(i + 1), pointsSortX.get(i)); - i += 2; - } - } - - // Get all the polygons - List> polygons = new ArrayList<>(); - Point2D nextVertex; - while (!edgesH.isEmpty()) { - ArrayList polygon = new ArrayList<>(); - Point2D first = edgesH.keySet() - .iterator().next(); - polygon.add(new PolygonVertex(first, Direction.HORIZONTAL)); - edgesH.remove(first); - - while (true) { - PolygonVertex curr = polygon.get(polygon.size() - 1); - PolygonVertex lastAddedVertex; - if (curr.direction == Direction.HORIZONTAL) { - nextVertex = edgesV.get(curr.point); - edgesV.remove(curr.point); - lastAddedVertex = new PolygonVertex(nextVertex, Direction.VERTICAL); - } else { - nextVertex = edgesH.get(curr.point); - edgesH.remove(curr.point); - lastAddedVertex = new PolygonVertex(nextVertex, Direction.HORIZONTAL); - } - polygon.add(lastAddedVertex); - - if (lastAddedVertex.equals(polygon.get(0))) { - // closed polygon - polygon.remove(polygon.size() - 1); - break; - } - } - - for (PolygonVertex vertex : polygon) { - edgesH.remove(vertex.point); - edgesV.remove(vertex.point); - } - polygons.add(polygon); - } - - // calculate grid-aligned minimum area rectangles for each found polygon - for (List poly : polygons) { - float top = Float.MAX_VALUE; - float left = Float.MAX_VALUE; - float bottom = Float.MIN_VALUE; - float right = Float.MIN_VALUE; - for (PolygonVertex pt : poly) { - top = (float) Math.min(top, pt.point.getY()); - left = (float) Math.min(left, pt.point.getX()); - bottom = (float) Math.max(bottom, pt.point.getY()); - right = (float) Math.max(right, pt.point.getX()); - } - - // do not add polygons with too many outer points as they are unlikely to be tables - if (poly.size() <= MAX_TABLE_OUTER_POINT_TOLERANCE) { - rectangles.add(new Rectangle(top - SPREADSHEET_AREA_TOLERANCE, - left - SPREADSHEET_AREA_TOLERANCE, - right - left + 2 * SPREADSHEET_AREA_TOLERANCE, - bottom - top + 2 * SPREADSHEET_AREA_TOLERANCE)); - } - } - - return rectangles; - } - - - private enum Direction { - HORIZONTAL, - VERTICAL - } - - static class PolygonVertex { - - Point2D point; - Direction direction; - - - PolygonVertex(Point2D point, Direction direction) { - - this.direction = direction; - this.point = point; - } - - - @Override - public boolean equals(Object other) { - - if (this == other) { - return true; - } - if (!(other instanceof PolygonVertex)) { - return false; - } - return this.point.equals(((PolygonVertex) other).point); - } - - - @Override - public int hashCode() { - - return this.point.hashCode(); - } - - - @Override - public String toString() { - - return String.format("%s[point=%s,direction=%s]", this.getClass().getName(), this.point.toString(), this.direction.toString()); - } + public static List findCells(List horizontalRulingLines, List verticalRulingLines) { + return RectangularIntersectionFinder.find(horizontalRulingLines, verticalRulingLines) + .stream() + .map(Cell::new) + .collect(Collectors.toList()); } } diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/factory/DocumentGraphFactory.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/factory/DocumentGraphFactory.java index 6bde310..20440eb 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/factory/DocumentGraphFactory.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/factory/DocumentGraphFactory.java @@ -105,8 +105,8 @@ public class DocumentGraphFactory { .build(); page.getMainBody().add(imageNode); - List tocId = context.getDocumentTree().createNewChildEntryAndReturnId(section, imageNode); - imageNode.setTreeId(tocId); + List treeId = context.getDocumentTree().createNewChildEntryAndReturnId(section, imageNode); + imageNode.setTreeId(treeId); } diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/visualization/LayoutGridService.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/visualization/LayoutGridService.java index a2ebcc0..92ff832 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/visualization/LayoutGridService.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/services/visualization/LayoutGridService.java @@ -188,14 +188,33 @@ public class LayoutGridService { @SneakyThrows private void addPlacedText(Page page, Rectangle2D textBBox, String s, LayoutGrid layoutGrid) { - Point2D.Float upperLeftCorner = switch (page.getRotation()) { - case 90 -> new Point2D.Float((float) (textBBox.getMinX()), (float) textBBox.getMinY()); - case 180 -> new Point2D.Float((float) (textBBox.getMaxX()), (float) textBBox.getMinY()); - case 270 -> new Point2D.Float((float) (textBBox.getMaxX()), (float) textBBox.getMaxY()); - default -> new Point2D.Float((float) (textBBox.getMinX()), (float) textBBox.getMaxY()); - }; + // translates text, such that its right edge is a bit to the left of the drawn box + float translationAmount = ((FONT.getStringWidth(s) / 1000) * FONT_SIZE + (2 * LINE_WIDTH) + 4); + + Point2D upperLeftCorner; + Point2D translationVector; + switch (page.getRotation()) { + case 90 -> { + upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMinY()); + translationVector = new Point2D.Double(FONT_SIZE, -translationAmount); + } + case 180 -> { + upperLeftCorner = new Point2D.Double(textBBox.getMaxX(), textBBox.getMinY()); + translationVector = new Point2D.Double(translationAmount, FONT_SIZE); + } + case 270 -> { + upperLeftCorner = new Point2D.Double(textBBox.getMaxX(), textBBox.getMaxY()); + translationVector = new Point2D.Double(-FONT_SIZE, translationAmount); + } + default -> { + upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMaxY()); + translationVector = new Point2D.Double(-translationAmount, -FONT_SIZE); + } + } + + upperLeftCorner = add(upperLeftCorner, translationVector); + var placedTexts = layoutGrid.getVisualizationsPerPages().get(page.getNumber() - 1).getPlacedTexts(); - upperLeftCorner.setLocation(upperLeftCorner.getX() - ((FONT.getStringWidth(s) / 1000) * FONT_SIZE + (2 * LINE_WIDTH) + 4), upperLeftCorner.getY() - FONT_SIZE); placedTexts.add(PlacedText.textFacingUp(s, upperLeftCorner, FONT_SIZE, Color.BLACK, FONT)); } @@ -317,4 +336,10 @@ public class LayoutGridService { .add(new ColoredRectangle(textBBox, color, LINE_WIDTH))); } + + private Point2D add(Point2D a, Point2D b) { + + return new Point2D.Double(a.getX() + b.getX(), a.getY() + b.getY()); + } + } diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/DrawingOptions.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/DrawingOptions.java new file mode 100644 index 0000000..341e127 --- /dev/null +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/DrawingOptions.java @@ -0,0 +1,28 @@ +package com.knecon.fforesight.service.layoutparser.processor.utils; + +import java.awt.Color; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.experimental.FieldDefaults; + +@Builder +@AllArgsConstructor +@Getter +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +public class DrawingOptions { + + boolean stroke; + @Builder.Default + Color strokeColor = Color.BLACK; + @Builder.Default + float strokeWidth = 1f; + + boolean fill; + @Builder.Default + Color fillColor = Color.BLACK; + +} + diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/GeometricComparators.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/GeometricComparators.java new file mode 100644 index 0000000..c21b516 --- /dev/null +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/GeometricComparators.java @@ -0,0 +1,88 @@ +package com.knecon.fforesight.service.layoutparser.processor.utils; + +import java.awt.geom.Point2D; +import java.util.Comparator; + +import com.knecon.fforesight.service.layoutparser.processor.model.table.Cell; +import com.knecon.fforesight.service.layoutparser.processor.model.table.Rectangle; +import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling; + +public class GeometricComparators { + + private static final int COMPARATOR_ROUNDING = 2; + + public static final Comparator X_FIRST_POINT_COMPARATOR = (point1, point2) -> { + + int rv = 0; + float point1X = DoubleComparisons.round(point1.getX(), COMPARATOR_ROUNDING); + float point1Y = DoubleComparisons.round(point1.getY(), COMPARATOR_ROUNDING); + float point2X = DoubleComparisons.round(point2.getX(), COMPARATOR_ROUNDING); + float point2Y = DoubleComparisons.round(point2.getY(), COMPARATOR_ROUNDING); + + if (point1X > point2X) { + rv = 1; + } else if (point1X < point2X) { + rv = -1; + } else if (point1Y > point2Y) { + rv = 1; + } else if (point1Y < point2Y) { + rv = -1; + } + return rv; + }; + + public static final Comparator Y_FIRST_POINT_COMPARATOR = (point1, point2) -> { + + int rv = 0; + float point1X = DoubleComparisons.round(point1.getX(), COMPARATOR_ROUNDING); + float point1Y = DoubleComparisons.round(point1.getY(), COMPARATOR_ROUNDING); + float point2X = DoubleComparisons.round(point2.getX(), COMPARATOR_ROUNDING); + float point2Y = DoubleComparisons.round(point2.getY(), COMPARATOR_ROUNDING); + + if (point1Y > point2Y) { + rv = 1; + } else if (point1Y < point2Y) { + rv = -1; + } else if (point1X > point2X) { + rv = 1; + } else if (point1X < point2X) { + rv = -1; + } + return rv; + }; + + public static final Comparator CELL_SIZE_COMPARATOR = (cell1, cell2) -> { + + Double cell1Size = cell1.getHeight() * cell1.getWidth(); + Double cell2Size = cell2.getHeight() * cell2.getWidth(); + return cell1Size.compareTo(cell2Size); + }; + + public static final Comparator RECTANGLE_SIZE_COMPARATOR = (rect1, rect2) -> { + + Double rect1Size = rect1.getHeight() * rect1.getWidth(); + Double rect2Size = rect2.getHeight() * rect2.getWidth(); + return rect1Size.compareTo(rect2Size); + }; + + public static final Comparator X_FIRST_RULING_COMPARATOR = (ruling1, ruling2) -> { + + int rv = 0; + float point1X = DoubleComparisons.round(Math.min(ruling1.getLeft(), ruling1.getRight()), COMPARATOR_ROUNDING); + float point1Y = DoubleComparisons.round(Math.min(ruling1.getTop(), ruling1.getBottom()), COMPARATOR_ROUNDING); + float point2X = DoubleComparisons.round(Math.min(ruling2.getLeft(), ruling2.getRight()), COMPARATOR_ROUNDING); + float point2Y = DoubleComparisons.round(Math.min(ruling2.getTop(), ruling2.getBottom()), COMPARATOR_ROUNDING); + + if (point1X > point2X) { + rv = 1; + } else if (point1X < point2X) { + rv = -1; + } else if (point1Y > point2Y) { + rv = 1; + } else if (point1Y < point2Y) { + rv = -1; + } + return rv; + }; + +} diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/PdfVisualisationUtility.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/PdfVisualisationUtility.java index b950340..bf64c12 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/PdfVisualisationUtility.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/PdfVisualisationUtility.java @@ -21,11 +21,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Pa import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock; import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; import lombok.SneakyThrows; -import lombok.experimental.FieldDefaults; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; @@ -41,20 +37,20 @@ public class PdfVisualisationUtility { public void drawNode(PDDocument document, DocumentTree.Entry entry) { - Options options = buildStandardOptionsForNodes(entry); + DrawingOptions options = buildStandardOptionsForNodes(entry); drawBBoxAndLabelAndNumberOnPage(document, entry, options); } - public void drawTextBlock(PDDocument document, TextBlock textBlock, Options options) { + public void drawTextBlock(PDDocument document, TextBlock textBlock, DrawingOptions options) { textBlock.getAtomicTextBlocks().forEach(atb -> drawAtomicTextBlock(document, atb, options)); } - public void drawAtomicTextBlock(PDDocument document, AtomicTextBlock atomicTextBlock, Options options) { + public void drawAtomicTextBlock(PDDocument document, AtomicTextBlock atomicTextBlock, DrawingOptions options) { drawRectangle2DList(document, atomicTextBlock.getPage().getNumber(), atomicTextBlock.getPositions().stream().toList(), options); @@ -62,7 +58,7 @@ public class PdfVisualisationUtility { @SneakyThrows - public void drawText(String string, PDDocument document, Point2D location, Integer pageNumber, Options options) { + public void drawText(String string, PDDocument document, Point2D location, Integer pageNumber, DrawingOptions options) { var pdPage = document.getPage(pageNumber - 1); var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true); @@ -80,14 +76,14 @@ public class PdfVisualisationUtility { @SneakyThrows - public void drawRectangle2DList(PDDocument document, int pageNumber, List rectCollection, Options options) { + public void drawRectangle2DList(PDDocument document, int pageNumber, List rectCollection, DrawingOptions options) { var pdPage = document.getPage(pageNumber - 1); drawRectangle2DList(document, rectCollection, options, pdPage); } - private void drawRectangle2DList(PDDocument document, List rectCollection, Options options, PDPage pdPage) throws IOException { + private void drawRectangle2DList(PDDocument document, List rectCollection, DrawingOptions options, PDPage pdPage) throws IOException { var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true); @@ -110,9 +106,9 @@ public class PdfVisualisationUtility { } - private Options buildStandardOptionsForNodes(DocumentTree.Entry entry) { + private DrawingOptions buildStandardOptionsForNodes(DocumentTree.Entry entry) { - return Options.builder().stroke(true).strokeColor(switch (entry.getType()) { + return DrawingOptions.builder().stroke(true).strokeColor(switch (entry.getType()) { case DOCUMENT -> Color.LIGHT_GRAY; case HEADER, FOOTER -> Color.GREEN; case PARAGRAPH -> Color.BLUE; @@ -125,7 +121,7 @@ public class PdfVisualisationUtility { } - private void drawBBoxAndLabelAndNumberOnPage(PDDocument document, DocumentTree.Entry entry, Options options) { + private void drawBBoxAndLabelAndNumberOnPage(PDDocument document, DocumentTree.Entry entry, DrawingOptions options) { Map rectanglesPerPage = entry.getNode().getBBox(); rectanglesPerPage.forEach((page, rectangle2D) -> { @@ -152,7 +148,7 @@ public class PdfVisualisationUtility { @SneakyThrows - public static void drawLine2DList(PDDocument pdDocument, int pageNumber, List line2DS, Options options) { + public static void drawLine2DList(PDDocument pdDocument, int pageNumber, List line2DS, DrawingOptions options) { var pdPage = pdDocument.getPage(pageNumber - 1); var contentStream = new PDPageContentStream(pdDocument, pdPage, PDPageContentStream.AppendMode.APPEND, true); @@ -176,21 +172,4 @@ public class PdfVisualisationUtility { contentStream.close(); } - - @Builder - @Getter - @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) - public static class Options { - - boolean fill; - boolean stroke; - @Builder.Default - Color strokeColor = Color.BLACK; - @Builder.Default - float strokeWidth = 1f; - @Builder.Default - Color fillColor = Color.BLACK; - - } - } diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/RectangleTransformations.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/RectangleTransformations.java index 1a49607..70e9460 100644 --- a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/RectangleTransformations.java +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/RectangleTransformations.java @@ -2,6 +2,7 @@ package com.knecon.fforesight.service.layoutparser.processor.utils; import static java.lang.String.format; +import java.awt.geom.Area; import java.awt.geom.Rectangle2D; import java.awt.geom.RectangularShape; import java.util.Collections; @@ -37,15 +38,28 @@ public class RectangleTransformations { } + public static double calculateIntersectedArea(Rectangle2D r1, Rectangle2D r2) { + + Area a1 = new Area(r1); + Area a2 = new Area(r2); + a1.intersect(a2); + Rectangle2D intersection = a1.getBounds2D(); + return intersection.getWidth() * intersection.getHeight(); + } + + public static Rectangle2D bBoxUnionAtomicTextBlock(List atomicTextBlocks) { return atomicTextBlocks.stream().flatMap(atomicTextBlock -> atomicTextBlock.getPositions().stream()).collect(new Rectangle2DBBoxCollector()); } + + public static Collector collectBBox() { return new Rectangle2DBBoxCollector(); } + public static PDRectangle toPDRectangleBBox(List rectangles) { Rectangle2D rectangle2D = RectangleTransformations.rectangleBBox(rectangles); @@ -70,6 +84,7 @@ public class RectangleTransformations { return format("%f,%f,%f,%f", rectangle2D.getX(), rectangle2D.getY(), rectangle2D.getWidth(), rectangle2D.getHeight()); } + public static Rectangle2D rectangleBBox(List rectangles) { return rectangles.stream().map(RectangleTransformations::toRectangle2D).collect(new Rectangle2DBBoxCollector()); @@ -84,6 +99,7 @@ public class RectangleTransformations { -redactionLogRectangle.getHeight()); } + public static Rectangle2D toRectangle2D(PDRectangle rectangle) { return new Rectangle2D.Double(rectangle.getLowerLeftX(), rectangle.getLowerLeftY(), rectangle.getWidth(), rectangle.getHeight()); diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/RectangularIntersectionFinder.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/RectangularIntersectionFinder.java new file mode 100644 index 0000000..3f47b40 --- /dev/null +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/RectangularIntersectionFinder.java @@ -0,0 +1,77 @@ +package com.knecon.fforesight.service.layoutparser.processor.utils; + +import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.Y_FIRST_POINT_COMPARATOR; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling; + +public class RectangularIntersectionFinder { + + public static List find(List horizontalRulingLines, List verticalRulingLines) { + + // Fix for 211.pdf + for (Ruling r : horizontalRulingLines) { + if (r.getX2() < r.getX1()) { + double a = r.getX2(); + r.x2 = (float) r.getX1(); + r.x1 = (float) a; + } + } + + List foundRectangles = new ArrayList<>(); + Map intersectionPoints = Ruling.findIntersections(horizontalRulingLines, verticalRulingLines); + List intersectionPointsList = new ArrayList<>(intersectionPoints.keySet()); + intersectionPointsList.sort(Y_FIRST_POINT_COMPARATOR); + + for (int i = 0; i < intersectionPointsList.size(); i++) { + Point2D topLeft = intersectionPointsList.get(i); + Ruling[] hv = intersectionPoints.get(topLeft); + + // CrossingPointsDirectlyBelow( topLeft ); + List xPoints = new ArrayList<>(); + // CrossingPointsDirectlyToTheRight( topLeft ); + List yPoints = new ArrayList<>(); + + for (Point2D p : intersectionPointsList.subList(i, intersectionPointsList.size())) { + if (p.getX() == topLeft.getX() && p.getY() > topLeft.getY()) { + xPoints.add(p); + } + if (p.getY() == topLeft.getY() && p.getX() > topLeft.getX()) { + yPoints.add(p); + } + } + outer: + for (Point2D xPoint : xPoints) { + // is there a vertical edge b/w topLeft and xPoint? + if (!hv[1].equals(intersectionPoints.get(xPoint)[1])) { + continue; + } + for (Point2D yPoint : yPoints) { + // is there a horizontal edge b/w topLeft and yPoint ? + if (!hv[0].equals(intersectionPoints.get(yPoint)[0])) { + continue; + } + Point2D btmRight = new Point2D.Float((float) yPoint.getX(), (float) xPoint.getY()); + if (intersectionPoints.containsKey(btmRight) + && intersectionPoints.get(btmRight)[0].equals(intersectionPoints.get(xPoint)[0]) + && intersectionPoints.get(btmRight)[1].equals(intersectionPoints.get(yPoint)[1])) { + foundRectangles.add(new Rectangle2D.Double(topLeft.getX(), topLeft.getY(), btmRight.getX() - topLeft.getX(), btmRight.getY() - topLeft.getY())); + break outer; + } + } + } + } + + // TODO create cells for vertical ruling lines with aligned endpoints at the top/bottom of a grid + // that aren't connected with an horizontal ruler? + // see: https://github.com/jazzido/tabula-extractor/issues/78#issuecomment-41481207 + + return foundRectangles; + } + +} diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/SpreadsheetFinder.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/SpreadsheetFinder.java new file mode 100644 index 0000000..660ef3f --- /dev/null +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/SpreadsheetFinder.java @@ -0,0 +1,172 @@ +package com.knecon.fforesight.service.layoutparser.processor.utils; + +import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.X_FIRST_POINT_COMPARATOR; +import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.Y_FIRST_POINT_COMPARATOR; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.knecon.fforesight.service.layoutparser.processor.model.table.Rectangle; + +public class SpreadsheetFinder { + + private static final int MAX_OUTER_POINT_TOLERANCE = 10; + private static final float AREA_TOLERANCE = 0.001f; + + + public static List findSpreadsheetsFromCells(List cells) { + // via: http://stackoverflow.com/questions/13746284/merging-multiple-adjacent-rectangles-into-one-polygon + List rectangles = new ArrayList<>(); + Set pointSet = new HashSet<>(); + Map edgesH = new HashMap<>(); + Map edgesV = new HashMap<>(); + + for (Rectangle cell : cells) { + for (Point2D pt : cell.getPoints()) { + if (pointSet.contains(pt)) { // shared vertex, remove it + pointSet.remove(pt); + } else { + pointSet.add(pt); + } + } + } + + // X first sort + List pointsSortX = new ArrayList<>(pointSet); + pointsSortX.sort(X_FIRST_POINT_COMPARATOR); + // Y first sort + List pointsSortY = new ArrayList<>(pointSet); + pointsSortY.sort(Y_FIRST_POINT_COMPARATOR); + + int i = 0; + while (i < pointSet.size()) { + float currY = (float) pointsSortY.get(i).getY(); + while (i < pointSet.size() && DoubleComparisons.feq(pointsSortY.get(i).getY(), currY)) { + edgesH.put(pointsSortY.get(i), pointsSortY.get(i + 1)); + edgesH.put(pointsSortY.get(i + 1), pointsSortY.get(i)); + i += 2; + } + } + + i = 0; + while (i < pointSet.size()) { + float currX = (float) pointsSortX.get(i).getX(); + while (i < pointSet.size() && DoubleComparisons.feq(pointsSortX.get(i).getX(), currX)) { + edgesV.put(pointsSortX.get(i), pointsSortX.get(i + 1)); + edgesV.put(pointsSortX.get(i + 1), pointsSortX.get(i)); + i += 2; + } + } + + // Get all the polygons + List> polygons = new ArrayList<>(); + Point2D nextVertex; + while (!edgesH.isEmpty()) { + ArrayList polygon = new ArrayList<>(); + Point2D first = edgesH.keySet() + .iterator().next(); + polygon.add(new PolygonVertex(first, Direction.HORIZONTAL)); + edgesH.remove(first); + + while (true) { + PolygonVertex curr = polygon.get(polygon.size() - 1); + PolygonVertex lastAddedVertex; + if (curr.direction == Direction.HORIZONTAL) { + nextVertex = edgesV.get(curr.point); + edgesV.remove(curr.point); + lastAddedVertex = new PolygonVertex(nextVertex, Direction.VERTICAL); + } else { + nextVertex = edgesH.get(curr.point); + edgesH.remove(curr.point); + lastAddedVertex = new PolygonVertex(nextVertex, Direction.HORIZONTAL); + } + polygon.add(lastAddedVertex); + + if (lastAddedVertex.equals(polygon.get(0))) { + // closed polygon + polygon.remove(polygon.size() - 1); + break; + } + } + + for (PolygonVertex vertex : polygon) { + edgesH.remove(vertex.point); + edgesV.remove(vertex.point); + } + polygons.add(polygon); + } + + // calculate grid-aligned minimum area rectangles for each found polygon + for (List poly : polygons) { + float top = Float.MAX_VALUE; + float left = Float.MAX_VALUE; + float bottom = Float.MIN_VALUE; + float right = Float.MIN_VALUE; + for (PolygonVertex pt : poly) { + top = (float) Math.min(top, pt.point.getY()); + left = (float) Math.min(left, pt.point.getX()); + bottom = (float) Math.max(bottom, pt.point.getY()); + right = (float) Math.max(right, pt.point.getX()); + } + + // do not add polygons with too many outer points as they are unlikely to be tables + if (poly.size() <= MAX_OUTER_POINT_TOLERANCE) { + rectangles.add(new Rectangle(top - AREA_TOLERANCE, left - AREA_TOLERANCE, right - left + 2 * AREA_TOLERANCE, bottom - top + 2 * AREA_TOLERANCE)); + } + } + return rectangles; + } + + + private enum Direction { + HORIZONTAL, + VERTICAL + } + + static class PolygonVertex { + + Point2D point; + Direction direction; + + + PolygonVertex(Point2D point, Direction direction) { + + this.direction = direction; + this.point = point; + } + + + @Override + public boolean equals(Object other) { + + if (this == other) { + return true; + } + if (!(other instanceof PolygonVertex)) { + return false; + } + return this.point.equals(((PolygonVertex) other).point); + } + + + @Override + public int hashCode() { + + return this.point.hashCode(); + } + + + @Override + public String toString() { + + return String.format("%s[point=%s,direction=%s]", this.getClass().getName(), this.point.toString(), this.direction.toString()); + } + + } + +} diff --git a/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/UnionFind.java b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/UnionFind.java new file mode 100644 index 0000000..d6af3fa --- /dev/null +++ b/layoutparser-service/layoutparser-service-processor/src/main/java/com/knecon/fforesight/service/layoutparser/processor/utils/UnionFind.java @@ -0,0 +1,44 @@ +package com.knecon.fforesight.service.layoutparser.processor.utils; + +import java.util.HashMap; +import java.util.Map; + +// simple implementation of a disjoint-set data structure +// https://en.wikipedia.org/wiki/Disjoint-set_data_structure +public class UnionFind { + + Map parents = new HashMap<>(); + Map numberOfObjects = new HashMap<>(); + + + public T find(T node) { + + if (!parents.containsKey(node)) { + parents.put(node, node); + numberOfObjects.put(node, 1); + } + if (!node.equals(parents.get(node))) { + parents.put(node, find(parents.get(node))); + } + return parents.get(node); + } + + + public void union(T node1, T node2) { + + T root1 = find(node1); + T root2 = find(node2); + + if (!root1.equals(root2)) { + if (numberOfObjects.getOrDefault(root1, 1) < numberOfObjects.getOrDefault(root2, 1)) { + parents.put(root1, root2); + numberOfObjects.put(root2, numberOfObjects.get(root2) + numberOfObjects.get(root1)); + } else { + parents.put(root2, root1); + numberOfObjects.put(root1, numberOfObjects.get(root1) + numberOfObjects.get(root2)); + } + } + } + +} + diff --git a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/BdrJsonBuildTest.java b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/BdrJsonBuildTest.java index 4b2358e..1f62c3f 100644 --- a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/BdrJsonBuildTest.java +++ b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/BdrJsonBuildTest.java @@ -29,6 +29,7 @@ import com.knecon.fforesight.service.layoutparser.processor.python_api.model.ima import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.TableServiceResponse; import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory; import com.knecon.fforesight.service.layoutparser.processor.services.mapper.TaasDocumentDataMapper; +import com.knecon.fforesight.service.layoutparser.processor.utils.DrawingOptions; import com.knecon.fforesight.service.layoutparser.server.utils.AbstractTest; import com.knecon.fforesight.service.layoutparser.server.utils.visualizations.PdfDraw; @@ -111,7 +112,7 @@ public class BdrJsonBuildTest extends AbstractTest { try (PDDocument pdDocument = Loader.loadPDF(file); var outputStream = new FileOutputStream(resultingFileName)) { PdfDraw.drawDocumentGraph(pdDocument, document); - PdfDraw.drawTextBlock(pdDocument, textBlock, PdfDraw.Options.builder().stroke(true).strokeWidth(0.1f).strokeColor(Color.YELLOW).build()); + PdfDraw.drawTextBlock(pdDocument, textBlock, DrawingOptions.builder().stroke(true).strokeWidth(0.1f).strokeColor(Color.YELLOW).build()); pdDocument.save(outputStream); } } diff --git a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/graph/DocumentGraphVisualizationTest.java b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/graph/DocumentGraphVisualizationTest.java index 5f150e2..1e98204 100644 --- a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/graph/DocumentGraphVisualizationTest.java +++ b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/graph/DocumentGraphVisualizationTest.java @@ -13,6 +13,7 @@ import org.springframework.core.io.ClassPathResource; import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document; import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock; +import com.knecon.fforesight.service.layoutparser.processor.utils.DrawingOptions; import com.knecon.fforesight.service.layoutparser.server.utils.BuildDocumentTest; import com.knecon.fforesight.service.layoutparser.server.utils.visualizations.PdfDraw; @@ -70,7 +71,7 @@ public class DocumentGraphVisualizationTest extends BuildDocumentTest { try (PDDocument pdDocument = Loader.loadPDF(fileResource.getFile())) { log.info("drawing document"); PdfDraw.drawDocumentGraph(pdDocument, documentGraph); - PdfDraw.drawTextBlock(pdDocument, textBlock, PdfDraw.Options.builder().stroke(true).strokeWidth(0.1f).strokeColor(Color.YELLOW).build()); + PdfDraw.drawTextBlock(pdDocument, textBlock, DrawingOptions.builder().stroke(true).strokeWidth(0.1f).strokeColor(Color.YELLOW).build()); log.info("saving document"); pdDocument.save(tmpFile); log.info("saved document"); diff --git a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/graph/ViewerDocumentTest.java b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/graph/ViewerDocumentTest.java index b437809..2db3906 100644 --- a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/graph/ViewerDocumentTest.java +++ b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/graph/ViewerDocumentTest.java @@ -25,7 +25,7 @@ public class ViewerDocumentTest extends BuildDocumentTest { @SneakyThrows public void testViewerDocument() { - String fileName = "files/S-Metolachlor_RAR_01_Volume_1_2018-09-06.pdf"; + String fileName = "files/SinglePages/T5 VV-640252-Page16.pdf"; String tmpFileName = "/tmp/" + Path.of(fileName).getFileName() + "_VIEWER.pdf"; var documentFile = new ClassPathResource(fileName).getFile(); diff --git a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/segmentation/PdfSegmentationServiceTest.java b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/segmentation/PdfSegmentationServiceTest.java index a671655..a9400a6 100644 --- a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/segmentation/PdfSegmentationServiceTest.java +++ b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/segmentation/PdfSegmentationServiceTest.java @@ -681,7 +681,7 @@ public class PdfSegmentationServiceTest extends AbstractTest { validateTableSize(document, 4); validateTable(document, 0, 3, 3, 0, 0); - validateTable(document, 1, 3, 5, 2, 0); + validateTable(document, 1, 3, 6, 2, 0); validateTable(document, 2, 3, 3, 1, 0); validateTable(document, 3, 3, 3, 0, 0); @@ -742,13 +742,12 @@ public class PdfSegmentationServiceTest extends AbstractTest { ClassificationDocument document = buildClassificationDocument(pdfFileResource.getFile()); - validateTableSize(document, 6); + validateTableSize(document, 5); validateTable(document, 0, 1, 1, 0, 0); validateTable(document, 1, 1, 1, 0, 0); validateTable(document, 2, 1, 1, 0, 0); validateTable(document, 3, 1, 1, 0, 0); validateTable(document, 4, 1, 1, 0, 0); - validateTable(document, 5, 1, 1, 0, 0); } diff --git a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/services/RulingCleaningServiceTest.java b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/services/RulingCleaningServiceTest.java index 92d31c1..fb3280a 100644 --- a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/services/RulingCleaningServiceTest.java +++ b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/services/RulingCleaningServiceTest.java @@ -1,13 +1,17 @@ package com.knecon.fforesight.service.layoutparser.server.services; +import java.awt.Color; +import java.awt.geom.Rectangle2D; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; @@ -26,6 +30,8 @@ import com.knecon.fforesight.service.layoutparser.processor.services.RulingClean import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory; import com.knecon.fforesight.service.layoutparser.processor.services.mapper.DocumentDataMapper; import com.knecon.fforesight.service.layoutparser.processor.services.mapper.PropertiesMapper; +import com.knecon.fforesight.service.layoutparser.processor.utils.DrawingOptions; +import com.knecon.fforesight.service.layoutparser.processor.utils.RectangularIntersectionFinder; import com.knecon.fforesight.service.layoutparser.server.utils.BuildDocumentTest; import com.knecon.fforesight.service.layoutparser.server.utils.visualizations.PdfDraw; @@ -34,19 +40,40 @@ import lombok.SneakyThrows; public class RulingCleaningServiceTest extends BuildDocumentTest { @Test -// @Disabled + @Disabled + @SneakyThrows + public void textRectanglesFromRulingsExtraction() { + + String fileName = "files/SinglePages/T5 VV-640252-Page16.pdf"; + String lineFileName = "/tmp/" + Path.of(fileName).getFileName().toString() + "_CELLS.pdf"; + List pageContents = PageContentExtractor.getSortedPageContents(fileName); + RulingCleaningService rulingCleaningService = new RulingCleaningService(); + List> rectanglesPerPage = new LinkedList<>(); + for (PageContents pageContent : pageContents) { + CleanRulings cleanRulings = rulingCleaningService.getCleanRulings(Collections.emptyList(), pageContent.getRulings()); + List rects = RectangularIntersectionFinder.find(cleanRulings.getHorizontal(), cleanRulings.getVertical()); + rectanglesPerPage.add(rects); + } + + PdfDraw.drawRectanglesPerPage(fileName, rectanglesPerPage, lineFileName, DrawingOptions.builder().stroke(true).strokeColor(Color.RED).build()); + } + + + @Test + @Disabled @SneakyThrows public void textRulingExtraction() { - String fileName = "files/211.pdf"; + String fileName = "files/SinglePages/19 Chlorothalonil RAR 08 Volume 3CA B 6b metabolites Oct 2017_Page35.pdf"; String lineFileName = "/tmp/" + Path.of(fileName).getFileName().toString() + "_LINES.pdf"; List pageContents = PageContentExtractor.getSortedPageContents(fileName); RulingCleaningService rulingCleaningService = new RulingCleaningService(); - PdfDraw.drawLinesPerPage(fileName, pageContents.stream().map(PageContents::getRulings).toList(), lineFileName); List cleanRulingsPerPage = new LinkedList<>(); for (PageContents pageContent : pageContents) { cleanRulingsPerPage.add(rulingCleaningService.getCleanRulings(Collections.emptyList(), pageContent.getRulings())); } + var cleanRulings = cleanRulingsPerPage.stream().map(CleanRulings::getVertical).collect(Collectors.toList()); + PdfDraw.drawLinesPerPage(fileName, cleanRulings, lineFileName); } diff --git a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/utils/visualizations/PdfDraw.java b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/utils/visualizations/PdfDraw.java index 5576017..4e3280f 100644 --- a/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/utils/visualizations/PdfDraw.java +++ b/layoutparser-service/layoutparser-service-server/src/test/java/com/knecon/fforesight/service/layoutparser/server/utils/visualizations/PdfDraw.java @@ -24,20 +24,31 @@ import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Pa import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock; import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock; import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling; +import com.knecon.fforesight.service.layoutparser.processor.utils.DrawingOptions; import com.knecon.fforesight.service.layoutparser.processor.utils.PdfVisualisationUtility; import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; import lombok.SneakyThrows; -import lombok.experimental.FieldDefaults; import lombok.experimental.UtilityClass; @UtilityClass public class PdfDraw { + public static void drawRectanglesPerPage(String filename, List> rectanglesPerPage, String tmpFileName, DrawingOptions options) throws IOException { + + ClassPathResource pdfResource = new ClassPathResource(filename); + try (PDDocument pdDocument = Loader.loadPDF(pdfResource.getFile()); var out = new FileOutputStream(tmpFileName)) { + for (int pageNumber = 1; pageNumber < pdDocument.getNumberOfPages() + 1; pageNumber++) { + PdfVisualisationUtility.drawRectangle2DList(pdDocument, + pageNumber, + rectanglesPerPage.get(pageNumber - 1), + options); + } + pdDocument.save(out); + } + + } + public static void drawRectanglesPerPage(String filename, List> rectanglesPerPage, String tmpFileName) throws IOException { ClassPathResource pdfResource = new ClassPathResource(filename); @@ -46,7 +57,7 @@ public class PdfDraw { PdfVisualisationUtility.drawRectangle2DList(pdDocument, pageNumber, rectanglesPerPage.get(pageNumber - 1), - PdfVisualisationUtility.Options.builder().stroke(true).build()); + DrawingOptions.builder().stroke(true).build()); } pdDocument.save(out); } @@ -62,13 +73,13 @@ public class PdfDraw { var rectanglesOnPage = rectanglesPerPage.get(pageNumber - 1); for (int lineNumber = 0; lineNumber < rectanglesOnPage.size(); lineNumber++) { var rectanglesInLine = rectanglesOnPage.get(lineNumber); - PdfVisualisationUtility.drawRectangle2DList(pdDocument, pageNumber, rectanglesInLine, PdfVisualisationUtility.Options.builder().stroke(true).build()); + PdfVisualisationUtility.drawRectangle2DList(pdDocument, pageNumber, rectanglesInLine, DrawingOptions.builder().stroke(true).build()); double y = Math.min(rectanglesInLine.get(0).getMinY(), rectanglesInLine.get(0).getMaxY()); PdfVisualisationUtility.drawText(String.format("%d", lineNumber), pdDocument, new Point2D.Double(rectanglesInLine.get(0).getX() - (5 + (5 * countNumberOfDigits(lineNumber))), y + 2), pageNumber, - PdfVisualisationUtility.Options.builder().stroke(true).build()); + DrawingOptions.builder().stroke(true).build()); } } pdDocument.save(out); @@ -99,20 +110,20 @@ public class PdfDraw { public static void drawNode(PDDocument document, DocumentTree.Entry entry) { - Options options = buildStandardOptionsForNodes(entry); + DrawingOptions options = buildStandardOptionsForNodes(entry); drawBBoxAndLabelAndNumberOnPage(document, entry, options); } - public static void drawTextBlock(PDDocument document, TextBlock textBlock, Options options) { + public static void drawTextBlock(PDDocument document, TextBlock textBlock, DrawingOptions options) { textBlock.getAtomicTextBlocks().forEach(atb -> drawAtomicTextBlock(document, atb, options)); } - public static void drawAtomicTextBlock(PDDocument document, AtomicTextBlock atomicTextBlock, Options options) { + public static void drawAtomicTextBlock(PDDocument document, AtomicTextBlock atomicTextBlock, DrawingOptions options) { drawRectangle2DList(document, atomicTextBlock.getPage().getNumber(), atomicTextBlock.getPositions().stream().toList(), options); @@ -120,7 +131,7 @@ public class PdfDraw { @SneakyThrows - private static void drawText(String string, PDDocument document, Point2D location, Integer pageNumber, Options options, boolean rotate) { + private static void drawText(String string, PDDocument document, Point2D location, Integer pageNumber, DrawingOptions options, boolean rotate) { var pdPage = document.getPage(pageNumber - 1); var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true); @@ -142,14 +153,14 @@ public class PdfDraw { @SneakyThrows - public static void drawRectangle2DList(PDDocument document, int pageNumber, List rectCollection, Options options) { + public static void drawRectangle2DList(PDDocument document, int pageNumber, List rectCollection, DrawingOptions options) { var pdPage = document.getPage(pageNumber - 1); drawRectangle2DList(document, rectCollection, options, pdPage); } - private static void drawRectangle2DList(PDDocument document, List rectCollection, Options options, PDPage pdPage) throws IOException { + private static void drawRectangle2DList(PDDocument document, List rectCollection, DrawingOptions options, PDPage pdPage) throws IOException { var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true); @@ -181,12 +192,12 @@ public class PdfDraw { // PdfVisualisationUtility.drawLine2DList(pdDocument, // pageNumber, // list.get(pageNumber - 1), -// PdfVisualisationUtility.Options.builder().stroke(true).build()); +// PdfVisualisationUtility.DrawingOptions.builder().stroke(true).build()); PdfVisualisationUtility.drawRectangle2DList(pdDocument, pageNumber, rectanglesPerPage.get(pageNumber - 1), - PdfVisualisationUtility.Options.builder().stroke(true).build()); - PdfVisualisationUtility.drawRectangle2DList(pdDocument, pageNumber, list.get(pageNumber - 1), PdfVisualisationUtility.Options.builder().stroke(true).build()); + DrawingOptions.builder().stroke(true).build()); + PdfVisualisationUtility.drawRectangle2DList(pdDocument, pageNumber, list.get(pageNumber - 1), DrawingOptions.builder().stroke(true).build()); } pdDocument.save(out); } @@ -202,35 +213,18 @@ public class PdfDraw { PdfVisualisationUtility.drawLine2DList(pdDocument, pageNumber, linesPerPage.get(pageNumber - 1), - PdfVisualisationUtility.Options.builder().strokeColor(Color.RED).stroke(true).build()); + DrawingOptions.builder().strokeColor(Color.RED).stroke(true).build()); } pdDocument.save(out); } } - @Builder - @AllArgsConstructor - @Getter - @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) - public static class Options { - - boolean stroke; - @Builder.Default - Color strokeColor = Color.BLACK; - @Builder.Default - float strokeWidth = 1f; - - boolean fill; - @Builder.Default - Color fillColor = Color.BLACK; - - } - private static Options buildStandardOptionsForNodes(DocumentTree.Entry entry) { + private static DrawingOptions buildStandardOptionsForNodes(DocumentTree.Entry entry) { - return Options.builder().stroke(true).strokeColor(switch (entry.getType()) { + return DrawingOptions.builder().stroke(true).strokeColor(switch (entry.getType()) { case DOCUMENT -> Color.LIGHT_GRAY; case HEADER, FOOTER -> Color.GREEN; case PARAGRAPH -> Color.BLUE; @@ -243,7 +237,7 @@ public class PdfDraw { } - private static void drawBBoxAndLabelAndNumberOnPage(PDDocument document, DocumentTree.Entry entry, Options options) { + private static void drawBBoxAndLabelAndNumberOnPage(PDDocument document, DocumentTree.Entry entry, DrawingOptions options) { Map rectanglesPerPage = entry.getNode().getBBox(); for (Page page : rectanglesPerPage.keySet()) { diff --git a/layoutparser-service/layoutparser-service-server/src/test/resources/files/SinglePages/DontMergeNonConsecutiveTables.pdf b/layoutparser-service/layoutparser-service-server/src/test/resources/files/SinglePages/DontMergeNonConsecutiveTables.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4e18c90d62a0502fc113932603e4bf6f05222b83 GIT binary patch literal 16012 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 BBTxVU literal 0 HcmV?d00001 diff --git a/layoutparser-service/layoutparser-service-server/src/test/resources/files/SinglePages/Meto_vol2_Page10.pdf b/layoutparser-service/layoutparser-service-server/src/test/resources/files/SinglePages/Meto_vol2_Page10.pdf new file mode 100644 index 0000000000000000000000000000000000000000..10ce129be20b724fd95f02cb722947409d16288b GIT binary patch literal 88319 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 )Vnd}7hZZX|-@zOr~uBhF2X`WPFNkn!$EKyO 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 c0}l>y2@3h2309C-04h*X2nuSO>QGVq16-I(8vp