Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
859dba2ecf | ||
|
|
1c5d755111 | ||
|
|
133e06460f | ||
|
|
da91fcff97 | ||
|
|
79795e408a | ||
|
|
b719db86ab | ||
|
|
797602e373 | ||
|
|
3d2f66cf10 | ||
|
|
e304a9f2d7 | ||
|
|
c05f67cf44 | ||
|
|
3a2ee903af | ||
|
|
072a8aa3da | ||
|
|
b5cfa7b63d | ||
|
|
5f5a6258c5 | ||
|
|
ac0e83725a | ||
|
|
5d33ad570e | ||
|
|
fd698a78fc | ||
|
|
c3edeb3c7d | ||
|
|
fc06dba2ce | ||
|
|
b6742c1e89 | ||
|
|
efb1a748af | ||
|
|
23985b14be | ||
|
|
48b7a22e2b | ||
|
|
546341ee75 | ||
|
|
0ed1481517 | ||
|
|
b2a47f66ae | ||
|
|
3835d03036 | ||
|
|
b867deb9f9 | ||
|
|
8648ed0952 | ||
|
|
53f786b539 | ||
|
|
40465e8778 | ||
|
|
a76b2ace3f | ||
|
|
aeaca2f278 | ||
|
|
f1dbcc24a2 | ||
|
|
fda25852d1 | ||
|
|
471fadbcca | ||
|
|
87001090d5 | ||
|
|
ea355429c2 | ||
|
|
6a65d7f9fc | ||
|
|
e935cc7b14 | ||
|
|
07733d0855 | ||
|
|
abb249e966 | ||
|
|
bcd1eb9afa | ||
|
|
60acbac53f | ||
|
|
a3decd292d | ||
|
|
b6f0a21886 | ||
|
|
d61cac8b4f | ||
|
|
ae46c5f1ca | ||
|
|
f0a70a5242 | ||
|
|
15ea385f4d | ||
|
|
08be18db2d | ||
|
|
64209255cb | ||
|
|
4761d2e1a2 | ||
|
|
1916e626df | ||
|
|
e4663ac8db | ||
|
|
6a691183dc | ||
|
|
3dd215288a | ||
|
|
6fb1a0bef3 | ||
|
|
4e7c3f584b | ||
|
|
84bdb4d1ed | ||
|
|
75ab4df592 | ||
|
|
8442e60055 | ||
|
|
0ef67fc07b | ||
|
|
ea02f31a84 | ||
|
|
58acbab85f | ||
|
|
d38d023485 | ||
|
|
c1afe9b11f | ||
|
|
bdcb9aeda4 | ||
|
|
6a86036a78 | ||
|
|
a358d7565e | ||
|
|
069a6c0b49 | ||
|
|
683f7f1fb8 | ||
|
|
7eab3a4088 | ||
|
|
970fc99ed1 | ||
|
|
48c54f63a0 | ||
|
|
20e4e5ddff | ||
|
|
b53930328a | ||
|
|
c947d552d2 | ||
|
|
6b1b5eab84 | ||
|
|
cc9816c8cb | ||
|
|
f256f9b30f | ||
|
|
6167e3fb57 | ||
|
|
a78fb0244a | ||
|
|
8099a00bb6 | ||
|
|
9bb0468b2b | ||
|
|
c4d9c5df02 | ||
|
|
976f408237 | ||
|
|
319268c53d | ||
|
|
014eba9fc3 | ||
|
|
9bd8419770 | ||
|
|
c13ff7fbf6 | ||
|
|
5d3826e9b9 | ||
|
|
0c3194276a | ||
|
|
e302d9784e | ||
|
|
f185b13f2b | ||
|
|
990c376ce6 | ||
|
|
bf6a0d770b | ||
|
|
f18bda1d4e | ||
|
|
0a11992361 | ||
|
|
456b8fe4a1 | ||
|
|
9778ece992 | ||
|
|
8bd0de6263 | ||
|
|
5c1708f97f | ||
|
|
a35d77be2e | ||
|
|
631160eb22 | ||
|
|
8e7e588d26 | ||
|
|
ac850c2626 | ||
|
|
1d765a6baa | ||
|
|
c55984aa67 | ||
|
|
27aa418029 | ||
|
|
c4edff4696 | ||
|
|
92fd1a72de | ||
|
|
0d3d25e7d7 | ||
|
|
956fbff872 | ||
|
|
2488009af1 | ||
|
|
16be2467fd | ||
|
|
f4cae8a7dc |
@@ -0,0 +1 @@
|
||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -42,3 +42,5 @@ gradlew.bat
|
||||
gradlew
|
||||
gradle.properties
|
||||
gradle/
|
||||
.DS_Store
|
||||
.DS_Store/
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
variables:
|
||||
# SONAR_PROJECT_KEY: 'fforesight_layout-parser_AYd5quv2mRkBOCG22hvF'
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
GIT_SUBMODULE_FORCE_HTTPS: 'true'
|
||||
include:
|
||||
- project: 'gitlab/gitlab'
|
||||
ref: 'main'
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[submodule "layoutparser-service/layoutparser-service-server/src/test/resources/files/basf"]
|
||||
path = layoutparser-service/layoutparser-service-server/src/test/resources/files/basf
|
||||
url = ssh://git@git.knecon.com:22222/fforesight/documents/basf.git
|
||||
update = merge
|
||||
[submodule "layoutparser-service/layoutparser-service-server/src/test/resources/files/syngenta"]
|
||||
path = layoutparser-service/layoutparser-service-server/src/test/resources/files/syngenta
|
||||
url = ssh://git@git.knecon.com:22222/fforesight/documents/syngenta.git
|
||||
update = merge
|
||||
+2
-1
@@ -2,5 +2,6 @@ package com.knecon.fforesight.service.layoutparser.internal.api.data.redaction;
|
||||
|
||||
public enum LayoutEngine {
|
||||
ALGORITHM,
|
||||
AI
|
||||
AI,
|
||||
OUTLINE
|
||||
}
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ import java.util.Locale;
|
||||
public enum NodeType implements Serializable {
|
||||
DOCUMENT,
|
||||
SECTION,
|
||||
SUPER_SECTION,
|
||||
HEADLINE,
|
||||
PARAGRAPH,
|
||||
TABLE,
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import lombok.NoArgsConstructor;
|
||||
public class SimplifiedSectionText {
|
||||
|
||||
@Schema(description = "The number of this Section. This is used to map the simplified section text back to the original Section.")
|
||||
private int sectionNumber;
|
||||
private String sectionNumber;
|
||||
@Schema(description = "The text in this Section.")
|
||||
private String text;
|
||||
|
||||
|
||||
+2
@@ -4,7 +4,9 @@ public enum LayoutParsingType {
|
||||
REDACT_MANAGER,
|
||||
REDACT_MANAGER_OLD,
|
||||
REDACT_MANAGER_PARAGRAPH_DEBUG,
|
||||
REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH,
|
||||
DOCUMINE,
|
||||
DOCUMINE_OLD,
|
||||
CLARIFYND,
|
||||
CLARIFYND_PARAGRAPH_DEBUG
|
||||
}
|
||||
|
||||
@@ -25,4 +25,5 @@ dependencies {
|
||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web:3.1.3")
|
||||
implementation("org.jgrapht:jgrapht-core:1.5.2")
|
||||
implementation("org.tinspin:tinspin-indexes:2.1.3")
|
||||
}
|
||||
|
||||
+151
-65
@@ -2,6 +2,7 @@ package com.knecon.fforesight.service.layoutparser.processor;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -27,7 +28,13 @@ import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBl
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.ImageType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.OutlineExtractorService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.OutlineObject;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.OutlineValidationService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.TOCEnrichmentService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.TableOfContents;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Cell;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
@@ -44,6 +51,8 @@ import com.knecon.fforesight.service.layoutparser.processor.services.RulingClean
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.SectionsBuilderService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.SimplifiedSectionTextService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.TableExtractionService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.TextRulingsClassifier;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.blockification.BlockificationPostprocessingService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.blockification.DocstrumBlockificationService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.blockification.DocuMineBlockificationService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.blockification.RedactManagerBlockificationService;
|
||||
@@ -51,11 +60,14 @@ import com.knecon.fforesight.service.layoutparser.processor.services.classificat
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.classification.DocuMineClassificationService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.classification.RedactManagerClassificationService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.graphics.Box;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.graphics.GraphicExtractorService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.mapper.DocumentDataMapper;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.mapper.TaasDocumentDataMapper;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.parsing.PDFLinesTextStripper;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.visualization.LayoutGridService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.MarkedContentUtils;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.PageInformation;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
@@ -85,11 +97,17 @@ public class LayoutParsingPipeline {
|
||||
TableExtractionService tableExtractionService;
|
||||
DocuMineBlockificationService docuMineBlockificationService;
|
||||
RedactManagerBlockificationService redactManagerBlockificationService;
|
||||
BlockificationPostprocessingService blockificationPostprocessingService;
|
||||
DocstrumBlockificationService docstrumBlockificationService;
|
||||
LayoutGridService layoutGridService;
|
||||
ObservationRegistry observationRegistry;
|
||||
VisualLayoutParsingAdapter visualLayoutParsingAdapter;
|
||||
ClarifyndClassificationService clarifyndClassificationService;
|
||||
GraphicExtractorService graphicExtractorService;
|
||||
OutlineExtractorService outlineExtractorService;
|
||||
OutlineValidationService outlineValidationService;
|
||||
TOCEnrichmentService tocEnrichmentService;
|
||||
LayoutparserSettings settings;
|
||||
|
||||
|
||||
public LayoutParsingFinishedEvent parseLayoutAndSaveFilesToStorage(LayoutParsingRequest layoutParsingRequest) throws IOException {
|
||||
@@ -98,37 +116,46 @@ public class LayoutParsingPipeline {
|
||||
log.info("Starting layout parsing for {}", layoutParsingRequest.identifier());
|
||||
|
||||
File originFile = layoutParsingStorageService.getOriginFile(layoutParsingRequest.originFileStorageId());
|
||||
File viewerDocumentFile = layoutParsingStorageService.getViewerDocFile(layoutParsingRequest.viewerDocumentStorageId()).orElse(originFile);
|
||||
// File viewerDocumentFile = layoutParsingStorageService.getViewerDocFile(layoutParsingRequest.viewerDocumentStorageId()).orElse(originFile);
|
||||
File viewerDocumentFile = originFile;
|
||||
|
||||
VisualLayoutParsingResponse visualLayoutParsingResponse = new VisualLayoutParsingResponse();
|
||||
if (layoutParsingRequest.visualLayoutParsingFileId().isPresent()) {
|
||||
visualLayoutParsingResponse = layoutParsingStorageService.getVisualLayoutParsingFile(layoutParsingRequest.visualLayoutParsingFileId().get());
|
||||
if (layoutParsingRequest.visualLayoutParsingFileId()
|
||||
.isPresent()) {
|
||||
visualLayoutParsingResponse = layoutParsingStorageService.getVisualLayoutParsingFile(layoutParsingRequest.visualLayoutParsingFileId()
|
||||
.get());
|
||||
}
|
||||
|
||||
ImageServiceResponse imageServiceResponse = new ImageServiceResponse();
|
||||
if (layoutParsingRequest.imagesFileStorageId().isPresent()) {
|
||||
imageServiceResponse = layoutParsingStorageService.getImagesFile(layoutParsingRequest.imagesFileStorageId().get());
|
||||
if (layoutParsingRequest.imagesFileStorageId()
|
||||
.isPresent()) {
|
||||
imageServiceResponse = layoutParsingStorageService.getImagesFile(layoutParsingRequest.imagesFileStorageId()
|
||||
.get());
|
||||
}
|
||||
|
||||
TableServiceResponse tableServiceResponse = new TableServiceResponse();
|
||||
if (layoutParsingRequest.tablesFileStorageId().isPresent()) {
|
||||
tableServiceResponse = layoutParsingStorageService.getTablesFile(layoutParsingRequest.tablesFileStorageId().get());
|
||||
if (layoutParsingRequest.tablesFileStorageId()
|
||||
.isPresent()) {
|
||||
tableServiceResponse = layoutParsingStorageService.getTablesFile(layoutParsingRequest.tablesFileStorageId()
|
||||
.get());
|
||||
}
|
||||
|
||||
ClassificationDocument classificationDocument = parseLayout(layoutParsingRequest.layoutParsingType(),
|
||||
originFile,
|
||||
imageServiceResponse,
|
||||
tableServiceResponse,
|
||||
visualLayoutParsingResponse,
|
||||
layoutParsingRequest.identifier());
|
||||
ClassificationDocument classificationDocument = parseLayout(settings.getLayoutParsingTypeOverride() == null //
|
||||
? layoutParsingRequest.layoutParsingType() : settings.getLayoutParsingTypeOverride(),
|
||||
originFile,
|
||||
imageServiceResponse,
|
||||
tableServiceResponse,
|
||||
visualLayoutParsingResponse,
|
||||
layoutParsingRequest.identifier());
|
||||
|
||||
log.info("Building document graph for {}", layoutParsingRequest.identifier());
|
||||
|
||||
Document documentGraph = observeBuildDocumentGraph(layoutParsingRequest.layoutParsingType(), classificationDocument);
|
||||
Document documentGraph = observeBuildDocumentGraph(settings.getLayoutParsingTypeOverride() == null //
|
||||
? layoutParsingRequest.layoutParsingType() : settings.getLayoutParsingTypeOverride(), classificationDocument);
|
||||
|
||||
log.info("Creating viewer document for {}", layoutParsingRequest.identifier());
|
||||
|
||||
layoutGridService.addLayoutGrid(viewerDocumentFile, documentGraph, viewerDocumentFile, false);
|
||||
layoutGridService.addLayoutGrid(viewerDocumentFile, documentGraph, viewerDocumentFile, false, layoutParsingRequest.visualLayoutParsingFileId().isPresent());
|
||||
|
||||
log.info("Storing resulting files for {}", layoutParsingRequest.identifier());
|
||||
|
||||
@@ -152,25 +179,25 @@ public class LayoutParsingPipeline {
|
||||
.numberOfPages(documentGraph.getNumberOfPages())
|
||||
.duration(System.currentTimeMillis() - start)
|
||||
.message(format("""
|
||||
Layout parsing has finished in %.02f s.
|
||||
identifiers: %s
|
||||
%s
|
||||
Files have been saved with Ids:
|
||||
Structure: %s
|
||||
Text: %s
|
||||
Positions: %s
|
||||
PageData: %s
|
||||
Simplified Text: %s
|
||||
Viewer Doc: %s""",
|
||||
((float) (System.currentTimeMillis() - start)) / 1000,
|
||||
layoutParsingRequest.identifier(),
|
||||
buildSemanticNodeCountMessage(documentGraph.getNumberOfPages(), documentGraph.buildSemanticNodeCounts()),
|
||||
layoutParsingRequest.structureFileStorageId(),
|
||||
layoutParsingRequest.textBlockFileStorageId(),
|
||||
layoutParsingRequest.positionBlockFileStorageId(),
|
||||
layoutParsingRequest.pageFileStorageId(),
|
||||
layoutParsingRequest.simplifiedTextStorageId(),
|
||||
layoutParsingRequest.viewerDocumentStorageId()))
|
||||
Layout parsing has finished in %.02f s.
|
||||
identifiers: %s
|
||||
%s
|
||||
Files have been saved with Ids:
|
||||
Structure: %s
|
||||
Text: %s
|
||||
Positions: %s
|
||||
PageData: %s
|
||||
Simplified Text: %s
|
||||
Viewer Doc: %s""",
|
||||
((float) (System.currentTimeMillis() - start)) / 1000,
|
||||
layoutParsingRequest.identifier(),
|
||||
buildSemanticNodeCountMessage(documentGraph.getNumberOfPages(), documentGraph.buildSemanticNodeCounts()),
|
||||
layoutParsingRequest.structureFileStorageId(),
|
||||
layoutParsingRequest.textBlockFileStorageId(),
|
||||
layoutParsingRequest.positionBlockFileStorageId(),
|
||||
layoutParsingRequest.pageFileStorageId(),
|
||||
layoutParsingRequest.simplifiedTextStorageId(),
|
||||
layoutParsingRequest.viewerDocumentStorageId()))
|
||||
.build();
|
||||
|
||||
}
|
||||
@@ -190,15 +217,15 @@ public class LayoutParsingPipeline {
|
||||
|
||||
private String buildSemanticNodeCountMessage(int numberOfPages, Map<NodeType, Long> semanticNodeCounts) {
|
||||
|
||||
return String.format("%d pages with %d sections, %d headlines, %d paragraphs, %d tables with %d cells, %d headers, and %d footers parsed",
|
||||
numberOfPages,
|
||||
semanticNodeCounts.get(NodeType.SECTION) == null ? 0 : semanticNodeCounts.get(NodeType.SECTION),
|
||||
semanticNodeCounts.get(NodeType.HEADLINE) == null ? 0 : semanticNodeCounts.get(NodeType.HEADLINE),
|
||||
semanticNodeCounts.get(NodeType.PARAGRAPH) == null ? 0 : semanticNodeCounts.get(NodeType.PARAGRAPH),
|
||||
semanticNodeCounts.get(NodeType.TABLE) == null ? 0 : semanticNodeCounts.get(NodeType.TABLE),
|
||||
semanticNodeCounts.get(NodeType.TABLE_CELL) == null ? 0 : semanticNodeCounts.get(NodeType.TABLE_CELL),
|
||||
semanticNodeCounts.get(NodeType.HEADER) == null ? 0 : semanticNodeCounts.get(NodeType.HEADER),
|
||||
semanticNodeCounts.get(NodeType.FOOTER) == null ? 0 : semanticNodeCounts.get(NodeType.FOOTER));
|
||||
return format("%d pages with %d sections, %d headlines, %d paragraphs, %d tables with %d cells, %d headers, and %d footers parsed",
|
||||
numberOfPages,
|
||||
semanticNodeCounts.get(NodeType.SECTION) == null ? 0 : semanticNodeCounts.get(NodeType.SECTION),
|
||||
semanticNodeCounts.get(NodeType.HEADLINE) == null ? 0 : semanticNodeCounts.get(NodeType.HEADLINE),
|
||||
semanticNodeCounts.get(NodeType.PARAGRAPH) == null ? 0 : semanticNodeCounts.get(NodeType.PARAGRAPH),
|
||||
semanticNodeCounts.get(NodeType.TABLE) == null ? 0 : semanticNodeCounts.get(NodeType.TABLE),
|
||||
semanticNodeCounts.get(NodeType.TABLE_CELL) == null ? 0 : semanticNodeCounts.get(NodeType.TABLE_CELL),
|
||||
semanticNodeCounts.get(NodeType.HEADER) == null ? 0 : semanticNodeCounts.get(NodeType.HEADER),
|
||||
semanticNodeCounts.get(NodeType.FOOTER) == null ? 0 : semanticNodeCounts.get(NodeType.FOOTER));
|
||||
}
|
||||
|
||||
|
||||
@@ -213,11 +240,23 @@ public class LayoutParsingPipeline {
|
||||
|
||||
PDDocument originDocument = openDocument(originFile);
|
||||
addNumberOfPagesToTrace(originDocument.getNumberOfPages(), Files.size(originFile.toPath()));
|
||||
|
||||
Map<Integer, List<TableCells>> pdfTableCells = cvTableParsingAdapter.buildCvParsedTablesPerPage(tableServiceResponse);
|
||||
Map<Integer, List<ClassifiedImage>> pdfImages = imageServiceResponseAdapter.buildClassifiedImagesPerPage(imageServiceResponse);
|
||||
Map<Integer, List<ClassifiedImage>> signatures = visualLayoutParsingAdapter.buildExtractedSignaturesPerPage(visualLayoutParsingResponse);
|
||||
ClassificationDocument classificationDocument = new ClassificationDocument();
|
||||
|
||||
if (settings.isDebug() || identifier.containsKey("debug")) {
|
||||
classificationDocument.getVisualizations().setActive(true);
|
||||
}
|
||||
|
||||
List<ClassificationPage> classificationPages = new ArrayList<>();
|
||||
OutlineObject lastProcessedOutlineObject = null;
|
||||
|
||||
// parsing the structure elements could be useful as well
|
||||
if(layoutParsingType != LayoutParsingType.REDACT_MANAGER_OLD && layoutParsingType != LayoutParsingType.DOCUMINE_OLD) {
|
||||
classificationDocument.setOutlineObjectTree(outlineExtractorService.getOutlineObjectTree(originDocument));
|
||||
}
|
||||
|
||||
long pageCount = originDocument.getNumberOfPages();
|
||||
|
||||
@@ -241,10 +280,12 @@ public class LayoutParsingPipeline {
|
||||
stripper.setStartPage(pageNumber);
|
||||
stripper.setEndPage(pageNumber);
|
||||
stripper.setPdpage(pdPage);
|
||||
if (layoutParsingType.equals(LayoutParsingType.DOCUMINE)) {
|
||||
if (layoutParsingType.equals(LayoutParsingType.DOCUMINE_OLD)) {
|
||||
stripper.setSortByPosition(true);
|
||||
}
|
||||
stripper.getText(originDocument);
|
||||
List<TextPositionSequence> words = stripper.getTextPositionSequences();
|
||||
classificationDocument.getVisualizations().addTextVisualizations(words, pageNumber);
|
||||
|
||||
PDRectangle pdr = pdPage.getMediaBox();
|
||||
|
||||
@@ -252,16 +293,42 @@ public class LayoutParsingPipeline {
|
||||
boolean isLandscape = pdr.getWidth() > pdr.getHeight() && (rotation == 0 || rotation == 180) || pdr.getHeight() > pdr.getWidth() && (rotation == 90 || rotation == 270);
|
||||
|
||||
PDRectangle cropbox = pdPage.getCropBox();
|
||||
CleanRulings cleanRulings = rulingCleaningService.getCleanRulings(pdfTableCells.get(pageNumber), stripper.getRulings());
|
||||
classificationDocument.getVisualizations().addRulingVisualization(stripper.getRulings(), pageNumber);
|
||||
CleanRulings cleanRulings = rulingCleaningService.deduplicateAndStraightenRulings(pdfTableCells.get(pageNumber), stripper.getRulings());
|
||||
|
||||
List<Cell> emptyTableCells = TableExtractionService.findCells(cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
PageInformation pageInformation = PageInformation.fromPDPage(pageNumber, pdPage);
|
||||
List<Cell> emptyTableCells = TableExtractionService.findCells(cleanRulings.getHorizontals(), cleanRulings.getVerticals(), pageInformation);
|
||||
classificationDocument.getVisualizations().addCellVisualizations(emptyTableCells, pageNumber);
|
||||
|
||||
TextRulingsClassifier.classifyUnderlinedAndStrikethroughText(words, cleanRulings);
|
||||
|
||||
List<Box> graphics = graphicExtractorService.extractPathElementGraphics(originDocument,
|
||||
pdPage,
|
||||
pageNumber,
|
||||
cleanRulings,
|
||||
stripper.getTextPositionSequences(),
|
||||
|
||||
false);
|
||||
|
||||
pdfImages.computeIfAbsent(pageNumber, x -> new ArrayList<>())
|
||||
.addAll(graphics.stream()
|
||||
.map(g -> new ClassifiedImage(new Rectangle2D.Double(g.x1, g.y1, g.width(), g.height()), ImageType.GRAPHIC, false, stripper.getPageNumber()))
|
||||
.toList());
|
||||
|
||||
ClassificationPage classificationPage = switch (layoutParsingType) {
|
||||
case REDACT_MANAGER_OLD ->
|
||||
redactManagerBlockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
case DOCUMINE -> docuMineBlockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
||||
case REDACT_MANAGER, REDACT_MANAGER_PARAGRAPH_DEBUG -> docstrumBlockificationService.blockify(stripper.getTextPositionSequences(), emptyTableCells, true);
|
||||
case CLARIFYND, CLARIFYND_PARAGRAPH_DEBUG -> docstrumBlockificationService.blockify(stripper.getTextPositionSequences(), emptyTableCells, false);
|
||||
redactManagerBlockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings, classificationDocument.getVisualizations());
|
||||
case DOCUMINE_OLD -> docuMineBlockificationService.blockify(words, cleanRulings);
|
||||
case DOCUMINE, REDACT_MANAGER, REDACT_MANAGER_PARAGRAPH_DEBUG, REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH -> docstrumBlockificationService.blockify(words,
|
||||
cleanRulings,
|
||||
true,
|
||||
classificationDocument.getVisualizations(),
|
||||
layoutParsingType);
|
||||
case CLARIFYND, CLARIFYND_PARAGRAPH_DEBUG -> docstrumBlockificationService.blockify(words,
|
||||
cleanRulings,
|
||||
false,
|
||||
classificationDocument.getVisualizations(),
|
||||
layoutParsingType);
|
||||
};
|
||||
|
||||
classificationPage.setCleanRulings(cleanRulings);
|
||||
@@ -271,17 +338,32 @@ public class LayoutParsingPipeline {
|
||||
classificationPage.setPageWidth(cropbox.getWidth());
|
||||
classificationPage.setPageHeight(cropbox.getHeight());
|
||||
|
||||
if(layoutParsingType != LayoutParsingType.REDACT_MANAGER_OLD && layoutParsingType != LayoutParsingType.DOCUMINE_OLD) {
|
||||
List<OutlineObject> outlineObjects = classificationDocument.getOutlineObjectTree().getOutlineObjectsPerPage().getOrDefault(pageNumber - 1, new ArrayList<>());
|
||||
|
||||
OutlineObject notFoundOutlineObject = null;
|
||||
if (lastProcessedOutlineObject != null && !lastProcessedOutlineObject.isFound()) {
|
||||
lastProcessedOutlineObject.setPoint(new Point2D.Float(0, cropbox.getHeight()));
|
||||
notFoundOutlineObject = lastProcessedOutlineObject;
|
||||
}
|
||||
if (!outlineObjects.isEmpty()) {
|
||||
classificationPage.setOutlineObjects(outlineObjects);
|
||||
lastProcessedOutlineObject = blockificationPostprocessingService.sanitizeOutlineBlocks(classificationPage, notFoundOutlineObject);
|
||||
}
|
||||
}
|
||||
|
||||
classificationDocument.getVisualizations().addMarkedContentVisualizations(stripper.getMarkedContents(), pageNumber);
|
||||
// MarkedContent needs to be converted at this point, otherwise it leads to GC Problems in Pdfbox.
|
||||
classificationPage.setMarkedContentBboxPerType(convertMarkedContents(stripper.getMarkedContents()));
|
||||
|
||||
// If images is ocr needs to be calculated before textBlocks are moved into tables, otherwise findOcr algorithm needs to be adopted.
|
||||
if (pdfImages != null && pdfImages.containsKey(pageNumber)) {
|
||||
if (pdfImages.containsKey(pageNumber)) {
|
||||
classificationPage.setImages(pdfImages.get(pageNumber));
|
||||
imageServiceResponseAdapter.findOcr(classificationPage);
|
||||
}
|
||||
|
||||
if (signatures.containsKey(pageNumber)) {
|
||||
if (classificationPage.getImages() == null || classificationPage.getImages().size() == 0) {
|
||||
if (classificationPage.getImages() == null || classificationPage.getImages().isEmpty()) {
|
||||
classificationPage.setImages(signatures.get(pageNumber));
|
||||
} else {
|
||||
classificationPage.getImages().addAll(signatures.get(pageNumber));
|
||||
@@ -290,12 +372,6 @@ public class LayoutParsingPipeline {
|
||||
|
||||
tableExtractionService.extractTables(emptyTableCells, classificationPage);
|
||||
|
||||
if (layoutParsingType == LayoutParsingType.REDACT_MANAGER) {
|
||||
docstrumBlockificationService.combineBlocks(classificationPage);
|
||||
} else if (layoutParsingType == LayoutParsingType.CLARIFYND) {
|
||||
docstrumBlockificationService.mergeIntersectingBlocks(classificationPage.getTextBlocks(), 0, 6.5f);
|
||||
}
|
||||
|
||||
buildPageStatistics(classificationPage);
|
||||
increaseDocumentStatistics(classificationPage, classificationDocument);
|
||||
|
||||
@@ -306,22 +382,32 @@ public class LayoutParsingPipeline {
|
||||
|
||||
log.info("Calculating BodyTextFrame for {}", identifier);
|
||||
bodyTextFrameService.setBodyTextFrames(classificationDocument, layoutParsingType);
|
||||
for (ClassificationPage page : classificationDocument.getPages()) {
|
||||
classificationDocument.getVisualizations().addCleanRulingVisualization(page.getCleanRulings(), page.getPageNumber());
|
||||
}
|
||||
log.info("Classify TextBlocks for {}", identifier);
|
||||
switch (layoutParsingType) {
|
||||
case REDACT_MANAGER, REDACT_MANAGER_PARAGRAPH_DEBUG, REDACT_MANAGER_OLD, CLARIFYND_PARAGRAPH_DEBUG ->
|
||||
redactManagerClassificationService.classifyDocument(classificationDocument);
|
||||
case DOCUMINE -> docuMineClassificationService.classifyDocument(classificationDocument);
|
||||
case REDACT_MANAGER, REDACT_MANAGER_PARAGRAPH_DEBUG, REDACT_MANAGER_OLD, CLARIFYND_PARAGRAPH_DEBUG, REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH -> redactManagerClassificationService.classifyDocument(
|
||||
classificationDocument);
|
||||
case DOCUMINE_OLD, DOCUMINE -> docuMineClassificationService.classifyDocument(classificationDocument);
|
||||
case CLARIFYND -> clarifyndClassificationService.classifyDocument(classificationDocument);
|
||||
}
|
||||
|
||||
List<TextPageBlock> headlines = classificationDocument.getPages()
|
||||
.stream()
|
||||
.flatMap(classificationPage -> classificationPage.getTextBlocks()
|
||||
.stream()
|
||||
.filter(tb -> tb instanceof TextPageBlock && tb.getClassification() != null && tb.getClassification().isHeadline())
|
||||
.map(tb -> (TextPageBlock) tb))
|
||||
.toList();
|
||||
TableOfContents tableOfContents = outlineValidationService.createToC(headlines);
|
||||
classificationDocument.setTableOfContents(tableOfContents);
|
||||
|
||||
log.info("Building Sections for {}", identifier);
|
||||
|
||||
switch (layoutParsingType) {
|
||||
case CLARIFYND_PARAGRAPH_DEBUG, REDACT_MANAGER_PARAGRAPH_DEBUG -> sectionsBuilderService.buildParagraphDebugSections(classificationDocument);
|
||||
default -> {
|
||||
sectionsBuilderService.buildSections(classificationDocument);
|
||||
sectionsBuilderService.addImagesToSections(classificationDocument);
|
||||
}
|
||||
default -> tocEnrichmentService.assignSectionBlocksAndImages(classificationDocument);
|
||||
}
|
||||
|
||||
return classificationDocument;
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties("layoutparser")
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class LayoutparserSettings {
|
||||
|
||||
boolean debug;
|
||||
LayoutParsingType layoutParsingTypeOverride;
|
||||
}
|
||||
+22
-12
@@ -7,14 +7,18 @@ import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Character;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Line;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Zone;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.service.LineBuilderService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.service.NearestNeighbourService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.service.ReadingOrderService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.service.SpacingService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.service.ZoneBuilderService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextDirection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -29,31 +33,37 @@ public class DocstrumSegmentationService {
|
||||
private final ReadingOrderService readingOrderService;
|
||||
|
||||
|
||||
public List<Zone> segmentPage(List<TextPositionSequence> textPositions, boolean xyOrder) {
|
||||
public List<Zone> segmentPage(List<TextPositionSequence> textPositions, boolean xyOrder, CleanRulings usedRulings, LayoutparsingVisualizations visualizations) {
|
||||
|
||||
List<Zone> zones = new ArrayList<>();
|
||||
zones.addAll(computeZones(textPositions, TextDirection.ZERO));
|
||||
zones.addAll(computeZones(textPositions, TextDirection.QUARTER_CIRCLE));
|
||||
zones.addAll(computeZones(textPositions, TextDirection.HALF_CIRCLE));
|
||||
zones.addAll(computeZones(textPositions, TextDirection.THREE_QUARTER_CIRCLE));
|
||||
zones.addAll(computeZones(textPositions, usedRulings, visualizations, TextDirection.ZERO));
|
||||
zones.addAll(computeZones(textPositions, usedRulings, visualizations, TextDirection.QUARTER_CIRCLE));
|
||||
zones.addAll(computeZones(textPositions, usedRulings, visualizations, TextDirection.HALF_CIRCLE));
|
||||
zones.addAll(computeZones(textPositions, usedRulings, visualizations, TextDirection.THREE_QUARTER_CIRCLE));
|
||||
|
||||
return readingOrderService.resolve(zones, xyOrder);
|
||||
}
|
||||
|
||||
|
||||
private List<Zone> computeZones(List<TextPositionSequence> textPositions, TextDirection direction) {
|
||||
private List<Zone> computeZones(List<TextPositionSequence> textPositions, CleanRulings rulings, LayoutparsingVisualizations visualizations, TextDirection direction) {
|
||||
|
||||
var positions = textPositions.stream().filter(t -> t.getDir() == direction).map(TextPositionSequence::getTextPositions).flatMap(List::stream).toList();
|
||||
List<RedTextPosition> positions = textPositions.stream()
|
||||
.filter(t -> t.getDir() == direction)
|
||||
.map(TextPositionSequence::getTextPositions)
|
||||
.flatMap(List::stream)
|
||||
.toList();
|
||||
|
||||
var characters = positions.stream().map(Character::new).collect(Collectors.toList());
|
||||
List<Character> characters = positions.stream()
|
||||
.map(Character::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
nearestNeighbourService.findNearestNeighbors(characters);
|
||||
|
||||
var characterSpacing = spacingService.computeCharacterSpacing(characters);
|
||||
var lineSpacing = Math.min(spacingService.computeLineSpacing(characters), 20);
|
||||
double characterSpacing = spacingService.computeCharacterSpacing(characters);
|
||||
double lineSpacing = Math.min(spacingService.computeLineSpacing(characters), 20);
|
||||
|
||||
var lines = lineBuilderService.buildLines(characters, characterSpacing, lineSpacing);
|
||||
return zoneBuilderService.buildZones(lines, characterSpacing, lineSpacing);
|
||||
List<Line> lines = lineBuilderService.buildLines(characters, characterSpacing, lineSpacing, rulings);
|
||||
return zoneBuilderService.buildZones(lines, characterSpacing, lineSpacing, rulings);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+203
-4
@@ -1,13 +1,27 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.docstrum.model;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public abstract class BoundingBox {
|
||||
|
||||
private Rectangle2D bBox;
|
||||
// Java coordinate system: (0, 0) is always upper left, x is increasing left to right and y is increasing from top to bottom.
|
||||
// should be used when determining reading order or other tasks which require coordinates in a harmonized system.
|
||||
protected Rectangle2D bBox; // I would not trust this coordinate when comparing rulings and text, due to the text positions being slightly off.
|
||||
|
||||
// PDF coordinate system: depends on page rotation, (0, 0) is lower left corner, x is increasing left to right and y from bottom to top.
|
||||
// This rotates completely in 90 degree steps with page rotation.
|
||||
// Needs to be used when writing to a PDF.
|
||||
// Also, these are definitely correct and should be used whenever possible.
|
||||
protected Rectangle2D bBoxInitialUserSpace;
|
||||
|
||||
protected static final float VERTICAL_COMPARISON_THRESHOLD = 0.4f;
|
||||
|
||||
|
||||
public double getX() {
|
||||
@@ -22,6 +36,42 @@ public abstract class BoundingBox {
|
||||
}
|
||||
|
||||
|
||||
public double getMinX() {
|
||||
|
||||
return bBox.getMinX();
|
||||
}
|
||||
|
||||
|
||||
public double getMinY() {
|
||||
|
||||
return bBox.getMinY();
|
||||
}
|
||||
|
||||
|
||||
public double getPdfMinX() {
|
||||
|
||||
return bBoxInitialUserSpace.getMinX();
|
||||
}
|
||||
|
||||
|
||||
public double getPdfMaxX() {
|
||||
|
||||
return bBoxInitialUserSpace.getMaxX();
|
||||
}
|
||||
|
||||
|
||||
public double getPdfMinY() {
|
||||
|
||||
return bBoxInitialUserSpace.getMinY();
|
||||
}
|
||||
|
||||
|
||||
public double getPdfMaxY() {
|
||||
|
||||
return bBoxInitialUserSpace.getMaxY();
|
||||
}
|
||||
|
||||
|
||||
public double getWidth() {
|
||||
|
||||
return bBox.getWidth();
|
||||
@@ -34,21 +84,170 @@ public abstract class BoundingBox {
|
||||
}
|
||||
|
||||
|
||||
public double getMaxX() {
|
||||
|
||||
return bBox.getMaxX();
|
||||
}
|
||||
|
||||
|
||||
public double getMaxY() {
|
||||
|
||||
return bBox.getMaxY();
|
||||
}
|
||||
|
||||
|
||||
public double getArea() {
|
||||
|
||||
return (bBox.getHeight() * bBox.getWidth());
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(Rectangle2D contained, double tolerance) {
|
||||
public boolean contains(BoundingBox contained) {
|
||||
|
||||
return bBox.getX() <= contained.getX() + tolerance && bBox.getY() <= contained.getY() + tolerance && bBox.getX() + bBox.getWidth() >= contained.getX() + contained.getWidth() - tolerance && bBox.getY() + bBox.getHeight() >= contained.getY() + contained.getHeight() - tolerance;
|
||||
return contains(contained, 0);
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(BoundingBox contained, double tolerance) {
|
||||
|
||||
return getPdfMinX() <= contained.getPdfMinX() + tolerance
|
||||
&& getPdfMinY() <= contained.getPdfMinY() + tolerance
|
||||
&& getPdfMaxX() >= contained.getPdfMaxX() - tolerance
|
||||
&& getPdfMaxY() >= contained.getPdfMaxY() - tolerance;
|
||||
}
|
||||
|
||||
|
||||
public boolean intersects(BoundingBox other) {
|
||||
|
||||
return this.intersectsX(other) && this.intersectsY(other);
|
||||
}
|
||||
|
||||
|
||||
public boolean intersects(BoundingBox other, float yThreshold, float xThreshold) {
|
||||
|
||||
return this.intersectsX(other, xThreshold) && this.intersectsY(other, yThreshold);
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsY(BoundingBox other) {
|
||||
|
||||
return this.getBBox().getMinY() <= other.getBBox().getMaxY() && this.getBBox().getMaxY() >= other.getBBox().getMinY();
|
||||
return this.getPdfMinY() <= other.getPdfMaxY() && this.getPdfMaxY() >= other.getPdfMinY();
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsYJava(BoundingBox other) {
|
||||
|
||||
return this.getY() <= other.getMaxY() && this.getMaxY() >= other.getY();
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsY(BoundingBox other, float threshold) {
|
||||
|
||||
return this.getPdfMinY() - threshold <= other.getPdfMaxY() && this.getPdfMaxY() + threshold >= other.getPdfMinY();
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsX(BoundingBox other) {
|
||||
|
||||
return this.getPdfMinX() <= other.getPdfMaxX() && this.getPdfMaxX() >= other.getPdfMinX();
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsXJava(BoundingBox other) {
|
||||
|
||||
return this.getX() <= other.getMaxX() && this.getMaxX() >= other.getMinX();
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsX(BoundingBox other, float threshold) {
|
||||
|
||||
return this.getPdfMinX() - threshold <= other.getMaxX() && this.getMaxX() + threshold >= other.getPdfMinX();
|
||||
}
|
||||
|
||||
|
||||
public void setToBBoxOfComponents(List<? extends BoundingBox> components) {
|
||||
|
||||
this.bBox = components.stream()
|
||||
.map(BoundingBox::getBBox)
|
||||
.collect(RectangleTransformations.collectBBox());
|
||||
this.bBoxInitialUserSpace = components.stream()
|
||||
.map(BoundingBox::getBBoxInitialUserSpace)
|
||||
.collect(RectangleTransformations.collectBBox());
|
||||
}
|
||||
|
||||
|
||||
public double verticalOverlap(BoundingBox other) {
|
||||
|
||||
return Math.max(0, Math.min(this.getPdfMaxY(), other.getPdfMaxY()) - Math.max(this.getPdfMinY(), other.getPdfMinY()));
|
||||
}
|
||||
|
||||
|
||||
public static final Comparator<BoundingBox> ILL_DEFINED_ORDER = (o1, o2) -> {
|
||||
|
||||
if (o1.equals(o2)) {
|
||||
return 0;
|
||||
}
|
||||
if (o1.verticalOverlap(o2) > VERTICAL_COMPARISON_THRESHOLD * ((o1.getHeight() + o2.getHeight()) / 2)) {
|
||||
return Double.compare(o1.getPdfMinX(), o2.getPdfMinX());
|
||||
} else {
|
||||
return Double.compare(o1.getPdfMaxY(), o2.getPdfMaxY());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
public double horizontalDistance(BoundingBox other) {
|
||||
|
||||
Rectangle2D left;
|
||||
Rectangle2D right;
|
||||
if (this.leftOf(other)) {
|
||||
left = this.getBBox();
|
||||
right = other.getBBox();
|
||||
} else {
|
||||
left = other.getBBox();
|
||||
right = this.getBBox();
|
||||
}
|
||||
|
||||
return Math.max(0, right.getMinX() - left.getMaxX());
|
||||
}
|
||||
|
||||
|
||||
public double verticalDistance(BoundingBox other) {
|
||||
|
||||
Rectangle2D bottom;
|
||||
Rectangle2D top;
|
||||
if (this.isAbove(other)) {
|
||||
top = this.getBBox();
|
||||
bottom = other.getBBox();
|
||||
} else {
|
||||
bottom = this.getBBox();
|
||||
top = other.getBBox();
|
||||
}
|
||||
|
||||
return Math.max(0, bottom.getMinY() - top.getMaxY());
|
||||
}
|
||||
|
||||
|
||||
public boolean rightOf(BoundingBox other) {
|
||||
|
||||
return this.intersectsYJava(other) && other.getMaxX() <= this.getMinX();
|
||||
}
|
||||
|
||||
|
||||
public boolean leftOf(BoundingBox other) {
|
||||
|
||||
return this.intersectsYJava(other) && other.getMinX() >= this.getMaxX();
|
||||
}
|
||||
|
||||
|
||||
public boolean isAbove(BoundingBox other) {
|
||||
|
||||
return this.intersectsXJava(other) && other.getMinY() >= this.getMaxY();
|
||||
}
|
||||
|
||||
|
||||
public boolean isBelow(BoundingBox other) {
|
||||
|
||||
return this.intersectsXJava(other) && this.getMinY() >= other.getMaxY();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -27,8 +27,8 @@ public class Character {
|
||||
|
||||
public Character(RedTextPosition chunk) {
|
||||
|
||||
this.x = chunk.getXDirAdj() + chunk.getWidthDirAdj() / 2;
|
||||
this.y = chunk.getYDirAdj() + chunk.getHeightDir() / 2;
|
||||
this.x = chunk.getBBoxDirAdj().getCenterX();
|
||||
this.y = chunk.getBBoxDirAdj().getCenterY();
|
||||
this.textPosition = chunk;
|
||||
}
|
||||
|
||||
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.docstrum.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/*
|
||||
WIP, mostly working, needs to be tested a bit more
|
||||
*/
|
||||
public class ColumnDetector {
|
||||
|
||||
public static final double MAX_VALUE_THRESHOLD = 0.5;
|
||||
final static int bins_num = 512;
|
||||
final static int globalStartIdx = 0; // ignore outer parts completely, we don't expect columns there
|
||||
final static int globalEndIdx = bins_num; // i chose 7, since thirds seems a likely split for columns, therefore divided by 6 would eliminate those.
|
||||
public static final double DERIVATIVE_ZERO_THRESHOLD = 1e-10;
|
||||
public static final double MINIMUM_THRESHOLD_FOR_COLUMNS = 0.05;
|
||||
public static final double NEAR_GLOBAL_THRESHOLD = 0.5;
|
||||
double minY;
|
||||
double maxY;
|
||||
double midY;
|
||||
double[] histogram;
|
||||
double min;
|
||||
double max;
|
||||
double resolution;
|
||||
double sum;
|
||||
int N;
|
||||
|
||||
|
||||
public ColumnDetector(double min, double max, double minY, double maxY) {
|
||||
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.minY = minY;
|
||||
this.maxY = maxY;
|
||||
this.midY = maxY - minY;
|
||||
this.resolution = (max - min) / bins_num;
|
||||
this.histogram = new double[bins_num];
|
||||
}
|
||||
|
||||
|
||||
public void add(BoundingBox zone) {
|
||||
|
||||
N++;
|
||||
double weight = computeWeight(zone);
|
||||
int start = (int) ((zone.getMinX() - min) / resolution);
|
||||
int end = (int) ((zone.getMaxX() - min) / resolution);
|
||||
for (int i = start; i < end; i++) {
|
||||
histogram[i] += weight;
|
||||
sum += histogram[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private double computeWeight(BoundingBox zone) {
|
||||
|
||||
double areaWeight = zone.getBBox().getHeight();
|
||||
|
||||
double relativeDistance = relativeDistanceToMiddle(zone.getBBox().getCenterY());
|
||||
|
||||
double distanceWeight;
|
||||
if (relativeDistance < 0.6) {
|
||||
distanceWeight = 1;
|
||||
} else if (relativeDistance < 0.8) {
|
||||
distanceWeight = 0.8;
|
||||
} else {
|
||||
distanceWeight = 0.1;
|
||||
}
|
||||
|
||||
return areaWeight * distanceWeight;
|
||||
}
|
||||
|
||||
|
||||
private double relativeDistanceToMiddle(double y) {
|
||||
|
||||
double range = (maxY - minY) / 2;
|
||||
double mid = minY + range;
|
||||
|
||||
return Math.abs(y - mid) / range;
|
||||
}
|
||||
|
||||
|
||||
public double[] computeDerivative() {
|
||||
|
||||
int length = histogram.length;
|
||||
double[] derivative = new double[length];
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (i == 0) {
|
||||
derivative[i] = (histogram[i + 1] - histogram[i]) / resolution;
|
||||
} else if (i == length - 1) {
|
||||
derivative[i] = (histogram[i] - histogram[i - 1]) / resolution;
|
||||
} else {
|
||||
derivative[i] = (histogram[i + 1] - histogram[i - 1]) / (2 * resolution);
|
||||
}
|
||||
}
|
||||
|
||||
return derivative;
|
||||
}
|
||||
|
||||
|
||||
public double calcMean(double[] arr, int start, int end) {
|
||||
|
||||
if (start == end) {
|
||||
return 0;
|
||||
}
|
||||
double sum = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
sum += arr[i];
|
||||
}
|
||||
return sum / (end - start);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Find columns, by finding all local maxima/minima of the derivative. Filtering them for the ones with the biggest values.
|
||||
For each found minima, we will step to the right until we hit a 0 in the derivative, this indicates a minimum in the main histogram. If this minimum is below a threshold, it is deemed a column divider.
|
||||
Same goes for maxima, but stepping to the left now, since minima in the function will always be to the left of a maximum in its derivative.
|
||||
*/
|
||||
public List<Double> determineColumnsWithDerivative(double[] derivative) {
|
||||
|
||||
assert derivative.length == histogram.length;
|
||||
|
||||
Set<Integer> columnIndices = new HashSet<>();
|
||||
double mean = calcMean(histogram, 0, histogram.length);
|
||||
double maxDvValue = calcMax(derivative);
|
||||
double minDvValue = calcMin(derivative);
|
||||
|
||||
if (maxDvValue - minDvValue < mean * MAX_VALUE_THRESHOLD) {
|
||||
Collections.emptyList();
|
||||
}
|
||||
|
||||
Extrema derivativeExtrema = calculateNearGlobalExtrema(derivative, maxDvValue, minDvValue);
|
||||
|
||||
List<Integer> columnsRightOfMinima = findZerosToTheRightOfMinima(derivative, derivativeExtrema.minima(), mean);
|
||||
columnIndices.addAll(columnsRightOfMinima);
|
||||
|
||||
List<Integer> columnsLeftOfMaxima = findZerosToTheLeftOfMaxima(derivative, derivativeExtrema.maxima(), mean);
|
||||
columnIndices.addAll(columnsLeftOfMaxima);
|
||||
|
||||
return columnIndices.stream()
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.map(this::calculateXCoordinateFromIdx)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
private List<Integer> findZerosToTheLeftOfMaxima(double[] derivative, List<Integer> derivativeMaxima, double mean) {
|
||||
|
||||
List<Integer> columnsLeftOfMaxima = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < derivativeMaxima.size(); i++) {
|
||||
List<Integer> consecutiveZeroes = new LinkedList<>();
|
||||
boolean maximumFound = false;
|
||||
int maximaIdx = derivativeMaxima.get(i) - 1; // the highest derivative will always be at least one step away from the lowest value.
|
||||
int endIdx = (int) Math.max(globalStartIdx,
|
||||
Math.min(maximaIdx - 1,
|
||||
maximaIdx - 0.1 * bins_num)); // search through 10% of array to the right, but at least one step and at most to the left edge;
|
||||
|
||||
for (int j = maximaIdx; j >= endIdx; j--) {
|
||||
if (derivative[j] < DERIVATIVE_ZERO_THRESHOLD) {
|
||||
maximumFound = true;
|
||||
consecutiveZeroes.add(j);
|
||||
} else if (maximumFound) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (maximumFound) {
|
||||
int midIdx = consecutiveZeroes.size() / 2;
|
||||
int middleMinimumIdx = consecutiveZeroes.get(midIdx);
|
||||
if (histogram[middleMinimumIdx] < mean * MINIMUM_THRESHOLD_FOR_COLUMNS) {
|
||||
columnsLeftOfMaxima.add(middleMinimumIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
return columnsLeftOfMaxima;
|
||||
}
|
||||
|
||||
|
||||
private List<Integer> findZerosToTheRightOfMinima(double[] derivative, List<Integer> derivativeMinima, double mean) {
|
||||
|
||||
List<Integer> columnIndixes = new LinkedList<>();
|
||||
for (int i = 0; i < derivativeMinima.size(); i++) {
|
||||
List<Integer> consecutiveZeroes = new LinkedList<>();
|
||||
boolean minimumFound = false;
|
||||
int minimaIdx = derivativeMinima.get(i) + 1; // the highest derivative will always be at least one step earlier than the lowest value.
|
||||
int endIdx = (int) Math.min(globalEndIdx,
|
||||
Math.max(minimaIdx + 1,
|
||||
minimaIdx + 0.1 * bins_num)); // search through 10% of array to the right, but at least one step and at most to the right edge;
|
||||
|
||||
for (int j = minimaIdx; j < endIdx; j++) {
|
||||
if (derivative[j] < DERIVATIVE_ZERO_THRESHOLD) {
|
||||
minimumFound = true;
|
||||
consecutiveZeroes.add(j);
|
||||
} else if (minimumFound) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (minimumFound) {
|
||||
int midIdx = consecutiveZeroes.size() / 2;
|
||||
int middleMinimumIdx = consecutiveZeroes.get(midIdx);
|
||||
if (histogram[middleMinimumIdx] < mean * MINIMUM_THRESHOLD_FOR_COLUMNS) {
|
||||
columnIndixes.add(middleMinimumIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
return columnIndixes;
|
||||
}
|
||||
|
||||
|
||||
private double calcMax(double[] array) {
|
||||
|
||||
double max = Double.NEGATIVE_INFINITY;
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
if (array[i] > max) {
|
||||
max = array[i];
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
|
||||
private double calcMin(double[] array) {
|
||||
|
||||
double min = Double.POSITIVE_INFINITY;
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
if (array[i] < min) {
|
||||
min = array[i];
|
||||
}
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
|
||||
private Extrema calculateNearGlobalExtrema(double[] derivative, double maxDvValue, double minDvValue) {
|
||||
|
||||
List<Integer> nearGlobalDvMaximaIdx = new LinkedList<>();
|
||||
List<Integer> nearGlobalDvMinimaIdx = new LinkedList<>();
|
||||
for (int i = globalStartIdx; i < globalEndIdx; i++) {
|
||||
if (derivative[i] <= minDvValue * NEAR_GLOBAL_THRESHOLD) {
|
||||
nearGlobalDvMinimaIdx.add(i);
|
||||
}
|
||||
if (derivative[i] >= maxDvValue * NEAR_GLOBAL_THRESHOLD) {
|
||||
nearGlobalDvMaximaIdx.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
nearGlobalDvMinimaIdx = removeConsecutive(nearGlobalDvMinimaIdx);
|
||||
nearGlobalDvMaximaIdx = removeConsecutive(nearGlobalDvMaximaIdx);
|
||||
|
||||
return new Extrema(nearGlobalDvMaximaIdx, nearGlobalDvMinimaIdx);
|
||||
}
|
||||
|
||||
|
||||
private record Extrema(List<Integer> maxima, List<Integer> minima) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private Double calculateXCoordinateFromIdx(int globalMinIdx) {
|
||||
|
||||
return min + ((globalMinIdx + 1) * resolution);
|
||||
}
|
||||
|
||||
|
||||
public static List<Integer> removeConsecutive(List<Integer> numbers) {
|
||||
|
||||
List<Integer> result = new ArrayList<>();
|
||||
if (numbers == null || numbers.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.add(numbers.get(0)); // Add the first number
|
||||
|
||||
for (int i = 1; i < numbers.size(); i++) {
|
||||
if (numbers.get(i) != numbers.get(i - 1) + 1) {
|
||||
result.add(numbers.get(i)); // Add non-consecutive numbers
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public void kernelSmooth(double[] kernel) {
|
||||
|
||||
double[] newFrequencies = new double[histogram.length];
|
||||
int shift = (kernel.length - 1) / 2;
|
||||
for (int i = 0; i < kernel.length; i++) {
|
||||
int jStart = Math.max(0, i - shift);
|
||||
int jEnd = Math.min(histogram.length, histogram.length + i - shift);
|
||||
for (int j = jStart; j < jEnd; j++) {
|
||||
newFrequencies[j - i + shift] += kernel[i] * histogram[j];
|
||||
}
|
||||
}
|
||||
histogram = newFrequencies;
|
||||
}
|
||||
|
||||
|
||||
public double[] createGaussianKernel(int length, double stdDeviation) {
|
||||
|
||||
int r = length / 2;
|
||||
|
||||
int size = 2 * r + 1;
|
||||
double[] kernel = new double[size];
|
||||
double sum = 0;
|
||||
double b = 2 * (stdDeviation) * (stdDeviation);
|
||||
double a = 1 / Math.sqrt(Math.PI * b);
|
||||
for (int i = 0; i < size; i++) {
|
||||
kernel[i] = a * Math.exp(-(i - r) * (i - r) / b);
|
||||
sum += kernel[i];
|
||||
}
|
||||
for (int i = 0; i < size; i++) {
|
||||
kernel[i] /= sum;
|
||||
}
|
||||
return kernel;
|
||||
}
|
||||
|
||||
}
|
||||
+9
-19
@@ -1,10 +1,10 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.docstrum.model;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.utils.FastAtan2;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
|
||||
import lombok.Data;
|
||||
@@ -72,7 +72,7 @@ public class Line extends BoundingBox {
|
||||
|
||||
public double getAngle() {
|
||||
|
||||
return Math.atan2(y1 - y0, x1 - x0);
|
||||
return FastAtan2.fastAtan2(y1 - y0, x1 - x0);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,9 @@ public class Line extends BoundingBox {
|
||||
|
||||
private double computeHeight() {
|
||||
|
||||
return characters.stream().map(Character::getHeight).reduce(0d, Double::sum) / characters.size();
|
||||
return characters.stream()
|
||||
.map(Character::getHeight)
|
||||
.reduce(0d, Double::sum) / characters.size();
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +118,7 @@ public class Line extends BoundingBox {
|
||||
|
||||
double ym = (y0 + y1) / 2;
|
||||
double yn = (other.y0 + other.y1) / 2;
|
||||
return Math.abs(ym - yn) / Math.sqrt(1);
|
||||
return Math.abs(ym - yn);
|
||||
}
|
||||
|
||||
|
||||
@@ -141,21 +143,9 @@ public class Line extends BoundingBox {
|
||||
|
||||
private void buildBBox() {
|
||||
|
||||
double minX = Double.POSITIVE_INFINITY;
|
||||
double minY = Double.POSITIVE_INFINITY;
|
||||
double maxX = Double.NEGATIVE_INFINITY;
|
||||
double maxY = Double.NEGATIVE_INFINITY;
|
||||
|
||||
for (Character character : characters) {
|
||||
|
||||
minX = Math.min(minX, character.getTextPosition().getXDirAdj());
|
||||
minY = Math.min(minY, character.getTextPosition().getYDirAdj());
|
||||
maxX = Math.max(maxX, character.getTextPosition().getXDirAdj() + character.getTextPosition().getWidthDirAdj());
|
||||
maxY = Math.max(maxY, character.getTextPosition().getYDirAdj() + character.getTextPosition().getHeightDir());
|
||||
|
||||
}
|
||||
|
||||
this.setBBox(new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY));
|
||||
this.setToBBoxOfComponents(characters.stream()
|
||||
.map(Character::getTextPosition)
|
||||
.toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
+4
-23
@@ -1,9 +1,10 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.docstrum.model;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@@ -15,29 +16,9 @@ public class Zone extends BoundingBox {
|
||||
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||
public Zone(List<Line> lines) {
|
||||
|
||||
lines.sort(Comparator.comparingDouble(Line::getY));
|
||||
lines.sort(Comparator.comparingDouble(Line::getY0));
|
||||
this.lines = lines;
|
||||
buildBBox();
|
||||
}
|
||||
|
||||
|
||||
public void buildBBox() {
|
||||
|
||||
double minX = Double.POSITIVE_INFINITY;
|
||||
double minY = Double.POSITIVE_INFINITY;
|
||||
double maxX = Double.NEGATIVE_INFINITY;
|
||||
double maxY = Double.NEGATIVE_INFINITY;
|
||||
|
||||
for (Line line : lines) {
|
||||
|
||||
minX = Math.min(minX, line.getX());
|
||||
minY = Math.min(minY, line.getY());
|
||||
maxX = Math.max(maxX, line.getX() + line.getWidth());
|
||||
maxY = Math.max(maxY, line.getY() + line.getHeight());
|
||||
|
||||
}
|
||||
|
||||
this.setBBox(new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY));
|
||||
setToBBoxOfComponents(lines);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+26
-21
@@ -1,6 +1,5 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.docstrum.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -11,43 +10,49 @@ import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Angle
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Character;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Line;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.UnionFind;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
|
||||
@Service
|
||||
public class LineBuilderService {
|
||||
|
||||
private static final double CHARACTER_SPACING_DISTANCE_MULTIPLIER = 3.5;
|
||||
private static final double MAX_VERTICAL_CHARACTER_DISTANCE = 0.67;
|
||||
private static final double LINE_SPACING_THRESHOLD_MULTIPLIER = 0.67;
|
||||
private static final double ANGLE_TOLERANCE = Math.PI / 6;
|
||||
|
||||
|
||||
public List<Line> buildLines(List<Character> characters, double characterSpacing, double lineSpacing) {
|
||||
public List<Line> buildLines(List<Character> characters, double characterSpacing, double lineSpacing, CleanRulings rulings) {
|
||||
|
||||
double maxHorizontalDistance = characterSpacing * CHARACTER_SPACING_DISTANCE_MULTIPLIER;
|
||||
double maxVerticalDistance = lineSpacing * MAX_VERTICAL_CHARACTER_DISTANCE;
|
||||
double maxVerticalDistance = lineSpacing * LINE_SPACING_THRESHOLD_MULTIPLIER;
|
||||
|
||||
UnionFind<Character> unionFind = new UnionFind<>(new HashSet<>(characters));
|
||||
|
||||
AngleFilter filter = new AngleFilter(-ANGLE_TOLERANCE, ANGLE_TOLERANCE);
|
||||
AngleFilter angleFilter = new AngleFilter(-ANGLE_TOLERANCE, ANGLE_TOLERANCE);
|
||||
|
||||
characters.forEach(character -> {
|
||||
character.getNeighbors().forEach(neighbor -> {
|
||||
double x = neighbor.getHorizontalDistance() / maxHorizontalDistance;
|
||||
double y = neighbor.getVerticalDistance() / maxVerticalDistance;
|
||||
if (character.getTextPosition().getDir() == neighbor.getCharacter().getTextPosition().getDir() && filter.matches(neighbor) && Math.pow(x, 2) + Math.pow(y,
|
||||
2) <= 1) {
|
||||
unionFind.union(character, neighbor.getCharacter());
|
||||
}
|
||||
});
|
||||
character.getNeighbors()
|
||||
.forEach(neighbor -> {
|
||||
double normalizedHorizontalDistance = neighbor.getHorizontalDistance() / maxHorizontalDistance;
|
||||
double normalizedVerticalDistance = neighbor.getVerticalDistance() / maxVerticalDistance;
|
||||
|
||||
if (character.getTextPosition().getDir() != neighbor.getCharacter().getTextPosition().getDir() //
|
||||
|| !angleFilter.matches(neighbor) //
|
||||
|| Math.pow(normalizedHorizontalDistance, 2) + Math.pow(normalizedVerticalDistance, 2) > 1 //
|
||||
|| rulings.lineBetween(character.getTextPosition(), neighbor.getCharacter().getTextPosition())) {
|
||||
return;
|
||||
}
|
||||
|
||||
unionFind.union(character, neighbor.getCharacter());
|
||||
});
|
||||
});
|
||||
|
||||
List<Line> lines = new ArrayList<>();
|
||||
unionFind.getGroups().forEach(group -> {
|
||||
List<Character> lineCharacters = new ArrayList<>(group);
|
||||
lineCharacters.sort(Comparator.comparingDouble(Character::getX));
|
||||
lines.add(new Line(lineCharacters, characterSpacing));
|
||||
});
|
||||
|
||||
return lines;
|
||||
return unionFind.getGroups()
|
||||
.stream()
|
||||
.map(lineCharacters -> lineCharacters.stream()
|
||||
.sorted(Comparator.comparingDouble(Character::getX))
|
||||
.toList())
|
||||
.map(lineCharacters -> new Line(lineCharacters, characterSpacing))
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-103
@@ -1,10 +1,10 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.docstrum.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.tinspin.index.Index;
|
||||
import org.tinspin.index.kdtree.KDTree;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Character;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Neighbor;
|
||||
@@ -13,115 +13,23 @@ import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Neigh
|
||||
public class NearestNeighbourService {
|
||||
|
||||
private static final int NUMBER_OF_NEIGHBOURS = 8;
|
||||
private static final double STEP = 16.0;
|
||||
|
||||
|
||||
public void findNearestNeighbors(List<Character> characters) {
|
||||
KDTree<Character> kdTree = KDTree.create(2);
|
||||
characters.forEach(c -> kdTree.insert(new double[]{c.getX(), c.getY()}, c));
|
||||
|
||||
if (characters.isEmpty() || characters.size() == 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
characters.sort(Comparator.comparingDouble(Character::getX));
|
||||
|
||||
int maxNeighborCount = NUMBER_OF_NEIGHBOURS;
|
||||
if (characters.size() <= NUMBER_OF_NEIGHBOURS) {
|
||||
maxNeighborCount = characters.size() - 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < characters.size(); i++) {
|
||||
|
||||
Neighbor[] candidates = new Neighbor[maxNeighborCount + 1];
|
||||
int neighborInsertionIndex = 0;
|
||||
int neighborCount = 0;
|
||||
int start = i;
|
||||
int end = i + 1;
|
||||
|
||||
double distance = Double.POSITIVE_INFINITY;
|
||||
|
||||
for (double searchDistance = 0; searchDistance < distance; ) {
|
||||
|
||||
searchDistance += STEP;
|
||||
boolean newCandidatesFound = false;
|
||||
|
||||
while (start > 0 && characters.get(i).getX() - characters.get(start - 1).getX() < searchDistance) {
|
||||
start--;
|
||||
candidates[neighborInsertionIndex] = new Neighbor(characters.get(start), characters.get(i));
|
||||
neighborCount++;
|
||||
if (neighborCount > maxNeighborCount) {
|
||||
neighborInsertionIndex = clearMostDistant(candidates);
|
||||
neighborCount--;
|
||||
} else {
|
||||
neighborInsertionIndex++;
|
||||
}
|
||||
newCandidatesFound = true;
|
||||
}
|
||||
|
||||
while (end < characters.size() && characters.get(end).getX() - characters.get(i).getX() < searchDistance) {
|
||||
candidates[neighborInsertionIndex] = new Neighbor(characters.get(end), characters.get(i));
|
||||
neighborCount++;
|
||||
if (neighborCount > maxNeighborCount) {
|
||||
neighborInsertionIndex = clearMostDistant(candidates);
|
||||
neighborCount--;
|
||||
} else {
|
||||
neighborInsertionIndex++;
|
||||
}
|
||||
end++;
|
||||
newCandidatesFound = true;
|
||||
}
|
||||
|
||||
if (newCandidatesFound && neighborCount >= maxNeighborCount) {
|
||||
distance = maxDistance(candidates);
|
||||
}
|
||||
for(Character c : characters) {
|
||||
Index.PointIteratorKnn<Character> iterator = kdTree.queryKnn(new double[]{c.getX(), c.getY()}, NUMBER_OF_NEIGHBOURS + 1);
|
||||
// skip the first as this is identity
|
||||
if(iterator.hasNext()) {
|
||||
iterator.next();
|
||||
}
|
||||
if (neighborCount < maxNeighborCount) {
|
||||
clearMostDistant(candidates);
|
||||
}
|
||||
|
||||
List<Neighbor> candidatesList = new ArrayList<>(maxNeighborCount);
|
||||
for (Neighbor candidate : candidates) {
|
||||
if (candidate != null) {
|
||||
candidatesList.add(candidate);
|
||||
}
|
||||
}
|
||||
candidatesList.sort(Comparator.comparingDouble(Neighbor::getDistance));
|
||||
assert candidatesList.size() == maxNeighborCount;
|
||||
characters.get(i).setNeighbors(candidatesList);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private double maxDistance(Neighbor[] candidates) {
|
||||
|
||||
double maxDistance = 0;
|
||||
for (Neighbor candidate : candidates) {
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.getDistance() > maxDistance) {
|
||||
maxDistance = candidate.getDistance();
|
||||
while(iterator.hasNext()) {
|
||||
c.getNeighbors().add(new Neighbor(iterator.next().value(), c));
|
||||
}
|
||||
}
|
||||
return maxDistance;
|
||||
}
|
||||
|
||||
|
||||
private int clearMostDistant(Neighbor[] candidates) {
|
||||
|
||||
double maxDistance = 0;
|
||||
int maxIndex = 0;
|
||||
for (int i = 0; i < candidates.length; i++) {
|
||||
Neighbor candidate = candidates[i];
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.getDistance() > maxDistance) {
|
||||
maxDistance = candidate.getDistance();
|
||||
maxIndex = i;
|
||||
}
|
||||
}
|
||||
candidates[maxIndex] = null;
|
||||
return maxIndex;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-7
@@ -39,7 +39,10 @@ public class ReadingOrderService {
|
||||
}
|
||||
}
|
||||
|
||||
if (histogram.values().stream().mapToInt(Integer::intValue).average().orElse(1) < MULTI_COLUMN_DETECTION_THRESHOLD) {
|
||||
if (histogram.values()
|
||||
.stream()
|
||||
.mapToInt(Integer::intValue).average()
|
||||
.orElse(1) < MULTI_COLUMN_DETECTION_THRESHOLD) {
|
||||
return resolveSingleColumnReadingOrder(zones);
|
||||
} else {
|
||||
|
||||
@@ -52,7 +55,7 @@ public class ReadingOrderService {
|
||||
private static List<Zone> resolveSingleColumnReadingOrder(List<Zone> zones) {
|
||||
|
||||
zones.sort(Comparator.comparing(BoundingBox::getY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
|
||||
.thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
|
||||
.thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
|
||||
return zones;
|
||||
}
|
||||
|
||||
@@ -90,14 +93,14 @@ public class ReadingOrderService {
|
||||
}
|
||||
|
||||
leftOf.sort(Comparator.comparing(BoundingBox::getY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
|
||||
.thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
|
||||
.thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
|
||||
|
||||
rightOf.sort(Comparator.comparing(BoundingBox::getY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
|
||||
.thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
|
||||
.thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
|
||||
|
||||
middle.sort(Comparator.comparing(BoundingBox::getY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
|
||||
.thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
|
||||
|
||||
.thenComparing(BoundingBox::getX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
|
||||
/*
|
||||
List<Zone> leftNotIntersecting = new ArrayList<>();
|
||||
for (Zone leftZone : leftOf) {
|
||||
boolean intersects = false;
|
||||
@@ -139,7 +142,7 @@ public class ReadingOrderService {
|
||||
|
||||
middle.addAll(leftNotIntersecting);
|
||||
middle.addAll(rightNotIntersecting);
|
||||
|
||||
*/
|
||||
List<Zone> sortedZones = new ArrayList<>();
|
||||
sortedZones.addAll(leftOf);
|
||||
sortedZones.addAll(rightOf);
|
||||
|
||||
+59
-51
@@ -5,6 +5,7 @@ import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -12,6 +13,7 @@ import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Chara
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Line;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.UnionFind;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Zone;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
|
||||
@Service
|
||||
public class ZoneBuilderService {
|
||||
@@ -29,12 +31,10 @@ public class ZoneBuilderService {
|
||||
|
||||
private static final double ANGLE_TOLERANCE = Math.PI / 6;
|
||||
|
||||
private static final int MAX_ZONES = 300;
|
||||
|
||||
private static final double MAX_VERTICAL_MERGE_DISTANCE = 0.5;
|
||||
|
||||
|
||||
public List<Zone> buildZones(List<Line> lines, double characterSpacing, double lineSpacing) {
|
||||
public List<Zone> buildZones(List<Line> lines, double characterSpacing, double lineSpacing, CleanRulings rulings) {
|
||||
|
||||
double minHorizontalDistance = characterSpacing * MIN_HORIZONTAL_DISTANCE_MULTIPLIER;
|
||||
double maxVerticalDistance = lineSpacing * MAX_VERTICAL_DISTANCE_MULTIPLIER;
|
||||
@@ -45,38 +45,39 @@ public class ZoneBuilderService {
|
||||
|
||||
double meanHeight = calculateMeanHeight(lines);
|
||||
|
||||
lines.forEach(outerLine -> //
|
||||
lines.forEach(innerLine -> {
|
||||
lines.forEach(outerLine -> {
|
||||
lines.forEach(innerLine -> {
|
||||
|
||||
double scale = Math.min(outerLine.getHeight(), innerLine.getHeight()) / meanHeight;
|
||||
scale = Math.max(MIN_LINE_SIZE_SCALE, Math.min(scale, MAX_LINE_SIZE_SCALE));
|
||||
if (innerLine == outerLine //
|
||||
|| unionFind.inSameSet(outerLine, innerLine)//
|
||||
|| outerLine.angularDifference(innerLine) > ANGLE_TOLERANCE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!unionFind.inSameSet(outerLine, innerLine) && outerLine.angularDifference(innerLine) <= ANGLE_TOLERANCE) {
|
||||
double scale = Math.min(outerLine.getHeight(), innerLine.getHeight()) / meanHeight;
|
||||
scale = Math.max(MIN_LINE_SIZE_SCALE, Math.min(scale, MAX_LINE_SIZE_SCALE));
|
||||
|
||||
double horizontalDistance = outerLine.horizontalDistance(innerLine) / scale;
|
||||
double verticalDistance = outerLine.verticalDistance(innerLine) / scale;
|
||||
double horizontalDistance = outerLine.horizontalDistance(innerLine) / scale;
|
||||
double verticalDistance = outerLine.verticalDistance(innerLine) / scale;
|
||||
|
||||
if (minHorizontalDistance <= horizontalDistance && verticalDistance <= maxVerticalDistance //
|
||||
|| minHorizontalMergeDistance <= horizontalDistance && verticalDistance <= maxVerticalMergeDistance) {
|
||||
unionFind.union(outerLine, innerLine);
|
||||
}
|
||||
}
|
||||
}));
|
||||
if ((!(minHorizontalDistance <= horizontalDistance) || !(verticalDistance <= maxVerticalDistance)) //
|
||||
&& (!(minHorizontalMergeDistance <= horizontalDistance) || !(verticalDistance <= maxVerticalMergeDistance))) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Zone> zones = new ArrayList<>();
|
||||
unionFind.getGroups().forEach(group -> {
|
||||
zones.add(new Zone(new ArrayList<>(group)));
|
||||
if (rulings.lineBetween(outerLine, innerLine)) {
|
||||
return;
|
||||
}
|
||||
|
||||
unionFind.union(outerLine, innerLine);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
if (zones.size() > MAX_ZONES) {
|
||||
List<Line> oneZoneLines = new ArrayList<>();
|
||||
for (Zone zone : zones) {
|
||||
oneZoneLines.addAll(zone.getLines());
|
||||
}
|
||||
return List.of(mergeLinesInZone(oneZoneLines, characterSpacing, lineSpacing));
|
||||
}
|
||||
|
||||
return zones;
|
||||
return unionFind.getGroups()
|
||||
.stream()
|
||||
.map(group -> mergeLinesInZone(new ArrayList<>(group), characterSpacing, lineSpacing))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -103,35 +104,40 @@ public class ZoneBuilderService {
|
||||
UnionFind<Line> unionFind = new UnionFind<>(new HashSet<>(lines));
|
||||
|
||||
lines.forEach(outer -> {
|
||||
|
||||
lines.forEach(inner -> {
|
||||
if (inner != outer) {
|
||||
if (inner == outer) {
|
||||
return;
|
||||
}
|
||||
|
||||
double horizontalDistance = outer.horizontalDistance(inner);
|
||||
double verticalDistance = outer.verticalDistance(inner);
|
||||
double horizontalDistance = outer.horizontalDistance(inner);
|
||||
double verticalDistance = outer.verticalDistance(inner);
|
||||
|
||||
if (horizontalDistance <= maxHorizontalDistance && minVerticalDistance <= verticalDistance && verticalDistance <= maxVerticalDistance) {
|
||||
unionFind.union(outer, inner);
|
||||
} else if (minVerticalDistance <= verticalDistance && verticalDistance <= maxVerticalDistance && Math.abs(horizontalDistance - Math.min(outer.getLength(),
|
||||
inner.getLength())) < 0.1) {
|
||||
boolean characterOverlap = false;
|
||||
int overlappingCount = 0;
|
||||
for (Character outerCharacter : outer.getCharacters()) {
|
||||
for (Character innerCharacter : inner.getCharacters()) {
|
||||
double characterOverlapDistance = outerCharacter.overlappingDistance(innerCharacter);
|
||||
if (characterOverlapDistance > 2) {
|
||||
characterOverlap = true;
|
||||
}
|
||||
if (characterOverlapDistance > 0) {
|
||||
overlappingCount++;
|
||||
}
|
||||
if (horizontalDistance <= maxHorizontalDistance && minVerticalDistance <= verticalDistance && verticalDistance <= maxVerticalDistance) {
|
||||
|
||||
unionFind.union(outer, inner);
|
||||
|
||||
} else if (minVerticalDistance <= verticalDistance
|
||||
&& verticalDistance <= maxVerticalDistance
|
||||
&& Math.abs(horizontalDistance - Math.min(outer.getLength(), inner.getLength())) < 0.1) {
|
||||
|
||||
boolean characterOverlap = false;
|
||||
int overlappingCount = 0;
|
||||
for (Character outerCharacter : outer.getCharacters()) {
|
||||
for (Character innerCharacter : inner.getCharacters()) {
|
||||
double characterOverlapDistance = outerCharacter.overlappingDistance(innerCharacter);
|
||||
if (characterOverlapDistance > 2) {
|
||||
characterOverlap = true;
|
||||
}
|
||||
if (characterOverlapDistance > 0) {
|
||||
overlappingCount++;
|
||||
}
|
||||
}
|
||||
if (!characterOverlap && overlappingCount <= 2) {
|
||||
unionFind.union(outer, inner);
|
||||
}
|
||||
}
|
||||
if (!characterOverlap && overlappingCount <= 2) {
|
||||
unionFind.union(outer, inner);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,7 +152,9 @@ public class ZoneBuilderService {
|
||||
outputZone.add(new Line(characters, characterSpacing));
|
||||
}
|
||||
|
||||
return new Zone(outputZone);
|
||||
return new Zone(outputZone.stream()
|
||||
.sorted(Comparator.comparing(Line::getY0))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-67
@@ -1,7 +1,11 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -13,18 +17,13 @@ import lombok.NoArgsConstructor;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public abstract class AbstractPageBlock extends Rectangle {
|
||||
public abstract class AbstractPageBlock extends BoundingBox {
|
||||
|
||||
@JsonIgnore
|
||||
protected float minX;
|
||||
@JsonIgnore
|
||||
protected float maxX;
|
||||
@JsonIgnore
|
||||
protected float minY;
|
||||
@JsonIgnore
|
||||
protected float maxY;
|
||||
@JsonIgnore
|
||||
protected PageBlockType classification;
|
||||
|
||||
Set<LayoutEngine> engines = new HashSet<>();
|
||||
|
||||
@JsonIgnore
|
||||
protected int page;
|
||||
|
||||
@@ -41,63 +40,6 @@ public abstract class AbstractPageBlock extends Rectangle {
|
||||
}
|
||||
|
||||
|
||||
public boolean containsBlock(TextPageBlock other) {
|
||||
|
||||
return this.minX <= other.getPdfMinX() && this.maxX >= other.getPdfMaxX() && this.minY >= other.getPdfMinY() && this.maxY <= other.getPdfMaxY();
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(AbstractPageBlock other) {
|
||||
|
||||
return this.minX <= other.minX && this.maxX >= other.maxX && this.minY >= other.minY && this.maxY <= other.maxY;
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(Rectangle other) {
|
||||
|
||||
return page == other.getPage() && this.minX <= other.getTopLeft().getX() && this.maxX >= other.getTopLeft().getX() + other.getWidth() && this.minY <= other.getTopLeft()
|
||||
.getY() && this.maxY >= other.getTopLeft().getY() + other.getHeight();
|
||||
}
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
public float getHeight() {
|
||||
|
||||
return maxY - minY;
|
||||
}
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
public float getWidth() {
|
||||
|
||||
return maxX - minX;
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsY(AbstractPageBlock apb) {
|
||||
|
||||
return this.minY <= apb.getMaxY() && this.maxY >= apb.getMinY();
|
||||
}
|
||||
|
||||
|
||||
public boolean almostIntersects(AbstractPageBlock apb, float yThreshold, float xThreshold) {
|
||||
|
||||
return this.almostIntersectsX(apb, xThreshold) && this.almostIntersectsY(apb, yThreshold);
|
||||
}
|
||||
|
||||
|
||||
private boolean almostIntersectsY(AbstractPageBlock apb, float threshold) {
|
||||
|
||||
return this.minY - threshold <= apb.getMaxY() && this.maxY + threshold >= apb.getMinY();
|
||||
}
|
||||
|
||||
|
||||
private boolean almostIntersectsX(AbstractPageBlock apb, float threshold) {
|
||||
|
||||
return this.minX - threshold <= apb.getMaxX() && this.maxX + threshold >= apb.getMinX();
|
||||
}
|
||||
|
||||
|
||||
public abstract boolean isEmpty();
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -3,8 +3,11 @@ package com.knecon.fforesight.service.layoutparser.processor.model;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.OutlineObjectTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.TableOfContents;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.StringFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.UnclassifiedText;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -22,8 +25,12 @@ public class ClassificationDocument {
|
||||
private FloatFrequencyCounter fontSizeCounter = new FloatFrequencyCounter();
|
||||
private StringFrequencyCounter fontCounter = new StringFrequencyCounter();
|
||||
private StringFrequencyCounter fontStyleCounter = new StringFrequencyCounter();
|
||||
private LayoutparsingVisualizations visualizations = new LayoutparsingVisualizations();
|
||||
private boolean headlines;
|
||||
|
||||
private long rulesVersion;
|
||||
|
||||
private OutlineObjectTree outlineObjectTree;
|
||||
private TableOfContents tableOfContents;
|
||||
|
||||
}
|
||||
|
||||
+5
-1
@@ -8,13 +8,13 @@ import java.util.Map;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.OutlineObject;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.StringFrequencyCounter;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent;
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
@@ -23,6 +23,10 @@ public class ClassificationPage {
|
||||
@NonNull
|
||||
private List<AbstractPageBlock> textBlocks;
|
||||
|
||||
private List<OutlineObject> outlineObjects = new ArrayList<>();
|
||||
|
||||
private List<AbstractPageBlock> headlines = new ArrayList<>();
|
||||
|
||||
private List<ClassifiedImage> images = new ArrayList<>();
|
||||
|
||||
private Rectangle bodyTextFrame;
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Deprecated
|
||||
public class ClassificationSection {
|
||||
|
||||
private List<AbstractPageBlock> pageBlocks = new ArrayList<>();
|
||||
|
||||
+13
@@ -31,6 +31,19 @@ public enum PageBlockType {
|
||||
}
|
||||
|
||||
|
||||
public static int getHeadlineNumber(PageBlockType pageBlockType) {
|
||||
|
||||
return switch (pageBlockType) {
|
||||
case H1 -> 1;
|
||||
case H2 -> 2;
|
||||
case H3 -> 3;
|
||||
case H4 -> 4;
|
||||
case H5 -> 5;
|
||||
default -> 6;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public boolean isHeadline() {
|
||||
|
||||
return this.equals(H1) || this.equals(H2) || this.equals(H3) || this.equals(H4) || this.equals(H5) || this.equals(H6);
|
||||
|
||||
+4
-1
@@ -8,6 +8,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@AllArgsConstructor
|
||||
@@ -16,13 +17,15 @@ public class SectionIdentifier {
|
||||
|
||||
static Pattern numericalIdentifierPattern = Pattern.compile("^[\\s]?(\\d+)[\\s.,;]?(\\d+)?[\\s.,;]?(\\d+)?[\\s.,;]?(\\d+)?");
|
||||
|
||||
private enum Format {
|
||||
public enum Format {
|
||||
EMPTY,
|
||||
NUMERICAL,
|
||||
DOCUMENT
|
||||
}
|
||||
|
||||
@Getter
|
||||
Format format;
|
||||
@Getter
|
||||
String identifierString;
|
||||
List<Integer> identifiers;
|
||||
boolean asChild;
|
||||
|
||||
+2
-2
@@ -140,8 +140,8 @@ public class DocumentTree {
|
||||
if (treeId.isEmpty()) {
|
||||
return root;
|
||||
}
|
||||
Entry entry = root.children.get(treeId.get(0));
|
||||
for (int id : treeId.subList(1, treeId.size())) {
|
||||
Entry entry = root;
|
||||
for (int id : treeId) {
|
||||
entry = entry.children.get(id);
|
||||
}
|
||||
return entry;
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public abstract class AbstractSemanticNode implements GenericSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
|
||||
List<Integer> treeId;
|
||||
|
||||
TextBlock textBlock;
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public TextBlock getTextBlock() {
|
||||
|
||||
if (textBlock == null) {
|
||||
textBlock = GenericSemanticNode.super.getTextBlock();
|
||||
}
|
||||
return textBlock;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return treeId.toString() + ": " + getType() + ": " + this.getTextBlock().buildSummary();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
}
|
||||
+63
-29
@@ -3,42 +3,37 @@ package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Collections;
|
||||
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 java.util.stream.Stream;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class Document implements GenericSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
|
||||
public class Document extends AbstractSemanticNode {
|
||||
|
||||
Set<Page> pages;
|
||||
DocumentTree documentTree;
|
||||
Integer numberOfPages;
|
||||
TextBlock textBlock;
|
||||
@Builder.Default
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
|
||||
LayoutparsingVisualizations visualizations;
|
||||
|
||||
|
||||
@Override
|
||||
@@ -48,15 +43,27 @@ public class Document implements GenericSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
public TextBlock getTextBlock() {
|
||||
/**
|
||||
* Gets the sections of the document as a list.
|
||||
*
|
||||
* @return A list of all sections within the document.
|
||||
*/
|
||||
public List<Section> getAllSections() {
|
||||
|
||||
if (textBlock == null) {
|
||||
textBlock = GenericSemanticNode.super.getTextBlock();
|
||||
}
|
||||
return textBlock;
|
||||
return streamAllSubNodesOfType(NodeType.SECTION).map(node -> (Section) node)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the main sections of the document as a list.
|
||||
*
|
||||
* @return A list of main sections within the document
|
||||
* @deprecated This method is marked for removal.
|
||||
* Use {@link #streamChildrenOfType(NodeType)} instead,
|
||||
* or {@link #getChildrenOfTypeSectionOrSuperSection()} which returns children of type SECTION as well as SUPER_SECTION.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public List<Section> getMainSections() {
|
||||
|
||||
return streamChildrenOfType(NodeType.SECTION).map(node -> (Section) node)
|
||||
@@ -64,9 +71,45 @@ public class Document implements GenericSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the direct children of type SECTION or SUPER_SECTION of the document as a list of SemanticNode objects.
|
||||
*
|
||||
* @return A list of all children of type SECTION or SUPER_SECTION.
|
||||
*/
|
||||
public List<SemanticNode> getChildrenOfTypeSectionOrSuperSection() {
|
||||
|
||||
return streamChildren().filter(semanticNode -> semanticNode.getType().equals(NodeType.SECTION) || semanticNode.getType().equals(NodeType.SUPER_SECTION))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
public List<Header> getHeaders() {
|
||||
|
||||
return streamChildrenOfType(NodeType.HEADER).map(node -> (Header) node)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
public List<Footer> getFooters() {
|
||||
|
||||
return streamChildrenOfType(NodeType.FOOTER).map(node -> (Footer) node)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Headline getHeadline() {
|
||||
|
||||
return streamAllSubNodesOfType(NodeType.HEADLINE).map(node -> (Headline) node)
|
||||
.findFirst()
|
||||
.orElse(Headline.builder().build());
|
||||
}
|
||||
|
||||
|
||||
public Stream<TextBlock> streamTerminalTextBlocksInOrder() {
|
||||
|
||||
return streamAllNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getTextBlock);
|
||||
return streamAllNodes().filter(SemanticNode::isLeaf)
|
||||
.map(SemanticNode::getTextBlock);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,18 +127,9 @@ public class Document implements GenericSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Headline getHeadline() {
|
||||
|
||||
return streamAllSubNodesOfType(NodeType.HEADLINE).map(node -> (Headline) node)
|
||||
.findFirst()
|
||||
.orElse(Headline.builder().build());
|
||||
}
|
||||
|
||||
|
||||
private Stream<SemanticNode> streamAllNodes() {
|
||||
|
||||
return documentTree.allEntriesInOrder()
|
||||
return getDocumentTree().allEntriesInOrder()
|
||||
.map(DocumentTree.Entry::getNode);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -20,7 +20,8 @@ public class DuplicatedParagraph extends Paragraph {
|
||||
@Override
|
||||
public TextBlock getTextBlock() {
|
||||
|
||||
return Stream.of(leafTextBlock, unsortedLeafTextBlock).collect(new TextBlockCollector());
|
||||
return Stream.of(super.getLeafTextBlock(), unsortedLeafTextBlock)
|
||||
.collect(new TextBlockCollector());
|
||||
|
||||
}
|
||||
|
||||
|
||||
+5
-39
@@ -1,48 +1,24 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class Footer implements GenericSemanticNode {
|
||||
public class Footer extends AbstractSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
|
||||
|
||||
List<Integer> treeId;
|
||||
TextBlock leafTextBlock;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
@@ -68,17 +44,7 @@ public class Footer implements GenericSemanticNode {
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return treeId + ": " + NodeType.FOOTER + ": " + leafTextBlock.buildSummary();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
return getTreeId() + ": " + NodeType.FOOTER + ": " + leafTextBlock.buildSummary();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-38
@@ -1,47 +1,24 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class Header implements GenericSemanticNode {
|
||||
public class Header extends AbstractSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
|
||||
List<Integer> treeId;
|
||||
TextBlock leafTextBlock;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
@@ -67,17 +44,7 @@ public class Header implements GenericSemanticNode {
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return treeId + ": " + NodeType.HEADER + ": " + leafTextBlock.buildSummary();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
return getTreeId() + ": " + NodeType.HEADER + ": " + leafTextBlock.buildSummary();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-38
@@ -1,47 +1,24 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class Headline implements GenericSemanticNode {
|
||||
public class Headline extends AbstractSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
|
||||
List<Integer> treeId;
|
||||
TextBlock leafTextBlock;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
@@ -67,7 +44,7 @@ public class Headline implements GenericSemanticNode {
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return treeId + ": " + NodeType.HEADLINE + ": " + leafTextBlock.buildSummary();
|
||||
return getTreeId() + ": " + NodeType.HEADLINE + ": " + leafTextBlock.buildSummary();
|
||||
}
|
||||
|
||||
|
||||
@@ -77,14 +54,4 @@ public class Headline implements GenericSemanticNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+15
-23
@@ -3,17 +3,11 @@ package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Collections;
|
||||
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.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlockCollector;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -22,24 +16,24 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class Image implements GenericSemanticNode {
|
||||
public class Image extends AbstractSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
|
||||
|
||||
List<Integer> treeId;
|
||||
String id;
|
||||
|
||||
ImageType imageType;
|
||||
boolean transparent;
|
||||
Rectangle2D position;
|
||||
|
||||
TextBlock leafTextBlock;
|
||||
|
||||
boolean redaction;
|
||||
boolean ignored;
|
||||
@Builder.Default
|
||||
@@ -52,13 +46,6 @@ public class Image implements GenericSemanticNode {
|
||||
@EqualsAndHashCode.Exclude
|
||||
Page page;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
@@ -70,9 +57,7 @@ public class Image implements GenericSemanticNode {
|
||||
@Override
|
||||
public TextBlock getTextBlock() {
|
||||
|
||||
return streamAllSubNodes().filter(SemanticNode::isLeaf)
|
||||
.map(SemanticNode::getLeafTextBlock)
|
||||
.collect(new TextBlockCollector());
|
||||
return leafTextBlock;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +71,7 @@ public class Image implements GenericSemanticNode {
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return treeId + ": " + NodeType.IMAGE + ": " + imageType.toString() + " " + position;
|
||||
return getTreeId() + ": " + NodeType.IMAGE + ": " + imageType.toString() + " " + position;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,4 +83,11 @@ public class Image implements GenericSemanticNode {
|
||||
return bBoxPerPage;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-1
@@ -9,7 +9,8 @@ public enum ImageType {
|
||||
|
||||
SIGNATURE_VISUAL,
|
||||
OTHER,
|
||||
OCR;
|
||||
OCR,
|
||||
GRAPHIC;
|
||||
|
||||
|
||||
public static ImageType fromString(String imageType) {
|
||||
@@ -19,6 +20,7 @@ public enum ImageType {
|
||||
case "formula" -> ImageType.FORMULA;
|
||||
case "signature" -> ImageType.SIGNATURE;
|
||||
case "ocr" -> ImageType.OCR;
|
||||
case "graphic" -> ImageType.GRAPHIC;
|
||||
default -> ImageType.OTHER;
|
||||
};
|
||||
}
|
||||
|
||||
+2
-42
@@ -1,20 +1,10 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
@@ -23,25 +13,12 @@ import lombok.experimental.SuperBuilder;
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@FieldDefaults(level = AccessLevel.PROTECTED)
|
||||
public class Paragraph implements GenericSemanticNode {
|
||||
public class Paragraph extends AbstractSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
|
||||
|
||||
List<Integer> treeId;
|
||||
TextBlock leafTextBlock;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
@@ -63,21 +40,4 @@ public class Paragraph implements GenericSemanticNode {
|
||||
return leafTextBlock;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return treeId + ": " + NodeType.PARAGRAPH + ": " + leafTextBlock.buildSummary();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
-61
@@ -1,47 +1,20 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
@Builder
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class Section implements GenericSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
|
||||
List<Integer> treeId;
|
||||
|
||||
TextBlock textBlock;
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Section extends AbstractSemanticNode {
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
@@ -50,6 +23,14 @@ public class Section implements GenericSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
public Headline getHeadline() {
|
||||
|
||||
return streamChildrenOfType(NodeType.HEADLINE).map(node -> (Headline) node)
|
||||
.findFirst()
|
||||
.orElseGet(() -> getParent().getHeadline());
|
||||
}
|
||||
|
||||
|
||||
public boolean hasTables() {
|
||||
|
||||
return streamAllSubNodesOfType(NodeType.TABLE).findAny()
|
||||
@@ -57,39 +38,10 @@ public class Section implements GenericSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TextBlock getTextBlock() {
|
||||
|
||||
if (textBlock == null) {
|
||||
textBlock = GenericSemanticNode.super.getTextBlock();
|
||||
}
|
||||
return textBlock;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return treeId.toString() + ": " + NodeType.SECTION + ": " + this.getTextBlock().buildSummary();
|
||||
}
|
||||
|
||||
|
||||
public Headline getHeadline() {
|
||||
|
||||
return streamChildrenOfType(NodeType.HEADLINE)//
|
||||
.map(node -> (Headline) node)//
|
||||
.findFirst()//
|
||||
.orElseGet(() -> getParent().getHeadline());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
|
||||
if (bBoxCache == null) {
|
||||
bBoxCache = GenericSemanticNode.super.getBBox();
|
||||
}
|
||||
return bBoxCache;
|
||||
return getTreeId() + ": " + NodeType.SECTION + ": " + this.getTextBlock().buildSummary();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SuperSection extends AbstractSemanticNode {
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
|
||||
return NodeType.SUPER_SECTION;
|
||||
}
|
||||
|
||||
|
||||
public Headline getHeadline() {
|
||||
|
||||
return streamChildrenOfType(NodeType.HEADLINE).map(node -> (Headline) node)
|
||||
.findFirst()
|
||||
.orElseGet(() -> getParent().getHeadline());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return getTreeId() + ": " + NodeType.SUPER_SECTION + ": " + this.getTextBlock().buildSummary();
|
||||
}
|
||||
|
||||
}
|
||||
+5
-20
@@ -2,34 +2,26 @@ package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
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.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlockCollector;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@SuperBuilder
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class TableCell implements GenericSemanticNode {
|
||||
public class TableCell extends AbstractSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
|
||||
List<Integer> treeId;
|
||||
int row;
|
||||
int col;
|
||||
boolean header;
|
||||
@@ -40,13 +32,6 @@ public class TableCell implements GenericSemanticNode {
|
||||
|
||||
TextBlock textBlock;
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
DocumentTree documentTree;
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, Rectangle2D> getBBox() {
|
||||
@@ -96,7 +81,7 @@ public class TableCell implements GenericSemanticNode {
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return treeId + ": " + NodeType.TABLE_CELL + ": " + this.getTextBlock().buildSummary();
|
||||
return getTreeId() + ": " + NodeType.TABLE_CELL + ": " + this.getTextBlock().buildSummary();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -50,14 +50,16 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
|
||||
public ConcatenatedTextBlock concat(TextBlock textBlock) {
|
||||
|
||||
int start = textBlock.getBoundary().start();
|
||||
int end = textBlock.getBoundary().end();
|
||||
if (this.atomicTextBlocks.isEmpty()) {
|
||||
boundary.setStart(textBlock.getBoundary().start());
|
||||
boundary.setEnd(textBlock.getBoundary().end());
|
||||
} else if (boundary.end() != textBlock.getBoundary().start()) {
|
||||
boundary.setStart(start);
|
||||
boundary.setEnd(end);
|
||||
} else if (boundary.end() != start) {
|
||||
throw new UnsupportedOperationException(format("Can only concat consecutive TextBlocks, trying to concat %s and %s", boundary, textBlock.getBoundary()));
|
||||
}
|
||||
this.atomicTextBlocks.addAll(textBlock.getAtomicTextBlocks());
|
||||
boundary.setEnd(textBlock.getBoundary().end());
|
||||
boundary.setEnd(end);
|
||||
this.searchText = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.outline;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.pdfbox.cos.COSArray;
|
||||
import org.apache.pdfbox.cos.COSBase;
|
||||
import org.apache.pdfbox.cos.COSDictionary;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.cos.COSString;
|
||||
import org.apache.pdfbox.pdmodel.PDDestinationNameTreeNode;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.PDAction;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitHeightDestination;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitRectangleDestination;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitWidthDestination;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OutlineExtractorService {
|
||||
|
||||
private static final String PDDESTINATION_TYPE_FIT = "Fit";
|
||||
private static final String PDDESTINATION_TYPE_FIT_B = "FitB";
|
||||
private static final String PDDESTINATION_TYPE_FIT_H = "FitH";
|
||||
private static final String PDDESTINATION_TYPE_FIT_V = "FitV";
|
||||
private static final String PDDESTINATION_TYPE_FIT_R = "FitR";
|
||||
private static final String PDDESTINATION_TYPE_FIT_BH = "FitBH";
|
||||
private static final String PDDESTINATION_TYPE_FIT_BV = "FitBV";
|
||||
private static final String PDDESTINATION_TYPE_XYZ = "XYZ";
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public OutlineObjectTree getOutlineObjectTree(PDDocument document) {
|
||||
|
||||
PDDocumentOutline documentOutline = document.getDocumentCatalog().getDocumentOutline();
|
||||
|
||||
List<OutlineObjectTreeNode> rootNodes = new ArrayList<>();
|
||||
if (documentOutline != null) {
|
||||
for (PDOutlineItem child : documentOutline.children()) {
|
||||
Optional<OutlineObjectTreeNode> outlineObjectWithChildren = createOutlineObjectWithChildren(child, document, 1);
|
||||
outlineObjectWithChildren.ifPresent(rootNodes::add);
|
||||
}
|
||||
}
|
||||
|
||||
return new OutlineObjectTree(rootNodes);
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private Optional<OutlineObjectTreeNode> createOutlineObjectWithChildren(PDOutlineItem item, PDDocument document, int depth) {
|
||||
|
||||
Optional<OutlineObjectTreeNode> outlineObject = createOutlineObject(item, document, depth);
|
||||
if (outlineObject.isPresent()) {
|
||||
for (var child : item.children()) {
|
||||
Optional<OutlineObjectTreeNode> outlineObjectWithChildren = createOutlineObjectWithChildren(child, document, depth + 1);
|
||||
outlineObjectWithChildren.ifPresent(outlineObjectTreeNode -> outlineObject.get().addChild(outlineObjectTreeNode));
|
||||
}
|
||||
}
|
||||
|
||||
return outlineObject;
|
||||
}
|
||||
|
||||
|
||||
// if the structure elements are processed beforehand, another case can be handled here as well:
|
||||
// outline objects can reference structure elements (see pdf documentation)
|
||||
@SneakyThrows
|
||||
private Optional<OutlineObjectTreeNode> createOutlineObject(PDOutlineItem item, PDDocument document, int depth) {
|
||||
|
||||
String title = item.getTitle();
|
||||
|
||||
PDPage page = item.findDestinationPage(document);
|
||||
if (page == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
int pageNumber = document.getPages().indexOf(page);
|
||||
|
||||
Optional<Point2D> outlinePosition = Optional.empty();
|
||||
|
||||
try {
|
||||
PDDocumentNameDictionary names = document.getDocumentCatalog().getNames();
|
||||
PDDestinationNameTreeNode destinations = null;
|
||||
if (names != null) {
|
||||
destinations = names.getDests();
|
||||
}
|
||||
|
||||
PDDestination destination = item.getDestination();
|
||||
if (destination != null) {
|
||||
outlinePosition = getLocationFromCOSBase(destinations, destination.getCOSObject());
|
||||
}
|
||||
|
||||
if (outlinePosition.isEmpty()) {
|
||||
|
||||
PDAction action = item.getAction();
|
||||
if (action != null) {
|
||||
outlinePosition = extractOutlineLocationGoTo(destinations, action.getCOSObject());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.info(String.format("Error occurred during position resolution for outline item on page %s with title %s: " + e, pageNumber, title));
|
||||
}
|
||||
|
||||
return Optional.of(new OutlineObjectTreeNode(new OutlineObject(title, pageNumber, outlinePosition.orElse(new Point2D.Float(0, 0)), depth)));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private static Optional<Point2D> extractOutlineLocationGoTo(PDDestinationNameTreeNode destinations, COSDictionary cosDictionary) {
|
||||
|
||||
if (isGoToAction(cosDictionary)) {
|
||||
COSBase cosBase = cosDictionary.getItem(COSName.D);
|
||||
return getLocationFromCOSBase(destinations, cosBase);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
private static Optional<Point2D> getLocationFromCOSBase(PDDestinationNameTreeNode destinations, COSBase cosBase) throws IOException {
|
||||
|
||||
if (cosBase != null) {
|
||||
if (cosBase instanceof COSArray cosArray) {
|
||||
return getLocationFromCosArray(cosArray);
|
||||
}
|
||||
|
||||
if (cosBase instanceof COSString cosString) {
|
||||
String destinationName = cosString.getString();
|
||||
COSArray cosArray = destinations.getValue(destinationName).getCOSObject();
|
||||
return getLocationFromCosArray(cosArray);
|
||||
}
|
||||
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
private static Optional<Point2D> getLocationFromCosArray(COSArray cosArray) {
|
||||
|
||||
boolean located = false;
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
|
||||
try {
|
||||
|
||||
PDDestination destination = PDDestination.create(cosArray);
|
||||
COSName type = (COSName) cosArray.getObject(1);
|
||||
String typeString = type.getName();
|
||||
|
||||
switch (typeString) {
|
||||
case PDDESTINATION_TYPE_FIT_V:
|
||||
case PDDESTINATION_TYPE_FIT_BV:
|
||||
PDPageFitHeightDestination fitHeightDestination = (PDPageFitHeightDestination) destination;
|
||||
x = fitHeightDestination.getLeft();
|
||||
located = true;
|
||||
break;
|
||||
case PDDESTINATION_TYPE_FIT_R:
|
||||
PDPageFitRectangleDestination fitRectangleDestination = (PDPageFitRectangleDestination) destination;
|
||||
x = fitRectangleDestination.getLeft();
|
||||
y = fitRectangleDestination.getTop();
|
||||
located = true;
|
||||
break;
|
||||
case PDDESTINATION_TYPE_FIT_H:
|
||||
case PDDESTINATION_TYPE_FIT_BH:
|
||||
PDPageFitWidthDestination fitWidthDestination = (PDPageFitWidthDestination) destination;
|
||||
y = fitWidthDestination.getTop();
|
||||
located = true;
|
||||
break;
|
||||
case PDDESTINATION_TYPE_XYZ:
|
||||
PDPageXYZDestination xyzDestination = (PDPageXYZDestination) destination;
|
||||
x = xyzDestination.getLeft();
|
||||
y = xyzDestination.getTop();
|
||||
located = true;
|
||||
break;
|
||||
case PDDESTINATION_TYPE_FIT:
|
||||
case PDDESTINATION_TYPE_FIT_B:
|
||||
default:
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return located ? Optional.of(new Point2D.Float(x, y)) : Optional.empty();
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static boolean isGoToAction(COSDictionary cosDictionary) {
|
||||
|
||||
return cosDictionary.getNameAsString("S").toLowerCase(Locale.ROOT).equals("goto");
|
||||
}
|
||||
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.outline;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OutlineObject {
|
||||
|
||||
private final String title;
|
||||
private final int pageNumber;
|
||||
private Point2D point;
|
||||
private final int treeDepth;
|
||||
|
||||
private boolean found;
|
||||
|
||||
|
||||
public OutlineObject(String title, int pageNumber, Point2D point2D, int depth) {
|
||||
|
||||
this(title, pageNumber, depth);
|
||||
this.point = point2D;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return "OutlineObject{" + "title='" + title + '\'' + '}';
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.outline;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
public class OutlineObjectTree {
|
||||
|
||||
private List<OutlineObjectTreeNode> rootNodes = new ArrayList<>();
|
||||
|
||||
private Map<Integer, List<OutlineObject>> outlineObjectsPerPage = new HashMap<>();
|
||||
|
||||
|
||||
public OutlineObjectTree(List<OutlineObjectTreeNode> rootNodes) {
|
||||
|
||||
this.rootNodes = rootNodes;
|
||||
flattenNodesAndGroupByPage(rootNodes);
|
||||
}
|
||||
|
||||
|
||||
private void flattenNodesAndGroupByPage(List<OutlineObjectTreeNode> outlineObjectTreeNodes) {
|
||||
|
||||
for (OutlineObjectTreeNode node : outlineObjectTreeNodes) {
|
||||
int pageNumber = node.getOutlineObject().getPageNumber();
|
||||
if (!this.outlineObjectsPerPage.containsKey(pageNumber)) {
|
||||
outlineObjectsPerPage.put(pageNumber, new ArrayList<>());
|
||||
}
|
||||
outlineObjectsPerPage.get(pageNumber).add(node.getOutlineObject());
|
||||
|
||||
if (!node.getChildren().isEmpty()) {
|
||||
flattenNodesAndGroupByPage(node.getChildren());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.outline;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class OutlineObjectTreeNode {
|
||||
|
||||
private OutlineObject outlineObject;
|
||||
|
||||
private List<OutlineObjectTreeNode> children = new ArrayList<>();
|
||||
|
||||
|
||||
public OutlineObjectTreeNode(OutlineObject outlineObject) {
|
||||
|
||||
this.outlineObject = outlineObject;
|
||||
}
|
||||
|
||||
|
||||
public void addChild(OutlineObjectTreeNode outlineObject) {
|
||||
|
||||
children.add(outlineObject);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return "OutlineObjectTreeNode{" + "outlineObject=" + outlineObject + '}';
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.outline;
|
||||
|
||||
import static com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType.getHeadlineNumber;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OutlineValidationService {
|
||||
|
||||
public TableOfContents createToC(List<TextPageBlock> headlines) {
|
||||
|
||||
List<TableOfContentItem> mainSections = new ArrayList<>();
|
||||
Map<Integer, TableOfContentItem> lastItemsPerDepth = new HashMap<>();
|
||||
TableOfContentItem last = null;
|
||||
TreeSet<Integer> depths = new TreeSet<>();
|
||||
|
||||
for (TextPageBlock current : headlines) {
|
||||
int currentDepth = getHeadlineNumber(current.getClassification());
|
||||
Integer parentDepth = depths.floor(currentDepth - 1);
|
||||
|
||||
var tocItem = new TableOfContentItem(current);
|
||||
|
||||
if (parentDepth == null) {
|
||||
mainSections.add(tocItem);
|
||||
lastItemsPerDepth = new HashMap<>();
|
||||
depths = new TreeSet<>();
|
||||
|
||||
} else {
|
||||
assert last != null;
|
||||
int lastDepth = getHeadlineNumber(last.getHeadline().getClassification());
|
||||
|
||||
if (lastDepth < parentDepth) {
|
||||
parentDepth = lastDepth;
|
||||
} else if (lastDepth == currentDepth && last.getParent() != null) {
|
||||
parentDepth = getHeadlineNumber(last.getParent().getHeadline().getClassification());
|
||||
}
|
||||
|
||||
TableOfContentItem parent = lastItemsPerDepth.get(parentDepth);
|
||||
parent.addChild(tocItem);
|
||||
}
|
||||
|
||||
last = tocItem;
|
||||
lastItemsPerDepth.put(currentDepth, tocItem);
|
||||
depths.add(currentDepth);
|
||||
}
|
||||
|
||||
return new TableOfContents(mainSections);
|
||||
}
|
||||
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.outline;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationFooter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationHeader;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Cell;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class TOCEnrichmentService {
|
||||
|
||||
public void assignSectionBlocksAndImages(ClassificationDocument document) {
|
||||
|
||||
TableOfContents toc = document.getTableOfContents();
|
||||
Iterator<TableOfContentItem> iterator = toc.iterator();
|
||||
TableOfContentItem currentTOCItem = null;
|
||||
if (iterator.hasNext()) {
|
||||
currentTOCItem = iterator.next();
|
||||
}
|
||||
List<AbstractPageBlock> startBlocks = new ArrayList<>();
|
||||
List<ClassifiedImage> startImages = new ArrayList<>();
|
||||
TableOfContentItem currentSection = null;
|
||||
boolean foundFirstHeadline = false;
|
||||
|
||||
List<ClassificationHeader> headers = new ArrayList<>();
|
||||
List<ClassificationFooter> footers = new ArrayList<>();
|
||||
TablePageBlock previousTable = null;
|
||||
List<TableOfContentItem> lastFoundTOCItems = new ArrayList<>();
|
||||
|
||||
for (ClassificationPage page : document.getPages()) {
|
||||
List<TableOfContentItem> currentPageTOCItems = new ArrayList<>();
|
||||
List<TextPageBlock> header = new ArrayList<>();
|
||||
List<TextPageBlock> footer = new ArrayList<>();
|
||||
for (AbstractPageBlock current : page.getTextBlocks()) {
|
||||
|
||||
if (current.getClassification() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
current.setPage(page.getPageNumber());
|
||||
|
||||
if (current.getClassification().equals(PageBlockType.HEADER)) {
|
||||
header.add((TextPageBlock) current);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.getClassification().equals(PageBlockType.FOOTER)) {
|
||||
footer.add((TextPageBlock) current);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current instanceof TablePageBlock table) {
|
||||
if (previousTable != null) {
|
||||
mergeTableMetadata(table, previousTable);
|
||||
}
|
||||
previousTable = table;
|
||||
}
|
||||
|
||||
if (current instanceof TextPageBlock && currentTOCItem != null && currentTOCItem.getHeadline().getText().equals(current.getText())) {
|
||||
if (!foundFirstHeadline) {
|
||||
foundFirstHeadline = true;
|
||||
}
|
||||
currentSection = currentTOCItem;
|
||||
currentTOCItem.getSectionBlocks().add(current);
|
||||
currentPageTOCItems.add(currentTOCItem);
|
||||
|
||||
if (iterator.hasNext()) {
|
||||
currentTOCItem = iterator.next();
|
||||
}
|
||||
} else if (!foundFirstHeadline) {
|
||||
startBlocks.add(current);
|
||||
} else {
|
||||
currentSection.getSectionBlocks().add(current);
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentPageTOCItems.isEmpty()) {
|
||||
lastFoundTOCItems = currentPageTOCItems;
|
||||
}
|
||||
|
||||
for (ClassifiedImage image : page.getImages()) {
|
||||
|
||||
Double xMin = null;
|
||||
Double yMin = null;
|
||||
Double xMax = null;
|
||||
Double yMax = null;
|
||||
|
||||
for (TableOfContentItem tocItem : lastFoundTOCItems) {
|
||||
var headline = tocItem.getHeadline();
|
||||
|
||||
if (headline.getPage() != page.getPageNumber()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (headline.getMinX() < headline.getMaxX()) {
|
||||
if (xMin == null || headline.getMinX() < xMin) {
|
||||
xMin = headline.getMinX();
|
||||
}
|
||||
if (xMax == null || headline.getMaxX() > xMax) {
|
||||
xMax = headline.getMaxX();
|
||||
}
|
||||
} else {
|
||||
if (xMin == null || headline.getMaxX() < xMin) {
|
||||
xMin = headline.getMaxX();
|
||||
}
|
||||
if (xMax == null || headline.getMinX() > xMax) {
|
||||
xMax = headline.getMinX();
|
||||
}
|
||||
}
|
||||
|
||||
if (headline.getMinY() < headline.getMaxY()) {
|
||||
if (yMin == null || headline.getMinY() < yMin) {
|
||||
yMin = headline.getMinY();
|
||||
}
|
||||
if (yMax == null || headline.getMaxY() > yMax) {
|
||||
yMax = headline.getMaxY();
|
||||
}
|
||||
} else {
|
||||
if (yMin == null || headline.getMaxY() < yMin) {
|
||||
yMin = headline.getMaxY();
|
||||
}
|
||||
if (yMax == null || headline.getMinY() > yMax) {
|
||||
yMax = headline.getMinY();
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Image position x: {}, y: {}", image.getPosition().getX(), image.getPosition().getY());
|
||||
log.debug("Headline position xMin: {}, xMax: {}, yMin: {}, yMax: {}", xMin, xMax, yMin, yMax);
|
||||
|
||||
if (image.getPosition().getX() >= xMin && image.getPosition().getX() <= xMax && image.getPosition().getY() >= yMin && image.getPosition().getY() <= yMax) {
|
||||
tocItem.getImages().add(image);
|
||||
image.setAppendedToSection(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!image.isAppendedToSection()) {
|
||||
log.debug("Image uses last found section");
|
||||
if (!lastFoundTOCItems.isEmpty()) {
|
||||
lastFoundTOCItems.get(lastFoundTOCItems.size() - 1).getImages().add(image);
|
||||
} else {
|
||||
startImages.add(image);
|
||||
}
|
||||
image.setAppendedToSection(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!header.isEmpty()) {
|
||||
headers.add(new ClassificationHeader(header));
|
||||
}
|
||||
if (!footer.isEmpty()) {
|
||||
footers.add(new ClassificationFooter(footer));
|
||||
}
|
||||
}
|
||||
|
||||
if (!startBlocks.isEmpty()) {
|
||||
TableOfContentItem unassigned = new TableOfContentItem(null);
|
||||
unassigned.setSectionBlocks(startBlocks);
|
||||
unassigned.setImages(startImages);
|
||||
document.getTableOfContents().getMainSections().add(0, unassigned);
|
||||
}
|
||||
document.setHeaders(headers);
|
||||
document.setFooters(footers);
|
||||
}
|
||||
|
||||
|
||||
private void mergeTableMetadata(TablePageBlock currentTable, TablePageBlock previousTable) {
|
||||
|
||||
// Distribute header information for subsequent tables
|
||||
if (previousTable != null && hasInvalidHeaderInformation(currentTable) && hasValidHeaderInformation(previousTable)) {
|
||||
List<Cell> previousTableNonHeaderRow = getRowWithNonHeaderCells(previousTable);
|
||||
List<Cell> tableNonHeaderRow = getRowWithNonHeaderCells(currentTable);
|
||||
// Allow merging of tables if header row is separated from first logical non-header row
|
||||
if (previousTableNonHeaderRow.isEmpty()
|
||||
&& previousTable.getRowCount() == 1
|
||||
&& previousTable.getRows()
|
||||
.get(0).size() == tableNonHeaderRow.size()) {
|
||||
previousTableNonHeaderRow = previousTable.getRows()
|
||||
.get(0)
|
||||
.stream()
|
||||
.map(cell -> {
|
||||
Cell fakeCell = Cell.copy(cell);
|
||||
fakeCell.setHeaderCells(Collections.singletonList(cell));
|
||||
return fakeCell;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
if (previousTableNonHeaderRow.size() == tableNonHeaderRow.size()) {
|
||||
for (int i = currentTable.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
|
||||
List<Cell> row = currentTable.getRows()
|
||||
.get(i);
|
||||
if (row.size() == tableNonHeaderRow.size() && row.stream()
|
||||
.allMatch(cell -> cell.getHeaderCells().isEmpty())) {
|
||||
for (int j = 0; j < row.size(); j++) {
|
||||
row.get(j).setHeaderCells(previousTableNonHeaderRow.get(j).getHeaderCells());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean hasValidHeaderInformation(TablePageBlock table) {
|
||||
|
||||
return !hasInvalidHeaderInformation(table);
|
||||
}
|
||||
|
||||
|
||||
private boolean hasInvalidHeaderInformation(TablePageBlock table) {
|
||||
|
||||
return table.getRows()
|
||||
.stream()
|
||||
.flatMap(row -> row.stream()
|
||||
.filter(cell -> !cell.getHeaderCells().isEmpty()))
|
||||
.findAny().isEmpty();
|
||||
|
||||
}
|
||||
|
||||
|
||||
private List<Cell> getRowWithNonHeaderCells(TablePageBlock table) {
|
||||
|
||||
for (int i = table.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
|
||||
List<Cell> row = table.getRows()
|
||||
.get(i);
|
||||
if (row.size() == 1) {
|
||||
continue;
|
||||
}
|
||||
boolean allNonHeader = true;
|
||||
for (Cell cell : row) {
|
||||
if (cell.isHeaderCell()) {
|
||||
allNonHeader = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allNonHeader) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.outline;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.AbstractSemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class TableOfContentItem {
|
||||
|
||||
@EqualsAndHashCode.Include
|
||||
private TextPageBlock headline;
|
||||
private List<TableOfContentItem> children = new ArrayList<>();
|
||||
private TableOfContentItem parent;
|
||||
|
||||
private List<AbstractPageBlock> sectionBlocks = new ArrayList<>();
|
||||
private List<ClassifiedImage> images = new ArrayList<>();
|
||||
|
||||
private AbstractSemanticNode section;
|
||||
|
||||
|
||||
public TableOfContentItem(TextPageBlock headline) {
|
||||
|
||||
this.headline = headline;
|
||||
}
|
||||
|
||||
|
||||
public void addChild(TableOfContentItem tableOfContentItem) {
|
||||
|
||||
children.add(tableOfContentItem);
|
||||
tableOfContentItem.setParent(this);
|
||||
}
|
||||
|
||||
|
||||
public TableOfContentItem getSiblingBefore() {
|
||||
|
||||
if (parent != null) {
|
||||
int index = parent.getChildren().indexOf(this);
|
||||
if (index > 0) {
|
||||
return parent.getChildren()
|
||||
.get(index - 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public TableOfContentItem getSiblingAfter() {
|
||||
|
||||
if (parent != null) {
|
||||
int index = parent.getChildren().indexOf(this);
|
||||
if (index >= 0 && index < parent.getChildren().size() - 1) {
|
||||
return parent.getChildren()
|
||||
.get(index + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(TextPageBlock block) {
|
||||
|
||||
if (headline.equals(block)) {
|
||||
return true;
|
||||
}
|
||||
for (TableOfContentItem child : children) {
|
||||
if (child.contains(block)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(TableOfContentItem tocItem) {
|
||||
|
||||
if (this.equals(tocItem)) {
|
||||
return true;
|
||||
}
|
||||
for (TableOfContentItem child : children) {
|
||||
if (child.contains(tocItem)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<AbstractPageBlock> getNonEmptySectionBlocks() {
|
||||
|
||||
return sectionBlocks.stream().filter(pageBlock -> !pageBlock.isEmpty()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return "OutlineObjectTreeNode{" + "textPageBlock=" + headline + '}';
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.outline;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
public class TableOfContents implements Iterable<TableOfContentItem> {
|
||||
|
||||
private List<TableOfContentItem> mainSections = new ArrayList<>();
|
||||
|
||||
|
||||
public TableOfContents(List<TableOfContentItem> mainSections) {
|
||||
|
||||
this.mainSections = mainSections;
|
||||
}
|
||||
|
||||
|
||||
public List<TextPageBlock> getAllTextPageBlocks() {
|
||||
|
||||
List<TextPageBlock> allTextPageBlocks = new ArrayList<>();
|
||||
for (TableOfContentItem item : mainSections) {
|
||||
collectTextPageBlocks(item, allTextPageBlocks);
|
||||
}
|
||||
return allTextPageBlocks;
|
||||
}
|
||||
|
||||
|
||||
private void collectTextPageBlocks(TableOfContentItem item, List<TextPageBlock> textPageBlocks) {
|
||||
|
||||
textPageBlocks.add(item.getHeadline());
|
||||
for (TableOfContentItem child : item.getChildren()) {
|
||||
collectTextPageBlocks(child, textPageBlocks);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public List<TableOfContentItem> getAllTableOfContentItems() {
|
||||
|
||||
List<TableOfContentItem> allItems = new ArrayList<>();
|
||||
for (TableOfContentItem item : mainSections) {
|
||||
collectTableOfContentItems(item, allItems);
|
||||
}
|
||||
return allItems;
|
||||
}
|
||||
|
||||
|
||||
private void collectTableOfContentItems(TableOfContentItem item, List<TableOfContentItem> allItems) {
|
||||
|
||||
allItems.add(item);
|
||||
for (TableOfContentItem child : item.getChildren()) {
|
||||
collectTableOfContentItems(child, allItems);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean containsBlock(TextPageBlock block) {
|
||||
|
||||
for (TableOfContentItem existingItem : this.getMainSections()) {
|
||||
if (existingItem.getHeadline().equals(block) || existingItem.contains(block)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private boolean containsItem(TableOfContentItem tocItem) {
|
||||
|
||||
for (TableOfContentItem existingItem : this.getMainSections()) {
|
||||
if (existingItem.equals(tocItem) || existingItem.contains(tocItem)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public @NonNull Iterator<TableOfContentItem> iterator() {
|
||||
|
||||
return new TableOfContentItemIterator(mainSections);
|
||||
}
|
||||
|
||||
|
||||
private static class TableOfContentItemIterator implements Iterator<TableOfContentItem> {
|
||||
|
||||
private final Stack<Iterator<TableOfContentItem>> stack = new Stack<>();
|
||||
|
||||
|
||||
TableOfContentItemIterator(List<TableOfContentItem> mainSections) {
|
||||
|
||||
stack.push(mainSections.iterator());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
|
||||
ensureStackTopIsCurrent();
|
||||
return !stack.isEmpty() && stack.peek().hasNext();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TableOfContentItem next() {
|
||||
|
||||
ensureStackTopIsCurrent();
|
||||
TableOfContentItem currentItem = stack.peek().next();
|
||||
if (currentItem.getChildren() != null && !currentItem.getChildren().isEmpty()) {
|
||||
stack.push(currentItem.getChildren()
|
||||
.iterator());
|
||||
}
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
|
||||
private void ensureStackTopIsCurrent() {
|
||||
|
||||
while (!stack.isEmpty() && !stack.peek().hasNext()) {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+17
-4
@@ -1,11 +1,13 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.table;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.TextNormalizationUtilities;
|
||||
@@ -18,7 +20,7 @@ import lombok.NoArgsConstructor;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@NoArgsConstructor
|
||||
public class Cell extends Rectangle {
|
||||
public class Cell extends BoundingBox {
|
||||
|
||||
private List<TextPageBlock> textBlocks = new ArrayList<>();
|
||||
|
||||
@@ -33,13 +35,24 @@ public class Cell extends Rectangle {
|
||||
|
||||
public Cell(Point2D topLeft, Point2D bottomRight) {
|
||||
|
||||
super((float) topLeft.getY(), (float) topLeft.getX(), (float) (bottomRight.getX() - topLeft.getX()), (float) (bottomRight.getY() - topLeft.getY()));
|
||||
this.bBoxInitialUserSpace = new Rectangle2D.Double(topLeft.getX(), topLeft.getY(), (bottomRight.getX() - topLeft.getX()), (bottomRight.getY() - topLeft.getY()));
|
||||
this.bBox = bBoxInitialUserSpace;
|
||||
}
|
||||
|
||||
|
||||
public Cell(Rectangle2D r) {
|
||||
public Cell(Rectangle2D bBoxInitialUserSpace, AffineTransform initialUserSpaceToJava) {
|
||||
|
||||
super((float) r.getY(), (float) r.getX(), (float) r.getWidth(), (float) r.getHeight());
|
||||
this.bBoxInitialUserSpace = bBoxInitialUserSpace;
|
||||
this.bBox = initialUserSpaceToJava.createTransformedShape(bBoxInitialUserSpace).getBounds2D();
|
||||
}
|
||||
|
||||
|
||||
public static Cell copy(Cell cell) {
|
||||
|
||||
Cell copy = new Cell();
|
||||
copy.bBoxInitialUserSpace = cell.bBoxInitialUserSpace;
|
||||
copy.bBox = cell.bBox;
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
||||
|
||||
+197
-6
@@ -1,15 +1,206 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.table;
|
||||
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class CleanRulings {
|
||||
|
||||
List<Ruling> horizontal;
|
||||
List<Ruling> vertical;
|
||||
List<Ruling> horizontals; // unmodifiable sorted by Y list
|
||||
List<Ruling> verticals; // unmodifiable sorted by X list
|
||||
|
||||
|
||||
public CleanRulings(List<Ruling> horizontals, List<Ruling> verticals) {
|
||||
|
||||
this.horizontals = horizontals.stream()
|
||||
.peek(Ruling::assertHorizontal)
|
||||
.sorted(Comparator.comparing(Line2D.Float::getY1))
|
||||
.toList();
|
||||
this.verticals = verticals.stream()
|
||||
.peek(Ruling::assertVertical)
|
||||
.sorted(Comparator.comparing(Line2D.Float::getX1))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
public CleanRulings getTableLines() {
|
||||
|
||||
return new CleanRulings(horizontals.stream()
|
||||
.filter(ruling -> ruling.getClassification().equals(Ruling.Classification.TABLE_LINE))
|
||||
.toList(),
|
||||
verticals.stream()
|
||||
.filter(ruling -> ruling.getClassification().equals(Ruling.Classification.TABLE_LINE))
|
||||
.toList());
|
||||
}
|
||||
|
||||
|
||||
public CleanRulings withoutTextRulings() {
|
||||
|
||||
return new CleanRulings(horizontals.stream()
|
||||
.filter(ruling -> !(ruling.getClassification().equals(Ruling.Classification.UNDERLINE) || ruling.getClassification()
|
||||
.equals(Ruling.Classification.STRIKETROUGH)))
|
||||
.toList(),
|
||||
verticals.stream()
|
||||
.filter(ruling -> !(ruling.getClassification().equals(Ruling.Classification.UNDERLINE) || ruling.getClassification()
|
||||
.equals(Ruling.Classification.STRIKETROUGH)))
|
||||
.toList());
|
||||
}
|
||||
|
||||
|
||||
public List<Ruling> buildAll() {
|
||||
|
||||
ArrayList<Ruling> rulings = new ArrayList<>(horizontals.size() + verticals.size());
|
||||
rulings.addAll(horizontals);
|
||||
rulings.addAll(verticals);
|
||||
return rulings;
|
||||
}
|
||||
|
||||
|
||||
public boolean lineBetween(BoundingBox a, BoundingBox b) {
|
||||
|
||||
return lineBetween(a.getBBoxInitialUserSpace(), b.getBBoxInitialUserSpace());
|
||||
}
|
||||
|
||||
|
||||
public boolean lineBetween(Rectangle2D a, Rectangle2D b) {
|
||||
|
||||
return lineBetween(new Point2D.Double(a.getCenterX(), a.getCenterY()), new Point2D.Double(b.getCenterX(), b.getCenterY()));
|
||||
}
|
||||
|
||||
|
||||
public boolean lineBetween(Point2D p1, Point2D p2) {
|
||||
|
||||
Ruling ruling = new Ruling(p1, p2);
|
||||
|
||||
if (ruling.isHorizontal()) {
|
||||
return getVerticalsInXInterval(ruling.x1, ruling.x2).stream()
|
||||
.anyMatch(vertical -> vertical.intersectsLine(ruling));
|
||||
|
||||
}
|
||||
|
||||
if (ruling.isVertical()) {
|
||||
return getHorizontalsInYInterval(ruling.y1, ruling.y2).stream()
|
||||
.anyMatch(horizontal -> horizontal.intersectsLine(ruling));
|
||||
|
||||
}
|
||||
|
||||
return Stream.of(getVerticalsInXInterval(ruling.x1, ruling.x2), getHorizontalsInYInterval(ruling.y1, ruling.y2))
|
||||
.flatMap(Collection::stream)
|
||||
.anyMatch(other -> other.intersectsLine(ruling));
|
||||
}
|
||||
|
||||
|
||||
public List<Ruling> getHorizontalsInYInterval(float y1, float y2) {
|
||||
|
||||
float startY = Math.min(y1, y2);
|
||||
float endY = Math.max(y1, y2);
|
||||
|
||||
if (horizontals.isEmpty() || Float.isNaN(startY) || Float.isNaN(endY)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int firstGreaterThanIdx = findFirstHorizontalRulingIdxAbove(startY);
|
||||
|
||||
if (firstGreaterThanIdx == -1) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Ruling> result = new LinkedList<>();
|
||||
for (int i = firstGreaterThanIdx; i < horizontals.size(); i++) {
|
||||
Ruling horizontal = horizontals.get(i);
|
||||
if (horizontal.y1 > endY) {
|
||||
break;
|
||||
}
|
||||
result.add(horizontal);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private int findFirstHorizontalRulingIdxAbove(float y) {
|
||||
|
||||
int low = 0;
|
||||
int high = horizontals.size() - 1;
|
||||
|
||||
while (low <= high) {
|
||||
int mid = low + (high - low) / 2;
|
||||
Line2D.Float midLine = horizontals.get(mid);
|
||||
float midY = midLine.y1;
|
||||
|
||||
if (midY == y) {
|
||||
return mid;
|
||||
} else if (midY > y) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the index of the first element greater than y or -1 if not found
|
||||
return horizontals.size() > low && horizontals.get(low).y1 > y ? low : -1;
|
||||
}
|
||||
|
||||
|
||||
public List<Ruling> getVerticalsInXInterval(float x1, float x2) {
|
||||
|
||||
float startX = Math.min(x1, x2);
|
||||
float endX = Math.max(x1, x2);
|
||||
|
||||
if (verticals.isEmpty() || Float.isNaN(startX) || Float.isNaN(endX)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int firstGreaterThanIdx = findFirstVerticalRulingIdxRightOf(startX);
|
||||
|
||||
if (firstGreaterThanIdx == -1) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Ruling> result = new LinkedList<>();
|
||||
for (int i = firstGreaterThanIdx; i < verticals.size(); i++) {
|
||||
Ruling horizontal = verticals.get(i);
|
||||
if (horizontal.x1 > endX) {
|
||||
break;
|
||||
}
|
||||
result.add(horizontal);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private int findFirstVerticalRulingIdxRightOf(float x) {
|
||||
|
||||
int low = 0;
|
||||
int high = verticals.size() - 1;
|
||||
|
||||
while (low <= high) {
|
||||
int mid = low + (high - low) / 2;
|
||||
Line2D.Float midLine = verticals.get(mid);
|
||||
float midX = midLine.x1;
|
||||
|
||||
if (midX == x) {
|
||||
return mid;
|
||||
} else if (midX > x) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the index of the first element greater than y or -1 if not found
|
||||
return verticals.size() > low && verticals.get(low).x1 > x ? low : -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-218
@@ -1,218 +0,0 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.table;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public class Rectangle extends Rectangle2D.Float {
|
||||
|
||||
protected static final float VERTICAL_COMPARISON_THRESHOLD = 0.4f;
|
||||
/**
|
||||
* Ill-defined comparator, from when Rectangle was Comparable.
|
||||
* <p>
|
||||
* see https://github.com/tabulapdf/tabula-java/issues/116
|
||||
*
|
||||
* @deprecated with no replacement
|
||||
*/
|
||||
@Deprecated
|
||||
public static final Comparator<Rectangle> ILL_DEFINED_ORDER = new Comparator<Rectangle>() {
|
||||
@Override
|
||||
public int compare(Rectangle o1, Rectangle o2) {
|
||||
|
||||
if (o1.equals(o2)) {
|
||||
return 0;
|
||||
}
|
||||
if (o1.verticalOverlap(o2) > VERTICAL_COMPARISON_THRESHOLD) {
|
||||
return o1.isLtrDominant() == -1 && o2.isLtrDominant() == -1 ? -java.lang.Double.compare(o1.getX(), o2.getX()) : java.lang.Double.compare(o1.getX(), o2.getX());
|
||||
} else {
|
||||
return java.lang.Float.compare(o1.getBottom(), o2.getBottom());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
public Rectangle() {
|
||||
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
public Rectangle(float top, float left, float width, float height) {
|
||||
|
||||
super();
|
||||
this.setRect(left, top, width, height);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param rectangles
|
||||
* @return minimum bounding box that contains all the rectangles
|
||||
*/
|
||||
public static Rectangle boundingBoxOf(List<? extends Rectangle> rectangles) {
|
||||
|
||||
float minx = java.lang.Float.MAX_VALUE;
|
||||
float miny = java.lang.Float.MAX_VALUE;
|
||||
float maxx = java.lang.Float.MIN_VALUE;
|
||||
float maxy = java.lang.Float.MIN_VALUE;
|
||||
|
||||
for (Rectangle r : rectangles) {
|
||||
minx = (float) Math.min(r.getMinX(), minx);
|
||||
miny = (float) Math.min(r.getMinY(), miny);
|
||||
maxx = (float) Math.max(r.getMaxX(), maxx);
|
||||
maxy = (float) Math.max(r.getMaxY(), maxy);
|
||||
}
|
||||
return new Rectangle(miny, minx, maxx - minx, maxy - miny);
|
||||
}
|
||||
|
||||
|
||||
public int compareTo(Rectangle other) {
|
||||
|
||||
return ILL_DEFINED_ORDER.compare(this, other);
|
||||
}
|
||||
|
||||
|
||||
// I'm bad at Java and need this for fancy sorting in
|
||||
// technology.tabula.TextChunk.
|
||||
public int isLtrDominant() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
public float getArea() {
|
||||
|
||||
return this.width * this.height;
|
||||
}
|
||||
|
||||
|
||||
public float verticalOverlap(Rectangle other) {
|
||||
|
||||
return Math.max(0, Math.min(this.getBottom(), other.getBottom()) - Math.max(this.getTop(), other.getTop()));
|
||||
}
|
||||
|
||||
|
||||
public boolean verticallyOverlaps(Rectangle other) {
|
||||
|
||||
return verticalOverlap(other) > 0;
|
||||
}
|
||||
|
||||
|
||||
public float horizontalOverlap(Rectangle other) {
|
||||
|
||||
return Math.max(0, Math.min(this.getRight(), other.getRight()) - Math.max(this.getLeft(), other.getLeft()));
|
||||
}
|
||||
|
||||
|
||||
public boolean horizontallyOverlaps(Rectangle other) {
|
||||
|
||||
return horizontalOverlap(other) > 0;
|
||||
}
|
||||
|
||||
|
||||
public float verticalOverlapRatio(Rectangle other) {
|
||||
|
||||
float rv = 0, delta = Math.min(this.getBottom() - this.getTop(), other.getBottom() - other.getTop());
|
||||
|
||||
if (other.getTop() <= this.getTop() && this.getTop() <= other.getBottom() && other.getBottom() <= this.getBottom()) {
|
||||
rv = (other.getBottom() - this.getTop()) / delta;
|
||||
} else if (this.getTop() <= other.getTop() && other.getTop() <= this.getBottom() && this.getBottom() <= other.getBottom()) {
|
||||
rv = (this.getBottom() - other.getTop()) / delta;
|
||||
} else if (this.getTop() <= other.getTop() && other.getTop() <= other.getBottom() && other.getBottom() <= this.getBottom()) {
|
||||
rv = (other.getBottom() - other.getTop()) / delta;
|
||||
} else if (other.getTop() <= this.getTop() && this.getTop() <= this.getBottom() && this.getBottom() <= other.getBottom()) {
|
||||
rv = (this.getBottom() - this.getTop()) / delta;
|
||||
}
|
||||
|
||||
return rv;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public float overlapRatio(Rectangle other) {
|
||||
|
||||
double intersectionWidth = Math.max(0, Math.min(this.getRight(), other.getRight()) - Math.max(this.getLeft(), other.getLeft()));
|
||||
double intersectionHeight = Math.max(0, Math.min(this.getBottom(), other.getBottom()) - Math.max(this.getTop(), other.getTop()));
|
||||
double intersectionArea = Math.max(0, intersectionWidth * intersectionHeight);
|
||||
double unionArea = this.getArea() + other.getArea() - intersectionArea;
|
||||
|
||||
return (float) (intersectionArea / unionArea);
|
||||
}
|
||||
|
||||
|
||||
public Rectangle merge(Rectangle other) {
|
||||
|
||||
this.setRect(this.createUnion(other));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public float getTop() {
|
||||
|
||||
return (float) this.getMinY();
|
||||
}
|
||||
|
||||
|
||||
public void setTop(float top) {
|
||||
|
||||
float deltaHeight = top - this.y;
|
||||
this.setRect(this.x, top, this.width, this.height - deltaHeight);
|
||||
}
|
||||
|
||||
|
||||
public float getRight() {
|
||||
|
||||
return (float) this.getMaxX();
|
||||
}
|
||||
|
||||
|
||||
public void setRight(float right) {
|
||||
|
||||
this.setRect(this.x, this.y, right - this.x, this.height);
|
||||
}
|
||||
|
||||
|
||||
public float getLeft() {
|
||||
|
||||
return (float) this.getMinX();
|
||||
}
|
||||
|
||||
|
||||
public void setLeft(float left) {
|
||||
|
||||
float deltaWidth = left - this.x;
|
||||
this.setRect(left, this.y, this.width - deltaWidth, this.height);
|
||||
}
|
||||
|
||||
|
||||
public float getBottom() {
|
||||
|
||||
return (float) this.getMaxY();
|
||||
}
|
||||
|
||||
|
||||
public void setBottom(float bottom) {
|
||||
|
||||
this.setRect(this.x, this.y, this.width, bottom - this.y);
|
||||
}
|
||||
|
||||
|
||||
public Point2D[] getPoints() {
|
||||
|
||||
return new Point2D[]{new Point2D.Float(this.getLeft(), this.getTop()), new Point2D.Float(this.getRight(), this.getTop()), new Point2D.Float(this.getRight(),
|
||||
this.getBottom()), new Point2D.Float(this.getLeft(), this.getBottom())};
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String s = super.toString();
|
||||
sb.append(s.substring(0, s.length() - 1));
|
||||
sb.append(String.format(",bottom=%f,right=%f]", this.getBottom(), this.getRight()));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
+49
-162
@@ -4,16 +4,14 @@ import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Formatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.CohenSutherlandClipping;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.DoubleComparisons;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@@ -23,10 +21,24 @@ public class Ruling extends Line2D.Float {
|
||||
public static final int PERPENDICULAR_UNIT_EXPAND_AMOUNT = 2;
|
||||
public static final int COLINEAR_OR_PARALLEL_UNIT_EXPAND_AMOUNT = 2;
|
||||
|
||||
public enum Classification {
|
||||
TABLE_LINE,
|
||||
UNDERLINE,
|
||||
STRIKETROUGH,
|
||||
HEADER_SEPARATOR,
|
||||
FOOTER_SEPARATOR,
|
||||
OTHER
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private Classification classification;
|
||||
|
||||
|
||||
public Ruling(Point2D p1, Point2D p2) {
|
||||
|
||||
super(p1, p2);
|
||||
this.classification = Classification.OTHER;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,126 +72,32 @@ public class Ruling extends Line2D.Float {
|
||||
}
|
||||
|
||||
|
||||
// log(n) implementation of find_intersections
|
||||
// based on http://people.csail.mit.edu/indyk/6.838-old/handouts/lec2.pdf
|
||||
public static Map<Point2D, Ruling[]> findIntersections(List<Ruling> horizontals, List<Ruling> verticals) {
|
||||
|
||||
class SortObject {
|
||||
|
||||
protected SOType type;
|
||||
protected float position;
|
||||
protected Ruling ruling;
|
||||
|
||||
|
||||
public SortObject(SOType type, float position, Ruling ruling) {
|
||||
|
||||
this.type = type;
|
||||
this.position = position;
|
||||
this.ruling = ruling;
|
||||
}
|
||||
public void assertHorizontal() {
|
||||
|
||||
if (isHorizontal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SortObject> sos = new ArrayList<>();
|
||||
|
||||
TreeMap<Ruling, Boolean> tree = new TreeMap<>(new Comparator<Ruling>() {
|
||||
@Override
|
||||
public int compare(Ruling o1, Ruling o2) {
|
||||
|
||||
return java.lang.Double.compare(o1.getTop(), o2.getTop());
|
||||
}
|
||||
});
|
||||
|
||||
TreeMap<Point2D, Ruling[]> rv = new TreeMap<>(new Comparator<Point2D>() {
|
||||
@Override
|
||||
public int compare(Point2D o1, Point2D o2) {
|
||||
|
||||
if (o1.getY() > o2.getY()) {
|
||||
return 1;
|
||||
}
|
||||
if (o1.getY() < o2.getY()) {
|
||||
return -1;
|
||||
}
|
||||
if (o1.getX() > o2.getX()) {
|
||||
return 1;
|
||||
}
|
||||
if (o1.getX() < o2.getX()) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
for (Ruling h : horizontals) {
|
||||
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) {
|
||||
sos.add(new SortObject(SOType.VERTICAL, v.getLeft(), v));
|
||||
}
|
||||
|
||||
Collections.sort(sos, new Comparator<SortObject>() {
|
||||
@Override
|
||||
public int compare(SortObject a, SortObject b) {
|
||||
|
||||
int rv;
|
||||
if (DoubleComparisons.feq(a.position, b.position)) {
|
||||
if (a.type == SOType.VERTICAL && b.type == SOType.HLEFT) {
|
||||
rv = 1;
|
||||
} else if (a.type == SOType.VERTICAL && b.type == SOType.HRIGHT) {
|
||||
rv = -1;
|
||||
} else if (a.type == SOType.HLEFT && b.type == SOType.VERTICAL) {
|
||||
rv = -1;
|
||||
} else if (a.type == SOType.HRIGHT && b.type == SOType.VERTICAL) {
|
||||
rv = 1;
|
||||
} else {
|
||||
rv = java.lang.Double.compare(a.position, b.position);
|
||||
}
|
||||
} else {
|
||||
return java.lang.Double.compare(a.position, b.position);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
});
|
||||
|
||||
for (SortObject so : sos) {
|
||||
switch (so.type) {
|
||||
case VERTICAL:
|
||||
for (Map.Entry<Ruling, Boolean> h : tree.entrySet()) {
|
||||
try {
|
||||
Point2D i = h.getKey().intersectionPoint(so.ruling);
|
||||
if (i == null) {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case HRIGHT:
|
||||
tree.remove(so.ruling);
|
||||
break;
|
||||
case HLEFT:
|
||||
tree.put(so.ruling, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
throw new IllegalArgumentException("Ruling " + this + " is not horizontal");
|
||||
|
||||
}
|
||||
|
||||
|
||||
public boolean vertical() {
|
||||
public void assertVertical() {
|
||||
|
||||
if (isVertical()) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalArgumentException("Ruling " + this + " is not vertical");
|
||||
}
|
||||
|
||||
|
||||
public boolean isVertical() {
|
||||
|
||||
return this.length() > 0 && DoubleComparisons.feq(this.x1, this.x2); //diff < ORIENTATION_CHECK_THRESHOLD;
|
||||
}
|
||||
|
||||
|
||||
public boolean horizontal() {
|
||||
public boolean isHorizontal() {
|
||||
|
||||
return this.length() > 0 && DoubleComparisons.feq(this.y1, this.y2); //diff < ORIENTATION_CHECK_THRESHOLD;
|
||||
}
|
||||
@@ -188,36 +106,36 @@ public class Ruling extends Line2D.Float {
|
||||
// these are used to have a single collapse method (in page, currently)
|
||||
|
||||
|
||||
public boolean oblique() {
|
||||
public boolean isOblique() {
|
||||
|
||||
return !(this.vertical() || this.horizontal());
|
||||
return !(this.isVertical() || this.isHorizontal());
|
||||
}
|
||||
|
||||
|
||||
public float getPosition() {
|
||||
|
||||
if (this.oblique()) {
|
||||
if (this.isOblique()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
return this.vertical() ? this.getLeft() : this.getTop();
|
||||
return this.isVertical() ? this.getLeft() : this.getTop();
|
||||
}
|
||||
|
||||
|
||||
public float getStart() {
|
||||
|
||||
if (this.oblique()) {
|
||||
if (this.isOblique()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
return this.vertical() ? this.getTop() : this.getLeft();
|
||||
return this.isVertical() ? this.getTop() : this.getLeft();
|
||||
}
|
||||
|
||||
|
||||
public void setStart(float v) {
|
||||
|
||||
if (this.oblique()) {
|
||||
if (this.isOblique()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
if (this.vertical()) {
|
||||
if (this.isVertical()) {
|
||||
this.setTop(v);
|
||||
} else {
|
||||
this.setLeft(v);
|
||||
@@ -227,19 +145,19 @@ public class Ruling extends Line2D.Float {
|
||||
|
||||
public float getEnd() {
|
||||
|
||||
if (this.oblique()) {
|
||||
if (this.isOblique()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
return this.vertical() ? this.getBottom() : this.getRight();
|
||||
return this.isVertical() ? this.getBottom() : this.getRight();
|
||||
}
|
||||
|
||||
|
||||
public void setEnd(float v) {
|
||||
|
||||
if (this.oblique()) {
|
||||
if (this.isOblique()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
if (this.vertical()) {
|
||||
if (this.isVertical()) {
|
||||
this.setBottom(v);
|
||||
} else {
|
||||
this.setRight(v);
|
||||
@@ -249,10 +167,10 @@ public class Ruling extends Line2D.Float {
|
||||
|
||||
public void setStartEnd(float start, float end) {
|
||||
|
||||
if (this.oblique()) {
|
||||
if (this.isOblique()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
if (this.vertical()) {
|
||||
if (this.isVertical()) {
|
||||
this.setTop(start);
|
||||
this.setBottom(end);
|
||||
} else {
|
||||
@@ -264,7 +182,7 @@ public class Ruling extends Line2D.Float {
|
||||
|
||||
public boolean perpendicularTo(Ruling other) {
|
||||
|
||||
return this.vertical() == other.horizontal();
|
||||
return this.isVertical() == other.isHorizontal();
|
||||
}
|
||||
|
||||
|
||||
@@ -318,30 +236,6 @@ public class Ruling extends Line2D.Float {
|
||||
}
|
||||
|
||||
|
||||
public Point2D intersectionPoint(Ruling other) {
|
||||
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this_l.horizontal() && other_l.vertical()) {
|
||||
horizontal = this_l;
|
||||
vertical = other_l;
|
||||
} else if (this_l.vertical() && other_l.horizontal()) {
|
||||
vertical = this_l;
|
||||
horizontal = other_l;
|
||||
} else {
|
||||
log.warn("lines must be orthogonal, vertical and horizontal");
|
||||
return null;
|
||||
}
|
||||
return new Point2D.Float(vertical.getLeft(), horizontal.getTop());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
|
||||
@@ -451,16 +345,9 @@ public class Ruling extends Line2D.Float {
|
||||
|
||||
final float TOLERANCE = 1;
|
||||
return Math.abs(ruling.getX1() - x1) < TOLERANCE &&//
|
||||
Math.abs(ruling.getY1() - y1) < TOLERANCE &&//
|
||||
Math.abs(ruling.getX2() - x2) < TOLERANCE &&//
|
||||
Math.abs(ruling.getY2() - y2) < TOLERANCE;
|
||||
}
|
||||
|
||||
|
||||
private enum SOType {
|
||||
VERTICAL,
|
||||
HRIGHT,
|
||||
HLEFT
|
||||
Math.abs(ruling.getY1() - y1) < TOLERANCE &&//
|
||||
Math.abs(ruling.getX2() - x2) < TOLERANCE &&//
|
||||
Math.abs(ruling.getY2() - y2) < TOLERANCE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-17
@@ -36,14 +36,11 @@ public class TablePageBlock extends AbstractPageBlock {
|
||||
private List<Cell> cells;
|
||||
|
||||
|
||||
public TablePageBlock(List<Cell> cells, Rectangle area, int rotation) {
|
||||
public TablePageBlock(List<Cell> cells, int rotation) {
|
||||
|
||||
setToBBoxOfComponents(cells);
|
||||
this.cells = cells;
|
||||
addCells(cells);
|
||||
minX = area.getLeft();
|
||||
minY = area.getBottom();
|
||||
maxX = area.getRight();
|
||||
maxY = area.getTop();
|
||||
classification = PageBlockType.TABLE;
|
||||
this.rotation = rotation;
|
||||
}
|
||||
@@ -230,15 +227,15 @@ public class TablePageBlock extends AbstractPageBlock {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
Set<Float> uniqueX = new HashSet<>();
|
||||
Set<Float> uniqueY = new HashSet<>();
|
||||
Set<Double> uniqueX = new HashSet<>();
|
||||
Set<Double> uniqueY = new HashSet<>();
|
||||
cells.stream()
|
||||
.filter(c -> !c.getTextBlocks().isEmpty() || c.getHeight() > 3 && c.getWidth() > 3)
|
||||
.forEach(c -> {
|
||||
uniqueX.add(c.getLeft());
|
||||
uniqueX.add(c.getRight());
|
||||
uniqueY.add(c.getBottom());
|
||||
uniqueY.add(c.getTop());
|
||||
uniqueX.add(c.getPdfMinX());
|
||||
uniqueX.add(c.getPdfMaxX());
|
||||
uniqueY.add(c.getPdfMinY());
|
||||
uniqueY.add(c.getPdfMaxY());
|
||||
});
|
||||
|
||||
var sortedUniqueX = uniqueX.stream()
|
||||
@@ -250,22 +247,24 @@ public class TablePageBlock extends AbstractPageBlock {
|
||||
|
||||
List<List<Cell>> rowsOfCells = new ArrayList<>();
|
||||
|
||||
Float prevY = null;
|
||||
Double prevY = null;
|
||||
|
||||
for (Float y : sortedUniqueY) {
|
||||
for (Double y : sortedUniqueY) {
|
||||
|
||||
List<Cell> row = new ArrayList<>();
|
||||
|
||||
Float prevX = null;
|
||||
for (Float x : sortedUniqueX) {
|
||||
Double prevX = null;
|
||||
for (Double x : sortedUniqueX) {
|
||||
|
||||
if (prevY != null && prevX != null) {
|
||||
var cellFromGridStructure = new Cell(new Point2D.Float(prevX, prevY), new Point2D.Float(x, y));
|
||||
var cellFromGridStructure = new Cell(new Point2D.Double(prevX, prevY), new Point2D.Double(x, y));
|
||||
|
||||
if (cellFromGridStructure.hasMinimumSize()) {
|
||||
|
||||
cells.stream()
|
||||
.map(originalCell -> new CellWithIntersection(originalCell, RectangleTransformations.calculateIntersectedArea(cellFromGridStructure, originalCell)))
|
||||
.map(originalCell -> new CellWithIntersection(originalCell,
|
||||
RectangleTransformations.calculateIntersectedArea(cellFromGridStructure.getBBoxInitialUserSpace(),
|
||||
originalCell.getBBoxInitialUserSpace())))
|
||||
.filter(cellWithIntersection -> cellWithIntersection.intersectedArea > 0)
|
||||
.filter(cellWithIntersection -> cellWithIntersection.originalCell.getArea() > cellWithIntersection.intersectedArea * CELL_AREA_CONTAINED_THRESHOLD)
|
||||
.max(Comparator.comparing(CellWithIntersection::intersectedArea))
|
||||
|
||||
+46
-12
@@ -1,8 +1,12 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.text;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -14,9 +18,11 @@ import lombok.SneakyThrows;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RedTextPosition {
|
||||
public class RedTextPosition extends BoundingBox {
|
||||
|
||||
private float[] position;
|
||||
public final static int HEIGHT_PADDING = 2;
|
||||
|
||||
private Rectangle2D.Float bBoxDirAdj; // adjusted to text rotation
|
||||
|
||||
@JsonIgnore
|
||||
private int rotation;
|
||||
@@ -58,43 +64,71 @@ public class RedTextPosition {
|
||||
pos.setFontSizeInPt(textPosition.getFontSizeInPt());
|
||||
pos.setFontName(textPosition.getFont().getName());
|
||||
|
||||
var position = new float[4];
|
||||
//TODO: There is a mismatch in the java coords of the text and the rulings,
|
||||
// I guess if we start with the initial user space positions and transform them the same way we do the rulings it would work.
|
||||
pos.setBBox(new Rectangle2D.Float(textPosition.getX(), textPosition.getY(), textPosition.getWidthDirAdj(), textPosition.getHeight()));
|
||||
|
||||
position[0] = textPosition.getXDirAdj();
|
||||
position[1] = textPosition.getYDirAdj();
|
||||
position[2] = textPosition.getWidthDirAdj();
|
||||
position[3] = textPosition.getHeightDir();
|
||||
float textHeight = textPosition.getHeight() + 2 * HEIGHT_PADDING;
|
||||
Rectangle2D.Float dirAdjPosition = new Rectangle2D.Float(textPosition.getXDirAdj(),
|
||||
textPosition.getYDirAdj() - textHeight,
|
||||
textPosition.getWidthDirAdj(),
|
||||
textHeight + HEIGHT_PADDING);
|
||||
pos.setBBoxDirAdj(dirAdjPosition);
|
||||
|
||||
AffineTransform affineTransform = getRotationMatrix(TextDirection.fromDegrees(textPosition.getDir()), textPosition.getPageWidth(), textPosition.getPageHeight());
|
||||
Rectangle2D bBoxInitialUserSpace = affineTransform.createTransformedShape(dirAdjPosition).getBounds2D();
|
||||
|
||||
pos.setBBoxInitialUserSpace(bBoxInitialUserSpace); // These are definitely correct
|
||||
|
||||
pos.setPosition(position);
|
||||
return pos;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static AffineTransform getRotationMatrix(TextDirection textDirection, float pageWidth, float pageHeight) {
|
||||
|
||||
AffineTransform transform = new AffineTransform();
|
||||
|
||||
if (textDirection == TextDirection.ZERO || textDirection == TextDirection.HALF_CIRCLE) {
|
||||
transform.rotate(textDirection.getRadians(), pageWidth / 2f, pageHeight / 2f);
|
||||
transform.translate(0f, pageHeight);
|
||||
} else if (textDirection == TextDirection.QUARTER_CIRCLE) {
|
||||
transform.rotate(textDirection.getRadians(), pageWidth / 2f, pageWidth / 2f);
|
||||
transform.translate(0f, pageWidth);
|
||||
} else {
|
||||
transform.rotate(textDirection.getRadians(), pageHeight / 2f, pageHeight / 2f);
|
||||
transform.translate(0f, pageWidth);
|
||||
}
|
||||
transform.scale(1., -1.);
|
||||
return transform;
|
||||
}
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
public float getXDirAdj() {
|
||||
|
||||
return position[0];
|
||||
return this.bBoxDirAdj.x;
|
||||
}
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
public float getYDirAdj() {
|
||||
|
||||
return position[1];
|
||||
return this.bBoxDirAdj.y;
|
||||
}
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
public float getWidthDirAdj() {
|
||||
|
||||
return position[2];
|
||||
return this.bBoxDirAdj.width;
|
||||
}
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
public float getHeightDir() {
|
||||
|
||||
return position[3];
|
||||
return this.bBoxDirAdj.height;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+65
-211
@@ -1,16 +1,13 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.text;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.FloatFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.DoubleComparisons;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.TextNormalizationUtilities;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -29,34 +26,31 @@ public class TextPageBlock extends AbstractPageBlock {
|
||||
@Builder.Default
|
||||
private List<TextPositionSequence> sequences = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
private int rotation;
|
||||
|
||||
@JsonIgnore
|
||||
private String mostPopularWordFont;
|
||||
|
||||
@JsonIgnore
|
||||
private String mostPopularWordStyle;
|
||||
|
||||
@JsonIgnore
|
||||
private float mostPopularWordFontSize;
|
||||
|
||||
@JsonIgnore
|
||||
private float mostPopularWordHeight;
|
||||
|
||||
@JsonIgnore
|
||||
private float mostPopularWordSpaceWidth;
|
||||
|
||||
@JsonIgnore
|
||||
private float highestFontSize;
|
||||
|
||||
@JsonIgnore
|
||||
private PageBlockType classification;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean toDuplicate;
|
||||
|
||||
|
||||
public TextPageBlock(List<TextPositionSequence> sequences) {
|
||||
|
||||
this.sequences = sequences;
|
||||
calculateFrequencyCounters();
|
||||
calculateBBox();
|
||||
}
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
public TextDirection getDir() {
|
||||
|
||||
@@ -65,30 +59,59 @@ public class TextPageBlock extends AbstractPageBlock {
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
private float getPageHeight() {
|
||||
public float getPageHeight() {
|
||||
|
||||
return sequences.get(0).getPageHeight();
|
||||
}
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
private float getPageWidth() {
|
||||
public float getPageWidth() {
|
||||
|
||||
return sequences.get(0).getPageWidth();
|
||||
}
|
||||
|
||||
|
||||
public static TextPageBlock merge(List<TextPageBlock> textBlocksToMerge) {
|
||||
private void calculateBBox() {
|
||||
|
||||
List<TextPositionSequence> sequences = textBlocksToMerge.stream().map(TextPageBlock::getSequences).flatMap(java.util.Collection::stream).toList();
|
||||
sequences = new ArrayList<>(sequences);
|
||||
return fromTextPositionSequences(sequences);
|
||||
if (sequences == null) {
|
||||
this.bBox = new Rectangle2D.Double();
|
||||
this.bBoxInitialUserSpace = new Rectangle2D.Double();
|
||||
return;
|
||||
}
|
||||
setToBBoxOfComponents(sequences);
|
||||
}
|
||||
|
||||
|
||||
public static TextPageBlock fromTextPositionSequences(List<TextPositionSequence> wordBlockList) {
|
||||
public void recalculateBBox() {
|
||||
|
||||
TextPageBlock textBlock = null;
|
||||
calculateBBox();
|
||||
}
|
||||
|
||||
|
||||
public static TextPageBlock merge(List<TextPageBlock> textBlocksToMerge) {
|
||||
|
||||
if (textBlocksToMerge.isEmpty()) {
|
||||
throw new IllegalArgumentException("Need to provide at least one TextPageBlock.");
|
||||
}
|
||||
if (textBlocksToMerge.stream()
|
||||
.map(AbstractPageBlock::getPage)
|
||||
.distinct()
|
||||
.count() != 1) {
|
||||
throw new IllegalArgumentException("Cannot merge textBlocks on different pages.");
|
||||
}
|
||||
|
||||
List<TextPositionSequence> sequences = textBlocksToMerge.stream()
|
||||
.map(TextPageBlock::getSequences)
|
||||
.flatMap(java.util.Collection::stream)
|
||||
.toList();
|
||||
sequences = new ArrayList<>(sequences);
|
||||
|
||||
return new TextPageBlock(sequences);
|
||||
}
|
||||
|
||||
|
||||
private void calculateFrequencyCounters() {
|
||||
|
||||
FloatFrequencyCounter lineHeightFrequencyCounter = new FloatFrequencyCounter();
|
||||
FloatFrequencyCounter fontSizeFrequencyCounter = new FloatFrequencyCounter();
|
||||
@@ -96,7 +119,7 @@ public class TextPageBlock extends AbstractPageBlock {
|
||||
StringFrequencyCounter fontFrequencyCounter = new StringFrequencyCounter();
|
||||
StringFrequencyCounter styleFrequencyCounter = new StringFrequencyCounter();
|
||||
|
||||
for (TextPositionSequence wordBlock : wordBlockList) {
|
||||
for (TextPositionSequence wordBlock : sequences) {
|
||||
|
||||
lineHeightFrequencyCounter.add(wordBlock.getTextHeight());
|
||||
fontSizeFrequencyCounter.add(wordBlock.getFontSize());
|
||||
@@ -104,160 +127,23 @@ public class TextPageBlock extends AbstractPageBlock {
|
||||
fontFrequencyCounter.add(wordBlock.getFont());
|
||||
styleFrequencyCounter.add(wordBlock.getFontStyle());
|
||||
|
||||
if (textBlock == null) {
|
||||
textBlock = new TextPageBlock(wordBlock.getMinXDirAdj(),
|
||||
wordBlock.getMaxXDirAdj(),
|
||||
wordBlock.getMinYDirAdj(),
|
||||
wordBlock.getMaxYDirAdj(),
|
||||
wordBlockList,
|
||||
wordBlock.getRotation());
|
||||
} else {
|
||||
TextPageBlock spatialEntity = textBlock.union(wordBlock);
|
||||
textBlock.resize(spatialEntity.getMinX(), spatialEntity.getMinY(), spatialEntity.getWidth(), spatialEntity.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
if (textBlock != null) {
|
||||
textBlock.setMostPopularWordFont(fontFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordStyle(styleFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordFontSize(fontSizeFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordHeight(lineHeightFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordSpaceWidth(spaceFrequencyCounter.getMostPopular());
|
||||
textBlock.setHighestFontSize(fontSizeFrequencyCounter.getHighest());
|
||||
}
|
||||
|
||||
if (textBlock != null && textBlock.getSequences() != null && textBlock.getSequences()
|
||||
.stream()
|
||||
.map(t -> DoubleComparisons.round(t.getMinYDirAdj(), 3))
|
||||
.collect(toSet())
|
||||
.size() == 1) {
|
||||
textBlock.getSequences().sort(Comparator.comparing(TextPositionSequence::getMinXDirAdj));
|
||||
}
|
||||
return textBlock;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the minX value in pdf coordinate system.
|
||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
||||
* 0 -> LowerLeft
|
||||
* 90 -> UpperLeft
|
||||
* 180 -> UpperRight
|
||||
* 270 -> LowerRight
|
||||
*
|
||||
* @return the minX value in pdf coordinate system
|
||||
*/
|
||||
@JsonIgnore
|
||||
public float getPdfMinX() {
|
||||
|
||||
if (getDir().getDegrees() == 90) {
|
||||
return minY;
|
||||
} else if (getDir().getDegrees() == 180) {
|
||||
return getPageWidth() - maxX;
|
||||
|
||||
} else if (getDir().getDegrees() == 270) {
|
||||
|
||||
return getPageWidth() - maxY;
|
||||
} else {
|
||||
return minX;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the maxX value in pdf coordinate system.
|
||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
||||
* 0 -> LowerLeft
|
||||
* 90 -> UpperLeft
|
||||
* 180 -> UpperRight
|
||||
* 270 -> LowerRight
|
||||
*
|
||||
* @return the maxX value in pdf coordinate system
|
||||
*/
|
||||
@JsonIgnore
|
||||
public float getPdfMaxX() {
|
||||
|
||||
if (getDir().getDegrees() == 90) {
|
||||
return maxY;
|
||||
} else if (getDir().getDegrees() == 180) {
|
||||
return getPageWidth() - minX;
|
||||
} else if (getDir().getDegrees() == 270) {
|
||||
return getPageWidth() - minY;
|
||||
|
||||
} else {
|
||||
return maxX;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the minY value in pdf coordinate system.
|
||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
||||
* 0 -> LowerLeft
|
||||
* 90 -> UpperLeft
|
||||
* 180 -> UpperRight
|
||||
* 270 -> LowerRight
|
||||
*
|
||||
* @return the minY value in pdf coordinate system
|
||||
*/
|
||||
@JsonIgnore
|
||||
public float getPdfMinY() {
|
||||
|
||||
if (getDir().getDegrees() == 90) {
|
||||
return minX;
|
||||
} else if (getDir().getDegrees() == 180) {
|
||||
return maxY;
|
||||
|
||||
} else if (getDir().getDegrees() == 270) {
|
||||
return getPageHeight() - maxX;
|
||||
|
||||
} else {
|
||||
return getPageHeight() - maxY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the maxY value in pdf coordinate system.
|
||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
||||
* 0 -> LowerLeft
|
||||
* 90 -> UpperLeft
|
||||
* 180 -> UpperRight
|
||||
* 270 -> LowerRight
|
||||
*
|
||||
* @return the maxY value in pdf coordinate system
|
||||
*/
|
||||
@JsonIgnore
|
||||
public float getPdfMaxY() {
|
||||
|
||||
if (getDir().getDegrees() == 90) {
|
||||
return maxX;
|
||||
} else if (getDir().getDegrees() == 180) {
|
||||
|
||||
return minY;
|
||||
} else if (getDir().getDegrees() == 270) {
|
||||
return getPageHeight() - minX;
|
||||
} else {
|
||||
return getPageHeight() - minY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public TextPageBlock(float minX, float maxX, float minY, float maxY, List<TextPositionSequence> sequences, int rotation) {
|
||||
|
||||
this.minX = minX;
|
||||
this.maxX = maxX;
|
||||
this.minY = minY;
|
||||
this.maxY = maxY;
|
||||
this.sequences = sequences;
|
||||
this.rotation = rotation;
|
||||
setMostPopularWordFont(fontFrequencyCounter.getMostPopular());
|
||||
setMostPopularWordStyle(styleFrequencyCounter.getMostPopular());
|
||||
setMostPopularWordFontSize(fontSizeFrequencyCounter.getMostPopular());
|
||||
setMostPopularWordHeight(lineHeightFrequencyCounter.getMostPopular());
|
||||
setMostPopularWordSpaceWidth(spaceFrequencyCounter.getMostPopular());
|
||||
setHighestFontSize(fontSizeFrequencyCounter.getHighest());
|
||||
}
|
||||
|
||||
|
||||
public TextPageBlock union(TextPositionSequence r) {
|
||||
|
||||
TextPageBlock union = this.copy();
|
||||
union.add(r);
|
||||
union.getSequences().add(r);
|
||||
calculateFrequencyCounters();
|
||||
calculateBBox();
|
||||
return union;
|
||||
}
|
||||
|
||||
@@ -265,64 +151,32 @@ public class TextPageBlock extends AbstractPageBlock {
|
||||
public TextPageBlock union(TextPageBlock r) {
|
||||
|
||||
TextPageBlock union = this.copy();
|
||||
union.add(r);
|
||||
union.getSequences().addAll(r.getSequences());
|
||||
calculateFrequencyCounters();
|
||||
calculateBBox();
|
||||
return union;
|
||||
}
|
||||
|
||||
|
||||
public void add(TextPageBlock r) {
|
||||
|
||||
if (r.getMinX() < minX) {
|
||||
minX = r.getMinX();
|
||||
}
|
||||
if (r.getMaxX() > maxX) {
|
||||
maxX = r.getMaxX();
|
||||
}
|
||||
if (r.getMinY() < minY) {
|
||||
minY = r.getMinY();
|
||||
}
|
||||
if (r.getMaxY() > maxY) {
|
||||
maxY = r.getMaxY();
|
||||
}
|
||||
sequences.addAll(r.getSequences());
|
||||
calculateFrequencyCounters();
|
||||
calculateBBox();
|
||||
}
|
||||
|
||||
|
||||
public void add(TextPositionSequence r) {
|
||||
|
||||
if (r.getMinXDirAdj() < minX) {
|
||||
minX = r.getMinXDirAdj();
|
||||
}
|
||||
if (r.getMaxXDirAdj() > maxX) {
|
||||
maxX = r.getMaxXDirAdj();
|
||||
}
|
||||
if (r.getMinYDirAdj() < minY) {
|
||||
minY = r.getMinYDirAdj();
|
||||
}
|
||||
if (r.getMaxYDirAdj() > maxY) {
|
||||
maxY = r.getMaxYDirAdj();
|
||||
}
|
||||
sequences.add(r);
|
||||
calculateFrequencyCounters();
|
||||
calculateBBox();
|
||||
}
|
||||
|
||||
|
||||
public TextPageBlock copy() {
|
||||
|
||||
return new TextPageBlock(minX, maxX, minY, maxY, sequences, rotation);
|
||||
}
|
||||
|
||||
|
||||
public void resize(float x1, float y1, float width, float height) {
|
||||
|
||||
set(x1, y1, x1 + width, y1 + height);
|
||||
}
|
||||
|
||||
|
||||
public void set(float x1, float y1, float x2, float y2) {
|
||||
|
||||
this.minX = Math.min(x1, x2);
|
||||
this.maxX = Math.max(x1, x2);
|
||||
this.minY = Math.min(y1, y2);
|
||||
this.maxY = Math.max(y1, y2);
|
||||
return new TextPageBlock(new ArrayList<>(sequences));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+33
-80
@@ -1,7 +1,6 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.text;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -9,15 +8,14 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@@ -25,8 +23,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class TextPositionSequence implements CharSequence {
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class TextPositionSequence extends BoundingBox implements CharSequence {
|
||||
|
||||
public static final int HEIGHT_PADDING = 2;
|
||||
|
||||
@@ -36,29 +34,38 @@ public class TextPositionSequence implements CharSequence {
|
||||
@EqualsAndHashCode.Include
|
||||
private List<RedTextPosition> textPositions = new ArrayList<>();
|
||||
|
||||
private Rectangle2D bBoxDirAdj;
|
||||
@EqualsAndHashCode.Include
|
||||
private TextDirection dir;
|
||||
private int rotation;
|
||||
private float pageHeight;
|
||||
private float pageWidth;
|
||||
private boolean isParagraphStart;
|
||||
private boolean strikethrough;
|
||||
private boolean underline;
|
||||
|
||||
|
||||
public TextPositionSequence(int page) {
|
||||
public TextPositionSequence(List<TextPosition> textPositions, int pageNumber, boolean isParagraphStart) {
|
||||
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
|
||||
public TextPositionSequence(List<TextPosition> textPositions, int page, boolean isParagraphStart) {
|
||||
|
||||
this.textPositions = textPositions.stream().map(RedTextPosition::fromTextPosition).collect(Collectors.toList());
|
||||
this.page = page;
|
||||
this.textPositions = textPositions.stream()
|
||||
.map(RedTextPosition::fromTextPosition)
|
||||
.collect(Collectors.toList());
|
||||
this.page = pageNumber;
|
||||
this.dir = TextDirection.fromDegrees(textPositions.get(0).getDir());
|
||||
this.rotation = textPositions.get(0).getRotation();
|
||||
this.pageHeight = textPositions.get(0).getPageHeight();
|
||||
this.pageWidth = textPositions.get(0).getPageWidth();
|
||||
this.isParagraphStart = isParagraphStart;
|
||||
calculateBBox();
|
||||
}
|
||||
|
||||
|
||||
private void calculateBBox() {
|
||||
|
||||
this.bBoxDirAdj = textPositions.stream()
|
||||
.map(RedTextPosition::getBBoxDirAdj)
|
||||
.collect(RectangleTransformations.collectBBox());
|
||||
setToBBoxOfComponents(getTextPositions());
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +77,7 @@ public class TextPositionSequence implements CharSequence {
|
||||
this.rotation = textPositions.get(0).getRotation();
|
||||
this.pageHeight = textPositions.get(0).getPageHeight();
|
||||
this.pageWidth = textPositions.get(0).getPageWidth();
|
||||
calculateBBox();
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +115,7 @@ public class TextPositionSequence implements CharSequence {
|
||||
textPositionSequence.rotation = rotation;
|
||||
textPositionSequence.pageHeight = pageHeight;
|
||||
textPositionSequence.pageWidth = pageWidth;
|
||||
|
||||
textPositionSequence.setToBBoxOfComponents(getTextPositions());
|
||||
return textPositionSequence;
|
||||
}
|
||||
|
||||
@@ -137,18 +145,18 @@ public class TextPositionSequence implements CharSequence {
|
||||
this.rotation = textPositionSequence.getRotation();
|
||||
this.pageHeight = textPositionSequence.getPageHeight();
|
||||
this.pageWidth = textPositionSequence.getPageWidth();
|
||||
calculateBBox();
|
||||
}
|
||||
|
||||
|
||||
public void add(TextPosition textPosition) {
|
||||
|
||||
this.textPositions.add(RedTextPosition.fromTextPosition(textPosition));
|
||||
|
||||
this.dir = TextDirection.fromDegrees(textPositions.get(0).getDir());
|
||||
this.rotation = textPositions.get(0).getRotation();
|
||||
this.pageHeight = textPositions.get(0).getPageHeight();
|
||||
this.pageWidth = textPositions.get(0).getPageWidth();
|
||||
|
||||
calculateBBox();
|
||||
}
|
||||
|
||||
|
||||
@@ -208,24 +216,18 @@ public class TextPositionSequence implements CharSequence {
|
||||
}
|
||||
|
||||
|
||||
public float getTextHeightNoPadding() {
|
||||
|
||||
return textPositions.get(0).getHeightDir();
|
||||
}
|
||||
|
||||
|
||||
public float getTextHeight() {
|
||||
|
||||
return textPositions.get(0).getHeightDir() + HEIGHT_PADDING;
|
||||
}
|
||||
|
||||
|
||||
public float getHeight() {
|
||||
|
||||
return getMaxYDirAdj() - getMinYDirAdj();
|
||||
}
|
||||
|
||||
|
||||
public float getWidth() {
|
||||
|
||||
return getMaxXDirAdj() - getMinXDirAdj();
|
||||
}
|
||||
|
||||
|
||||
public String getFont() {
|
||||
|
||||
if (textPositions.get(0).getFontName() == null) {
|
||||
@@ -265,54 +267,5 @@ public class TextPositionSequence implements CharSequence {
|
||||
return textPositions.get(0).getWidthOfSpace();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This returns the bounding box of the word in Pdf Coordinate System where {0,0} rotated with the page rotation.
|
||||
* 0 -> LowerLeft
|
||||
* 90 -> UpperLeft
|
||||
* 180 -> UpperRight
|
||||
* 270 -> LowerRight
|
||||
*
|
||||
* @return bounding box of the word in Pdf Coordinate System
|
||||
*/
|
||||
|
||||
@SneakyThrows
|
||||
public Rectangle getRectangle() {
|
||||
|
||||
log.debug("Page: '{}', Word: '{}', Rotation: '{}', textRotation {}", page, this, rotation, dir);
|
||||
|
||||
float textHeight = getTextHeight();
|
||||
|
||||
RedTextPosition firstTextPos = textPositions.get(0);
|
||||
RedTextPosition lastTextPos = textPositions.get(textPositions.size() - 1);
|
||||
|
||||
Point2D bottomLeft = new Point2D.Double(firstTextPos.getXDirAdj(), firstTextPos.getYDirAdj() - HEIGHT_PADDING);
|
||||
Point2D topRight = new Point2D.Double(lastTextPos.getXDirAdj() + lastTextPos.getWidthDirAdj(), lastTextPos.getYDirAdj() + textHeight + HEIGHT_PADDING);
|
||||
|
||||
AffineTransform transform = new AffineTransform();
|
||||
if (dir == TextDirection.ZERO || dir == TextDirection.HALF_CIRCLE) {
|
||||
transform.rotate(dir.getRadians(), pageWidth / 2f, pageHeight / 2f);
|
||||
transform.translate(0f, pageHeight + textHeight);
|
||||
transform.scale(1., -1.);
|
||||
} else if (dir == TextDirection.QUARTER_CIRCLE) {
|
||||
transform.rotate(dir.getRadians(), pageWidth / 2f, pageWidth / 2f);
|
||||
transform.translate(0f, pageWidth + textHeight);
|
||||
transform.scale(1., -1.);
|
||||
} else {
|
||||
transform.rotate(dir.getRadians(), pageHeight / 2f, pageHeight / 2f);
|
||||
transform.translate(0f, pageWidth + textHeight);
|
||||
transform.scale(1., -1.);
|
||||
}
|
||||
|
||||
bottomLeft = transform.transform(bottomLeft, null);
|
||||
topRight = transform.transform(topRight, null);
|
||||
|
||||
return new Rectangle( //
|
||||
new Point((float) bottomLeft.getX(), (float) bottomLeft.getY()),
|
||||
(float) (topRight.getX() - bottomLeft.getX()),
|
||||
(float) (topRight.getY() - bottomLeft.getY()),
|
||||
page);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -9,10 +9,11 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.image.ImageServiceResponse;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.ImageType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.image.ImageServiceResponse;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -20,8 +21,7 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public class ImageServiceResponseAdapter {
|
||||
|
||||
|
||||
public Map<Integer, List<ClassifiedImage>> buildClassifiedImagesPerPage(ImageServiceResponse imageServiceResponse ) {
|
||||
public Map<Integer, List<ClassifiedImage>> buildClassifiedImagesPerPage(ImageServiceResponse imageServiceResponse) {
|
||||
|
||||
Map<Integer, List<ClassifiedImage>> images = new HashMap<>();
|
||||
imageServiceResponse.getData().forEach(imageMetadata -> {
|
||||
@@ -55,11 +55,12 @@ public class ImageServiceResponseAdapter {
|
||||
|
||||
classificationPage.getImages().forEach(image -> {
|
||||
if (image.getImageType().equals(ImageType.OTHER)) {
|
||||
classificationPage.getTextBlocks().forEach(textblock -> {
|
||||
if (image.getPosition().contains(textblock.getMinX(), textblock.getMinY(), textblock.getWidth(), textblock.getHeight())) {
|
||||
for (AbstractPageBlock textblock : classificationPage.getTextBlocks()) {
|
||||
if (image.getPosition().contains(textblock.getBBoxInitialUserSpace())) {
|
||||
image.setImageType(ImageType.OCR);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+30
-24
@@ -31,8 +31,9 @@ public class BodyTextFrameService {
|
||||
Rectangle bodyTextFrame = calculateBodyTextFrame(classificationDocument.getPages(), classificationDocument.getFontSizeCounter(), false, layoutParsingType);
|
||||
Rectangle landscapeBodyTextFrame = calculateBodyTextFrame(classificationDocument.getPages(), classificationDocument.getFontSizeCounter(), true, layoutParsingType);
|
||||
for (ClassificationPage page : classificationDocument.getPages()) {
|
||||
// var updatedBodyTextFrame = getBodyTextFrameFromRulings(page, bodyTextFrame, landscapeBodyTextFrame);
|
||||
var updatedBodyTextFrame = getBodyTextFrameFromRulings(page, bodyTextFrame, landscapeBodyTextFrame);
|
||||
setBodyTextFrameAdjustedToPage(page, bodyTextFrame, landscapeBodyTextFrame);
|
||||
classificationDocument.getVisualizations().addMainBodyVisualization(page.isLandscape() ? landscapeBodyTextFrame : bodyTextFrame, page.getPageNumber());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,24 +59,26 @@ public class BodyTextFrameService {
|
||||
|
||||
private List<Ruling> getPotentialFooterRulings(ClassificationPage page) {
|
||||
|
||||
return page.getCleanRulings()
|
||||
.getHorizontal()
|
||||
return page.getCleanRulings().getHorizontals()
|
||||
.stream()
|
||||
.filter(ruling -> ruling.getClassification().equals(Ruling.Classification.OTHER))
|
||||
.filter(ruling -> ruling.getY1() < page.getPageHeight() * RULING_HEIGHT_THRESHOLD)
|
||||
.filter(ruling -> ruling.getWidth() > RULING_WIDTH_THRESHOLD * page.getPageWidth())
|
||||
.sorted(Comparator.comparingDouble(Ruling::getTop))
|
||||
.peek(ruling -> ruling.setClassification(Ruling.Classification.FOOTER_SEPARATOR))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
private List<Ruling> getPotentialHeaderRulings(ClassificationPage page) {
|
||||
|
||||
return page.getCleanRulings()
|
||||
.getHorizontal()
|
||||
return page.getCleanRulings().getHorizontals()
|
||||
.stream()
|
||||
.filter(ruling -> ruling.getClassification().equals(Ruling.Classification.OTHER))
|
||||
.filter(ruling -> ruling.getY1() > page.getPageHeight() * (1 - RULING_HEIGHT_THRESHOLD))
|
||||
.filter(ruling -> ruling.getWidth() > RULING_WIDTH_THRESHOLD * page.getPageWidth())
|
||||
.sorted(Comparator.comparingDouble(Ruling::getBottom).reversed())
|
||||
.peek(ruling -> ruling.setClassification(Ruling.Classification.HEADER_SEPARATOR))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -99,16 +102,16 @@ public class BodyTextFrameService {
|
||||
|
||||
if (page.getPageWidth() > page.getPageHeight() && page.getRotation() == 270) {
|
||||
textFrame = new Rectangle(new Point(textFrame.getTopLeft().getY(), page.getPageHeight() - textFrame.getTopLeft().getX() - textFrame.getWidth()),
|
||||
textFrame.getHeight(),
|
||||
textFrame.getWidth(),
|
||||
0);
|
||||
textFrame.getHeight(),
|
||||
textFrame.getWidth(),
|
||||
0);
|
||||
} else if (page.getPageWidth() > page.getPageHeight() && page.getRotation() != 0) {
|
||||
textFrame = new Rectangle(new Point(textFrame.getTopLeft().getY(), textFrame.getTopLeft().getX()), textFrame.getHeight(), textFrame.getWidth(), page.getPageNumber());
|
||||
} else if (page.getRotation() == 180) {
|
||||
textFrame = new Rectangle(new Point(textFrame.getTopLeft().getX(), page.getPageHeight() - textFrame.getTopLeft().getY() - textFrame.getHeight()),
|
||||
textFrame.getWidth(),
|
||||
textFrame.getHeight(),
|
||||
0);
|
||||
textFrame.getWidth(),
|
||||
textFrame.getHeight(),
|
||||
0);
|
||||
}
|
||||
page.setBodyTextFrame(textFrame);
|
||||
}
|
||||
@@ -152,14 +155,17 @@ public class BodyTextFrameService {
|
||||
}
|
||||
|
||||
if (MarkedContentUtils.intersects(textBlock, page.getMarkedContentBboxPerType(), MarkedContentUtils.HEADER) || MarkedContentUtils.intersects(textBlock,
|
||||
page.getMarkedContentBboxPerType(),
|
||||
MarkedContentUtils.FOOTER)) {
|
||||
page.getMarkedContentBboxPerType(),
|
||||
MarkedContentUtils.FOOTER)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float approxLineCount = PositionUtils.getApproxLineCount(textBlock);
|
||||
if (layoutParsingType.equals(LayoutParsingType.DOCUMINE) && approxLineCount < approximateHeaderLineCount && textBlock.getMaxY() >= page.getPageHeight() - (page.getPageHeight() / 10) || !layoutParsingType.equals(
|
||||
LayoutParsingType.DOCUMINE) && approxLineCount < approximateHeaderLineCount) {
|
||||
double approxLineCount = PositionUtils.getApproxLineCount(textBlock);
|
||||
if (layoutParsingType.equals(LayoutParsingType.DOCUMINE_OLD) //
|
||||
&& approxLineCount < approximateHeaderLineCount //
|
||||
&& textBlock.getMaxY() >= page.getPageHeight() - (page.getPageHeight() / 10)//
|
||||
|| !layoutParsingType.equals(LayoutParsingType.DOCUMINE_OLD) //
|
||||
&& approxLineCount < approximateHeaderLineCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -185,10 +191,10 @@ public class BodyTextFrameService {
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Rectangle(new Point(expansionsRectangle.minX, expansionsRectangle.minY),
|
||||
expansionsRectangle.maxX - expansionsRectangle.minX,
|
||||
expansionsRectangle.maxY - expansionsRectangle.minY,
|
||||
0);
|
||||
return new Rectangle(new Point((float) expansionsRectangle.minX, (float) expansionsRectangle.minY),
|
||||
(float) (expansionsRectangle.maxX - expansionsRectangle.minX),
|
||||
(float) (expansionsRectangle.maxY - expansionsRectangle.minY),
|
||||
0);
|
||||
}
|
||||
|
||||
|
||||
@@ -226,10 +232,10 @@ public class BodyTextFrameService {
|
||||
|
||||
private class BodyTextFrameExpansionsRectangle {
|
||||
|
||||
float minX = 10000;
|
||||
float maxX = -100;
|
||||
float minY = 10000;
|
||||
float maxY = -100;
|
||||
double minX = 10000;
|
||||
double maxX = -100;
|
||||
double minY = 10000;
|
||||
double maxY = -100;
|
||||
|
||||
}
|
||||
|
||||
|
||||
+20
-15
@@ -44,9 +44,9 @@ public class GapDetectionService {
|
||||
|
||||
if (yDifference > avgTextPositionHeight * Y_GAP_FACTOR) {
|
||||
yGapContext.addGap(mainBodyTextFrame.getMinX(),
|
||||
previousTextPositionBBox.getMaxY(),
|
||||
mainBodyTextFrame.getWidth(),
|
||||
-(previousTextPositionBBox.getMaxY() - currentTextPositionBBox.getMinY()));
|
||||
previousTextPositionBBox.getMaxY(),
|
||||
mainBodyTextFrame.getWidth(),
|
||||
-(previousTextPositionBBox.getMaxY() - currentTextPositionBBox.getMinY()));
|
||||
}
|
||||
if (yDifference > avgTextPositionHeight * NEW_LINE_FACTOR) {
|
||||
|
||||
@@ -69,32 +69,37 @@ public class GapDetectionService {
|
||||
|
||||
private static Rectangle2D toRectangle2D(TextPositionSequence textPosition) {
|
||||
|
||||
return mirrorY(RectangleTransformations.toRectangle2D(textPosition.getRectangle()));
|
||||
return mirrorY(textPosition.getBBox());
|
||||
}
|
||||
|
||||
|
||||
private static Rectangle2D mirrorY(Rectangle2D rectangle2D) {
|
||||
|
||||
return new Rectangle2D.Double(rectangle2D.getX(), Math.min(rectangle2D.getMinY(), rectangle2D.getMaxY()), rectangle2D.getWidth(), Math.abs(rectangle2D.getHeight()));
|
||||
}
|
||||
|
||||
|
||||
private static void addGapToLine(Rectangle2D currentTextPosition, Rectangle2D previousTextPosition, XGapsContext context) {
|
||||
|
||||
context.gapsInCurrentLine.add(new Rectangle2D.Double(previousTextPosition.getMaxX(),
|
||||
previousTextPosition.getMinY(),
|
||||
currentTextPosition.getMinX() - previousTextPosition.getMaxX(),
|
||||
(previousTextPosition.getHeight() + currentTextPosition.getHeight()) / 2));
|
||||
previousTextPosition.getMinY(),
|
||||
currentTextPosition.getMinX() - previousTextPosition.getMaxX(),
|
||||
(previousTextPosition.getHeight() + currentTextPosition.getHeight()) / 2));
|
||||
}
|
||||
|
||||
|
||||
private static void assertAllTextPositionsHaveSameDir(List<TextPositionSequence> textPositionSequences) {
|
||||
|
||||
assert textPositionSequences.stream().map(TextPositionSequence::getDir).allMatch(a -> a.equals(textPositionSequences.get(0).getDir()));
|
||||
assert textPositionSequences.stream()
|
||||
.map(TextPositionSequence::getDir)
|
||||
.allMatch(a -> a.equals(textPositionSequences.get(0).getDir()));
|
||||
}
|
||||
|
||||
|
||||
private static double getAvgTextPositionHeight(List<TextPositionSequence> textPositionSequences) {
|
||||
|
||||
return textPositionSequences.stream().mapToDouble(TextPositionSequence::getHeight).average().orElseThrow();
|
||||
return textPositionSequences.stream()
|
||||
.mapToDouble(TextPositionSequence::getHeight).average().orElseThrow();
|
||||
}
|
||||
|
||||
|
||||
@@ -142,9 +147,9 @@ public class GapDetectionService {
|
||||
public void addGapToRightEdgeOfMainBody(Rectangle2D textPosition) {
|
||||
|
||||
Rectangle2D leftGap = new Rectangle2D.Double(textPosition.getMaxX(),
|
||||
textPosition.getMinY(),
|
||||
mainBodyTextFrame.getMaxX() - textPosition.getMaxX(),
|
||||
textPosition.getHeight());
|
||||
textPosition.getMinY(),
|
||||
mainBodyTextFrame.getMaxX() - textPosition.getMaxX(),
|
||||
textPosition.getHeight());
|
||||
gapsInCurrentLine.add(leftGap);
|
||||
}
|
||||
|
||||
@@ -152,9 +157,9 @@ public class GapDetectionService {
|
||||
public void addGapFromLeftEdgeOfMainBody(Rectangle2D textPosition) {
|
||||
|
||||
Rectangle2D leftGap = new Rectangle2D.Double(mainBodyTextFrame.getMinX(),
|
||||
textPosition.getMinY(),
|
||||
textPosition.getMinX() - mainBodyTextFrame.getMinX(),
|
||||
textPosition.getHeight());
|
||||
textPosition.getMinY(),
|
||||
textPosition.getMinX() - mainBodyTextFrame.getMinX(),
|
||||
textPosition.getHeight());
|
||||
gapsInCurrentLine.add(leftGap);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@ public class LineDetectionService {
|
||||
|
||||
private Rectangle2D textPositionBBox(List<TextPositionSequence> textPositionSequences) {
|
||||
|
||||
return RectangleTransformations.rectangleBBox(textPositionSequences.stream().map(TextPositionSequence::getRectangle).toList());
|
||||
return RectangleTransformations.rectangle2DBBox(textPositionSequences.stream().map(TextPositionSequence::getBBox).toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
+48
-45
@@ -3,6 +3,7 @@ package com.knecon.fforesight.service.layoutparser.processor.services;
|
||||
import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.X_FIRST_RULING_COMPARATOR;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -12,9 +13,9 @@ 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.RectangleTransformations;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.UnionFind;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -31,7 +32,7 @@ public class RulingCleaningService {
|
||||
private static final float THRESHOLD_Y_HORIZONTAL = 3;
|
||||
|
||||
|
||||
public CleanRulings getCleanRulings(List<TableCells> tableCells, List<Ruling> rulings) {
|
||||
public CleanRulings deduplicateAndStraightenRulings(List<TableCells> tableCells, List<Ruling> rulings) {
|
||||
|
||||
Rulings verticalAndHorizontalRulingLines;
|
||||
|
||||
@@ -45,43 +46,45 @@ public class RulingCleaningService {
|
||||
verticalAndHorizontalRulingLines.horizontalLines.sort(X_FIRST_RULING_COMPARATOR);
|
||||
verticalAndHorizontalRulingLines = cleanRulings(verticalAndHorizontalRulingLines);
|
||||
|
||||
return CleanRulings.builder().vertical(verticalAndHorizontalRulingLines.verticalLines()).horizontal(verticalAndHorizontalRulingLines.horizontalLines()).build();
|
||||
return new CleanRulings(verticalAndHorizontalRulingLines.horizontalLines(), verticalAndHorizontalRulingLines.verticalLines());
|
||||
}
|
||||
|
||||
|
||||
private Rulings cleanRulings(Rulings rulings) {
|
||||
|
||||
List<List<Rectangle>> groupedOverlappingVerticalRectangles = groupOverlappingRectangles(rulings.verticalLines.stream()
|
||||
.map(RulingCleaningService::getOverlapRectangle)
|
||||
.distinct()
|
||||
.toList());
|
||||
List<Ruling> cleanedVerticalRulings = groupedOverlappingVerticalRectangles.stream()
|
||||
.map(rectList -> getXCenteredRuling(Rectangle.boundingBoxOf(rectList)))
|
||||
.toList();
|
||||
|
||||
List<List<Rectangle>> groupedOverlappingHorizontalRectangles = groupOverlappingRectangles(rulings.horizontalLines.stream()
|
||||
List<List<Rectangle2D>> groupedOverlappingVerticalRectangles = groupOverlappingRectangles(rulings.verticalLines.stream()
|
||||
.map(RulingCleaningService::getOverlapRectangle)
|
||||
.distinct()
|
||||
.toList());
|
||||
List<Ruling> cleanedVerticalRulings = groupedOverlappingVerticalRectangles.stream()
|
||||
.map(rectList -> getXCenteredRuling(RectangleTransformations.rectangle2DBBox(rectList)))
|
||||
.filter(ruling -> ruling.length() > 0)
|
||||
.toList();
|
||||
|
||||
List<List<Rectangle2D>> groupedOverlappingHorizontalRectangles = groupOverlappingRectangles(rulings.horizontalLines.stream()
|
||||
.map(RulingCleaningService::getOverlapRectangle)
|
||||
.distinct()
|
||||
.toList());
|
||||
|
||||
List<Ruling> cleanedHorizontalRulings = groupedOverlappingHorizontalRectangles.stream()
|
||||
.map(rectList -> getYCenteredRuling(Rectangle.boundingBoxOf(rectList)))
|
||||
.map(rectList -> getYCenteredRuling(RectangleTransformations.rectangle2DBBox(rectList)))
|
||||
.filter(ruling -> ruling.length() > 0)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new Rulings(cleanedVerticalRulings, cleanedHorizontalRulings);
|
||||
}
|
||||
|
||||
|
||||
private List<List<Rectangle>> groupOverlappingRectangles(List<Rectangle> rectangles) {
|
||||
private List<List<Rectangle2D>> groupOverlappingRectangles(List<Rectangle2D> rectangles) {
|
||||
|
||||
UnionFind<Rectangle> unionFind = new UnionFind<>();
|
||||
UnionFind<Rectangle2D> 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);
|
||||
Rectangle2D rectangle1 = rectangles.get(i);
|
||||
Rectangle2D 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()) {
|
||||
if (rectangle1.getMaxX() < rectangle2.getMinX() && rectangle1.getMaxY() < rectangle2.getMinY()) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -91,66 +94,66 @@ public class RulingCleaningService {
|
||||
}
|
||||
}
|
||||
|
||||
Map<Rectangle, List<Rectangle>> groups = new HashMap<>();
|
||||
for (Rectangle rectangle : rectangles) {
|
||||
Rectangle root = unionFind.find(rectangle);
|
||||
Map<Rectangle2D, List<Rectangle2D>> groups = new HashMap<>();
|
||||
for (Rectangle2D rectangle : rectangles) {
|
||||
Rectangle2D root = unionFind.find(rectangle);
|
||||
groups.computeIfAbsent(root, k -> new ArrayList<>()).add(rectangle);
|
||||
}
|
||||
return new ArrayList<>(groups.values());
|
||||
}
|
||||
|
||||
|
||||
private static Rectangle getOverlapRectangle(Ruling ruling) {
|
||||
private static Rectangle2D getOverlapRectangle(Ruling ruling) {
|
||||
|
||||
float top;
|
||||
float left;
|
||||
float y;
|
||||
float x;
|
||||
float w;
|
||||
float h;
|
||||
|
||||
if (ruling.x1 < ruling.x2) {
|
||||
left = ruling.x1;
|
||||
x = ruling.x1;
|
||||
w = ruling.x2 - ruling.x1;
|
||||
} else {
|
||||
left = ruling.x2;
|
||||
x = ruling.x2;
|
||||
w = ruling.x1 - ruling.x2;
|
||||
}
|
||||
if (ruling.y1 < ruling.y2) {
|
||||
top = ruling.y1;
|
||||
y = ruling.y1;
|
||||
h = ruling.y2 - ruling.y1;
|
||||
} else {
|
||||
top = ruling.y2;
|
||||
y = 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);
|
||||
if (ruling.isHorizontal()) {
|
||||
return new Rectangle2D.Double(x - THRESHOLD_X_HORIZONTAL, y - THRESHOLD_Y_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);
|
||||
return new Rectangle2D.Double(x - THRESHOLD_X_VERTICAL, y - THRESHOLD_Y_VERTICAL, w + 2 * THRESHOLD_X_VERTICAL, h + 2 * THRESHOLD_Y_VERTICAL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Ruling getXCenteredRuling(Rectangle rectangle) {
|
||||
public static Ruling getXCenteredRuling(Rectangle2D rectangle) {
|
||||
|
||||
float x = (float) rectangle.getCenterX();
|
||||
float y1 = rectangle.getTop();
|
||||
float y2 = rectangle.getBottom();
|
||||
double x = rectangle.getCenterX();
|
||||
double y1 = rectangle.getMinY();
|
||||
double y2 = rectangle.getMaxY();
|
||||
|
||||
Point2D point1 = new Point2D.Float(x, y1 + THRESHOLD_Y_VERTICAL);
|
||||
Point2D point2 = new Point2D.Float(x, y2 - THRESHOLD_Y_VERTICAL);
|
||||
Point2D point1 = new Point2D.Double(x, y1 + THRESHOLD_Y_VERTICAL);
|
||||
Point2D point2 = new Point2D.Double(x, y2 - THRESHOLD_Y_VERTICAL);
|
||||
|
||||
return new Ruling(point1, point2);
|
||||
}
|
||||
|
||||
|
||||
public static Ruling getYCenteredRuling(Rectangle rectangle) {
|
||||
public static Ruling getYCenteredRuling(Rectangle2D rectangle) {
|
||||
|
||||
float x1 = rectangle.getLeft();
|
||||
float x2 = rectangle.getRight();
|
||||
float y = (float) rectangle.getCenterY();
|
||||
double x1 = rectangle.getX();
|
||||
double x2 = rectangle.getMaxX();
|
||||
double y = rectangle.getCenterY();
|
||||
|
||||
Point2D point1 = new Point2D.Float(x1 + THRESHOLD_X_HORIZONTAL, y);
|
||||
Point2D point2 = new Point2D.Float(x2 - THRESHOLD_X_HORIZONTAL, y);
|
||||
Point2D point1 = new Point2D.Double(x1 + THRESHOLD_X_HORIZONTAL, y);
|
||||
Point2D point2 = new Point2D.Double(x2 - THRESHOLD_X_HORIZONTAL, y);
|
||||
|
||||
return new Ruling(point1, point2);
|
||||
}
|
||||
@@ -160,14 +163,14 @@ public class RulingCleaningService {
|
||||
|
||||
List<Ruling> vrs = new ArrayList<>();
|
||||
for (Ruling vr : rulings) {
|
||||
if (vr.vertical()) {
|
||||
if (vr.isVertical()) {
|
||||
vrs.add(vr);
|
||||
}
|
||||
}
|
||||
|
||||
List<Ruling> hrs = new ArrayList<>();
|
||||
for (Ruling hr : rulings) {
|
||||
if (hr.horizontal()) {
|
||||
if (hr.isHorizontal()) {
|
||||
hrs.add(hr);
|
||||
}
|
||||
}
|
||||
|
||||
+47
-31
@@ -27,8 +27,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@Deprecated
|
||||
public class SectionsBuilderService {
|
||||
|
||||
|
||||
public void buildSections(ClassificationDocument document) {
|
||||
|
||||
List<AbstractPageBlock> chunkWords = new ArrayList<>();
|
||||
@@ -44,7 +46,6 @@ public class SectionsBuilderService {
|
||||
for (ClassificationPage page : document.getPages()) {
|
||||
List<TextPageBlock> header = new ArrayList<>();
|
||||
List<TextPageBlock> footer = new ArrayList<>();
|
||||
List<TextPageBlock> unclassifiedText = new ArrayList<>();
|
||||
for (AbstractPageBlock current : page.getTextBlocks()) {
|
||||
|
||||
if (current.getClassification() == null) {
|
||||
@@ -63,11 +64,6 @@ public class SectionsBuilderService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.getClassification().equals(PageBlockType.OTHER)) {
|
||||
unclassifiedText.add((TextPageBlock) current);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prev != null && current.getClassification().isHeadline() && !prev.getClassification().isHeadline() || !document.isHeadlines()) {
|
||||
ClassificationSection chunkBlock = buildTextBlock(chunkWords, lastHeadline);
|
||||
chunkBlock.setHeadline(lastHeadline);
|
||||
@@ -77,7 +73,8 @@ public class SectionsBuilderService {
|
||||
chunkBlockList.add(chunkBlock);
|
||||
chunkWords = new ArrayList<>();
|
||||
if (!chunkBlock.getTables().isEmpty()) {
|
||||
previousTable = chunkBlock.getTables().get(chunkBlock.getTables().size() - 1);
|
||||
previousTable = chunkBlock.getTables()
|
||||
.get(chunkBlock.getTables().size() - 1);
|
||||
}
|
||||
}
|
||||
if (current instanceof TablePageBlock table) {
|
||||
@@ -95,9 +92,6 @@ public class SectionsBuilderService {
|
||||
if (!footer.isEmpty()) {
|
||||
footers.add(new ClassificationFooter(footer));
|
||||
}
|
||||
if (!unclassifiedText.isEmpty()) {
|
||||
unclassifiedTexts.add(new UnclassifiedText(unclassifiedText));
|
||||
}
|
||||
}
|
||||
|
||||
ClassificationSection chunkBlock = buildTextBlock(chunkWords, lastHeadline);
|
||||
@@ -115,11 +109,12 @@ public class SectionsBuilderService {
|
||||
|
||||
List<ClassificationSection> sections = new ArrayList<>();
|
||||
for (var page : document.getPages()) {
|
||||
page.getTextBlocks().forEach(block -> {
|
||||
block.setPage(page.getPageNumber());
|
||||
var section = buildTextBlock(List.of(block), Strings.EMPTY);
|
||||
sections.add(section);
|
||||
});
|
||||
page.getTextBlocks()
|
||||
.forEach(block -> {
|
||||
block.setPage(page.getPageNumber());
|
||||
var section = buildTextBlock(List.of(block), Strings.EMPTY);
|
||||
sections.add(section);
|
||||
});
|
||||
}
|
||||
document.setSections(sections);
|
||||
}
|
||||
@@ -164,10 +159,10 @@ public class SectionsBuilderService {
|
||||
}
|
||||
}
|
||||
for (ClassificationSection section : sectionsOnPage) {
|
||||
Float xMin = null;
|
||||
Float yMin = null;
|
||||
Float xMax = null;
|
||||
Float yMax = null;
|
||||
Double xMin = null;
|
||||
Double yMin = null;
|
||||
Double xMax = null;
|
||||
Double yMax = null;
|
||||
|
||||
for (AbstractPageBlock abs : section.getPageBlocks()) {
|
||||
if (abs.getPage() != page.getPageNumber()) {
|
||||
@@ -211,10 +206,17 @@ public class SectionsBuilderService {
|
||||
log.debug("Image position x: {}, y: {}", image.getPosition().getX(), image.getPosition().getY());
|
||||
log.debug("Paragraph position xMin: {}, xMax: {}, yMin: {}, yMax: {}", xMin, xMax, yMin, yMax);
|
||||
|
||||
if (xMin != null && xMax != null && yMin != null && yMax != null && image.getPosition().getX() >= xMin && image.getPosition()
|
||||
.getX() <= xMax && image.getPosition().getY() >= yMin && image.getPosition().getY() <= yMax) {
|
||||
if (xMin != null
|
||||
&& xMax != null
|
||||
&& yMin != null
|
||||
&& yMax != null
|
||||
&& image.getPosition().getX() >= xMin
|
||||
&& image.getPosition().getX() <= xMax
|
||||
&& image.getPosition().getY() >= yMin
|
||||
&& image.getPosition().getY() <= yMax) {
|
||||
section.getImages().add(image);
|
||||
image.setAppendedToSection(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!image.isAppendedToSection()) {
|
||||
@@ -234,17 +236,26 @@ public class SectionsBuilderService {
|
||||
List<Cell> previousTableNonHeaderRow = getRowWithNonHeaderCells(previousTable);
|
||||
List<Cell> tableNonHeaderRow = getRowWithNonHeaderCells(currentTable);
|
||||
// Allow merging of tables if header row is separated from first logical non-header row
|
||||
if (previousTableNonHeaderRow.isEmpty() && previousTable.getRowCount() == 1 && previousTable.getRows().get(0).size() == tableNonHeaderRow.size()) {
|
||||
previousTableNonHeaderRow = previousTable.getRows().get(0).stream().map(cell -> {
|
||||
Cell fakeCell = new Cell(cell.getPoints()[0], cell.getPoints()[2]);
|
||||
fakeCell.setHeaderCells(Collections.singletonList(cell));
|
||||
return fakeCell;
|
||||
}).collect(Collectors.toList());
|
||||
if (previousTableNonHeaderRow.isEmpty()
|
||||
&& previousTable.getRowCount() == 1
|
||||
&& previousTable.getRows()
|
||||
.get(0).size() == tableNonHeaderRow.size()) {
|
||||
previousTableNonHeaderRow = previousTable.getRows()
|
||||
.get(0)
|
||||
.stream()
|
||||
.map(cell -> {
|
||||
Cell fakeCell = Cell.copy(cell);
|
||||
fakeCell.setHeaderCells(Collections.singletonList(cell));
|
||||
return fakeCell;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
if (previousTableNonHeaderRow.size() == tableNonHeaderRow.size()) {
|
||||
for (int i = currentTable.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
|
||||
List<Cell> row = currentTable.getRows().get(i);
|
||||
if (row.size() == tableNonHeaderRow.size() && row.stream().allMatch(cell -> cell.getHeaderCells().isEmpty())) {
|
||||
List<Cell> row = currentTable.getRows()
|
||||
.get(i);
|
||||
if (row.size() == tableNonHeaderRow.size() && row.stream()
|
||||
.allMatch(cell -> cell.getHeaderCells().isEmpty())) {
|
||||
for (int j = 0; j < row.size(); j++) {
|
||||
row.get(j).setHeaderCells(previousTableNonHeaderRow.get(j).getHeaderCells());
|
||||
}
|
||||
@@ -287,7 +298,11 @@ public class SectionsBuilderService {
|
||||
|
||||
private boolean hasInvalidHeaderInformation(TablePageBlock table) {
|
||||
|
||||
return table.getRows().stream().flatMap(row -> row.stream().filter(cell -> !cell.getHeaderCells().isEmpty())).findAny().isEmpty();
|
||||
return table.getRows()
|
||||
.stream()
|
||||
.flatMap(row -> row.stream()
|
||||
.filter(cell -> !cell.getHeaderCells().isEmpty()))
|
||||
.findAny().isEmpty();
|
||||
|
||||
}
|
||||
|
||||
@@ -295,7 +310,8 @@ public class SectionsBuilderService {
|
||||
private List<Cell> getRowWithNonHeaderCells(TablePageBlock table) {
|
||||
|
||||
for (int i = table.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
|
||||
List<Cell> row = table.getRows().get(i);
|
||||
List<Cell> row = table.getRows()
|
||||
.get(i);
|
||||
if (row.size() == 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+32
-6
@@ -1,26 +1,52 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.SimplifiedSectionText;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Section;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.SimplifiedText;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SimplifiedSectionTextService {
|
||||
|
||||
public SimplifiedText toSimplifiedText(Document document) {
|
||||
|
||||
List<SimplifiedSectionText> simplifiedSectionTexts = document.getMainSections().stream().map(this::toSimplifiedSectionText).toList();
|
||||
return SimplifiedText.builder().numberOfPages(document.getNumberOfPages()).sectionTexts(simplifiedSectionTexts).build();
|
||||
List<SimplifiedSectionText> simplifiedMainSectionsList = document.getAllSections()
|
||||
.stream()
|
||||
.map(this::toSimplifiedSectionText)
|
||||
.toList();
|
||||
List<SimplifiedSectionText> simplifiedHeadersList = document.getHeaders()
|
||||
.stream()
|
||||
.map(this::toSimplifiedSectionText)
|
||||
.toList();
|
||||
List<SimplifiedSectionText> simplifiedFootersList = document.getFooters()
|
||||
.stream()
|
||||
.map(this::toSimplifiedSectionText)
|
||||
.toList();
|
||||
List<SimplifiedSectionText> simplifiedText = Stream.of(simplifiedMainSectionsList, simplifiedHeadersList, simplifiedFootersList)
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.toList());
|
||||
return SimplifiedText.builder().numberOfPages(document.getNumberOfPages()).sectionTexts(simplifiedText).build();
|
||||
}
|
||||
|
||||
|
||||
private SimplifiedSectionText toSimplifiedSectionText(Section section) {
|
||||
private SimplifiedSectionText toSimplifiedSectionText(SemanticNode section) {
|
||||
|
||||
return SimplifiedSectionText.builder().sectionNumber(section.getTreeId().get(0)).text(section.getTextBlock().getSearchText()).build();
|
||||
return SimplifiedSectionText.builder()
|
||||
.sectionNumber(section.getTreeId()
|
||||
.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(".")))
|
||||
.text(section.getTextBlock().getSearchText())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
-24
@@ -3,6 +3,8 @@ package com.knecon.fforesight.service.layoutparser.processor.services;
|
||||
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.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -11,22 +13,26 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
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;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.CoordinateTransforms;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.DoubleComparisons;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.PageInformation;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangularIntersectionFinder;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.SpreadsheetFinder;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
@Service
|
||||
public class TableExtractionService {
|
||||
|
||||
private static final int MAX_TABLE_CONTAINED_CELLS_WITH_TEXT = 1;
|
||||
private static final int TEXT_BLOCK_CONTAINMENT_TOLERANCE = 2;
|
||||
private static final double TABLE_UNIFORMITY_THRESHOLD = 0.7;
|
||||
|
||||
|
||||
@@ -59,29 +65,31 @@ public class TableExtractionService {
|
||||
}
|
||||
}
|
||||
|
||||
var cells = new ArrayList<>(new HashSet<>(emptyCells));
|
||||
DoubleComparisons.sort(cells, Rectangle.ILL_DEFINED_ORDER);
|
||||
List<Cell> cells = new ArrayList<>(new HashSet<>(emptyCells));
|
||||
DoubleComparisons.sort(cells, BoundingBox.ILL_DEFINED_ORDER);
|
||||
|
||||
List<Rectangle> spreadsheetAreas = SpreadsheetFinder.findSpreadsheetsFromCells(cells);
|
||||
List<Rectangle2D> 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);
|
||||
|
||||
List<TablePageBlock> tables = new ArrayList<>();
|
||||
for (Rectangle area : spreadsheetAreas) {
|
||||
for (Rectangle2D area : spreadsheetAreas) {
|
||||
|
||||
List<Cell> containedCells = new ArrayList<>();
|
||||
for (Cell c : cells) {
|
||||
if (c.hasMinimumSize() && area.contains(c)) {
|
||||
if (c.hasMinimumSize() && area.contains(c.getBBoxInitialUserSpace())) {
|
||||
containedCells.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
var containedCellsWithText = containedCells.stream().filter(cell -> !cell.getTextBlocks().isEmpty()).toList();
|
||||
var containedCellsWithText = containedCells.stream()
|
||||
.filter(cell -> !cell.getTextBlocks().isEmpty())
|
||||
.toList();
|
||||
|
||||
// verify if table would contain fewer cells with text than the threshold allows
|
||||
if (containedCellsWithText.size() >= MAX_TABLE_CONTAINED_CELLS_WITH_TEXT && checkIfTableCellsAreUniform(containedCells)) {
|
||||
tables.add(new TablePageBlock(containedCells, area, page.getRotation()));
|
||||
tables.add(new TablePageBlock(containedCells, page.getRotation()));
|
||||
cells.removeAll(containedCells);
|
||||
}
|
||||
}
|
||||
@@ -90,14 +98,18 @@ public class TableExtractionService {
|
||||
int position = -1;
|
||||
|
||||
for (AbstractPageBlock pageBlock : page.getTextBlocks()) {
|
||||
if (pageBlock instanceof TextPageBlock ? table.containsBlock((TextPageBlock) pageBlock) : table.contains(pageBlock) && position == -1) {
|
||||
if (pageBlock instanceof TextPageBlock ? table.contains(pageBlock) : table.contains(pageBlock) && position == -1) {
|
||||
position = page.getTextBlocks().indexOf(pageBlock);
|
||||
}
|
||||
}
|
||||
if (position != -1) {
|
||||
page.getTextBlocks().add(position, table);
|
||||
|
||||
var toBeRemoved = table.getCells().stream().map(Cell::getTextBlocks).flatMap(List::stream).toList();
|
||||
var toBeRemoved = table.getCells()
|
||||
.stream()
|
||||
.map(Cell::getTextBlocks)
|
||||
.flatMap(List::stream)
|
||||
.toList();
|
||||
// remove text blocks from the page that were also added with the table (from its contained cells)
|
||||
page.getTextBlocks().removeAll(toBeRemoved);
|
||||
}
|
||||
@@ -112,7 +124,7 @@ public class TableExtractionService {
|
||||
}
|
||||
|
||||
Map<Long, List<Long>> cellsGroupedByRoundedWidth = containedCells.stream()
|
||||
.map(Rectangle::getWidth)
|
||||
.map(BoundingBox::getWidth)
|
||||
.map(size -> Math.round(size / 10.0) * 10)
|
||||
.collect(Collectors.groupingBy(Long::longValue));
|
||||
|
||||
@@ -122,22 +134,26 @@ public class TableExtractionService {
|
||||
|
||||
private boolean doesCellContainTextBlock(Cell cell, TextPageBlock textBlock) {
|
||||
|
||||
double x = textBlock.getPdfMinX();
|
||||
double y = textBlock.getPdfMinY();
|
||||
double w = textBlock.getPdfMaxX() - textBlock.getPdfMinX();
|
||||
double h = textBlock.getPdfMaxY() - textBlock.getPdfMinY();
|
||||
if (cell.isEmpty() || w <= 0 || h <= 0) {
|
||||
return false;
|
||||
}
|
||||
double x0 = cell.getX();
|
||||
double y0 = cell.getY();
|
||||
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);
|
||||
return cell.contains(textBlock, RedTextPosition.HEIGHT_PADDING);
|
||||
}
|
||||
|
||||
|
||||
public static List<Cell> findCells(List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {
|
||||
@SneakyThrows
|
||||
public static List<Cell> findCells(List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines, PageInformation pageInformation) {
|
||||
|
||||
return RectangularIntersectionFinder.find(horizontalRulingLines, verticalRulingLines).stream().map(Cell::new).collect(Collectors.toList());
|
||||
AffineTransform affineTransform = CoordinateTransforms.calculateInitialUserSpaceCoordsToImageCoords(pageInformation, 1);
|
||||
/*
|
||||
switch (pageInformation.rotationDegrees()) {
|
||||
case 90 -> affineTransform.translate(RedTextPosition.HEIGHT_PADDING, 0); //although this is wrong, our text coordinates are wrong as well
|
||||
case 180 -> affineTransform.translate(0, RedTextPosition.HEIGHT_PADDING);
|
||||
case 270 -> affineTransform.translate(-RedTextPosition.HEIGHT_PADDING, 0);
|
||||
default -> affineTransform.translate(0, -RedTextPosition.HEIGHT_PADDING);
|
||||
}
|
||||
*/
|
||||
return RectangularIntersectionFinder.find(horizontalRulingLines, verticalRulingLines)
|
||||
.stream()
|
||||
.map(rect -> new Cell(rect, affineTransform))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextDirection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class TextRulingsClassifier {
|
||||
|
||||
private final static double STRIKETHROUGH_ZONE = 0.5; // multiplied with text height, determines height of intersection interval for strikethrough lines.
|
||||
private final static double UNDERLINE_ZONE = 0.2; // multiplied with text height, determines height of intersection interval of underline lines.
|
||||
private final static double TEXT_BBOX_THRESHOLD_FACTOR = 0.15; // multiplied with text width then subtracted from word width. If ruling covers this width, it is considered as strikethrough/underline.
|
||||
|
||||
|
||||
public static void classifyUnderlinedAndStrikethroughText(List<TextPositionSequence> words, CleanRulings cleanRulings) {
|
||||
|
||||
for (TextPositionSequence word : words) {
|
||||
if (word.getDir().equals(TextDirection.ZERO) || word.getDir().equals(TextDirection.HALF_CIRCLE)) {
|
||||
handleHorizontalText(cleanRulings, word);
|
||||
} else {
|
||||
handleVerticalText(cleanRulings, word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void handleVerticalText(CleanRulings cleanRulings, TextPositionSequence word) {
|
||||
|
||||
float lowerY = (float) (word.getBBoxInitialUserSpace().getMinY() + TEXT_BBOX_THRESHOLD_FACTOR * word.getWidth());
|
||||
float upperY = (float) (word.getBBoxInitialUserSpace().getMaxY() - TEXT_BBOX_THRESHOLD_FACTOR * word.getWidth());
|
||||
|
||||
float strikethroughCenterX = (float) word.getBBoxInitialUserSpace().getCenterX();
|
||||
float strikethroughBoxHeight = (float) ((word.getBBoxDirAdj().getHeight() * STRIKETHROUGH_ZONE) / 2);
|
||||
|
||||
float underlineCenterX = (float) (word.getDir().equals(TextDirection.QUARTER_CIRCLE) ? word.getBBoxInitialUserSpace().getMaxX() : word.getBBoxInitialUserSpace().getMinX());
|
||||
float underlineBoxHeight = (float) ((word.getBBoxDirAdj().getHeight() * UNDERLINE_ZONE) / 2);
|
||||
|
||||
float leftX = Math.min(underlineCenterX - underlineBoxHeight, strikethroughCenterX - strikethroughBoxHeight);
|
||||
float rightX = Math.max(underlineCenterX + underlineBoxHeight, strikethroughCenterX + strikethroughBoxHeight);
|
||||
|
||||
List<Ruling> rulingsIntersectingWord = cleanRulings.getVerticalsInXInterval(leftX, rightX)
|
||||
.stream()
|
||||
.filter(ruling -> ruling.getClassification().equals(Ruling.Classification.OTHER))
|
||||
.filter(ruling -> ruling.y1 <= lowerY && upperY <= ruling.y2)
|
||||
.toList();
|
||||
|
||||
for (Ruling ruling : rulingsIntersectingWord) {
|
||||
if (strikethroughCenterX - strikethroughBoxHeight < ruling.x1 && ruling.x1 < strikethroughCenterX + strikethroughBoxHeight) {
|
||||
ruling.setClassification(Ruling.Classification.STRIKETROUGH);
|
||||
word.setStrikethrough(true);
|
||||
}
|
||||
|
||||
if (underlineCenterX - underlineBoxHeight < ruling.x1 && ruling.x1 < underlineCenterX + underlineBoxHeight) {
|
||||
ruling.setClassification(Ruling.Classification.UNDERLINE);
|
||||
word.setUnderline(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void handleHorizontalText(CleanRulings cleanRulings, TextPositionSequence word) {
|
||||
|
||||
float leftX = (float) (word.getBBoxInitialUserSpace().getMinX() + TEXT_BBOX_THRESHOLD_FACTOR * word.getWidth());
|
||||
float rightX = (float) (word.getBBoxInitialUserSpace().getMaxX() - TEXT_BBOX_THRESHOLD_FACTOR * word.getWidth());
|
||||
|
||||
float strikethroughCenterY = (float) word.getBBoxInitialUserSpace().getCenterY();
|
||||
float strikethroughBoxHeight = (float) ((word.getBBoxDirAdj().getHeight() * STRIKETHROUGH_ZONE) / 2);
|
||||
|
||||
float underlineCenterY = (float) (word.getDir().equals(TextDirection.ZERO) ? word.getBBoxInitialUserSpace().getMinY() : word.getBBoxInitialUserSpace().getMaxY());
|
||||
float underlineBoxHeight = (float) ((word.getBBoxDirAdj().getHeight() * UNDERLINE_ZONE) / 2);
|
||||
|
||||
float lowerY = Math.min(underlineCenterY - underlineBoxHeight, strikethroughCenterY - strikethroughBoxHeight);
|
||||
float upperY = Math.max(underlineCenterY + underlineBoxHeight, strikethroughCenterY + strikethroughBoxHeight);
|
||||
|
||||
List<Ruling> rulingsIntersectingWord = cleanRulings.getHorizontalsInYInterval(lowerY, upperY)
|
||||
.stream()
|
||||
.filter(ruling -> ruling.getClassification().equals(Ruling.Classification.OTHER))
|
||||
.filter(ruling -> ruling.x1 <= leftX && rightX <= ruling.x2)
|
||||
.toList();
|
||||
|
||||
for (Ruling ruling : rulingsIntersectingWord) {
|
||||
if (strikethroughCenterY - strikethroughBoxHeight < ruling.y1 && ruling.y1 < strikethroughCenterY + strikethroughBoxHeight) {
|
||||
ruling.setClassification(Ruling.Classification.STRIKETROUGH);
|
||||
word.setStrikethrough(true);
|
||||
}
|
||||
|
||||
if (underlineCenterY - underlineBoxHeight < ruling.y1 && ruling.y1 < underlineCenterY + underlineBoxHeight) {
|
||||
ruling.setClassification(Ruling.Classification.UNDERLINE);
|
||||
word.setUnderline(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+533
@@ -0,0 +1,533 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.blockification;
|
||||
|
||||
import static com.knecon.fforesight.service.layoutparser.processor.services.blockification.DocstrumBlockificationService.buildTextBlock;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.SectionIdentifier;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.OutlineObject;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.factory.SearchTextWithTextPositionFactory;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Service
|
||||
public class BlockificationPostprocessingService {
|
||||
|
||||
private static final float BLOCK_TO_OUTLINE_DISTANCE_THRESHOLD = 5.0f;
|
||||
|
||||
private static final Function<TextPageBlock, Rectangle2D> blockToBoundingBox = (abstractPageBlock) -> abstractPageBlock.getSequences()
|
||||
.stream()
|
||||
.map(textPositionSequence -> textPositionSequence.getTextPositions()
|
||||
.stream()
|
||||
.map(tp -> SearchTextWithTextPositionFactory.mapRedTextPositionToInitialUserSpace(tp, textPositionSequence))
|
||||
.collect(RectangleTransformations.collectBBox()))
|
||||
.collect(RectangleTransformations.collectBBox());
|
||||
|
||||
|
||||
public OutlineObject sanitizeOutlineBlocks(ClassificationPage classificationPage, OutlineObject notFoundOutlineObject) {
|
||||
|
||||
List<OutlineObject> outlineObjects = classificationPage.getOutlineObjects();
|
||||
|
||||
if (getTextPageBlocks(classificationPage).isEmpty() || outlineObjects.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
float pageHeight = classificationPage.getPageHeight();
|
||||
|
||||
ListIterator<OutlineObject> outlineObjectListIterator = outlineObjects.listIterator();
|
||||
|
||||
if (notFoundOutlineObject != null) {
|
||||
OutlineProcessionContext notFoundOutlineObjectProcessionContext = new OutlineProcessionContext(notFoundOutlineObject);
|
||||
processTextBlocks(getTextPageBlocks(classificationPage), pageHeight, notFoundOutlineObjectProcessionContext);
|
||||
|
||||
OutlineObject firstOutlineObject = null;
|
||||
OutlineProcessionContext firstOutlineObjectProcessionContext = null;
|
||||
if (outlineObjectListIterator.hasNext()) {
|
||||
firstOutlineObject = outlineObjectListIterator.next();
|
||||
firstOutlineObjectProcessionContext = new OutlineProcessionContext(firstOutlineObject);
|
||||
processTextBlocks(getTextPageBlocks(classificationPage), pageHeight, firstOutlineObjectProcessionContext);
|
||||
}
|
||||
|
||||
if (!contextsOverlap(notFoundOutlineObjectProcessionContext, firstOutlineObjectProcessionContext)) {
|
||||
notFoundOutlineObject.setFound(selectMatch(classificationPage, notFoundOutlineObjectProcessionContext));
|
||||
}
|
||||
if (firstOutlineObject != null) {
|
||||
// re-create the context for the updated blocks
|
||||
firstOutlineObjectProcessionContext = new OutlineProcessionContext(firstOutlineObject);
|
||||
processTextBlocks(getTextPageBlocks(classificationPage), pageHeight, firstOutlineObjectProcessionContext);
|
||||
firstOutlineObject.setFound(selectMatch(classificationPage, firstOutlineObjectProcessionContext));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
outlineObjectListIterator.forEachRemaining(outlineObject -> {
|
||||
OutlineProcessionContext outlineObjectProcessionContext = new OutlineProcessionContext(outlineObject);
|
||||
processTextBlocks(getTextPageBlocks(classificationPage), pageHeight, outlineObjectProcessionContext);
|
||||
outlineObject.setFound(selectMatch(classificationPage, outlineObjectProcessionContext));
|
||||
});
|
||||
|
||||
if (!outlineObjects.isEmpty()) {
|
||||
return outlineObjects.get(outlineObjects.size() - 1);
|
||||
} else {
|
||||
return notFoundOutlineObject;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static List<TextPageBlock> getTextPageBlocks(ClassificationPage classificationPage) {
|
||||
|
||||
return classificationPage.getTextBlocks()
|
||||
.stream()
|
||||
.filter(block -> block instanceof TextPageBlock)
|
||||
.map(block -> (TextPageBlock) block)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
private boolean contextsOverlap(OutlineProcessionContext notFoundOutlineObjectProcessionContext, OutlineProcessionContext firstOutlineObjectProcessionContext) {
|
||||
|
||||
if (firstOutlineObjectProcessionContext == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String notFoundTitle = notFoundOutlineObjectProcessionContext.getOutlineObject().getTitle();
|
||||
String firstTitle = firstOutlineObjectProcessionContext.getOutlineObject().getTitle();
|
||||
|
||||
if (!firstTitle.startsWith(notFoundTitle)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var blocksOfNotFoundOutline = getAllMatchingBlocks(notFoundOutlineObjectProcessionContext);
|
||||
var blocksOfFirstOutline = getAllMatchingBlocks(firstOutlineObjectProcessionContext);
|
||||
|
||||
double maxYFirst = blocksOfFirstOutline.stream()
|
||||
.mapToDouble(TextPageBlock::getPdfMaxY)
|
||||
.max()
|
||||
.orElse(Double.NEGATIVE_INFINITY);
|
||||
|
||||
return blocksOfNotFoundOutline.stream()
|
||||
.mapToDouble(TextPageBlock::getPdfMaxY)
|
||||
.anyMatch(y -> y >= maxYFirst);
|
||||
}
|
||||
|
||||
|
||||
private List<TextPageBlock> getAllMatchingBlocks(OutlineProcessionContext context) {
|
||||
|
||||
List<TextPageBlock> blocks = new ArrayList<>();
|
||||
if (context.getDirectMatch() != null) {
|
||||
blocks.add(context.getDirectMatch());
|
||||
}
|
||||
if (context.getSplitCandidate() != null) {
|
||||
blocks.add(context.getSplitCandidate());
|
||||
}
|
||||
blocks.addAll(context.getMergeCandidates());
|
||||
return blocks;
|
||||
}
|
||||
|
||||
|
||||
private void processTextBlocks(List<TextPageBlock> textBlocks, float pageHeight, OutlineProcessionContext context) {
|
||||
|
||||
OutlineObject outlineObject = context.getOutlineObject();
|
||||
ListIterator<TextPageBlock> iterator = textBlocks.listIterator();
|
||||
while (iterator.hasNext()) {
|
||||
TextPageBlock pageBlock = iterator.next();
|
||||
if (pageHeight - outlineObject.getPoint().getY() - BLOCK_TO_OUTLINE_DISTANCE_THRESHOLD <= pageBlock.getMaxY()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (iterator.hasPrevious()) {
|
||||
iterator.previous();
|
||||
}
|
||||
boolean earlyStop = false;
|
||||
while (iterator.hasNext() && !earlyStop) {
|
||||
TextPageBlock pageBlock = iterator.next();
|
||||
earlyStop = processOutlineForTextBlock(pageBlock, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean selectMatch(ClassificationPage classificationPage, OutlineProcessionContext context) {
|
||||
|
||||
OutlineObject outlineObject = context.outlineObject;
|
||||
TextPageBlock directMatch = context.directMatch;
|
||||
List<TextPageBlock> mergeCandidates = context.mergeCandidates;
|
||||
TextPageBlock splitCandidate = context.splitCandidate;
|
||||
PageBlockType headlineType = PageBlockType.getHeadlineType(outlineObject.getTreeDepth());
|
||||
|
||||
double distanceToDirectMatch = directMatch != null ? calculateDistance(outlineObject, directMatch) : Double.MAX_VALUE;
|
||||
double distanceToSplitCandidate = splitCandidate != null ? calculateDistance(outlineObject, splitCandidate) : Double.MAX_VALUE;
|
||||
|
||||
double distanceToBestMergeCandidates = Double.MAX_VALUE;
|
||||
List<TextPageBlock> bestMergeCandidateCombination = new ArrayList<>();
|
||||
if (!mergeCandidates.isEmpty()) {
|
||||
|
||||
// with this code adjacent blocks to the first and last merge candidate get added, this could be useful for some edge cases:
|
||||
//List<TextPageBlock> allMergeCandidates = new ArrayList<>(mergeCandidates);
|
||||
//addNeighborsOfCandidate(kdTree, mergeCandidates.get(0), allMergeCandidates);
|
||||
//if (mergeCandidates.size() > 1) {
|
||||
// addNeighborsOfCandidate(kdTree, mergeCandidates.get(mergeCandidates.size() - 1), allMergeCandidates);
|
||||
//}
|
||||
//allMergeCandidates = allMergeCandidates.stream()
|
||||
// .distinct()
|
||||
// .toList();
|
||||
|
||||
List<List<TextPageBlock>> combinations = findCombinations(outlineObject.getTitle(), mergeCandidates);
|
||||
|
||||
for (List<TextPageBlock> combination : combinations) {
|
||||
double averageDistance = combination.stream()
|
||||
.map(block -> calculateDistance(outlineObject, block))
|
||||
.mapToDouble(Double::doubleValue).average()
|
||||
.orElse(Double.MAX_VALUE);
|
||||
if (distanceToBestMergeCandidates > averageDistance) {
|
||||
distanceToBestMergeCandidates = averageDistance;
|
||||
bestMergeCandidateCombination = combination;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double minDistance = Math.min(distanceToDirectMatch, Math.min(distanceToSplitCandidate, distanceToBestMergeCandidates));
|
||||
|
||||
if (minDistance == Double.MAX_VALUE) {
|
||||
return false;
|
||||
}
|
||||
if (minDistance == distanceToDirectMatch) {
|
||||
setClassificationAndAddOutlineEngine(directMatch, headlineType);
|
||||
} else if (minDistance == distanceToSplitCandidate) {
|
||||
SplitBlockResult splitBlockResult = splitBlock(classificationPage, splitCandidate, context.sectionIdentifier, outlineObject.getTitle());
|
||||
if (splitBlockResult.modifiedBlockToSplit) {
|
||||
setClassificationAndAddOutlineEngine(splitCandidate, headlineType);
|
||||
}
|
||||
splitBlockResult.otherBlocks.forEach(other -> other.setClassification(null));
|
||||
} else {
|
||||
var merged = mergeBlocks(classificationPage, bestMergeCandidateCombination);
|
||||
setClassificationAndAddOutlineEngine(merged, headlineType);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static void setClassificationAndAddOutlineEngine(TextPageBlock block, PageBlockType headlineType) {
|
||||
|
||||
block.setClassification(headlineType);
|
||||
block.getEngines().add(LayoutEngine.OUTLINE);
|
||||
}
|
||||
|
||||
|
||||
private SplitBlockResult splitBlock(ClassificationPage classificationPage, TextPageBlock blockToSplit, SectionIdentifier sectionIdentifier, String title) {
|
||||
|
||||
List<TextPageBlock> otherBlocks = new ArrayList<>();
|
||||
int blockToSplitIdx = classificationPage.getTextBlocks().indexOf(blockToSplit);
|
||||
|
||||
String headline = title;
|
||||
if (!sectionIdentifier.getFormat().equals(SectionIdentifier.Format.EMPTY) && !title.startsWith(sectionIdentifier.getIdentifierString())) {
|
||||
headline = sectionIdentifier + headline;
|
||||
}
|
||||
|
||||
WordSequenceResult wordSequenceResult = findWordSequence(blockToSplit.getSequences(), headline);
|
||||
if (wordSequenceResult.inSequence.isEmpty() && !headline.equals(title)) {
|
||||
wordSequenceResult = findWordSequence(blockToSplit.getSequences(), title);
|
||||
}
|
||||
|
||||
boolean modifiedBlockToSplit = false;
|
||||
if (!wordSequenceResult.inSequence.isEmpty()) {
|
||||
blockToSplit.setSequences(wordSequenceResult.inSequence);
|
||||
blockToSplit.recalculateBBox();
|
||||
modifiedBlockToSplit = true;
|
||||
}
|
||||
|
||||
if (!wordSequenceResult.preSequence.isEmpty()) {
|
||||
TextPageBlock block = buildTextBlock(wordSequenceResult.preSequence, 0);
|
||||
classificationPage.getTextBlocks().add(blockToSplitIdx, block);
|
||||
otherBlocks.add(block);
|
||||
blockToSplitIdx++;
|
||||
}
|
||||
if (!wordSequenceResult.postSequence.isEmpty()) {
|
||||
TextPageBlock block = buildTextBlock(wordSequenceResult.postSequence, 0);
|
||||
classificationPage.getTextBlocks().add(blockToSplitIdx + 1, block);
|
||||
otherBlocks.add(block);
|
||||
}
|
||||
|
||||
return new SplitBlockResult(modifiedBlockToSplit, otherBlocks);
|
||||
}
|
||||
|
||||
|
||||
private static WordSequenceResult findWordSequence(List<TextPositionSequence> textPositionSequences, String text) {
|
||||
|
||||
String target = sanitizeString(text);
|
||||
List<TextPositionSequence> inSequence = new ArrayList<>();
|
||||
List<TextPositionSequence> preSequence = new ArrayList<>();
|
||||
List<TextPositionSequence> postSequence = new ArrayList<>();
|
||||
StringBuilder currentSequence = new StringBuilder();
|
||||
|
||||
for (TextPositionSequence sequence : textPositionSequences) {
|
||||
|
||||
currentSequence.append(sanitizeString(sequence.toString()));
|
||||
inSequence.add(sequence);
|
||||
|
||||
if (currentSequence.length() >= target.length()) {
|
||||
|
||||
if (currentSequence.toString().endsWith(target)) {
|
||||
|
||||
int index = 0;
|
||||
String toRemove = currentSequence.substring(0, currentSequence.length() - target.length());
|
||||
|
||||
TextPositionSequence next = inSequence.get(index);
|
||||
while (currentSequence.length() - next.length() >= target.length()) {
|
||||
|
||||
TextPositionSequence removed = inSequence.remove(index);
|
||||
currentSequence.delete(0, removed.toString().length());
|
||||
preSequence.add(removed);
|
||||
|
||||
next = inSequence.get(index);
|
||||
toRemove = toRemove.substring(removed.length());
|
||||
}
|
||||
|
||||
if (!toRemove.isEmpty()) {
|
||||
SplitSequenceResult splitSequenceResult = splitSequence(inSequence.remove(index), toRemove);
|
||||
|
||||
currentSequence.delete(0, splitSequenceResult.out.length());
|
||||
preSequence.add(splitSequenceResult.out);
|
||||
inSequence.add(index, splitSequenceResult.in);
|
||||
}
|
||||
|
||||
} else if (currentSequence.toString().startsWith(target)) {
|
||||
|
||||
int index = inSequence.size() - 1;
|
||||
String toRemove = currentSequence.substring(target.length());
|
||||
|
||||
SplitSequenceResult splitSequenceResult = splitSequence(inSequence.remove(index), toRemove);
|
||||
currentSequence.delete(currentSequence.length() - splitSequenceResult.out.length(), currentSequence.length());
|
||||
|
||||
inSequence.add(index, splitSequenceResult.in);
|
||||
postSequence.add(splitSequenceResult.out);
|
||||
}
|
||||
|
||||
if (currentSequence.toString().equals(target)) {
|
||||
postSequence.addAll(textPositionSequences.subList(textPositionSequences.indexOf(sequence) + 1, textPositionSequences.size()));
|
||||
return new WordSequenceResult(inSequence, preSequence, postSequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new WordSequenceResult();
|
||||
}
|
||||
|
||||
|
||||
private static SplitSequenceResult splitSequence(TextPositionSequence sequence, String toRemove) {
|
||||
|
||||
TextPositionSequence in = null;
|
||||
TextPositionSequence out;
|
||||
|
||||
String currentSequence = sequence.toString();
|
||||
int index = currentSequence.indexOf(toRemove);
|
||||
int endIndex = index + toRemove.length();
|
||||
|
||||
out = createSubSequence(sequence, index, endIndex);
|
||||
|
||||
if (index > 0) {
|
||||
in = createSubSequence(sequence, 0, index);
|
||||
} else if (endIndex < sequence.getTextPositions().size()) {
|
||||
in = createSubSequence(sequence, endIndex, sequence.getTextPositions().size());
|
||||
}
|
||||
|
||||
return new SplitSequenceResult(in, out);
|
||||
}
|
||||
|
||||
|
||||
private static TextPositionSequence createSubSequence(TextPositionSequence sequence, int start, int end) {
|
||||
|
||||
TextPositionSequence newSeq = new TextPositionSequence(new ArrayList<>(sequence.getTextPositions().subList(start, end)), sequence.getPage());
|
||||
newSeq.setParagraphStart(sequence.isParagraphStart());
|
||||
return newSeq;
|
||||
}
|
||||
|
||||
|
||||
private TextPageBlock mergeBlocks(ClassificationPage classificationPage, List<TextPageBlock> blocksToMerge) {
|
||||
|
||||
TextPageBlock firstBlock = blocksToMerge.get(0);
|
||||
|
||||
if (blocksToMerge.size() > 1) {
|
||||
|
||||
List<TextPageBlock> mergedBlocks = new ArrayList<>();
|
||||
for (TextPageBlock textPageBlock : blocksToMerge.subList(1, blocksToMerge.size())) {
|
||||
|
||||
if (firstBlock != null && !firstBlock.getSequences().isEmpty()) {
|
||||
|
||||
if (textPageBlock.getDir() == firstBlock.getDir()) {
|
||||
firstBlock.getSequences().addAll(textPageBlock.getSequences());
|
||||
mergedBlocks.add(textPageBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert firstBlock != null;
|
||||
firstBlock.setToDuplicate(false);
|
||||
firstBlock.recalculateBBox();
|
||||
classificationPage.getTextBlocks().removeAll(mergedBlocks);
|
||||
}
|
||||
|
||||
return firstBlock;
|
||||
}
|
||||
|
||||
|
||||
private static List<List<TextPageBlock>> findCombinations(String title, List<TextPageBlock> blocks) {
|
||||
|
||||
List<List<TextPageBlock>> combinations = new ArrayList<>();
|
||||
findCombinations(title, blocks, new ArrayList<>(), combinations);
|
||||
return combinations;
|
||||
}
|
||||
|
||||
|
||||
private static void findCombinations(String title, List<TextPageBlock> blocks, List<TextPageBlock> current, List<List<TextPageBlock>> combinations) {
|
||||
|
||||
String target = sanitizeString(title);
|
||||
if (target.isEmpty()) {
|
||||
combinations.add(new ArrayList<>(current));
|
||||
return;
|
||||
}
|
||||
|
||||
List<TextPageBlock> remaining = blocks.stream()
|
||||
.filter(block -> !current.contains(block))
|
||||
.toList();
|
||||
for (TextPageBlock block : remaining) {
|
||||
String prefix = sanitizeString(block.getText());
|
||||
if (target.startsWith(prefix)) {
|
||||
current.add(block);
|
||||
findCombinations(target.substring(prefix.length()), blocks.subList(blocks.indexOf(block) + 1, blocks.size()), current, combinations);
|
||||
current.remove(current.size() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private double calculateDistance(OutlineObject outlineObject, TextPageBlock pageBlock) {
|
||||
|
||||
double deltaX = outlineObject.getPoint().getX() - pageBlock.getMinX();
|
||||
double deltaY = pageBlock.getPageHeight() - outlineObject.getPoint().getY() - pageBlock.getMinY();
|
||||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
}
|
||||
|
||||
|
||||
// currently only three cases are handled here:
|
||||
// 1. equality
|
||||
// 2. outline title contains block text
|
||||
// 3. block text contains outline title
|
||||
// another possible case is an intersection, meaning a title is split up between two different blocks
|
||||
// this should not happen with how docstrum creates the blocks
|
||||
// if it is indeed necessary, a splitting has to be done with a follow-up merge
|
||||
private boolean processOutlineForTextBlock(TextPageBlock pageBlock, OutlineProcessionContext context) {
|
||||
|
||||
OutlineObject outlineObject = context.getOutlineObject();
|
||||
String blockText = sanitizeString(pageBlock.getText());
|
||||
String outlineTitle = sanitizeString(outlineObject.getTitle());
|
||||
|
||||
boolean blockTextContainsOutlineTitle = blockText.contains(outlineTitle);
|
||||
boolean outlineTitleContainsBlockText = outlineTitle.contains(blockText);
|
||||
|
||||
if (!blockTextContainsOutlineTitle && !outlineTitleContainsBlockText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (blockText.equals(outlineTitle) && context.directMatch == null) {
|
||||
context.directMatch = pageBlock;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (outlineTitleContainsBlockText) {
|
||||
context.mergeCandidates.add(pageBlock);
|
||||
}
|
||||
|
||||
if (blockTextContainsOutlineTitle) {
|
||||
SectionIdentifier sectionIdentifier = SectionIdentifier.fromSearchText(blockText);
|
||||
|
||||
if (sectionIdentifier.getFormat() != SectionIdentifier.Format.EMPTY && !outlineTitle.startsWith(sectionIdentifier.getIdentifierString())) {
|
||||
|
||||
if (blockText.startsWith(sectionIdentifier.getIdentifierString()) && blockText.endsWith(outlineTitle) && context.directMatch == null) {
|
||||
context.directMatch = pageBlock;
|
||||
return true;
|
||||
} else if (context.splitCandidate == null) {
|
||||
context.sectionIdentifier = sectionIdentifier;
|
||||
}
|
||||
}
|
||||
if (context.splitCandidate == null) {
|
||||
context.splitCandidate = pageBlock;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static String sanitizeString(String text) {
|
||||
|
||||
return StringUtils.deleteWhitespace(text).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
|
||||
@Data
|
||||
private static class OutlineProcessionContext {
|
||||
|
||||
private TextPageBlock directMatch;
|
||||
private OutlineObject outlineObject;
|
||||
private List<TextPageBlock> mergeCandidates;
|
||||
private TextPageBlock splitCandidate;
|
||||
private SectionIdentifier sectionIdentifier;
|
||||
|
||||
|
||||
OutlineProcessionContext(OutlineObject outlineObject) {
|
||||
|
||||
this.outlineObject = outlineObject;
|
||||
this.directMatch = null;
|
||||
this.mergeCandidates = new ArrayList<>();
|
||||
this.splitCandidate = null;
|
||||
this.sectionIdentifier = SectionIdentifier.empty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class WordSequenceResult {
|
||||
|
||||
public List<TextPositionSequence> inSequence;
|
||||
public List<TextPositionSequence> preSequence;
|
||||
public List<TextPositionSequence> postSequence;
|
||||
|
||||
|
||||
public WordSequenceResult(List<TextPositionSequence> inSequence, List<TextPositionSequence> preSequence, List<TextPositionSequence> postSequence) {
|
||||
|
||||
this.inSequence = inSequence;
|
||||
this.preSequence = preSequence;
|
||||
this.postSequence = postSequence;
|
||||
}
|
||||
|
||||
|
||||
public WordSequenceResult() {
|
||||
|
||||
this.inSequence = new ArrayList<>();
|
||||
this.preSequence = new ArrayList<>();
|
||||
this.postSequence = new ArrayList<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public record SplitBlockResult(boolean modifiedBlockToSplit, List<TextPageBlock> otherBlocks) {
|
||||
|
||||
}
|
||||
|
||||
public record SplitSequenceResult(TextPositionSequence in, TextPositionSequence out) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+134
-232
@@ -1,31 +1,23 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.blockification;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.DocstrumSegmentationService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Zone;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.utils.DoubleUtils;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.FloatFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Cell;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.StringFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.QuickSort;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RulingTextDirAdjustUtil;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.TextPositionSequenceComparator;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -34,88 +26,137 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public class DocstrumBlockificationService {
|
||||
|
||||
public static final float Y_THRESHOLD = 5f;
|
||||
private final DocstrumSegmentationService docstrumSegmentationService;
|
||||
|
||||
static final float THRESHOLD = 1f;
|
||||
|
||||
|
||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions, List<Cell> cells, boolean xyOrder) {
|
||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions,
|
||||
CleanRulings rulings,
|
||||
boolean xyOrder,
|
||||
LayoutparsingVisualizations visualizations,
|
||||
LayoutParsingType layoutParsingType) {
|
||||
|
||||
// Underlined or strikethrough are also in rulings but we dont want to split blocks with them so we use cells.
|
||||
List<Ruling> usedHorizonalRulings = new ArrayList<>();
|
||||
List<Ruling> usedVerticalRulings = new ArrayList<>();
|
||||
CleanRulings usedRulings = rulings.withoutTextRulings();
|
||||
|
||||
cells.forEach(cell -> {
|
||||
usedHorizonalRulings.add(new Ruling(new Point2D.Float(cell.x, cell.y), new Point2D.Float(cell.x + cell.width, cell.y)));
|
||||
usedHorizonalRulings.add(new Ruling(new Point2D.Float(cell.x, cell.y + cell.height), new Point2D.Float(cell.x + cell.width, cell.y + cell.height)));
|
||||
usedVerticalRulings.add(new Ruling(new Point2D.Float(cell.x, cell.y), new Point2D.Float(cell.x, cell.y + cell.height)));
|
||||
usedVerticalRulings.add(new Ruling(new Point2D.Float(cell.x + cell.width, cell.y), new Point2D.Float(cell.x + cell.width, cell.y + cell.height)));
|
||||
});
|
||||
var zones = docstrumSegmentationService.segmentPage(textPositions, xyOrder, usedRulings, visualizations);
|
||||
|
||||
var zones = docstrumSegmentationService.segmentPage(textPositions, xyOrder);
|
||||
var pageBlocks = toAbstractPageBlocks(zones, usedHorizonalRulings, usedVerticalRulings);
|
||||
if (!textPositions.isEmpty()) {
|
||||
visualizations.addZoneVisualizations(zones, textPositions.get(0).getPage());
|
||||
visualizations.addLineVisualizationsFromZones(zones, textPositions.get(0).getPage());
|
||||
visualizations.addCharactersWithNeighbours(zones, textPositions.get(0).getPage());
|
||||
}
|
||||
|
||||
return new ClassificationPage(pageBlocks);
|
||||
var pageBlocks = toAbstractPageBlocks(zones, xyOrder, usedRulings);
|
||||
|
||||
if (xyOrder) {
|
||||
sortPageBlocksXThenY(pageBlocks);
|
||||
}
|
||||
|
||||
var classificationPage = new ClassificationPage(pageBlocks);
|
||||
classificationPage.setCleanRulings(rulings);
|
||||
|
||||
mergeIntersectingBlocks(classificationPage, usedRulings, 0, 0);
|
||||
|
||||
if (layoutParsingType == LayoutParsingType.DOCUMINE
|
||||
|| layoutParsingType == LayoutParsingType.REDACT_MANAGER
|
||||
|| layoutParsingType == LayoutParsingType.REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH) {
|
||||
combineBlocks(classificationPage, layoutParsingType);
|
||||
}
|
||||
|
||||
if (layoutParsingType == LayoutParsingType.CLARIFYND) {
|
||||
mergeIntersectingBlocks(classificationPage, usedRulings, 0, 0);
|
||||
}
|
||||
|
||||
return classificationPage;
|
||||
}
|
||||
|
||||
|
||||
private List<AbstractPageBlock> toAbstractPageBlocks(List<Zone> zones, List<Ruling> horizontalRulings, List<Ruling> verticalRulings) {
|
||||
private static void sortPageBlocksXThenY(List<AbstractPageBlock> pageBlocks) {
|
||||
|
||||
pageBlocks.sort(Comparator.comparing(AbstractPageBlock::getMinY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
|
||||
.thenComparing(AbstractPageBlock::getMinX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
|
||||
pageBlocks.sort(new Comparator<AbstractPageBlock>() {
|
||||
@Override
|
||||
public int compare(AbstractPageBlock o1, AbstractPageBlock o2) {
|
||||
|
||||
return Math.abs(o1.getMinY() - o2.getMinY()) < 5 && o1.getMinX() < o2.getMinX() == true ? -1 : 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private List<AbstractPageBlock> toAbstractPageBlocks(List<Zone> zones, boolean xyOrder, CleanRulings usedRulings) {
|
||||
|
||||
List<AbstractPageBlock> abstractPageBlocks = new ArrayList<>();
|
||||
zones.forEach(zone -> {
|
||||
|
||||
List<TextPositionSequence> textPositionSequences = new ArrayList<>();
|
||||
zone.getLines().forEach(line -> {
|
||||
line.getWords().forEach(word -> {
|
||||
textPositionSequences.add(new TextPositionSequence(word.getTextPositions(), word.getPage()));
|
||||
});
|
||||
});
|
||||
zone.getLines()
|
||||
.forEach(line -> {
|
||||
line.getWords()
|
||||
.forEach(word -> {
|
||||
textPositionSequences.add(new TextPositionSequence(word.getTextPositions(), word.getPage()));
|
||||
});
|
||||
});
|
||||
|
||||
abstractPageBlocks.addAll(splitZonesAtRulings(textPositionSequences, horizontalRulings, verticalRulings));
|
||||
abstractPageBlocks.add(buildTextBlock(textPositionSequences, 0));
|
||||
});
|
||||
|
||||
return abstractPageBlocks;
|
||||
}
|
||||
|
||||
|
||||
public void combineBlocks(ClassificationPage page) {
|
||||
|
||||
mergeIntersectingBlocks(page.getTextBlocks(), 0, 0);
|
||||
public void combineBlocks(ClassificationPage page, LayoutParsingType layoutParsingType) {
|
||||
|
||||
TextPageBlock previous = new TextPageBlock();
|
||||
ListIterator<AbstractPageBlock> itty = page.getTextBlocks().listIterator();
|
||||
CleanRulings usedRulings = page.getCleanRulings().withoutTextRulings();
|
||||
while (itty.hasNext()) {
|
||||
|
||||
AbstractPageBlock block = itty.next();
|
||||
if (block instanceof TablePageBlock) {
|
||||
previous = new TextPageBlock();
|
||||
continue;
|
||||
}
|
||||
TextPageBlock current = (TextPageBlock) block;
|
||||
|
||||
if (previous != null && !previous.getSequences().isEmpty()) {
|
||||
|
||||
if (current.getDir() != previous.getDir()) {
|
||||
if (current.getDir() != previous.getDir() || usedRulings.lineBetween(current, previous)) {
|
||||
previous = current;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (areTheOnlyTwoBlocksOnHeightsWithBothMoreThanTwoLines(previous, current, page)) {
|
||||
combineBlocksAndResetIterator(previous, current, itty, true);
|
||||
if (current.isHeadline() || previous.isHeadline()) {
|
||||
if (intersectsYWithPreviousHavingMaxOneLine(previous, current, page)) {
|
||||
previous = combineBlocksAndResetIterator(previous, current, itty, false);
|
||||
} else {
|
||||
previous = current;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previous.almostIntersects(current, 0, 0)) {
|
||||
combineBlocksAndResetIterator(previous, current, itty, previous.isToDuplicate());
|
||||
if (areTheOnlyTwoBlocksOnHeightsWithBothMoreThanTwoLines(previous, current, page)) {
|
||||
// previous = combineBlocksAndResetIterator(previous, current, itty, true);
|
||||
previous = combineBlocksAndResetIterator(previous, current, itty, layoutParsingType != LayoutParsingType.REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previous.intersects(current)) {
|
||||
previous = combineBlocksAndResetIterator(previous, current, itty, previous.isToDuplicate());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSameTopOrBottomWithPreviousHavingMaxTwoLinesAndCurrentThanOneAndMax4OtherBlocksOnHeight(previous, current, page)) {
|
||||
combineBlocksAndResetIterator(previous, current, itty, false);
|
||||
previous = combineBlocksAndResetIterator(previous, current, itty, previous.isToDuplicate());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isOnlyIntersectingYAndOnelineOrPrevoiusTwoLines(previous, current, page)) {
|
||||
combineBlocksAndResetIterator(previous, current, itty, false);
|
||||
previous = combineBlocksAndResetIterator(previous, current, itty, previous.isToDuplicate());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -123,15 +164,15 @@ public class DocstrumBlockificationService {
|
||||
previous = current;
|
||||
}
|
||||
|
||||
mergeIntersectingBlocks(page.getTextBlocks(), 0, 0);
|
||||
mergeIntersectingBlocks(page, usedRulings, 0, Y_THRESHOLD);
|
||||
}
|
||||
|
||||
|
||||
private boolean isOnlyIntersectingYAndOnelineOrPrevoiusTwoLines(TextPageBlock previous, TextPageBlock current, ClassificationPage page) {
|
||||
|
||||
return current.intersectsY(previous) //
|
||||
&& (previous.getNumberOfLines() == 1 && current.getNumberOfLines() >= 1 || previous.getNumberOfLines() == 2 && current.getNumberOfLines() == 1) //
|
||||
&& numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(current, previous, page.getTextBlocks()) <= 0;
|
||||
&& (previous.getNumberOfLines() == 1 && current.getNumberOfLines() >= 1 || previous.getNumberOfLines() == 2 && current.getNumberOfLines() == 1) //
|
||||
&& numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(current, previous, page.getTextBlocks()) <= 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -140,28 +181,38 @@ public class DocstrumBlockificationService {
|
||||
ClassificationPage page) {
|
||||
|
||||
return (Math.abs(previous.getMaxY() - current.getMaxY()) < THRESHOLD || Math.abs(previous.getMinY() - current.getMinY()) < THRESHOLD) //
|
||||
&& (previous.getNumberOfLines() == 1 && current.getNumberOfLines() >= 1 || previous.getNumberOfLines() == 2 && current.getNumberOfLines() == 1) //
|
||||
&& !hasBetween(current, previous, page.getTextBlocks()) && numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(current, previous, page.getTextBlocks()) <= 4;
|
||||
&& (previous.getNumberOfLines() == 1 && current.getNumberOfLines() >= 1 || previous.getNumberOfLines() >= 2 && current.getNumberOfLines() == 1) //
|
||||
&& !hasBetween(current, previous, page.getTextBlocks()) && numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(current, previous, page.getTextBlocks()) <= 4;
|
||||
}
|
||||
|
||||
|
||||
private boolean intersectsYWithPreviousHavingMaxOneLine(TextPageBlock previous, TextPageBlock current, ClassificationPage page) {
|
||||
|
||||
return previous.intersectsY(current) && (previous.getNumberOfLines() == 1 && current.getNumberOfLines() >= 1);
|
||||
}
|
||||
|
||||
|
||||
private boolean areTheOnlyTwoBlocksOnHeightsWithBothMoreThanTwoLines(TextPageBlock previous, TextPageBlock current, ClassificationPage page) {
|
||||
|
||||
return previous.getNumberOfLines() >= 2 && current.getNumberOfLines() >= 2 //
|
||||
&& previous.intersectsY(current) //
|
||||
&& numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(current, previous, page.getTextBlocks()) == 0;
|
||||
&& previous.intersectsY(current) //
|
||||
&& numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(current, previous, page.getTextBlocks()) == 0;
|
||||
}
|
||||
|
||||
|
||||
private void combineBlocksAndResetIterator(TextPageBlock previous, TextPageBlock current, ListIterator<AbstractPageBlock> itty, boolean toDuplicate) {
|
||||
private TextPageBlock combineBlocksAndResetIterator(TextPageBlock previous, TextPageBlock current, ListIterator<AbstractPageBlock> itty, boolean toDuplicate) {
|
||||
|
||||
previous.getSequences().addAll(current.getSequences());
|
||||
previous = buildTextBlock(previous.getSequences(), 0);
|
||||
previous.setToDuplicate(toDuplicate);
|
||||
if (current.getClassification() != null && previous.getClassification() == null) {
|
||||
previous.setClassification(current.getClassification());
|
||||
}
|
||||
itty.remove();
|
||||
itty.previous();
|
||||
itty.set(previous);
|
||||
itty.next();
|
||||
return previous;
|
||||
}
|
||||
|
||||
|
||||
@@ -203,221 +254,72 @@ public class DocstrumBlockificationService {
|
||||
}
|
||||
|
||||
|
||||
public void mergeIntersectingBlocks(List<AbstractPageBlock> blocks, float xThreshold, float yThreshold) {
|
||||
public void mergeIntersectingBlocks(ClassificationPage page, CleanRulings usedRulings, float xThreshold, float yThreshold) {
|
||||
|
||||
var blocks = page.getTextBlocks();
|
||||
ListIterator<AbstractPageBlock> itty = blocks.listIterator();
|
||||
Set<AbstractPageBlock> toRemove = new HashSet<>();
|
||||
while (itty.hasNext()) {
|
||||
AbstractPageBlock block = itty.next();
|
||||
if (block == null) {
|
||||
continue;
|
||||
}
|
||||
if (block instanceof TablePageBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (block.getClassification() != null && block.getClassification().isHeadline()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TextPageBlock current = (TextPageBlock) block;
|
||||
|
||||
if (current.isToDuplicate()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < blocks.size(); i++) {
|
||||
|
||||
if (toRemove.contains(blocks.get(i))) {
|
||||
AbstractPageBlock abstractPageBlock = blocks.get(i);
|
||||
if (abstractPageBlock == null) {
|
||||
continue;
|
||||
}
|
||||
if (blocks.get(i) == current) {
|
||||
if (abstractPageBlock == current) {
|
||||
continue;
|
||||
}
|
||||
if (blocks.get(i) instanceof TablePageBlock) {
|
||||
if (abstractPageBlock instanceof TablePageBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TextPageBlock inner = (TextPageBlock) blocks.get(i);
|
||||
|
||||
if (inner.isToDuplicate()) {
|
||||
if (abstractPageBlock.getClassification() != null && abstractPageBlock.getClassification().isHeadline()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.getDir() == inner.getDir() && current.almostIntersects(inner, yThreshold, xThreshold)) {
|
||||
TextPageBlock inner = (TextPageBlock) abstractPageBlock;
|
||||
|
||||
if (usedRulings.lineBetween(current, blocks.get(i))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.getDir() == inner.getDir() && current.intersects(inner, yThreshold, xThreshold)) {
|
||||
|
||||
boolean toDuplicate = current.isToDuplicate() || inner.isToDuplicate();
|
||||
current.getSequences().addAll(inner.getSequences());
|
||||
QuickSort.sort(current.getSequences(), new TextPositionSequenceComparator());
|
||||
current = buildTextBlock(current.getSequences(), 0);
|
||||
toRemove.add(inner);
|
||||
|
||||
current.setToDuplicate(toDuplicate);
|
||||
blocks.set(i, null);
|
||||
itty.set(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
blocks.removeAll(toRemove);
|
||||
var blocksIterator = blocks.iterator();
|
||||
while (blocksIterator.hasNext()) {
|
||||
if (blocksIterator.next() == null) {
|
||||
blocksIterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public List<AbstractPageBlock> splitZonesAtRulings(List<TextPositionSequence> textPositions, List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {
|
||||
public static TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList, int indexOnPage) {
|
||||
|
||||
int indexOnPage = 0;
|
||||
List<TextPositionSequence> chunkWords = new ArrayList<>();
|
||||
List<AbstractPageBlock> chunkBlockList = new ArrayList<>();
|
||||
|
||||
float minX = 1000, maxX = 0, minY = 1000, maxY = 0;
|
||||
TextPositionSequence prev = null;
|
||||
|
||||
for (TextPositionSequence word : textPositions) {
|
||||
|
||||
boolean isSplitByRuling = isSplitByRuling(minX, minY, maxX, maxY, word, horizontalRulingLines, verticalRulingLines);
|
||||
boolean splitByDir = prev != null && !prev.getDir().equals(word.getDir());
|
||||
|
||||
if (prev != null && (splitByDir || isSplitByRuling)) {
|
||||
|
||||
TextPageBlock cb1 = buildTextBlock(chunkWords, indexOnPage);
|
||||
indexOnPage++;
|
||||
|
||||
chunkBlockList.add(cb1);
|
||||
chunkWords = new ArrayList<>();
|
||||
|
||||
minX = 1000;
|
||||
maxX = 0;
|
||||
minY = 1000;
|
||||
maxY = 0;
|
||||
prev = null;
|
||||
}
|
||||
|
||||
chunkWords.add(word);
|
||||
|
||||
prev = word;
|
||||
if (word.getMinXDirAdj() < minX) {
|
||||
minX = word.getMinXDirAdj();
|
||||
}
|
||||
if (word.getMaxXDirAdj() > maxX) {
|
||||
maxX = word.getMaxXDirAdj();
|
||||
}
|
||||
if (word.getMinYDirAdj() < minY) {
|
||||
minY = word.getMinYDirAdj();
|
||||
}
|
||||
if (word.getMaxYDirAdj() > maxY) {
|
||||
maxY = word.getMaxYDirAdj();
|
||||
}
|
||||
}
|
||||
|
||||
TextPageBlock cb1 = buildTextBlock(chunkWords, indexOnPage);
|
||||
if (cb1 != null) {
|
||||
chunkBlockList.add(cb1);
|
||||
}
|
||||
|
||||
return chunkBlockList;
|
||||
}
|
||||
|
||||
|
||||
private boolean equalsWithThreshold(float f1, float f2) {
|
||||
|
||||
return Math.abs(f1 - f2) < THRESHOLD;
|
||||
}
|
||||
|
||||
|
||||
private TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList, int indexOnPage) {
|
||||
|
||||
TextPageBlock textBlock = null;
|
||||
|
||||
FloatFrequencyCounter lineHeightFrequencyCounter = new FloatFrequencyCounter();
|
||||
FloatFrequencyCounter fontSizeFrequencyCounter = new FloatFrequencyCounter();
|
||||
FloatFrequencyCounter spaceFrequencyCounter = new FloatFrequencyCounter();
|
||||
StringFrequencyCounter fontFrequencyCounter = new StringFrequencyCounter();
|
||||
StringFrequencyCounter styleFrequencyCounter = new StringFrequencyCounter();
|
||||
|
||||
for (TextPositionSequence wordBlock : wordBlockList) {
|
||||
|
||||
lineHeightFrequencyCounter.add(wordBlock.getTextHeight());
|
||||
fontSizeFrequencyCounter.add(wordBlock.getFontSize());
|
||||
spaceFrequencyCounter.add(wordBlock.getSpaceWidth());
|
||||
fontFrequencyCounter.add(wordBlock.getFont());
|
||||
styleFrequencyCounter.add(wordBlock.getFontStyle());
|
||||
|
||||
if (textBlock == null) {
|
||||
textBlock = new TextPageBlock(wordBlock.getMinXDirAdj(),
|
||||
wordBlock.getMaxXDirAdj(),
|
||||
wordBlock.getMinYDirAdj(),
|
||||
wordBlock.getMaxYDirAdj(),
|
||||
wordBlockList,
|
||||
wordBlock.getRotation());
|
||||
} else {
|
||||
TextPageBlock spatialEntity = textBlock.union(wordBlock);
|
||||
textBlock.resize(spatialEntity.getMinX(), spatialEntity.getMinY(), spatialEntity.getWidth(), spatialEntity.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
if (textBlock != null) {
|
||||
textBlock.setMostPopularWordFont(fontFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordStyle(styleFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordFontSize(fontSizeFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordHeight(lineHeightFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordSpaceWidth(spaceFrequencyCounter.getMostPopular());
|
||||
textBlock.setHighestFontSize(fontSizeFrequencyCounter.getHighest());
|
||||
}
|
||||
|
||||
if (textBlock != null && textBlock.getSequences() != null && textBlock.getSequences().stream().map(t -> round(t.getMinYDirAdj(), 3)).collect(toSet()).size() == 1) {
|
||||
textBlock.getSequences().sort(Comparator.comparing(TextPositionSequence::getMinXDirAdj));
|
||||
}
|
||||
return textBlock;
|
||||
}
|
||||
|
||||
|
||||
private boolean isSplitByRuling(float minX,
|
||||
float minY,
|
||||
float maxX,
|
||||
float maxY,
|
||||
TextPositionSequence word,
|
||||
List<Ruling> horizontalRulingLines,
|
||||
List<Ruling> verticalRulingLines) {
|
||||
|
||||
return isSplitByRuling(maxX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMinYDirAdj(),
|
||||
verticalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()) //
|
||||
|| isSplitByRuling(minX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMaxYDirAdj(),
|
||||
horizontalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()) //
|
||||
|| isSplitByRuling(maxX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMinYDirAdj(),
|
||||
horizontalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()) //
|
||||
|| isSplitByRuling(minX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMaxYDirAdj(),
|
||||
verticalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight());
|
||||
}
|
||||
|
||||
|
||||
private boolean isSplitByRuling(float previousX2, float previousY1, float currentX1, float currentY1, List<Ruling> rulingLines, float dir, float pageWidth, float pageHeight) {
|
||||
|
||||
for (Ruling ruling : rulingLines) {
|
||||
var line = RulingTextDirAdjustUtil.convertToDirAdj(ruling, dir, pageWidth, pageHeight);
|
||||
if (line.intersectsLine(previousX2, previousY1, currentX1, currentY1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private double round(float value, int decimalPoints) {
|
||||
|
||||
var d = Math.pow(10, decimalPoints);
|
||||
return Math.round(value * d) / d;
|
||||
return new TextPageBlock(wordBlockList);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+24
-133
@@ -15,11 +15,10 @@ import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBl
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.FloatFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.Orientation;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.StringFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RulingTextDirAdjustUtil;
|
||||
|
||||
@Service
|
||||
public class DocuMineBlockificationService {
|
||||
@@ -34,15 +33,16 @@ public class DocuMineBlockificationService {
|
||||
* This method must use text direction adjusted postions (DirAdj). Where {0,0} is on the upper left. Never try to change this!
|
||||
* Rulings (Table lines) must be adjusted to the text directions as well, when checking if a block is split by a ruling.
|
||||
*
|
||||
* @param textPositions The words of a page.
|
||||
* @param horizontalRulingLines Horizontal table lines.
|
||||
* @param verticalRulingLines Vertical table lines.
|
||||
* @param textPositions The textPositions of a page.
|
||||
* @param cleanRulings All rulings on a page
|
||||
* @return Page object that contains the Textblock and text statistics.
|
||||
*/
|
||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions, List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {
|
||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions, CleanRulings cleanRulings) {
|
||||
|
||||
List<TextPositionSequence> chunkWords = new ArrayList<>();
|
||||
List<AbstractPageBlock> chunkBlockList1 = new ArrayList<>();
|
||||
List<AbstractPageBlock> textPageBlocks = new ArrayList<>();
|
||||
|
||||
CleanRulings usedRulings = cleanRulings.withoutTextRulings();
|
||||
|
||||
float minX = 1000;
|
||||
float maxX = 0;
|
||||
@@ -59,23 +59,26 @@ public class DocuMineBlockificationService {
|
||||
boolean splitByX = prev != null && maxX + 50 < word.getMinXDirAdj() && prev.getMinYDirAdj() == word.getMinYDirAdj();
|
||||
boolean negativeXGap = prev != null && word.getMinXDirAdj() - minX < -5;
|
||||
boolean newLineAfterSplit = prev != null && word.getMinYDirAdj() != prev.getMinYDirAdj() && wasSplitted && splitX1 != word.getMinXDirAdj();
|
||||
boolean isSplitByRuling = isSplitByRuling(minX, minY, maxX, maxY, word, horizontalRulingLines, verticalRulingLines);
|
||||
boolean isSplitByRuling = prev != null && usedRulings.lineBetween(prev, word);
|
||||
boolean splitByDir = prev != null && !prev.getDir().equals(word.getDir());
|
||||
boolean splitByOtherFontAndOtherY = prev != null && prev.getMaxYDirAdj() != word.getMaxYDirAdj() && (word.getFontStyle().contains("bold") && !prev.getFontStyle()
|
||||
boolean splitByOtherFontAndOtherY = prev != null && Math.abs(prev.getMaxYDirAdj() - word.getMaxYDirAdj()) > word.getTextHeight() * 0.2 && (word.getFontStyle()
|
||||
.contains("bold")
|
||||
&& !prev.getFontStyle()
|
||||
.contains("bold") || prev.getFontStyle().contains("bold") && !word.getFontStyle().contains("bold"));
|
||||
|
||||
Matcher matcher = pattern.matcher(chunkWords.stream().collect(Collectors.joining(" ")).toString());
|
||||
Matcher matcher = pattern.matcher(chunkWords.stream()
|
||||
.collect(Collectors.joining(" ")).toString());
|
||||
boolean startsOnSameX = Math.abs(minX - word.getMinXDirAdj()) < 5 && matcher.matches();
|
||||
|
||||
if (prev != null && (lineSeparation || startFromTop || splitByDir || isSplitByRuling || splitByOtherFontAndOtherY || negativeXGap || startsOnSameX)) {
|
||||
|
||||
Orientation prevOrientation = null;
|
||||
if (!chunkBlockList1.isEmpty()) {
|
||||
prevOrientation = chunkBlockList1.get(chunkBlockList1.size() - 1).getOrientation();
|
||||
if (!textPageBlocks.isEmpty()) {
|
||||
prevOrientation = textPageBlocks.get(textPageBlocks.size() - 1).getOrientation();
|
||||
}
|
||||
|
||||
TextPageBlock cb1 = buildTextBlock(chunkWords);
|
||||
chunkBlockList1.add(cb1);
|
||||
TextPageBlock cb1 = new TextPageBlock(chunkWords);
|
||||
textPageBlocks.add(cb1);
|
||||
chunkWords = new ArrayList<>();
|
||||
|
||||
if (splitByX && !isSplitByRuling) {
|
||||
@@ -86,7 +89,11 @@ public class DocuMineBlockificationService {
|
||||
wasSplitted = false;
|
||||
cb1.setOrientation(Orientation.RIGHT);
|
||||
splitX1 = null;
|
||||
} else if (prevOrientation != null && prevOrientation.equals(Orientation.RIGHT) && (lineSeparation || !startFromTop || !splitByX || !newLineAfterSplit || !isSplitByRuling)) {
|
||||
} else if (prevOrientation != null && prevOrientation.equals(Orientation.RIGHT) && (lineSeparation
|
||||
|| !startFromTop
|
||||
|| !splitByX
|
||||
|| !newLineAfterSplit
|
||||
|| !isSplitByRuling)) {
|
||||
cb1.setOrientation(Orientation.LEFT);
|
||||
}
|
||||
|
||||
@@ -114,128 +121,12 @@ public class DocuMineBlockificationService {
|
||||
}
|
||||
}
|
||||
|
||||
TextPageBlock cb1 = buildTextBlock(chunkWords);
|
||||
if (cb1 != null) {
|
||||
chunkBlockList1.add(cb1);
|
||||
}
|
||||
textPageBlocks.add(new TextPageBlock(chunkWords));
|
||||
|
||||
return new ClassificationPage(chunkBlockList1);
|
||||
return new ClassificationPage(textPageBlocks);
|
||||
}
|
||||
|
||||
|
||||
private boolean equalsWithThreshold(float f1, float f2) {
|
||||
|
||||
return Math.abs(f1 - f2) < THRESHOLD;
|
||||
}
|
||||
|
||||
|
||||
private TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList) {
|
||||
|
||||
TextPageBlock textBlock = null;
|
||||
|
||||
FloatFrequencyCounter lineHeightFrequencyCounter = new FloatFrequencyCounter();
|
||||
FloatFrequencyCounter fontSizeFrequencyCounter = new FloatFrequencyCounter();
|
||||
FloatFrequencyCounter spaceFrequencyCounter = new FloatFrequencyCounter();
|
||||
StringFrequencyCounter fontFrequencyCounter = new StringFrequencyCounter();
|
||||
StringFrequencyCounter styleFrequencyCounter = new StringFrequencyCounter();
|
||||
|
||||
for (TextPositionSequence wordBlock : wordBlockList) {
|
||||
|
||||
lineHeightFrequencyCounter.add(wordBlock.getTextHeight());
|
||||
fontSizeFrequencyCounter.add(wordBlock.getFontSize());
|
||||
spaceFrequencyCounter.add(wordBlock.getSpaceWidth());
|
||||
fontFrequencyCounter.add(wordBlock.getFont());
|
||||
styleFrequencyCounter.add(wordBlock.getFontStyle());
|
||||
|
||||
if (textBlock == null) {
|
||||
textBlock = new TextPageBlock(wordBlock.getMinXDirAdj(),
|
||||
wordBlock.getMaxXDirAdj(),
|
||||
wordBlock.getMinYDirAdj(),
|
||||
wordBlock.getMaxYDirAdj(),
|
||||
wordBlockList,
|
||||
wordBlock.getRotation());
|
||||
} else {
|
||||
TextPageBlock spatialEntity = textBlock.union(wordBlock);
|
||||
textBlock.resize(spatialEntity.getMinX(), spatialEntity.getMinY(), spatialEntity.getWidth(), spatialEntity.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
if (textBlock != null) {
|
||||
textBlock.setMostPopularWordFont(fontFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordStyle(styleFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordFontSize(fontSizeFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordHeight(lineHeightFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordSpaceWidth(spaceFrequencyCounter.getMostPopular());
|
||||
textBlock.setHighestFontSize(fontSizeFrequencyCounter.getHighest());
|
||||
}
|
||||
|
||||
if (textBlock != null && textBlock.getSequences() != null && textBlock.getSequences().stream().map(t -> round(t.getMinYDirAdj(), 3)).collect(toSet()).size() == 1) {
|
||||
textBlock.getSequences().sort(Comparator.comparing(TextPositionSequence::getMinXDirAdj));
|
||||
}
|
||||
return textBlock;
|
||||
}
|
||||
|
||||
|
||||
private boolean isSplitByRuling(float minX,
|
||||
float minY,
|
||||
float maxX,
|
||||
float maxY,
|
||||
TextPositionSequence word,
|
||||
List<Ruling> horizontalRulingLines,
|
||||
List<Ruling> verticalRulingLines) {
|
||||
|
||||
return isSplitByRuling(maxX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMinYDirAdj(),
|
||||
verticalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()) //
|
||||
|| isSplitByRuling(minX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMaxYDirAdj(),
|
||||
horizontalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()) //
|
||||
|| isSplitByRuling(maxX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMinYDirAdj(),
|
||||
horizontalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()) //
|
||||
|| isSplitByRuling(minX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMaxYDirAdj(),
|
||||
verticalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()); //
|
||||
}
|
||||
|
||||
|
||||
private boolean isSplitByRuling(float previousX2, float previousY1, float currentX1, float currentY1, List<Ruling> rulingLines, float dir, float pageWidth, float pageHeight) {
|
||||
|
||||
for (Ruling ruling : rulingLines) {
|
||||
var line = RulingTextDirAdjustUtil.convertToDirAdj(ruling, dir, pageWidth, pageHeight);
|
||||
if (line.intersectsLine(previousX2, previousY1, currentX1, currentY1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private double round(float value, int decimalPoints) {
|
||||
|
||||
var d = Math.pow(10, decimalPoints);
|
||||
return Math.round(value * d) / d;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+27
-123
@@ -13,11 +13,11 @@ import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBl
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.FloatFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.Orientation;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.StringFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RulingTextDirAdjustUtil;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
|
||||
@SuppressWarnings("all")
|
||||
@Service
|
||||
@@ -31,12 +31,13 @@ public class RedactManagerBlockificationService {
|
||||
* This method must use text direction adjusted postions (DirAdj). Where {0,0} is on the upper left. Never try to change this!
|
||||
* Rulings (TablePageBlock lines) must be adjusted to the text directions as well, when checking if a block is split by a ruling.
|
||||
*
|
||||
* @param textPositions The words of a page.
|
||||
* @param horizontalRulingLines Horizontal table lines.
|
||||
* @param verticalRulingLines Vertical table lines.
|
||||
* @param textPositions The words of a page.
|
||||
* @param visualizations
|
||||
* @return Page object that contains the Textblock and text statistics.
|
||||
*/
|
||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions, List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {
|
||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions, CleanRulings cleanRulings, LayoutparsingVisualizations visualizations) {
|
||||
|
||||
CleanRulings usedRulings = cleanRulings.withoutTextRulings();
|
||||
|
||||
int indexOnPage = 0;
|
||||
List<TextPositionSequence> chunkWords = new ArrayList<>();
|
||||
@@ -54,7 +55,7 @@ public class RedactManagerBlockificationService {
|
||||
boolean splitByX = prev != null && maxX + 50 < word.getMinXDirAdj() && prev.getMinYDirAdj() == word.getMinYDirAdj();
|
||||
boolean xIsBeforeFirstX = prev != null && word.getMinXDirAdj() < minX;
|
||||
boolean newLineAfterSplit = prev != null && word.getMinYDirAdj() != prev.getMinYDirAdj() && wasSplitted && splitX1 != word.getMinXDirAdj();
|
||||
boolean isSplitByRuling = isSplitByRuling(minX, minY, maxX, maxY, word, horizontalRulingLines, verticalRulingLines);
|
||||
boolean isSplitByRuling = prev != null && usedRulings.lineBetween(prev, word);
|
||||
boolean splitByDir = prev != null && !prev.getDir().equals(word.getDir());
|
||||
|
||||
if (prev != null && (lineSeparation || startFromTop || splitByX || splitByDir || isSplitByRuling)) {
|
||||
@@ -64,7 +65,7 @@ public class RedactManagerBlockificationService {
|
||||
prevOrientation = chunkBlockList.get(chunkBlockList.size() - 1).getOrientation();
|
||||
}
|
||||
|
||||
TextPageBlock cb1 = buildTextBlock(chunkWords, indexOnPage);
|
||||
TextPageBlock cb1 = new TextPageBlock(chunkWords);
|
||||
indexOnPage++;
|
||||
|
||||
chunkBlockList.add(cb1);
|
||||
@@ -78,7 +79,11 @@ public class RedactManagerBlockificationService {
|
||||
wasSplitted = false;
|
||||
cb1.setOrientation(Orientation.RIGHT);
|
||||
splitX1 = null;
|
||||
} else if (prevOrientation != null && prevOrientation.equals(Orientation.RIGHT) && (lineSeparation || !startFromTop || !splitByX || !newLineAfterSplit || !isSplitByRuling)) {
|
||||
} else if (prevOrientation != null && prevOrientation.equals(Orientation.RIGHT) && (lineSeparation
|
||||
|| !startFromTop
|
||||
|| !splitByX
|
||||
|| !newLineAfterSplit
|
||||
|| !isSplitByRuling)) {
|
||||
cb1.setOrientation(Orientation.LEFT);
|
||||
}
|
||||
|
||||
@@ -106,8 +111,8 @@ public class RedactManagerBlockificationService {
|
||||
}
|
||||
}
|
||||
|
||||
TextPageBlock cb1 = buildTextBlock(chunkWords, indexOnPage);
|
||||
if (cb1 != null) {
|
||||
if (!chunkWords.isEmpty()) {
|
||||
TextPageBlock cb1 = new TextPageBlock(chunkWords);
|
||||
chunkBlockList.add(cb1);
|
||||
}
|
||||
|
||||
@@ -147,8 +152,11 @@ public class RedactManagerBlockificationService {
|
||||
TextPageBlock block = (TextPageBlock) itty.next();
|
||||
|
||||
if (previous != null && previous.getOrientation().equals(Orientation.LEFT) && block.getOrientation().equals(Orientation.LEFT) && equalsWithThreshold(block.getMaxY(),
|
||||
previous.getMaxY()) || previous != null && previous.getOrientation().equals(Orientation.LEFT) && block.getOrientation()
|
||||
.equals(Orientation.RIGHT) && equalsWithThreshold(block.getMaxY(), previous.getMaxY())) {
|
||||
previous.getMaxY())
|
||||
|| previous != null
|
||||
&& previous.getOrientation().equals(Orientation.LEFT)
|
||||
&& block.getOrientation().equals(Orientation.RIGHT)
|
||||
&& equalsWithThreshold(block.getMaxY(), previous.getMaxY())) {
|
||||
previous.add(block);
|
||||
itty.remove();
|
||||
continue;
|
||||
@@ -156,123 +164,19 @@ public class RedactManagerBlockificationService {
|
||||
|
||||
previous = block;
|
||||
}
|
||||
if (!textPositions.isEmpty()) {
|
||||
visualizations.addTextBlockVisualizations(chunkBlockList.stream()
|
||||
.map(tb -> (TextPageBlock) tb)
|
||||
.toList(), textPositions.get(0).getPage());
|
||||
}
|
||||
|
||||
return new ClassificationPage(chunkBlockList);
|
||||
}
|
||||
|
||||
|
||||
private boolean equalsWithThreshold(float f1, float f2) {
|
||||
private boolean equalsWithThreshold(double f1, double f2) {
|
||||
|
||||
return Math.abs(f1 - f2) < THRESHOLD;
|
||||
}
|
||||
|
||||
|
||||
private TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList, int indexOnPage) {
|
||||
|
||||
TextPageBlock textBlock = null;
|
||||
|
||||
FloatFrequencyCounter lineHeightFrequencyCounter = new FloatFrequencyCounter();
|
||||
FloatFrequencyCounter fontSizeFrequencyCounter = new FloatFrequencyCounter();
|
||||
FloatFrequencyCounter spaceFrequencyCounter = new FloatFrequencyCounter();
|
||||
StringFrequencyCounter fontFrequencyCounter = new StringFrequencyCounter();
|
||||
StringFrequencyCounter styleFrequencyCounter = new StringFrequencyCounter();
|
||||
|
||||
for (TextPositionSequence wordBlock : wordBlockList) {
|
||||
|
||||
lineHeightFrequencyCounter.add(wordBlock.getTextHeight());
|
||||
fontSizeFrequencyCounter.add(wordBlock.getFontSize());
|
||||
spaceFrequencyCounter.add(wordBlock.getSpaceWidth());
|
||||
fontFrequencyCounter.add(wordBlock.getFont());
|
||||
styleFrequencyCounter.add(wordBlock.getFontStyle());
|
||||
|
||||
if (textBlock == null) {
|
||||
textBlock = new TextPageBlock(wordBlock.getMinXDirAdj(),
|
||||
wordBlock.getMaxXDirAdj(),
|
||||
wordBlock.getMinYDirAdj(),
|
||||
wordBlock.getMaxYDirAdj(),
|
||||
wordBlockList,
|
||||
wordBlock.getRotation());
|
||||
} else {
|
||||
TextPageBlock spatialEntity = textBlock.union(wordBlock);
|
||||
textBlock.resize(spatialEntity.getMinX(), spatialEntity.getMinY(), spatialEntity.getWidth(), spatialEntity.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
if (textBlock != null) {
|
||||
textBlock.setMostPopularWordFont(fontFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordStyle(styleFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordFontSize(fontSizeFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordHeight(lineHeightFrequencyCounter.getMostPopular());
|
||||
textBlock.setMostPopularWordSpaceWidth(spaceFrequencyCounter.getMostPopular());
|
||||
textBlock.setHighestFontSize(fontSizeFrequencyCounter.getHighest());
|
||||
}
|
||||
|
||||
if (textBlock != null && textBlock.getSequences() != null && textBlock.getSequences().stream().map(t -> round(t.getMinYDirAdj(), 3)).collect(toSet()).size() == 1) {
|
||||
textBlock.getSequences().sort(Comparator.comparing(TextPositionSequence::getMinXDirAdj));
|
||||
}
|
||||
return textBlock;
|
||||
}
|
||||
|
||||
|
||||
private boolean isSplitByRuling(float minX,
|
||||
float minY,
|
||||
float maxX,
|
||||
float maxY,
|
||||
TextPositionSequence word,
|
||||
List<Ruling> horizontalRulingLines,
|
||||
List<Ruling> verticalRulingLines) {
|
||||
|
||||
return isSplitByRuling(maxX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMinYDirAdj(),
|
||||
verticalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()) //
|
||||
|| isSplitByRuling(minX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMaxYDirAdj(),
|
||||
horizontalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()) //
|
||||
|| isSplitByRuling(maxX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMinYDirAdj(),
|
||||
horizontalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight()) //
|
||||
|| isSplitByRuling(minX,
|
||||
minY,
|
||||
word.getMinXDirAdj(),
|
||||
word.getMaxYDirAdj(),
|
||||
verticalRulingLines,
|
||||
word.getDir().getDegrees(),
|
||||
word.getPageWidth(),
|
||||
word.getPageHeight());
|
||||
}
|
||||
|
||||
|
||||
private boolean isSplitByRuling(float previousX2, float previousY1, float currentX1, float currentY1, List<Ruling> rulingLines, float dir, float pageWidth, float pageHeight) {
|
||||
|
||||
for (Ruling ruling : rulingLines) {
|
||||
var line = RulingTextDirAdjustUtil.convertToDirAdj(ruling, dir, pageWidth, pageHeight);
|
||||
if (line.intersectsLine(previousX2, previousY1, currentX1, currentY1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private double round(float value, int decimalPoints) {
|
||||
|
||||
var d = Math.pow(10, decimalPoints);
|
||||
return Math.round(value * d) / d;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+14
-6
@@ -27,26 +27,32 @@ public class ClarifyndClassificationService {
|
||||
|
||||
log.debug("Document FontSize counters are: {}", document.getFontSizeCounter().getCountPerValue());
|
||||
|
||||
HeadlineClassificationService headlineClassificationService = new HeadlineClassificationService();
|
||||
|
||||
for (ClassificationPage page : document.getPages()) {
|
||||
classifyPage(page, document, headlineFontSizes);
|
||||
classifyPage(headlineClassificationService, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyPage(ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyPage(HeadlineClassificationService headlineClassificationService, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
for (AbstractPageBlock textBlock : page.getTextBlocks()) {
|
||||
if (textBlock instanceof TextPageBlock) {
|
||||
classifyBlock((TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
classifyBlock(headlineClassificationService, (TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyBlock(TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyBlock(HeadlineClassificationService headlineClassificationService, TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
var bodyTextFrame = page.getBodyTextFrame();
|
||||
|
||||
if (textBlock.getClassification() != null && textBlock.getClassification().isHeadline()) {
|
||||
headlineClassificationService.setLastHeadlineFromOutline(textBlock);
|
||||
return;
|
||||
}
|
||||
if (document.getFontSizeCounter().getMostPopular() == null) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH);
|
||||
return;
|
||||
@@ -79,7 +85,8 @@ public class ClarifyndClassificationService {
|
||||
|
||||
for (int i = 1; i <= headlineFontSizes.size(); i++) {
|
||||
if (textBlock.getMostPopularWordFontSize() == headlineFontSizes.get(i - 1)) {
|
||||
textBlock.setClassification(PageBlockType.getHeadlineType(i));
|
||||
PageBlockType headlineType = PageBlockType.getHeadlineType(i);
|
||||
headlineClassificationService.classifyHeadline(textBlock, headlineType);
|
||||
document.setHeadlines(true);
|
||||
}
|
||||
}
|
||||
@@ -89,7 +96,8 @@ public class ClarifyndClassificationService {
|
||||
.getTextPositions()
|
||||
.get(0)
|
||||
.getFontSizeInPt() >= textBlock.getMostPopularWordFontSize()) {
|
||||
textBlock.setClassification(PageBlockType.getHeadlineType(headlineFontSizes.size() + 1));
|
||||
PageBlockType headlineType = PageBlockType.getHeadlineType(headlineFontSizes.size() + 1);
|
||||
headlineClassificationService.classifyHeadline(textBlock, headlineType);
|
||||
document.setHeadlines(true);
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter()
|
||||
.getMostPopular() && textBlock.getMostPopularWordStyle().equals("bold") && !document.getFontStyleCounter().getMostPopular().equals("bold")) {
|
||||
|
||||
+67
-38
@@ -5,7 +5,6 @@ import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.MarkedContentUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
@@ -13,6 +12,8 @@ import com.knecon.fforesight.service.layoutparser.processor.model.Classification
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
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.HeaderFooterDetection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.MarkedContentUtils;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.PositionUtils;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -23,9 +24,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class DocuMineClassificationService {
|
||||
|
||||
private static final Pattern pattern = Pattern.compile("^(\\d{1,2}\\.){1,3}\\d{1,2}\\.?\\s[0-9A-Za-z \\[\\]]{2,50}", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern pattern2 = Pattern.compile("\\p{L}{3,}", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern pattern3 = Pattern.compile("^(\\d{1,1}\\.){1,3}\\d{1,2}\\.?\\s[a-z]{1,2}\\/[a-z]{1,2}.*");
|
||||
private static final Pattern HEADLINE_WITH_IDENTIFER_PATTERN = Pattern.compile("^([1-9]\\d?\\.){0,3}\\d{1,2}\\.?\\s[0-9A-Za-z \\[\\]]{2,50}", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern AT_LEAST_3_PATTERN = Pattern.compile("\\p{L}{3,}", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern HEADLINE_PATTTERN_WITH_SLASHES = Pattern.compile("^(\\d{1,1}\\.){0,3}\\d{1,2}\\.?\\s[a-z]{1,2}\\/[a-z]{1,2}.*");
|
||||
|
||||
|
||||
public void classifyDocument(ClassificationDocument document) {
|
||||
@@ -34,76 +35,104 @@ public class DocuMineClassificationService {
|
||||
|
||||
log.debug("Document FontSize counters are: {}", document.getFontSizeCounter().getCountPerValue());
|
||||
|
||||
HeadlineClassificationService headlineClassificationService = new HeadlineClassificationService();
|
||||
|
||||
for (ClassificationPage page : document.getPages()) {
|
||||
classifyPage(page, document, headlineFontSizes);
|
||||
classifyPage(headlineClassificationService, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyPage(ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyPage(HeadlineClassificationService headlineClassificationService, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
for (AbstractPageBlock textBlock : page.getTextBlocks()) {
|
||||
if (textBlock instanceof TextPageBlock) {
|
||||
classifyBlock((TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
classifyBlock(headlineClassificationService, (TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyBlock(TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyBlock(HeadlineClassificationService headlineClassificationService, TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
log.debug("headlineFontSizes: {}", headlineFontSizes);
|
||||
var bodyTextFrame = page.getBodyTextFrame();
|
||||
|
||||
Matcher matcher = pattern.matcher(textBlock.toString());
|
||||
Matcher matcher2 = pattern2.matcher(textBlock.toString());
|
||||
Matcher matcher3 = pattern3.matcher(textBlock.toString());
|
||||
Matcher headlineWithIdentifierMatcher = HEADLINE_WITH_IDENTIFER_PATTERN.matcher(textBlock.toString());
|
||||
Matcher atLeast3Matcher = AT_LEAST_3_PATTERN.matcher(textBlock.toString());
|
||||
Matcher headlineWithSlashesMatcher = HEADLINE_PATTTERN_WITH_SLASHES.matcher(textBlock.toString());
|
||||
|
||||
if (textBlock.getClassification() != null && textBlock.getClassification().isHeadline()) {
|
||||
headlineClassificationService.setLastHeadlineFromOutline(textBlock);
|
||||
return;
|
||||
}
|
||||
if (document.getFontSizeCounter().getMostPopular() == null) {
|
||||
textBlock.setClassification(PageBlockType.OTHER);
|
||||
return;
|
||||
}
|
||||
if (MarkedContentUtils.intersects(textBlock, page.getMarkedContentBboxPerType(), MarkedContentUtils.HEADER)
|
||||
|| PositionUtils.isOverBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter()
|
||||
.getMostPopular() == null || textBlock.getHighestFontSize() <= document.getFontSizeCounter().getMostPopular())
|
||||
) {
|
||||
|| (PositionUtils.isOverBodyTextFrame(bodyTextFrame,
|
||||
textBlock,
|
||||
page.getRotation()) && (document.getFontSizeCounter().getMostPopular()
|
||||
== null
|
||||
|| textBlock.getHighestFontSize()
|
||||
<= document.getFontSizeCounter()
|
||||
.getMostPopular()))) {
|
||||
textBlock.setClassification(PageBlockType.HEADER);
|
||||
|
||||
} else if (MarkedContentUtils.intersects(textBlock, page.getMarkedContentBboxPerType(), MarkedContentUtils.FOOTER)
|
||||
|| PositionUtils.isUnderBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter()
|
||||
.getMostPopular() == null || textBlock.getHighestFontSize() <= document.getFontSizeCounter().getMostPopular())
|
||||
) {
|
||||
|| (PositionUtils.isUnderBodyTextFrame(bodyTextFrame,
|
||||
textBlock,
|
||||
page.getRotation())
|
||||
&& (document.getFontSizeCounter().getMostPopular()
|
||||
== null
|
||||
|| textBlock.getHighestFontSize()
|
||||
<= document.getFontSizeCounter()
|
||||
.getMostPopular()))
|
||||
|| HeaderFooterDetection.isLikelyFooter(textBlock, document, page)) {
|
||||
textBlock.setClassification(PageBlockType.FOOTER);
|
||||
} else if (page.getPageNumber() == 1 && (PositionUtils.getHeightDifferenceBetweenChunkWordAndDocumentWord(textBlock,
|
||||
document.getTextHeightCounter().getMostPopular()) > 2.5 && textBlock.getHighestFontSize() > document.getFontSizeCounter().getMostPopular() || page.getTextBlocks()
|
||||
.size() == 1)) {
|
||||
} else if (page.getPageNumber() == 1 && (PositionUtils.getHeightDifferenceBetweenChunkWordAndDocumentWord(textBlock, document.getTextHeightCounter().getMostPopular()) > 2.5
|
||||
&& textBlock.getHighestFontSize() > document.getFontSizeCounter().getMostPopular() || page.getTextBlocks().size() == 1)) {
|
||||
if (!Pattern.matches("[0-9]+", textBlock.toString())) {
|
||||
textBlock.setClassification(PageBlockType.TITLE);
|
||||
}
|
||||
} else if (textBlock.getText().length() > 5 && (textBlock.getMostPopularWordHeight() > document.getTextHeightCounter()
|
||||
.getMostPopular() || textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter().getMostPopular()) && PositionUtils.getApproxLineCount(textBlock) < 5.9
|
||||
} else if (textBlock.getText().length() > 5
|
||||
&& (textBlock.getMostPopularWordHeight() > document.getTextHeightCounter().getMostPopular()
|
||||
|| textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter().getMostPopular())
|
||||
&& PositionUtils.getApproxLineCount(textBlock) < 5.9
|
||||
|
||||
&& (textBlock.getMostPopularWordStyle().contains("bold") && Character.isDigit(textBlock.toString().charAt(0)) && !matcher2.matches() && !textBlock.toString()
|
||||
.contains(":") || textBlock.toString().equals(textBlock.toString().toUpperCase(Locale.ROOT)) && !matcher2.matches() && !textBlock.toString()
|
||||
.contains(":") || textBlock.toString().startsWith("APPENDIX") || textBlock.toString().startsWith("FIGURE") || textBlock.toString()
|
||||
.startsWith("TABLE")) && !textBlock.toString().endsWith(":") && matcher2.find()) {
|
||||
textBlock.setClassification(PageBlockType.getHeadlineType(1));
|
||||
&& (textBlock.getMostPopularWordStyle().contains("bold") && Character.isDigit(textBlock.toString().charAt(0)) && atLeast3Matcher.reset().find() && !textBlock.toString()
|
||||
.contains(":")
|
||||
|| textBlock.toString().equals(textBlock.toString().toUpperCase(Locale.ROOT)) && atLeast3Matcher.reset().find() && !textBlock.toString().contains(":")
|
||||
|| textBlock.toString().startsWith("APPENDIX")
|
||||
|| textBlock.toString().startsWith("FIGURE")
|
||||
|| textBlock.toString().startsWith("Continued TABLE")
|
||||
|| textBlock.toString().startsWith("TABLE"))
|
||||
&& !textBlock.toString().endsWith(":")
|
||||
&& atLeast3Matcher.reset().find()) {
|
||||
PageBlockType headlineType = PageBlockType.getHeadlineType(1);
|
||||
headlineClassificationService.classifyHeadline(textBlock, headlineType);
|
||||
document.setHeadlines(true);
|
||||
|
||||
} else if (matcher.find() && PositionUtils.getApproxLineCount(textBlock) < 2.9 && matcher2.find() && !matcher3.matches()) {
|
||||
textBlock.setClassification(PageBlockType.getHeadlineType(2));
|
||||
} else if (headlineWithIdentifierMatcher.reset().find() && PositionUtils.getApproxLineCount(textBlock) < 2.9 && atLeast3Matcher.reset().find() && !headlineWithSlashesMatcher.reset().matches()) {
|
||||
PageBlockType headlineType = PageBlockType.getHeadlineType(2);
|
||||
headlineClassificationService.classifyHeadline(textBlock, headlineType);
|
||||
document.setHeadlines(true);
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter()
|
||||
.getMostPopular() && textBlock.getMostPopularWordStyle().equals("bold") && !document.getFontStyleCounter().getMostPopular().equals("bold")) {
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
|
||||
&& textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()
|
||||
&& textBlock.getMostPopularWordStyle().equals("bold")
|
||||
&& !document.getFontStyleCounter().getMostPopular().equals("bold")) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH_BOLD);
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFont()
|
||||
.equals(document.getFontCounter().getMostPopular()) && textBlock.getMostPopularWordStyle()
|
||||
.equals(document.getFontStyleCounter().getMostPopular()) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()) {
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
|
||||
&& textBlock.getMostPopularWordFont().equals(document.getFontCounter().getMostPopular())
|
||||
&& textBlock.getMostPopularWordStyle().equals(document.getFontStyleCounter().getMostPopular())
|
||||
&& textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH);
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter()
|
||||
.getMostPopular() && textBlock.getMostPopularWordStyle().equals("italic") && !document.getFontStyleCounter()
|
||||
.getMostPopular()
|
||||
.equals("italic") && PositionUtils.getApproxLineCount(textBlock) < 2.9) {
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
|
||||
&& textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()
|
||||
&& textBlock.getMostPopularWordStyle().equals("italic")
|
||||
&& !document.getFontStyleCounter().getMostPopular().equals("italic")
|
||||
&& PositionUtils.getApproxLineCount(textBlock) < 2.9) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH_ITALIC);
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH_UNKNOWN);
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.classification;
|
||||
|
||||
import static com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType.getHeadlineNumber;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class HeadlineClassificationService {
|
||||
|
||||
TextPageBlock lastHeadline;
|
||||
PageBlockType originalClassifiedBlockType;
|
||||
TextPageBlock lastHeadlineFromOutline;
|
||||
|
||||
public void setLastHeadlineFromOutline(TextPageBlock lastHeadlineFromOutline) {
|
||||
|
||||
this.lastHeadlineFromOutline = lastHeadlineFromOutline;
|
||||
this.setLastHeadline(lastHeadlineFromOutline);
|
||||
}
|
||||
|
||||
|
||||
public void classifyHeadline(TextPageBlock textBlock, PageBlockType initialHeadlineType) {
|
||||
|
||||
TextPageBlock lastHeadline = getLastHeadline();
|
||||
TextPageBlock lastHeadlineFromOutline = getLastHeadlineFromOutline();
|
||||
PageBlockType originalClassifiedBlockType = getOriginalClassifiedBlockType();
|
||||
PageBlockType finalHeadlineType = initialHeadlineType;
|
||||
|
||||
if (lastHeadline != null) {
|
||||
|
||||
if (lastHeadline.equals(lastHeadlineFromOutline)) {
|
||||
|
||||
finalHeadlineType = PageBlockType.getHeadlineType(getHeadlineNumber(lastHeadline.getClassification()) + 1);
|
||||
|
||||
} else if (originalClassifiedBlockType != null && lastHeadline.getClassification() != originalClassifiedBlockType) {
|
||||
|
||||
PageBlockType lastHeadlineType = lastHeadline.getClassification();
|
||||
int difference = getHeadlineNumber(originalClassifiedBlockType) - getHeadlineNumber(lastHeadlineType);
|
||||
finalHeadlineType = PageBlockType.getHeadlineType(getHeadlineNumber(initialHeadlineType) - difference);
|
||||
}
|
||||
}
|
||||
|
||||
setOriginalClassifiedBlockType(initialHeadlineType);
|
||||
textBlock.setClassification(finalHeadlineType);
|
||||
setLastHeadline(textBlock);
|
||||
}
|
||||
|
||||
}
|
||||
+61
-37
@@ -3,14 +3,15 @@ package com.knecon.fforesight.service.layoutparser.processor.services.classifica
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.MarkedContentUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.ImageType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.MarkedContentUtils;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.PositionUtils;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -28,79 +29,102 @@ public class RedactManagerClassificationService {
|
||||
|
||||
log.debug("Document FontSize counters are: {}", document.getFontSizeCounter().getCountPerValue());
|
||||
|
||||
HeadlineClassificationService headlineClassificationService = new HeadlineClassificationService();
|
||||
|
||||
for (ClassificationPage page : document.getPages()) {
|
||||
classifyPage(page, document, headlineFontSizes);
|
||||
classifyPage(headlineClassificationService, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyPage(ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyPage(HeadlineClassificationService headlineClassificationService, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
for (AbstractPageBlock textBlock : page.getTextBlocks()) {
|
||||
if (textBlock instanceof TextPageBlock) {
|
||||
classifyBlock((TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
classifyBlock(headlineClassificationService, (TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyBlock(TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyBlock(HeadlineClassificationService headlineClassificationService, TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
var bodyTextFrame = page.getBodyTextFrame();
|
||||
|
||||
if (textBlock.getClassification() != null && textBlock.getClassification().isHeadline()) {
|
||||
headlineClassificationService.setLastHeadlineFromOutline(textBlock);
|
||||
return;
|
||||
}
|
||||
if (document.getFontSizeCounter().getMostPopular() == null) {
|
||||
textBlock.setClassification(PageBlockType.OTHER);
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.getImages()
|
||||
.stream()
|
||||
.filter(image -> image.getImageType().equals(ImageType.GRAPHIC))
|
||||
.anyMatch(graphic -> graphic.getPosition().intersects(textBlock.getPdfMinX(), textBlock.getPdfMinY(), textBlock.getWidth(), textBlock.getHeight()))) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH);
|
||||
return;
|
||||
}
|
||||
if (MarkedContentUtils.intersects(textBlock, page.getMarkedContentBboxPerType(), MarkedContentUtils.HEADER)
|
||||
|| PositionUtils.isOverBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter()
|
||||
.getMostPopular() == null || textBlock.getHighestFontSize() <= document.getFontSizeCounter().getMostPopular())) {
|
||||
|| PositionUtils.isOverBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter().getMostPopular() == null
|
||||
|| textBlock.getHighestFontSize() <= document.getFontSizeCounter()
|
||||
.getMostPopular())) {
|
||||
textBlock.setClassification(PageBlockType.HEADER);
|
||||
|
||||
} else if (MarkedContentUtils.intersects(textBlock, page.getMarkedContentBboxPerType(), MarkedContentUtils.FOOTER)
|
||||
|| PositionUtils.isUnderBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter()
|
||||
.getMostPopular() == null || textBlock.getHighestFontSize() <= document.getFontSizeCounter().getMostPopular())) {
|
||||
|| PositionUtils.isUnderBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter().getMostPopular() == null
|
||||
|| textBlock.getHighestFontSize() <= document.getFontSizeCounter()
|
||||
.getMostPopular())) {
|
||||
textBlock.setClassification(PageBlockType.FOOTER);
|
||||
} else if (page.getPageNumber() == 1 && (PositionUtils.getHeightDifferenceBetweenChunkWordAndDocumentWord(textBlock,
|
||||
document.getTextHeightCounter().getMostPopular()) > 2.5 && textBlock.getHighestFontSize() > document.getFontSizeCounter().getMostPopular() || page.getTextBlocks()
|
||||
.size() == 1)) {
|
||||
} else if (page.getPageNumber() == 1 && (PositionUtils.getHeightDifferenceBetweenChunkWordAndDocumentWord(textBlock, document.getTextHeightCounter().getMostPopular()) > 2.5
|
||||
&& textBlock.getHighestFontSize() > document.getFontSizeCounter().getMostPopular() || page.getTextBlocks().size() == 1)) {
|
||||
if (!Pattern.matches("[0-9]+", textBlock.toString())) {
|
||||
textBlock.setClassification(PageBlockType.TITLE);
|
||||
}
|
||||
} else if (textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter()
|
||||
.getMostPopular() && PositionUtils.getApproxLineCount(textBlock) < 4.9 && (textBlock.getMostPopularWordStyle().equals("bold") || !document.getFontStyleCounter()
|
||||
.getCountPerValue()
|
||||
.containsKey("bold") && textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter().getMostPopular() + 1) && textBlock.getSequences()
|
||||
.get(0)
|
||||
.getTextPositions()
|
||||
.get(0)
|
||||
.getFontSizeInPt() >= textBlock.getMostPopularWordFontSize()) {
|
||||
} else if (textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter().getMostPopular()
|
||||
&& PositionUtils.getApproxLineCount(textBlock) < 4.9
|
||||
&& (textBlock.getMostPopularWordStyle().equals("bold")
|
||||
|| !document.getFontStyleCounter().getCountPerValue().containsKey("bold")
|
||||
&& textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter().getMostPopular() + 1)
|
||||
&& textBlock.getSequences()
|
||||
.get(0).getTextPositions()
|
||||
.get(0).getFontSizeInPt() >= textBlock.getMostPopularWordFontSize()) {
|
||||
|
||||
for (int i = 1; i <= headlineFontSizes.size(); i++) {
|
||||
if (textBlock.getMostPopularWordFontSize() == headlineFontSizes.get(i - 1)) {
|
||||
textBlock.setClassification(PageBlockType.getHeadlineType(i));
|
||||
PageBlockType headlineType = PageBlockType.getHeadlineType(i);
|
||||
headlineClassificationService.classifyHeadline(textBlock, headlineType);
|
||||
document.setHeadlines(true);
|
||||
}
|
||||
}
|
||||
} else if (!textBlock.getText().startsWith("Figure ") && PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordStyle()
|
||||
.equals("bold") && !document.getFontStyleCounter().getMostPopular().equals("bold") && PositionUtils.getApproxLineCount(textBlock) < 2.9 && textBlock.getSequences()
|
||||
.get(0)
|
||||
.getTextPositions()
|
||||
.get(0)
|
||||
.getFontSizeInPt() >= textBlock.getMostPopularWordFontSize()) {
|
||||
textBlock.setClassification(PageBlockType.getHeadlineType(headlineFontSizes.size() + 1));
|
||||
} else if (!textBlock.getText().startsWith("Figure ")
|
||||
&& PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
|
||||
&& textBlock.getMostPopularWordStyle().equals("bold")
|
||||
&& !document.getFontStyleCounter().getMostPopular().equals("bold")
|
||||
&& PositionUtils.getApproxLineCount(textBlock) < 2.9
|
||||
&& textBlock.getSequences()
|
||||
.get(0).getTextPositions()
|
||||
.get(0).getFontSizeInPt() >= textBlock.getMostPopularWordFontSize()) {
|
||||
PageBlockType headlineType = PageBlockType.getHeadlineType(headlineFontSizes.size() + 1);
|
||||
headlineClassificationService.classifyHeadline(textBlock, headlineType);
|
||||
document.setHeadlines(true);
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter()
|
||||
.getMostPopular() && textBlock.getMostPopularWordStyle().equals("bold") && !document.getFontStyleCounter().getMostPopular().equals("bold")) {
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
|
||||
&& textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()
|
||||
&& textBlock.getMostPopularWordStyle().equals("bold")
|
||||
&& !document.getFontStyleCounter().getMostPopular().equals("bold")) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH_BOLD);
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFont()
|
||||
.equals(document.getFontCounter().getMostPopular()) && textBlock.getMostPopularWordStyle()
|
||||
.equals(document.getFontStyleCounter().getMostPopular()) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()) {
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
|
||||
&& textBlock.getMostPopularWordFont().equals(document.getFontCounter().getMostPopular())
|
||||
&& textBlock.getMostPopularWordStyle().equals(document.getFontStyleCounter().getMostPopular())
|
||||
&& textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH);
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter()
|
||||
.getMostPopular() && textBlock.getMostPopularWordStyle().equals("italic") && !document.getFontStyleCounter()
|
||||
.getMostPopular()
|
||||
.equals("italic") && PositionUtils.getApproxLineCount(textBlock) < 2.9) {
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
|
||||
&& textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()
|
||||
&& textBlock.getMostPopularWordStyle().equals("italic")
|
||||
&& !document.getFontStyleCounter().getMostPopular().equals("italic")
|
||||
&& PositionUtils.getApproxLineCount(textBlock) < 2.9) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH_ITALIC);
|
||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)) {
|
||||
textBlock.setClassification(PageBlockType.PARAGRAPH_UNKNOWN);
|
||||
|
||||
+49
-30
@@ -7,15 +7,14 @@ import static java.util.stream.Collectors.toList;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
|
||||
@@ -32,9 +31,10 @@ import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.He
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Image;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Section;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.AbstractSemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.TableOfContentItem;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.IdBuilder;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.TextPositionOperations;
|
||||
@@ -52,11 +52,14 @@ public class DocumentGraphFactory {
|
||||
public Document buildDocumentGraph(LayoutParsingType layoutParsingType, ClassificationDocument document) {
|
||||
|
||||
Document documentGraph = new Document();
|
||||
|
||||
documentGraph.setVisualizations(document.getVisualizations());
|
||||
|
||||
Context context = new Context(documentGraph);
|
||||
|
||||
document.getPages().forEach(context::buildAndAddPageWithCounter);
|
||||
document.getSections().stream().flatMap(section -> section.getImages().stream()).forEach(image -> context.getImages().add(image));
|
||||
addSections(layoutParsingType, document, context);
|
||||
document.getPages()
|
||||
.forEach(context::buildAndAddPageWithCounter);
|
||||
addSections(layoutParsingType, document, context, documentGraph);
|
||||
addHeaderAndFooterToEachPage(document, context);
|
||||
|
||||
documentGraph.setNumberOfPages(context.pages.size());
|
||||
@@ -67,20 +70,34 @@ public class DocumentGraphFactory {
|
||||
}
|
||||
|
||||
|
||||
private void addSections(LayoutParsingType layoutParsingType, ClassificationDocument document, Context context) {
|
||||
private void addSections(LayoutParsingType layoutParsingType, ClassificationDocument classificationDocument, Context context, Document document) {
|
||||
|
||||
document.getSections().forEach(section -> SectionNodeFactory.addSection(layoutParsingType, null, section.getNonEmptyPageBlocks(), section.getImages(), context));
|
||||
for (TableOfContentItem tocItem : classificationDocument.getTableOfContents()) {
|
||||
var parent = tocItem.getParent() == null ? null : tocItem.getParent().getSection();
|
||||
Optional<AbstractSemanticNode> section = SectionNodeFactory.addSection(layoutParsingType,
|
||||
parent,
|
||||
tocItem.getChildren().isEmpty(),
|
||||
tocItem.getNonEmptySectionBlocks(),
|
||||
tocItem.getImages(),
|
||||
context,
|
||||
document);
|
||||
tocItem.setSection(section.orElse(null));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addParagraphOrHeadline(GenericSemanticNode parentNode, TextPageBlock originalTextBlock, Context context, List<TextPageBlock> textBlocksToMerge) {
|
||||
public void addParagraphOrHeadline(GenericSemanticNode parentNode,
|
||||
TextPageBlock originalTextBlock,
|
||||
Context context,
|
||||
List<TextPageBlock> textBlocksToMerge,
|
||||
LayoutParsingType layoutParsingType) {
|
||||
|
||||
Page page = context.getPage(originalTextBlock.getPage());
|
||||
|
||||
GenericSemanticNode node;
|
||||
if (originalTextBlock.isHeadline()) {
|
||||
node = Headline.builder().documentTree(context.getDocumentTree()).build();
|
||||
} else if (originalTextBlock.isToDuplicate()) {
|
||||
} else if (originalTextBlock.isToDuplicate() && layoutParsingType.equals(LayoutParsingType.REDACT_MANAGER)) {
|
||||
node = DuplicatedParagraph.builder().documentTree(context.getDocumentTree()).build();
|
||||
} else {
|
||||
node = Paragraph.builder().documentTree(context.getDocumentTree()).build();
|
||||
@@ -96,36 +113,42 @@ public class DocumentGraphFactory {
|
||||
|
||||
if (node instanceof DuplicatedParagraph duplicatedParagraph) {
|
||||
AtomicTextBlock unsortedTextBlock = context.textBlockFactory.buildAtomicTextBlock(textBlocks.stream()
|
||||
.flatMap(tb -> tb.getSequences().stream())
|
||||
.collect(Collectors.toList()), node, context, page);
|
||||
.flatMap(tb -> tb.getSequences()
|
||||
.stream())
|
||||
.collect(Collectors.toList()), node, context, page);
|
||||
duplicatedParagraph.setUnsortedLeafTextBlock(unsortedTextBlock);
|
||||
}
|
||||
|
||||
List<Integer> treeId = context.documentTree.createNewChildEntryAndReturnId(parentNode, node);
|
||||
node.setLeafTextBlock(textBlock);
|
||||
node.setTreeId(treeId);
|
||||
node.getEngines().addAll(originalTextBlock.getEngines());
|
||||
}
|
||||
|
||||
|
||||
public void addImage(Section section, ClassifiedImage image, Context context) {
|
||||
public void addImage(GenericSemanticNode parent, ClassifiedImage image, Context context) {
|
||||
|
||||
Image imageNode = createImage(image, context);
|
||||
List<Integer> treeId = context.getDocumentTree().createNewChildEntryAndReturnId(parent, imageNode);
|
||||
imageNode.setTreeId(treeId);
|
||||
imageNode.setLeafTextBlock(context.textBlockFactory.emptyTextBlock(parent, context, context.getPage(image.getPage())));
|
||||
}
|
||||
|
||||
|
||||
private Image createImage(ClassifiedImage image, Context context) {
|
||||
|
||||
Rectangle2D position = image.getPosition();
|
||||
Page page = context.getPage(image.getPage());
|
||||
var imageBuilder = Image.builder()
|
||||
Image imageNode = Image.builder()
|
||||
.id(IdBuilder.buildId(Set.of(page), List.of(position)))
|
||||
.imageType(image.getImageType())
|
||||
.position(position)
|
||||
.transparent(image.isHasTransparency())
|
||||
.page(page)
|
||||
.documentTree(context.getDocumentTree());
|
||||
if (image.isSourceByAi()) {
|
||||
imageBuilder.engines(new HashSet<>(Set.of(LayoutEngine.AI)));
|
||||
}
|
||||
Image imageNode = imageBuilder.build();
|
||||
.documentTree(context.getDocumentTree())
|
||||
.build();
|
||||
page.getMainBody().add(imageNode);
|
||||
|
||||
List<Integer> treeId = context.getDocumentTree().createNewChildEntryAndReturnId(section, imageNode);
|
||||
imageNode.setTreeId(treeId);
|
||||
return imageNode;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,10 +188,7 @@ public class DocumentGraphFactory {
|
||||
|
||||
Page page = context.getPage(textBlocks.get(0).getPage());
|
||||
Footer footer = Footer.builder().documentTree(context.getDocumentTree()).build();
|
||||
AtomicTextBlock textBlock = context.textBlockFactory.buildAtomicTextBlock(TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX(textBlocks),
|
||||
footer,
|
||||
context,
|
||||
page);
|
||||
AtomicTextBlock textBlock = context.textBlockFactory.buildAtomicTextBlock(TextPositionOperations.mergeTextPositionSequence(textBlocks), footer, context, page);
|
||||
List<Integer> tocId = context.getDocumentTree().createNewMainEntryAndReturnId(footer);
|
||||
footer.setTreeId(tocId);
|
||||
footer.setLeafTextBlock(textBlock);
|
||||
@@ -180,7 +200,7 @@ public class DocumentGraphFactory {
|
||||
|
||||
Page page = context.getPage(textBlocks.get(0).getPage());
|
||||
Header header = Header.builder().documentTree(context.getDocumentTree()).build();
|
||||
AtomicTextBlock textBlock = context.textBlockFactory.buildAtomicTextBlock(TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX(textBlocks), header, 0, page);
|
||||
AtomicTextBlock textBlock = context.textBlockFactory.buildAtomicTextBlock(TextPositionOperations.mergeTextPositionSequence(textBlocks), header, 0, page);
|
||||
List<Integer> tocId = context.getDocumentTree().createNewMainEntryAndReturnId(header);
|
||||
header.setTreeId(tocId);
|
||||
header.setLeafTextBlock(textBlock);
|
||||
@@ -220,7 +240,7 @@ public class DocumentGraphFactory {
|
||||
|
||||
DocumentTree documentTree;
|
||||
Map<Page, Integer> pages;
|
||||
List<Section> sections;
|
||||
List<AbstractSemanticNode> sections;
|
||||
List<ClassifiedImage> images;
|
||||
TextBlockFactory textBlockFactory;
|
||||
|
||||
@@ -257,8 +277,7 @@ public class DocumentGraphFactory {
|
||||
return pages.keySet()
|
||||
.stream()
|
||||
.filter(page -> page.getNumber() == pageIndex)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new NoSuchElementException(format("ClassificationPage with number %d not found", pageIndex)));
|
||||
.findFirst().orElseThrow(() -> new NoSuchElementException(format("ClassificationPage with number %d not found", pageIndex)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+31
-21
@@ -2,16 +2,17 @@ package com.knecon.fforesight.service.layoutparser.processor.services.factory;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextDirection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@@ -29,19 +30,22 @@ public class SearchTextWithTextPositionFactory {
|
||||
|
||||
public SearchTextWithTextPositionDto buildSearchTextToTextPositionDto(List<TextPositionSequence> sequences) {
|
||||
|
||||
if (sequences.isEmpty() || sequences.stream().allMatch(sequence -> sequence.getTextPositions().isEmpty())) {
|
||||
if (sequences.isEmpty() || sequences.stream()
|
||||
.allMatch(sequence -> sequence.getTextPositions().isEmpty())) {
|
||||
return SearchTextWithTextPositionDto.empty();
|
||||
}
|
||||
|
||||
Context context = new Context();
|
||||
|
||||
RedTextPosition currentTextPosition = sequences.get(0).getTextPositions().get(0);
|
||||
RedTextPosition previousTextPosition = RedTextPosition.builder().unicode(" ").position(currentTextPosition.getPosition()).build();
|
||||
RedTextPosition currentTextPosition = sequences.get(0).getTextPositions()
|
||||
.get(0);
|
||||
RedTextPosition previousTextPosition = RedTextPosition.builder().unicode(" ").bBoxDirAdj(currentTextPosition.getBBoxDirAdj()).build();
|
||||
|
||||
for (TextPositionSequence word : sequences) {
|
||||
for (int i = 0; i < word.getTextPositions().size(); ++i) {
|
||||
|
||||
currentTextPosition = word.getTextPositions().get(i);
|
||||
currentTextPosition = word.getTextPositions()
|
||||
.get(i);
|
||||
if (isLineBreak(currentTextPosition, previousTextPosition)) {
|
||||
removeHyphenLinebreaks(context);
|
||||
context.lineBreaksStringIdx.add(context.stringIdx);
|
||||
@@ -57,18 +61,21 @@ public class SearchTextWithTextPositionFactory {
|
||||
++context.positionIdx;
|
||||
}
|
||||
|
||||
previousTextPosition = RedTextPosition.builder().unicode(" ").position(previousTextPosition.getPosition()).build();
|
||||
previousTextPosition = RedTextPosition.builder().unicode(" ").bBoxDirAdj(previousTextPosition.getBBoxDirAdj()).build();
|
||||
context.stringBuilder.append(" ");
|
||||
context.stringIdxToPositionIdx.add(context.positionIdx);
|
||||
++context.stringIdx;
|
||||
}
|
||||
|
||||
assert context.stringBuilder.length() == context.stringIdxToPositionIdx.size();
|
||||
|
||||
List<Rectangle2D> positions = sequences.stream()
|
||||
.flatMap(sequence -> sequence.getTextPositions().stream().map(textPosition -> mapRedTextPositionToInitialUserSpace(textPosition, sequence)))
|
||||
.map(TextPositionSequence::getTextPositions)
|
||||
.flatMap(Collection::stream)
|
||||
.map(RedTextPosition::getBBoxInitialUserSpace)
|
||||
.toList();
|
||||
|
||||
assert context.stringBuilder.length() == context.stringIdxToPositionIdx.size();
|
||||
|
||||
return SearchTextWithTextPositionDto.builder()
|
||||
.searchText(context.stringBuilder.toString())
|
||||
.lineBreaks(context.lineBreaksStringIdx)
|
||||
@@ -110,6 +117,7 @@ public class SearchTextWithTextPositionFactory {
|
||||
return context.stringIdx - context.lastHyphenIdx < MAX_HYPHEN_LINEBREAK_DISTANCE;
|
||||
}
|
||||
|
||||
|
||||
private static List<Boundary> mergeToBoundaries(List<Integer> integers) {
|
||||
|
||||
if (integers.isEmpty()) {
|
||||
@@ -125,8 +133,9 @@ public class SearchTextWithTextPositionFactory {
|
||||
}
|
||||
end = current + 1;
|
||||
}
|
||||
if (boundaries.isEmpty())
|
||||
if (boundaries.isEmpty()) {
|
||||
boundaries.add(new Boundary(start, end));
|
||||
}
|
||||
return boundaries;
|
||||
}
|
||||
|
||||
@@ -138,6 +147,7 @@ public class SearchTextWithTextPositionFactory {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean isLineBreak(RedTextPosition currentTextPosition, RedTextPosition previousTextPosition) {
|
||||
|
||||
return Objects.equals(currentTextPosition.getUnicode(), "\n") || isDeltaYLargerThanTextHeight(currentTextPosition, previousTextPosition);
|
||||
@@ -150,7 +160,7 @@ public class SearchTextWithTextPositionFactory {
|
||||
return false;
|
||||
}
|
||||
|
||||
float deltaY = Math.abs(currentPosition.getYDirAdj() - previousPosition.getYDirAdj());
|
||||
double deltaY = Math.abs(currentPosition.getYDirAdj() - previousPosition.getYDirAdj());
|
||||
return deltaY >= currentPosition.getHeightDir();
|
||||
}
|
||||
|
||||
@@ -164,20 +174,20 @@ public class SearchTextWithTextPositionFactory {
|
||||
private boolean isHyphen(String unicodeCharacter) {
|
||||
|
||||
return Objects.equals(unicodeCharacter, "-") || //
|
||||
Objects.equals(unicodeCharacter, "~") || //
|
||||
Objects.equals(unicodeCharacter, "‐") || //
|
||||
Objects.equals(unicodeCharacter, "‒") || //
|
||||
Objects.equals(unicodeCharacter, "⁻") || //
|
||||
Objects.equals(unicodeCharacter, "−") || //
|
||||
Objects.equals(unicodeCharacter, "﹣") || //
|
||||
Objects.equals(unicodeCharacter, "゠") || //
|
||||
Objects.equals(unicodeCharacter, "⁓") || //
|
||||
Objects.equals(unicodeCharacter, "‑") || //
|
||||
Objects.equals(unicodeCharacter, "\u00AD");
|
||||
Objects.equals(unicodeCharacter, "~") || //
|
||||
Objects.equals(unicodeCharacter, "‐") || //
|
||||
Objects.equals(unicodeCharacter, "‒") || //
|
||||
Objects.equals(unicodeCharacter, "⁻") || //
|
||||
Objects.equals(unicodeCharacter, "−") || //
|
||||
Objects.equals(unicodeCharacter, "﹣") || //
|
||||
Objects.equals(unicodeCharacter, "゠") || //
|
||||
Objects.equals(unicodeCharacter, "⁓") || //
|
||||
Objects.equals(unicodeCharacter, "‑") || //
|
||||
Objects.equals(unicodeCharacter, "\u00AD");
|
||||
}
|
||||
|
||||
|
||||
private Rectangle2D mapRedTextPositionToInitialUserSpace(RedTextPosition textPosition, TextPositionSequence sequence) {
|
||||
public Rectangle2D mapRedTextPositionToInitialUserSpace(RedTextPosition textPosition, TextPositionSequence sequence) {
|
||||
|
||||
float textHeight = sequence.getTextHeight() + HEIGHT_PADDING;
|
||||
Rectangle2D rectangle2D = new Rectangle2D.Double(textPosition.getXDirAdj(),
|
||||
|
||||
+73
-26
@@ -9,13 +9,17 @@ import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.AbstractSemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.GenericSemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Section;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SuperSection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
@@ -26,35 +30,68 @@ import lombok.experimental.UtilityClass;
|
||||
@UtilityClass
|
||||
public class SectionNodeFactory {
|
||||
|
||||
public void addSection(LayoutParsingType layoutParsingType,
|
||||
GenericSemanticNode parentNode,
|
||||
List<AbstractPageBlock> pageBlocks,
|
||||
List<ClassifiedImage> images,
|
||||
DocumentGraphFactory.Context context) {
|
||||
public Optional<AbstractSemanticNode> addSection(LayoutParsingType layoutParsingType,
|
||||
GenericSemanticNode parentNode,
|
||||
boolean isLeaf,
|
||||
List<AbstractPageBlock> pageBlocks,
|
||||
List<ClassifiedImage> images,
|
||||
DocumentGraphFactory.Context context,
|
||||
Document document) {
|
||||
|
||||
// This is for the case where we have images on a page without any text/footer/header.
|
||||
// The pageBlocks list is empty, but we still need to add those images to the document.
|
||||
if (!images.isEmpty() && pageBlocks.isEmpty()) {
|
||||
images.stream()
|
||||
.distinct()
|
||||
.forEach(image -> DocumentGraphFactory.addImage(document, image, context));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (pageBlocks.isEmpty()) {
|
||||
return;
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Map<Integer, List<AbstractPageBlock>> blocksPerPage = pageBlocks.stream()
|
||||
.collect(groupingBy(AbstractPageBlock::getPage));
|
||||
|
||||
AbstractSemanticNode section;
|
||||
boolean containsTablesAndTextBlocks = containsTablesAndTextBlocks(pageBlocks);
|
||||
if (isLeaf && !containsTablesAndTextBlocks) {
|
||||
section = Section.builder().documentTree(context.getDocumentTree()).build();
|
||||
} else {
|
||||
section = SuperSection.builder().documentTree(context.getDocumentTree()).build();
|
||||
}
|
||||
Map<Integer, List<AbstractPageBlock>> blocksPerPage = pageBlocks.stream().collect(groupingBy(AbstractPageBlock::getPage));
|
||||
Section section = Section.builder().documentTree(context.getDocumentTree()).build();
|
||||
|
||||
context.getSections().add(section);
|
||||
blocksPerPage.keySet().forEach(pageNumber -> addSectionNodeToPageNode(context, section, pageNumber));
|
||||
blocksPerPage.keySet()
|
||||
.forEach(pageNumber -> addSectionNodeToPageNode(context, section, pageNumber));
|
||||
|
||||
section.setTreeId(getTreeId(parentNode, context, section));
|
||||
|
||||
addFirstHeadlineDirectlyToSection(layoutParsingType, pageBlocks, context, section);
|
||||
if (containsTablesAndTextBlocks(pageBlocks)) {
|
||||
splitPageBlocksIntoSubSections(pageBlocks).forEach(subSectionPageBlocks -> addSection(layoutParsingType, section, subSectionPageBlocks, emptyList(), context));
|
||||
addFirstHeadlineDirectlyToSection(layoutParsingType, pageBlocks, context, section, document);
|
||||
if (containsTablesAndTextBlocks) {
|
||||
splitPageBlocksIntoSubSections(pageBlocks).forEach(subSectionPageBlocks -> addSection(layoutParsingType,
|
||||
section,
|
||||
true,
|
||||
subSectionPageBlocks,
|
||||
emptyList(),
|
||||
context,
|
||||
document));
|
||||
} else if (!isLeaf) {
|
||||
addSection(layoutParsingType, section, true, pageBlocks, emptyList(), context, document);
|
||||
} else {
|
||||
addTablesAndParagraphsAndHeadlinesToSection(layoutParsingType, pageBlocks, context, section);
|
||||
addTablesAndParagraphsAndHeadlinesToSection(layoutParsingType, pageBlocks, context, section, document);
|
||||
}
|
||||
|
||||
images.stream().distinct().forEach(image -> DocumentGraphFactory.addImage(section, image, context));
|
||||
images.stream()
|
||||
.distinct()
|
||||
.forEach(image -> DocumentGraphFactory.addImage(section, image, context));
|
||||
|
||||
return Optional.of(section);
|
||||
}
|
||||
|
||||
|
||||
private List<Integer> getTreeId(GenericSemanticNode parentNode, DocumentGraphFactory.Context context, Section section) {
|
||||
private List<Integer> getTreeId(GenericSemanticNode parentNode, DocumentGraphFactory.Context context, AbstractSemanticNode section) {
|
||||
|
||||
if (parentNode == null) {
|
||||
return context.getDocumentTree().createNewMainEntryAndReturnId(section);
|
||||
@@ -64,10 +101,14 @@ public class SectionNodeFactory {
|
||||
}
|
||||
|
||||
|
||||
private void addFirstHeadlineDirectlyToSection(LayoutParsingType layoutParsingType, List<AbstractPageBlock> pageBlocks, DocumentGraphFactory.Context context, Section section) {
|
||||
private void addFirstHeadlineDirectlyToSection(LayoutParsingType layoutParsingType,
|
||||
List<AbstractPageBlock> pageBlocks,
|
||||
DocumentGraphFactory.Context context,
|
||||
AbstractSemanticNode section,
|
||||
Document document) {
|
||||
|
||||
if (pageBlocks.get(0).isHeadline()) {
|
||||
addTablesAndParagraphsAndHeadlinesToSection(layoutParsingType, List.of(pageBlocks.get(0)), context, section);
|
||||
addTablesAndParagraphsAndHeadlinesToSection(layoutParsingType, List.of(pageBlocks.get(0)), context, section, document);
|
||||
pageBlocks.remove(0);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +117,8 @@ public class SectionNodeFactory {
|
||||
private void addTablesAndParagraphsAndHeadlinesToSection(LayoutParsingType layoutParsingType,
|
||||
List<AbstractPageBlock> pageBlocks,
|
||||
DocumentGraphFactory.Context context,
|
||||
Section section) {
|
||||
AbstractSemanticNode section,
|
||||
Document document) {
|
||||
|
||||
Set<AbstractPageBlock> alreadyMerged = new HashSet<>();
|
||||
List<AbstractPageBlock> remainingBlocks = new LinkedList<>(pageBlocks);
|
||||
@@ -91,21 +133,21 @@ public class SectionNodeFactory {
|
||||
if (abstractPageBlock instanceof TextPageBlock) {
|
||||
|
||||
switch (layoutParsingType) {
|
||||
case CLARIFYND, CLARIFYND_PARAGRAPH_DEBUG -> {
|
||||
case REDACT_MANAGER, DOCUMINE, CLARIFYND, CLARIFYND_PARAGRAPH_DEBUG, REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH -> {
|
||||
alreadyMerged.add(abstractPageBlock);
|
||||
remainingBlocks.remove(abstractPageBlock);
|
||||
DocumentGraphFactory.addParagraphOrHeadline(section, (TextPageBlock) abstractPageBlock, context, new ArrayList<>());
|
||||
DocumentGraphFactory.addParagraphOrHeadline(section, (TextPageBlock) abstractPageBlock, context, new ArrayList<>(), layoutParsingType);
|
||||
}
|
||||
default -> {
|
||||
List<TextPageBlock> textBlocks = findTextBlocksWithSameClassificationAndAlignsY((TextPageBlock) abstractPageBlock, remainingBlocks);
|
||||
alreadyMerged.addAll(textBlocks);
|
||||
DocumentGraphFactory.addParagraphOrHeadline(section, (TextPageBlock) abstractPageBlock, context, textBlocks);
|
||||
DocumentGraphFactory.addParagraphOrHeadline(section, (TextPageBlock) abstractPageBlock, context, textBlocks, layoutParsingType);
|
||||
}
|
||||
}
|
||||
} else if (abstractPageBlock instanceof TablePageBlock tablePageBlock) {
|
||||
List<TablePageBlock> tablesToMerge = TableMergingUtility.findConsecutiveTablesWithSameColCountAndSameHeaders(tablePageBlock, remainingBlocks);
|
||||
alreadyMerged.addAll(tablesToMerge);
|
||||
TableNodeFactory.addTable(layoutParsingType, section, tablesToMerge, context);
|
||||
TableNodeFactory.addTable(layoutParsingType, section, tablesToMerge, context, document);
|
||||
} else {
|
||||
throw new RuntimeException(format("Unhandled AbstractPageBlockType %s!", abstractPageBlock.getClass()));
|
||||
}
|
||||
@@ -115,7 +157,9 @@ public class SectionNodeFactory {
|
||||
|
||||
private boolean containsTablesAndTextBlocks(List<AbstractPageBlock> pageBlocks) {
|
||||
|
||||
return pageBlocks.stream().anyMatch(pageBlock -> pageBlock instanceof TablePageBlock) && pageBlocks.stream().anyMatch(pageBlock -> pageBlock instanceof TextPageBlock);
|
||||
return pageBlocks.stream()
|
||||
.anyMatch(pageBlock -> pageBlock instanceof TablePageBlock) && pageBlocks.stream()
|
||||
.anyMatch(pageBlock -> pageBlock instanceof TextPageBlock);
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +175,9 @@ public class SectionNodeFactory {
|
||||
|
||||
List<List<AbstractPageBlock>> splitList = splitIntoCoherentList(pageBlocks);
|
||||
movePrecedingHeadlineToTableList(splitList);
|
||||
return splitList.stream().filter(list -> !list.isEmpty()).toList();
|
||||
return splitList.stream()
|
||||
.filter(list -> !list.isEmpty())
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +198,8 @@ public class SectionNodeFactory {
|
||||
|
||||
private boolean listIsTablesOnly(List<AbstractPageBlock> abstractPageBlocks) {
|
||||
|
||||
return abstractPageBlocks.stream().allMatch(abstractPageBlock -> abstractPageBlock instanceof TablePageBlock);
|
||||
return abstractPageBlocks.stream()
|
||||
.allMatch(abstractPageBlock -> abstractPageBlock instanceof TablePageBlock);
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +242,7 @@ public class SectionNodeFactory {
|
||||
}
|
||||
|
||||
|
||||
private void addSectionNodeToPageNode(DocumentGraphFactory.Context context, Section section, Integer pageNumber) {
|
||||
private void addSectionNodeToPageNode(DocumentGraphFactory.Context context, AbstractSemanticNode section, Integer pageNumber) {
|
||||
|
||||
Page page = context.getPage(pageNumber);
|
||||
page.getMainBody().add(section);
|
||||
|
||||
+61
-15
@@ -9,6 +9,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.GenericSemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
@@ -28,11 +29,21 @@ public class TableNodeFactory {
|
||||
public final double TABLE_CELL_MERGE_CONTENTS_SIZE_THRESHOLD = 0.05;
|
||||
|
||||
|
||||
public void addTable(LayoutParsingType layoutParsingType, GenericSemanticNode parentNode, List<TablePageBlock> tablesToMerge, DocumentGraphFactory.Context context) {
|
||||
public void addTable(LayoutParsingType layoutParsingType,
|
||||
GenericSemanticNode parentNode,
|
||||
List<TablePageBlock> tablesToMerge,
|
||||
DocumentGraphFactory.Context context,
|
||||
Document document) {
|
||||
|
||||
setPageNumberInCells(tablesToMerge);
|
||||
Set<Page> pages = tablesToMerge.stream().map(AbstractPageBlock::getPage).map(context::getPage).collect(Collectors.toSet());
|
||||
List<List<Cell>> mergedRows = tablesToMerge.stream().map(TablePageBlock::getRows).flatMap(Collection::stream).toList();
|
||||
Set<Page> pages = tablesToMerge.stream()
|
||||
.map(AbstractPageBlock::getPage)
|
||||
.map(context::getPage)
|
||||
.collect(Collectors.toSet());
|
||||
List<List<Cell>> mergedRows = tablesToMerge.stream()
|
||||
.map(TablePageBlock::getRows)
|
||||
.flatMap(Collection::stream)
|
||||
.toList();
|
||||
|
||||
Table table = Table.builder()
|
||||
.documentTree(context.getDocumentTree())
|
||||
@@ -44,7 +55,7 @@ public class TableNodeFactory {
|
||||
|
||||
List<Integer> treeId = context.getDocumentTree().createNewChildEntryAndReturnId(parentNode, table);
|
||||
table.setTreeId(treeId);
|
||||
addTableCells(layoutParsingType, mergedRows, table, context);
|
||||
addTableCells(layoutParsingType, mergedRows, table, context, document);
|
||||
|
||||
ifTableHasNoHeadersSetFirstRowAsHeaders(table);
|
||||
}
|
||||
@@ -64,7 +75,8 @@ public class TableNodeFactory {
|
||||
|
||||
private void setPageNumberInTextBlocksWithPageNumberSetTo0(TablePageBlock table, Cell cell) {
|
||||
|
||||
cell.getTextBlocks().stream()//
|
||||
cell.getTextBlocks()
|
||||
.stream()//
|
||||
.filter(tb -> tb.getPage() == 0)//
|
||||
.forEach(tb -> tb.setPage(table.getPage()));
|
||||
}
|
||||
@@ -83,28 +95,49 @@ public class TableNodeFactory {
|
||||
|
||||
private void ifTableHasNoHeadersSetFirstRowAsHeaders(Table table) {
|
||||
|
||||
if (table.streamHeaders().findAny().isEmpty()) {
|
||||
table.streamRow(0).forEach(tableCellNode -> tableCellNode.setHeader(true));
|
||||
if (table.streamHeaders()
|
||||
.findAny().isEmpty()) {
|
||||
table.streamRow(0)
|
||||
.forEach(tableCellNode -> tableCellNode.setHeader(true));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void addTableCells(LayoutParsingType layoutParsingType, List<List<Cell>> rows, Table table, DocumentGraphFactory.Context context) {
|
||||
private void addTableCells(LayoutParsingType layoutParsingType, List<List<Cell>> rows, Table table, DocumentGraphFactory.Context context, Document document) {
|
||||
|
||||
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
|
||||
for (int colIndex = 0; colIndex < rows.get(rowIndex).size(); colIndex++) {
|
||||
addTableCell(layoutParsingType, rows.get(rowIndex).get(colIndex), rowIndex, colIndex, table, context);
|
||||
addTableCell(layoutParsingType,
|
||||
rows.get(rowIndex)
|
||||
.get(colIndex),
|
||||
rowIndex,
|
||||
colIndex,
|
||||
table,
|
||||
context,
|
||||
document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("PMD.UnusedPrivateMethod") // PMD actually flags this wrong
|
||||
private void addTableCell(LayoutParsingType layoutParsingType, Cell cell, int rowIndex, int colIndex, Table tableNode, DocumentGraphFactory.Context context) {
|
||||
private void addTableCell(LayoutParsingType layoutParsingType,
|
||||
Cell cell,
|
||||
int rowIndex,
|
||||
int colIndex,
|
||||
Table tableNode,
|
||||
DocumentGraphFactory.Context context,
|
||||
Document document) {
|
||||
|
||||
Page page = context.getPage(cell.getPageNumber());
|
||||
|
||||
TableCell tableCell = TableCell.builder().documentTree(context.getDocumentTree()).row(rowIndex).col(colIndex).header(cell.isHeaderCell()).bBox(cell.getBounds2D()).build();
|
||||
TableCell tableCell = TableCell.builder()
|
||||
.documentTree(context.getDocumentTree())
|
||||
.row(rowIndex)
|
||||
.col(colIndex)
|
||||
.header(cell.isHeaderCell())
|
||||
.bBox(cell.getBBoxInitialUserSpace())
|
||||
.build();
|
||||
page.getMainBody().add(tableCell);
|
||||
|
||||
List<Integer> treeId = context.getDocumentTree().createNewTableChildEntryAndReturnId(tableNode, tableCell);
|
||||
@@ -114,16 +147,28 @@ public class TableNodeFactory {
|
||||
if (cell.getTextBlocks().isEmpty()) {
|
||||
tableCell.setLeafTextBlock(context.getTextBlockFactory().emptyTextBlock(tableNode, context, page));
|
||||
} else if (cell.getTextBlocks().size() == 1) {
|
||||
textBlock = context.getTextBlockFactory().buildAtomicTextBlock(cell.getTextBlocks().get(0).getSequences(), tableCell, context, page);
|
||||
textBlock = context.getTextBlockFactory()
|
||||
.buildAtomicTextBlock(cell.getTextBlocks()
|
||||
.get(0).getSequences(), tableCell, context, page);
|
||||
tableCell.setLeafTextBlock(textBlock);
|
||||
} else if (firstTextBlockIsHeadline(cell)) {
|
||||
SectionNodeFactory.addSection(layoutParsingType, tableCell, cell.getTextBlocks().stream().map(tb -> (AbstractPageBlock) tb).toList(), emptyList(), context);
|
||||
SectionNodeFactory.addSection(layoutParsingType,
|
||||
tableCell,
|
||||
true,
|
||||
cell.getTextBlocks()
|
||||
.stream()
|
||||
.map(tb -> (AbstractPageBlock) tb)
|
||||
.collect(Collectors.toList()),
|
||||
emptyList(),
|
||||
context,
|
||||
document);
|
||||
} else if (cellAreaIsSmallerThanPageAreaTimesThreshold(cell, page)) {
|
||||
List<TextPositionSequence> sequences = TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX(cell.getTextBlocks());
|
||||
textBlock = context.getTextBlockFactory().buildAtomicTextBlock(sequences, tableCell, context, page);
|
||||
tableCell.setLeafTextBlock(textBlock);
|
||||
} else {
|
||||
cell.getTextBlocks().forEach(tb -> DocumentGraphFactory.addParagraphOrHeadline(tableCell, tb, context, emptyList()));
|
||||
cell.getTextBlocks()
|
||||
.forEach(tb -> DocumentGraphFactory.addParagraphOrHeadline(tableCell, tb, context, emptyList(), layoutParsingType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +181,8 @@ public class TableNodeFactory {
|
||||
|
||||
private boolean firstTextBlockIsHeadline(Cell cell) {
|
||||
|
||||
return cell.getTextBlocks().get(0).isHeadline();
|
||||
return cell.getTextBlocks()
|
||||
.get(0).isHeadline();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.graphics;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class Box {
|
||||
|
||||
public double x1;
|
||||
public double y1;
|
||||
public double x2;
|
||||
public double y2;
|
||||
|
||||
|
||||
public Box(double x1, double y1, double x2, double y2) {
|
||||
|
||||
this.x1 = x1;
|
||||
this.y1 = y1;
|
||||
this.x2 = x2;
|
||||
this.y2 = y2;
|
||||
}
|
||||
|
||||
|
||||
public Box(Rectangle2D rectangle2D) {
|
||||
|
||||
this.x1 = rectangle2D.getMinX();
|
||||
this.y1 = rectangle2D.getMinY();
|
||||
this.x2 = rectangle2D.getMaxX();
|
||||
this.y2 = rectangle2D.getMaxY();
|
||||
}
|
||||
|
||||
|
||||
public double width() {
|
||||
|
||||
return x2 - x1;
|
||||
}
|
||||
|
||||
|
||||
public double height() {
|
||||
|
||||
return y2 - y1;
|
||||
}
|
||||
|
||||
|
||||
public double xCenter() {
|
||||
|
||||
return (x2 + x1) / 2;
|
||||
}
|
||||
|
||||
|
||||
public double yCenter() {
|
||||
|
||||
return (y2 + y1) / 2;
|
||||
}
|
||||
|
||||
|
||||
public double area() {
|
||||
|
||||
return width() * height();
|
||||
}
|
||||
|
||||
|
||||
public Box scale(double scale) {
|
||||
|
||||
return new Box(x1 * scale, y1 * scale, x2 * scale, y2 * scale);
|
||||
}
|
||||
|
||||
|
||||
public boolean horizontallyAligned(Box other, double tol) {
|
||||
|
||||
return !(other.x1 - tol > x2 || other.x2 + tol < x1);
|
||||
}
|
||||
|
||||
|
||||
public double yDistanceTo(Box other) {
|
||||
|
||||
return Math.min(Math.abs(other.y1 - y2), Math.abs(y2 - other.y1));
|
||||
}
|
||||
|
||||
|
||||
public boolean intersects(Box other, double tol) {
|
||||
|
||||
return !((x2 < other.x1 - tol) || (x1 > other.x2 + tol) || (y2 < other.y1 - tol) || (y1 > other.y2 + tol));
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsAndOver(Box other, double tol) {
|
||||
|
||||
return (!((x2 < other.x1 - tol) || (x1 > other.x2 + tol) || (y2 < other.y1 - tol) || (y1 > other.y2 + tol))) && other.y1 > y1;
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsCenter(Box other, double tol) {
|
||||
|
||||
return !((x2 < other.xCenter() - tol) || (x1 > other.xCenter() + tol) || (y2 < other.yCenter() - tol) || (y1 > other.yCenter() + tol));
|
||||
}
|
||||
|
||||
|
||||
public Optional<Box> intersectRegion(Box other, double tol) {
|
||||
|
||||
if (!intersects(other, tol)) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
var overlapX1 = Math.max(x1, other.x1);
|
||||
var overlapY1 = Math.max(y1, other.y1);
|
||||
var overlapX2 = Math.min(x2, other.x2);
|
||||
var overlapY2 = Math.min(y2, other.y2);
|
||||
return Optional.of(new Box(overlapX1, overlapY1, overlapX2, overlapY2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public double intersectArea(Box other, double tol) {
|
||||
|
||||
return intersectRegion(other, tol).map(Box::area).orElse(0d);
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsAny(List<Box> others, double tol) {
|
||||
|
||||
return others.stream().anyMatch(other -> intersects(other, tol));
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsAnyAndOver(List<Box> others, double tol) {
|
||||
|
||||
return others.stream().anyMatch(other -> intersectsAndOver(other, tol));
|
||||
}
|
||||
|
||||
|
||||
public boolean intersectsCenter(List<Box> others, double tol) {
|
||||
|
||||
return others.stream().anyMatch(other -> intersectsCenter(other, tol));
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(Box other, double tol) {
|
||||
|
||||
return (x1 <= other.x1 + tol) && (y1 <= other.y1 + tol) && (x2 >= other.x2 - tol) && (y2 >= other.y2 - tol);
|
||||
}
|
||||
|
||||
|
||||
public Box container(Box other) {
|
||||
|
||||
var minX = Math.min(x1, other.x1);
|
||||
var minY = Math.min(y1, other.y1);
|
||||
var maxX = Math.max(x2, other.x2);
|
||||
var maxY = Math.max(y2, other.y2);
|
||||
return new Box(minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
|
||||
public Box transform(AffineTransform affineTransform) {
|
||||
|
||||
Point2D point = affineTransform.transform(new Point2D.Double(x1, y1), null);
|
||||
Point2D point2 = affineTransform.transform(new Point2D.Double(x2, y2), null);
|
||||
return new Box(Math.min(point.getX(), point2.getX()), Math.min(point.getY(), point2.getY()), Math.max(point.getX(), point2.getX()), Math.max(point.getY(), point2.getY()));
|
||||
}
|
||||
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.graphics;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
|
||||
public class DistinctQueue<T> {
|
||||
|
||||
private Queue<T> queue;
|
||||
private Set<T> set;
|
||||
|
||||
|
||||
public DistinctQueue() {
|
||||
|
||||
queue = new LinkedList<>();
|
||||
set = new HashSet<>();
|
||||
}
|
||||
|
||||
|
||||
public void enqueue(T element) {
|
||||
|
||||
if (!set.contains(element)) {
|
||||
queue.add(element);
|
||||
set.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public T dequeue() {
|
||||
|
||||
T element = queue.poll();
|
||||
if (element != null) {
|
||||
set.remove(element);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
public boolean isEmpty() {
|
||||
|
||||
return queue.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
public int size() {
|
||||
|
||||
return queue.size();
|
||||
}
|
||||
// Other methods as needed
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.graphics;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.CoordinateTransforms;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.PageInformation;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
@Service
|
||||
public class FindGraphicsRaster {
|
||||
|
||||
// Pixels that are lighter then this threshold are ignored
|
||||
private final static int THRESHOLD = 240;
|
||||
|
||||
// DPI to render the image at, in practice sub-72 seems to risk pixels being lost
|
||||
private final static int DPI = 72;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public List<Box> findCCBoundingBoxes(PDDocument doc, List<Rectangle2D> remove, PageInformation pageInformation) {
|
||||
|
||||
var renderer = new PDFRenderer(doc);
|
||||
var img = renderer.renderImageWithDPI(pageInformation.number() - 1, DPI, ImageType.GRAY);
|
||||
var imageCtm = CoordinateTransforms.calculateImageCoordsToInitialUserSpaceCoords(pageInformation, CoordinateTransforms.calculateScalingFactor(pageInformation, img.getWidth()));
|
||||
return findCCBoundingBoxes(img, remove, THRESHOLD, DPI / 72, imageCtm);
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private List<Box> findCCBoundingBoxes(BufferedImage image, List<Rectangle2D> remove, int grayScaleTresh, int rescale, AffineTransform imageCTM) {
|
||||
|
||||
var inverseCTM = imageCTM.createInverse();
|
||||
|
||||
var h = image.getHeight();
|
||||
var w = image.getWidth();
|
||||
var pixels = new int[w * h];
|
||||
image.getRaster().getPixels(0, 0, w, h, pixels);
|
||||
remove.stream().map(rect -> inverseCTM.createTransformedShape(rect).getBounds2D()).forEach(box -> {
|
||||
for (int y = (int) Math.floor(box.getMinY() / rescale); y <= (int) Math.min(Math.ceil(box.getMaxY() / rescale), h); y++) {
|
||||
for (int x = (int) Math.floor(box.getMinX() / rescale); x <= (int) Math.min(Math.ceil(box.getMaxX() / rescale), w); x++) {
|
||||
pixels[w * y + x] = grayScaleTresh;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// var image2 = createImageFromMatrix(pixels, w, h);
|
||||
|
||||
return findCCBoundingBoxes(pixels, w, h, grayScaleTresh, rescale, imageCTM);
|
||||
}
|
||||
|
||||
|
||||
public static BufferedImage createImageFromMatrix(int[] matrix, int width, int height) {
|
||||
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
|
||||
|
||||
byte[] pixelData = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int index = y * width + x;
|
||||
int pixel = matrix[index]; // Assuming each element in the matrix represents a pixel color
|
||||
pixelData[index] = (byte) pixel;
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
private List<Box> findCCBoundingBoxes(int[] pixels, int w, int h, int pixThreshold, int rescale, AffineTransform imageCTM) {
|
||||
|
||||
DistinctQueue<Integer> pixelsToExplore = new DistinctQueue<>();
|
||||
var boundingBoxes = new ArrayList<Box>();
|
||||
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
var pixelIndex = x + y * w;
|
||||
if (pixels[pixelIndex] < pixThreshold) {
|
||||
var minX = x;
|
||||
var maxX = x;
|
||||
var minY = y;
|
||||
var maxY = y;
|
||||
pixelsToExplore.enqueue(pixelIndex);
|
||||
while (!pixelsToExplore.isEmpty()) {
|
||||
var currentPixel = pixelsToExplore.dequeue();
|
||||
if (currentPixel > w) {
|
||||
var lowerPixel = currentPixel - w;
|
||||
if (pixels[lowerPixel] < pixThreshold) {
|
||||
pixelsToExplore.enqueue(currentPixel - w);
|
||||
minY = Math.min(minY, lowerPixel / w);
|
||||
}
|
||||
}
|
||||
if (currentPixel < pixels.length - w) {
|
||||
var upperPixel = currentPixel + w;
|
||||
if (pixels[upperPixel] < pixThreshold) {
|
||||
pixelsToExplore.enqueue(upperPixel);
|
||||
maxY = Math.max(maxY, upperPixel / w);
|
||||
}
|
||||
}
|
||||
if (currentPixel % w != 0) {
|
||||
var leftPixel = currentPixel - 1;
|
||||
if (pixels[leftPixel] < pixThreshold) {
|
||||
pixelsToExplore.enqueue(leftPixel);
|
||||
minX = Math.min(minX, leftPixel % w);
|
||||
}
|
||||
}
|
||||
if ((currentPixel + 1) % w != 0) {
|
||||
var rightPixel = currentPixel + 1;
|
||||
if (pixels[rightPixel] < pixThreshold) {
|
||||
pixelsToExplore.enqueue(rightPixel + 1);
|
||||
maxX = Math.max(maxX, rightPixel % w);
|
||||
}
|
||||
}
|
||||
// Set the current pixel to white so we don't visit it again.
|
||||
pixels[currentPixel] = pixThreshold;
|
||||
}
|
||||
boundingBoxes.add(new Box(minX * rescale, minY * rescale, maxX * rescale, maxY * rescale));
|
||||
}
|
||||
}
|
||||
}
|
||||
return boundingBoxes.stream().filter(box -> box.area() > 0).map(box -> box.transform(imageCTM)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.graphics;
|
||||
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.color.CMMException;
|
||||
import java.awt.geom.GeneralPath;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine;
|
||||
import org.apache.pdfbox.contentstream.PDFStreamEngine;
|
||||
import org.apache.pdfbox.contentstream.operator.Operator;
|
||||
import org.apache.pdfbox.contentstream.operator.OperatorProcessor;
|
||||
import org.apache.pdfbox.cos.COSBase;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImage;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class GraphicBBDetector extends PDFGraphicsStreamEngine {
|
||||
|
||||
private int clipWindingRule = -1;
|
||||
private final GeneralPath linePath = new GeneralPath();
|
||||
private final List<Rectangle> bounds = new ArrayList<>();
|
||||
private final PDColor emptyPattern = new PDColor(new float[3], null);
|
||||
|
||||
|
||||
public GraphicBBDetector(PDPage page, boolean ignoreWhite) {
|
||||
|
||||
super(page);
|
||||
|
||||
if (!ignoreWhite) {
|
||||
addOperator(new NullOp("d", this));
|
||||
addOperator(new NullOp("k", this));
|
||||
addOperator(new NullOp("K", this));
|
||||
addOperator(new NullOp("g", this));
|
||||
addOperator(new NullOp("G", this));
|
||||
addOperator(new NullOp("CS", this));
|
||||
addOperator(new NullOp("cs", this));
|
||||
addOperator(new NullOp("RG", this));
|
||||
addOperator(new NullOp("rg", this));
|
||||
addOperator(new NullOp("sc", this));
|
||||
addOperator(new NullOp("SC", this));
|
||||
addOperator(new NullOp("scn", this));
|
||||
addOperator(new NullOp("SCN", this));
|
||||
}
|
||||
|
||||
// Ignore text and font ops:
|
||||
addOperator(new NullOp("Tf", this));
|
||||
addOperator(new NullOp("Tj", this));
|
||||
addOperator(new NullOp("TJ", this));
|
||||
addOperator(new NullOp("T*", this));
|
||||
addOperator(new NullOp("'", this));
|
||||
addOperator(new NullOp("\"", this));
|
||||
}
|
||||
|
||||
|
||||
public List<Box> findGraphicBB() throws IOException {
|
||||
|
||||
processPage(getPage());
|
||||
return bounds.stream().map(r -> new Box(r.x, r.y, r.x + r.width, r.y + r.height)).filter(box -> box.area() > 0).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) {
|
||||
|
||||
linePath.moveTo((float) p0.getX(), (float) p0.getY());
|
||||
linePath.lineTo((float) p1.getX(), (float) p1.getY());
|
||||
linePath.lineTo((float) p2.getX(), (float) p2.getY());
|
||||
linePath.lineTo((float) p3.getX(), (float) p3.getY());
|
||||
linePath.closePath();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void drawImage(PDImage pdImage) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void clip(int windingRule) {
|
||||
|
||||
clipWindingRule = windingRule;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void moveTo(float x, float y) {
|
||||
|
||||
linePath.moveTo(x, y);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void lineTo(float x, float y) {
|
||||
|
||||
linePath.lineTo(x, y);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) {
|
||||
|
||||
linePath.curveTo(x1, y1, x2, y2, x3, y3);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Point2D getCurrentPoint() {
|
||||
|
||||
return linePath.getCurrentPoint();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void closePath() {
|
||||
|
||||
linePath.closePath();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void endPath() {
|
||||
|
||||
if (clipWindingRule != -1) {
|
||||
linePath.setWindingRule(clipWindingRule);
|
||||
getGraphicsState().intersectClippingPath(linePath);
|
||||
clipWindingRule = -1;
|
||||
}
|
||||
linePath.reset();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void strokePath() throws IOException {
|
||||
|
||||
addLinePath(true, false);
|
||||
linePath.reset();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void fillPath(int windingRule) throws IOException {
|
||||
|
||||
linePath.setWindingRule(windingRule);
|
||||
addLinePath(false, true);
|
||||
linePath.reset();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void fillAndStrokePath(int windingRule) throws IOException {
|
||||
|
||||
linePath.setWindingRule(windingRule);
|
||||
addLinePath(true, true);
|
||||
linePath.reset();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void shadingFill(COSName shadingName) {
|
||||
|
||||
var newBound = getGraphicsState().getCurrentClippingPath().getBounds();
|
||||
if (newBound.getWidth() > 0 && newBound.getHeight() > 0) {
|
||||
bounds.add(newBound);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void addLinePath(boolean stroke, boolean fill) throws IOException {
|
||||
|
||||
var newBound = getGraphicsState().getCurrentClippingPath().getBounds().intersection(linePath.getBounds());
|
||||
if (newBound.getWidth() > 0 && newBound.getHeight() > 0) {
|
||||
|
||||
if (stroke && !getGraphicsState().getStrokingColor().isPattern() && isBlack(getGraphicsState().getStrokingColor()) || //
|
||||
!stroke && !getGraphicsState().getNonStrokingColor().isPattern() && isBlack(getGraphicsState().getNonStrokingColor())) {
|
||||
bounds.add(newBound);
|
||||
}
|
||||
|
||||
// var skipWhiteGraphic = ignoreWhite && (!stroke || isWhite(getGraphicsState().getStrokingColor())) && (!fill || isWhite(getGraphicsState().getNonStrokingColor()));
|
||||
// if (!skipWhiteGraphic) {
|
||||
// bounds.add(newBound);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private boolean isBlack(PDColor color) {
|
||||
|
||||
try {
|
||||
return color.toRGB() == 0;
|
||||
} catch (CMMException e) {
|
||||
// see https://github.com/haraldk/TwelveMonkeys/issues/124 or https://issues.apache.org/jira/browse/PDFBOX-3531
|
||||
// This is a quick and dirt hack
|
||||
// Happens for file 216.pdf
|
||||
log.debug(e.getMessage());
|
||||
var result = true;
|
||||
for (var component : color.getComponents()) {
|
||||
result = result && component == 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean isWhite(PDColor color) throws IOException {
|
||||
|
||||
return !color.isPattern() && color.toRGB() == 16777215 || color.equals(emptyPattern);
|
||||
}
|
||||
|
||||
|
||||
private final class NullOp extends OperatorProcessor {
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
private NullOp(String name, PDFStreamEngine context) {
|
||||
|
||||
super(context);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void process(Operator operator, List<COSBase> operands) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.graphics;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.factory.SearchTextWithTextPositionFactory;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.PageInformation;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GraphicExtractorService {
|
||||
|
||||
private static final int MIN_GRAPHICS_SIDE_LENGTH = 30;
|
||||
private static final int MIN_GRAPHICS_AREA = 500;
|
||||
|
||||
private final GraphicsClusteringService graphicsClusteringService;
|
||||
private final FindGraphicsRaster findGraphicsRaster;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public List<Box> extractPathElementGraphics(PDDocument pdDocument,
|
||||
PDPage pdPage,
|
||||
int pageNumber,
|
||||
CleanRulings cleanRulings,
|
||||
List<TextPositionSequence> textPositionSequences,
|
||||
boolean graphicsRaster) {
|
||||
|
||||
List<Box> characterBBoxes = getCharacterBBoxes(textPositionSequences);
|
||||
List<Box> classifiedRulingsBoxes = getLineBBoxesOfAllClassifiedRulings(cleanRulings);
|
||||
|
||||
GraphicBBDetector graphicBBDetector = new GraphicBBDetector(pdPage, true);
|
||||
List<Box> graphicBBoxes = graphicBBDetector.findGraphicBB();
|
||||
|
||||
if (graphicsRaster) {
|
||||
// This should only be used if ocr was performed, it is currently in an early stage and needs to be improved.
|
||||
graphicBBoxes.addAll(findGraphicsRaster.findCCBoundingBoxes(pdDocument,
|
||||
characterBBoxes.stream()
|
||||
.map(box -> new Rectangle2D.Double(box.x1 - 2, box.y1 - 2, box.width() + 4, box.height() + 4))
|
||||
.collect(Collectors.toList()),
|
||||
PageInformation.fromPDPage(pageNumber, pdPage)));
|
||||
}
|
||||
|
||||
List<Box> filteredGraphicBBoxes = graphicBBoxes.stream()
|
||||
.filter(box -> !box.intersectsAny(classifiedRulingsBoxes, 4))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Box> clusters = graphicsClusteringService.getClusters(filteredGraphicBBoxes, 14);
|
||||
|
||||
return clusters.stream()
|
||||
.filter(box -> box.area() > MIN_GRAPHICS_AREA && box.height() > MIN_GRAPHICS_SIDE_LENGTH && box.width() > MIN_GRAPHICS_SIDE_LENGTH)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
private List<Box> getCharacterBBoxes(List<TextPositionSequence> textPositionSequences) {
|
||||
|
||||
return textPositionSequences.stream()
|
||||
.map(pos -> pos.getTextPositions()
|
||||
.stream()
|
||||
.map(tp -> SearchTextWithTextPositionFactory.mapRedTextPositionToInitialUserSpace(tp, pos))
|
||||
.collect(RectangleTransformations.collectBBox()))
|
||||
.map(Box::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
private List<Box> getLineBBoxesOfAllClassifiedRulings(CleanRulings cleanRulings) {
|
||||
|
||||
return cleanRulings.buildAll()
|
||||
.stream()
|
||||
.filter(ruling -> !ruling.getClassification().equals(Ruling.Classification.OTHER))
|
||||
.map(h -> new Box(h.x1, h.y1, h.x2, h.y2))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.graphics;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class GraphicsClusteringService {
|
||||
|
||||
public List<Box> getClusters(List<Box> boxes, double tol) {
|
||||
|
||||
if (boxes.isEmpty()) {
|
||||
return boxes;
|
||||
} else {
|
||||
// We iteratively pick a Box that intersects at least one other box and replace the
|
||||
// intersecting box with a Box containing them
|
||||
var currentBoxes = boxes;
|
||||
var foundIntersectingBoxes = true;
|
||||
while (foundIntersectingBoxes) {
|
||||
foundIntersectingBoxes = false;
|
||||
|
||||
// The box we are going to check to see if there are any intersecting boxes, followed by
|
||||
// any boxes that we have already check
|
||||
var checked = List.of(currentBoxes.get(0));
|
||||
var unchecked = currentBoxes.subList(1, currentBoxes.size());
|
||||
|
||||
while (!foundIntersectingBoxes && !unchecked.isEmpty()) {
|
||||
|
||||
List<Box> intersects = new ArrayList<>();
|
||||
List<Box> nonIntersects = new ArrayList<>();
|
||||
|
||||
for (Box uncheck : unchecked) {
|
||||
if (uncheck.intersects(checked.get(0), tol)) {
|
||||
intersects.add(uncheck);
|
||||
} else {
|
||||
nonIntersects.add(uncheck);
|
||||
}
|
||||
}
|
||||
|
||||
if (!intersects.isEmpty()) {
|
||||
List<Box> combinedIntersecting = new ArrayList<>();
|
||||
combinedIntersecting.add(checked.get(0));
|
||||
combinedIntersecting.addAll(intersects);
|
||||
var newBox = merge(combinedIntersecting);
|
||||
|
||||
List<Box> newCurrentBoxes = new ArrayList<>();
|
||||
newCurrentBoxes.add(newBox);
|
||||
newCurrentBoxes.addAll(checked.subList(1, checked.size()));
|
||||
newCurrentBoxes.addAll(nonIntersects);
|
||||
currentBoxes = newCurrentBoxes;
|
||||
foundIntersectingBoxes = true; // Exit this loop and re-enter the outer loop
|
||||
} else {
|
||||
List<Box> newChecked = new ArrayList<>();
|
||||
newChecked.add(unchecked.get(0));
|
||||
newChecked.addAll(checked);
|
||||
checked = newChecked;
|
||||
unchecked = unchecked.subList(1, unchecked.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentBoxes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Box merge(List<Box> boxes) {
|
||||
|
||||
double minX = Double.POSITIVE_INFINITY;
|
||||
double minY = Double.POSITIVE_INFINITY;
|
||||
double maxX = Double.NEGATIVE_INFINITY;
|
||||
double maxY = Double.NEGATIVE_INFINITY;
|
||||
|
||||
for (Box box : boxes) {
|
||||
minX = Math.min(minX, box.x1);
|
||||
minY = Math.min(minY, box.y1);
|
||||
maxX = Math.max(maxX, box.x2);
|
||||
maxY = Math.max(maxY, box.y2);
|
||||
}
|
||||
return new Box(minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
}
|
||||
+8
-1
@@ -23,6 +23,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Pa
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Section;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SuperSection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Table;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.TableCell;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
|
||||
@@ -62,6 +63,7 @@ public class DocumentGraphMapper {
|
||||
|
||||
SemanticNode node = switch (entryData.getType()) {
|
||||
case SECTION -> buildSection(context);
|
||||
case SUPER_SECTION -> buildSuperSection(context);
|
||||
case PARAGRAPH -> buildParagraph(context, entryData.getProperties());
|
||||
case HEADLINE -> buildHeadline(context);
|
||||
case HEADER -> buildHeader(context);
|
||||
@@ -109,7 +111,7 @@ public class DocumentGraphMapper {
|
||||
|
||||
private TableCell buildTableCell(Context context, Map<String, String> properties) {
|
||||
|
||||
TableCell.TableCellBuilder builder = TableCell.builder();
|
||||
TableCell.TableCellBuilder<?, ?> builder = TableCell.builder();
|
||||
PropertiesMapper.parseTableCellProperties(properties, builder);
|
||||
return builder.documentTree(context.documentTree).build();
|
||||
}
|
||||
@@ -140,6 +142,11 @@ public class DocumentGraphMapper {
|
||||
return Section.builder().documentTree(context.documentTree).build();
|
||||
}
|
||||
|
||||
private SuperSection buildSuperSection(Context context) {
|
||||
|
||||
return SuperSection.builder().documentTree(context.documentTree).build();
|
||||
}
|
||||
|
||||
|
||||
private Paragraph buildParagraph(Context context, Map<String, String> properties) {
|
||||
|
||||
|
||||
+39
-56
@@ -82,7 +82,6 @@ public class LegacyPDFStreamEngine extends PDFStreamEngine {
|
||||
|
||||
private int pageRotation;
|
||||
private PDRectangle pageSize;
|
||||
private Matrix translateMatrix;
|
||||
private final GlyphList glyphList;
|
||||
private final Map<COSDictionary, Float> fontHeightMap = new WeakHashMap<COSDictionary, Float>();
|
||||
|
||||
@@ -134,12 +133,6 @@ public class LegacyPDFStreamEngine extends PDFStreamEngine {
|
||||
this.pageRotation = page.getRotation();
|
||||
this.pageSize = page.getCropBox();
|
||||
|
||||
if (pageSize.getLowerLeftX() == 0 && pageSize.getLowerLeftY() == 0) {
|
||||
translateMatrix = null;
|
||||
} else {
|
||||
// translation matrix for cropbox
|
||||
translateMatrix = Matrix.getTranslateInstance(-pageSize.getLowerLeftX(), -pageSize.getLowerLeftY());
|
||||
}
|
||||
super.processPage(page);
|
||||
}
|
||||
|
||||
@@ -265,62 +258,52 @@ public class LegacyPDFStreamEngine extends PDFStreamEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// adjust for cropbox if needed
|
||||
Matrix translatedTextRenderingMatrix;
|
||||
if (translateMatrix == null) {
|
||||
translatedTextRenderingMatrix = textRenderingMatrix;
|
||||
} else {
|
||||
translatedTextRenderingMatrix = Matrix.concatenate(translateMatrix, textRenderingMatrix);
|
||||
nextX -= pageSize.getLowerLeftX();
|
||||
nextY -= pageSize.getLowerLeftY();
|
||||
}
|
||||
|
||||
// This is a hack for unicode letter with 2 chars e.g. RA see unicodeProblem.pdf
|
||||
if (unicodeMapping.length() == 2) {
|
||||
processTextPosition(new TextPosition(pageRotation,
|
||||
pageSize.getWidth(),
|
||||
pageSize.getHeight(),
|
||||
translatedTextRenderingMatrix,
|
||||
nextX,
|
||||
nextY,
|
||||
Math.abs(dyDisplay),
|
||||
dxDisplay,
|
||||
Math.abs(spaceWidthDisplay),
|
||||
Character.toString(unicodeMapping.charAt(0)),
|
||||
new int[]{code},
|
||||
font,
|
||||
fontSize,
|
||||
(int) (fontSize * textMatrix.getScalingFactorX())));
|
||||
pageSize.getWidth(),
|
||||
pageSize.getHeight(),
|
||||
textRenderingMatrix,
|
||||
nextX,
|
||||
nextY,
|
||||
Math.abs(dyDisplay),
|
||||
dxDisplay,
|
||||
Math.abs(spaceWidthDisplay),
|
||||
Character.toString(unicodeMapping.charAt(0)),
|
||||
new int[]{code},
|
||||
font,
|
||||
fontSize,
|
||||
(int) (fontSize * textMatrix.getScalingFactorX())));
|
||||
processTextPosition(new TextPosition(pageRotation,
|
||||
pageSize.getWidth(),
|
||||
pageSize.getHeight(),
|
||||
translatedTextRenderingMatrix,
|
||||
nextX,
|
||||
nextY,
|
||||
Math.abs(dyDisplay),
|
||||
dxDisplay,
|
||||
Math.abs(spaceWidthDisplay),
|
||||
Character.toString(unicodeMapping.charAt(1)),
|
||||
new int[]{code},
|
||||
font,
|
||||
fontSize,
|
||||
(int) (fontSize * textMatrix.getScalingFactorX())));
|
||||
pageSize.getWidth(),
|
||||
pageSize.getHeight(),
|
||||
textRenderingMatrix,
|
||||
nextX,
|
||||
nextY,
|
||||
Math.abs(dyDisplay),
|
||||
dxDisplay,
|
||||
Math.abs(spaceWidthDisplay),
|
||||
Character.toString(unicodeMapping.charAt(1)),
|
||||
new int[]{code},
|
||||
font,
|
||||
fontSize,
|
||||
(int) (fontSize * textMatrix.getScalingFactorX())));
|
||||
} else {
|
||||
|
||||
processTextPosition(new TextPosition(pageRotation,
|
||||
pageSize.getWidth(),
|
||||
pageSize.getHeight(),
|
||||
translatedTextRenderingMatrix,
|
||||
nextX,
|
||||
nextY,
|
||||
Math.abs(dyDisplay),
|
||||
dxDisplay,
|
||||
Math.abs(spaceWidthDisplay),
|
||||
unicodeMapping,
|
||||
new int[]{code},
|
||||
font,
|
||||
fontSize,
|
||||
(int) (fontSize * textMatrix.getScalingFactorX())));
|
||||
pageSize.getWidth(),
|
||||
pageSize.getHeight(),
|
||||
textRenderingMatrix,
|
||||
nextX,
|
||||
nextY,
|
||||
Math.abs(dyDisplay),
|
||||
dxDisplay,
|
||||
Math.abs(spaceWidthDisplay),
|
||||
unicodeMapping,
|
||||
new int[]{code},
|
||||
font,
|
||||
fontSize,
|
||||
(int) (fontSize * textMatrix.getScalingFactorX())));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -1007,7 +1007,7 @@ public class PDFTextStripper extends LegacyPDFStreamEngine {
|
||||
|
||||
/**
|
||||
* Set the desired word separator for output text. The PDFBox text extraction algorithm will output a space
|
||||
* character if there is enough space between two words. By default a space character is used. If you need and
|
||||
* character if there is enough space between two textPositions. By default a space character is used. If you need and
|
||||
* accurate count of characters that are found in a PDF document then you might want to set the word separator to
|
||||
* the empty string.
|
||||
*
|
||||
@@ -1703,7 +1703,7 @@ public class PDFTextStripper extends LegacyPDFStreamEngine {
|
||||
/**
|
||||
* Write a list of string containing a whole line of a document.
|
||||
*
|
||||
* @param line a list with the words of the given line
|
||||
* @param line a list with the textPositions of the given line
|
||||
* @throws IOException if something went wrong
|
||||
*/
|
||||
private void writeLine(List<WordWithTextPositions> line, boolean isParagraphEnd) throws IOException {
|
||||
@@ -1744,9 +1744,9 @@ public class PDFTextStripper extends LegacyPDFStreamEngine {
|
||||
|
||||
|
||||
/**
|
||||
* Handles the LTR and RTL direction of the given words. The whole implementation stands and falls with the given
|
||||
* word. If the word is a full line, the results will be the best. If the word contains of single words or
|
||||
* characters, the order of the characters in a word or words in a line may wrong, due to RTL and LTR marks and
|
||||
* Handles the LTR and RTL direction of the given textPositions. The whole implementation stands and falls with the given
|
||||
* word. If the word is a full line, the results will be the best. If the word contains of single textPositions or
|
||||
* characters, the order of the characters in a word or textPositions in a line may wrong, due to RTL and LTR marks and
|
||||
* characters!
|
||||
* <p>
|
||||
* Based on http://www.nesterovsky-bros.com/weblog/2013/07/28/VisualToLogicalConversionInJava.aspx
|
||||
|
||||
+217
-83
@@ -9,6 +9,7 @@ import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -23,6 +24,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Do
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.DuplicatedParagraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SuperSection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Table;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.TableCell;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
@@ -62,15 +64,29 @@ public class LayoutGridService {
|
||||
static Color HEADER_COLOR = new Color(171, 131, 6);
|
||||
static Color IMAGE_COLOR = new Color(253, 63, 146);
|
||||
|
||||
private record RectangleIdentifier(List<Integer> treeId, Integer pageNumber) {
|
||||
|
||||
}
|
||||
|
||||
HashMap<RectangleIdentifier, Rectangle2D> rectangleMap = new HashMap<>();
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Observed(name = "ViewerDocumentService", contextualName = "create-viewer-document")
|
||||
public void addLayoutGrid(File originFile, Document document, File destinationFile, boolean layerVisibilityDefaultValue) {
|
||||
public void addLayoutGrid(File originFile, Document document, File destinationFile, boolean layerVisibilityDefaultValue, boolean writeVisualLayoutParsingGrid) {
|
||||
|
||||
List<Visualizations> allVisualizations;
|
||||
Visualizations layoutGrid = this.addLayoutGrid(document, layerVisibilityDefaultValue, false);
|
||||
Visualizations visualLayoutGrid = this.addLayoutGrid(document, layerVisibilityDefaultValue, true);
|
||||
if (writeVisualLayoutParsingGrid) {
|
||||
Visualizations visualLayoutGrid = this.addLayoutGrid(document, layerVisibilityDefaultValue, true);
|
||||
allVisualizations = Stream.concat(Stream.of(layoutGrid, visualLayoutGrid), document.getVisualizations().streamAll())
|
||||
.toList();
|
||||
} else {
|
||||
allVisualizations = Stream.concat(Stream.of(layoutGrid), document.getVisualizations().streamAll())
|
||||
.toList();
|
||||
}
|
||||
|
||||
viewerDocumentService.addVisualizationsOnPage(originFile, destinationFile, List.of(layoutGrid, visualLayoutGrid));
|
||||
viewerDocumentService.addVisualizationsOnPage(originFile, destinationFile, allVisualizations);
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +113,7 @@ public class LayoutGridService {
|
||||
Color color = switch (semanticNode.getType()) {
|
||||
case PARAGRAPH -> PARAGRAPH_COLOR;
|
||||
case TABLE -> TABLE_COLOR;
|
||||
case SECTION -> SECTION_COLOR;
|
||||
case SECTION, SUPER_SECTION -> SECTION_COLOR;
|
||||
case HEADLINE -> HEADLINE_COLOR;
|
||||
case HEADER, FOOTER -> HEADER_COLOR;
|
||||
case IMAGE -> IMAGE_COLOR;
|
||||
@@ -111,7 +127,7 @@ public class LayoutGridService {
|
||||
if (isNotSectionOrTableCellOrDocument(semanticNode)) {
|
||||
addAsRectangle(semanticNode, layoutGrid, color);
|
||||
}
|
||||
if (semanticNode.getType().equals(NodeType.SECTION)) {
|
||||
if (semanticNode.getType().equals(NodeType.SECTION) || semanticNode.getType().equals(NodeType.SUPER_SECTION)) {
|
||||
addSection(semanticNode, layoutGrid, color);
|
||||
}
|
||||
if (semanticNode.getType().equals(NodeType.TABLE)) {
|
||||
@@ -129,11 +145,16 @@ public class LayoutGridService {
|
||||
return;
|
||||
}
|
||||
for (Page page : table.getPages()) {
|
||||
Optional<Integer> optionalFirstRowOnPage = table.streamCol(0).filter(tableCell -> tableCell.isOnPage(page.getNumber())).map(TableCell::getRow).findFirst();
|
||||
|
||||
Optional<Integer> optionalFirstRowOnPage = table.streamCol(0)
|
||||
.filter(tableCell -> tableCell.isOnPage(page.getNumber()))
|
||||
.map(TableCell::getRow)
|
||||
.findFirst();
|
||||
if (optionalFirstRowOnPage.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int firstRowOnPage = optionalFirstRowOnPage.get();
|
||||
|
||||
Stream<Double> xStream = switch (page.getRotation()) {
|
||||
case 90 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMinX);
|
||||
case 180 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMaxX);
|
||||
@@ -142,6 +163,7 @@ public class LayoutGridService {
|
||||
};
|
||||
List<Double> xs = xStream.collect(Collectors.toList());
|
||||
xs.remove(0);
|
||||
|
||||
Stream<Double> yStream = switch (page.getRotation()) {
|
||||
case 90 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMinY);
|
||||
case 180 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMinY);
|
||||
@@ -151,8 +173,10 @@ public class LayoutGridService {
|
||||
List<Double> ys = yStream.collect(Collectors.toList());
|
||||
ys.remove(0);
|
||||
|
||||
Rectangle2D tableBBox = table.getBBox().get(table.getFirstPage());
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages().get(page.getNumber() - 1).getColoredLines();
|
||||
Rectangle2D tableBBox = table.getBBox()
|
||||
.get(page);
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages()
|
||||
.get(page.getNumber() - 1).getColoredLines();
|
||||
xs.forEach(x -> {
|
||||
Line2D line = new Line2D.Double(new Point2D.Double(x, tableBBox.getMaxY()), new Point2D.Double(x, tableBBox.getMinY()));
|
||||
coloredLines.add(new ColoredLine(line, INNER_LINES_COLOR, LINE_WIDTH));
|
||||
@@ -167,130 +191,239 @@ public class LayoutGridService {
|
||||
|
||||
private static Stream<Rectangle2D> streamBBoxOfCellsOnPage(Stream<TableCell> table, Page page) {
|
||||
|
||||
return table.filter(tableCell -> tableCell.isOnPage(page.getNumber())).map(TableCell::getBBox).map(bBoxMap -> bBoxMap.get(page));
|
||||
return table.filter(tableCell -> tableCell.isOnPage(page.getNumber()))
|
||||
.map(TableCell::getBBox)
|
||||
.map(bBoxMap -> bBoxMap.get(page));
|
||||
}
|
||||
|
||||
|
||||
private void addSection(SemanticNode semanticNode, LayoutGrid layoutGrid, Color color) {
|
||||
|
||||
Map<Page, Rectangle2D> bBoxMap = semanticNode.getBBox();
|
||||
List<SemanticNode> subSections = semanticNode.streamAllSubNodesOfType(NodeType.SECTION).toList();
|
||||
|
||||
List<SemanticNode> subSections = semanticNode.streamAllSubNodesOfType(NodeType.SECTION)
|
||||
.toList();
|
||||
Integer maxChildDepth = subSections.stream()
|
||||
.map(node -> node.getTreeId().size())
|
||||
.max(Integer::compareTo)
|
||||
.orElse(semanticNode.getTreeId().size());
|
||||
int ownDepth = semanticNode.getTreeId().size();
|
||||
|
||||
Page firstPage = semanticNode.getFirstPage();
|
||||
if (!subSections.isEmpty()) {
|
||||
addPlacedText(firstPage, bBoxMap.get(firstPage), buildTreeIdString(semanticNode), layoutGrid);
|
||||
} else {
|
||||
bBoxMap.forEach(((page, textBBox) -> addPlacedText(page, textBBox, buildTreeIdString(semanticNode), layoutGrid)));
|
||||
}
|
||||
String treeIdString = buildTreeIdString(semanticNode);
|
||||
|
||||
if (bBoxMap.values().size() == 1) {
|
||||
Rectangle2D r = RectangleTransformations.pad(bBoxMap.get(firstPage), LINE_WIDTH, LINE_WIDTH);
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages().get(firstPage.getNumber() - 1).getColoredLines();
|
||||
List<Line2D> lines = createLinesFromRectangle(r, firstPage.getRotation());
|
||||
// add string to top line
|
||||
var firstLine = lines.remove(0);
|
||||
coloredLines.add(new ColoredLine(firstLine, color, LINE_WIDTH));
|
||||
for (Line2D line : lines) {
|
||||
coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
handleSinglePage(semanticNode, layoutGrid, color, firstPage, bBoxMap.get(firstPage), treeIdString, maxChildDepth, ownDepth);
|
||||
return;
|
||||
}
|
||||
List<Page> pagesInOrder = bBoxMap.keySet().stream().sorted(Comparator.comparingInt(Page::getNumber)).collect(Collectors.toList());
|
||||
List<Page> pagesInOrder = bBoxMap.keySet()
|
||||
.stream()
|
||||
.sorted(Comparator.comparingInt(Page::getNumber))
|
||||
.collect(Collectors.toList());
|
||||
pagesInOrder.remove(0);
|
||||
addLinesForFirstPageOfSection(semanticNode, color, firstPage, layoutGrid);
|
||||
var lastPage = pagesInOrder.remove(pagesInOrder.size() - 1);
|
||||
addLinesForLastPageOfSection(semanticNode, color, lastPage, layoutGrid);
|
||||
for (Page middlePage : pagesInOrder) {
|
||||
addLinesForMiddlePageOfSection(semanticNode, color, middlePage, layoutGrid);
|
||||
handleFirstPageOfSection(semanticNode, color, firstPage, bBoxMap.get(firstPage), treeIdString, layoutGrid, maxChildDepth, ownDepth);
|
||||
if (semanticNode instanceof SuperSection) {
|
||||
return;
|
||||
}
|
||||
for (Page middlePage : pagesInOrder.subList(0, pagesInOrder.size() - 1)) {
|
||||
handleForMiddlePageOfSection(semanticNode, color, middlePage, bBoxMap.get(middlePage), treeIdString, layoutGrid, maxChildDepth, ownDepth);
|
||||
}
|
||||
var lastPage = pagesInOrder.remove(pagesInOrder.size() - 1);
|
||||
handleLastPageOfSection(semanticNode, color, lastPage, bBoxMap.get(lastPage), treeIdString, layoutGrid, maxChildDepth, ownDepth);
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private void addPlacedText(Page page, Rectangle2D textBBox, String s, LayoutGrid layoutGrid) {
|
||||
private void addPlacedText(Page page, Rectangle2D textBBox, Rectangle2D highestParentRect, String s, LayoutGrid layoutGrid, Integer maxChildDepth) {
|
||||
|
||||
// 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);
|
||||
float translationAmount = ((FONT.getStringWidth(s) / 1000) * FONT_SIZE + LINE_WIDTH + 2 * maxChildDepth);
|
||||
|
||||
Point2D upperLeftCorner;
|
||||
Point2D translationVector;
|
||||
switch (page.getRotation()) {
|
||||
case 90 -> {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMinY());
|
||||
if (highestParentRect != null) {
|
||||
upperLeftCorner = new Point2D.Double(highestParentRect.getMinX(), textBBox.getMinY());
|
||||
} else {
|
||||
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());
|
||||
if (highestParentRect != null) {
|
||||
upperLeftCorner = new Point2D.Double(highestParentRect.getMaxX(), textBBox.getMinY());
|
||||
} else {
|
||||
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());
|
||||
|
||||
if (highestParentRect != null) {
|
||||
upperLeftCorner = new Point2D.Double(highestParentRect.getMaxX(), textBBox.getMaxY());
|
||||
} else {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMaxX(), textBBox.getMaxY());
|
||||
}
|
||||
translationVector = new Point2D.Double(-FONT_SIZE, translationAmount);
|
||||
}
|
||||
default -> {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMaxY());
|
||||
|
||||
if (highestParentRect != null) {
|
||||
upperLeftCorner = new Point2D.Double(highestParentRect.getMinX(), textBBox.getMaxY());
|
||||
} else {
|
||||
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();
|
||||
placedTexts.add(PlacedText.textFacingUp(s, upperLeftCorner, FONT_SIZE, Color.BLACK, FONT));
|
||||
}
|
||||
List<PlacedText> placedTexts = layoutGrid.getVisualizationsPerPages()
|
||||
.get(page.getNumber() - 1).getPlacedTexts();
|
||||
|
||||
PlacedText newText = PlacedText.textFacingUp(s, upperLeftCorner, FONT_SIZE, Color.BLACK, FONT);
|
||||
|
||||
private void addLinesForMiddlePageOfSection(SemanticNode semanticNode, Color color, Page middlePage, LayoutGrid layoutGrid) {
|
||||
Optional<PlacedText> conflictingText = placedTexts.stream()
|
||||
.filter(pt -> Math.abs(pt.lineStart().getY() - newText.lineStart().getY()) <= FONT_SIZE)
|
||||
.findFirst();
|
||||
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages().get(middlePage.getNumber() - 1).getColoredLines();
|
||||
Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox().get(middlePage), LINE_WIDTH, LINE_WIDTH);
|
||||
var midPageLines = createLinesFromRectangle(r, middlePage.getRotation());
|
||||
// remove top line
|
||||
midPageLines.remove(0);
|
||||
// remove top line
|
||||
midPageLines.remove(1);
|
||||
// add string to left line
|
||||
var leftLine = midPageLines.remove(1);
|
||||
coloredLines.add(new ColoredLine(leftLine, color, LINE_WIDTH));
|
||||
for (Line2D line : midPageLines) {
|
||||
coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
if (conflictingText.isPresent()) {
|
||||
PlacedText existingText = conflictingText.get();
|
||||
if (newText.text().length() > existingText.text().length()) {
|
||||
placedTexts.remove(existingText);
|
||||
placedTexts.add(newText);
|
||||
}
|
||||
} else {
|
||||
placedTexts.add(newText);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void addLinesForLastPageOfSection(SemanticNode semanticNode, Color color, Page lastPage, LayoutGrid layoutGrid) {
|
||||
private void handleSinglePage(SemanticNode semanticNode,
|
||||
LayoutGrid layoutGrid,
|
||||
Color color,
|
||||
Page page,
|
||||
Rectangle2D rectangle2D,
|
||||
String treeIdString,
|
||||
Integer maxChildDepth,
|
||||
Integer ownDepth) {
|
||||
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages().get(lastPage.getNumber() - 1).getColoredLines();
|
||||
Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox().get(lastPage), LINE_WIDTH, LINE_WIDTH);
|
||||
var lastPageLines = createLinesFromRectangle(r, lastPage.getRotation());
|
||||
// remove top line
|
||||
lastPageLines.remove(0);
|
||||
// add string to left line
|
||||
var leftLine = lastPageLines.remove(2);
|
||||
coloredLines.add(new ColoredLine(leftLine, color, LINE_WIDTH));
|
||||
for (Line2D line : lastPageLines) {
|
||||
coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void addLinesForFirstPageOfSection(SemanticNode semanticNode, Color color, Page firstPage, LayoutGrid layoutGrid) {
|
||||
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages().get(firstPage.getNumber() - 1).getColoredLines();
|
||||
Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox().get(firstPage), LINE_WIDTH, LINE_WIDTH);
|
||||
var firstPageLines = createLinesFromRectangle(r, firstPage.getRotation());
|
||||
// remove bottom line
|
||||
firstPageLines.remove(2);
|
||||
RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, page, rectangle2D, treeIdString, layoutGrid, maxChildDepth, ownDepth);
|
||||
// add string to top line
|
||||
var firstLine = firstPageLines.remove(0);
|
||||
coloredLines.add(new ColoredLine(firstLine, color, LINE_WIDTH));
|
||||
for (Line2D line : firstPageLines) {
|
||||
coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
var firstLine = result.pageLines().remove(0);
|
||||
result.coloredLines().add(new ColoredLine(firstLine, color, LINE_WIDTH));
|
||||
for (Line2D line : result.pageLines()) {
|
||||
result.coloredLines().add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void handleFirstPageOfSection(SemanticNode semanticNode,
|
||||
Color color,
|
||||
Page firstPage,
|
||||
Rectangle2D rectangle2D,
|
||||
String treeIdString,
|
||||
LayoutGrid layoutGrid,
|
||||
Integer maxChildDepth,
|
||||
Integer ownDepth) {
|
||||
|
||||
RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, firstPage, rectangle2D, treeIdString, layoutGrid, maxChildDepth, ownDepth);
|
||||
// remove bottom line
|
||||
result.pageLines().remove(2);
|
||||
// add string to top line
|
||||
var firstLine = result.pageLines().remove(0);
|
||||
result.coloredLines().add(new ColoredLine(firstLine, color, LINE_WIDTH));
|
||||
for (Line2D line : result.pageLines()) {
|
||||
result.coloredLines().add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void handleForMiddlePageOfSection(SemanticNode semanticNode,
|
||||
Color color,
|
||||
Page middlePage,
|
||||
Rectangle2D rectangle2D,
|
||||
String treeIdString,
|
||||
LayoutGrid layoutGrid,
|
||||
Integer maxChildDepth,
|
||||
Integer ownDepth) {
|
||||
|
||||
RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, middlePage, rectangle2D, treeIdString, layoutGrid, maxChildDepth, ownDepth);
|
||||
// remove top line
|
||||
result.pageLines().remove(0);
|
||||
// remove bottom line
|
||||
result.pageLines().remove(1);
|
||||
// add string to left line
|
||||
var leftLine = result.pageLines().remove(1);
|
||||
result.coloredLines().add(new ColoredLine(leftLine, color, LINE_WIDTH));
|
||||
for (Line2D line : result.pageLines()) {
|
||||
result.coloredLines().add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void handleLastPageOfSection(SemanticNode semanticNode,
|
||||
Color color,
|
||||
Page lastPage,
|
||||
Rectangle2D rectangle2D,
|
||||
String treeIdString,
|
||||
LayoutGrid layoutGrid,
|
||||
Integer maxChildDepth,
|
||||
Integer ownDepth) {
|
||||
|
||||
RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, lastPage, rectangle2D, treeIdString, layoutGrid, maxChildDepth, ownDepth);
|
||||
// remove top line
|
||||
result.pageLines().remove(0);
|
||||
// add string to left line
|
||||
var leftLine = result.pageLines().remove(2);
|
||||
result.coloredLines().add(new ColoredLine(leftLine, color, LINE_WIDTH));
|
||||
for (Line2D line : result.pageLines()) {
|
||||
result.coloredLines().add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private RectangleAndLinesResult createLinesAndPlaceText(SemanticNode semanticNode,
|
||||
Page page,
|
||||
Rectangle2D rectangle2D,
|
||||
String treeIdString,
|
||||
LayoutGrid layoutGrid,
|
||||
Integer maxChildDepth,
|
||||
Integer ownDepth) {
|
||||
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages()
|
||||
.get(page.getNumber() - 1).getColoredLines();
|
||||
int lineWidthModifier = maxChildDepth - ownDepth;
|
||||
Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox()
|
||||
.get(page), LINE_WIDTH * (1 + lineWidthModifier), LINE_WIDTH * (1 + lineWidthModifier));
|
||||
var lastPageLines = createLinesFromRectangle(r, page.getRotation());
|
||||
|
||||
SemanticNode highestParent = semanticNode.getHighestParent();
|
||||
Rectangle2D highestParentRect = rectangleMap.get(new RectangleIdentifier(highestParent.getTreeId(), page.getNumber()));
|
||||
addPlacedText(page, rectangle2D, highestParentRect, treeIdString, layoutGrid, maxChildDepth);
|
||||
|
||||
if (semanticNode instanceof SuperSection) {
|
||||
rectangleMap.put(new RectangleIdentifier(semanticNode.getTreeId(), page.getNumber()), r);
|
||||
}
|
||||
|
||||
return new RectangleAndLinesResult(coloredLines, r, lastPageLines);
|
||||
}
|
||||
|
||||
|
||||
private record RectangleAndLinesResult(List<ColoredLine> coloredLines, Rectangle2D rectangle, List<Line2D> pageLines) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private String buildTreeIdString(SemanticNode semanticNode) {
|
||||
|
||||
return semanticNode.getTreeId().stream().map(Object::toString).collect(Collectors.joining("."));
|
||||
return semanticNode.getTreeId()
|
||||
.stream()
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.joining("."));
|
||||
}
|
||||
|
||||
|
||||
@@ -342,7 +475,10 @@ public class LayoutGridService {
|
||||
|
||||
private static boolean isNotSectionOrTableCellOrDocument(SemanticNode semanticNode) {
|
||||
|
||||
return !(semanticNode.getType().equals(NodeType.DOCUMENT) || semanticNode.getType().equals(NodeType.SECTION) || semanticNode.getType().equals(NodeType.TABLE_CELL));
|
||||
return !(semanticNode.getType().equals(NodeType.DOCUMENT)
|
||||
|| semanticNode.getType().equals(NodeType.SECTION)
|
||||
|| semanticNode.getType().equals(NodeType.SUPER_SECTION)
|
||||
|| semanticNode.getType().equals(NodeType.TABLE_CELL));
|
||||
}
|
||||
|
||||
|
||||
@@ -350,9 +486,7 @@ public class LayoutGridService {
|
||||
|
||||
semanticNode.getBBox()
|
||||
.forEach((page, textBBox) -> layoutGrid.getVisualizationsPerPages()
|
||||
.get(page.getNumber() - 1)
|
||||
.getColoredRectangles()
|
||||
.add(new ColoredRectangle(textBBox, color, LINE_WIDTH)));
|
||||
.get(page.getNumber() - 1).getColoredRectangles().add(new ColoredRectangle(textBBox, color, LINE_WIDTH)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.utils;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class CoordinateTransforms {
|
||||
|
||||
public AffineTransform calculateImageCoordsToInitialUserSpaceCoords(PageInformation pageInformation, double scalingFactor) {
|
||||
|
||||
AffineTransform imageToCropBoxScaling = new AffineTransform(scalingFactor, 0, 0, scalingFactor, -pageInformation.minX(), -pageInformation.minY());
|
||||
|
||||
AffineTransform mirrorMatrix = new AffineTransform(1, 0, 0, -1, 0, pageInformation.height());
|
||||
|
||||
AffineTransform rotationMatrix = switch (pageInformation.rotationDegrees()) {
|
||||
case 90 -> new AffineTransform(0, 1, -1, 0, pageInformation.height(), 0);
|
||||
case 180 -> new AffineTransform(-1, 0, 0, -1, pageInformation.width(), pageInformation.height());
|
||||
case 270 -> new AffineTransform(0, -1, 1, 0, pageInformation.width() - pageInformation.height(), pageInformation.height()); // results from 90 + 180 rotations
|
||||
default -> new AffineTransform();
|
||||
};
|
||||
|
||||
// matrix multiplication is performed from right to left, so the order is reversed.
|
||||
// scaling -> mirror -> rotation
|
||||
AffineTransform resultMatrix = new AffineTransform();
|
||||
|
||||
resultMatrix.concatenate(rotationMatrix);
|
||||
resultMatrix.concatenate(mirrorMatrix);
|
||||
resultMatrix.concatenate(imageToCropBoxScaling);
|
||||
return resultMatrix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public AffineTransform calculateInitialUserSpaceCoordsToImageCoords(PageInformation pageInformation, double scalingFactor) {
|
||||
|
||||
return calculateImageCoordsToInitialUserSpaceCoords(pageInformation, scalingFactor).createInverse();
|
||||
}
|
||||
|
||||
|
||||
public double calculateScalingFactor(PageInformation pageInformation, double imageWidth) {
|
||||
|
||||
// PDFBox always returns page height and width based on rotation
|
||||
double pageWidth;
|
||||
if (pageInformation.rotationDegrees() == 90 || pageInformation.rotationDegrees() == 270) {
|
||||
pageWidth = pageInformation.height();
|
||||
} else {
|
||||
pageWidth = pageInformation.width();
|
||||
}
|
||||
|
||||
return pageWidth / imageWidth;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.utils;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
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 {
|
||||
@@ -58,7 +58,7 @@ public class GeometricComparators {
|
||||
return cell1Size.compareTo(cell2Size);
|
||||
};
|
||||
|
||||
public static final Comparator<Rectangle> RECTANGLE_SIZE_COMPARATOR = (rect1, rect2) -> {
|
||||
public static final Comparator<Rectangle2D> RECTANGLE_SIZE_COMPARATOR = (rect1, rect2) -> {
|
||||
|
||||
Double rect1Size = rect1.getHeight() * rect1.getWidth();
|
||||
Double rect2Size = rect2.getHeight() * rect2.getWidth();
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class HeaderFooterDetection {
|
||||
|
||||
private final Map<Integer, ClassificationPage> pagesCache = new HashMap<>();
|
||||
private static final double THRESHOLD = 0.5;
|
||||
// Weight will go from 1.0 to 0.5 because the first element is the most likely to be the header on the page.
|
||||
private static final double[] headerWeights = {1.0, 0.75, 0.5};
|
||||
// Weight will go from 0.5 to 1.0 because the last element is the most likely to be the footer on the page.
|
||||
private static final double[] footerWeights = {0.5, 0.75, 1.0};
|
||||
|
||||
|
||||
public boolean isLikelyFooter(TextPageBlock textPageBlock, ClassificationDocument document, ClassificationPage classificationPage) {
|
||||
|
||||
int numberOfPages = document.getPages().size();
|
||||
if (numberOfPages < 3) {
|
||||
// If the document has 1 or 2 pages this may lead to more false positives than actual findings.
|
||||
return false;
|
||||
}
|
||||
|
||||
int window = Math.min(numberOfPages, 8);
|
||||
|
||||
List<ClassificationPage> nearestPages = findNearestPages(classificationPage, document.getPages(), window);
|
||||
List<List<AbstractPageBlock>> footerCandidates = getFooterCandidates(nearestPages);
|
||||
|
||||
return detectHeadersOrFootersByPageAssociation(textPageBlock.getText(), footerCandidates, window, footerWeights);
|
||||
}
|
||||
|
||||
|
||||
public boolean isLikelyHeader(TextPageBlock textPageBlock, ClassificationDocument document, ClassificationPage classificationPage) {
|
||||
|
||||
int numberOfPages = document.getPages().size();
|
||||
if (numberOfPages < 3) {
|
||||
// If the document has 1 or 2 pages this may lead to more false positives than actual findings.
|
||||
return false;
|
||||
}
|
||||
|
||||
int window = Math.min(numberOfPages, 8);
|
||||
|
||||
List<ClassificationPage> nearestPages = findNearestPages(classificationPage, document.getPages(), window);
|
||||
List<List<AbstractPageBlock>> headerCandidates = getHeaderCandidates(nearestPages);
|
||||
|
||||
return detectHeadersOrFootersByPageAssociation(textPageBlock.getText(), headerCandidates, window, headerWeights);
|
||||
}
|
||||
|
||||
|
||||
private boolean detectHeadersOrFootersByPageAssociation(String testString, List<List<AbstractPageBlock>> candidates, int window, double[] weights) {
|
||||
|
||||
double highestScore = 0.0;
|
||||
|
||||
for (int i = 0; i < candidates.size(); i++) {
|
||||
List<List<String>> candidateStrings = new ArrayList<>();
|
||||
for (int k = Math.max(i - window, 0); k < Math.min(i + window, candidates.size()); k++) {
|
||||
candidateStrings.add(candidates.get(k)
|
||||
.stream()
|
||||
.map(AbstractPageBlock::getText)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
int maxLen = candidateStrings.stream()
|
||||
.mapToInt(List::size)
|
||||
.max()
|
||||
.orElse(0);
|
||||
for (List<String> sublist : candidateStrings) {
|
||||
while (sublist.size() < maxLen) {
|
||||
sublist.add(0, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Compare the testString against each candidate in the window
|
||||
for (int j = 0; j < maxLen; j++) {
|
||||
double score = 0.0;
|
||||
int finalJ = j;
|
||||
List<String> paddedCandidateStrings = candidateStrings.stream()
|
||||
.map(sublist -> sublist.size() > finalJ ? sublist.get(finalJ) : "")
|
||||
.toList();
|
||||
for (String paddedString : paddedCandidateStrings) {
|
||||
if ((testString.length() >= 5 && paddedString.length() >= 5) && (testString.length() > 2 * paddedString.length()
|
||||
|| paddedString.length() > 2 * testString.length())) {
|
||||
// If both strings are at least 5 characters long and one string is more than twice as long as the other,
|
||||
// skip the distance calculation as it's time-consuming, and we can assume they are not similar enough
|
||||
continue;
|
||||
}
|
||||
|
||||
int distance = calculateHammingDistanceWithPreprocessing(testString, paddedString);
|
||||
double normalizedScore = 1 - (double) distance / Math.max(testString.length(), paddedString.length());
|
||||
score += normalizedScore * (j < weights.length ? weights[j] : 1);
|
||||
}
|
||||
score /= paddedCandidateStrings.size();
|
||||
highestScore = Math.max(highestScore, score);
|
||||
// Early stop
|
||||
if (highestScore > THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the nearest n pages for a given page.
|
||||
* For example: nearest 8 pages for page 4 are: 1, 2, 3, 5, 6, 7, 8, 9.
|
||||
*
|
||||
* @param currentPage Current page to find the nearest ones.
|
||||
* @param allPages All pages in the document.
|
||||
* @param numNeighbors Number of neighbouring pages to find.
|
||||
* @return The nearest pages.
|
||||
*/
|
||||
private List<ClassificationPage> findNearestPages(ClassificationPage currentPage, List<ClassificationPage> allPages, int numNeighbors) {
|
||||
|
||||
int totalPages = allPages.size();
|
||||
List<ClassificationPage> nearestPages = new ArrayList<>();
|
||||
|
||||
int currentPageIndex = currentPage.getPageNumber() - 1;
|
||||
int halfWin = numNeighbors / 2;
|
||||
int start = Math.max(0, currentPageIndex - halfWin);
|
||||
int end = Math.min(totalPages - 1, currentPageIndex + halfWin);
|
||||
|
||||
for (int i = start; i <= end; i++) {
|
||||
if (i != currentPageIndex) {
|
||||
nearestPages.add(pagesCache.computeIfAbsent(i, allPages::get));
|
||||
}
|
||||
}
|
||||
|
||||
pagesCache.keySet().removeIf(key -> key < start || key > end);
|
||||
|
||||
return nearestPages;
|
||||
}
|
||||
|
||||
|
||||
// Get the last 3 TextBlocks on the page as they are likely to be a footer
|
||||
private List<List<AbstractPageBlock>> getFooterCandidates(List<ClassificationPage> pages) {
|
||||
|
||||
List<List<AbstractPageBlock>> footerCandidates = new ArrayList<>();
|
||||
for (ClassificationPage page : pages) {
|
||||
List<AbstractPageBlock> textBlocks = page.getTextBlocks();
|
||||
List<TextPageBlock> textPageBlocks = textBlocks.stream()
|
||||
.filter(textBlock -> textBlock instanceof TextPageBlock)
|
||||
.map(textBlock -> (TextPageBlock) textBlock)
|
||||
.toList();
|
||||
int blockCount = textPageBlocks.size();
|
||||
if (blockCount > 0) {
|
||||
int start = Math.max(0, blockCount - 3);
|
||||
footerCandidates.add(new ArrayList<>(textPageBlocks.subList(start, blockCount)));
|
||||
}
|
||||
}
|
||||
return footerCandidates;
|
||||
}
|
||||
|
||||
|
||||
// Get the first 3 TextBlocks on the page as they are likely to be a header
|
||||
private List<List<AbstractPageBlock>> getHeaderCandidates(List<ClassificationPage> pages) {
|
||||
|
||||
List<List<AbstractPageBlock>> headerCandidates = new ArrayList<>();
|
||||
for (ClassificationPage page : pages) {
|
||||
List<AbstractPageBlock> textBlocks = page.getTextBlocks();
|
||||
List<TextPageBlock> textPageBlocks = textBlocks.stream()
|
||||
.filter(textBlock -> textBlock instanceof TextPageBlock)
|
||||
.map(textBlock -> (TextPageBlock) textBlock)
|
||||
.toList();
|
||||
int count = Math.min(3, textPageBlocks.size());
|
||||
headerCandidates.add(new ArrayList<>(textPageBlocks.subList(0, count)));
|
||||
}
|
||||
return headerCandidates;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the Hamming distance between two strings after preprocessing to make them the same length
|
||||
* and replacing all digits with a special character '@' since they are a common occurrence in headers/footers.
|
||||
*
|
||||
* @param firstCandidate First string
|
||||
* @param secondCandidate Second string
|
||||
* @return The Hamming distance between the two preprocessed strings.
|
||||
*/
|
||||
private int calculateHammingDistanceWithPreprocessing(String firstCandidate, String secondCandidate) {
|
||||
|
||||
int maxLength = Math.max(firstCandidate.length(), secondCandidate.length());
|
||||
|
||||
String cleanFirstCandidate = padString(firstCandidate, maxLength, '\0').replaceAll("\\d", "@");
|
||||
String cleanSecondCandidate = padString(secondCandidate, maxLength, '\0').replaceAll("\\d", "@");
|
||||
|
||||
int distance = 0;
|
||||
for (int i = 0; i < maxLength; i++) {
|
||||
if (cleanFirstCandidate.charAt(i) != cleanSecondCandidate.charAt(i)) {
|
||||
distance++;
|
||||
}
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
|
||||
|
||||
private String padString(String input, int length, char padChar) {
|
||||
|
||||
if (input.length() >= length) {
|
||||
return input;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(input);
|
||||
|
||||
while (sb.length() < length) {
|
||||
sb.append(padChar);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
+80
-14
@@ -1,12 +1,5 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.utils;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -14,12 +7,23 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class MarkedContentUtils {
|
||||
|
||||
public static final String HEADER = "Header";
|
||||
public static final String FOOTER = "Footer";
|
||||
|
||||
|
||||
public List<Rectangle2D> getMarkedContentBboxPerLine(List<PDMarkedContent> markedContents, String subtype) {
|
||||
|
||||
if (markedContents == null) {
|
||||
@@ -31,7 +35,8 @@ public class MarkedContentUtils {
|
||||
.filter(m -> m.getProperties() != null)
|
||||
.filter(m -> m.getProperties().getItem("Subtype") != null)
|
||||
.filter(m -> ((COSName) m.getProperties().getItem("Subtype")).getName().equals(subtype))
|
||||
.map(PDMarkedContent::getContents).flatMap(Collection::stream)
|
||||
.map(PDMarkedContent::getContents)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(t -> t instanceof TextPosition)
|
||||
.map(t -> (TextPosition) t)
|
||||
.filter(t -> !t.getUnicode().equals(" "))
|
||||
@@ -41,16 +46,77 @@ public class MarkedContentUtils {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return markedContentByYPosition.values().stream()
|
||||
.map(textPositions -> new TextPositionSequence(textPositions.stream()
|
||||
.toList(), 0, true)
|
||||
.getRectangle())
|
||||
.map(t -> new Rectangle2D.Float(t.getTopLeft().getX(), t.getTopLeft().getY() - Math.abs(t.getHeight()), t.getWidth(), Math.abs(t.getHeight()))).collect(Collectors.toList());
|
||||
return markedContentByYPosition.values()
|
||||
.stream()
|
||||
.map(textPositions -> new TextPositionSequence(textPositions, 0, true).getBBoxInitialUserSpace())
|
||||
.map(t -> new Rectangle2D.Double(t.getX(), t.getY() - Math.abs(t.getHeight()), t.getWidth(), Math.abs(t.getHeight())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
public List<MarkedContentPosition> getMarkedContentPositions(List<PDMarkedContent> markedContents) {
|
||||
|
||||
if (markedContents == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return markedContents.stream()
|
||||
.filter(m -> !m.getContents().isEmpty())
|
||||
.map(MarkedContentPosition::fromPDMarkedContent)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
public boolean intersects(TextPageBlock textBlock, Map<String, List<Rectangle2D>> markedContentBboxPerType, String type) {
|
||||
return markedContentBboxPerType.get(type) != null && markedContentBboxPerType.get(type).stream().anyMatch(rectangle -> rectangle.intersects(textBlock.getPdfMinX(), textBlock.getPdfMinY(), textBlock.getWidth(), textBlock.getHeight()));
|
||||
|
||||
return markedContentBboxPerType.get(type) != null && markedContentBboxPerType.get(type)
|
||||
.stream()
|
||||
.anyMatch(rectangle -> rectangle.intersects(textBlock.getPdfMinX(), textBlock.getPdfMinY(), textBlock.getWidth(), textBlock.getHeight()));
|
||||
}
|
||||
|
||||
|
||||
public record MarkedContentPosition(String type, String subType, List<Rectangle2D> textPositions) {
|
||||
|
||||
public static MarkedContentPosition fromPDMarkedContent(PDMarkedContent markedContent) {
|
||||
|
||||
return new MarkedContentPosition(markedContent.getTag(), parseSubType(markedContent), parseTextPositions(markedContent.getContents()));
|
||||
}
|
||||
|
||||
|
||||
private static List<Rectangle2D> parseTextPositions(List<Object> contents) {
|
||||
|
||||
return contents.stream()
|
||||
.filter(content -> content instanceof TextPosition)
|
||||
.map(content -> (TextPosition) content)
|
||||
.filter(content -> !content.getUnicode().equals(" "))
|
||||
.map(textPositions -> new TextPositionSequence(List.of(textPositions), 0, true))
|
||||
.map(BoundingBox::getBBoxInitialUserSpace)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
private static String parseSubType(PDMarkedContent markedContent) {
|
||||
|
||||
if (markedContent == null || markedContent.getProperties() == null || markedContent.getProperties().getItem("Subtype") == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((COSName) markedContent.getProperties().getItem("Subtype")).getName();
|
||||
}
|
||||
|
||||
|
||||
public String formattedType() {
|
||||
|
||||
if (subType == null || subType.isEmpty()) {
|
||||
return type;
|
||||
}
|
||||
if (type.equals("Artifact")) {
|
||||
return subType;
|
||||
}
|
||||
return String.format("%s-%s", type, subType);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.utils;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
|
||||
public record PageInformation(Rectangle2D mediabox, int number, int rotationDegrees) {
|
||||
|
||||
public static PageInformation fromPDPage(int pageNum, PDPage page) {
|
||||
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
return new PageInformation(new Rectangle2D.Double(mediaBox.getLowerLeftX(), mediaBox.getLowerLeftY(), mediaBox.getWidth(), mediaBox.getHeight()),
|
||||
pageNum,
|
||||
page.getRotation());
|
||||
}
|
||||
|
||||
|
||||
public double height() {
|
||||
|
||||
return mediabox.getHeight();
|
||||
}
|
||||
|
||||
|
||||
public double heightRot() {
|
||||
|
||||
if (rotationDegrees == 90 || rotationDegrees == 270) {
|
||||
return width();
|
||||
}
|
||||
return height();
|
||||
}
|
||||
|
||||
|
||||
public double width() {
|
||||
|
||||
return mediabox.getWidth();
|
||||
}
|
||||
|
||||
|
||||
public double minX() {
|
||||
|
||||
return mediabox.getX();
|
||||
}
|
||||
|
||||
|
||||
public double minY() {
|
||||
|
||||
return mediabox.getY();
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -112,8 +112,8 @@ public class PdfVisualisationUtility {
|
||||
case DOCUMENT -> Color.LIGHT_GRAY;
|
||||
case HEADER, FOOTER -> Color.GREEN;
|
||||
case PARAGRAPH -> Color.BLUE;
|
||||
case SUPER_SECTION, SECTION -> Color.BLACK;
|
||||
case HEADLINE -> Color.RED;
|
||||
case SECTION -> Color.BLACK;
|
||||
case TABLE -> Color.ORANGE;
|
||||
case TABLE_CELL -> Color.GRAY;
|
||||
case IMAGE -> Color.MAGENTA;
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ public final class PositionUtils {
|
||||
}
|
||||
|
||||
|
||||
public Float getApproxLineCount(TextPageBlock textBlock) {
|
||||
public double getApproxLineCount(TextPageBlock textBlock) {
|
||||
|
||||
return textBlock.getHeight() / textBlock.getMostPopularWordHeight();
|
||||
}
|
||||
|
||||
+49
-15
@@ -2,8 +2,10 @@ package com.knecon.fforesight.service.layoutparser.processor.utils;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RectangularShape;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -19,6 +21,8 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -48,7 +52,10 @@ public class RectangleTransformations {
|
||||
|
||||
public static Rectangle2D bBoxUnionAtomicTextBlock(List<AtomicTextBlock> atomicTextBlocks) {
|
||||
|
||||
return atomicTextBlocks.stream().flatMap(atomicTextBlock -> atomicTextBlock.getPositions().stream()).collect(new Rectangle2DBBoxCollector());
|
||||
return atomicTextBlocks.stream()
|
||||
.flatMap(atomicTextBlock -> atomicTextBlock.getPositions()
|
||||
.stream())
|
||||
.collect(new Rectangle2DBBoxCollector());
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +80,10 @@ public class RectangleTransformations {
|
||||
|
||||
public static Rectangle2D atomicTextBlockBBox(List<AtomicTextBlock> atomicTextBlocks) {
|
||||
|
||||
return atomicTextBlocks.stream().flatMap(atomicTextBlock -> atomicTextBlock.getPositions().stream()).collect(new Rectangle2DBBoxCollector());
|
||||
return atomicTextBlocks.stream()
|
||||
.flatMap(atomicTextBlock -> atomicTextBlock.getPositions()
|
||||
.stream())
|
||||
.collect(new Rectangle2DBBoxCollector());
|
||||
}
|
||||
|
||||
|
||||
@@ -85,16 +95,18 @@ public class RectangleTransformations {
|
||||
|
||||
public static Rectangle2D rectangleBBox(List<Rectangle> rectangles) {
|
||||
|
||||
return rectangles.stream().map(RectangleTransformations::toRectangle2D).collect(new Rectangle2DBBoxCollector());
|
||||
return rectangles.stream()
|
||||
.map(RectangleTransformations::toRectangle2D)
|
||||
.collect(new Rectangle2DBBoxCollector());
|
||||
}
|
||||
|
||||
|
||||
public static Rectangle2D toRectangle2D(Rectangle redactionLogRectangle) {
|
||||
|
||||
return new Rectangle2D.Double(redactionLogRectangle.getTopLeft().getX(),
|
||||
redactionLogRectangle.getTopLeft().getY() + redactionLogRectangle.getHeight(),
|
||||
redactionLogRectangle.getWidth(),
|
||||
-redactionLogRectangle.getHeight());
|
||||
redactionLogRectangle.getTopLeft().getY() + redactionLogRectangle.getHeight(),
|
||||
redactionLogRectangle.getWidth(),
|
||||
-redactionLogRectangle.getHeight());
|
||||
}
|
||||
|
||||
|
||||
@@ -107,15 +119,16 @@ public class RectangleTransformations {
|
||||
public static Rectangle toRedactionLogRectangle(Rectangle2D rectangle2D, int pageNumber) {
|
||||
|
||||
return new Rectangle(new Point((float) rectangle2D.getMinX(), (float) (rectangle2D.getMinY() + rectangle2D.getHeight())),
|
||||
(float) rectangle2D.getWidth(),
|
||||
-(float) rectangle2D.getHeight(),
|
||||
pageNumber);
|
||||
(float) rectangle2D.getWidth(),
|
||||
-(float) rectangle2D.getHeight(),
|
||||
pageNumber);
|
||||
}
|
||||
|
||||
|
||||
public static Rectangle2D rectangle2DBBox(List<Rectangle2D> rectangle2DList) {
|
||||
|
||||
return rectangle2DList.stream().collect(new Rectangle2DBBoxCollector());
|
||||
return rectangle2DList.stream()
|
||||
.collect(new Rectangle2DBBoxCollector());
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +143,8 @@ public class RectangleTransformations {
|
||||
if (rectangle2DList.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
double splitThreshold = rectangle2DList.stream().mapToDouble(RectangularShape::getWidth).average().orElse(5) * 5.0;
|
||||
double splitThreshold = rectangle2DList.stream()
|
||||
.mapToDouble(RectangularShape::getWidth).average().orElse(5) * 5.0;
|
||||
|
||||
List<List<Rectangle2D>> rectangleListsWithGaps = new LinkedList<>();
|
||||
List<Rectangle2D> rectangleListWithoutGaps = new LinkedList<>();
|
||||
@@ -147,7 +161,27 @@ public class RectangleTransformations {
|
||||
previousRectangle = currentRectangle;
|
||||
}
|
||||
}
|
||||
return rectangleListsWithGaps.stream().map(RectangleTransformations::rectangle2DBBox).toList();
|
||||
return rectangleListsWithGaps.stream()
|
||||
.map(RectangleTransformations::rectangle2DBBox)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
public static CleanRulings extractRulings(List<? extends Rectangle2D.Float> rectangles) {
|
||||
|
||||
// Underlined or strikethrough are also in rulings but we dont want to split blocks with them so we use cells.
|
||||
List<Ruling> horizontalRulings = new ArrayList<>();
|
||||
List<Ruling> verticalRulings = new ArrayList<>();
|
||||
|
||||
rectangles.forEach(rectangle -> {
|
||||
horizontalRulings.add(new Ruling(new Point2D.Float(rectangle.x, rectangle.y), new Point2D.Float(rectangle.x + rectangle.width, rectangle.y)));
|
||||
horizontalRulings.add(new Ruling(new Point2D.Float(rectangle.x, rectangle.y + rectangle.height),
|
||||
new Point2D.Float(rectangle.x + rectangle.width, rectangle.y + rectangle.height)));
|
||||
verticalRulings.add(new Ruling(new Point2D.Float(rectangle.x, rectangle.y), new Point2D.Float(rectangle.x, rectangle.y + rectangle.height)));
|
||||
verticalRulings.add(new Ruling(new Point2D.Float(rectangle.x + rectangle.width, rectangle.y),
|
||||
new Point2D.Float(rectangle.x + rectangle.width, rectangle.y + rectangle.height)));
|
||||
});
|
||||
return new CleanRulings(verticalRulings, horizontalRulings);
|
||||
}
|
||||
|
||||
|
||||
@@ -171,9 +205,9 @@ public class RectangleTransformations {
|
||||
public BinaryOperator<BBox> combiner() {
|
||||
|
||||
return (b1, b2) -> new BBox(Math.min(b1.lowerLeftX, b2.lowerLeftX),
|
||||
Math.min(b1.lowerLeftY, b2.lowerLeftY),
|
||||
Math.max(b1.upperRightX, b2.upperRightX),
|
||||
Math.max(b1.upperRightY, b2.upperRightY));
|
||||
Math.min(b1.lowerLeftY, b2.lowerLeftY),
|
||||
Math.max(b1.upperRightX, b2.upperRightX),
|
||||
Math.max(b1.upperRightY, b2.upperRightY));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+20
-14
@@ -14,23 +14,24 @@ public class RectangularIntersectionFinder {
|
||||
|
||||
public static List<Rectangle2D> find(List<Ruling> horizontalRulingLines, List<Ruling> 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;
|
||||
}
|
||||
}
|
||||
// // 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<Rectangle2D> foundRectangles = new ArrayList<>();
|
||||
Map<Point2D, Ruling[]> intersectionPoints = Ruling.findIntersections(horizontalRulingLines, verticalRulingLines);
|
||||
Map<Point2D, RulingIntersectionFinder.IntersectingRulings> intersectionPoints = RulingIntersectionFinder.findNaive(horizontalRulingLines, verticalRulingLines);
|
||||
|
||||
List<Point2D> 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);
|
||||
RulingIntersectionFinder.IntersectingRulings intersectingRulingsFromTopLeft = intersectionPoints.get(topLeft);
|
||||
|
||||
// CrossingPointsDirectlyBelow( topLeft );
|
||||
List<Point2D> xPoints = new ArrayList<>();
|
||||
@@ -48,19 +49,24 @@ public class RectangularIntersectionFinder {
|
||||
outer:
|
||||
for (Point2D xPoint : xPoints) {
|
||||
// is there a vertical edge b/w topLeft and xPoint?
|
||||
if (!hv[1].equals(intersectionPoints.get(xPoint)[1])) {
|
||||
if (!intersectingRulingsFromTopLeft.vertical().equals(intersectionPoints.get(xPoint).vertical())) {
|
||||
continue;
|
||||
}
|
||||
for (Point2D yPoint : yPoints) {
|
||||
// is there a horizontal edge b/w topLeft and yPoint ?
|
||||
if (!hv[0].equals(intersectionPoints.get(yPoint)[0])) {
|
||||
if (!intersectingRulingsFromTopLeft.horizontal().equals(intersectionPoints.get(yPoint).horizontal())) {
|
||||
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])) {
|
||||
&& intersectionPoints.get(btmRight).horizontal().equals(intersectionPoints.get(xPoint).horizontal())
|
||||
&& intersectionPoints.get(btmRight).vertical().equals(intersectionPoints.get(yPoint).vertical())) {
|
||||
|
||||
foundRectangles.add(new Rectangle2D.Double(topLeft.getX(), topLeft.getY(), btmRight.getX() - topLeft.getX(), btmRight.getY() - topLeft.getY()));
|
||||
intersectionPoints.get(topLeft).horizontal().setClassification(Ruling.Classification.TABLE_LINE);
|
||||
intersectionPoints.get(topLeft).vertical().setClassification(Ruling.Classification.TABLE_LINE);
|
||||
intersectionPoints.get(btmRight).horizontal().setClassification(Ruling.Classification.TABLE_LINE);
|
||||
intersectionPoints.get(btmRight).vertical().setClassification(Ruling.Classification.TABLE_LINE);
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.utils;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class RulingIntersectionFinder {
|
||||
|
||||
public static final int PERPENDICULAR_UNIT_EXPAND_AMOUNT = 2;
|
||||
|
||||
public static final Comparator<Point2D> Y_THEN_X_POINT_COMPARATOR = Comparator.comparingDouble(Point2D::getY).thenComparing(Point2D::getX);
|
||||
|
||||
|
||||
/**
|
||||
* Implementation to find line intersection in O(P + n log n), where n is the number of lines and P the numer of intersections.
|
||||
* based on <a href="http://people.csail.mit.edu/indyk/6.838-old/handouts/lec2.pdf">Segment Intersection by Piotr Indyk</a>
|
||||
*
|
||||
* @param horizontals a list of non-overlapping horizontal rulings
|
||||
* @param verticals a list of non-overlapping vertical rulings
|
||||
* @return a Map of each found intersection point pointing to the two lines forming the intersection.
|
||||
*/
|
||||
/*
|
||||
* The algorithm assumes there are only horizontal and vertical lines which are unique in their coordinates. (E.g. no overlapping horizontal lines exist)
|
||||
* As a high level overview, the algorithm uses a sweep line advancing from left to right.
|
||||
* It dynamically updates the horizontal rulings which are intersected by the current sweep line.
|
||||
* When the sweep line hits a vertical line, it then checks for all intersections with the currently intersected horizontal rulings.
|
||||
* THe trick of the algorithm is using a binary search tree to store the currently intersected horizontal rulings. This way the lookup should be in O(log n).
|
||||
* This way the initial sorting step has the highest complexity class (O(n log n) and thus determines the complexity class of the entire algorithm
|
||||
* Unfortunately, the implementation here takes a few liberties compared to the original algorithm. The binary search tree is replaced by an ordered Set which is simply looped over.
|
||||
* Therefore, this implementation's worst case, where all horizontal lines span the entire sweep, you are essentially performing the naive approach with a bunch of overhead.
|
||||
* Since we are using this implementation to find table cells, one can expect this worst case to always be the case.
|
||||
* A simple runtime comparison for a single page with the most lines we can expect (SinglePages/AbsolutelyEnormousTable.pdf with 30 horizontals and 144 verticals) shows this implementation takes roughly 14 ms, whereas the naive approach takes 7 ms. Both are negligible, but the naive approach is two times as fast.
|
||||
* If we would like to make this faster, we would need a better data structure for 'TreeMap<Ruling, Void> horizontalRulingsInCurrentSweep', where we can query the TreeMap for all horizontal rulings in a given interval in O(log n).
|
||||
*/
|
||||
public Map<Point2D, IntersectingRulings> find(List<Ruling> horizontals, List<Ruling> verticals) {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
List<SweepStep> sweepTrajectory = buildSweepTrajectory(horizontals, verticals);
|
||||
|
||||
TreeMap<Ruling, Void> horizontalRulingsInCurrentSweep = new TreeMap<>(Comparator.comparingDouble(Ruling::getTop));
|
||||
|
||||
TreeMap<Point2D, IntersectingRulings> intersections = new TreeMap<>(Y_THEN_X_POINT_COMPARATOR);
|
||||
|
||||
for (SweepStep step : sweepTrajectory) {
|
||||
switch (step.type) {
|
||||
case VERTICAL: // check for intersections with currently intersected horizontal lines
|
||||
for (Ruling horizontalRuling : horizontalRulingsInCurrentSweep.navigableKeySet()) {
|
||||
|
||||
Optional<Point2D> intersectionPoint = findIntersectionPoint(horizontalRuling, step.ruling);
|
||||
|
||||
if (intersectionPoint.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
intersections.put(intersectionPoint.get(), new IntersectingRulings(horizontalRuling, step.ruling));
|
||||
}
|
||||
break;
|
||||
case HORIZONTAL_ENTRY: // sweep line now intersects this horizontal ruling
|
||||
horizontalRulingsInCurrentSweep.put(step.ruling, null);
|
||||
break;
|
||||
case HORIZONTAL_EXIT: // sweep line no longer intersects this horizontal ruling
|
||||
horizontalRulingsInCurrentSweep.remove(step.ruling);
|
||||
break;
|
||||
}
|
||||
}
|
||||
log.debug("Finished building intersections with line sweep in {} ms", System.currentTimeMillis() - start);
|
||||
|
||||
return intersections;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Naive Approach in O(n^2) of finding intersections between lines by iterating over all lines.
|
||||
*
|
||||
* @param horizontals a list of non-overlapping horizontal rulings
|
||||
* @param verticals a list of non-overlapping vertical rulings
|
||||
* @return a Map of each found intersection point pointing to the two lines forming the intersection.
|
||||
*/
|
||||
public Map<Point2D, IntersectingRulings> findNaive(List<Ruling> horizontals, List<Ruling> verticals) {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
TreeMap<Point2D, IntersectingRulings> intersections = new TreeMap<>(Y_THEN_X_POINT_COMPARATOR);
|
||||
|
||||
for (Ruling horizontal : horizontals) {
|
||||
for (Ruling vertical : verticals) {
|
||||
Optional<Point2D> intersectionPoint = findIntersectionPoint(horizontal, vertical);
|
||||
|
||||
if (intersectionPoint.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
intersections.put(intersectionPoint.get(), new IntersectingRulings(horizontal, vertical));
|
||||
}
|
||||
}
|
||||
log.debug("Finished building intersections naively in {} ms", System.currentTimeMillis() - start);
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
|
||||
private static List<SweepStep> buildSweepTrajectory(List<Ruling> horizontals, List<Ruling> verticals) {
|
||||
|
||||
List<SweepStep> sweepTrajectory = new LinkedList<>();
|
||||
|
||||
for (Ruling horizontalRuling : horizontals) {
|
||||
sweepTrajectory.add(new SweepStep(SweepStep.Type.HORIZONTAL_ENTRY, horizontalRuling.getLeft() - PERPENDICULAR_UNIT_EXPAND_AMOUNT, horizontalRuling));
|
||||
sweepTrajectory.add(new SweepStep(SweepStep.Type.HORIZONTAL_EXIT, horizontalRuling.getRight() + PERPENDICULAR_UNIT_EXPAND_AMOUNT, horizontalRuling));
|
||||
}
|
||||
|
||||
for (Ruling verticalRuling : verticals) {
|
||||
sweepTrajectory.add(new SweepStep(SweepStep.Type.VERTICAL, verticalRuling.getLeft(), verticalRuling));
|
||||
}
|
||||
|
||||
Collections.sort(sweepTrajectory);
|
||||
|
||||
return sweepTrajectory;
|
||||
}
|
||||
|
||||
|
||||
public Optional<Point2D> findIntersectionPoint(Ruling horizontal, Ruling vertical) {
|
||||
|
||||
if (!horizontal.isHorizontal() || !vertical.isVertical()) {
|
||||
log.warn("lines must be orthogonal, vertical and horizontal");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Ruling expanded_horizontal = horizontal.expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT);
|
||||
Ruling expanded_vertical = vertical.expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT);
|
||||
|
||||
if (!expanded_horizontal.intersectsLine(expanded_vertical)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new Point2D.Float(vertical.getLeft(), horizontal.getTop()));
|
||||
}
|
||||
|
||||
|
||||
private class SweepStep implements Comparable<SweepStep> {
|
||||
|
||||
protected Type type;
|
||||
protected float y_position;
|
||||
protected Ruling ruling;
|
||||
|
||||
private enum Type {
|
||||
VERTICAL,
|
||||
HORIZONTAL_EXIT,
|
||||
HORIZONTAL_ENTRY
|
||||
}
|
||||
|
||||
|
||||
SweepStep(Type type, float y_position, Ruling ruling) {
|
||||
|
||||
this.type = type;
|
||||
this.y_position = y_position;
|
||||
this.ruling = ruling;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int compareTo(SweepStep other) {
|
||||
|
||||
int rv;
|
||||
if (DoubleComparisons.feq(y_position, other.y_position)) {
|
||||
if (type == SweepStep.Type.VERTICAL && other.type == SweepStep.Type.HORIZONTAL_ENTRY) {
|
||||
rv = 1;
|
||||
} else if (type == SweepStep.Type.VERTICAL && other.type == SweepStep.Type.HORIZONTAL_EXIT) {
|
||||
rv = -1;
|
||||
} else if (type == SweepStep.Type.HORIZONTAL_ENTRY && other.type == SweepStep.Type.VERTICAL) {
|
||||
rv = -1;
|
||||
} else if (type == SweepStep.Type.HORIZONTAL_EXIT && other.type == SweepStep.Type.VERTICAL) {
|
||||
rv = 1;
|
||||
} else {
|
||||
rv = Double.compare(y_position, other.y_position);
|
||||
}
|
||||
} else {
|
||||
return Double.compare(y_position, other.y_position);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public record IntersectingRulings(Ruling horizontal, Ruling vertical) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+16
-6
@@ -4,6 +4,7 @@ import static com.knecon.fforesight.service.layoutparser.processor.utils.Geometr
|
||||
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.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -11,7 +12,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Rectangle;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.Cell;
|
||||
|
||||
public class SpreadsheetFinder {
|
||||
|
||||
@@ -19,15 +20,15 @@ public class SpreadsheetFinder {
|
||||
private static final float AREA_TOLERANCE = 0.001f;
|
||||
|
||||
|
||||
public static List<Rectangle> findSpreadsheetsFromCells(List<? extends Rectangle> cells) {
|
||||
public static List<Rectangle2D> findSpreadsheetsFromCells(List<Cell> cells) {
|
||||
// via: http://stackoverflow.com/questions/13746284/merging-multiple-adjacent-rectangles-into-one-polygon
|
||||
List<Rectangle> rectangles = new ArrayList<>();
|
||||
List<Rectangle2D> rectangles = new ArrayList<>();
|
||||
Set<Point2D> pointSet = new HashSet<>();
|
||||
Map<Point2D, Point2D> edgesH = new HashMap<>();
|
||||
Map<Point2D, Point2D> edgesV = new HashMap<>();
|
||||
|
||||
for (Rectangle cell : cells) {
|
||||
for (Point2D pt : cell.getPoints()) {
|
||||
for (Cell cell : cells) {
|
||||
for (Point2D pt : getPoints(cell.getBBoxInitialUserSpace())) {
|
||||
if (pointSet.contains(pt)) { // shared vertex, remove it
|
||||
pointSet.remove(pt);
|
||||
} else {
|
||||
@@ -116,13 +117,22 @@ public class SpreadsheetFinder {
|
||||
|
||||
// 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));
|
||||
rectangles.add(new Rectangle2D.Double(left - AREA_TOLERANCE, top - AREA_TOLERANCE, right - left + (2 * AREA_TOLERANCE), bottom - top + (2 * AREA_TOLERANCE)));
|
||||
}
|
||||
}
|
||||
return rectangles;
|
||||
}
|
||||
|
||||
|
||||
public static List<Point2D> getPoints(Rectangle2D rectangle2D) {
|
||||
|
||||
return List.of(new Point2D.Double(rectangle2D.getX(), rectangle2D.getY()),
|
||||
new Point2D.Double(rectangle2D.getMaxX(), rectangle2D.getY()),
|
||||
new Point2D.Double(rectangle2D.getMaxX(), rectangle2D.getMaxY()),
|
||||
new Point2D.Double(rectangle2D.getX(), rectangle2D.getMaxY()));
|
||||
}
|
||||
|
||||
|
||||
private enum Direction {
|
||||
HORIZONTAL,
|
||||
VERTICAL
|
||||
|
||||
+5
@@ -22,4 +22,9 @@ public class TextPositionOperations {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
public static List<TextPositionSequence> mergeTextPositionSequence(List<TextPageBlock> textBlocks) {
|
||||
|
||||
return textBlocks.stream().flatMap(tb -> tb.getSequences().stream()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user