Merge branch 'RED-8481-fix' into 'main'

Red 8481 fix

See merge request fforesight/layout-parser!103
This commit is contained in:
Yannik Hampe 2024-02-23 14:08:57 +01:00
commit 9817eae897
20 changed files with 219 additions and 75 deletions

View File

@ -5,6 +5,7 @@ import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -115,6 +116,8 @@ public class DocumentStructure implements Serializable {
Map<String, String> properties; Map<String, String> properties;
@Schema(description = "All child Entries of this Entry.", example = "[1, 2, 3]") @Schema(description = "All child Entries of this Entry.", example = "[1, 2, 3]")
List<EntryData> children; List<EntryData> children;
@Schema(description = "Describes the origin of the semantic node", example = "[ALGORITHM]")
Set<LayoutEngine> engines;
@Override @Override

View File

@ -0,0 +1,6 @@
package com.knecon.fforesight.service.layoutparser.internal.api.data.redaction;
public enum LayoutEngine {
ALGORITHM,
AI
}

View File

@ -101,8 +101,10 @@ public class LayoutParsingPipeline {
.orElse(originFile); .orElse(originFile);
VisualLayoutParsingResponse visualLayoutParsingResponse = new VisualLayoutParsingResponse(); VisualLayoutParsingResponse visualLayoutParsingResponse = new VisualLayoutParsingResponse();
if (layoutParsingRequest.visualLayoutParsingFileId().isPresent()) { if (layoutParsingRequest.visualLayoutParsingFileId()
visualLayoutParsingResponse = layoutParsingStorageService.getVisualLayoutParsingFile(layoutParsingRequest.visualLayoutParsingFileId().get()); .isPresent()) {
visualLayoutParsingResponse = layoutParsingStorageService.getVisualLayoutParsingFile(layoutParsingRequest.visualLayoutParsingFileId()
.get());
} }
ImageServiceResponse imageServiceResponse = new ImageServiceResponse(); ImageServiceResponse imageServiceResponse = new ImageServiceResponse();
@ -124,7 +126,7 @@ public class LayoutParsingPipeline {
imageServiceResponse, imageServiceResponse,
tableServiceResponse, tableServiceResponse,
visualLayoutParsingResponse, visualLayoutParsingResponse,
layoutParsingRequest.identifier().toString()); layoutParsingRequest.identifier().toString());
log.info("Building document graph for {}", layoutParsingRequest.identifier()); log.info("Building document graph for {}", layoutParsingRequest.identifier());
@ -134,6 +136,8 @@ public class LayoutParsingPipeline {
layoutGridService.addLayoutGrid(viewerDocumentFile, documentGraph, viewerDocumentFile, false); layoutGridService.addLayoutGrid(viewerDocumentFile, documentGraph, viewerDocumentFile, false);
layoutGridService.addLayoutGrid(viewerDocumentFile, documentGraph, viewerDocumentFile, false, true);
log.info("Storing resulting files for {}", layoutParsingRequest.identifier()); log.info("Storing resulting files for {}", layoutParsingRequest.identifier());
layoutParsingStorageService.storeDocumentData(layoutParsingRequest, DocumentDataMapper.toDocumentData(documentGraph)); layoutParsingStorageService.storeDocumentData(layoutParsingRequest, DocumentDataMapper.toDocumentData(documentGraph));
@ -219,11 +223,7 @@ public class LayoutParsingPipeline {
addNumberOfPagesToTrace(originDocument.getNumberOfPages(), Files.size(originFile.toPath())); addNumberOfPagesToTrace(originDocument.getNumberOfPages(), Files.size(originFile.toPath()));
Map<Integer, List<TableCells>> pdfTableCells = cvTableParsingAdapter.buildCvParsedTablesPerPage(tableServiceResponse); Map<Integer, List<TableCells>> pdfTableCells = cvTableParsingAdapter.buildCvParsedTablesPerPage(tableServiceResponse);
Map<Integer, List<ClassifiedImage>> pdfImages = imageServiceResponseAdapter.buildClassifiedImagesPerPage(imageServiceResponse); Map<Integer, List<ClassifiedImage>> pdfImages = imageServiceResponseAdapter.buildClassifiedImagesPerPage(imageServiceResponse);
Map<Integer, List<ClassifiedImage>> signatures = new HashMap<>(); Map<Integer, List<ClassifiedImage>> signatures = visualLayoutParsingAdapter.buildExtractedSignaturesPerPage(visualLayoutParsingResponse);
if(signatures.size() > 0) {
visualLayoutParsingAdapter.buildExtractedSignaturesPerPage(visualLayoutParsingResponse);
}
ClassificationDocument classificationDocument = new ClassificationDocument(); ClassificationDocument classificationDocument = new ClassificationDocument();
List<ClassificationPage> classificationPages = new ArrayList<>(); List<ClassificationPage> classificationPages = new ArrayList<>();
@ -283,8 +283,12 @@ public class LayoutParsingPipeline {
imageServiceResponseAdapter.findOcr(classificationPage); imageServiceResponseAdapter.findOcr(classificationPage);
} }
if(signatures.containsKey(pageNumber)) { if (signatures.containsKey(pageNumber)) {
classificationPage.setImages(signatures.get(pageNumber)); if (classificationPage.getImages() == null || classificationPage.getImages().size() == 0) {
classificationPage.setImages(signatures.get(pageNumber));
} else {
classificationPage.getImages().addAll(signatures.get(pageNumber));
}
} }
tableExtractionService.extractTables(cleanRulings, classificationPage); tableExtractionService.extractTables(cleanRulings, classificationPage);

View File

@ -10,6 +10,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity; import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
@ -30,6 +31,9 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Document implements GenericSemanticNode { public class Document implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
Set<Page> pages; Set<Page> pages;
DocumentTree documentTree; DocumentTree documentTree;
Integer numberOfPages; Integer numberOfPages;
@ -56,13 +60,15 @@ public class Document implements GenericSemanticNode {
public List<Section> getMainSections() { public List<Section> getMainSections() {
return streamChildrenOfType(NodeType.SECTION).map(node -> (Section) node).collect(Collectors.toList()); return streamChildrenOfType(NodeType.SECTION).map(node -> (Section) node)
.collect(Collectors.toList());
} }
public Stream<TextBlock> streamTerminalTextBlocksInOrder() { public Stream<TextBlock> streamTerminalTextBlocksInOrder() {
return streamAllNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock); return streamAllNodes().filter(SemanticNode::isLeaf)
.map(SemanticNode::getLeafTextBlock);
} }
@ -83,13 +89,16 @@ public class Document implements GenericSemanticNode {
@Override @Override
public Headline getHeadline() { public Headline getHeadline() {
return streamAllSubNodesOfType(NodeType.HEADLINE).map(node -> (Headline) node).findFirst().orElse(Headline.builder().build()); return streamAllSubNodesOfType(NodeType.HEADLINE).map(node -> (Headline) node)
.findFirst()
.orElse(Headline.builder().build());
} }
private Stream<SemanticNode> streamAllNodes() { private Stream<SemanticNode> streamAllNodes() {
return documentTree.allEntriesInOrder().map(DocumentTree.Entry::getNode); return documentTree.allEntriesInOrder()
.map(DocumentTree.Entry::getNode);
} }

View File

@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity; import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
@ -26,6 +27,9 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Footer implements GenericSemanticNode { public class Footer implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
List<Integer> treeId; List<Integer> treeId;
TextBlock leafTextBlock; TextBlock leafTextBlock;

View File

@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity; import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
@ -26,6 +27,8 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Header implements GenericSemanticNode { public class Header implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
List<Integer> treeId; List<Integer> treeId;
TextBlock leafTextBlock; TextBlock leafTextBlock;

View File

@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity; import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
@ -26,6 +27,8 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Headline implements GenericSemanticNode { public class Headline implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
List<Integer> treeId; List<Integer> treeId;
TextBlock leafTextBlock; TextBlock leafTextBlock;

View File

@ -8,6 +8,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity; import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
@ -29,6 +30,9 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Image implements GenericSemanticNode { public class Image implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
List<Integer> treeId; List<Integer> treeId;
String id; String id;
@ -66,7 +70,9 @@ public class Image implements GenericSemanticNode {
@Override @Override
public TextBlock getTextBlock() { public TextBlock getTextBlock() {
return streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector()); return streamAllSubNodes().filter(SemanticNode::isLeaf)
.map(SemanticNode::getLeafTextBlock)
.collect(new TextBlockCollector());
} }

View File

@ -6,6 +6,8 @@ public enum ImageType {
LOGO, LOGO,
FORMULA, FORMULA,
SIGNATURE, SIGNATURE,
SIGNATURE_VISUAL,
OTHER, OTHER,
OCR; OCR;

View File

@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity; import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
@ -24,6 +25,9 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Paragraph implements GenericSemanticNode { public class Paragraph implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
List<Integer> treeId; List<Integer> treeId;
TextBlock leafTextBlock; TextBlock leafTextBlock;

View File

@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity; import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
@ -27,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Section implements GenericSemanticNode { public class Section implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
List<Integer> treeId; List<Integer> treeId;
TextBlock textBlock; TextBlock textBlock;
@ -50,7 +53,8 @@ public class Section implements GenericSemanticNode {
public boolean hasTables() { public boolean hasTables() {
return streamAllSubNodesOfType(NodeType.TABLE).findAny().isPresent(); return streamAllSubNodesOfType(NodeType.TABLE).findAny()
.isPresent();
} }
@ -58,7 +62,9 @@ public class Section implements GenericSemanticNode {
public TextBlock getTextBlock() { public TextBlock getTextBlock() {
if (textBlock == null) { if (textBlock == null) {
textBlock = streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector()); textBlock = streamAllSubNodes().filter(SemanticNode::isLeaf)
.map(SemanticNode::getLeafTextBlock)
.collect(new TextBlockCollector());
} }
return textBlock; return textBlock;
} }

View File

@ -12,6 +12,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary; import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
@ -335,6 +336,23 @@ public interface SemanticNode {
} }
/**
* returns the set of layoutengines.
*
* @return set of layoutengines.
*/
Set<LayoutEngine> getEngines();
/**
* adds a layoutengine to the set.
*/
default void addEngine(LayoutEngine engine) {
getEngines().add(engine);
}
/** /**
* Streams all children located directly underneath this node in the DocumentTree. * Streams all children located directly underneath this node in the DocumentTree.
* *
@ -426,6 +444,7 @@ public interface SemanticNode {
/** /**
* TODO: this produces unwanted results for sections spanning multiple columns. * TODO: this produces unwanted results for sections spanning multiple columns.
* Computes the Union of the bounding boxes of all children recursively. * Computes the Union of the bounding boxes of all children recursively.
*
* @return The union of the BoundingBoxes of all children * @return The union of the BoundingBoxes of all children
*/ */
private Map<Page, Rectangle2D> getBBoxFromChildren() { private Map<Page, Rectangle2D> getBBoxFromChildren() {

View File

@ -12,6 +12,7 @@ import java.util.Set;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity; import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
@ -31,6 +32,8 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Table implements SemanticNode { public class Table implements SemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
List<Integer> treeId; List<Integer> treeId;
DocumentTree documentTree; DocumentTree documentTree;
@ -45,6 +48,7 @@ public class Table implements SemanticNode {
@EqualsAndHashCode.Exclude @EqualsAndHashCode.Exclude
Map<Page, Rectangle2D> bBoxCache; Map<Page, Rectangle2D> bBoxCache;
/** /**
* Streams all entities in this table, that appear in a row, which contains any of the provided strings. * Streams all entities in this table, that appear in a row, which contains any of the provided strings.
* *
@ -53,8 +57,7 @@ public class Table implements SemanticNode {
*/ */
public Stream<RedactionEntity> streamEntitiesWhereRowContainsStringsIgnoreCase(List<String> strings) { public Stream<RedactionEntity> streamEntitiesWhereRowContainsStringsIgnoreCase(List<String> strings) {
return IntStream.range(0, numberOfRows) return IntStream.range(0, numberOfRows).boxed()
.boxed()
.filter(row -> rowContainsStringsIgnoreCase(row, strings)) .filter(row -> rowContainsStringsIgnoreCase(row, strings))
.flatMap(this::streamRow) .flatMap(this::streamRow)
.map(TableCell::getEntities) .map(TableCell::getEntities)
@ -71,8 +74,11 @@ public class Table implements SemanticNode {
*/ */
public boolean rowContainsStringsIgnoreCase(Integer row, List<String> strings) { public boolean rowContainsStringsIgnoreCase(Integer row, List<String> strings) {
String rowText = streamRow(row).map(TableCell::getTextBlock).collect(new TextBlockCollector()).getSearchText().toLowerCase(Locale.ROOT); String rowText = streamRow(row).map(TableCell::getTextBlock)
return strings.stream().map(String::toLowerCase).allMatch(rowText::contains); .collect(new TextBlockCollector()).getSearchText().toLowerCase(Locale.ROOT);
return strings.stream()
.map(String::toLowerCase)
.allMatch(rowText::contains);
} }
@ -85,9 +91,13 @@ public class Table implements SemanticNode {
*/ */
public Stream<RedactionEntity> streamEntitiesWhereRowHasHeaderAndValue(String header, String value) { public Stream<RedactionEntity> streamEntitiesWhereRowHasHeaderAndValue(String header, String value) {
List<Integer> vertebrateStudyCols = streamHeaders().filter(headerNode -> headerNode.containsString(header)).map(TableCell::getCol).toList(); List<Integer> vertebrateStudyCols = streamHeaders().filter(headerNode -> headerNode.containsString(header))
.map(TableCell::getCol)
.toList();
return streamTableCells().filter(tableCellNode -> vertebrateStudyCols.stream() return streamTableCells().filter(tableCellNode -> vertebrateStudyCols.stream()
.anyMatch(vertebrateStudyCol -> getCell(tableCellNode.getRow(), vertebrateStudyCol).containsString(value))).map(TableCell::getEntities).flatMap(Collection::stream); .anyMatch(vertebrateStudyCol -> getCell(tableCellNode.getRow(), vertebrateStudyCol).containsString(value)))
.map(TableCell::getEntities)
.flatMap(Collection::stream);
} }
@ -100,9 +110,13 @@ public class Table implements SemanticNode {
*/ */
public Stream<RedactionEntity> streamEntitiesWhereRowHasHeaderAndAnyValue(String header, List<String> values) { public Stream<RedactionEntity> streamEntitiesWhereRowHasHeaderAndAnyValue(String header, List<String> values) {
List<Integer> colsWithHeader = streamHeaders().filter(headerNode -> headerNode.containsString(header)).map(TableCell::getCol).toList(); List<Integer> colsWithHeader = streamHeaders().filter(headerNode -> headerNode.containsString(header))
.map(TableCell::getCol)
.toList();
return streamTableCells().filter(tableCellNode -> colsWithHeader.stream() return streamTableCells().filter(tableCellNode -> colsWithHeader.stream()
.anyMatch(colWithHeader -> getCell(tableCellNode.getRow(), colWithHeader).containsAnyString(values))).map(TableCell::getEntities).flatMap(Collection::stream); .anyMatch(colWithHeader -> getCell(tableCellNode.getRow(), colWithHeader).containsAnyString(values)))
.map(TableCell::getEntities)
.flatMap(Collection::stream);
} }
@ -114,12 +128,15 @@ public class Table implements SemanticNode {
*/ */
public Stream<RedactionEntity> streamEntitiesWhereRowContainsEntitiesOfType(List<String> types) { public Stream<RedactionEntity> streamEntitiesWhereRowContainsEntitiesOfType(List<String> types) {
List<Integer> rowsWithEntityOfType = IntStream.range(0, numberOfRows) List<Integer> rowsWithEntityOfType = IntStream.range(0, numberOfRows).boxed()
.boxed() .filter(rowNumber -> streamEntityTypesInRow(rowNumber).anyMatch(existingType -> types.stream()
.filter(rowNumber -> streamEntityTypesInRow(rowNumber).anyMatch(existingType -> types.stream().anyMatch(typeToCheck -> typeToCheck.equals(existingType)))) .anyMatch(typeToCheck -> typeToCheck.equals(existingType))))
.toList(); .toList();
return rowsWithEntityOfType.stream().flatMap(this::streamRow).map(TableCell::getEntities).flatMap(Collection::stream); return rowsWithEntityOfType.stream()
.flatMap(this::streamRow)
.map(TableCell::getEntities)
.flatMap(Collection::stream);
} }
@ -131,18 +148,24 @@ public class Table implements SemanticNode {
*/ */
public Stream<RedactionEntity> streamEntitiesWhereRowContainsNoEntitiesOfType(List<String> types) { public Stream<RedactionEntity> streamEntitiesWhereRowContainsNoEntitiesOfType(List<String> types) {
List<Integer> rowsWithNoEntityOfType = IntStream.range(0, numberOfRows) List<Integer> rowsWithNoEntityOfType = IntStream.range(0, numberOfRows).boxed()
.boxed() .filter(rowNumber -> streamEntityTypesInRow(rowNumber).noneMatch(existingType -> types.stream()
.filter(rowNumber -> streamEntityTypesInRow(rowNumber).noneMatch(existingType -> types.stream().anyMatch(typeToCheck -> typeToCheck.equals(existingType)))) .anyMatch(typeToCheck -> typeToCheck.equals(existingType))))
.toList(); .toList();
return rowsWithNoEntityOfType.stream().flatMap(this::streamRow).map(TableCell::getEntities).flatMap(Collection::stream); return rowsWithNoEntityOfType.stream()
.flatMap(this::streamRow)
.map(TableCell::getEntities)
.flatMap(Collection::stream);
} }
private Stream<String> streamEntityTypesInRow(Integer rowNumber) { private Stream<String> streamEntityTypesInRow(Integer rowNumber) {
return streamRow(rowNumber).map(TableCell::getEntities).flatMap(Collection::stream).map(RedactionEntity::getType).distinct(); return streamRow(rowNumber).map(TableCell::getEntities)
.flatMap(Collection::stream)
.map(RedactionEntity::getType)
.distinct();
} }
@ -159,7 +182,8 @@ public class Table implements SemanticNode {
throw new IllegalArgumentException(format("row %d, col %d is out of bounds for number of rows of %d and number of cols %d", row, col, numberOfRows, numberOfCols)); throw new IllegalArgumentException(format("row %d, col %d is out of bounds for number of rows of %d and number of cols %d", row, col, numberOfRows, numberOfCols));
} }
int idx = row * numberOfCols + col; int idx = row * numberOfCols + col;
return (TableCell) documentTree.getEntryById(treeId).getChildren().get(idx).getNode(); return (TableCell) documentTree.getEntryById(treeId).getChildren()
.get(idx).getNode();
} }
@ -196,7 +220,8 @@ public class Table implements SemanticNode {
*/ */
public Stream<TableCell> streamCol(int col) { public Stream<TableCell> streamCol(int col) {
return IntStream.range(0, numberOfRows).boxed().map(row -> getCell(row, col)); return IntStream.range(0, numberOfRows).boxed()
.map(row -> getCell(row, col));
} }
@ -208,9 +233,11 @@ public class Table implements SemanticNode {
*/ */
public Stream<TableCell> streamRow(int row) { public Stream<TableCell> streamRow(int row) {
return IntStream.range(0, numberOfCols).boxed().map(col -> getCell(row, col)); return IntStream.range(0, numberOfCols).boxed()
.map(col -> getCell(row, col));
} }
/** /**
* Streams all TableCells row-wise and filters them with header == true. * Streams all TableCells row-wise and filters them with header == true.
* *
@ -231,7 +258,8 @@ public class Table implements SemanticNode {
*/ */
public Stream<TableCell> streamHeadersForCell(int row, int col) { public Stream<TableCell> streamHeadersForCell(int row, int col) {
return Stream.concat(streamRow(row), streamCol(col)).filter(TableCell::isHeader); return Stream.concat(streamRow(row), streamCol(col))
.filter(TableCell::isHeader);
} }
@ -304,7 +332,9 @@ public class Table implements SemanticNode {
public TextBlock getTextBlock() { public TextBlock getTextBlock() {
if (textBlock == null) { if (textBlock == null) {
textBlock = streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector()); textBlock = streamAllSubNodes().filter(SemanticNode::isLeaf)
.map(SemanticNode::getLeafTextBlock)
.collect(new TextBlockCollector());
} }
return textBlock; return textBlock;
} }
@ -315,6 +345,8 @@ public class Table implements SemanticNode {
return treeId.toString() + ": " + NodeType.TABLE + ": #cols: " + numberOfCols + ", #rows: " + numberOfRows + ", " + this.getTextBlock().buildSummary(); return treeId.toString() + ": " + NodeType.TABLE + ": #cols: " + numberOfCols + ", #rows: " + numberOfRows + ", " + this.getTextBlock().buildSummary();
} }
@Override @Override
public Map<Page, Rectangle2D> getBBox() { public Map<Page, Rectangle2D> getBBox() {
@ -323,4 +355,5 @@ public class Table implements SemanticNode {
} }
return bBoxCache; return bBoxCache;
} }
} }

View File

@ -7,6 +7,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity; import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
@ -26,6 +27,8 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class TableCell implements GenericSemanticNode { public class TableCell implements GenericSemanticNode {
@Builder.Default
Set<LayoutEngine> engines = new HashSet<>(Set.of(LayoutEngine.ALGORITHM));
List<Integer> treeId; List<Integer> treeId;
int row; int row;
int col; int col;

View File

@ -4,18 +4,21 @@ import java.awt.geom.Rectangle2D;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.ImageType; import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.ImageType;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@Data @Data
@RequiredArgsConstructor @RequiredArgsConstructor
@AllArgsConstructor
public class ClassifiedImage { public class ClassifiedImage {
@NonNull @NonNull
private Rectangle2D position; private Rectangle2D position;
@NonNull @NonNull
private ImageType imageType; private ImageType imageType;
private boolean sourceByAi;
private boolean isAppendedToSection; private boolean isAppendedToSection;
@NonNull @NonNull
private boolean hasTransparency; private boolean hasTransparency;

View File

@ -24,6 +24,7 @@ public class VisualLayoutParsingAdapter {
private static String SIGNATURES = "signature"; private static String SIGNATURES = "signature";
public Map<Integer, List<VisualLayoutParsingResult>> buildExtractedTablesPerPage(VisualLayoutParsingResponse visualLayoutParsingResponse) { public Map<Integer, List<VisualLayoutParsingResult>> buildExtractedTablesPerPage(VisualLayoutParsingResponse visualLayoutParsingResponse) {
Map<Integer, List<VisualLayoutParsingResult>> tableCells = new HashMap<>(); Map<Integer, List<VisualLayoutParsingResult>> tableCells = new HashMap<>();
@ -34,12 +35,17 @@ public class VisualLayoutParsingAdapter {
} }
public Map<Integer, List<ClassifiedImage>> buildExtractedSignaturesPerPage(VisualLayoutParsingResponse visualLayoutParsingResponse) { public Map<Integer, List<ClassifiedImage>> buildExtractedSignaturesPerPage(VisualLayoutParsingResponse visualLayoutParsingResponse) {
Map<Integer, List<ClassifiedImage>> signatures = new HashMap<>(); Map<Integer, List<ClassifiedImage>> signatures = new HashMap<>();
visualLayoutParsingResponse.getData().forEach(tableData -> signatures.computeIfAbsent(tableData.getPage_idx(), tableCell -> new ArrayList<>()).addAll(convertSignatures(tableData.getPage_idx(), tableData.getBoxes()))); if (visualLayoutParsingResponse.getData() != null) {
visualLayoutParsingResponse.getData()
.forEach(tableData -> signatures.computeIfAbsent(tableData.getPage_idx() + 1, tableCell -> new ArrayList<>())
.addAll(convertSignatures(tableData.getPage_idx(), tableData.getBoxes())));
}
return signatures;
return signatures;
} }
@ -63,14 +69,17 @@ public class VisualLayoutParsingAdapter {
} }
public List<ClassifiedImage> convertSignatures(int pageNumber, List<VisualLayoutParsingBox> tableObjects) { public List<ClassifiedImage> convertSignatures(int pageNumber, List<VisualLayoutParsingBox> tableObjects) {
List<ClassifiedImage> signatures = new ArrayList<>(); List<ClassifiedImage> signatures = new ArrayList<>();
tableObjects.stream().forEach(t -> { tableObjects.stream().forEach(t -> {
if(t.getLabel().equals(SIGNATURES)) { if (t.getLabel().equals(SIGNATURES)) {
ClassifiedImage signature = new ClassifiedImage(new Rectangle2D.Float(t.getBox().getX1(),t.getBox().getY1(),t.getBox().getX2() - t.getBox().getX1(),t.getBox().getY2() - t.getBox().getY1()), ClassifiedImage signature = new ClassifiedImage(new Rectangle2D.Float(t.getBox().getX1(),
ImageType.SIGNATURE,false,pageNumber); t.getBox().getY1(),
t.getBox().getX2() - t.getBox().getX1(),
t.getBox().getY2() - t.getBox().getY1()), ImageType.SIGNATURE, true, false, false, pageNumber);
signatures.add(signature); signatures.add(signature);
} }

View File

@ -7,19 +7,19 @@ import static java.util.stream.Collectors.toList;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Set; import java.util.Set;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock; import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument; import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationFooter; import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationFooter;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationHeader; import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationHeader;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage; import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree; import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document; import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Footer; import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Footer;
@ -31,6 +31,8 @@ import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Pa
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph; import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Section; import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Section;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock; import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.utils.IdBuilder; import com.knecon.fforesight.service.layoutparser.processor.utils.IdBuilder;
import com.knecon.fforesight.service.layoutparser.processor.utils.TextPositionOperations; import com.knecon.fforesight.service.layoutparser.processor.utils.TextPositionOperations;
@ -95,14 +97,17 @@ public class DocumentGraphFactory {
Rectangle2D position = image.getPosition(); Rectangle2D position = image.getPosition();
Page page = context.getPage(image.getPage()); Page page = context.getPage(image.getPage());
Image imageNode = Image.builder() var imageBuilder = Image.builder()
.id(IdBuilder.buildId(Set.of(page), List.of(position))) .id(IdBuilder.buildId(Set.of(page), List.of(position)))
.imageType(image.getImageType()) .imageType(image.getImageType())
.position(position) .position(position)
.transparent(image.isHasTransparency()) .transparent(image.isHasTransparency())
.page(page) .page(page)
.documentTree(context.getDocumentTree()) .documentTree(context.getDocumentTree());
.build(); if (image.isSourceByAi()) {
imageBuilder.engines(new HashSet<>(Set.of(LayoutEngine.AI)));
}
Image imageNode = imageBuilder.build();
page.getMainBody().add(imageNode); page.getMainBody().add(imageNode);
List<Integer> tocId = context.getDocumentTree().createNewChildEntryAndReturnId(section, imageNode); List<Integer> tocId = context.getDocumentTree().createNewChildEntryAndReturnId(section, imageNode);

View File

@ -80,6 +80,7 @@ public class DocumentDataMapper {
.treeId(toPrimitiveIntArray(entry.getTreeId())) .treeId(toPrimitiveIntArray(entry.getTreeId()))
.children(entry.getChildren().stream().map(DocumentDataMapper::toEntryData).toList()) .children(entry.getChildren().stream().map(DocumentDataMapper::toEntryData).toList())
.type(entry.getType()) .type(entry.getType())
.engines(entry.getNode().getEngines())
.atomicBlockIds(atomicTextBlocks) .atomicBlockIds(atomicTextBlocks)
.pageNumbers(entry.getNode().getPages().stream().map(Page::getNumber).map(Integer::longValue).toArray(Long[]::new)) .pageNumbers(entry.getNode().getPages().stream().map(Page::getNumber).map(Integer::longValue).toArray(Long[]::new))
.properties(properties) .properties(properties)

View File

@ -17,6 +17,7 @@ import java.util.stream.Stream;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document; import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page; import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
@ -58,48 +59,59 @@ public class LayoutGridService {
static Color HEADER_COLOR = new Color(171, 131, 6); static Color HEADER_COLOR = new Color(171, 131, 6);
static Color IMAGE_COLOR = new Color(253, 63, 146); static Color IMAGE_COLOR = new Color(253, 63, 146);
static Color IMAGE_VISUAL_COLOR = new Color(122, 0, 255);
@SneakyThrows @SneakyThrows
@Observed(name = "ViewerDocumentService", contextualName = "create-viewer-document") @Observed(name = "ViewerDocumentService", contextualName = "create-viewer-document")
public void addLayoutGrid(File originFile, Document document, File destinationFile, boolean layerVisibilityDefaultValue) { public void addLayoutGrid(File originFile, Document document, File destinationFile, boolean layerVisibilityDefaultValue) {
LayoutGrid layoutGrid = createLayoutGrid(document); this.addLayoutGrid(originFile, document, destinationFile, layerVisibilityDefaultValue, false);
}
@SneakyThrows
@Observed(name = "ViewerDocumentService", contextualName = "create-viewer-document")
public void addLayoutGrid(File originFile, Document document, File destinationFile, boolean layerVisibilityDefaultValue, boolean visualParsingGrid) {
LayoutGrid layoutGrid = createLayoutGrid(document, visualParsingGrid);
viewerDocumentService.addVisualizationsOnPage(originFile, viewerDocumentService.addVisualizationsOnPage(originFile,
destinationFile, destinationFile,
Visualizations.builder() Visualizations.builder()
.layer(ContentStreams.KNECON_LAYOUT) .layer(visualParsingGrid ? ContentStreams.KNECON_VISUAL_PARSING : ContentStreams.KNECON_LAYOUT)
.visualizationsOnPages(layoutGrid.getVisualizationsPerPages()) .visualizationsOnPages(layoutGrid.getVisualizationsPerPages())
.layerVisibilityDefaultValue(layerVisibilityDefaultValue) .layerVisibilityDefaultValue(layerVisibilityDefaultValue)
.build()); .build());
} }
private LayoutGrid createLayoutGrid(Document document) { private LayoutGrid createLayoutGrid(Document document, boolean visualParsingGrid) {
LayoutGrid layoutGrid = new LayoutGrid(document.getNumberOfPages()); LayoutGrid layoutGrid = new LayoutGrid(document.getNumberOfPages());
document.streamAllSubNodes().forEach(semanticNode -> { document.streamAllSubNodes()
Color color = switch (semanticNode.getType()) { .filter(node -> (node.getEngines().contains(LayoutEngine.AI) && visualParsingGrid) || (node.getEngines().contains(LayoutEngine.ALGORITHM) && !visualParsingGrid))
case PARAGRAPH -> PARAGRAPH_COLOR; .forEach(semanticNode -> {
case TABLE -> TABLE_COLOR; Color color = switch (semanticNode.getType()) {
case SECTION -> SECTION_COLOR; case PARAGRAPH -> PARAGRAPH_COLOR;
case HEADLINE -> HEADLINE_COLOR; case TABLE -> TABLE_COLOR;
case HEADER, FOOTER -> HEADER_COLOR; case SECTION -> SECTION_COLOR;
case IMAGE -> IMAGE_COLOR; case HEADLINE -> HEADLINE_COLOR;
default -> null; case HEADER, FOOTER -> HEADER_COLOR;
}; case IMAGE -> IMAGE_COLOR;
if (isNotSectionOrTableCellOrDocument(semanticNode)) { default -> null;
addAsRectangle(semanticNode, layoutGrid, color); };
} if (isNotSectionOrTableCellOrDocument(semanticNode)) {
if (semanticNode.getType().equals(NodeType.SECTION)) { addAsRectangle(semanticNode, layoutGrid, color);
addSection(semanticNode, layoutGrid, color); }
} if (semanticNode.getType().equals(NodeType.SECTION)) {
if (semanticNode.getType().equals(NodeType.TABLE)) { addSection(semanticNode, layoutGrid, color);
Table table = (Table) semanticNode; }
addInnerTableLines(table, layoutGrid); if (semanticNode.getType().equals(NodeType.TABLE)) {
} Table table = (Table) semanticNode;
}); addInnerTableLines(table, layoutGrid);
}
});
return layoutGrid; return layoutGrid;
} }

View File

@ -12,6 +12,8 @@ public class ContentStreams {
public static Identifier KNECON_LAYOUT = new Identifier("Layout grid", COSName.getPDFName("KNECON_LAYOUT"), true); public static Identifier KNECON_LAYOUT = new Identifier("Layout grid", COSName.getPDFName("KNECON_LAYOUT"), true);
public static Identifier KNECON_VISUAL_PARSING = new Identifier("Layout grid - visual", COSName.getPDFName("KNECON_VISUAL_PARSING"), true);
public static Identifier KNECON_OCR = new Identifier("OCR", COSName.getPDFName("KNECON_OCR"), false); public static Identifier KNECON_OCR = new Identifier("OCR", COSName.getPDFName("KNECON_OCR"), false);
public static Identifier KNECON_OCR_TEXT_DEBUG = new Identifier("OCR Text", COSName.getPDFName("KNECON_OCR_TEXT_DEBUG"), true); public static Identifier KNECON_OCR_TEXT_DEBUG = new Identifier("OCR Text", COSName.getPDFName("KNECON_OCR_TEXT_DEBUG"), true);
@ -24,7 +26,14 @@ public class ContentStreams {
public static Identifier ESCAPE_END = new Identifier("escape start", COSName.getPDFName("ESCAPE_END"), false); public static Identifier ESCAPE_END = new Identifier("escape start", COSName.getPDFName("ESCAPE_END"), false);
public static List<Identifier> allContentStreams = List.of(KNECON_LAYOUT, KNECON_OCR, KNECON_OCR_BBOX_DEBUG, KNECON_OCR_TEXT_DEBUG, OTHER, ESCAPE_START, ESCAPE_END); public static List<Identifier> allContentStreams = List.of(KNECON_LAYOUT,
KNECON_VISUAL_PARSING,
KNECON_OCR,
KNECON_OCR_BBOX_DEBUG,
KNECON_OCR_TEXT_DEBUG,
OTHER,
ESCAPE_START,
ESCAPE_END);
public record Identifier(String name, COSName cosName, boolean optionalContent) { public record Identifier(String name, COSName cosName, boolean optionalContent) {