From 7a529d5a88585c64dbb56f11b5d90d6edd7b5009 Mon Sep 17 00:00:00 2001 From: Kilian Schuettler Date: Fri, 17 Mar 2023 15:15:35 +0100 Subject: [PATCH] RED-6368: Include Tables and Images into Prototype Document Structure *added Tables And TableCell *added BoundingBox calculation function *added images to structure without restructuring them, some images are still added twice --- .../server/classification/model/Section.java | 19 +- .../classification/model/SectionText.java | 2 - .../v1/server/document/data/PageData.java | 3 - .../document/data/TableOfContentsData.java | 12 +- .../v1/server/document/graph/Boundary.java | 21 +- .../server/document/graph/DocumentGraph.java | 49 +- .../v1/server/document/graph/PageElement.java | 14 + .../document/graph/TableOfContents.java | 144 ++-- .../graph/factory/DocumentGraphFactory.java | 392 +++++++++++ .../factory/RectangleTransformations.java | 44 ++ .../SearchTextWithTextPositionFactory.java | 159 +++++ .../SearchTextWithTextPositionModel.java | 5 +- .../graph/factory/TextBlockFactory.java | 75 ++ .../graph/factory/TextPositionOperations.java | 22 + .../graph/nodes/DocumentGraphNode.java | 219 ------ .../document/graph/nodes/EntityNode.java | 54 +- .../document/graph/nodes/FooterNode.java | 64 ++ .../document/graph/nodes/HeaderNode.java | 62 ++ .../document/graph/nodes/HeadlineNode.java | 63 ++ .../document/graph/nodes/ImageNode.java | 88 +++ .../server/document/graph/nodes/NodeType.java | 23 + .../server/document/graph/nodes/PageNode.java | 79 +-- .../document/graph/nodes/ParagraphNode.java | 50 +- .../document/graph/nodes/SectionNode.java | 90 +-- .../document/graph/nodes/SemanticNode.java | 162 +++++ .../document/graph/nodes/TableCellNode.java | 74 +- .../document/graph/nodes/TableNode.java | 63 +- .../graph/textblock/AtomicTextBlock.java | 48 +- .../textblock/ConcatenatedTextBlock.java | 79 +-- .../document/graph/textblock/TextBlock.java | 38 +- .../graph/textblock/TextBlockCollector.java | 8 +- .../document/services/DocumentDataMapper.java | 28 +- .../services/DocumentGraphFactory.java | 304 --------- .../services/DocumentGraphMapper.java | 110 ++- .../services/EntityCreationService.java | 90 +++ .../services/EntityEnrichmentService.java | 93 +++ .../services/EntityEnrichmentUtility.java | 31 - .../document/services/ImageSortService.java | 153 +++++ .../document/services/RangeComparators.java | 38 -- .../server/parsing/PDFLinesTextStripper.java | 55 +- .../v1/server/redaction/model/Paragraph.java | 2 + .../v1/server/redaction/model/PdfImage.java | 2 +- .../SearchTextWithTextPositionFactory.java | 90 --- .../service/SurroundingWordsService.java | 10 +- .../redaction/utils/SearchImplementation.java | 8 +- .../segmentation/SectionsBuilderService.java | 84 +-- .../model/AbstractTextContainer.java | 6 + .../server/AbstractTestWithDictionaries.java | 10 +- .../server/DocumentGraphIntegrationTest.java | 130 ++-- .../graph}/DocumentGraphMappingTest.java | 6 +- .../document/graph/DocumentGraphTest.java | 308 +++++++++ .../graph/DocumentGraphVisualizationTest.java | 59 ++ .../document/graph/ImageSortServiceTest.java | 60 ++ .../redaction/v1/server/utils/PdfDraw.java | 136 +++- ...RAR_01_Volume_1_2018-09-06.IMAGE_INFO.json | 638 ++++++++++++++++++ .../resources/files/empty_image_response.json | 5 + 56 files changed, 3336 insertions(+), 1345 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/PageElement.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/DocumentGraphFactory.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/RectangleTransformations.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/SearchTextWithTextPositionFactory.java rename redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/{redaction/model => document/graph/factory}/SearchTextWithTextPositionModel.java (72%) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/TextBlockFactory.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/TextPositionOperations.java delete mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/DocumentGraphNode.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/FooterNode.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/HeaderNode.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/HeadlineNode.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/ImageNode.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/SemanticNode.java delete mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentGraphFactory.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityCreationService.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityEnrichmentService.java delete mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityEnrichmentUtility.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/ImageSortService.java delete mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/RangeComparators.java delete mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SearchTextWithTextPositionFactory.java rename redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/{ => document/graph}/DocumentGraphMappingTest.java (92%) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphTest.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphVisualizationTest.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/ImageSortServiceTest.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06.IMAGE_INFO.json create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/resources/files/empty_image_response.json diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Section.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Section.java index dd7ba8d6..5b373623 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Section.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Section.java @@ -1,23 +1,16 @@ package com.iqser.red.service.redaction.v1.server.classification.model; -import com.iqser.red.service.persistence.service.v1.api.model.annotations.AnnotationStatus; -import com.iqser.red.service.persistence.service.v1.api.model.annotations.entitymapped.IdRemoval; -import com.iqser.red.service.persistence.service.v1.api.model.annotations.entitymapped.ManualImageRecategorization; -import com.iqser.red.service.redaction.v1.server.redaction.model.Entities; -import com.iqser.red.service.redaction.v1.server.redaction.model.Image; +import java.util.ArrayList; +import java.util.List; + import com.iqser.red.service.redaction.v1.server.redaction.model.PdfImage; import com.iqser.red.service.redaction.v1.server.redaction.model.SearchableText; -import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - @Data @NoArgsConstructor public class Section implements Comparable { @@ -68,10 +61,4 @@ public class Section implements Comparable { return 0; } - - - - - - } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/SectionText.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/SectionText.java index 08f8920b..82dde03b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/SectionText.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/SectionText.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionArea; import com.iqser.red.service.redaction.v1.server.redaction.model.CellValue; import com.iqser.red.service.redaction.v1.server.redaction.model.Image; -import com.iqser.red.service.redaction.v1.server.redaction.model.Paragraph; import com.iqser.red.service.redaction.v1.server.redaction.model.SearchableText; import lombok.AllArgsConstructor; @@ -39,7 +38,6 @@ public class SectionText { private boolean isTable; private String headline; - List paragraphs; @Builder.Default private List sectionAreas = new ArrayList<>(); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/data/PageData.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/data/PageData.java index a00747bb..d6ac1086 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/data/PageData.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/data/PageData.java @@ -14,7 +14,4 @@ public class PageData { int number; int height; int width; - - Long header; - Long footer; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/data/TableOfContentsData.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/data/TableOfContentsData.java index 17273696..fcfd97c9 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/data/TableOfContentsData.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/data/TableOfContentsData.java @@ -24,14 +24,14 @@ public class TableOfContentsData { List entries; - public EntryData get(String tocId) { + public EntryData get(List tocId) { - List ids = getIds(tocId); - if (ids.size() < 1) { + + if (tocId.size() < 1) { throw new InvalidKeyException(format("Section Identifier: \"%s\" is not valid.", tocId)); } - EntryData entry = entries.get(ids.get(0)); - for (int id : ids.subList(1, ids.size())) { + EntryData entry = entries.get(tocId.get(0)); + for (int id : tocId.subList(1, tocId.size())) { entry = entry.subEntries().get(id); } return entry; @@ -45,7 +45,7 @@ public class TableOfContentsData { @Builder - public record EntryData(String tocId, List subEntries, NodeType type, Long atomicTextBlock, Long page, int numberOnPage) { + public record EntryData(int[] tocId, List subEntries, NodeType type, Long[] atomicTextBlocks, Long[] pages, int numberOnPage) { } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/Boundary.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/Boundary.java index 01121463..3d14b3d6 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/Boundary.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/Boundary.java @@ -5,7 +5,7 @@ import static java.lang.String.format; import lombok.Setter; @Setter -public class Boundary { +public class Boundary implements Comparable { private int start; private int end; @@ -13,7 +13,9 @@ public class Boundary { public Boundary(int start, int end) { - assert start <= end; + if (start > end) { + throw new IllegalArgumentException(format("start: %d > end: %d", start, end)); + } this.start = start; this.end = end; } @@ -75,7 +77,7 @@ public class Boundary { public boolean intersects(Boundary boundary) { - return contains(boundary.start()) || contains(boundary.end()); + return contains(boundary.start()) || contains(boundary.end() - 1); } @@ -85,4 +87,17 @@ public class Boundary { return format("Boundary [%d|%d)", start, end); } + + @Override + public int compareTo(Boundary boundary) { + if (end < boundary.end() && start < boundary.start()) { + return -1; + } + if (start > boundary.start() && end > boundary.end()) { + return 1; + } + + return 0; + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraph.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraph.java index 93eca932..3f342cd4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraph.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraph.java @@ -1,22 +1,16 @@ package com.iqser.red.service.redaction.v1.server.document.graph; -import static com.iqser.red.service.redaction.v1.server.document.services.EntityEnrichmentUtility.enrichEntity; - import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.DocumentGraphNode; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.ConcatenatedTextBlock; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector; -import com.iqser.red.service.redaction.v1.server.exception.NotFoundException; -import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -32,56 +26,37 @@ import lombok.extern.slf4j.Slf4j; @FieldDefaults(level = AccessLevel.PRIVATE) public class DocumentGraph { - List sections; List pages; TableOfContents tableOfContents; Integer numberOfPages; - TextBlock text; + TextBlock textBlock; - public ConcatenatedTextBlock buildTextBlock() { + public TextBlock buildTextBlock() { - return streamAtomicTextBlocksInOrder().collect(new TextBlockCollector()); + return streamTextBlocksInOrder().collect(new TextBlockCollector()); } - public Stream streamAtomicTextBlocksInOrder() { + public List getMainSections() { - return Stream.concat(// - streamAllNodes().filter(DocumentGraphNode::isTerminal).map(DocumentGraphNode::getAtomicTextBlock),// - Stream.concat(// - pages.stream().map(PageNode::getHeader),// - pages.stream().map(PageNode::getFooter))); + return tableOfContents.entries.stream().filter(entry -> entry.node() instanceof SectionNode).map(entry -> (SectionNode) entry.node()).collect(Collectors.toList()); } - public EntityNode createAndAddEntity(Boundary boundary, String type, EntityType entityType) { + public Stream streamTextBlocksInOrder() { - EntityNode entity = EntityNode.initialEntityNode(boundary, type, entityType); - addEntityToGraphAndSetFields(entity); - return entity; - } - - - public void addEntityToGraphAndSetFields(EntityNode entity) { - - try { - boolean inserted = streamAllNodes().anyMatch(node -> node.addEntityAndSetFieldsIfStartIndexContained(entity)); - } catch (NotFoundException e) { - enrichEntity(entity, text); - log.warn("Entity \"{}\" with {} is in between two main sections and will be removed!", entity.getValue(), entity.getBoundary()); - entity.removeFromGraph(); - } + return streamAllNodes().filter(SemanticNode::isTerminal).map(SemanticNode::getTerminalTextBlock); } public Set getEntities() { - return streamAllNodes().filter(DocumentGraphNode::isTerminal).map(DocumentGraphNode::getEntities).flatMap(List::stream).collect(Collectors.toSet()); + return streamAllNodes().map(SemanticNode::getEntities).flatMap(Set::stream).collect(Collectors.toUnmodifiableSet()); } - private Stream streamAllNodes() { + private Stream streamAllNodes() { return tableOfContents.streamEntriesInOrder().map(TableOfContents.Entry::node); } @@ -90,7 +65,7 @@ public class DocumentGraph { @Override public String toString() { - return text.toString(); + return tableOfContents.toString(); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/PageElement.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/PageElement.java new file mode 100644 index 00000000..ef5ad795 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/PageElement.java @@ -0,0 +1,14 @@ +package com.iqser.red.service.redaction.v1.server.document.graph; + +import java.util.Map; + +import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D; + +import lombok.Data; + +@Data +public class PageElement { + + Map positionsPerPage; + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/TableOfContents.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/TableOfContents.java index ec37e77a..9913b04e 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/TableOfContents.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/TableOfContents.java @@ -3,17 +3,18 @@ package com.iqser.red.service.redaction.v1.server.document.graph; import static java.lang.String.format; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.stream.Stream; -import javax.management.openmbean.InvalidKeyException; - import com.google.common.hash.Hashing; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.DocumentGraphNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType; +import lombok.Builder; import lombok.Data; @Data @@ -28,37 +29,96 @@ public class TableOfContents { } - public String createNewEntryAndReturnId(NodeType nodeType, String summary, DocumentGraphNode node) { + public List createNewEntryAndReturnId(NodeType nodeType, SemanticNode node) { - String id = String.format("%d", entries.size()); - entries.add(new Entry(nodeType, id, summary, new LinkedList<>(), node)); - return id; + return createNewChildEntryAndReturnId(Collections.emptyList(), nodeType, node); } - public String createNewChildEntryAndReturnId(String parentId, NodeType nodeType, String summary, DocumentGraphNode node) { + public List createNewChildEntryAndReturnId(List parentId, NodeType nodeType, SemanticNode node) { - Entry parent = getEntryById(parentId); - String childId = parentId + String.format(".%d", parent.children().size()); - parent.children().add(new Entry(nodeType, childId, summary, new LinkedList<>(), node)); - return childId; - } - - - public Entry getEntryById(String parentId) { - - List ids = getIds(parentId); - if (ids.size() < 1) { - throw new InvalidKeyException(format("Section Identifier: \"%s\" is not valid.", parentId)); + List newId; + if (entryExists(parentId)) { + Entry parent = getEntryById(parentId); + newId = new LinkedList<>(parentId); + newId.add(parent.children().size()); + parent.children().add(Entry.builder().tocId(newId).node(node).type(nodeType).children(new LinkedList<>()).build()); + } else { + newId = List.of(entries.size()); + entries.add(Entry.builder().tocId(newId).node(node).type(nodeType).children(new LinkedList<>()).build()); } - Entry entry = entries.get(ids.get(0)); - for (int id : ids.subList(1, ids.size())) { + + return newId; + } + + + private boolean entryExists(List tocId) { + + if (tocId.size() < 1) { + return false; + } + Entry entry = entries.get(tocId.get(0)); + for (int id : tocId.subList(1, tocId.size())) { + if (id >= entry.children.size() || 0 > id) { + return false; + } + entry = entry.children().get(id); + } + return true; + } + + + public Entry getParentEntryById(List tocId) { + + List parentIds = getParentId(tocId); + if (parentIds.size() < 1) { + throw new UnsupportedOperationException(format("Node with tocId \"%s\" has no parent!", tocId)); + } + return getEntryById(parentIds); + } + + + public boolean hasParentById(List tocId) { + + List parentId = getParentId(tocId); + return entryExists(parentId); + } + + + public Stream streamChildren(List tocId) { + + return getEntryById(tocId).children().stream().map(Entry::node); + } + + + private static List getParentId(List tocId) { + + return tocId.subList(0, tocId.size() - 1); + } + + + public Entry getEntryById(List tocId) { + + Entry entry = entries.get(tocId.get(0)); + for (int id : tocId.subList(1, tocId.size())) { entry = entry.children().get(id); } return entry; } + public Stream streamEntriesInOrder() { + + return entries.stream().flatMap(TableOfContents::flatten); + } + + + public Stream streamSubEntriesInOrder(List parentId) { + + return Stream.of(getEntryById(parentId)).flatMap(TableOfContents::flatten); + } + + @Override public String toString() { @@ -66,48 +126,44 @@ public class TableOfContents { } - public String toString(String id) { + public String toString(List id) { return String.join("\n", streamSubEntriesInOrder(id).map(Entry::toString).toList()); } - public Stream streamEntriesInOrder() { - - return entries.stream().flatMap(TableOfContents::flatten); - } - - - public Stream streamSubEntriesInOrder(String parentId) { - - return Stream.of(getEntryById(parentId)).flatMap(TableOfContents::flatten); - } - - - private static List getIds(String idsAsString) { - - return Arrays.stream(idsAsString.split("\\.")).map(Integer::valueOf).toList(); - } - - private static Stream flatten(Entry entry) { return Stream.concat(Stream.of(entry), entry.children().stream().flatMap(TableOfContents::flatten)); } - public record Entry(NodeType type, String id, String summary, List children, DocumentGraphNode node) { + @Builder + public record Entry(List tocId, NodeType type, SemanticNode node, List children) { @Override public String toString() { - return id + ": " + type + ".: " + summary; + return tocId + ": " + type + ".: " + buildSummary(node().buildTextBlock().getSearchText()); + } + + + private static String buildSummary(CharSequence text) { + + String[] words = text.toString().split(" "); + int bound = Math.min(words.length, 4); + List list = new ArrayList<>(Arrays.asList(words).subList(0, bound)); + + return String.join(" ", list); } @Override public int hashCode() { - return Hashing.murmur3_32_fixed().hashString(type + id + summary + children.hashCode(), StandardCharsets.UTF_8).hashCode(); + + return Hashing.murmur3_32_fixed().hashString(toString(), StandardCharsets.UTF_8).hashCode(); } + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/DocumentGraphFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/DocumentGraphFactory.java new file mode 100644 index 00000000..cfdb1960 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/DocumentGraphFactory.java @@ -0,0 +1,392 @@ +package com.iqser.red.service.redaction.v1.server.document.graph.factory; + +import static com.iqser.red.service.redaction.v1.server.document.graph.factory.RectangleTransformations.toRectangle2D; +import static com.iqser.red.service.redaction.v1.server.document.graph.factory.TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX; +import static com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType.FOOTER; +import static com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType.HEADER; +import static com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType.TABLE_CELL; +import static java.lang.String.format; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.server.classification.model.Document; +import com.iqser.red.service.redaction.v1.server.classification.model.Footer; +import com.iqser.red.service.redaction.v1.server.classification.model.Header; +import com.iqser.red.service.redaction.v1.server.classification.model.Page; +import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.FooterNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.HeaderNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.HeadlineNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ImageNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ParagraphNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableCellNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableNode; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; +import com.iqser.red.service.redaction.v1.server.document.services.ImageSortService; +import com.iqser.red.service.redaction.v1.server.exception.NotFoundException; +import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; +import com.iqser.red.service.redaction.v1.server.redaction.model.PdfImage; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class DocumentGraphFactory { + + public static final double TABLE_CELL_MERGE_SIZE_THRESHOLD = 0.05; + + private final ImageSortService imageSortService; + + + public DocumentGraph buildDocumentGraph(Document document) { + + ImageSortService.SortedImages sortedImages = imageSortService.sortImagesIntoStructure(document); + TextBlockFactory textBlockFactory = new TextBlockFactory(); + Context context = new Context(new TableOfContents(), new HashMap<>(), new LinkedList<>(), new LinkedList<>(), textBlockFactory); + + document.getPages().stream().map(this::buildPage).forEach(page -> context.pages().put(page, new AtomicInteger(1))); + document.getSections().stream().flatMap(section -> section.getImages().stream()).forEach(image -> context.images().add(image)); + addSections(document, context); + addHeaderAndFooterToEachPage(document, context); + + DocumentGraph documentGraph = DocumentGraph.builder() + .numberOfPages(context.pages.size()) + .pages(context.pages.keySet().stream().sorted(Comparator.comparingInt(PageNode::getNumber)).toList()) + .tableOfContents(context.tableOfContents) + .build(); + + documentGraph.setTextBlock(documentGraph.buildTextBlock()); + return documentGraph; + } + + + private void addSections(Document document, Context context) { + + document.getSections().forEach(section -> addSection(null, section.getPageBlocks(), section.getImages(), context)); + } + + + private void addSection(SemanticNode parentNode, List pageBlocks, List images, Context context) { + + Map> blocksPerPage = pageBlocks.stream().collect(groupingBy(AbstractTextContainer::getPage)); + SectionNode sectionNode = SectionNode.builder().entities(new HashSet<>()).pages(new HashSet<>()).tableOfContents(context.tableOfContents()).build(); + + context.sections().add(sectionNode); + blocksPerPage.keySet().forEach(pageNumber -> addSectionNodeToPageNode(context, sectionNode, pageNumber)); + + List tocId; + if (parentNode == null) { + tocId = context.tableOfContents.createNewEntryAndReturnId(NodeType.SECTION, sectionNode); + } else { + tocId = context.tableOfContents.createNewChildEntryAndReturnId(parentNode.getTocId(), NodeType.SECTION, sectionNode); + } + sectionNode.setTocId(tocId); + Set alreadyMerged = new HashSet<>(); + for (AbstractTextContainer abstractTextContainer : pageBlocks) { + + if (alreadyMerged.contains(abstractTextContainer)) { + continue; + } + + if (abstractTextContainer instanceof TextBlock) { + List textBlocks = findTextBlocksWithSameClassificationAndAlignsY(abstractTextContainer, pageBlocks); + alreadyMerged.addAll(textBlocks); + addParagraphOrHeadline(sectionNode, (TextBlock) abstractTextContainer, context, textBlocks); + } + if (abstractTextContainer instanceof Table) { + addTable(sectionNode, (Table) abstractTextContainer, context); + } + } + for (PdfImage image : images) { + + addImage(sectionNode, image, context); + } + } + + + private List findTextBlocksWithSameClassificationAndAlignsY(AbstractTextContainer atc, List pageBlocks) { + + return pageBlocks.stream() + .filter(abstractTextContainer -> abstractTextContainer instanceof TextBlock) + .filter(abstractTextContainer -> abstractTextContainer.intersectsY(atc)) + .filter(abstractTextContainer -> abstractTextContainer.getPage() == atc.getPage()) + .map(abstractTextContainer -> (TextBlock) abstractTextContainer) + .toList(); + } + + + + + private void addSectionNodeToPageNode(Context context, SectionNode sectionNode, Integer pageNumber) { + + PageNode page = getPage(pageNumber, context); + sectionNode.getPages().add(page); + page.getMainBody().add(sectionNode); + } + + + private void addTable(SemanticNode parentNode, Table table, Context context) { + + PageNode page = getPage(table.getPage(), context); + TableNode tableNode = TableNode.builder() + .tableOfContents(context.tableOfContents()) + .pages(Collections.singleton(page)) + .numberOfCols(table.getColCount()) + .numberOfRows(table.getRowCount()) + .build(); + + if (!page.getMainBody().contains(parentNode)) { + parentNode.getPages().add(page); + } + + page.getMainBody().add(tableNode); + + List tocId = context.tableOfContents().createNewChildEntryAndReturnId(parentNode.getTocId(), NodeType.TABLE, tableNode); + tableNode.setTocId(tocId); + + addTableCells(table.getRows(), tableNode, context); + } + + + private void addTableCells(List> rows, SemanticNode parentNode, Context context) { + + for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) { + for (int colIndex = 0; colIndex < rows.get(rowIndex).size(); colIndex++) { + addTableCell(rows.get(rowIndex).get(colIndex), rowIndex, colIndex, parentNode, context); + } + } + } + + + private void addTableCell(Cell cell, int rowIndex, int colIndex, SemanticNode parentNode, Context context) { + + PageNode page = parentNode.getFirstPage(); + TableCellNode tableCellNode = TableCellNode.builder() + .tableOfContents(context.tableOfContents()) + .row(rowIndex) + .col(colIndex) + .header(cell.isHeaderCell()) + .pages(new HashSet<>()) + .bBox(cell.getBounds2D()) + .build(); + tableCellNode.getPages().add(page); + page.getMainBody().add(tableCellNode); + + com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock textBlock; + + List tocId = context.tableOfContents().createNewChildEntryAndReturnId(parentNode.getTocId(), TABLE_CELL, tableCellNode); + tableCellNode.setTocId(tocId); + + if (cell.getTextBlocks().isEmpty()) { + tableCellNode.setTerminal(false); + return; + } + + if (cell.getTextBlocks().size() == 1) { + textBlock = context.textBlockFactory().buildAtomicTextBlock(cell.getTextBlocks().get(0).getSequences(), tableCellNode, context, page); + tableCellNode.setTerminalTextBlock(textBlock); + } else if (StringUtils.startsWith(cell.getTextBlocks().get(0).getClassification(), "H")) { + tableCellNode.setTerminal(false); + addSection(tableCellNode, cell.getTextBlocks().stream().map(tb -> (AbstractTextContainer) tb).toList(), Collections.emptyList(), context); + } else if (cell.getArea() < TABLE_CELL_MERGE_SIZE_THRESHOLD * page.getHeight() * page.getWidth()) { + List sequences = mergeAndSortTextPositionSequenceByYThenX(cell.getTextBlocks()); + textBlock = context.textBlockFactory().buildAtomicTextBlock(sequences, tableCellNode, context, page); + tableCellNode.setTerminalTextBlock(textBlock); + } else { + tableCellNode.setTerminal(false); + cell.getTextBlocks().forEach(tb -> addParagraphOrHeadline(tableCellNode, tb, context)); + } + + } + + + private void addParagraphOrHeadline(SemanticNode parentNode, TextBlock originalTextBlock, Context context) { + + addParagraphOrHeadline(parentNode, originalTextBlock, context, Collections.emptyList()); + } + + + private void addParagraphOrHeadline(SemanticNode parentNode, TextBlock originalTextBlock, Context context, List textBlocksToMerge) { + + PageNode page; + if (originalTextBlock.getPage() == 0) { + page = parentNode.getFirstPage(); + } else { + page = getPage(originalTextBlock.getPage(), context); + } + SemanticNode node; + if (StringUtils.startsWith(originalTextBlock.getClassification(), "H")) { + node = HeadlineNode.builder().pages(Collections.singleton(page)).tableOfContents(context.tableOfContents()).build(); + } else { + node = ParagraphNode.builder().pages(Collections.singleton(page)).tableOfContents(context.tableOfContents()).build(); + } + + if (!page.getMainBody().contains(parentNode)) { + parentNode.getPages().add(page); + } + page.getMainBody().add(node); + + List textBlocks = new LinkedList<>(textBlocksToMerge); + textBlocks.add(originalTextBlock); + AtomicTextBlock textBlock = context.textBlockFactory.buildAtomicTextBlock(mergeAndSortTextPositionSequenceByYThenX(textBlocks), node, context, page); + + if (node instanceof HeadlineNode headlineNode) { + List tocId = context.tableOfContents.createNewChildEntryAndReturnId(parentNode.getTocId(), NodeType.HEADLINE, node); + headlineNode.setTerminalTextBlock(textBlock); + headlineNode.setTocId(tocId); + } + if (node instanceof ParagraphNode paragraphNode) { + List tocId = context.tableOfContents.createNewChildEntryAndReturnId(parentNode.getTocId(), NodeType.PARAGRAPH, node); + paragraphNode.setTerminalTextBlock(textBlock); + paragraphNode.setTocId(tocId); + } + } + + + private void addImage(SectionNode sectionNode, PdfImage image, Context context) { + + PageNode page = getPage(image.getPage(), context); + ImageNode imageNode = ImageNode.builder() + .imageType(image.getImageType()) + .position(toRectangle2D(image.getPosition())) + .transparency(image.isHasTransparency()) + .page(page) + .tableOfContents(context.tableOfContents()) + .build(); + page.getMainBody().add(imageNode); + + List tocId = context.tableOfContents().createNewChildEntryAndReturnId(sectionNode.getTocId(), NodeType.IMAGE, imageNode); + imageNode.setTocId(tocId); + } + + + private void addHeaderAndFooterToEachPage(Document document, Context context) { + + Map> headers = document.getHeaders() + .stream() + .map(Header::getTextBlocks) + .flatMap(List::stream) + .collect(groupingBy(AbstractTextContainer::getPage, toList())); + + Map> footers = document.getFooters() + .stream() + .map(Footer::getTextBlocks) + .flatMap(List::stream) + .collect(groupingBy(AbstractTextContainer::getPage, toList())); + + for (int pageIndex = 1; pageIndex <= document.getPages().size(); pageIndex++) { + if (headers.containsKey(pageIndex)) { + addHeader(headers.get(pageIndex), context); + } else { + addEmptyHeader(pageIndex, context); + } + } + + for (int pageIndex = 1; pageIndex <= document.getPages().size(); pageIndex++) { + if (footers.containsKey(pageIndex)) { + addFooter(footers.get(pageIndex), context); + } else { + addEmptyFooter(pageIndex, context); + } + } + } + + + private void addFooter(List textBlocks, Context context) { + + PageNode page = getPage(textBlocks.get(0).getPage(), context); + FooterNode footer = FooterNode.builder().page(page).tableOfContents(context.tableOfContents()).build(); + AtomicTextBlock textBlock = context.textBlockFactory.buildAtomicTextBlock(mergeAndSortTextPositionSequenceByYThenX(textBlocks), footer, context, page); + List tocId = context.tableOfContents().createNewEntryAndReturnId(FOOTER, footer); + footer.setTocId(tocId); + footer.setTerminalTextBlock(textBlock); + page.setFooter(footer); + } + + + public void addHeader(List textBlocks, Context context) { + + PageNode page = getPage(textBlocks.get(0).getPage(), context); + HeaderNode header = HeaderNode.builder().page(page).tableOfContents(context.tableOfContents()).build(); + AtomicTextBlock textBlock = context.textBlockFactory.buildAtomicTextBlock(mergeAndSortTextPositionSequenceByYThenX(textBlocks), header, context, 0, page); + List tocId = context.tableOfContents().createNewEntryAndReturnId(HEADER, header); + header.setTocId(tocId); + header.setTerminalTextBlock(textBlock); + page.setHeader(header); + } + + + private void addEmptyFooter(int pageIndex, Context context) { + + PageNode page = getPage(pageIndex, context); + FooterNode footer = FooterNode.builder().page(page).tableOfContents(context.tableOfContents()).build(); + AtomicTextBlock textBlock = context.textBlockFactory.emptyTextBlock(footer, context, page); + List tocId = context.tableOfContents().createNewEntryAndReturnId(FOOTER, footer); + footer.setTocId(tocId); + footer.setTerminalTextBlock(textBlock); + page.setFooter(footer); + } + + + private void addEmptyHeader(int pageIndex, Context context) { + + PageNode page = getPage(pageIndex, context); + HeaderNode header = HeaderNode.builder().page(page).tableOfContents(context.tableOfContents()).build(); + AtomicTextBlock textBlock = context.textBlockFactory.emptyTextBlock(header, context, 0, page); + List tocId = context.tableOfContents().createNewEntryAndReturnId(HEADER, header); + header.setTocId(tocId); + header.setTerminalTextBlock(textBlock); + page.setHeader(header); + } + + + private PageNode buildPage(Page p) { + + return PageNode.builder() + .height((int) p.getPageHeight()) + .width((int) p.getPageWidth()) + .number(p.getPageNumber()) + .rotation(p.getRotation()) + .mainBody(new LinkedList<>()) + .build(); + } + + + private PageNode getPage(int pageIndex, Context context) { + + return context.pages.keySet() + .stream() + .filter(page -> page.getNumber() == pageIndex) + .findFirst() + .orElseThrow(() -> new NotFoundException(format("Page with number %d not found", pageIndex))); + } + + + record Context( + TableOfContents tableOfContents, Map pages, List sections, List images, TextBlockFactory textBlockFactory) { + + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/RectangleTransformations.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/RectangleTransformations.java new file mode 100644 index 00000000..05b91df7 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/RectangleTransformations.java @@ -0,0 +1,44 @@ +package com.iqser.red.service.redaction.v1.server.document.graph.factory; + +import java.awt.geom.Rectangle2D; +import java.util.List; + +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; +import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; + +public class RectangleTransformations { + + public static Rectangle2D pad(Rectangle2D rectangle2D, int deltaX, int deltaY) { + + return new Rectangle2D.Double(rectangle2D.getMinX() - deltaX, rectangle2D.getMinY() - deltaY, rectangle2D.getWidth() + 2 * deltaX, rectangle2D.getHeight() + 2 * deltaY); + } + + + public static Rectangle2D bBoxUnionAbstractTextContainer(List abstractTextContainers) { + + return abstractTextContainers.stream().map(RectangleTransformations::toRectangle2D).reduce((a, b) -> a.createUnion(b).getBounds2D()).orElseThrow(); + } + + + public static Rectangle2D bBoxUnionAtomicTextBlock(List atomicTextBlocks) { + + return atomicTextBlocks.stream() + .flatMap(atomicTextBlock -> atomicTextBlock.getPositions().stream()) + .reduce((a, b) -> a.createUnion(b).getBounds2D()) + .orElse(new Rectangle2D.Double()); + } + + + public static Rectangle2D toRectangle2D(AbstractTextContainer abstractTextContainer) { + + return new Rectangle2D.Float(abstractTextContainer.getMinX(), abstractTextContainer.getMinY(), abstractTextContainer.getWidth(), abstractTextContainer.getHeight()); + } + + + public static Rectangle2D toRectangle2D(RedRectangle2D redRectangle2D) { + + return new Rectangle2D.Double(redRectangle2D.getX(), redRectangle2D.getY(), redRectangle2D.getWidth(), redRectangle2D.getHeight()); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/SearchTextWithTextPositionFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/SearchTextWithTextPositionFactory.java new file mode 100644 index 00000000..be94dbc5 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/SearchTextWithTextPositionFactory.java @@ -0,0 +1,159 @@ +package com.iqser.red.service.redaction.v1.server.document.graph.factory; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.server.parsing.model.RedTextPosition; +import com.iqser.red.service.redaction.v1.server.parsing.model.TextDirection; +import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; + +@Service +public class SearchTextWithTextPositionFactory { + + public static final int HEIGHT_PADDING = 2; + + public static SearchTextWithTextPositionModel buildSearchTextToTextPositionModel(List sequences) { + + if (sequences.isEmpty() || sequences.stream().allMatch(sequence -> sequence.getTextPositions().isEmpty())) { + return SearchTextWithTextPositionModel.builder() + .searchText("") + .lineBreaks(Collections.emptyList()) + .positions(Collections.emptyList()) + .stringCoordsToPositionCoords(Collections.emptyList()) + .build(); + } + + List stringIdxToPositionIdx = new LinkedList<>(); + List lineBreaksStringIdx = new LinkedList<>(); + StringBuilder sb = new StringBuilder(); + + int stringIdx = 0; + int positionIdx = 0; + int lastHyphenIdx = -3; + + RedTextPosition currentTextPosition = sequences.get(0).getTextPositions().get(0); + RedTextPosition previousTextPosition = RedTextPosition.builder().unicode(" ").position(currentTextPosition.getPosition()).build(); + + for (TextPositionSequence word : sequences) { + for (int i = 0; i < word.getTextPositions().size(); ++i) { + + currentTextPosition = word.getTextPositions().get(i); + + if (isLineBreak(currentTextPosition, previousTextPosition)) { + + if (stringIdx - lastHyphenIdx < 3) { + sb.replace(lastHyphenIdx, sb.length(), ""); + stringIdxToPositionIdx = stringIdxToPositionIdx.subList(0, lastHyphenIdx); + stringIdx = lastHyphenIdx; + lastHyphenIdx = -3; + + } else { + lineBreaksStringIdx.add(stringIdx); + } + } + if (!isRepeatedWhitespace(currentTextPosition.getUnicode(), previousTextPosition.getUnicode())) { + + if (isHyphen(currentTextPosition.getUnicode())) { + lastHyphenIdx = stringIdx; + } + sb.append(currentTextPosition.getUnicode()); + stringIdxToPositionIdx.add(positionIdx); + ++stringIdx; + } + + previousTextPosition = currentTextPosition; + + ++positionIdx; + } + + previousTextPosition = RedTextPosition.builder().unicode(" ").position(previousTextPosition.getPosition()).build(); + sb.append(previousTextPosition.getUnicode()); + stringIdxToPositionIdx.add(positionIdx); + ++stringIdx; + } + + assert sb.length() == stringIdxToPositionIdx.size(); + + List positions = sequences.stream()// + .flatMap(sequence -> sequence.getTextPositions().stream().map(textPosition -> mapRedTextPositionToUserSpace(textPosition, sequence))).toList(); + + return SearchTextWithTextPositionModel.builder() + .searchText(sb.toString()) + .lineBreaks(lineBreaksStringIdx) + .stringCoordsToPositionCoords(stringIdxToPositionIdx) + .positions(positions) + .build(); + } + + + private static boolean isLineBreak(RedTextPosition currentTextPosition, RedTextPosition previousTextPosition) { + + return Objects.equals(currentTextPosition.getUnicode(), "\n") || isDeltaYLargerThanTextHeight(currentTextPosition, previousTextPosition); + } + + + private static boolean isDeltaYLargerThanTextHeight(RedTextPosition currentPosition, RedTextPosition previousPosition) { + + if (previousPosition == null) { + return false; + } + + float deltaY = Math.abs(currentPosition.getYDirAdj() - previousPosition.getYDirAdj()); + return deltaY >= currentPosition.getHeightDir(); + } + + + private static boolean isRepeatedWhitespace(String currentUnicode, String previousUnicode) { + + return Objects.equals(previousUnicode, " ") && Objects.equals(currentUnicode, " "); + } + + + private static 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"); + } + + + private static Rectangle2D mapRedTextPositionToUserSpace(RedTextPosition textPosition, TextPositionSequence sequence) { + + float textHeight = sequence.getTextHeight() + HEIGHT_PADDING; + Rectangle2D rectangle2D = new Rectangle2D.Double(textPosition.getXDirAdj(), + textPosition.getYDirAdj() - textHeight, + textPosition.getWidthDirAdj(), + textHeight + HEIGHT_PADDING); + + AffineTransform transform = new AffineTransform(); + + if (sequence.getDir() == TextDirection.ZERO || sequence.getDir() == TextDirection.HALF_CIRCLE) { + transform.rotate(sequence.getDir().getRadians(), sequence.getPageWidth() / 2f, sequence.getPageHeight() / 2f); + transform.translate(0f, sequence.getPageHeight()); + } else if (sequence.getDir() == TextDirection.QUARTER_CIRCLE) { + transform.rotate(sequence.getDir().getRadians(), sequence.getPageWidth() / 2f, sequence.getPageWidth() / 2f); + transform.translate(0f, sequence.getPageWidth()); + } else { + transform.rotate(sequence.getDir().getRadians(), sequence.getPageHeight() / 2f, sequence.getPageHeight() / 2f); + transform.translate(0f, sequence.getPageWidth()); + } + transform.scale(1., -1.); + + return transform.createTransformedShape(rectangle2D).getBounds2D(); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/SearchTextWithTextPositionModel.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/SearchTextWithTextPositionModel.java similarity index 72% rename from redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/SearchTextWithTextPositionModel.java rename to redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/SearchTextWithTextPositionModel.java index 1e4a26a3..eb69f622 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/SearchTextWithTextPositionModel.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/SearchTextWithTextPositionModel.java @@ -1,5 +1,6 @@ -package com.iqser.red.service.redaction.v1.server.redaction.model; +package com.iqser.red.service.redaction.v1.server.document.graph.factory; +import java.awt.geom.Rectangle2D; import java.util.List; import lombok.AccessLevel; @@ -15,5 +16,5 @@ public class SearchTextWithTextPositionModel { String searchText; List lineBreaks; List stringCoordsToPositionCoords; - List positions; + List positions; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/TextBlockFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/TextBlockFactory.java new file mode 100644 index 00000000..979e5c1f --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/TextBlockFactory.java @@ -0,0 +1,75 @@ +package com.iqser.red.service.redaction.v1.server.document.graph.factory; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import com.iqser.red.service.redaction.v1.server.document.graph.Boundary; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; +import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; + +public class TextBlockFactory { + + AtomicInteger stringOffset; + AtomicLong textBlockIdx; + + public TextBlockFactory() { + stringOffset = new AtomicInteger(); + textBlockIdx = new AtomicLong(); + + } + + + public AtomicTextBlock buildAtomicTextBlock(List sequences, SemanticNode parent, DocumentGraphFactory.Context context, PageNode page) { + + Integer numberOnPage = context.pages().get(page).getAndIncrement(); + return buildAtomicTextBlock(sequences, parent, context, numberOnPage, page); + } + + + public AtomicTextBlock buildAtomicTextBlock(List sequences, SemanticNode parent, DocumentGraphFactory.Context context, Integer numberOnPage, PageNode page) { + + SearchTextWithTextPositionModel searchTextWithTextPositionModel = SearchTextWithTextPositionFactory.buildSearchTextToTextPositionModel(sequences); + int offset = stringOffset.getAndAdd(searchTextWithTextPositionModel.getSearchText().length()); + + return AtomicTextBlock.builder() + .id(textBlockIdx.getAndIncrement()) + .parent(parent) + .searchText(searchTextWithTextPositionModel.getSearchText()) + .numberOnPage(numberOnPage) + .page(page) + .lineBreaks(searchTextWithTextPositionModel.getLineBreaks()) + .positions(searchTextWithTextPositionModel.getPositions()) + .stringIdxToPositionIdx(searchTextWithTextPositionModel.getStringCoordsToPositionCoords()) + .boundary(new Boundary(offset, offset + searchTextWithTextPositionModel.getSearchText().length())) + .build(); + } + + + public AtomicTextBlock emptyTextBlock(SemanticNode parent, DocumentGraphFactory.Context context, PageNode page) { + + return emptyTextBlock(parent, context, context.pages().get(page).getAndIncrement(), page); + } + + + public AtomicTextBlock emptyTextBlock(SemanticNode parent, DocumentGraphFactory.Context context, Integer numberOnPage, PageNode page) { + + return AtomicTextBlock.builder() + .id(textBlockIdx.getAndIncrement()) + .boundary(new Boundary(stringOffset.get(), stringOffset.get())) + .searchText("") + .lineBreaks(Collections.emptyList()) + .page(page) + .numberOnPage(numberOnPage) + .stringIdxToPositionIdx(Collections.emptyList()) + .positions(Collections.emptyList()) + .parent(parent) + .build(); + } + + + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/TextPositionOperations.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/TextPositionOperations.java new file mode 100644 index 00000000..e409f89a --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/TextPositionOperations.java @@ -0,0 +1,22 @@ +package com.iqser.red.service.redaction.v1.server.document.graph.factory; + +import java.util.Comparator; +import java.util.List; + +import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; +import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; + +public class TextPositionOperations { + + + public static List mergeAndSortTextPositionSequenceByYThenX(List textBlocks) { + + return textBlocks + .stream() + .flatMap(tb -> tb.getSequences().stream()) + .sorted(Comparator.comparingDouble(TextPositionSequence::getMaxYDirAdj)// + .thenComparing(TextPositionSequence::getMaxXDirAdj)) + .toList(); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/DocumentGraphNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/DocumentGraphNode.java deleted file mode 100644 index 14877d81..00000000 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/DocumentGraphNode.java +++ /dev/null @@ -1,219 +0,0 @@ -package com.iqser.red.service.redaction.v1.server.document.graph.nodes; - -import static com.iqser.red.service.redaction.v1.server.document.services.EntityEnrichmentUtility.enrichEntity; -import static java.lang.String.format; - -import java.util.Set; -import java.util.stream.Stream; - -import com.iqser.red.service.redaction.v1.server.document.graph.Boundary; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; -import com.iqser.red.service.redaction.v1.server.exception.NotFoundException; -import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType; - -public interface DocumentGraphNode { - - /** - * Searches all Nodes located underneath this Node in the TableOfContents and concatenates their AtomicTextBlocks into a single TextBlockEntity. - * So, for a Section all AtomicTextBlocks of Subsections, Paragraphs, and Tables are concatenated into a single TextBlockEntity - * - * @return TextBlock containing all AtomicTextBlocks that are located under this Node. - */ - TextBlock buildTextBlock(); - - - /** - * Any Node maintains its own Set of Entities. - * This Set contains all Entities, whose first index is located in any of the AtomicTextBlocks underneath this Node. - * - * @return Set of all Entities associated with this Node - */ - Set getEntities(); - - - /** - * Returns the PageNode associated with this Node. - * If the node has more than one PageNode associated, it returns the PageNode with the lowest number. - * For example a section might span multiple pages, it then returns the page where the section starts. - * - * @return PageNode representing the first page on which the Node is located in the document - */ - PageNode getPage(); - - - /** - * Any Node except the First level of Sections, Header, Footer, and Pages have a direct Parent. - * For example a Paragraph has a parent Section, a Table Cell has a parent Table, etc... - * hasParent() may be used to check whether a parent is present. - * - * @return Node that represents the Parent or null, if no parent is present. - */ - DocumentGraphNode getParent(); - - - Stream streamAllSubNodes(); - - - /** - * Each AtomicTextBlock has a number assigned per page, this returns the number of the first AtomicTextBlock underneath this node - * - * @return Integer representing the number on the page - */ - Integer getNumberOnPage(); - - - /** - * - * @return the fist headline whent traversing the tree upwards - */ - default CharSequence getHeadline() { - return getParent().getHeadline(); - } - - - /** - * By default, a parent is always present, this needs to be overwritten for Headers, Footers, Pages, and Sections. - * - * @return boolean, indicating whether a parent is present. - */ - default boolean hasParent() { - - return true; - } - - - /** - * by default a Node does not have direct access to an AtomicTextBlock - * - * @return boolean, indicating if a Node has direct access to an AtomicTextBlock - */ - default boolean isTerminal() { - - return false; - } - - - /** - * by default a Node does not have direct access to an AtomicTextBlock, this method throws a UnsupportedOperationException if not overridden. - * - * @return AtomicTextBlock - */ - default AtomicTextBlock getAtomicTextBlock() { - - throw new UnsupportedOperationException("Only terminal Nodes have access to AtomicTextBlocks!"); - } - - - /** - * creates an EntityNode with only initial values set, inserts it into the subgraph contained by this node and sets the inferrable fields. - * Throws NotFoundException and removes the entity if the provided boundary could not be found in the subgraph. - * - * @param boundary start and end indices in String coordinates of the entity to be created - * @param type type of the entity to be created - * @param entityType entityType of the entity to be created - * @return the newly created and inserted EntityNode with all fields set. - */ - default EntityNode createAndAddEntity(Boundary boundary, String type, EntityType entityType) { - - EntityNode entity = EntityNode.initialEntityNode(boundary, type, entityType); - addEntityToNodeAndSetFields(entity); - return entity; - } - - - /** - * searches for the first terminal Node containing the start index of the entity to be inserted. - * Catches NotFoundException to remove the EntityNode from the graph, then rethrows it - * - * @param entity newly created EntityNode with only initial values set - */ - default void addEntityToNodeAndSetFields(EntityNode entity) { - - try { - streamAllSubNodes().anyMatch(node -> node.addEntityAndSetFieldsIfStartIndexContained(entity)); - } catch (NotFoundException e) { - entity.removeFromGraph(); - throw new RuntimeException(e); - } - } - - - /** - * If this Node's AtomicTextBlock contains the start index of the entity, the entity's position is read from the AtomicTextBlock. - * If the position can not be fully read from the AtomicTextBlock, it recursively looks in the TextBlocks of the parents until all positions are found. - * Further, the function throws NotFoundException if no parent contains all positions. - * This occurs, when the Entity is in between Nodes that do not share a parent, e.g. main sections. - * Finally, the function adds the Entity to its own list of Entities and to every parents' list recursively. - * - * @param entity The entity to be added to the graph - * @return true, if the entity has been added successfully. - * false, if the entity's start index is not contained or the Node doesn't have an AtomicTextBlock - */ - default boolean addEntityAndSetFieldsIfStartIndexContained(EntityNode entity) { - - if (!isTerminal()) { - return false; - } - - AtomicTextBlock atomicTextBlock = getAtomicTextBlock(); - if (atomicTextBlock.containsIndex(entity.getBoundary().start())) { - - entity.addContainingNode(this); - - getEntities().add(entity); - - addEntityToPage(entity); - addEntityToParents(entity); - setFields(entity, atomicTextBlock); - return true; - } - - return false; - } - - - private void addEntityToPage(EntityNode entity) { - - getPage().getEntities().add(entity); - entity.setPage(getPage()); - } - - - private void setFields(EntityNode entity, AtomicTextBlock atomicTextBlock) { - - if (atomicTextBlock.containsBoundary(entity.getBoundary())) { - enrichEntity(entity, atomicTextBlock); - } else { - this.setFieldsFromParents(this, entity); - } - } - - - private void addEntityToParents(EntityNode entity) { - - DocumentGraphNode node = this; - while (node.hasParent()) { - node = node.getParent(); - node.getEntities().add(entity); - entity.addContainingNode(node); - } - } - - - private void setFieldsFromParents(DocumentGraphNode node, EntityNode entity) { - - if (node.hasParent()) { - DocumentGraphNode parent = node.getParent(); - TextBlock textBlock = parent.buildTextBlock(); - if (textBlock.containsBoundary(entity.getBoundary())) { - enrichEntity(entity, textBlock); - return; - } else { - setFieldsFromParents(parent, entity); - } - } - throw new NotFoundException(format("Position could not be found for Entity %s", entity.toString())); - } - -} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/EntityNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/EntityNode.java index 2c279ae0..c39165a7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/EntityNode.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/EntityNode.java @@ -3,6 +3,7 @@ package com.iqser.red.service.redaction.v1.server.document.graph.nodes; import java.awt.geom.Rectangle2D; import java.nio.charset.StandardCharsets; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -60,42 +61,65 @@ public class EntityNode { @Builder.Default String legalBasis = ""; - // inferrable from graph + // inferred on graph insertion String value; CharSequence textBefore; CharSequence textAfter; - PageNode page; + @Builder.Default + Set pages = new HashSet<>(); List positions; @Builder.Default - Set containingNodes = new HashSet<>(); + List intersectingNodes = new LinkedList<>(); + SemanticNode deepestFullyContainingNode; - public void addContainingNode(DocumentGraphNode containingNode) { + public void addIntersectingNode(SemanticNode containingNode) { - containingNodes.add(containingNode); + intersectingNodes.add(containingNode); } public void removeFromGraph() { - getContainingNodes().forEach(node -> node.getEntities().remove(this)); - getPage().getEntities().remove(this); + getIntersectingNodes().forEach(node -> node.getEntities().remove(this)); + deepestFullyContainingNode = null; + getPages().forEach(page -> page.getEntities().remove(this)); setRemoved(true); } + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + sb.append("Entity ["); + sb.append(value); + sb.append(", "); + sb.append(boundary); + sb.append(", Pages["); + pages.forEach(page -> { + sb.append(page.getNumber()); + sb.append(", "); + }); + sb.delete(sb.length() - 2, sb.length()); + sb.append("], containingNode "); + sb.append(deepestFullyContainingNode); + return sb.toString(); + } + + @Override public int hashCode() { - var sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); + sb.append("Entity ["); sb.append(value); - sb.append(boundary.start()); - sb.append(page.getNumber()); - positions.forEach(r -> { - sb.append(r.getMinX()); - sb.append(r.getMinY()); - sb.append(r.getWidth()); - sb.append(r.getHeight()); + sb.append(", "); + sb.append(boundary); + sb.append(", Pages["); + pages.forEach(page -> { + sb.append(page.getNumber()); + sb.append(", "); }); return Hashing.murmur3_128().hashString(sb.toString(), StandardCharsets.UTF_8).hashCode(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/FooterNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/FooterNode.java new file mode 100644 index 00000000..1395a8c9 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/FooterNode.java @@ -0,0 +1,64 @@ +package com.iqser.red.service.redaction.v1.server.document.graph.nodes; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.iqser.red.service.redaction.v1.server.document.graph.PageElement; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.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; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class FooterNode extends PageElement implements SemanticNode { + + List tocId; + TextBlock terminalTextBlock; + + @Builder.Default + boolean terminal = true; + + @EqualsAndHashCode.Exclude + PageNode page; + + @EqualsAndHashCode.Exclude + TableOfContents tableOfContents; + + @Builder.Default + @EqualsAndHashCode.Exclude + Set entities = new HashSet<>(); + + + @Override + public TextBlock buildTextBlock() { + + return terminalTextBlock; + } + + + @Override + public Set getPages() { + + return Collections.singleton(page); + } + + + @Override + public String toString() { + + return tocId + ": " + NodeType.FOOTER + ": " + terminalTextBlock.getSearchText(); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/HeaderNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/HeaderNode.java new file mode 100644 index 00000000..063597ee --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/HeaderNode.java @@ -0,0 +1,62 @@ +package com.iqser.red.service.redaction.v1.server.document.graph.nodes; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.iqser.red.service.redaction.v1.server.document.graph.PageElement; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.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; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class HeaderNode extends PageElement implements SemanticNode { + + List tocId; + TextBlock terminalTextBlock; + + @Builder.Default + boolean terminal = true; + + @EqualsAndHashCode.Exclude + PageNode page; + + @EqualsAndHashCode.Exclude + TableOfContents tableOfContents; + + @Builder.Default + @EqualsAndHashCode.Exclude + Set entities = new HashSet<>(); + + + @Override + public TextBlock buildTextBlock() { + + return terminalTextBlock; + } + + @Override + public Set getPages() { + + return Collections.singleton(page); + } + + @Override + public String toString() { + + return tocId + ": " + NodeType.HEADER + ": " + terminalTextBlock.getSearchText(); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/HeadlineNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/HeadlineNode.java new file mode 100644 index 00000000..0b25b583 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/HeadlineNode.java @@ -0,0 +1,63 @@ +package com.iqser.red.service.redaction.v1.server.document.graph.nodes; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.iqser.red.service.redaction.v1.server.document.graph.PageElement; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.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; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class HeadlineNode extends PageElement implements SemanticNode { + + List tocId; + TextBlock terminalTextBlock; + + @Builder.Default + boolean terminal = true; + + @EqualsAndHashCode.Exclude + TableOfContents tableOfContents; + + @EqualsAndHashCode.Exclude + Set pages; + + @Builder.Default + @EqualsAndHashCode.Exclude + Set entities = new HashSet<>(); + + + @Override + public TextBlock buildTextBlock() { + + return terminalTextBlock; + } + + + @Override + public String toString() { + + return tocId + ": " + NodeType.HEADLINE + ": " + terminalTextBlock.toString(); + } + + + @Override + public SemanticNode getHeadline() { + + return this; + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/ImageNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/ImageNode.java new file mode 100644 index 00000000..ec4f4e33 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/ImageNode.java @@ -0,0 +1,88 @@ +package com.iqser.red.service.redaction.v1.server.document.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.iqser.red.service.redaction.v1.server.document.graph.PageElement; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector; +import com.iqser.red.service.redaction.v1.server.redaction.model.ImageType; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ImageNode extends PageElement implements SemanticNode { + + List tocId; + + ImageType imageType; + boolean transparency; + Rectangle2D position; + + @Builder.Default + boolean redaction = false; + @Builder.Default + boolean ignored = false; + @Builder.Default + String redactionReason = ""; + @Builder.Default + String legalBasis = ""; + @Builder.Default + int matchedRule = -1; + + @EqualsAndHashCode.Exclude + PageNode page; + + @EqualsAndHashCode.Exclude + TableOfContents tableOfContents; + + @Builder.Default + @EqualsAndHashCode.Exclude + Set entities = new HashSet<>(); + + + @Override + public TextBlock buildTextBlock() { + + return streamAllSubNodes().filter(SemanticNode::isTerminal).map(SemanticNode::getTerminalTextBlock).collect(new TextBlockCollector()); + } + + + @Override + public Set getPages() { + + return Collections.singleton(page); + } + + + @Override + public String toString() { + + return tocId + ": " + NodeType.IMAGE + ": " + imageType.toString() + " " + position; + } + + @Override + public Map getBBox() { + + Map bBoxPerPage = new HashMap<>(); + bBoxPerPage.put(page, position); + return bBoxPerPage; + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/NodeType.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/NodeType.java index 631749ef..479ed388 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/NodeType.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/NodeType.java @@ -7,6 +7,11 @@ public enum NodeType { return "Section"; } }, + HEADLINE { + public String toString() { + return "Headline"; + } + }, PARAGRAPH { public String toString() { @@ -24,5 +29,23 @@ public enum NodeType { return "Cell"; } + }, + IMAGE { + public String toString() { + + return "Image"; + } + }, + HEADER { + public String toString() { + + return "Header"; + } + }, + FOOTER { + public String toString() { + + return "Footer"; + } } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/PageNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/PageNode.java index c1b91da8..be04426a 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/PageNode.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/PageNode.java @@ -3,83 +3,68 @@ package com.iqser.red.service.redaction.v1.server.document.graph.nodes; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Stream; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.ConcatenatedTextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.FooterNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.HeaderNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ImageNode; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import lombok.experimental.FieldDefaults; -@Data +@Getter +@Setter @Builder @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) -public class PageNode implements DocumentGraphNode{ +public class PageNode { Integer number; Integer height; Integer width; - List mainBody; - AtomicTextBlock header; - AtomicTextBlock footer; + Integer rotation; + + @EqualsAndHashCode.Exclude + List mainBody; + @EqualsAndHashCode.Exclude + HeaderNode header; + @EqualsAndHashCode.Exclude + FooterNode footer; @Builder.Default @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); + @Builder.Default + @EqualsAndHashCode.Exclude + Set images = new HashSet<>(); - public ConcatenatedTextBlock buildTextBlock() { + public TextBlock getMainBodyTextBlock() { - return mainBody.stream().filter(DocumentGraphNode::isTerminal).map(DocumentGraphNode::getAtomicTextBlock).collect(new TextBlockCollector()); - } - - - @Override - public PageNode getPage() { - - return this; - } - - - @Override - public DocumentGraphNode getParent() { - - return null; - } - - - @Override - public boolean hasParent() { - - return false; - } - - - @Override - public Stream streamAllSubNodes() { - - return mainBody.stream(); - } - - - @Override - public Integer getNumberOnPage() { - - return 0; + return mainBody.stream().filter(SemanticNode::isTerminal).map(SemanticNode::getTerminalTextBlock).collect(new TextBlockCollector()); } @Override public String toString() { - return header.getSearchText() + buildTextBlock().toString() + footer.getSearchText(); + return String.valueOf(number); + } + + + @Override + public int hashCode() { + + return number; } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/ParagraphNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/ParagraphNode.java index 258a64f9..21a2d12d 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/ParagraphNode.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/ParagraphNode.java @@ -1,10 +1,12 @@ package com.iqser.red.service.redaction.v1.server.document.graph.nodes; import java.util.HashSet; +import java.util.List; import java.util.Set; -import java.util.stream.Stream; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.PageElement; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -17,17 +19,19 @@ import lombok.experimental.FieldDefaults; @Builder @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) -public class ParagraphNode implements DocumentGraphNode { +public class ParagraphNode extends PageElement implements SemanticNode { - String tocId; - Integer numberOnPage; - Integer numberInSection; + List tocId; + TextBlock terminalTextBlock; + + @Builder.Default + boolean terminal = true; @EqualsAndHashCode.Exclude - SectionNode parentSection; + TableOfContents tableOfContents; + @EqualsAndHashCode.Exclude - PageNode page; - AtomicTextBlock atomicTextBlock; + Set pages; @Builder.Default @EqualsAndHashCode.Exclude @@ -35,36 +39,14 @@ public class ParagraphNode implements DocumentGraphNode { @Override - public AtomicTextBlock buildTextBlock() { + public TextBlock buildTextBlock() { - return atomicTextBlock; + return terminalTextBlock; } - - @Override - public DocumentGraphNode getParent() { - - return parentSection; - } - - - @Override - public boolean isTerminal() { - - return true; - } - - @Override public String toString() { - return tocId + ": " + atomicTextBlock.toString(); + return tocId + ": " + NodeType.PARAGRAPH + ": " + terminalTextBlock.toString(); } - - @Override - public Stream streamAllSubNodes() { - - return Stream.of(this); - } - } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/SectionNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/SectionNode.java index 7f04e9c3..573f1e19 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/SectionNode.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/SectionNode.java @@ -3,12 +3,12 @@ package com.iqser.red.service.redaction.v1.server.document.graph.nodes; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Stream; +import com.iqser.red.service.redaction.v1.server.document.graph.PageElement; import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.ConcatenatedTextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector; +import com.iqser.red.service.redaction.v1.server.exception.NotFoundException; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -23,86 +23,44 @@ import lombok.extern.slf4j.Slf4j; @Builder @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) -public class SectionNode implements DocumentGraphNode { +public class SectionNode extends PageElement implements SemanticNode { - String tocId; - Integer numberOnPage; + List tocId; + + TextBlock textBlock; @EqualsAndHashCode.Exclude TableOfContents tableOfContents; @EqualsAndHashCode.Exclude - DocumentGraphNode parentSection; - - - AtomicTextBlock headline; - - List subSections; - List paragraphs; - List tables; - @EqualsAndHashCode.Exclude - List pages; + Set pages; @Builder.Default @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); + @Override + public TextBlock buildTextBlock() { + + if (textBlock == null) { + textBlock = streamAllSubNodes().filter(SemanticNode::isTerminal).map(SemanticNode::getTerminalTextBlock).collect(new TextBlockCollector()); + } + return textBlock; + } + + @Override public String toString() { - return tocId + ": " + headline.toString(); + return tocId.toString() + ": " + NodeType.SECTION + ": " + buildTextBlock().getSearchText(); } - @Override - public ConcatenatedTextBlock buildTextBlock() { + public HeadlineNode getHeadline() { - return streamAllSubNodes().map(DocumentGraphNode::getAtomicTextBlock).collect(new TextBlockCollector()); - } - - - @Override - public DocumentGraphNode getParent() { - - if (hasParent()) { - return parentSection; - } else { - throw new UnsupportedOperationException("This section has no parent Section!"); - } - } - - - @Override - public Stream streamAllSubNodes() { - - return tableOfContents.streamSubEntriesInOrder(tocId).map(TableOfContents.Entry::node); - } - - - @Override - public boolean hasParent() { - - return parentSection != null; - } - - - @Override - public boolean isTerminal() { - - return true; - } - - - @Override - public AtomicTextBlock getAtomicTextBlock() { - - return headline; - } - - - @Override - public PageNode getPage() { - - return pages.get(0); + return streamChildren().filter(node -> node instanceof HeadlineNode) + .map(node -> (HeadlineNode) node) + .findFirst() + .orElseThrow(() -> new NotFoundException("Section has no Headline!")); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/SemanticNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/SemanticNode.java new file mode 100644 index 00000000..b276b8b0 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/SemanticNode.java @@ -0,0 +1,162 @@ +package com.iqser.red.service.redaction.v1.server.document.graph.nodes; + +import static java.util.stream.Collectors.groupingBy; + +import java.awt.geom.Rectangle2D; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.graph.factory.RectangleTransformations; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; + +public interface SemanticNode { + + /** + * Searches all Nodes located underneath this Node in the TableOfContents and concatenates their AtomicTextBlocks into a single TextBlockEntity. + * So, for a Section all TextBlocks of Subsections, Paragraphs, and Tables are concatenated into a single TextBlockEntity + * + * @return TextBlock containing all AtomicTextBlocks that are located under this Node. + */ + TextBlock buildTextBlock(); + + + /** + * Any Node maintains its own Set of Entities. + * This Set contains all Entities, whose boundary intersects the boundary of the entity. + * + * @return Set of all Entities associated with this Node + */ + Set getEntities(); + + + Set getPages(); + + + default PageNode getFirstPage() { + + return getPages().stream().min(Comparator.comparingInt(PageNode::getNumber)).orElseThrow(); + } + + + TableOfContents getTableOfContents(); + + + List getTocId(); + + + void setTocId(List tocId); + + + /** + * Traverses the Tree up, until it hits a HeadlineNode or hits a SectionNode which will then return its HeadlineNode. + * + * @return First HeadlineNode found + */ + default SemanticNode getHeadline() { + + return getParent().getHeadline(); + } + + + default boolean hasParent() { + + return getTableOfContents().hasParentById(getTocId()); + } + + + default SemanticNode getParent() { + + return getTableOfContents().getParentEntryById(getTocId()).node(); + } + + + /** + * Terminal means a SemanticNode has direct access to a TextBlock, by default this is false and must be overridden. + * Currently only Sections and Tables are not terminal. + * + * @return boolean, indicating if a Node has direct access to a TextBlock + */ + default boolean isTerminal() { + + return false; + } + + + /** + * Terminal means a SemanticNode has direct access to a TextBlock, by default this is false and must be overridden. + * Currently only Sections and Tables are not terminal. + * + * @return AtomicTextBlock + */ + default TextBlock getTerminalTextBlock() { + + throw new UnsupportedOperationException("Only terminal Nodes have access to TerminalTextBlocks!"); + } + + + /** + * Each AtomicTextBlock has an index on its page, this returns the number of the first AtomicTextBlock underneath this node. + * If this node does not have any AtomicTexBlocks underneath it, e.g. an empty TableCell. It returns -1. + * + * @return Integer representing the number on the page + */ + default Integer getNumberOnPage() { + + TextBlock textBlock = buildTextBlock(); + if (textBlock.getAtomicTextBlocks().size() > 0) { + return buildTextBlock().getAtomicTextBlocks().get(0).getNumberOnPage(); + } else { + return -1; + } + } + + + default void addThisToEntityIfIntersects(EntityNode entity) { + + TextBlock textBlock = buildTextBlock(); + if (textBlock.getBoundary().intersects(entity.getBoundary())) { + + if (textBlock.containsBoundary(entity.getBoundary())) { + entity.setDeepestFullyContainingNode(this); + } + + entity.addIntersectingNode(this); + streamChildren().forEach(node -> node.addThisToEntityIfIntersects(entity)); + } + } + + + default Stream streamChildren() { + + return getTableOfContents().streamChildren(getTocId()); + } + + + default Stream streamAllSubNodes() { + + return getTableOfContents().streamSubEntriesInOrder(getTocId()).map(TableOfContents.Entry::node); + } + + + default Map getBBox() { + + Map bBoxPerPage = new HashMap<>(); + if (isTerminal()) { + Map> atomicTextBlockPerPage = buildTextBlock().getAtomicTextBlocks().stream().collect(groupingBy(AtomicTextBlock::getPage)); + atomicTextBlockPerPage.forEach((page, atbs) -> bBoxPerPage.put(page, RectangleTransformations.bBoxUnionAtomicTextBlock(atbs))); + return bBoxPerPage; + } + + return streamChildren().map(SemanticNode::getBBox).reduce((map1, map2) -> { + map1.forEach((page, rectangle) -> map2.merge(page, rectangle, (rect1, rect2) -> rect1.createUnion(rect2).getBounds2D())); + return map2; + }).orElse(bBoxPerPage); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/TableCellNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/TableCellNode.java index 244731bf..8b2233e3 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/TableCellNode.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/TableCellNode.java @@ -1,10 +1,17 @@ package com.iqser.red.service.redaction.v1.server.document.graph.nodes; -import java.util.LinkedList; +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 java.util.stream.Stream; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.PageElement; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -17,43 +24,58 @@ import lombok.experimental.FieldDefaults; @Builder @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) -public class TableCellNode implements DocumentGraphNode { +public class TableCellNode extends PageElement implements SemanticNode { + + List tocId; + int row; + int col; + boolean header; + + Rectangle2D bBox; + + @Builder.Default + boolean terminal = true; + TextBlock terminalTextBlock; + + TextBlock textBlock; @EqualsAndHashCode.Exclude - TableNode parentTable; - Integer numberOnPage; - AtomicTextBlock atomicTextBlock; - PageNode page; + TableOfContents tableOfContents; + + @EqualsAndHashCode.Exclude + Set pages; @Builder.Default @EqualsAndHashCode.Exclude - List entities = new LinkedList<>(); + Set entities = new HashSet<>(); @Override - public AtomicTextBlock buildTextBlock() { + public Map getBBox() { - return atomicTextBlock; - } - - - @Override - public DocumentGraphNode getParent() { - - return parentTable; - } - - - @Override - public boolean isTerminal() { - - return true; + Map bBoxPerPage = new HashMap<>(); + pages.forEach(page -> bBoxPerPage.put(page, bBox)); + return bBoxPerPage; } @Override - public Stream streamAllSubNodes() { + public TextBlock buildTextBlock() { - return Stream.of(this); + if (textBlock == null) { + textBlock = streamAllSubNodes().filter(SemanticNode::isTerminal).map(SemanticNode::getTerminalTextBlock).collect(new TextBlockCollector()); + } + return textBlock; + } + + @Override + public String toString() { + + return tocId + ": " + NodeType.TABLE_CELL + ": " + buildTextBlock().getSearchText(); + } + + public Stream getHeaders() { + TableNode tableNode = (TableNode) getParent(); + return tableNode.streamHeadersForCell(row, col); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/TableNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/TableNode.java index bfb99062..c371f4c3 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/TableNode.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/nodes/TableNode.java @@ -1,13 +1,14 @@ package com.iqser.red.service.redaction.v1.server.document.graph.nodes; -import java.util.LinkedList; +import java.util.HashSet; import java.util.List; -import java.util.function.Function; +import java.util.Set; import java.util.stream.Stream; +import com.iqser.red.service.redaction.v1.server.document.graph.PageElement; import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.ConcatenatedTextBlock; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -20,69 +21,55 @@ import lombok.experimental.FieldDefaults; @Builder @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) -public class TableNode implements DocumentGraphNode { +public class TableNode extends PageElement implements SemanticNode { - Integer id; - String tocId; - Integer numberOfRows; - Integer numberOfCols; - Integer numberOnPage; - List tableHeaders; - List> tableCells; + List tocId; TableOfContents tableOfContents; + Integer numberOfRows; + Integer numberOfCols; + + TextBlock textBlock; @EqualsAndHashCode.Exclude - SectionNode parentSection; - @EqualsAndHashCode.Exclude - List pages; + Set pages; @Builder.Default @EqualsAndHashCode.Exclude - List entities = new LinkedList<>(); + Set entities = new HashSet<>(); - private Stream streamTableCells() { + public Stream streamTableCells() { - return tableCells.stream().flatMap(List::stream); + return streamChildren().map(node -> (TableCellNode) node); } - private Stream streamTableRow(int row) { + public Stream streamHeaders() { - return tableCells.get(row).stream(); + return streamTableCells().filter(TableCellNode::isHeader); } - private Stream streamTableCol(int col) { + public Stream streamHeadersForCell(int row, int col) { - return tableCells.stream().map(row -> row.get(col)); + return streamHeaders().filter(cell -> cell.getRow() == row || cell.getCol() == col); } @Override - public ConcatenatedTextBlock buildTextBlock() { + public TextBlock buildTextBlock() { - return streamTableCells().map(TableCellNode::getAtomicTextBlock).collect(new TextBlockCollector()); - } - - @Override - public Stream streamAllSubNodes() { - - return streamTableCells().map(Function.identity()); + if (textBlock == null) { + textBlock = streamAllSubNodes().filter(SemanticNode::isTerminal).map(SemanticNode::getTerminalTextBlock).collect(new TextBlockCollector()); + } + return textBlock; } @Override - public DocumentGraphNode getParent() { + public String toString() { - return parentSection; - } - - - @Override - public PageNode getPage() { - - return pages.get(0); + return tocId.toString() + ": " + NodeType.TABLE + ": " + buildTextBlock().getSearchText(); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/AtomicTextBlock.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/AtomicTextBlock.java index 7a8ceaf9..d99c9a9c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/AtomicTextBlock.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/AtomicTextBlock.java @@ -6,7 +6,8 @@ import java.awt.geom.Rectangle2D; import java.util.List; import com.iqser.red.service.redaction.v1.server.document.graph.Boundary; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.DocumentGraphNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -22,6 +23,8 @@ import lombok.experimental.FieldDefaults; public class AtomicTextBlock implements TextBlock { Long id; + Integer numberOnPage; + PageNode page; //string coordinates Boundary boundary; @@ -33,16 +36,10 @@ public class AtomicTextBlock implements TextBlock { List positions; @EqualsAndHashCode.Exclude - DocumentGraphNode parent; - - - public int indexOf(String searchTerm) { - - int pos = searchText.indexOf(searchTerm); - return pos == -1 ? -1 : pos + boundary.start(); - } + SemanticNode parent; + @Override public int numberOfLines() { return lineBreaks.size(); @@ -56,36 +53,38 @@ public class AtomicTextBlock implements TextBlock { } + @Override public int getNextLinebreak(int fromIndex) { return lineBreaks.stream()// - .filter(linebreak -> linebreak > fromIndex) // + .filter(linebreak -> linebreak > fromIndex - boundary.start()) // .findFirst() // .orElse(searchText.length()) + boundary.start(); } + @Override public int getPreviousLinebreak(int fromIndex) { return lineBreaks.stream()// - .filter(linebreak -> linebreak <= fromIndex)// + .filter(linebreak -> linebreak <= fromIndex - boundary.start())// .reduce((a, b) -> b)// .orElse(0) + boundary.start(); } + @Override public Rectangle2D getPosition(int stringIdx) { return positions.get(stringIdxToPositionIdx.get(stringIdx - boundary.start())); } + @Override public List getPositions(Boundary boundary) { if (!containsBoundary(boundary)) { - throw new IndexOutOfBoundsException(format("%s is out of bounds for %s", - boundary, - this.boundary)); + throw new IndexOutOfBoundsException(format("%s is out of bounds for %s", boundary, this.boundary)); } if (boundary.end() == this.boundary.end()) { @@ -96,27 +95,6 @@ public class AtomicTextBlock implements TextBlock { } - @Override - public int length() { - - return searchText.length(); - } - - - @Override - public char charAt(int index) { - - return searchText.charAt(index - boundary.start()); - } - - - @Override - public CharSequence subSequence(int start, int end) { - - return searchText.substring(start - boundary.start(), end - boundary.start()); - } - - @Override public String toString() { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/ConcatenatedTextBlock.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/ConcatenatedTextBlock.java index 8e1d68d6..5d01507d 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/ConcatenatedTextBlock.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/ConcatenatedTextBlock.java @@ -18,33 +18,25 @@ import lombok.experimental.FieldDefaults; public class ConcatenatedTextBlock implements TextBlock, Supplier { List atomicTextBlocks; - StringBuilder searchText; + String searchText; Boundary boundary; public ConcatenatedTextBlock(List atomicTextBlocks) { this.atomicTextBlocks = new LinkedList<>(); - this.searchText = new StringBuilder(); if (atomicTextBlocks.isEmpty()) { boundary = new Boundary(-1, -1); return; } var firstTextBlock = atomicTextBlocks.get(0); this.atomicTextBlocks.add(firstTextBlock); - this.searchText.append(firstTextBlock.getSearchText()); boundary = new Boundary(firstTextBlock.getBoundary().start(), firstTextBlock.getBoundary().end()); atomicTextBlocks.subList(1, atomicTextBlocks.size()).forEach(this::concat); } - public ConcatenatedTextBlock(AtomicTextBlock atomicTextBlocks) { - - new ConcatenatedTextBlock(List.of(atomicTextBlocks)); - } - - public ConcatenatedTextBlock concat(TextBlock textBlock) { if (this.atomicTextBlocks.isEmpty()) { @@ -53,44 +45,65 @@ public class ConcatenatedTextBlock implements TextBlock, Supplier (textBlock.getBoundary().contains(stringIdx))).findAny().orElseThrow(IndexOutOfBoundsException::new); } + private List getAllAtomicTextBlocksPartiallyInStringIdxRange(Boundary boundary) { + + return atomicTextBlocks.stream().filter(tb -> tb.getBoundary().intersects(boundary)).toList(); + } + + + @Override + public String getSearchText() { + + if (searchText == null) { + StringBuilder sb = new StringBuilder(); + getAtomicTextBlocks().forEach(atb -> sb.append(atb.getSearchText())); + searchText = sb.toString(); + } + return searchText; + } + + + @Override public int numberOfLines() { return atomicTextBlocks.stream().map(AtomicTextBlock::getLineBreaks).mapToInt(List::size).sum(); } + @Override public int getNextLinebreak(int fromIndex) { return getAtomicTextBlockByStringIndex(fromIndex).getNextLinebreak(fromIndex); } + @Override public int getPreviousLinebreak(int fromIndex) { return getAtomicTextBlockByStringIndex(fromIndex).getPreviousLinebreak(fromIndex); } + @Override public Rectangle2D getPosition(int stringIdx) { return getAtomicTextBlockByStringIndex(stringIdx).getPosition(stringIdx); } + @Override public List getPositions(Boundary boundary) { List textBlocks = getAllAtomicTextBlocksPartiallyInStringIdxRange(boundary); @@ -113,43 +126,17 @@ public class ConcatenatedTextBlock implements TextBlock, Supplier (textBlock.getBoundary().end()) > stringIdx).findFirst().orElseThrow(IndexOutOfBoundsException::new); - } - - - private List getAllAtomicTextBlocksPartiallyInStringIdxRange(Boundary boundary) { - - return atomicTextBlocks.stream().filter(tb -> tb.getBoundary().intersects(boundary)).toList(); - } - - - @Override - public int length() { - - return this.searchText.length(); - } - - - @Override - public char charAt(int index) { - - return searchText.charAt(index - boundary.start()); - } - - - @Override - public CharSequence subSequence(int start, int end) { - - return searchText.subSequence(start - boundary.start(), end - boundary.start()); - } - - @Override public ConcatenatedTextBlock get() { return this; } + + @Override + public String toString() { + + return getSearchText(); + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/TextBlock.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/TextBlock.java index 4431dfdc..0a880d63 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/TextBlock.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/TextBlock.java @@ -9,7 +9,7 @@ import com.iqser.red.service.redaction.v1.server.document.graph.Boundary; public interface TextBlock extends CharSequence { - CharSequence getSearchText(); + String getSearchText(); List getAtomicTextBlocks(); @@ -33,7 +33,20 @@ public interface TextBlock extends CharSequence { int numberOfLines(); - int indexOf(String searchTerm); + default int indexOf(String searchTerm) { + + return indexOf(searchTerm, getBoundary().start()); + } + + + default int indexOf(String searchTerm, int startOffset) { + + int start = getSearchText().indexOf(searchTerm, startOffset - getBoundary().start()); + if (start == -1) { + return -1; + } + return start + getBoundary().start(); + } default CharSequence getFirstLine() { @@ -62,4 +75,25 @@ public interface TextBlock extends CharSequence { return subSequence(boundary.start(), boundary.end()); } + + @Override + default CharSequence subSequence(int start, int end) { + + return getSearchText().substring(start - getBoundary().start(), end - getBoundary().start()); + } + + + @Override + default int length() { + + return getSearchText().length(); + } + + + @Override + default char charAt(int index) { + + return getSearchText().charAt(index - getBoundary().start()); + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/TextBlockCollector.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/TextBlockCollector.java index 27320c89..48c69084 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/TextBlockCollector.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/textblock/TextBlockCollector.java @@ -11,7 +11,7 @@ import java.util.stream.Collector; import lombok.NoArgsConstructor; @NoArgsConstructor -public class TextBlockCollector implements Collector { +public class TextBlockCollector implements Collector { @Override @@ -22,7 +22,7 @@ public class TextBlockCollector implements Collector accumulator() { + public BiConsumer accumulator() { return ConcatenatedTextBlock::concat; } @@ -36,9 +36,9 @@ public class TextBlockCollector implements Collector finisher() { + public Function finisher() { - return Function.identity(); + return a -> a; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentDataMapper.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentDataMapper.java index 2ee37811..7a540f51 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentDataMapper.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentDataMapper.java @@ -13,13 +13,18 @@ import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; @Service public class DocumentDataMapper { public DocumentData toDocumentData(DocumentGraph documentGraph) { - List atomicTextBlockData = documentGraph.streamAtomicTextBlocksInOrder().map(this::toAtomicTextBlockData).toList(); + List atomicTextBlockData = documentGraph.streamTextBlocksInOrder() + .flatMap(textBlock -> textBlock.getAtomicTextBlocks().stream()) + .distinct() + .map(this::toAtomicTextBlockData) + .toList(); List pageData = documentGraph.getPages().stream().map(this::toPageData).toList(); TableOfContentsData tableOfContentsData = toTableOfContentsData(documentGraph.getTableOfContents()); return DocumentData.builder().atomicTextBlocks(atomicTextBlockData).pages(pageData).tableOfContents(tableOfContentsData).build(); @@ -34,25 +39,36 @@ public class DocumentDataMapper { private TableOfContentsData.EntryData toEntryData(TableOfContents.Entry entry) { + Long[] atomicTextBlocks; + if (entry.node().isTerminal()) { + atomicTextBlocks = toAtomicTextBlockIds(entry.node().getTerminalTextBlock()); + } else { + atomicTextBlocks = new Long[]{-1L}; + } + return TableOfContentsData.EntryData.builder() - .tocId(entry.id()) + .tocId(toPrimitiveIntArray(entry.tocId())) .subEntries(entry.children().stream().map(this::toEntryData).toList()) .type(entry.type()) - .atomicTextBlock(entry.node().isTerminal() ? entry.node().getAtomicTextBlock().getId() : -1L) - .page(Long.valueOf(entry.node().getPage().getNumber())) + .atomicTextBlocks(atomicTextBlocks) + .pages(entry.node().getPages().stream().map(PageNode::getNumber).toArray(Long[]::new)) .numberOnPage(entry.node().getNumberOnPage()) .build(); } + private Long[] toAtomicTextBlockIds(TextBlock textBlock) { + + return textBlock.getAtomicTextBlocks().stream().map(AtomicTextBlock::getId).toArray(Long[]::new); + } + + private PageData toPageData(PageNode pageNode) { return PageData.builder() .height(pageNode.getHeight()) .width(pageNode.getWidth()) .number(pageNode.getNumber()) - .footer(pageNode.getFooter().getId()) - .header(pageNode.getHeader().getId()) .build(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentGraphFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentGraphFactory.java deleted file mode 100644 index ce1076cd..00000000 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentGraphFactory.java +++ /dev/null @@ -1,304 +0,0 @@ -package com.iqser.red.service.redaction.v1.server.document.services; - -import static java.lang.String.format; - -import java.awt.geom.Rectangle2D; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; - -import org.springframework.stereotype.Service; - -import com.iqser.red.service.redaction.v1.server.classification.model.Document; -import com.iqser.red.service.redaction.v1.server.classification.model.Footer; -import com.iqser.red.service.redaction.v1.server.classification.model.Header; -import com.iqser.red.service.redaction.v1.server.classification.model.Page; -import com.iqser.red.service.redaction.v1.server.classification.model.Section; -import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; -import com.iqser.red.service.redaction.v1.server.document.graph.Boundary; -import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; -import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.DocumentGraphNode; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ParagraphNode; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableNode; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; -import com.iqser.red.service.redaction.v1.server.exception.NotFoundException; -import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; -import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D; -import com.iqser.red.service.redaction.v1.server.redaction.model.SearchTextWithTextPositionModel; -import com.iqser.red.service.redaction.v1.server.redaction.service.SearchTextWithTextPositionFactory; -import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; -import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class DocumentGraphFactory { - - private final SearchTextWithTextPositionFactory searchTextWithTextPositionFactory; - - - public DocumentGraph buildDocumentGraph(Document document) { - - Context context = new Context(new TableOfContents(), new LinkedList<>(), new LinkedList<>(), new AtomicInteger(0), new AtomicLong(0)); - - context.pages.addAll(document.getPages().stream().map(this::buildPage).toList()); - - // is tracked by Table of Contents - addSections(document, context); - - // not tracked by Table of Contents - addHeaderAndFooterToEachPage(document, context); - DocumentGraph documentGraph = DocumentGraph.builder() - .numberOfPages(context.pages.size()) - .pages(context.pages) - .sections(context.sections) - .tableOfContents(context.tableOfContents) - .build(); - documentGraph.setText(documentGraph.buildTextBlock()); - return documentGraph; - } - - - private void addSections(Document document, Context context) { - - for (var section : document.getSections()) { - addSection(section, context); - } - } - - - private void addSection(Section section, Context context) { - - SectionNode sectionEntity = SectionNode.builder() - .entities(new LinkedList<>()) - .pages(new LinkedList<>()) - .paragraphs(new LinkedList<>()) - .tables(new LinkedList<>()) - .subSections(new LinkedList<>()) - .tableOfContents(context.tableOfContents()) - .build(); - - context.sections().add(sectionEntity); - List pageBlocks = new ArrayList<>(section.getPageBlocks()); - PageNode page = getPage(section.getPageBlocks().get(0).getPage(), context); - sectionEntity.getPages().add(page); - page.getMainBody().add(sectionEntity); - if (pageBlocks.get(0) instanceof TextBlock) { - sectionEntity.setHeadline(buildAtomicTextBlock(((TextBlock) pageBlocks.get(0)).getSequences(), sectionEntity, context)); - sectionEntity.setNumberOnPage(((TextBlock) pageBlocks.get(0)).getIndexOnPage()); - pageBlocks.remove(0); - } else { - sectionEntity.setNumberOnPage(1); - sectionEntity.setHeadline(emptyTextBlock(sectionEntity, context)); - } - - String sectionId = context.tableOfContents.createNewEntryAndReturnId(NodeType.SECTION, buildSummary(sectionEntity.getHeadline()), sectionEntity); - sectionEntity.setTocId(sectionId); - - int paragraphIdx = 0; - int tableIdx = 0; - for (AbstractTextContainer abstractTextContainer : pageBlocks) { - if (abstractTextContainer instanceof TextBlock) { - addParagraph(sectionEntity, (TextBlock) abstractTextContainer, paragraphIdx, context); - paragraphIdx++; - } else if (abstractTextContainer instanceof Table) { - //addTable(sectionEntity, (Table) abstractTextContainer, tableIdx, context); - tableIdx++; - - } - } - } - - - private void addTable(SectionNode sectionEntity, Table table, int tableIdx, Context context) { - - PageNode page = getPage(table.getPage(), context); - TableNode tableEntity = TableNode.builder().id(tableIdx).tableOfContents(context.tableOfContents()).pages(new LinkedList<>()).parentSection(sectionEntity).build(); - sectionEntity.getTables().add(tableEntity); - - if (!page.getMainBody().contains(sectionEntity)) { - sectionEntity.getPages().add(page); - } - page.getMainBody().add(tableEntity); - - } - - - private void addParagraph(SectionNode sectionEntity, TextBlock originalTextBlock, int paragraphIdx, Context context) { - - PageNode page = getPage(originalTextBlock.getPage(), context); - ParagraphNode paragraph = ParagraphNode.builder().numberOnPage(originalTextBlock.getIndexOnPage()).page(page).parentSection(sectionEntity).build(); - sectionEntity.getParagraphs().add(paragraph); - - if (!page.getMainBody().contains(sectionEntity)) { - sectionEntity.getPages().add(page); - } - page.getMainBody().add(paragraph); - - var textBlock = buildAtomicTextBlock(originalTextBlock.getSequences(), paragraph, context); - paragraph.setAtomicTextBlock(textBlock); - - String tocId = context.tableOfContents.createNewChildEntryAndReturnId(sectionEntity.getTocId(), NodeType.PARAGRAPH, buildSummary(textBlock), paragraph); - paragraph.setTocId(tocId); - } - - - private void addHeaderAndFooterToEachPage(Document document, Context context) { - - Map> headers = document.getHeaders() - .stream() - .map(Header::getTextBlocks) - .flatMap(List::stream) - .collect(Collectors.groupingBy(AbstractTextContainer::getPage, Collectors.toList())); - - Map> footers = document.getFooters() - .stream() - .map(Footer::getTextBlocks) - .flatMap(List::stream) - .collect(Collectors.groupingBy(AbstractTextContainer::getPage, Collectors.toList())); - - for (int pageIndex = 1; pageIndex <= document.getPages().size(); pageIndex++) { - if (headers.containsKey(pageIndex)) { - addHeader(headers.get(pageIndex), context); - } else { - addEmptyHeader(pageIndex, context); - } - } - - for (int pageIndex = 1; pageIndex <= document.getPages().size(); pageIndex++) { - if (footers.containsKey(pageIndex)) { - addFooter(footers.get(pageIndex), context); - } else { - addEmptyFooter(pageIndex, context); - } - } - } - - - private void addFooter(List textBlocks, Context context) { - - PageNode page = getPage(textBlocks.get(0).getPage(), context); - AtomicTextBlock footer = buildAtomicTextBlock(mergeAndSortTextPositionSequences(textBlocks), page, context); - page.setFooter(footer); - } - - - public void addHeader(List textBlocks, Context context) { - - PageNode page = getPage(textBlocks.get(0).getPage(), context); - AtomicTextBlock header = buildAtomicTextBlock(mergeAndSortTextPositionSequences(textBlocks), page, context); - page.setHeader(header); - } - - - private void addEmptyFooter(int pageIndex, Context context) { - - PageNode page = getPage(pageIndex, context); - page.setFooter(emptyTextBlock(page, context)); - } - - - private void addEmptyHeader(int pageIndex, Context context) { - - PageNode page = getPage(pageIndex, context); - page.setHeader(emptyTextBlock(page, context)); - } - - - private AtomicTextBlock emptyTextBlock(DocumentGraphNode parent, Context context) { - - return AtomicTextBlock.builder() - .id(context.textBlockIdx.getAndIncrement()) - .boundary(new Boundary(context.stringOffset.get(), context.stringOffset.get())) - .searchText("") - .lineBreaks(Collections.emptyList()) - .stringIdxToPositionIdx(Collections.emptyList()) - .positions(Collections.emptyList()) - .parent(parent) - .build(); - } - - - private static String buildSummary(AtomicTextBlock textBlock) { - - if (textBlock == null) { - return " probably a table"; - } - - String[] words = textBlock.getFirstLine().toString().split(" "); - int bound = Math.min(words.length, 4); - List list = new ArrayList<>(Arrays.asList(words).subList(0, bound)); - - return String.join(" ", list); - } - - - private PageNode buildPage(Page p) { - - return PageNode.builder().height((int) p.getPageHeight()).width((int) p.getPageWidth()).number(p.getPageNumber()).mainBody(new LinkedList<>()).build(); - } - - - private List mergeAndSortTextPositionSequences(List textBlocks) { - - Comparator sortByX = (sequence1, sequence2) -> (int) (sequence1.getTextPositions().get(0).getPosition()[0] - sequence2.getTextPositions() - .get(0) - .getPosition()[0]); - Comparator sortByY = (sequence1, sequence2) -> (int) (sequence1.getTextPositions().get(0).getPosition()[1] - sequence2.getTextPositions() - .get(0) - .getPosition()[1]); - - return textBlocks.stream().map(TextBlock::getSequences).flatMap(List::stream).sorted(sortByX.thenComparing(sortByY)).toList(); - } - - - private AtomicTextBlock buildAtomicTextBlock(List sequences, DocumentGraphNode parent, Context context) { - - SearchTextWithTextPositionModel searchTextWithTextPositionModel = searchTextWithTextPositionFactory.buildSearchTextToTextPositionModel(sequences); - int offset = context.stringOffset().getAndAdd(searchTextWithTextPositionModel.getSearchText().length()); - - return AtomicTextBlock.builder() - .id(context.textBlockIdx.getAndIncrement()) - .parent(parent) - .searchText(searchTextWithTextPositionModel.getSearchText()) - .lineBreaks(searchTextWithTextPositionModel.getLineBreaks()) - .positions(toRectangle2D(searchTextWithTextPositionModel.getPositions())) - .stringIdxToPositionIdx(searchTextWithTextPositionModel.getStringCoordsToPositionCoords()) - .boundary(new Boundary(offset, offset + searchTextWithTextPositionModel.getSearchText().length())) - .build(); - } - - - private List toRectangle2D(List positions) { - - return positions.stream().map(r -> (Rectangle2D) new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth(), r.getHeight())).toList(); - } - - - private PageNode getPage(int pageIndex, Context context) { - - return context.pages.stream() - .filter(page -> page.getNumber() == pageIndex) - .findFirst() - .orElseThrow(() -> new NotFoundException(format("Page with number %d not found", pageIndex))); - } - - - record Context( - TableOfContents tableOfContents, List pages, List sections, AtomicInteger stringOffset, AtomicLong textBlockIdx) { - - } - -} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentGraphMapper.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentGraphMapper.java index 65df9534..ea8f63dd 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentGraphMapper.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/DocumentGraphMapper.java @@ -1,12 +1,16 @@ package com.iqser.red.service.redaction.v1.server.document.services; + import static java.lang.Math.toIntExact; import static java.lang.String.format; import java.awt.geom.Rectangle2D; -import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.lang3.NotImplementedException; import org.springframework.stereotype.Service; @@ -18,13 +22,15 @@ import com.iqser.red.service.redaction.v1.server.document.data.PageData; import com.iqser.red.service.redaction.v1.server.document.data.TableOfContentsData; import com.iqser.red.service.redaction.v1.server.document.graph.Boundary; import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; -import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.DocumentGraphNode; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ParagraphNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector; import com.iqser.red.service.redaction.v1.server.exception.NotFoundException; @Service @@ -34,22 +40,20 @@ public class DocumentGraphMapper { Context context = new Context(documentData, new TableOfContents(), new LinkedList<>(), new LinkedList<>(), documentData.getAtomicTextBlocks()); - context.pages.addAll(documentData.getPages().stream().map(pageData -> buildPage(pageData, context)).toList()); - buildNodesFromTableOfContents("", context); - DocumentGraph documentGraph= DocumentGraph.builder() - .numberOfPages(documentData.getPages().size()) - .pages(context.pages) - .sections(context.sections) - .tableOfContents(context.tableOfContents) - .build(); - documentGraph.setText(documentGraph.buildTextBlock()); + context.pages.addAll(documentData.getPages().stream().map(this::buildPage).toList()); + + buildNodesFromTableOfContents(Collections.emptyList(), context); + + DocumentGraph documentGraph = DocumentGraph.builder().numberOfPages(documentData.getPages().size()).pages(context.pages).tableOfContents(context.tableOfContents).build(); + documentGraph.setTextBlock(documentGraph.buildTextBlock()); return documentGraph; } - private void buildNodesFromTableOfContents(String currentTocId, Context context) { - List entries; - if(currentTocId.equals("")) { + private void buildNodesFromTableOfContents(List currentTocId, Context context) { + + List entries; + if (currentTocId.size() == 0) { entries = context.documentData().getTableOfContents().getEntries(); } else { entries = context.documentData().getTableOfContents().get(currentTocId).subEntries(); @@ -65,81 +69,59 @@ public class DocumentGraphMapper { } - private void buildSection(TableOfContentsData.EntryData entryData, String currentTocId, Context context) { + private void buildSection(TableOfContentsData.EntryData entryData, List currentTocId, Context context) { - SectionNode section = SectionNode.builder() - .entities(new LinkedList<>()) - .pages(new LinkedList<>()) - .paragraphs(new LinkedList<>()) - .tables(new LinkedList<>()) - .subSections(new LinkedList<>()) - .tableOfContents(context.tableOfContents()) - .numberOnPage(entryData.numberOnPage()) - .build(); + SectionNode section = SectionNode.builder().entities(new HashSet<>()).pages(new HashSet<>()).tableOfContents(context.tableOfContents()).build(); context.sections().add(section); - section.setHeadline(toAtomicTextBlock(context.atomicTextBlockData().get(toIntExact(entryData.atomicTextBlock())), section)); - if (!currentTocId.equals("")) { - SectionNode parent = (SectionNode) context.tableOfContents().getEntryById(currentTocId).node(); - section.setParentSection(parent); - parent.getSubSections().add(section); - } + List pages = Arrays.stream(entryData.pages()).map(pageNumber -> getPage(pageNumber, context)).toList(); + pages.forEach(page -> page.getMainBody().add(section)); + section.getPages().addAll(pages); - PageNode page = getPage(entryData.page(), context); - page.getMainBody().add(section); - section.getPages().add(page); - - String sectionId = context.tableOfContents.createNewEntryAndReturnId(NodeType.SECTION, buildSummary(section.getHeadline()), section); + List sectionId = context.tableOfContents.createNewChildEntryAndReturnId(currentTocId, NodeType.SECTION, section); section.setTocId(sectionId); buildNodesFromTableOfContents(sectionId, context); } - private void buildParagraph(TableOfContentsData.EntryData entryData, String currentTocId, Context context) { + private void buildParagraph(TableOfContentsData.EntryData entryData, List currentTocId, Context context) { + + Set pages = Arrays.stream(entryData.pages()).map(pageNumber -> getPage(pageNumber, context)).collect(Collectors.toSet()); - PageNode page = getPage(entryData.page(), context); SectionNode parentSection = (SectionNode) context.tableOfContents().getEntryById(currentTocId).node(); - ParagraphNode paragraph = ParagraphNode.builder().numberOnPage(entryData.numberOnPage()).page(page).parentSection(parentSection).build(); - AtomicTextBlock atomicTextBlock = toAtomicTextBlock(context.atomicTextBlockData.get(toIntExact(entryData.atomicTextBlock())), paragraph); - paragraph.setAtomicTextBlock(atomicTextBlock); + ParagraphNode paragraph = ParagraphNode.builder().pages(pages).build(); + TextBlock textBlock = toTextBlock(entryData.atomicTextBlocks(), context, paragraph); - if (!page.getMainBody().contains(parentSection)) { - parentSection.getPages().add(page); + paragraph.setTerminalTextBlock(textBlock); + + pages.forEach(page -> page.getMainBody().add(paragraph)); + for (PageNode page : pages) { + if (!parentSection.getPages().contains(page)) { + parentSection.getPages().add(page); + } } - page.getMainBody().add(paragraph); - String tocId = context.tableOfContents.createNewChildEntryAndReturnId(currentTocId, NodeType.PARAGRAPH, buildSummary(atomicTextBlock), paragraph); + List tocId = context.tableOfContents.createNewChildEntryAndReturnId(currentTocId, NodeType.PARAGRAPH, paragraph); paragraph.setTocId(tocId); } - private PageNode buildPage(PageData p, Context context) { + private TextBlock toTextBlock(Long[] atomicTextBlockIds, Context context, SemanticNode parent) { - PageNode page = PageNode.builder().height(p.getHeight()).width(p.getWidth()).number(p.getNumber()).mainBody(new LinkedList<>()).build(); - AtomicTextBlock header = toAtomicTextBlock(context.atomicTextBlockData().get(toIntExact(p.getHeader())), page); - AtomicTextBlock footer = toAtomicTextBlock(context.atomicTextBlockData().get(toIntExact(p.getFooter())), page); - page.setHeader(header); - page.setFooter(footer); - return page; + return Arrays.stream(atomicTextBlockIds) + .map(atomicTextBlockId -> toAtomicTextBlock(context.atomicTextBlockData.get(toIntExact(atomicTextBlockId)), parent)) + .collect(new TextBlockCollector()); } - private static String buildSummary(com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock textBlock) { + private PageNode buildPage(PageData p) { - if (textBlock == null) { - return " probably a table"; - } - - String[] words = textBlock.getFirstLine().toString().split(" "); - int bound = Math.min(words.length, 4); - List list = new ArrayList<>(Arrays.asList(words).subList(0, bound)); - - return String.join(" ", list); + return PageNode.builder().height(p.getHeight()).width(p.getWidth()).number(p.getNumber()).mainBody(new LinkedList<>()).build(); } - private AtomicTextBlock toAtomicTextBlock(AtomicTextBlockData atomicTextBlockData, DocumentGraphNode parent) { + private AtomicTextBlock toAtomicTextBlock(AtomicTextBlockData atomicTextBlockData, SemanticNode parent) { return AtomicTextBlock.builder() .id(atomicTextBlockData.getId()) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityCreationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityCreationService.java new file mode 100644 index 00000000..30e09bcc --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityCreationService.java @@ -0,0 +1,90 @@ +package com.iqser.red.service.redaction.v1.server.document.services; + +import static java.lang.String.format; + +import java.util.Set; + +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.server.document.graph.Boundary; +import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.exception.NotFoundException; +import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EntityCreationService { + + private final EntityEnrichmentService entityEnrichmentService; + + + public EntityNode createAndAddEntity(Boundary boundary, String type, EntityType entityType, DocumentGraph documentGraph) { + + EntityNode entity = EntityNode.initialEntityNode(boundary, type, entityType); + addEntityToGraph(entity, documentGraph); + return entity; + } + + + public EntityNode createAndAddEntity(String entityName, int startOffset, String type, EntityType entityType, DocumentGraph documentGraph) { + + int start = documentGraph.buildTextBlock().indexOf(entityName, startOffset); + if (start == -1) { + throw new NotFoundException(format("Entity %s could no be found!", entityName)); + } + EntityNode entity = EntityNode.initialEntityNode(new Boundary(start, start + entityName.length()), type, entityType); + addEntityToGraph(entity, documentGraph); + return entity; + } + + + public void addEntityToGraph(EntityNode entity, DocumentGraph documentGraph) { + + try { + SemanticNode containingNode = documentGraph.getTableOfContents() + .getEntries() + .stream() + .map(TableOfContents.Entry::node) + .filter(node -> node.buildTextBlock().containsBoundary(entity.getBoundary())) + .findFirst() + .orElseThrow(() -> new NotFoundException("No containing Node found!")); + + containingNode.addThisToEntityIfIntersects(entity); + + TextBlock textBlock = entity.getDeepestFullyContainingNode().buildTextBlock(); + entityEnrichmentService.enrichEntity(entity, textBlock); + + addToPages(entity); + addToNodeEntitySets(entity); + + } catch (NotFoundException e) { + entityEnrichmentService.enrichEntity(entity, documentGraph.getTextBlock()); + log.warn("Entity \"{}\" with {} is in between two main sections and will be removed!", entity.getValue(), entity.getBoundary()); + entity.removeFromGraph(); + } + } + + + private void addToPages(EntityNode entity) { + + Set pages = entity.getDeepestFullyContainingNode().getPages(); + entity.getPages().addAll(pages); + pages.forEach(page -> page.getEntities().add(entity)); + } + + + private void addToNodeEntitySets(EntityNode entity) { + + entity.getIntersectingNodes().forEach(node -> node.getEntities().add(entity)); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityEnrichmentService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityEnrichmentService.java new file mode 100644 index 00000000..8e6717c8 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityEnrichmentService.java @@ -0,0 +1,93 @@ +package com.iqser.red.service.redaction.v1.server.document.services; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class EntityEnrichmentService { + + private final RedactionServiceSettings redactionServiceSettings; + + public EntityNode enrichEntity(EntityNode entity, TextBlock textBlock) { + + entity.setPositions(textBlock.getPositions(entity.getBoundary())); + entity.setTextAfter(findTextAfter(entity.getBoundary().end(), textBlock)); + entity.setTextBefore(findTextBefore(entity.getBoundary().start(), textBlock)); + entity.setValue(textBlock.subSequence(entity.getBoundary()).toString()); + return entity; + } + + + private CharSequence findTextAfter(int index, TextBlock textBlock) { + + int endOffset = Math.min(index + redactionServiceSettings.getSurroundingWordsOffsetWindow(), textBlock.getBoundary().end()); + String textAfter = textBlock.subSequence(index, endOffset).toString(); + if (!textAfter.isBlank()) { + List wordsAfter = splitToWordsAndRemoveEmptyWords(textAfter); + int numberOfWordsAfter = Math.min(wordsAfter.size(), redactionServiceSettings.getNumberOfSurroundingWords()); + if (wordsAfter.size() > 0) { + return concatWordsAfter(wordsAfter.subList(0, numberOfWordsAfter), textAfter.startsWith(" ")); + } + } + return ""; + } + + + + + private CharSequence findTextBefore(int index, TextBlock textBlock) { + + int offsetBefore = Math.max(index - redactionServiceSettings.getSurroundingWordsOffsetWindow(), textBlock.getBoundary().start()); + String textBefore = textBlock.subSequence(offsetBefore, index).toString(); + if (!textBefore.isBlank()) { + List wordsBefore = splitToWordsAndRemoveEmptyWords(textBefore); + int numberOfWordsBefore = Math.min(wordsBefore.size(), redactionServiceSettings.getNumberOfSurroundingWords()); + if (wordsBefore.size() > 0) { + return concatWordsBefore(wordsBefore.subList(wordsBefore.size() - numberOfWordsBefore, wordsBefore.size()), textBefore.endsWith(" ")); + } + } + return ""; + } + + + private static List splitToWordsAndRemoveEmptyWords(String textAfter) { + + return Arrays.stream(textAfter.split(" ")).filter(word -> !Objects.equals("", word)).toList(); + } + + private String concatWordsBefore(List words, boolean endWithSpace) { + + StringBuilder sb = new StringBuilder(); + + + for (String word : words) { + sb.append(word).append(" "); + } + + String result = sb.toString().trim(); + return endWithSpace ? result + " " : result; + } + + private String concatWordsAfter(List words, boolean startWithSpace) { + + StringBuilder sb = new StringBuilder(); + + for (String word: words) { + sb.append(word).append(" "); + } + + String result = sb.toString().trim(); + return startWithSpace ? " " + result : result; + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityEnrichmentUtility.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityEnrichmentUtility.java deleted file mode 100644 index a33bf92c..00000000 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/EntityEnrichmentUtility.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.iqser.red.service.redaction.v1.server.document.services; - -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode; -import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; - -public class EntityEnrichmentUtility { - - public static EntityNode enrichEntity(EntityNode entity, TextBlock textBlock) { - - entity.setPositions(textBlock.getPositions(entity.getBoundary())); - entity.setTextAfter(findTextAfter(entity.getBoundary().end(), textBlock)); - entity.setTextBefore(findTextBefore(entity.getBoundary().start(), textBlock)); - entity.setValue(textBlock.subSequence(entity.getBoundary()).toString()); - return entity; - } - - - private static CharSequence findTextAfter(int index, TextBlock textBlock) { - - int nextLineBreak = textBlock.getNextLinebreak(index); - return textBlock.subSequence(index, nextLineBreak); - } - - - private static CharSequence findTextBefore(int index, TextBlock textBlock) { - - int previousLinebreak = textBlock.getPreviousLinebreak(index); - return textBlock.subSequence(previousLinebreak, index); - } - -} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/ImageSortService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/ImageSortService.java new file mode 100644 index 00000000..00e36092 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/ImageSortService.java @@ -0,0 +1,153 @@ +package com.iqser.red.service.redaction.v1.server.document.services; + +import static java.util.stream.Collectors.groupingBy; + +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.server.classification.model.Document; +import com.iqser.red.service.redaction.v1.server.classification.model.Section; +import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; +import com.iqser.red.service.redaction.v1.server.redaction.model.PdfImage; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; + +@Service +public class ImageSortService { + + public SortedImages sortImagesIntoStructure(Document document) { + + SortedImages sortedImages = new SortedImages(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); + + Map> imagesByPage = document.getSections() + .stream() + .flatMap(section -> section.getImages().stream()) + .distinct() + .collect(groupingBy(PdfImage::getPage)); + + for (int pageNumber : imagesByPage.keySet()) { + List textContainersOnPage = document.getSections() + .stream() + .flatMap(section -> section.getPageBlocks().stream()) + .filter(abstractTextContainer -> abstractTextContainer.getPage() == pageNumber) + .toList(); + + List
sectionsOnPage = document.getSections() + .stream() + .filter(section -> section.getPageBlocks().stream().anyMatch(block -> block.getPage() == pageNumber)) + .toList(); + + for (PdfImage image : imagesByPage.get(pageNumber)) { + sortImage(textContainersOnPage, sectionsOnPage, image, sortedImages); + } + } + return sortedImages; + } + + + private void sortImage(List textContainersOnPage, List
sectionsOnPage, PdfImage image, SortedImages sortedImages) { + + Optional containingTextContainer = getContainingTextContainer(image, textContainersOnPage); + Optional
sectionContainingTextContainer = getContainingSection(image, sectionsOnPage); + List containedTextContainers = getContainedTextContainers(image, textContainersOnPage); + List
containedSections = getContainedSections(image, sectionsOnPage); + if (containingTextContainer.isPresent()) { + if (sortImageIntoTextContainerOrCell(image, sortedImages, containingTextContainer.get())) { + return; + } + } + if (sectionContainingTextContainer.isPresent()) { + + } + } + + + private static boolean sortImageIntoTextContainerOrCell(PdfImage image, SortedImages sortedImages, AbstractTextContainer containingTextContainer) { + + if (containingTextContainer instanceof TextBlock) { + sortedImages.containedInTextContainer().computeIfAbsent(containingTextContainer, sortedImage -> new ArrayList<>()).add(image); + return true; + } + if (containingTextContainer instanceof Table) { + Optional containingCell = getContainingCell((Table) containingTextContainer, image); + if (containingCell.isPresent()) { + sortedImages.containedInCell().computeIfAbsent(containingCell.get(), sortedImage -> new ArrayList<>()).add(image); + return true; + } + } + + return false; + } + + + private static Optional getContainingCell(Table table, PdfImage image) { + + return table.getRows().stream().flatMap(List::stream).filter(cell -> cell.contains(toRectangle2D(image))).findFirst(); + } + + + private List
getContainedSections(PdfImage image, List
sectionsOnPage) { + + return sectionsOnPage.stream() + .filter(section -> toRectangle2D(image).contains(bBoxUnion(section.getPageBlocks().stream().filter(block -> block.getPage() == image.getPage()).toList()))) + .toList(); + } + + + private List getContainedTextContainers(PdfImage image, List textContainersOnPage) { + + return textContainersOnPage.stream().filter(textContainer -> toRectangle2D(image).contains(toRectangle2D(textContainer))).toList(); + } + + + private Optional
getContainingSection(PdfImage image, List
sectionsOnPage) { + + return sectionsOnPage.stream()// + .filter(section -> // + bBoxUnion(section.getPageBlocks().stream().filter(block -> block.getPage() == image.getPage()).toList())// + .contains(toRectangle2D(image))).findFirst(); + } + + + private Optional getContainingTextContainer(PdfImage image, List textContainersOnPage) { + + return textContainersOnPage.stream().filter(textContainer -> toRectangle2D(textContainer).contains(toRectangle2D(image))).findFirst(); + } + + + private static Rectangle2D toRectangle2D(AbstractTextContainer container) { + + return new Rectangle2D.Double(container.getMinX(), container.getMinY(), container.getWidth(), container.getHeight()); + } + + + private static Rectangle2D toRectangle2D(PdfImage image) { + + var redRectangle2D = image.getPosition(); + return new Rectangle2D.Double(redRectangle2D.getX(), redRectangle2D.getY(), redRectangle2D.getWidth(), redRectangle2D.getHeight()); + } + + + private static Rectangle2D bBoxUnion(List blocks) { + + return blocks.stream().map(ImageSortService::toRectangle2D).reduce((a, b) -> a.createUnion(b).getBounds2D()).orElse(new Rectangle2D.Double()); + } + + + public record SortedImages( + Map> containedInCell, + Map> containedInTextContainer, + Map> containedInSection, + Map> containedByImage, + Map> sectionContainedByImage) { + + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/RangeComparators.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/RangeComparators.java deleted file mode 100644 index 52a44734..00000000 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/services/RangeComparators.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.iqser.red.service.redaction.v1.server.document.services; - -import java.util.Comparator; - -import com.iqser.red.service.redaction.v1.server.document.graph.Boundary; - -public class RangeComparators { - - public static Comparator contained() { - - return (range1, range2) -> { - if (contained(range1, range2)) { - return -1; - } else if (contained(range2, range1)) { - return 1; - } else { - return 0; - } - }; - } - - - /** - * @param range1 A Range - * @param range2 Also Range - * @return true, if range1 contains range2 - * false, otherwise - */ - public static boolean contained(Boundary range1, Boundary range2) { - - return range1.start() <= range2.start() && range2.end() <= range1.end(); - } - - public static boolean contained(Boundary range, int index) { - - return range.start() <= index && index < range.end(); - } -} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java index 560d5b2c..2d03f715 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java @@ -1,5 +1,39 @@ package com.iqser.red.service.redaction.v1.server.parsing; +import java.awt.geom.Point2D; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.contentstream.operator.OperatorName; +import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColor; +import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColorN; +import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColorSpace; +import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingDeviceCMYKColor; +import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingDeviceGrayColor; +import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingDeviceRGBColor; +import org.apache.pdfbox.contentstream.operator.color.SetStrokingColor; +import org.apache.pdfbox.contentstream.operator.color.SetStrokingColorN; +import org.apache.pdfbox.contentstream.operator.color.SetStrokingColorSpace; +import org.apache.pdfbox.contentstream.operator.color.SetStrokingDeviceCMYKColor; +import org.apache.pdfbox.contentstream.operator.color.SetStrokingDeviceGrayColor; +import org.apache.pdfbox.contentstream.operator.color.SetStrokingDeviceRGBColor; +import org.apache.pdfbox.contentstream.operator.state.SetFlatness; +import org.apache.pdfbox.contentstream.operator.state.SetLineCapStyle; +import org.apache.pdfbox.contentstream.operator.state.SetLineDashPattern; +import org.apache.pdfbox.contentstream.operator.state.SetLineJoinStyle; +import org.apache.pdfbox.contentstream.operator.state.SetLineMiterLimit; +import org.apache.pdfbox.contentstream.operator.state.SetLineWidth; +import org.apache.pdfbox.contentstream.operator.state.SetRenderingIntent; +import org.apache.pdfbox.contentstream.operator.text.SetFontAndSize; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSNumber; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.text.TextPosition; + import com.iqser.red.service.redaction.v1.server.parsing.model.RedTextPosition; import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Ruling; @@ -8,23 +42,6 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.apache.pdfbox.contentstream.operator.Operator; -import org.apache.pdfbox.contentstream.operator.OperatorName; -import org.apache.pdfbox.contentstream.operator.color.*; -import org.apache.pdfbox.contentstream.operator.state.*; -import org.apache.pdfbox.contentstream.operator.text.SetFontAndSize; -import org.apache.pdfbox.cos.COSBase; -import org.apache.pdfbox.cos.COSNumber; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.text.TextPosition; - -import java.awt.geom.Point2D; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - @Slf4j public class PDFLinesTextStripper extends PDFTextStripper { @@ -264,8 +281,8 @@ public class PDFLinesTextStripper extends PDFTextStripper { // Remove false sequence ends (whitespaces) if (previous != null && sublist.get(0).getYDirAdj() == previous.getYDirAdj() && sublist.get(0) .getXDirAdj() - (previous.getXDirAdj() + previous.getWidthDirAdj()) < 0.01) { - for (TextPosition t : sublist) { - textPositionSequences.get(textPositionSequences.size() - 1).add(t); + for (TextPosition textPosition : sublist) { + textPositionSequences.get(textPositionSequences.size() - 1).add(textPosition); } } else { textPositionSequences.add(new TextPositionSequence(sublist, pageNumber)); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Paragraph.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Paragraph.java index 8be92642..843233b3 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Paragraph.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Paragraph.java @@ -1,5 +1,7 @@ package com.iqser.red.service.redaction.v1.server.redaction.model; +import com.iqser.red.service.redaction.v1.server.document.graph.factory.SearchTextWithTextPositionModel; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PdfImage.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PdfImage.java index bc8de0bc..4ecb3484 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PdfImage.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PdfImage.java @@ -12,7 +12,7 @@ public class PdfImage { private RedRectangle2D position; @NonNull private ImageType imageType; - private boolean isAppendedToParagraph; + private boolean isAppendedToSection; @NonNull private boolean hasTransparency; @NonNull diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SearchTextWithTextPositionFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SearchTextWithTextPositionFactory.java deleted file mode 100644 index 85766ac9..00000000 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SearchTextWithTextPositionFactory.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.iqser.red.service.redaction.v1.server.redaction.service; - -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; - -import org.springframework.stereotype.Service; - -import com.iqser.red.service.redaction.v1.server.parsing.model.RedTextPosition; -import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; -import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D; -import com.iqser.red.service.redaction.v1.server.redaction.model.SearchTextWithTextPositionModel; - -@Service -public class SearchTextWithTextPositionFactory { - - public SearchTextWithTextPositionModel buildSearchTextToTextPositionModel(List sequences) { - - List stringIdxToPositionIdx = new LinkedList<>(); - List lineBreaksStringIdx = new LinkedList<>(); - StringBuilder sb = new StringBuilder(); - - int stringIdx = 0; - int positionIdx = 0; - - String currentUnicode; - String previousUnicode = " "; - - for (TextPositionSequence word : sequences) { - for (int i = 0; i < word.getTextPositions().size(); ++i) { - - currentUnicode = word.getTextPositions().get(i).getUnicode(); - - if (isLineBreak(currentUnicode)) { - lineBreaksStringIdx.add(stringIdx + 1); - } else if (!isRepeatedWhitespace(currentUnicode, previousUnicode) && // - !isHyphenLinebreak(currentUnicode)) { - - sb.append(currentUnicode); - stringIdxToPositionIdx.add(positionIdx); - ++stringIdx; - } - - previousUnicode = currentUnicode; - ++positionIdx; - } - - previousUnicode = " "; - sb.append(previousUnicode); - stringIdxToPositionIdx.add(positionIdx); - ++stringIdx; - } - - assert sb.length() == stringIdxToPositionIdx.size(); - - List positions = sequences.stream()// - .map(TextPositionSequence::getTextPositions)// - .flatMap(List::stream)// - .map(RedTextPosition::getPosition)// - .map(a -> (RedRectangle2D) new RedRectangle2D(a[0], a[1], a[2], a[3])) - .toList(); - - return SearchTextWithTextPositionModel.builder() - .searchText(sb.toString()) - .lineBreaks(lineBreaksStringIdx) - .stringCoordsToPositionCoords(stringIdxToPositionIdx) - .positions(positions) - .build(); - } - - - private boolean isLineBreak(String currentUnicode) { - - return Objects.equals(currentUnicode, "\n"); - } - - - private boolean isRepeatedWhitespace(String currentUnicode, String previousUnicode) { - - return Objects.equals(previousUnicode, " ") && Objects.equals(currentUnicode, " "); - } - - - private boolean isHyphenLinebreak(String unicodeCharacter) { - - return unicodeCharacter.matches("([^\\s\\d\\-]{2,500})[\\-\\u00AD]\\R"); - - } - -} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SurroundingWordsService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SurroundingWordsService.java index 0a2b89bb..2bba58b1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SurroundingWordsService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SurroundingWordsService.java @@ -1,20 +1,16 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; +import java.util.List; +import java.util.Set; + import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; import com.iqser.red.service.redaction.v1.server.redaction.model.SearchableText; -import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; import io.micrometer.core.annotation.Timed; -import lombok.RequiredArgsConstructor; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Set; - @Slf4j @UtilityClass public class SurroundingWordsService { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/SearchImplementation.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/SearchImplementation.java index 4a2b1e7f..b7fa1d84 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/SearchImplementation.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/SearchImplementation.java @@ -91,16 +91,16 @@ public class SearchImplementation { } - public List getMatches(CharSequence text, int startOffset) { + public List getMatches(CharSequence text, Boundary region) { if (this.values.isEmpty()) { return new ArrayList<>(); } - CharSequence subSequence = text.subSequence(startOffset, startOffset + text.length()); + CharSequence subSequence = text.subSequence(region.start(), region.end()); if (this.pattern != null) { - return this.pattern.matcher(subSequence).results().map(r -> new Boundary(r.start() + startOffset, r.end() + startOffset)).collect(Collectors.toList()); + return this.pattern.matcher(subSequence).results().map(r -> new Boundary(r.start() + region.start(), r.end() + region.start())).collect(Collectors.toList()); } else { - return this.trie.parseText(subSequence).stream().map(r -> new Boundary(r.getStart() + startOffset, r.getEnd() + startOffset + 1)).collect(Collectors.toList()); + return this.trie.parseText(subSequence).stream().map(r -> new Boundary(r.getStart() + region.start(), r.getEnd() + region.start() + 1)).collect(Collectors.toList()); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/SectionsBuilderService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/SectionsBuilderService.java index 38cbc513..58e7bab1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/SectionsBuilderService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/SectionsBuilderService.java @@ -3,7 +3,6 @@ package com.iqser.red.service.redaction.v1.server.segmentation; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -80,8 +79,7 @@ public class SectionsBuilderService { previousTable = chunkBlock.getTables().get(chunkBlock.getTables().size() - 1); } } - if (current instanceof Table) { - Table table = (Table) current; + if (current instanceof Table table) { // Distribute header information for subsequent tables mergeTableMetadata(table, previousTable); previousTable = table; @@ -114,46 +112,49 @@ public class SectionsBuilderService { public void addImagesToSections(Document document) { - Map> paragraphMap = new HashMap<>(); - for (Section paragraph : document.getSections()) { - for (AbstractTextContainer container : paragraph.getPageBlocks()) { - - paragraphMap.computeIfAbsent(container.getPage(), c -> new ArrayList<>()).add(paragraph); + Map> sectionMap = new HashMap<>(); + for (Section section : document.getSections()) { + for (AbstractTextContainer container : section.getPageBlocks()) { + List
sectionsOnPage = sectionMap.computeIfAbsent(container.getPage(), c -> new ArrayList<>()); + if (sectionsOnPage.contains(section)) { + continue; + } + sectionsOnPage.add(section); } } - if (paragraphMap.isEmpty()) { - Section paragraph = new Section(); - document.getSections().add(paragraph); - paragraphMap.computeIfAbsent(1, x -> new ArrayList<>()).add(paragraph); + if (sectionMap.isEmpty()) { + Section section = new Section(); + document.getSections().add(section); + sectionMap.computeIfAbsent(1, x -> new ArrayList<>()).add(section); } // first page is always a paragraph, else we can't process pages 1..N, // where N is the first found page with a paragraph - if (paragraphMap.get(1) == null) { - Section paragraph = new Section(); - document.getSections().add(paragraph); - paragraphMap.computeIfAbsent(1, x -> new ArrayList<>()).add(paragraph); + if (sectionMap.get(1) == null) { + Section section = new Section(); + document.getSections().add(section); + sectionMap.computeIfAbsent(1, x -> new ArrayList<>()).add(section); } for (Page page : document.getPages()) { for (PdfImage image : page.getImages()) { - List
paragraphsOnPage = paragraphMap.get(page.getPageNumber()); - if (paragraphsOnPage == null) { + List
sectionsOnPage = sectionMap.get(page.getPageNumber()); + if (sectionsOnPage == null) { int i = page.getPageNumber(); - while (paragraphsOnPage == null) { - paragraphsOnPage = paragraphMap.get(i); + while (sectionsOnPage == null) { + sectionsOnPage = sectionMap.get(i); i--; } } - for (Section paragraph : paragraphsOnPage) { + for (Section section : sectionsOnPage) { Float xMin = null; Float yMin = null; Float xMax = null; Float yMax = null; - for (AbstractTextContainer abs : paragraph.getPageBlocks()) { + for (AbstractTextContainer abs : section.getPageBlocks()) { if (abs.getPage() != page.getPageNumber()) { continue; } @@ -197,14 +198,14 @@ public class SectionsBuilderService { 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) { - paragraph.getImages().add(image); - image.setAppendedToParagraph(true); + section.getImages().add(image); + image.setAppendedToSection(true); } } - if (!image.isAppendedToParagraph()) { + if (!image.isAppendedToSection()) { log.debug("Image uses first paragraph"); - paragraphsOnPage.get(0).getImages().add(image); - image.setAppendedToParagraph(true); + sectionsOnPage.get(0).getImages().add(image); + image.setAppendedToSection(true); } } } @@ -242,35 +243,16 @@ public class SectionsBuilderService { private Section buildTextBlock(List wordBlockList, String lastHeadline) { Section section = new Section(); - TextBlock textBlock = null; - int pageBefore = -1; - boolean splitByTable = false; + for (AbstractTextContainer container : wordBlockList) { + if (container instanceof Table table) { - Iterator itty = wordBlockList.iterator(); - boolean alreadyAdded = false; - AbstractTextContainer previous = null; - while (itty.hasNext()) { - AbstractTextContainer container = itty.next(); - - if (container instanceof Table) { - Table table = (Table) container; - splitByTable = true; - - if (previous != null && previous.getText().startsWith("Table ")) { - table.setHeadline(previous.getText()); + if (lastHeadline == null || lastHeadline.isEmpty()) { + table.setHeadline("Text in table"); } else { - if (lastHeadline == null || lastHeadline.isEmpty()) { - table.setHeadline("Text in table"); - } else { - table.setHeadline("Table in: " + lastHeadline); - } + table.setHeadline("Table in: " + lastHeadline); } - if (textBlock != null && !alreadyAdded) { - section.getPageBlocks().add(textBlock); - alreadyAdded = true; - } section.getPageBlocks().add(table); continue; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/AbstractTextContainer.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/AbstractTextContainer.java index 8c7e169d..1234caf8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/AbstractTextContainer.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/AbstractTextContainer.java @@ -66,4 +66,10 @@ public abstract class AbstractTextContainer { return maxX - minX; } + + public boolean intersectsY(AbstractTextContainer atc) { + + return this.minY <= atc.getMaxY() && this.maxY >= atc.getMinY(); + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractTestWithDictionaries.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractTestWithDictionaries.java index b0cd5dde..d77998a6 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractTestWithDictionaries.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractTestWithDictionaries.java @@ -453,22 +453,23 @@ public class AbstractTestWithDictionaries { @SneakyThrows protected AnalyzeRequest prepareStorage(String file) { - return prepareStorage(file, "files/cv_service_empty_response.json"); + return prepareStorage(file, "files/cv_service_empty_response.json", "files/empty_image_response.json"); } @SneakyThrows - protected AnalyzeRequest prepareStorage(String file, String cvServiceResponseFile) { + protected AnalyzeRequest prepareStorage(String file, String cvServiceResponseFile, String imageInfoFile) { ClassPathResource pdfFileResource = new ClassPathResource(file); ClassPathResource cvServiceResponseFileResource = new ClassPathResource(cvServiceResponseFile); + ClassPathResource imageInfoFileResource = new ClassPathResource(imageInfoFile); - return prepareStorage(pdfFileResource.getInputStream(), cvServiceResponseFileResource.getInputStream()); + return prepareStorage(pdfFileResource.getInputStream(), cvServiceResponseFileResource.getInputStream(), imageInfoFileResource.getInputStream()); } @SneakyThrows - protected AnalyzeRequest prepareStorage(InputStream fileStream, InputStream cvServiceResponseFileStream) { + protected AnalyzeRequest prepareStorage(InputStream fileStream, InputStream cvServiceResponseFileStream, InputStream imageInfoStream) { AnalyzeRequest request = AnalyzeRequest.builder() .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) @@ -477,6 +478,7 @@ public class AbstractTestWithDictionaries { .lastProcessed(OffsetDateTime.now()) .build(); + storageService.storeObject(RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMAGE_INFO), imageInfoStream); storageService.storeObject(RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.TABLES), cvServiceResponseFileStream); storageService.storeObject(RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.ORIGIN), fileStream); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphIntegrationTest.java index 55506847..a7808274 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphIntegrationTest.java @@ -3,15 +3,11 @@ package com.iqser.red.service.redaction.v1.server; import static com.iqser.red.service.redaction.v1.server.utils.PdfDraw.drawRectangle2DList; import java.awt.Color; -import java.awt.Shape; -import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.io.File; import java.io.IOException; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Set; import org.apache.pdfbox.pdmodel.PDDocument; import org.junit.jupiter.api.Test; @@ -22,20 +18,20 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.io.ClassPathResource; import com.iqser.red.service.redaction.v1.model.FileAttribute; -import com.iqser.red.service.redaction.v1.server.document.data.DocumentData; import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; +import com.iqser.red.service.redaction.v1.server.document.graph.factory.DocumentGraphFactory; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; -import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ParagraphNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; -import com.iqser.red.service.redaction.v1.server.document.services.DocumentDataMapper; -import com.iqser.red.service.redaction.v1.server.document.services.DocumentGraphFactory; +import com.iqser.red.service.redaction.v1.server.document.services.EntityCreationService; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType; import com.iqser.red.service.redaction.v1.server.redaction.service.DictionaryService; import com.iqser.red.service.redaction.v1.server.redaction.utils.SearchImplementation; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; +import com.iqser.red.service.redaction.v1.server.utils.PdfDraw; import lombok.SneakyThrows; @@ -51,7 +47,7 @@ public class DocumentGraphIntegrationTest extends AbstractTestWithDictionaries { private DictionaryService dictionaryService; @Autowired - private DocumentDataMapper documentDataMapper; + private EntityCreationService entityCreationService; @Qualifier("kieContainer") @Autowired @@ -77,18 +73,21 @@ public class DocumentGraphIntegrationTest extends AbstractTestWithDictionaries { List foundEntities = new LinkedList<>(); for (var model : dictionary.getDictionaryModels()) { - findEntitiesWithSearchImplementationAndAddToGraph(document, model.getEntriesSearch(), EntityType.ENTITY, foundEntities, model.getType()); - findEntitiesWithSearchImplementationAndAddToGraph(document, model.getFalsePositiveSearch(), EntityType.FALSE_POSITIVE, foundEntities, model.getType()); - findEntitiesWithSearchImplementationAndAddToGraph(document, model.getFalseRecommendationsSearch(), EntityType.FALSE_RECOMMENDATION, foundEntities, model.getType()); + findEntitiesWithSearchImplementation(document, model.getEntriesSearch(), EntityType.ENTITY, foundEntities, model.getType()); + findEntitiesWithSearchImplementation(document, model.getFalsePositiveSearch(), EntityType.FALSE_POSITIVE, foundEntities, model.getType()); + findEntitiesWithSearchImplementation(document, model.getFalseRecommendationsSearch(), EntityType.FALSE_RECOMMENDATION, foundEntities, model.getType()); + foundEntities.forEach(entity -> entityCreationService.addEntityToGraph(entity, document)); } KieSession kieSession = kieContainer.newKieSession(); kieSession.setGlobal("document", document); + document.getEntities().forEach(kieSession::insert); - document.getSections().forEach(kieSession::insert); + document.getTableOfContents().streamEntriesInOrder().forEach(entry -> kieSession.insert(entry.node())); document.getPages().forEach(kieSession::insert); - document.getSections().forEach(sec -> sec.getParagraphs().forEach(kieSession::insert)); + kieSession.insert(FileAttribute.builder().label("Vertebrate Study").value("Yes").build()); + kieSession.fireAllRules(); drawAllEntities(filename, fileResource, document); @@ -109,12 +108,12 @@ public class DocumentGraphIntegrationTest extends AbstractTestWithDictionaries { DocumentGraph document = documentGraphFactory.buildDocumentGraph(classifiedDoc); var start = System.currentTimeMillis(); for (int i = 0; i < n; i++) { - TextBlock textBlock = document.getText(); + TextBlock textBlock = document.getTextBlock(); } float durationMillis = ((float) (System.currentTimeMillis() - start)); System.out.printf("%d calls of buildTextBlock() on document took %f s, average is %f ms\n", n, durationMillis / 1000, durationMillis / n); - SectionNode section = document.getSections().get(8); + SectionNode section = document.getMainSections().get(8); start = System.currentTimeMillis(); for (int i = 0; i < n; i++) { section.buildTextBlock(); @@ -122,7 +121,7 @@ public class DocumentGraphIntegrationTest extends AbstractTestWithDictionaries { durationMillis = ((float) (System.currentTimeMillis() - start)); System.out.printf("%d calls of buildTextBlock() on section took %f s, average is %f ms\n", n, durationMillis / 1000, durationMillis / n); - ParagraphNode paragraph = document.getSections().get(8).getParagraphs().get(1); + SemanticNode paragraph = document.getTableOfContents().getEntryById(List.of(8, 1)).node(); start = System.currentTimeMillis(); for (int i = 0; i < n; i++) { paragraph.buildTextBlock(); @@ -147,34 +146,42 @@ public class DocumentGraphIntegrationTest extends AbstractTestWithDictionaries { dictionaryService.updateDictionary(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID); Dictionary dictionary = dictionaryService.getDeepCopyDictionary(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID); - int numberOfSearches = 25; - float totalTime = 0; + int numberOfRuns = 25; + float totalSearchTime = 0; float totalGraphTime = 0; + float totalInsertTime = 0; List foundEntities = new LinkedList<>(); - for (int i = 0; i < numberOfSearches; i++) { + for (int i = 0; i < numberOfRuns; i++) { + foundEntities = new LinkedList<>(); var graphStart = System.currentTimeMillis(); document = documentGraphFactory.buildDocumentGraph(classifiedDoc); - var graphTime = ((float) (System.currentTimeMillis() - graphStart)) / 1000; + var graphTime = System.currentTimeMillis() - graphStart; totalGraphTime += graphTime; - var start = System.currentTimeMillis(); - foundEntities = new LinkedList<>(); + var searchStart = System.currentTimeMillis(); for (var model : dictionary.getDictionaryModels()) { - findEntitiesWithSearchImplementationAndAddToGraph(document, model.getEntriesSearch(), EntityType.ENTITY, foundEntities, model.getType()); - findEntitiesWithSearchImplementationAndAddToGraph(document, model.getFalsePositiveSearch(), EntityType.FALSE_POSITIVE, foundEntities, model.getType()); - findEntitiesWithSearchImplementationAndAddToGraph(document, model.getFalseRecommendationsSearch(), EntityType.FALSE_RECOMMENDATION, foundEntities, model.getType()); + findEntitiesWithSearchImplementation(document, model.getEntriesSearch(), EntityType.ENTITY, foundEntities, model.getType()); + findEntitiesWithSearchImplementation(document, model.getFalsePositiveSearch(), EntityType.FALSE_POSITIVE, foundEntities, model.getType()); + findEntitiesWithSearchImplementation(document, model.getFalseRecommendationsSearch(), EntityType.FALSE_RECOMMENDATION, foundEntities, model.getType()); } - var time = ((float) (System.currentTimeMillis() - start)) / 1000; - totalTime += time; - System.out.printf("%d Search %fs; Graph construction %fs \n", i, time, graphTime); + var searchTime = System.currentTimeMillis() - searchStart; + totalSearchTime += searchTime; + + var insertStart = System.currentTimeMillis(); + DocumentGraph finalDocument = document; + foundEntities.forEach(entity -> entityCreationService.addEntityToGraph(entity, finalDocument)); + var insertTime = System.currentTimeMillis() - insertStart; + totalInsertTime += insertTime; + + System.out.printf("%d Graph construction %d ms, Search %d ms, Insertion %d ms for %d entities\n", i, graphTime, searchTime, insertTime, document.getEntities().size()); } - System.out.printf("%d Searches took %f s, average %f s\n", numberOfSearches, totalTime, totalTime / numberOfSearches); - System.out.printf("%d Graph constructions took %f s, average %f s\n", numberOfSearches, totalGraphTime, totalGraphTime / numberOfSearches); - Set distinctFoundEntities = new HashSet<>(foundEntities); - System.out.printf("Found %d entities and saved %d\n", distinctFoundEntities.size(), document.getEntities().size()); - //assert document.getEntities().size() == distinctFoundEntities.size(); + System.out.printf("%d Graph constructions took %s s, average %.2f ms\n", numberOfRuns, ((float) totalGraphTime / 1000), totalGraphTime / numberOfRuns); + System.out.printf("%d Searches took %s s, average %.2f ms\n", numberOfRuns, ((float) totalSearchTime / 1000), totalSearchTime / numberOfRuns); + System.out.printf("%d Insertions took %s s, average %.2f ms\n", numberOfRuns, ((float) totalInsertTime / 1000), totalInsertTime / numberOfRuns); + System.out.printf("Found %d entities and saved %d\n", foundEntities.size(), document.getEntities().size()); + assert document.getEntities().size() == foundEntities.size(); drawAllEntities(filename, fileResource, document); } @@ -187,31 +194,27 @@ public class DocumentGraphIntegrationTest extends AbstractTestWithDictionaries { PDDocument pdDocument = PDDocument.load(fileStream); for (PageNode page : document.getPages()) { - AffineTransform mirrorY = new AffineTransform(1, 0, 0, -1, 0, page.getHeight() + 6); List entityPositionsOnPage = page.getEntities() .stream() - .filter(EntityNode::isRedact) + .filter(EntityNode::isRedaction) .map(EntityNode::getPositions) .flatMap(List::stream) - .map(mirrorY::createTransformedShape) - .map(Shape::getBounds2D) .toList(); - drawRectangle2DList(pdDocument, page.getNumber(), entityPositionsOnPage, Color.BLACK); + PdfDraw.Options options = PdfDraw.Options.builder().strokeColor(Color.BLACK).stroke(true).build(); + drawRectangle2DList(pdDocument, page.getNumber(), entityPositionsOnPage, options); } for (PageNode page : document.getPages()) { - AffineTransform mirrorY = new AffineTransform(1, 0, 0, -1, 0, page.getHeight() + 6); List entityPositionsOnPage = page.getEntities() .stream() - .filter(e -> !e.isRedact()) + .filter(e -> !e.isRedaction()) .map(EntityNode::getPositions) .flatMap(List::stream) - .map(mirrorY::createTransformedShape) - .map(Shape::getBounds2D) .toList(); - drawRectangle2DList(pdDocument, page.getNumber(), entityPositionsOnPage, Color.BLUE); + PdfDraw.Options options = PdfDraw.Options.builder().strokeColor(Color.BLUE).stroke(true).build(); + drawRectangle2DList(pdDocument, page.getNumber(), entityPositionsOnPage, options); } File outputFile = new File(tmpFileName); pdDocument.save(outputFile); @@ -220,39 +223,16 @@ public class DocumentGraphIntegrationTest extends AbstractTestWithDictionaries { } - private void findEntitiesWithSearchImplementationAndAddToGraph(DocumentGraph documentGraph, - SearchImplementation searchImplementation, - EntityType entityType, - List foundEntities, - String type) { + private void findEntitiesWithSearchImplementation(DocumentGraph documentGraph, + SearchImplementation searchImplementation, + EntityType entityType, + List foundEntities, + String type) { - documentGraph.getSections().forEach( section -> { - TextBlock textBlock = section.buildTextBlock(); - searchImplementation.getMatches(textBlock, textBlock.getBoundary().start()) + TextBlock textBlock = documentGraph.getTextBlock(); + searchImplementation.getMatches(textBlock, textBlock.getBoundary()) .stream() - .map(bounds -> section.createAndAddEntity(bounds, type, entityType)) + .map(bounds -> EntityNode.initialEntityNode(bounds, type, entityType)) .forEach(foundEntities::add); - }); - documentGraph.getPages().forEach( page -> { - TextBlock textBlock = page.getHeader(); - searchImplementation.getMatches(textBlock, textBlock.getBoundary().start()) - .stream() - .map(bounds -> page.createAndAddEntity(bounds, type, entityType)) - .forEach(foundEntities::add); - }); - documentGraph.getPages().forEach( page -> { - TextBlock textBlock = page.getFooter(); - searchImplementation.getMatches(textBlock, textBlock.getBoundary().start()) - .stream() - .map(bounds -> page.createAndAddEntity(bounds, type, entityType)) - .forEach(foundEntities::add); - }); - /* - searchImplementation.getMatches(documentGraph.getText()) - .stream() - .map(pos -> documentGraph.createAndAddEntity(new Boundary(pos.startIndex(), pos.endIndex()), type, entityType)) - .forEach(foundEntities::add); - - */ } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphMappingTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphMappingTest.java similarity index 92% rename from redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphMappingTest.java rename to redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphMappingTest.java index 653af2dc..887d430b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphMappingTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphMappingTest.java @@ -1,13 +1,13 @@ -package com.iqser.red.service.redaction.v1.server; +package com.iqser.red.service.redaction.v1.server.document.graph; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; +import com.iqser.red.service.redaction.v1.server.AbstractTestWithDictionaries; import com.iqser.red.service.redaction.v1.server.document.data.DocumentData; -import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; import com.iqser.red.service.redaction.v1.server.document.services.DocumentDataMapper; -import com.iqser.red.service.redaction.v1.server.document.services.DocumentGraphFactory; +import com.iqser.red.service.redaction.v1.server.document.graph.factory.DocumentGraphFactory; import com.iqser.red.service.redaction.v1.server.document.services.DocumentGraphMapper; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphTest.java new file mode 100644 index 00000000..7aa05a20 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphTest.java @@ -0,0 +1,308 @@ +package com.iqser.red.service.redaction.v1.server.document.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.wildfly.common.Assert.assertTrue; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; + +import com.iqser.red.service.redaction.v1.server.AbstractTestWithDictionaries; +import com.iqser.red.service.redaction.v1.server.classification.model.Document; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.HeadlineNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ParagraphNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableCellNode; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableNode; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.factory.DocumentGraphFactory; +import com.iqser.red.service.redaction.v1.server.document.services.EntityCreationService; +import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType; +import com.iqser.red.service.redaction.v1.server.redaction.model.PdfImage; +import com.iqser.red.service.redaction.v1.server.segmentation.ImageService; +import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; + +import lombok.SneakyThrows; + +public class DocumentGraphTest extends AbstractTestWithDictionaries { + + @Autowired + private DocumentGraphFactory documentGraphFactory; + + @Autowired + private PdfSegmentationService segmentationService; + + @Autowired + private EntityCreationService entityCreationService; + + @Autowired + private ImageService imageService; + + + @Test + public void assertTextBeforeAndTextAfterForParagraphCrafted() { + + DocumentGraph documentGraph = buildGraph("files/new/crafted document"); + String searchTerm = "Clarissa"; + int start = documentGraph.getTextBlock().indexOf(searchTerm); + assert start != -1; + + Boundary boundary = new Boundary(start, start + searchTerm.length()); + EntityNode entityNode = entityCreationService.createAndAddEntity(boundary, "CBI_author", EntityType.ENTITY, documentGraph); + + assertEquals("Expand to Hint ", entityNode.getTextBefore()); + assertEquals("’s Donut ←", entityNode.getTextAfter()); + assertEquals(searchTerm, entityNode.getValue()); + assertEquals("Rule 5: Do not redact genitive CBI_authors (Entries based on Dict) ", + entityNode.getDeepestFullyContainingNode().getHeadline().buildTextBlock().getSearchText()); + assertEquals(2, entityNode.getIntersectingNodes().size()); + assertEquals(5, entityNode.getDeepestFullyContainingNode().getNumberOnPage()); + assertInstanceOf(ParagraphNode.class, entityNode.getDeepestFullyContainingNode()); + + assertSameOffsetInAllIntersectingNodes(searchTerm, start, entityNode); + } + + + @Test + public void assertTextBeforeAndTextAfterForHeadlineCrafted() { + + DocumentGraph documentGraph = buildGraph("files/new/crafted document"); + String searchTerm = "Rule 39:"; + int start = documentGraph.getTextBlock().indexOf(searchTerm); + assert start != -1; + + Boundary boundary = new Boundary(start, start + searchTerm.length()); + EntityNode entityNode = entityCreationService.createAndAddEntity(boundary, "CBI_author", EntityType.ENTITY, documentGraph); + + assertEquals("", entityNode.getTextBefore()); + assertEquals(" Purity Hint", entityNode.getTextAfter()); + assertEquals(searchTerm, entityNode.getValue()); + assertEquals("Rule 39: Purity Hint ", entityNode.getDeepestFullyContainingNode().getHeadline().buildTextBlock().getSearchText()); + assertEquals(2, entityNode.getIntersectingNodes().size()); + assertEquals(6, entityNode.getDeepestFullyContainingNode().getNumberOnPage()); + assertInstanceOf(HeadlineNode.class, entityNode.getDeepestFullyContainingNode()); + + assertSameOffsetInAllIntersectingNodes(searchTerm, start, entityNode); + } + + + @Test + public void assertTextBeforeAndTextAfterForTableCellCrafted() { + + DocumentGraph documentGraph = buildGraph("files/new/crafted document"); + String searchTerm = "1998"; + int start = documentGraph.getTextBlock().indexOf(searchTerm); + assert start != -1; + + Boundary boundary = new Boundary(start, start + searchTerm.length()); + EntityNode entityNode = entityCreationService.createAndAddEntity(boundary, "CBI_author", EntityType.ENTITY, documentGraph); + + assertEquals("", entityNode.getTextBefore()); + assertEquals("", entityNode.getTextAfter()); + assertEquals(searchTerm, entityNode.getValue()); + assertEquals("Rule 6-11 (Authors Table) ", entityNode.getDeepestFullyContainingNode().getHeadline().buildTextBlock().getSearchText()); + assertEquals(3, entityNode.getIntersectingNodes().size()); + assertEquals(15, entityNode.getDeepestFullyContainingNode().getNumberOnPage()); + assertInstanceOf(TableCellNode.class, entityNode.getDeepestFullyContainingNode()); + + assertSameOffsetInAllIntersectingNodes(searchTerm, start, entityNode); + } + + + @Test + public void findAndCheckMultipleSearchTermsCrafted() { + + DocumentGraph documentGraph = buildGraph("files/new/crafted document"); + assertValueAndPageAndIntersectingNodes(documentGraph, "David", 1); + assertValueAndPageAndIntersectingNodes(documentGraph, "Weyland Industries", 2); + assertValueAndPageAndIntersectingNodes(documentGraph, "Desiree", 3); + assertValueAndPageAndIntersectingNodes(documentGraph, "kawasaki@me.com", 4); + assertValueAndPageAndIntersectingNodes(documentGraph, "Central Research Industry", 5); + } + + + @Test + public void assertTableStructure() { + + DocumentGraph documentGraph = buildGraph("files/new/crafted document"); + TableNode table = (TableNode) documentGraph.getTableOfContents()// + .streamEntriesInOrder()// + .filter(entry -> entry.type().equals(NodeType.TABLE))// + .map(TableOfContents.Entry::node)// + .findFirst().orElseThrow(); + assertEquals(5, table.getNumberOfCols()); + assertEquals(4, table.getNumberOfRows()); + assertEquals(5, table.streamHeaders().toList().size()); + CharSequence firstHeader = table.streamHeadersForCell(1, 1).map(TableCellNode::buildTextBlock).map(TextBlock::getSearchText).findFirst().orElseThrow(); + assertEquals("Author(s)", firstHeader.toString().stripTrailing()); + } + + + @Test + public void findAndCheckMultipleSearchTermsMetolachlor() { + + DocumentGraph documentGraph = buildGraph("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06"); + assertValueAndPageAndIntersectingNodes(documentGraph, "sideeffects", 4); + assertValueAndPageAndIntersectingNodes(documentGraph, "Commission Regulation", 9); + assertValueAndPageAndIntersectingNodes(documentGraph, "Pre-emergence", 15); + assertValueAndPageAndIntersectingNodes(documentGraph, "LiChrosorb CN +", 22); + assertValueAndPageAndIntersectingNodes(documentGraph, "RCC856132", 22); + assertValueAndPageAndIntersectingNodes(documentGraph, "Number of references included", 33); + } + + + @Test + public void assertTableStructureMetolachlor() { + + DocumentGraph documentGraph = buildGraph("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06"); + TableNode table = (TableNode) documentGraph.getTableOfContents() + .streamEntriesInOrder() + .filter(entry -> entry.node().getPages().stream().anyMatch(page -> page.getNumber() == 22)) + .filter(entry -> entry.type().equals(NodeType.TABLE)) + .map(TableOfContents.Entry::node) + .findFirst() + .orElseThrow(); + assertEquals(5, table.getNumberOfCols()); + assertEquals(14, table.getNumberOfRows()); + assertEquals(10, table.streamHeaders().toList().size()); + List twoHeaders = table.streamHeadersForCell(2, 1).map(TableCellNode::buildTextBlock).map(TextBlock::getSearchText).toList(); + assertEquals(2, twoHeaders.size()); + assertEquals("Component of residue definition: S-Metolachlor", twoHeaders.get(0).stripTrailing()); + assertEquals("Method type", twoHeaders.get(1).stripTrailing()); + } + + + @Test + public void assertTextBeforeAndTextAfterForParagraphMetolachlor() { + + DocumentGraph documentGraph = buildGraph("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06"); + String searchTerm = "Cucurbit"; + int start = documentGraph.getTextBlock().indexOf(searchTerm); + assert start != -1; + + Boundary boundary = new Boundary(start, start + searchTerm.length()); + EntityNode entityNode = entityCreationService.createAndAddEntity(boundary, "CBI_author", EntityType.ENTITY, documentGraph); + + assertEquals("except Cranberry; Vegetable, ", entityNode.getTextBefore()); + assertEquals(", Group 9;", entityNode.getTextAfter()); + assertEquals("1.1.4 Evaluations carried out under other regulatory contexts ", entityNode.getDeepestFullyContainingNode().getHeadline().buildTextBlock().getSearchText()); + assertEquals(searchTerm, entityNode.getValue()); + assertEquals(2, entityNode.getIntersectingNodes().size()); + assertEquals(5, entityNode.getDeepestFullyContainingNode().getNumberOnPage()); + assertInstanceOf(ParagraphNode.class, entityNode.getDeepestFullyContainingNode()); + + assertSameOffsetInAllIntersectingNodes(searchTerm, start, entityNode); + } + + + @Test + public void assertTextBeforeAndTextAfterForHeadlineMetolachlor() { + + DocumentGraph documentGraph = buildGraph("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06"); + String searchTerm = "absorption, distribution, metabolism"; + int start = documentGraph.getTextBlock().indexOf(searchTerm); + assert start != -1; + start = documentGraph.getTextBlock().indexOf(searchTerm, start + 1); + assert start != -1; + + Boundary boundary = new Boundary(start, start + searchTerm.length()); + EntityNode entityNode = entityCreationService.createAndAddEntity(boundary, "CBI_author", EntityType.ENTITY, documentGraph); + + assertEquals("2.6.1 Summary of ", entityNode.getTextBefore()); + assertEquals(" and excretion in", entityNode.getTextAfter()); + assertEquals("2.6.1 Summary of absorption, distribution, metabolism and excretion in mammals ", + entityNode.getDeepestFullyContainingNode().getHeadline().buildTextBlock().getSearchText()); + assertEquals(searchTerm, entityNode.getValue()); + assertEquals(2, entityNode.getIntersectingNodes().size()); + assertEquals(6, entityNode.getDeepestFullyContainingNode().getNumberOnPage()); + assertInstanceOf(HeadlineNode.class, entityNode.getDeepestFullyContainingNode()); + + assertSameOffsetInAllIntersectingNodes(searchTerm, start, entityNode); + } + + + @Test + public void assertTextBeforeAndTextAfterForTableCellMetolachlor() { + + DocumentGraph documentGraph = buildGraph("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06"); + String searchTerm = "N-deacetylation product"; + int start = documentGraph.getTextBlock().indexOf(searchTerm); + assert start != -1; + + Boundary boundary = new Boundary(start, start + searchTerm.length()); + EntityNode entityNode = entityCreationService.createAndAddEntity(boundary, "CBI_author", EntityType.ENTITY, documentGraph); + + assertEquals("2-[(2-(1-hydroxy-ethyl)-6methyl-phenyl-amino]propan-1-ol (", entityNode.getTextBefore()); + assertEquals(" of metabolite of", entityNode.getTextAfter()); + assertEquals(searchTerm, entityNode.getValue()); + assertEquals(3, entityNode.getIntersectingNodes().size()); + assertEquals("2.7.2 Summary of metabolism, distribution and expression of residues in plants, poultry, lactating ruminants, pigs and fish ", + entityNode.getDeepestFullyContainingNode().getHeadline().buildTextBlock().getSearchText()); + + // TODO: the number below is odd, due the cell (1, 2) containing 4 AtomicTextBlocks, even though it is an image. This should be fixed during structure analysis. + // In general, numberOnPage needs some more thought. + assertEquals(25, entityNode.getDeepestFullyContainingNode().getNumberOnPage()); + + assertInstanceOf(TableCellNode.class, entityNode.getDeepestFullyContainingNode()); + + assertSameOffsetInAllIntersectingNodes(searchTerm, start, entityNode); + } + + + @SneakyThrows + protected DocumentGraph buildGraph(String filename) { + + if (filename.equals("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06")) { + prepareStorage(filename + ".pdf", "files/cv_service_empty_response.json", filename + ".IMAGE_INFO.json"); + } else { + prepareStorage(filename + ".pdf"); + } + ClassPathResource fileResource = new ClassPathResource(filename + ".pdf"); + + try (InputStream inputStream = fileResource.getInputStream()) { + Map> pdfImages = imageService.convertImages(TEST_DOSSIER_ID, TEST_FILE_ID); + Document classifiedDoc = segmentationService.parseDocument(TEST_DOSSIER_ID, TEST_FILE_ID, inputStream, pdfImages); + return documentGraphFactory.buildDocumentGraph(classifiedDoc); + } + } + + + private static void assertSameOffsetInAllIntersectingNodes(String searchTerm, int start, EntityNode entityNode) { + + List paragraphStart = entityNode.getIntersectingNodes().stream()// + .map(SemanticNode::buildTextBlock)// + .map(textBlock -> textBlock.indexOf(searchTerm))// + .toList(); + + paragraphStart.forEach(nodeStart -> assertEquals(start, nodeStart)); + } + + + private void assertValueAndPageAndIntersectingNodes(DocumentGraph documentGraph, String searchTerm, int pageNumber) { + + int start = documentGraph.getTextBlock().indexOf(searchTerm); + + assert start != -1; + + Boundary boundary = new Boundary(start, start + searchTerm.length()); + EntityNode entityNode = entityCreationService.createAndAddEntity(boundary, "CBI_author", EntityType.ENTITY, documentGraph); + PageNode pageNode = documentGraph.getPages().get(pageNumber - 1); + + assertEquals(entityNode.getValue(), searchTerm); + assertTrue(pageNode.getEntities().contains(entityNode)); + assertTrue(documentGraph.getPages().stream().filter(page -> page != pageNode).noneMatch(page -> page.getEntities().contains(entityNode))); + assertTrue(entityNode.getPages().contains(pageNode)); + assertTrue(entityNode.getPositions().size() >= searchTerm.replace(" ", "").length()); + assertSameOffsetInAllIntersectingNodes(searchTerm, start, entityNode); + assertTrue(entityNode.getIntersectingNodes().stream().allMatch(node -> node.getEntities().contains(entityNode))); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphVisualizationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphVisualizationTest.java new file mode 100644 index 00000000..3b99cbc2 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphVisualizationTest.java @@ -0,0 +1,59 @@ +package com.iqser.red.service.redaction.v1.server.document.graph; + +import java.awt.Color; +import java.io.File; +import java.io.IOException; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.utils.PdfDraw; + +import lombok.SneakyThrows; + +public class DocumentGraphVisualizationTest extends DocumentGraphTest { + + @Test + @SneakyThrows + public void visualizeMetolachlor() { + + String filename = "files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06"; + + DocumentGraph documentGraph = buildGraph(filename); + TextBlock textBlock = documentGraph.buildTextBlock(); + + visualizeSemanticNodes(filename, documentGraph, textBlock); + } + + + @Test + @SneakyThrows + public void visualizeRotatedTestDocument() { + + String filename = "files/new/RotateTestFileWithImages"; + + DocumentGraph documentGraph = buildGraph(filename); + TextBlock textBlock = documentGraph.buildTextBlock(); + + visualizeSemanticNodes(filename, documentGraph, textBlock); + } + + + private static void visualizeSemanticNodes(String filename, DocumentGraph documentGraph, TextBlock textBlock) throws IOException { + + var tmpFileName = "/tmp/" + filename.split("/")[2] + "_SEMANTIC_NODES_BBOX.pdf"; + var fileResource = new ClassPathResource(filename + ".pdf"); + + try (var fileStream = fileResource.getInputStream()) { + PDDocument pdDocument = PDDocument.load(fileStream); + PdfDraw.drawDocumentGraph(pdDocument, documentGraph); + PdfDraw.drawTextBlock(pdDocument, textBlock, PdfDraw.Options.builder().stroke(true).strokeWidth(0.1f).strokeColor(Color.YELLOW).build()); + File outputFile = new File(tmpFileName); + pdDocument.save(outputFile); + pdDocument.close(); + } + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/ImageSortServiceTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/ImageSortServiceTest.java new file mode 100644 index 00000000..72ab57b3 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/ImageSortServiceTest.java @@ -0,0 +1,60 @@ +package com.iqser.red.service.redaction.v1.server.document.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; + +import com.iqser.red.service.redaction.v1.server.AbstractTestWithDictionaries; +import com.iqser.red.service.redaction.v1.server.classification.model.Document; +import com.iqser.red.service.redaction.v1.server.document.services.ImageSortService; +import com.iqser.red.service.redaction.v1.server.redaction.model.PdfImage; +import com.iqser.red.service.redaction.v1.server.segmentation.ImageService; +import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; + +import lombok.SneakyThrows; + +public class ImageSortServiceTest extends AbstractTestWithDictionaries { + + @Autowired + private ImageSortService imageSortService; + + @Autowired + private PdfSegmentationService segmentationService; + + @Autowired + private ImageService imageService; + + + @Test + public void assertImagesContainedInCells() { + + ImageSortService.SortedImages sortedImages = getSortedImages("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06"); + assertEquals(9, sortedImages.containedInCell().size()); + } + + + @SneakyThrows + private ImageSortService.SortedImages getSortedImages(String filename) { + + if (filename.equals("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06")) { + prepareStorage(filename + ".pdf", "files/cv_service_empty_response.json", filename + ".IMAGE_INFO.json"); + } else { + prepareStorage(filename + ".pdf"); + } + ClassPathResource fileResource = new ClassPathResource(filename + ".pdf"); + + try (InputStream inputStream = fileResource.getInputStream()) { + Map> pdfImages = imageService.convertImages(TEST_DOSSIER_ID, TEST_FILE_ID); + Document classifiedDoc = segmentationService.parseDocument(TEST_DOSSIER_ID, TEST_FILE_ID, inputStream, pdfImages); + return imageSortService.sortImagesIntoStructure(classifiedDoc); + } + + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/utils/PdfDraw.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/utils/PdfDraw.java index f5115f84..95fbc5ac 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/utils/PdfDraw.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/utils/PdfDraw.java @@ -1,27 +1,157 @@ package com.iqser.red.service.redaction.v1.server.utils; import java.awt.Color; +import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.List; +import java.util.Map; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; +import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; +import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.document.graph.factory.RectangleTransformations; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.SneakyThrows; +import lombok.experimental.FieldDefaults; import lombok.experimental.UtilityClass; @UtilityClass public class PdfDraw { + public static void drawDocumentGraph(PDDocument document, DocumentGraph documentGraph) { + + documentGraph.getTableOfContents().streamEntriesInOrder().forEach(entry -> drawNode(document, entry)); + } + + + public static void drawNode(PDDocument document, TableOfContents.Entry entry) { + + Options options = buildStandardOptionsForNodes(entry); + + drawBBoxAndLabelAndNumberOnPage(document, entry, options); + + } + + + public static void drawTextBlock(PDDocument document, TextBlock textBlock, Options options) { + + textBlock.getAtomicTextBlocks().forEach(atb -> drawAtomicTextBlock(document, atb, options)); + } + + + public static void drawAtomicTextBlock(PDDocument document, AtomicTextBlock atomicTextBlock, Options options) { + + drawRectangle2DList(document, atomicTextBlock.getPage().getNumber(), atomicTextBlock.getPositions().stream().toList(), options); + + } + + @SneakyThrows - public static void drawRectangle2DList(PDDocument document, int pageNumber, List rectCollection, Color color) { + private static void drawText(String string, PDDocument document, Point2D location, Integer pageNumber, Options options) { + var pdPage = document.getPage(pageNumber - 1); var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true); - contentStream.setStrokingColor(color); + + contentStream.setNonStrokingColor(options.getStrokeColor()); + contentStream.setLineWidth(options.getStrokeWidth()); + + contentStream.beginText(); + contentStream.newLineAtOffset((float) location.getX(), (float) location.getY()); + contentStream.setFont(PDType1Font.HELVETICA_OBLIQUE, 10); + contentStream.showText(string); + contentStream.endText(); + contentStream.close(); + } + + + @SneakyThrows + public static void drawRectangle2DList(PDDocument document, int pageNumber, List rectCollection, Options options) { + + var pdPage = document.getPage(pageNumber - 1); + var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true); + + contentStream.setStrokingColor(options.getStrokeColor()); + contentStream.setNonStrokingColor(options.getFillColor()); + contentStream.setLineWidth(options.getStrokeWidth()); + for (var r : rectCollection) { contentStream.addRect((float) r.getMinX(), (float) r.getMinY(), (float) r.getWidth(), (float) r.getHeight()); - contentStream.stroke(); + + if (options.isStroke() && options.isFill()) { + contentStream.fillAndStroke(); + } else if (options.isStroke()) { + contentStream.stroke(); + } else if (options.isFill()) { + contentStream.fill(); + } } contentStream.close(); } + + + @Builder + @AllArgsConstructor + @NoArgsConstructor + @Getter + @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) + public static class Options { + + @Builder.Default + boolean stroke = false; + @Builder.Default + Color strokeColor = Color.BLACK; + @Builder.Default + float strokeWidth = 1f; + @Builder.Default + boolean fill = false; + @Builder.Default + Color fillColor = Color.BLACK; + + } + + + private static Options buildStandardOptionsForNodes(TableOfContents.Entry entry) { + + return Options.builder().stroke(true).strokeColor(switch (entry.type()) { + case HEADER, FOOTER -> Color.GREEN; + case PARAGRAPH -> Color.BLUE; + case HEADLINE -> Color.RED; + case SECTION -> Color.BLACK; + case TABLE -> Color.ORANGE; + case TABLE_CELL -> Color.GRAY; + case IMAGE -> Color.MAGENTA; + }).build(); + } + + + private static void drawBBoxAndLabelAndNumberOnPage(PDDocument document, TableOfContents.Entry entry, Options options) { + + Map rectanglesPerPage = entry.node().getBBox(); + rectanglesPerPage.forEach((page, rectangle2D) -> { + if (entry.type() == NodeType.SECTION) { + rectangle2D = RectangleTransformations.pad(rectangle2D, 10, 10); + } + drawRectangle2DList(document, page.getNumber(), List.of(rectangle2D), options); + drawText(buildString(entry), document, new Point2D.Double(rectangle2D.getMinX(), rectangle2D.getMaxY() + 2), page.getNumber(), options); + }); + } + + + private static String buildString(TableOfContents.Entry entry) { + + return entry.node().getNumberOnPage() + ": " + entry.tocId() + ": " + entry.type().toString(); + } + } \ No newline at end of file diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06.IMAGE_INFO.json b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06.IMAGE_INFO.json new file mode 100644 index 00000000..c44818b3 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06.IMAGE_INFO.json @@ -0,0 +1,638 @@ +{ + "dossierId": "123", + "fileId": "123", + "targetFileExtension": "ORIGIN.pdf.gz", + "responseFileExtension": "IMAGE_INFO.json.gz", + "data": [ + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "DE7BE326BAC8CC872F3EF1FFC", + "position": { + "x1": 259, + "x2": 369, + "y1": 372, + "y2": 465, + "pageNumber": 54 + }, + "geometry": { + "width": 110, + "height": 93 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1429, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.1828, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "BDF2E766B088D8976E3EFBFFD", + "position": { + "x1": 259, + "x2": 370, + "y1": 558, + "y2": 633, + "pageNumber": 54 + }, + "geometry": { + "width": 111, + "height": 75 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1289, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.48, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "FCFBE026A8F86A802F3EFBFFE", + "position": { + "x1": 259, + "x2": 370, + "y1": 476, + "y2": 552, + "pageNumber": 54 + }, + "geometry": { + "width": 111, + "height": 76 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1298, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.4605, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "F3EF89CF8C7262A33E9EFAFFE", + "position": { + "x1": 259, + "x2": 372, + "y1": 274, + "y2": 366, + "pageNumber": 54 + }, + "geometry": { + "width": 113, + "height": 92 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1441, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.2283, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "FBFFC72232C8C8872F3EF1FFC", + "position": { + "x1": 259, + "x2": 372, + "y1": 183, + "y2": 268, + "pageNumber": 54 + }, + "geometry": { + "width": 113, + "height": 85 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1385, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.3294, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "FBFD873286106785EF78F0CF1", + "position": { + "x1": 259, + "x2": 449, + "y1": 111, + "y2": 177, + "pageNumber": 54 + }, + "geometry": { + "width": 190, + "height": 66 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1582, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 2.8788, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "7EF1C79E5B016ACFFFBD04442", + "position": { + "x1": 259, + "x2": 372, + "y1": 464, + "y2": 538, + "pageNumber": 55 + }, + "geometry": { + "width": 113, + "height": 74 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1292, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.527, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "F9F7ED37781D208B3FAEF0CF4", + "position": { + "x1": 259, + "x2": 440, + "y1": 628, + "y2": 721, + "pageNumber": 55 + }, + "geometry": { + "width": 181, + "height": 93 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1833, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.9462, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "FD778D164B00E389FF9EF0CF4", + "position": { + "x1": 259, + "x2": 440, + "y1": 544, + "y2": 622, + "pageNumber": 55 + }, + "geometry": { + "width": 181, + "height": 78 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1679, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 2.3205, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "other", + "probabilities": { + "other": 0.9342, + "signature": 0.0474, + "logo": 0.0183, + "formula": 0.0002 + } + }, + "representation": "00CFFEF9F7EF9F7EF9F7FF3", + "position": { + "x1": 537, + "x2": 707, + "y1": 462, + "y2": 511, + "pageNumber": 74 + }, + "geometry": { + "width": 170, + "height": 49 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.1289, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 3.4694, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "other", + "probabilities": { + "other": 0.9435, + "signature": 0.0403, + "logo": 0.016, + "formula": 0.0001 + } + }, + "representation": "00CFFFFFFFFFFFFFFFFFFF3", + "position": { + "x1": 537, + "x2": 707, + "y1": 437, + "y2": 453, + "pageNumber": 74 + }, + "geometry": { + "width": 170, + "height": 16 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.0737, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 10.625, + "tooTall": false, + "tooWide": true + } + }, + "probability": { + "unconfident": false + }, + "allPassed": false + } + }, + { + "classification": { + "label": "other", + "probabilities": { + "other": 0.9998, + "logo": 0.0001, + "formula": 0.0, + "signature": 0.0 + } + }, + "representation": "FFFFFFFFFFFFFFFFFFFFFFFFF", + "position": { + "x1": 442, + "x2": 499, + "y1": 308, + "y2": 351, + "pageNumber": 154 + }, + "geometry": { + "width": 57, + "height": 43 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.0699, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.3256, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "other", + "probabilities": { + "other": 1.0, + "formula": 0.0, + "logo": 0.0, + "signature": 0.0 + } + }, + "representation": "2C3083D04BD8CF2FFCF3F7EFF", + "position": { + "x1": 71, + "x2": 524, + "y1": 318, + "y2": 771, + "pageNumber": 165 + }, + "geometry": { + "width": 453, + "height": 453 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.64, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.0, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "other", + "probabilities": { + "other": 1.0, + "formula": 0.0, + "logo": 0.0, + "signature": 0.0 + } + }, + "representation": "FFF0010400FFECD31C60FFCFF", + "position": { + "x1": 93, + "x2": 502, + "y1": 374, + "y2": 495, + "pageNumber": 185 + }, + "geometry": { + "width": 409, + "height": 121 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.3143, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 3.3802, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + }, + { + "classification": { + "label": "formula", + "probabilities": { + "formula": 1.0, + "logo": 0.0, + "other": 0.0, + "signature": 0.0 + } + }, + "representation": "33F5D7760837FFF3660877FFF", + "position": { + "x1": 158, + "x2": 437, + "y1": 538, + "y2": 771, + "pageNumber": 185 + }, + "geometry": { + "width": 279, + "height": 233 + }, + "alpha": false, + "filters": { + "geometry": { + "imageSize": { + "quotient": 0.3602, + "tooLarge": false, + "tooSmall": false + }, + "imageFormat": { + "quotient": 1.1974, + "tooTall": false, + "tooWide": false + } + }, + "probability": { + "unconfident": false + }, + "allPassed": true + } + } + ] +} \ No newline at end of file diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/empty_image_response.json b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/empty_image_response.json new file mode 100644 index 00000000..856ee5dc --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/empty_image_response.json @@ -0,0 +1,5 @@ +{ + "dossierId": "f889853e-4bf8-49a9-aae5-c38605c6ef40", + "fileId": "22ef63e29bb2a27db8497272336f6b32", + "data": [] +}