Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28d8ad0a3f | ||
|
|
0c1583c1be | ||
|
|
7633566d9b | ||
|
|
cc4f09711e | ||
|
|
370165dc59 | ||
|
|
8c052c38d7 | ||
|
|
ea18d3d307 | ||
|
|
2726fc3fe1 | ||
|
|
033279e261 | ||
|
|
ec0dd032c9 | ||
|
|
598fa7f1c7 | ||
|
|
65b1f7d179 | ||
|
|
3173610be5 | ||
|
|
e920eb5a78 | ||
|
|
7e4baea7e5 | ||
|
|
66d3433e04 | ||
|
|
a2f559af51 | ||
|
|
39f527a57c | ||
|
|
5c2844fe31 | ||
|
|
b216f02e15 | ||
|
|
2e2f30ba35 | ||
|
|
9f7ed974ec | ||
|
|
570a348a77 | ||
|
|
859dba2ecf | ||
|
|
1c5d755111 | ||
|
|
133e06460f | ||
|
|
da91fcff97 | ||
|
|
79795e408a | ||
|
|
b719db86ab | ||
|
|
797602e373 | ||
|
|
3d2f66cf10 | ||
|
|
e304a9f2d7 | ||
|
|
c05f67cf44 | ||
|
|
9ecf9ca19f | ||
|
|
9be672c728 | ||
|
|
a5fcebce30 |
+25
-4
@@ -43,6 +43,8 @@ public class DocumentStructure implements Serializable {
|
||||
public static final String POSITION = "position";
|
||||
public static final String ID = "id";
|
||||
|
||||
public static final String REPRESENTATION_HASH = "representationHash";
|
||||
|
||||
}
|
||||
|
||||
@Schema(description = "Object containing the extra field names, a table cell has in its properties field.")
|
||||
@@ -67,11 +69,25 @@ public class DocumentStructure implements Serializable {
|
||||
|
||||
public static Rectangle2D parseRectangle2D(String bBox) {
|
||||
|
||||
List<Float> floats = Arrays.stream(bBox.split(RECTANGLE_DELIMITER)).map(Float::parseFloat).toList();
|
||||
List<Float> floats = Arrays.stream(bBox.split(RECTANGLE_DELIMITER))
|
||||
.map(Float::parseFloat)
|
||||
.toList();
|
||||
return new Rectangle2D.Float(floats.get(0), floats.get(1), floats.get(2), floats.get(3));
|
||||
}
|
||||
|
||||
|
||||
public static double[] parseRepresentationVector(String representationHash) {
|
||||
|
||||
String[] stringArray = representationHash.split("[,\\s]+");
|
||||
double[] doubleArray = new double[stringArray.length];
|
||||
for (int i = 0; i < stringArray.length; i++) {
|
||||
doubleArray[i] = Double.parseDouble(stringArray[i]);
|
||||
}
|
||||
|
||||
return doubleArray;
|
||||
}
|
||||
|
||||
|
||||
public EntryData get(List<Integer> tocId) {
|
||||
|
||||
if (tocId.isEmpty()) {
|
||||
@@ -87,19 +103,24 @@ public class DocumentStructure implements Serializable {
|
||||
|
||||
public Stream<EntryData> streamAllEntries() {
|
||||
|
||||
return Stream.concat(Stream.of(root), root.children.stream()).flatMap(DocumentStructure::flatten);
|
||||
return Stream.concat(Stream.of(root), root.children.stream())
|
||||
.flatMap(DocumentStructure::flatten);
|
||||
}
|
||||
|
||||
|
||||
public String toString() {
|
||||
|
||||
return String.join("\n", streamAllEntries().map(EntryData::toString).toList());
|
||||
return String.join("\n",
|
||||
streamAllEntries().map(EntryData::toString)
|
||||
.toList());
|
||||
}
|
||||
|
||||
|
||||
private static Stream<EntryData> flatten(EntryData entry) {
|
||||
|
||||
return Stream.concat(Stream.of(entry), entry.children.stream().flatMap(DocumentStructure::flatten));
|
||||
return Stream.concat(Stream.of(entry),
|
||||
entry.children.stream()
|
||||
.flatMap(DocumentStructure::flatten));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -2,5 +2,6 @@ package com.knecon.fforesight.service.layoutparser.internal.api.data.redaction;
|
||||
|
||||
public enum LayoutEngine {
|
||||
ALGORITHM,
|
||||
AI
|
||||
AI,
|
||||
OUTLINE
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import lombok.NoArgsConstructor;
|
||||
public class SimplifiedSectionText {
|
||||
|
||||
@Schema(description = "The number of this Section. This is used to map the simplified section text back to the original Section.")
|
||||
private int sectionNumber;
|
||||
private String sectionNumber;
|
||||
@Schema(description = "The text in this Section.")
|
||||
private String text;
|
||||
|
||||
|
||||
+4
@@ -1,5 +1,7 @@
|
||||
package com.knecon.fforesight.service.layoutparser.internal.api.data.taas;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -13,6 +15,8 @@ public class StructureObject {
|
||||
|
||||
@Schema(description = "The ID of this StructureObject.")
|
||||
Integer structureObjectNumber;
|
||||
@Schema(description = "The Tree ID of this StructureObject.")
|
||||
List<Integer> treeId;
|
||||
@Schema(description = "This value indicates the start of the string offsets in this Object, with respect to the reading order.")
|
||||
int page;
|
||||
@Schema(description = "This stringOffset indicates the start of the string offsets in this Object, with respect to the reading order of the entire document. It is equal to the previous' StructureObject stringOffset + its length.")
|
||||
|
||||
+4
-2
@@ -19,7 +19,6 @@ public record LayoutParsingRequest(
|
||||
@Schema(description = "Path to the original PDF file.")//
|
||||
@NonNull String originFileStorageId,//
|
||||
|
||||
|
||||
@Schema(description = "Optional Path to the table extraction file.")//
|
||||
Optional<String> tablesFileStorageId,//
|
||||
@Schema(description = "Optional Path to the image classification file.")//
|
||||
@@ -37,9 +36,12 @@ public record LayoutParsingRequest(
|
||||
@NonNull String positionBlockFileStorageId,//
|
||||
@Schema(description = "Path where the Document Pages File will be stored.")//
|
||||
@NonNull String pageFileStorageId,//
|
||||
@Schema(description = "Path where the Document Markdown File will be stored.")//
|
||||
Optional<String> documentMarkdownFileStorageId,//
|
||||
@Schema(description = "Path where the Simplified Text File will be stored.")//
|
||||
@NonNull String simplifiedTextStorageId,//
|
||||
@Schema(description = "Path where the Viewer Document PDF will be stored.")//
|
||||
@NonNull String viewerDocumentStorageId) {
|
||||
@NonNull String viewerDocumentStorageId
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
@@ -26,4 +26,6 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-web:3.1.3")
|
||||
implementation("org.jgrapht:jgrapht-core:1.5.2")
|
||||
implementation("org.tinspin:tinspin-indexes:2.1.3")
|
||||
implementation("org.commonmark:commonmark:0.22.0")
|
||||
implementation("org.commonmark:commonmark-ext-gfm-tables:0.22.0")
|
||||
}
|
||||
|
||||
+36
-53
@@ -24,6 +24,7 @@ import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.No
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingFinishedEvent;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingRequest;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.markdown.MarkdownMapper;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
@@ -116,29 +117,14 @@ public class LayoutParsingPipeline {
|
||||
log.info("Starting layout parsing for {}", layoutParsingRequest.identifier());
|
||||
|
||||
File originFile = layoutParsingStorageService.getOriginFile(layoutParsingRequest.originFileStorageId());
|
||||
// File viewerDocumentFile = layoutParsingStorageService.getViewerDocFile(layoutParsingRequest.viewerDocumentStorageId()).orElse(originFile);
|
||||
File viewerDocumentFile = originFile;
|
||||
File viewerDocumentFile = layoutParsingStorageService.getViewerDocFile(layoutParsingRequest.viewerDocumentStorageId()).orElse(originFile);
|
||||
|
||||
VisualLayoutParsingResponse visualLayoutParsingResponse = new VisualLayoutParsingResponse();
|
||||
if (layoutParsingRequest.visualLayoutParsingFileId()
|
||||
.isPresent()) {
|
||||
visualLayoutParsingResponse = layoutParsingStorageService.getVisualLayoutParsingFile(layoutParsingRequest.visualLayoutParsingFileId()
|
||||
.get());
|
||||
}
|
||||
|
||||
ImageServiceResponse imageServiceResponse = new ImageServiceResponse();
|
||||
if (layoutParsingRequest.imagesFileStorageId()
|
||||
.isPresent()) {
|
||||
imageServiceResponse = layoutParsingStorageService.getImagesFile(layoutParsingRequest.imagesFileStorageId()
|
||||
.get());
|
||||
}
|
||||
|
||||
TableServiceResponse tableServiceResponse = new TableServiceResponse();
|
||||
if (layoutParsingRequest.tablesFileStorageId()
|
||||
.isPresent()) {
|
||||
tableServiceResponse = layoutParsingStorageService.getTablesFile(layoutParsingRequest.tablesFileStorageId()
|
||||
.get());
|
||||
}
|
||||
VisualLayoutParsingResponse visualLayoutParsingResponse = layoutParsingRequest.visualLayoutParsingFileId()
|
||||
.map(layoutParsingStorageService::getVisualLayoutParsingFile).orElse(new VisualLayoutParsingResponse());
|
||||
ImageServiceResponse imageServiceResponse = layoutParsingRequest.imagesFileStorageId()
|
||||
.map(layoutParsingStorageService::getImagesFile).orElse(new ImageServiceResponse());
|
||||
TableServiceResponse tableServiceResponse = layoutParsingRequest.tablesFileStorageId()
|
||||
.map(layoutParsingStorageService::getTablesFile).orElse(new TableServiceResponse());
|
||||
|
||||
ClassificationDocument classificationDocument = parseLayout(settings.getLayoutParsingTypeOverride() == null //
|
||||
? layoutParsingRequest.layoutParsingType() : settings.getLayoutParsingTypeOverride(),
|
||||
@@ -151,7 +137,8 @@ public class LayoutParsingPipeline {
|
||||
log.info("Building document graph for {}", layoutParsingRequest.identifier());
|
||||
|
||||
Document documentGraph = observeBuildDocumentGraph(settings.getLayoutParsingTypeOverride() == null //
|
||||
? layoutParsingRequest.layoutParsingType() : settings.getLayoutParsingTypeOverride(), classificationDocument);
|
||||
? layoutParsingRequest.layoutParsingType() : settings.getLayoutParsingTypeOverride(),
|
||||
classificationDocument);
|
||||
|
||||
log.info("Creating viewer document for {}", layoutParsingRequest.identifier());
|
||||
|
||||
@@ -160,10 +147,13 @@ public class LayoutParsingPipeline {
|
||||
log.info("Storing resulting files for {}", layoutParsingRequest.identifier());
|
||||
|
||||
layoutParsingStorageService.storeDocumentData(layoutParsingRequest, DocumentDataMapper.toDocumentData(documentGraph));
|
||||
if(layoutParsingRequest.documentMarkdownFileStorageId().isPresent()) {
|
||||
layoutParsingStorageService.storeMarkdownFile(layoutParsingRequest.documentMarkdownFileStorageId().get(), new MarkdownMapper().toMarkdownContent(documentGraph));
|
||||
}
|
||||
layoutParsingStorageService.storeSimplifiedText(layoutParsingRequest, simplifiedSectionTextService.toSimplifiedText(documentGraph));
|
||||
layoutParsingStorageService.storeViewerDocument(layoutParsingRequest, viewerDocumentFile);
|
||||
|
||||
if (layoutParsingRequest.layoutParsingType().equals(LayoutParsingType.CLARIFYND)) {
|
||||
if (layoutParsingRequest.researchDocumentStorageId() != null) {
|
||||
log.info("Building research document data for {}", layoutParsingRequest.identifier());
|
||||
var researchDocumentData = TaasDocumentDataMapper.fromDocument(documentGraph);
|
||||
layoutParsingStorageService.storeResearchDocumentData(layoutParsingRequest, researchDocumentData);
|
||||
@@ -244,17 +234,18 @@ public class LayoutParsingPipeline {
|
||||
Map<Integer, List<TableCells>> pdfTableCells = cvTableParsingAdapter.buildCvParsedTablesPerPage(tableServiceResponse);
|
||||
Map<Integer, List<ClassifiedImage>> pdfImages = imageServiceResponseAdapter.buildClassifiedImagesPerPage(imageServiceResponse);
|
||||
Map<Integer, List<ClassifiedImage>> signatures = visualLayoutParsingAdapter.buildExtractedSignaturesPerPage(visualLayoutParsingResponse);
|
||||
|
||||
ClassificationDocument classificationDocument = new ClassificationDocument();
|
||||
|
||||
if (settings.isDebug() || identifier.containsKey("debug")) {
|
||||
classificationDocument.getVisualizations().setActive(true);
|
||||
classificationDocument.getLayoutDebugLayer().setActive(true);
|
||||
}
|
||||
|
||||
List<ClassificationPage> classificationPages = new ArrayList<>();
|
||||
OutlineObject lastProcessedOutlineObject = null;
|
||||
|
||||
// parsing the structure elements could be useful as well
|
||||
if(layoutParsingType != LayoutParsingType.REDACT_MANAGER_OLD && layoutParsingType != LayoutParsingType.DOCUMINE_OLD) {
|
||||
if (layoutParsingType != LayoutParsingType.REDACT_MANAGER_OLD && layoutParsingType != LayoutParsingType.DOCUMINE_OLD) {
|
||||
classificationDocument.setOutlineObjectTree(outlineExtractorService.getOutlineObjectTree(originDocument));
|
||||
}
|
||||
|
||||
@@ -285,7 +276,7 @@ public class LayoutParsingPipeline {
|
||||
}
|
||||
stripper.getText(originDocument);
|
||||
List<TextPositionSequence> words = stripper.getTextPositionSequences();
|
||||
classificationDocument.getVisualizations().addTextVisualizations(words, pageNumber);
|
||||
classificationDocument.getLayoutDebugLayer().addTextVisualizations(words, pageNumber);
|
||||
|
||||
PDRectangle pdr = pdPage.getMediaBox();
|
||||
|
||||
@@ -293,42 +284,34 @@ public class LayoutParsingPipeline {
|
||||
boolean isLandscape = pdr.getWidth() > pdr.getHeight() && (rotation == 0 || rotation == 180) || pdr.getHeight() > pdr.getWidth() && (rotation == 90 || rotation == 270);
|
||||
|
||||
PDRectangle cropbox = pdPage.getCropBox();
|
||||
classificationDocument.getVisualizations().addRulingVisualization(stripper.getRulings(), pageNumber);
|
||||
classificationDocument.getLayoutDebugLayer().addRulingVisualization(stripper.getRulings(), pageNumber);
|
||||
CleanRulings cleanRulings = rulingCleaningService.deduplicateAndStraightenRulings(pdfTableCells.get(pageNumber), stripper.getRulings());
|
||||
|
||||
PageInformation pageInformation = PageInformation.fromPDPage(pageNumber, pdPage);
|
||||
List<Cell> emptyTableCells = TableExtractionService.findCells(cleanRulings.getHorizontals(), cleanRulings.getVerticals(), pageInformation);
|
||||
classificationDocument.getVisualizations().addCellVisualizations(emptyTableCells, pageNumber);
|
||||
classificationDocument.getLayoutDebugLayer().addCellVisualizations(emptyTableCells, pageNumber);
|
||||
|
||||
TextRulingsClassifier.classifyUnderlinedAndStrikethroughText(words, cleanRulings);
|
||||
|
||||
List<Box> graphics = graphicExtractorService.extractPathElementGraphics(originDocument,
|
||||
pdPage,
|
||||
pageNumber,
|
||||
cleanRulings,
|
||||
stripper.getTextPositionSequences(),
|
||||
|
||||
false);
|
||||
List<Box> graphics = graphicExtractorService.extractPathElementGraphics(originDocument, pdPage, pageNumber, cleanRulings, stripper.getTextPositionSequences(), false);
|
||||
|
||||
pdfImages.computeIfAbsent(pageNumber, x -> new ArrayList<>())
|
||||
.addAll(graphics.stream()
|
||||
.map(g -> new ClassifiedImage(new Rectangle2D.Double(g.x1, g.y1, g.width(), g.height()), ImageType.GRAPHIC, false, stripper.getPageNumber()))
|
||||
.map(g -> new ClassifiedImage(new Rectangle2D.Double(g.x1, g.y1, g.width(), g.height()),
|
||||
ImageType.GRAPHIC,
|
||||
false,
|
||||
stripper.getPageNumber(),
|
||||
""))
|
||||
.toList());
|
||||
|
||||
ClassificationPage classificationPage = switch (layoutParsingType) {
|
||||
case REDACT_MANAGER_OLD ->
|
||||
redactManagerBlockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings, classificationDocument.getVisualizations());
|
||||
redactManagerBlockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings, classificationDocument.getLayoutDebugLayer());
|
||||
case DOCUMINE_OLD -> docuMineBlockificationService.blockify(words, cleanRulings);
|
||||
case DOCUMINE, REDACT_MANAGER, REDACT_MANAGER_PARAGRAPH_DEBUG, REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH -> docstrumBlockificationService.blockify(words,
|
||||
cleanRulings,
|
||||
true,
|
||||
classificationDocument.getVisualizations(),
|
||||
layoutParsingType);
|
||||
case CLARIFYND, CLARIFYND_PARAGRAPH_DEBUG -> docstrumBlockificationService.blockify(words,
|
||||
cleanRulings,
|
||||
false,
|
||||
classificationDocument.getVisualizations(),
|
||||
layoutParsingType);
|
||||
case DOCUMINE, REDACT_MANAGER, REDACT_MANAGER_PARAGRAPH_DEBUG, REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH ->
|
||||
docstrumBlockificationService.blockify(words, cleanRulings, true, classificationDocument.getLayoutDebugLayer(), layoutParsingType);
|
||||
case CLARIFYND, CLARIFYND_PARAGRAPH_DEBUG ->
|
||||
docstrumBlockificationService.blockify(words, cleanRulings, false, classificationDocument.getLayoutDebugLayer(), layoutParsingType);
|
||||
};
|
||||
|
||||
classificationPage.setCleanRulings(cleanRulings);
|
||||
@@ -338,7 +321,7 @@ public class LayoutParsingPipeline {
|
||||
classificationPage.setPageWidth(cropbox.getWidth());
|
||||
classificationPage.setPageHeight(cropbox.getHeight());
|
||||
|
||||
if(layoutParsingType != LayoutParsingType.REDACT_MANAGER_OLD && layoutParsingType != LayoutParsingType.DOCUMINE_OLD) {
|
||||
if (layoutParsingType != LayoutParsingType.REDACT_MANAGER_OLD && layoutParsingType != LayoutParsingType.DOCUMINE_OLD) {
|
||||
List<OutlineObject> outlineObjects = classificationDocument.getOutlineObjectTree().getOutlineObjectsPerPage().getOrDefault(pageNumber - 1, new ArrayList<>());
|
||||
|
||||
OutlineObject notFoundOutlineObject = null;
|
||||
@@ -352,7 +335,7 @@ public class LayoutParsingPipeline {
|
||||
}
|
||||
}
|
||||
|
||||
classificationDocument.getVisualizations().addMarkedContentVisualizations(stripper.getMarkedContents(), pageNumber);
|
||||
classificationDocument.getLayoutDebugLayer().addMarkedContentVisualizations(stripper.getMarkedContents(), pageNumber);
|
||||
// MarkedContent needs to be converted at this point, otherwise it leads to GC Problems in Pdfbox.
|
||||
classificationPage.setMarkedContentBboxPerType(convertMarkedContents(stripper.getMarkedContents()));
|
||||
|
||||
@@ -383,12 +366,12 @@ public class LayoutParsingPipeline {
|
||||
log.info("Calculating BodyTextFrame for {}", identifier);
|
||||
bodyTextFrameService.setBodyTextFrames(classificationDocument, layoutParsingType);
|
||||
for (ClassificationPage page : classificationDocument.getPages()) {
|
||||
classificationDocument.getVisualizations().addCleanRulingVisualization(page.getCleanRulings(), page.getPageNumber());
|
||||
classificationDocument.getLayoutDebugLayer().addCleanRulingVisualization(page.getCleanRulings(), page.getPageNumber());
|
||||
}
|
||||
log.info("Classify TextBlocks for {}", identifier);
|
||||
switch (layoutParsingType) {
|
||||
case REDACT_MANAGER, REDACT_MANAGER_PARAGRAPH_DEBUG, REDACT_MANAGER_OLD, CLARIFYND_PARAGRAPH_DEBUG, REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH -> redactManagerClassificationService.classifyDocument(
|
||||
classificationDocument);
|
||||
case REDACT_MANAGER, REDACT_MANAGER_PARAGRAPH_DEBUG, REDACT_MANAGER_OLD, CLARIFYND_PARAGRAPH_DEBUG, REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH ->
|
||||
redactManagerClassificationService.classifyDocument(classificationDocument);
|
||||
case DOCUMINE_OLD, DOCUMINE -> docuMineClassificationService.classifyDocument(classificationDocument);
|
||||
case CLARIFYND -> clarifyndClassificationService.classifyDocument(classificationDocument);
|
||||
}
|
||||
|
||||
+3
-4
@@ -5,7 +5,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.service.ViewerDocumentService;
|
||||
import com.knecon.fforesight.service.viewerdoc.service.PDFTronViewerDocumentService;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
@@ -13,12 +13,11 @@ import io.micrometer.observation.ObservationRegistry;
|
||||
@ComponentScan
|
||||
public class LayoutParsingServiceProcessorConfiguration {
|
||||
|
||||
|
||||
@Bean
|
||||
@Autowired
|
||||
public ViewerDocumentService viewerDocumentService(ObservationRegistry registry) {
|
||||
public PDFTronViewerDocumentService viewerDocumentService(ObservationRegistry registry) {
|
||||
|
||||
return new ViewerDocumentService(registry);
|
||||
return new PDFTronViewerDocumentService(registry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+30
-5
@@ -1,9 +1,11 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -21,6 +23,7 @@ import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsi
|
||||
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.image.ImageServiceResponse;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.TableServiceResponse;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.VisualLayoutParsingResponse;
|
||||
import com.knecon.fforesight.service.viewerdoc.service.ViewerDocVersioningUtility;
|
||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
@@ -36,6 +39,7 @@ public class LayoutParsingStorageService {
|
||||
private final StorageService storageService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
|
||||
@Observed(name = "LayoutParsingStorageService", contextualName = "get-origin-file")
|
||||
public File getOriginFile(String storageId) throws IOException {
|
||||
|
||||
@@ -53,11 +57,18 @@ public class LayoutParsingStorageService {
|
||||
}
|
||||
File tempFile = createTempFile("viewerDocument", ".pdf");
|
||||
storageService.downloadTo(TenantContext.getTenantId(), storageId, tempFile);
|
||||
|
||||
if (!ViewerDocVersioningUtility.isCurrentVersion(tempFile)) {
|
||||
assert tempFile.delete();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(tempFile);
|
||||
}
|
||||
|
||||
|
||||
public ImageServiceResponse getImagesFile(String storageId) throws IOException {
|
||||
@SneakyThrows
|
||||
public ImageServiceResponse getImagesFile(String storageId) {
|
||||
|
||||
try (InputStream inputStream = getObject(storageId)) {
|
||||
|
||||
@@ -68,7 +79,8 @@ public class LayoutParsingStorageService {
|
||||
}
|
||||
|
||||
|
||||
public TableServiceResponse getTablesFile(String storageId) throws IOException {
|
||||
@SneakyThrows
|
||||
public TableServiceResponse getTablesFile(String storageId) {
|
||||
|
||||
try (var tableClassificationStream = getObject(storageId)) {
|
||||
|
||||
@@ -78,11 +90,12 @@ public class LayoutParsingStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
public VisualLayoutParsingResponse getVisualLayoutParsingFile(String storageId) throws IOException {
|
||||
|
||||
@SneakyThrows
|
||||
public VisualLayoutParsingResponse getVisualLayoutParsingFile(String storageId) {
|
||||
|
||||
try (InputStream inputStream = getObject(storageId)) {
|
||||
VisualLayoutParsingResponse visualLayoutParsingResponse = objectMapper.readValue(inputStream, VisualLayoutParsingResponse.class);
|
||||
return visualLayoutParsingResponse;
|
||||
return objectMapper.readValue(inputStream, VisualLayoutParsingResponse.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,4 +167,16 @@ public class LayoutParsingStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Observed(name = "LayoutParsingStorageService", contextualName = "store-markdown-file")
|
||||
public void storeMarkdownFile(String markdownFileStorageId, String markdownContent) {
|
||||
|
||||
try (InputStream inputStream = new ByteArrayInputStream(markdownContent.getBytes(StandardCharsets.UTF_8))) {
|
||||
|
||||
storageService.storeObject(TenantContext.getTenantId(), markdownFileStorageId, inputStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -17,4 +17,5 @@ public class LayoutparserSettings {
|
||||
|
||||
boolean debug;
|
||||
LayoutParsingType layoutParsingTypeOverride;
|
||||
String pdftronLicense;
|
||||
}
|
||||
|
||||
+3
-3
@@ -18,7 +18,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRul
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextDirection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutDebugLayer;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -33,7 +33,7 @@ public class DocstrumSegmentationService {
|
||||
private final ReadingOrderService readingOrderService;
|
||||
|
||||
|
||||
public List<Zone> segmentPage(List<TextPositionSequence> textPositions, boolean xyOrder, CleanRulings usedRulings, LayoutparsingVisualizations visualizations) {
|
||||
public List<Zone> segmentPage(List<TextPositionSequence> textPositions, boolean xyOrder, CleanRulings usedRulings, LayoutDebugLayer visualizations) {
|
||||
|
||||
List<Zone> zones = new ArrayList<>();
|
||||
zones.addAll(computeZones(textPositions, usedRulings, visualizations, TextDirection.ZERO));
|
||||
@@ -45,7 +45,7 @@ public class DocstrumSegmentationService {
|
||||
}
|
||||
|
||||
|
||||
private List<Zone> computeZones(List<TextPositionSequence> textPositions, CleanRulings rulings, LayoutparsingVisualizations visualizations, TextDirection direction) {
|
||||
private List<Zone> computeZones(List<TextPositionSequence> textPositions, CleanRulings rulings, LayoutDebugLayer visualizations, TextDirection direction) {
|
||||
|
||||
List<RedTextPosition> positions = textPositions.stream()
|
||||
.filter(t -> t.getDir() == direction)
|
||||
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.markdown;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.commonmark.Extension;
|
||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||
import org.commonmark.ext.gfm.tables.TableBody;
|
||||
import org.commonmark.ext.gfm.tables.TableCell;
|
||||
import org.commonmark.ext.gfm.tables.TableHead;
|
||||
import org.commonmark.ext.gfm.tables.TableRow;
|
||||
import org.commonmark.ext.gfm.tables.TablesExtension;
|
||||
import org.commonmark.node.Block;
|
||||
import org.commonmark.node.Document;
|
||||
import org.commonmark.node.Emphasis;
|
||||
import org.commonmark.node.HardLineBreak;
|
||||
import org.commonmark.node.Heading;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.Paragraph;
|
||||
import org.commonmark.node.StrongEmphasis;
|
||||
import org.commonmark.node.Text;
|
||||
import org.commonmark.renderer.markdown.MarkdownRenderer;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.AbstractNodeVisitor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Headline;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Table;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
public class MarkdownMapper extends AbstractNodeVisitor {
|
||||
|
||||
Document markdownDocument = new Document();
|
||||
|
||||
|
||||
public String toMarkdownContent(com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document document) {
|
||||
|
||||
visit(document);
|
||||
|
||||
return buildRenderer().render(this.markdownDocument);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(Headline headline) {
|
||||
|
||||
markdownDocument.appendChild(parseHeadline(headline));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph paragraph) {
|
||||
|
||||
markdownDocument.appendChild(parseParagraph(paragraph));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(Table table) {
|
||||
|
||||
markdownDocument.appendChild(parseTable(table));
|
||||
}
|
||||
|
||||
|
||||
private static MarkdownRenderer buildRenderer() {
|
||||
|
||||
List<Extension> extensions = List.of(TablesExtension.create());
|
||||
return MarkdownRenderer.builder().extensions(extensions).build();
|
||||
}
|
||||
|
||||
|
||||
private Block parseTable(Table table) {
|
||||
|
||||
// if (table.getNumberOfRows() == 1 && table.getNumberOfCols() == 1) {
|
||||
// org.commonmark.node.Paragraph markdownParagraph = new org.commonmark.node.Paragraph();
|
||||
// parseTextBlock(table.getTextBlock(), true).forEach(markdownParagraph::appendChild);
|
||||
// return markdownParagraph;
|
||||
// }
|
||||
|
||||
TableBlock tableNode = new TableBlock();
|
||||
TableHead head = new TableHead();
|
||||
TableRow tableRow = createTableRow(table, 0);
|
||||
head.appendChild(tableRow);
|
||||
int row = 1;
|
||||
tableNode.appendChild(head);
|
||||
TableBody tableBody = new TableBody();
|
||||
for (; row < table.getNumberOfRows(); row++) {
|
||||
tableBody.appendChild(createTableRow(table, row));
|
||||
}
|
||||
tableNode.appendChild(tableBody);
|
||||
return tableNode;
|
||||
}
|
||||
|
||||
|
||||
private TableRow createTableRow(Table table, int row) {
|
||||
|
||||
TableRow tableRow = new TableRow();
|
||||
table.streamRow(row)
|
||||
.map(this::createTableCell)
|
||||
.forEach(tableRow::appendChild);
|
||||
return tableRow;
|
||||
}
|
||||
|
||||
|
||||
private Node createTableCell(com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.TableCell tc) {
|
||||
|
||||
var cell = new TableCell();
|
||||
List<SemanticNode> childNodes = tc.streamChildren()
|
||||
.toList();
|
||||
if (childNodes.isEmpty()) {
|
||||
parseTextBlock(tc.getTextBlock(), false).forEach(cell::appendChild);
|
||||
} else {
|
||||
childNodes.forEach(semanticNode -> parseTextBlock(semanticNode.getTextBlock(), false).forEach(cell::appendChild));
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
private Paragraph parseParagraph(com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph paragraph) {
|
||||
|
||||
org.commonmark.node.Paragraph markdownParagraph = new org.commonmark.node.Paragraph();
|
||||
parseTextBlock(paragraph.getTextBlock(), true).forEach(markdownParagraph::appendChild);
|
||||
return markdownParagraph;
|
||||
}
|
||||
|
||||
|
||||
private Heading parseHeadline(Headline headline) {
|
||||
|
||||
Heading heading = new Heading();
|
||||
heading.setLevel(headline.getTreeId().size());
|
||||
heading.appendChild(parseTextBlockAsText(headline.getTextBlock()));
|
||||
return heading;
|
||||
|
||||
}
|
||||
|
||||
|
||||
private Text parseTextBlockAsText(TextBlock textBlock) {
|
||||
|
||||
return new Text(textBlock.getSearchText());
|
||||
}
|
||||
|
||||
|
||||
private List<Node> parseTextBlock(TextBlock textBlock, boolean includeLineBreaks) {
|
||||
|
||||
LinkedList<Node> result = new LinkedList<>();
|
||||
List<TextRangeWithTextType> textRanges = mergeTextStyles(textBlock);
|
||||
|
||||
String fullText = getText(textBlock, textBlock.getTextRange(), includeLineBreaks);
|
||||
List<Integer> lineTextSizes = getLineTextSizes(fullText);
|
||||
int idx = 0;
|
||||
int charCount = 0;
|
||||
for (TextRangeWithTextType textRange : textRanges) {
|
||||
String text = getText(textBlock, textRange.textRange(), includeLineBreaks);
|
||||
String[] lines = text.split("\n");
|
||||
for (String line : lines) {
|
||||
charCount += line.length();
|
||||
switch (textRange.fontStyle()) {
|
||||
case REGULAR -> result.add(new Text(line));
|
||||
case BOLD -> {
|
||||
StrongEmphasis boldBlock = new StrongEmphasis();
|
||||
boldBlock.appendChild(new Text(line));
|
||||
result.add(boldBlock);
|
||||
}
|
||||
case ITALIC -> {
|
||||
Emphasis italicBlock = new Emphasis();
|
||||
italicBlock.appendChild(new Text(line));
|
||||
result.add(italicBlock);
|
||||
}
|
||||
case BOLD_ITALIC -> {
|
||||
Emphasis italicBlock = new Emphasis();
|
||||
|
||||
StrongEmphasis boldBlock = new StrongEmphasis();
|
||||
boldBlock.appendChild(new Text(line));
|
||||
|
||||
italicBlock.appendChild(boldBlock);
|
||||
result.add(italicBlock);
|
||||
}
|
||||
}
|
||||
if (includeLineBreaks && lineTextSizes.get(idx).equals(charCount)) {
|
||||
result.add(new HardLineBreak());
|
||||
idx++;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (!result.isEmpty() && result.getLast() instanceof HardLineBreak) {
|
||||
result.removeLast();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static List<Integer> getLineTextSizes(String input) {
|
||||
|
||||
String[] parts = input.split("\n");
|
||||
List<Integer> textSizes = new ArrayList<>();
|
||||
|
||||
int size = 0;
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
size += parts[i].length();
|
||||
textSizes.add(size);
|
||||
}
|
||||
|
||||
if (textSizes.isEmpty()) {
|
||||
textSizes.add(0);
|
||||
}
|
||||
|
||||
return textSizes;
|
||||
}
|
||||
|
||||
|
||||
private static String getText(TextBlock textBlock, TextRange textRange, boolean includeLineBreaks) {
|
||||
|
||||
return includeLineBreaks ? textBlock.subSequenceWithLineBreaks(textRange) : textBlock.subSequence(textRange).toString();
|
||||
}
|
||||
|
||||
|
||||
private List<TextRangeWithTextType> mergeTextStyles(TextBlock textBlock) {
|
||||
|
||||
List<TextRangeWithTextType> result = new ArrayList<>();
|
||||
|
||||
TreeMap<Integer, Set<FontStyleChange>> styleChanges = new TreeMap<>();
|
||||
|
||||
int start = textBlock.getTextRange().start();
|
||||
int end = textBlock.getTextRange().end();
|
||||
|
||||
for (TextRange bold : textBlock.getBoldTextBoundaries()) {
|
||||
styleChanges.computeIfAbsent(bold.start() + start, k -> new HashSet<>()).add(FontStyleChange.enter(FontStyle.BOLD));
|
||||
styleChanges.computeIfAbsent(bold.end() + start, k -> new HashSet<>()).add(FontStyleChange.leave(FontStyle.BOLD));
|
||||
}
|
||||
|
||||
for (TextRange italic : textBlock.getItalicTextBoundaries()) {
|
||||
styleChanges.computeIfAbsent(italic.start() + start, k -> new HashSet<>()).add(FontStyleChange.enter(FontStyle.ITALIC));
|
||||
styleChanges.computeIfAbsent(italic.end() + start, k -> new HashSet<>()).add(FontStyleChange.leave(FontStyle.ITALIC));
|
||||
}
|
||||
|
||||
if (styleChanges.isEmpty()) {
|
||||
result.add(new TextRangeWithTextType(new TextRange(start, end), FontStyle.REGULAR));
|
||||
return result;
|
||||
}
|
||||
|
||||
Set<FontStyle> currentStyles = new HashSet<>();
|
||||
currentStyles.add(FontStyle.REGULAR);
|
||||
|
||||
for (Map.Entry<Integer, Set<FontStyleChange>> entry : styleChanges.entrySet()) {
|
||||
int point = entry.getKey();
|
||||
Set<FontStyleChange> changes = entry.getValue();
|
||||
|
||||
if (point > start) {
|
||||
FontStyle style = determineFontStyle(currentStyles);
|
||||
result.add(new TextRangeWithTextType(new TextRange(start, point), style));
|
||||
}
|
||||
|
||||
changes.stream()
|
||||
.filter(FontStyleChange::leave)
|
||||
.map(FontStyleChange::style)
|
||||
.toList()
|
||||
.forEach(currentStyles::remove);
|
||||
|
||||
currentStyles.addAll(changes.stream()
|
||||
.filter(FontStyleChange::enter)
|
||||
.map(FontStyleChange::style)
|
||||
.toList());
|
||||
|
||||
if (currentStyles.isEmpty()) {
|
||||
currentStyles.add(FontStyle.REGULAR);
|
||||
}
|
||||
|
||||
start = point;
|
||||
}
|
||||
|
||||
if (start < end) {
|
||||
FontStyle style = determineFontStyle(currentStyles);
|
||||
result.add(new TextRangeWithTextType(new TextRange(start, textBlock.getTextRange().end()), style));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private FontStyle determineFontStyle(Set<FontStyle> styles) {
|
||||
|
||||
if (styles.contains(FontStyle.BOLD) && styles.contains(FontStyle.ITALIC)) {
|
||||
return FontStyle.BOLD_ITALIC;
|
||||
} else if (styles.contains(FontStyle.BOLD)) {
|
||||
return FontStyle.BOLD;
|
||||
} else if (styles.contains(FontStyle.ITALIC)) {
|
||||
return FontStyle.ITALIC;
|
||||
} else {
|
||||
return FontStyle.REGULAR;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum FontStyle {
|
||||
REGULAR,
|
||||
BOLD,
|
||||
ITALIC,
|
||||
BOLD_ITALIC;
|
||||
}
|
||||
|
||||
record FontStyleChange(boolean enter, FontStyle style) {
|
||||
|
||||
public static FontStyleChange enter(FontStyle style) {
|
||||
|
||||
return new FontStyleChange(true, style);
|
||||
}
|
||||
|
||||
|
||||
public static FontStyleChange leave(FontStyle style) {
|
||||
|
||||
return new FontStyleChange(false, style);
|
||||
}
|
||||
|
||||
|
||||
public boolean leave() {
|
||||
|
||||
return !enter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record TextRangeWithTextType(TextRange textRange, FontStyle fontStyle) {
|
||||
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -1,9 +1,10 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
@@ -20,6 +21,9 @@ public abstract class AbstractPageBlock extends BoundingBox {
|
||||
|
||||
@JsonIgnore
|
||||
protected PageBlockType classification;
|
||||
|
||||
Set<LayoutEngine> engines = new HashSet<>();
|
||||
|
||||
@JsonIgnore
|
||||
protected int page;
|
||||
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.outline.Outlin
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.TableOfContents;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.StringFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.UnclassifiedText;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutDebugLayer;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -25,7 +25,7 @@ public class ClassificationDocument {
|
||||
private FloatFrequencyCounter fontSizeCounter = new FloatFrequencyCounter();
|
||||
private StringFrequencyCounter fontCounter = new StringFrequencyCounter();
|
||||
private StringFrequencyCounter fontStyleCounter = new StringFrequencyCounter();
|
||||
private LayoutparsingVisualizations visualizations = new LayoutparsingVisualizations();
|
||||
private LayoutDebugLayer layoutDebugLayer = new LayoutDebugLayer();
|
||||
private boolean headlines;
|
||||
|
||||
private long rulesVersion;
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph;
|
||||
|
||||
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.Header;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Headline;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Image;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Section;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SuperSection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Table;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.TableCell;
|
||||
|
||||
public abstract class AbstractNodeVisitor implements NodeVisitor {
|
||||
|
||||
@Override
|
||||
public void visit(Document document) {
|
||||
|
||||
visitChildren(document);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(SuperSection superSection) {
|
||||
|
||||
visitChildren(superSection);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(Section section) {
|
||||
|
||||
visitChildren(section);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(Headline headline) {
|
||||
|
||||
visitChildren(headline);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(Paragraph paragraph) {
|
||||
|
||||
visitChildren(paragraph);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(Footer footer) {
|
||||
|
||||
visitChildren(footer);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(Header header) {
|
||||
|
||||
visitChildren(header);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(Image image) {
|
||||
|
||||
visitChildren(image);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(Table table) {
|
||||
|
||||
visitChildren(table);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void visit(TableCell tableCell) {
|
||||
|
||||
visitChildren(tableCell);
|
||||
}
|
||||
|
||||
|
||||
private void visitChildren(SemanticNode semanticNode) {
|
||||
|
||||
semanticNode.streamChildren()
|
||||
.forEach(node -> node.accept(this));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+20
-7
@@ -2,6 +2,7 @@ package com.knecon.fforesight.service.layoutparser.processor.model.graph;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -39,7 +40,10 @@ public class DocumentTree {
|
||||
|
||||
public TextBlock buildTextBlock() {
|
||||
|
||||
return allEntriesInOrder().map(Entry::getNode).filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector());
|
||||
return allEntriesInOrder().map(Entry::getNode)
|
||||
.filter(SemanticNode::isLeaf)
|
||||
.map(SemanticNode::getLeafTextBlock)
|
||||
.collect(new TextBlockCollector());
|
||||
}
|
||||
|
||||
|
||||
@@ -113,13 +117,16 @@ public class DocumentTree {
|
||||
|
||||
public Stream<SemanticNode> childNodes(List<Integer> treeId) {
|
||||
|
||||
return getEntryById(treeId).children.stream().map(Entry::getNode);
|
||||
return getEntryById(treeId).children.stream()
|
||||
.map(Entry::getNode);
|
||||
}
|
||||
|
||||
|
||||
public Stream<SemanticNode> childNodesOfType(List<Integer> treeId, NodeType nodeType) {
|
||||
|
||||
return getEntryById(treeId).children.stream().filter(entry -> entry.node.getType().equals(nodeType)).map(Entry::getNode);
|
||||
return getEntryById(treeId).children.stream()
|
||||
.filter(entry -> entry.node.getType().equals(nodeType))
|
||||
.map(Entry::getNode);
|
||||
}
|
||||
|
||||
|
||||
@@ -156,26 +163,32 @@ public class DocumentTree {
|
||||
|
||||
public Stream<Entry> allEntriesInOrder() {
|
||||
|
||||
return Stream.of(root).flatMap(DocumentTree::flatten);
|
||||
return Stream.of(root)
|
||||
.flatMap(DocumentTree::flatten);
|
||||
}
|
||||
|
||||
|
||||
public Stream<Entry> allSubEntriesInOrder(List<Integer> parentId) {
|
||||
|
||||
return getEntryById(parentId).children.stream().flatMap(DocumentTree::flatten);
|
||||
return getEntryById(parentId).children.stream()
|
||||
.flatMap(DocumentTree::flatten);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return String.join("\n", allEntriesInOrder().map(Entry::toString).toList());
|
||||
return String.join("\n",
|
||||
allEntriesInOrder().map(Entry::toString)
|
||||
.toList());
|
||||
}
|
||||
|
||||
|
||||
private static Stream<Entry> flatten(Entry entry) {
|
||||
|
||||
return Stream.concat(Stream.of(entry), entry.children.stream().flatMap(DocumentTree::flatten));
|
||||
return Stream.concat(Stream.of(entry),
|
||||
entry.children.stream()
|
||||
.flatMap(DocumentTree::flatten));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph;
|
||||
|
||||
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.Header;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Headline;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Image;
|
||||
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.SuperSection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Table;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.TableCell;
|
||||
|
||||
public interface NodeVisitor {
|
||||
|
||||
void visit(Document document);
|
||||
|
||||
|
||||
void visit(SuperSection superSection);
|
||||
|
||||
|
||||
void visit(Section section);
|
||||
|
||||
|
||||
void visit(Headline headline);
|
||||
|
||||
|
||||
void visit(Paragraph paragraph);
|
||||
|
||||
|
||||
void visit(Footer footer);
|
||||
|
||||
|
||||
void visit(Header header);
|
||||
|
||||
|
||||
void visit(Image image);
|
||||
|
||||
|
||||
void visit(Table table);
|
||||
|
||||
|
||||
void visit(TableCell tableCell);
|
||||
|
||||
}
|
||||
+40
-21
@@ -13,13 +13,13 @@ import lombok.Setter;
|
||||
@Setter
|
||||
@EqualsAndHashCode
|
||||
@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
|
||||
public class Boundary implements Comparable<Boundary> {
|
||||
public class TextRange implements Comparable<TextRange> {
|
||||
|
||||
private int start;
|
||||
private int end;
|
||||
|
||||
|
||||
public Boundary(int start, int end) {
|
||||
public TextRange(int start, int end) {
|
||||
|
||||
if (start > end) {
|
||||
throw new IllegalArgumentException(format("start: %d > end: %d", start, end));
|
||||
@@ -47,15 +47,15 @@ public class Boundary implements Comparable<Boundary> {
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(Boundary boundary) {
|
||||
public boolean contains(TextRange textRange) {
|
||||
|
||||
return start <= boundary.start() && boundary.end() <= end;
|
||||
return start <= textRange.start() && textRange.end() <= end;
|
||||
}
|
||||
|
||||
|
||||
public boolean containedBy(Boundary boundary) {
|
||||
public boolean containedBy(TextRange textRange) {
|
||||
|
||||
return boundary.contains(this);
|
||||
return textRange.contains(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,22 +79,33 @@ public class Boundary implements Comparable<Boundary> {
|
||||
|
||||
public boolean contains(int index) {
|
||||
|
||||
return start <= index && index <= end;
|
||||
}
|
||||
|
||||
|
||||
public boolean containsExclusive(int index) {
|
||||
|
||||
return start <= index && index < end;
|
||||
}
|
||||
|
||||
|
||||
public boolean intersects(Boundary boundary) {
|
||||
public boolean intersects(TextRange textRange) {
|
||||
|
||||
return boundary.start() < this.end && this.start < boundary.end();
|
||||
return textRange.start() < this.end && this.start < textRange.end();
|
||||
}
|
||||
|
||||
|
||||
public List<Boundary> split(List<Integer> splitIndices) {
|
||||
public List<TextRange> split(List<Integer> splitIndices) {
|
||||
|
||||
if (splitIndices.stream().anyMatch(idx -> !this.contains(idx))) {
|
||||
throw new IndexOutOfBoundsException(format("%s splitting indices are out of range for %s", splitIndices.stream().filter(idx -> !this.contains(idx)).toList(), this));
|
||||
if (splitIndices.stream()
|
||||
.anyMatch(idx -> !this.containsExclusive(idx))) {
|
||||
throw new IndexOutOfBoundsException(format("%s splitting indices are out of range for %s",
|
||||
splitIndices.stream()
|
||||
.filter(idx -> !this.containsExclusive(idx))
|
||||
.toList(),
|
||||
this));
|
||||
}
|
||||
List<Boundary> splitBoundaries = new LinkedList<>();
|
||||
List<TextRange> splitBoundaries = new LinkedList<>();
|
||||
int previousIndex = start;
|
||||
for (int splitIndex : splitIndices) {
|
||||
|
||||
@@ -102,23 +113,31 @@ public class Boundary implements Comparable<Boundary> {
|
||||
if (splitIndex == previousIndex) {
|
||||
continue;
|
||||
}
|
||||
splitBoundaries.add(new Boundary(previousIndex, splitIndex));
|
||||
splitBoundaries.add(new TextRange(previousIndex, splitIndex));
|
||||
previousIndex = splitIndex;
|
||||
}
|
||||
splitBoundaries.add(new Boundary(previousIndex, end));
|
||||
splitBoundaries.add(new TextRange(previousIndex, end));
|
||||
return splitBoundaries;
|
||||
}
|
||||
|
||||
|
||||
public IntStream intStream() {
|
||||
|
||||
return IntStream.range(start, end);
|
||||
}
|
||||
|
||||
public static Boundary merge(Collection<Boundary> boundaries) {
|
||||
|
||||
int minStart = boundaries.stream().mapToInt(Boundary::start).min().orElseThrow(IllegalArgumentException::new);
|
||||
int maxEnd = boundaries.stream().mapToInt(Boundary::end).max().orElseThrow(IllegalArgumentException::new);
|
||||
return new Boundary(minStart, maxEnd);
|
||||
public static TextRange merge(Collection<TextRange> boundaries) {
|
||||
|
||||
int minStart = boundaries.stream()
|
||||
.mapToInt(TextRange::start)
|
||||
.min()
|
||||
.orElseThrow(IllegalArgumentException::new);
|
||||
int maxEnd = boundaries.stream()
|
||||
.mapToInt(TextRange::end)
|
||||
.max()
|
||||
.orElseThrow(IllegalArgumentException::new);
|
||||
return new TextRange(minStart, maxEnd);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,12 +149,12 @@ public class Boundary implements Comparable<Boundary> {
|
||||
|
||||
|
||||
@Override
|
||||
public int compareTo(Boundary boundary) {
|
||||
public int compareTo(TextRange textRange) {
|
||||
|
||||
if (end < boundary.end() && start < boundary.start()) {
|
||||
if (end < textRange.end() && start < textRange.start()) {
|
||||
return -1;
|
||||
}
|
||||
if (start > boundary.start() && end > boundary.end()) {
|
||||
if (start > textRange.start() && end > textRange.end()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
+16
-16
@@ -11,7 +11,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.IdBuilder;
|
||||
@@ -28,11 +28,11 @@ import lombok.experimental.FieldDefaults;
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class RedactionEntity {
|
||||
public class TextEntity {
|
||||
|
||||
// initial values
|
||||
@EqualsAndHashCode.Include
|
||||
final Boundary boundary;
|
||||
final TextRange textRange;
|
||||
@EqualsAndHashCode.Include
|
||||
final String type;
|
||||
@EqualsAndHashCode.Include
|
||||
@@ -47,7 +47,7 @@ public class RedactionEntity {
|
||||
boolean dictionaryEntry;
|
||||
boolean dossierDictionaryEntry;
|
||||
Set<Engine> engines;
|
||||
Set<RedactionEntity> references;
|
||||
Set<TextEntity> references;
|
||||
@Builder.Default
|
||||
Deque<Integer> matchedRules = new LinkedList<>();
|
||||
String redactionReason;
|
||||
@@ -66,9 +66,9 @@ public class RedactionEntity {
|
||||
SemanticNode deepestFullyContainingNode;
|
||||
|
||||
|
||||
public static RedactionEntity initialEntityNode(Boundary boundary, String type, EntityType entityType) {
|
||||
public static TextEntity initialEntityNode(TextRange textRange, String type, EntityType entityType) {
|
||||
|
||||
return RedactionEntity.builder().type(type).entityType(entityType).boundary(boundary).engines(new HashSet<>()).references(new HashSet<>()).build();
|
||||
return TextEntity.builder().type(type).entityType(entityType).textRange(textRange).engines(new HashSet<>()).references(new HashSet<>()).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ public class RedactionEntity {
|
||||
public List<RedactionPosition> getRedactionPositionsPerPage() {
|
||||
|
||||
if (redactionPositionsPerPage == null || redactionPositionsPerPage.isEmpty()) {
|
||||
Map<Page, List<Rectangle2D>> rectanglesPerLinePerPage = deepestFullyContainingNode.getTextBlock().getPositionsPerPage(boundary);
|
||||
Map<Page, List<Rectangle2D>> rectanglesPerLinePerPage = deepestFullyContainingNode.getTextBlock().getPositionsPerPage(textRange);
|
||||
|
||||
Page firstPage = rectanglesPerLinePerPage.keySet()
|
||||
.stream()
|
||||
@@ -155,21 +155,21 @@ public class RedactionEntity {
|
||||
}
|
||||
|
||||
|
||||
public boolean containedBy(RedactionEntity redactionEntity) {
|
||||
public boolean containedBy(TextEntity textEntity) {
|
||||
|
||||
return this.boundary.containedBy(redactionEntity.getBoundary());
|
||||
return this.textRange.containedBy(textEntity.getTextRange());
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(RedactionEntity redactionEntity) {
|
||||
public boolean contains(TextEntity textEntity) {
|
||||
|
||||
return this.boundary.contains(redactionEntity.getBoundary());
|
||||
return this.textRange.contains(textEntity.getTextRange());
|
||||
}
|
||||
|
||||
|
||||
public boolean intersects(RedactionEntity redactionEntity) {
|
||||
public boolean intersects(TextEntity textEntity) {
|
||||
|
||||
return this.boundary.intersects(redactionEntity.getBoundary());
|
||||
return this.textRange.intersects(textEntity.getTextRange());
|
||||
}
|
||||
|
||||
|
||||
@@ -185,13 +185,13 @@ public class RedactionEntity {
|
||||
}
|
||||
|
||||
|
||||
public void addReference(RedactionEntity reference) {
|
||||
public void addReference(TextEntity reference) {
|
||||
|
||||
references.add(reference);
|
||||
}
|
||||
|
||||
|
||||
public void addReferences(List<RedactionEntity> references) {
|
||||
public void addReferences(List<TextEntity> references) {
|
||||
|
||||
this.references.addAll(references);
|
||||
}
|
||||
@@ -210,7 +210,7 @@ public class RedactionEntity {
|
||||
sb.append("Entity[\"");
|
||||
sb.append(value);
|
||||
sb.append("\", ");
|
||||
sb.append(boundary);
|
||||
sb.append(textRange);
|
||||
sb.append(", pages[");
|
||||
pages.forEach(page -> {
|
||||
sb.append(page.getNumber());
|
||||
+2
-2
@@ -8,7 +8,7 @@ import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.TextEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
@@ -39,7 +39,7 @@ public abstract class AbstractSemanticNode implements GenericSemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
|
||||
+13
-6
@@ -11,8 +11,9 @@ import java.util.stream.Stream;
|
||||
|
||||
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.NodeVisitor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutDebugLayer;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -33,7 +34,7 @@ public class Document extends AbstractSemanticNode {
|
||||
Set<Page> pages;
|
||||
Integer numberOfPages;
|
||||
|
||||
LayoutparsingVisualizations visualizations;
|
||||
LayoutDebugLayer layoutDebugLayer;
|
||||
|
||||
|
||||
@Override
|
||||
@@ -60,8 +61,8 @@ public class Document extends AbstractSemanticNode {
|
||||
*
|
||||
* @return A list of main sections within the document
|
||||
* @deprecated This method is marked for removal.
|
||||
* Use {@link #streamChildrenOfType(NodeType)} instead,
|
||||
* or {@link #getChildrenOfTypeSectionOrSuperSection()} which returns children of type SECTION as well as SUPER_SECTION.
|
||||
* Use {@link #streamChildrenOfType(NodeType)} instead,
|
||||
* or {@link #getChildrenOfTypeSectionOrSuperSection()} which returns children of type SECTION as well as SUPER_SECTION.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public List<Section> getMainSections() {
|
||||
@@ -101,8 +102,7 @@ public class Document extends AbstractSemanticNode {
|
||||
public Headline getHeadline() {
|
||||
|
||||
return streamAllSubNodesOfType(NodeType.HEADLINE).map(node -> (Headline) node)
|
||||
.findFirst()
|
||||
.orElse(Headline.builder().build());
|
||||
.findFirst().orElse(Headline.builder().build());
|
||||
}
|
||||
|
||||
|
||||
@@ -163,4 +163,11 @@ public class Document extends AbstractSemanticNode {
|
||||
return bBox;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -1,6 +1,7 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
@@ -34,6 +35,12 @@ public class Footer extends AbstractSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TextBlock getTextBlock() {
|
||||
|
||||
|
||||
+8
@@ -1,6 +1,7 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
@@ -27,6 +28,13 @@ public class Header extends AbstractSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
|
||||
|
||||
+8
@@ -1,6 +1,7 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
@@ -34,6 +35,13 @@ public class Headline extends AbstractSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TextBlock getTextBlock() {
|
||||
|
||||
|
||||
+22
@@ -7,6 +7,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
@@ -28,6 +29,8 @@ public class Image extends AbstractSemanticNode {
|
||||
|
||||
String id;
|
||||
|
||||
String representationHash;
|
||||
|
||||
ImageType imageType;
|
||||
boolean transparent;
|
||||
Rectangle2D position;
|
||||
@@ -84,10 +87,29 @@ public class Image extends AbstractSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public double getArea() {
|
||||
|
||||
return position.getWidth() * position.getHeight();
|
||||
}
|
||||
|
||||
|
||||
public boolean isFullPageImage() {
|
||||
|
||||
return imageType.equals(ImageType.OCR) || getArea() >= 0.5 * page.getArea();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-1
@@ -6,7 +6,6 @@ public enum ImageType {
|
||||
LOGO,
|
||||
FORMULA,
|
||||
SIGNATURE,
|
||||
|
||||
SIGNATURE_VISUAL,
|
||||
OTHER,
|
||||
OCR,
|
||||
|
||||
+12
-3
@@ -6,7 +6,7 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.TextEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlockCollector;
|
||||
|
||||
@@ -39,7 +39,7 @@ public class Page {
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
@@ -60,7 +60,10 @@ public class Page {
|
||||
|
||||
public TextBlock getMainBodyTextBlock() {
|
||||
|
||||
return mainBody.stream().filter(SemanticNode::isLeaf).map(SemanticNode::getLeafTextBlock).collect(new TextBlockCollector());
|
||||
return mainBody.stream()
|
||||
.filter(SemanticNode::isLeaf)
|
||||
.map(SemanticNode::getLeafTextBlock)
|
||||
.collect(new TextBlockCollector());
|
||||
}
|
||||
|
||||
|
||||
@@ -84,4 +87,10 @@ public class Page {
|
||||
return o instanceof Page && o.hashCode() == this.hashCode();
|
||||
}
|
||||
|
||||
|
||||
public double getArea() {
|
||||
|
||||
return height * width;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
@@ -1,6 +1,7 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
@@ -34,6 +35,13 @@ public class Paragraph extends AbstractSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TextBlock getTextBlock() {
|
||||
|
||||
|
||||
+10
-4
@@ -1,6 +1,7 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -26,15 +27,20 @@ public class Section extends AbstractSemanticNode {
|
||||
public Headline getHeadline() {
|
||||
|
||||
return streamChildrenOfType(NodeType.HEADLINE).map(node -> (Headline) node)
|
||||
.findFirst()
|
||||
.orElseGet(() -> getParent().getHeadline());
|
||||
.findFirst().orElseGet(() -> getParent().getHeadline());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
|
||||
public boolean hasTables() {
|
||||
|
||||
return streamAllSubNodesOfType(NodeType.TABLE).findAny()
|
||||
.isPresent();
|
||||
return streamAllSubNodesOfType(NodeType.TABLE).findAny().isPresent();
|
||||
}
|
||||
|
||||
|
||||
|
||||
+67
-41
@@ -14,14 +14,16 @@ import java.util.stream.Stream;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.EntityType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.TextEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlockCollector;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.BBoxMergingUtility;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
|
||||
public interface SemanticNode {
|
||||
|
||||
@@ -42,7 +44,9 @@ public interface SemanticNode {
|
||||
*/
|
||||
default TextBlock getTextBlock() {
|
||||
|
||||
return streamAllSubNodes().filter(SemanticNode::isLeaf).map(SemanticNode::getTextBlock).collect(new TextBlockCollector());
|
||||
return streamAllSubNodes().filter(SemanticNode::isLeaf)
|
||||
.map(SemanticNode::getTextBlock)
|
||||
.collect(new TextBlockCollector());
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +56,7 @@ public interface SemanticNode {
|
||||
*
|
||||
* @return Set of all Entities associated with this Node
|
||||
*/
|
||||
Set<RedactionEntity> getEntities();
|
||||
Set<TextEntity> getEntities();
|
||||
|
||||
|
||||
/**
|
||||
@@ -68,7 +72,9 @@ public interface SemanticNode {
|
||||
|
||||
default Page getFirstPage() {
|
||||
|
||||
return getTextBlock().getPages().stream().min(Comparator.comparingInt(Page::getNumber)).orElseThrow(() -> new IllegalStateException("SemanticNode has no Page!"));
|
||||
return getTextBlock().getPages()
|
||||
.stream()
|
||||
.min(Comparator.comparingInt(Page::getNumber)).orElseThrow(() -> new IllegalStateException("SemanticNode has no Page!"));
|
||||
}
|
||||
|
||||
|
||||
@@ -77,18 +83,19 @@ public interface SemanticNode {
|
||||
*
|
||||
* @return Set of PageNodes this node appears on.
|
||||
*/
|
||||
default Set<Page> getPages(Boundary boundary) {
|
||||
default Set<Page> getPages(TextRange textRange) {
|
||||
|
||||
if (!getBoundary().contains(boundary)) {
|
||||
throw new IllegalArgumentException(format("%s which was used to query for pages is not contained in the %s of this node!", boundary, getBoundary()));
|
||||
if (!getBoundary().contains(textRange)) {
|
||||
throw new IllegalArgumentException(format("%s which was used to query for pages is not contained in the %s of this node!", textRange, getBoundary()));
|
||||
}
|
||||
return getTextBlock().getPages(boundary);
|
||||
return getTextBlock().getPages(textRange);
|
||||
}
|
||||
|
||||
|
||||
default boolean isOnPage(int pageNumber) {
|
||||
|
||||
return getPages().stream().anyMatch(page -> page.getNumber() == pageNumber);
|
||||
return getPages().stream()
|
||||
.anyMatch(page -> page.getNumber() == pageNumber);
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +210,9 @@ public interface SemanticNode {
|
||||
*/
|
||||
default boolean hasEntitiesOfType(String type) {
|
||||
|
||||
return getEntities().stream().filter(entity -> entity.getEntityType().equals(EntityType.ENTITY)).anyMatch(redactionEntity -> redactionEntity.getType().equals(type));
|
||||
return getEntities().stream()
|
||||
.filter(entity -> entity.getEntityType().equals(EntityType.ENTITY))
|
||||
.anyMatch(redactionEntity -> redactionEntity.getType().equals(type));
|
||||
}
|
||||
|
||||
|
||||
@@ -213,9 +222,11 @@ public interface SemanticNode {
|
||||
* @param type string representing the type of entities to return
|
||||
* @return List of RedactionEntities of any the type
|
||||
*/
|
||||
default List<RedactionEntity> getEntitiesOfType(String type) {
|
||||
default List<TextEntity> getEntitiesOfType(String type) {
|
||||
|
||||
return getEntities().stream().filter(redactionEntity -> redactionEntity.getType().equals(type)).toList();
|
||||
return getEntities().stream()
|
||||
.filter(redactionEntity -> redactionEntity.getType().equals(type))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -225,9 +236,11 @@ public interface SemanticNode {
|
||||
* @param types A list of strings representing the types of entities to return
|
||||
* @return List of RedactionEntities of any provided type
|
||||
*/
|
||||
default List<RedactionEntity> getEntitiesOfType(List<String> types) {
|
||||
default List<TextEntity> getEntitiesOfType(List<String> types) {
|
||||
|
||||
return getEntities().stream().filter(redactionEntity -> redactionEntity.isAnyType(types)).toList();
|
||||
return getEntities().stream()
|
||||
.filter(redactionEntity -> redactionEntity.isAnyType(types))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -279,7 +292,8 @@ public interface SemanticNode {
|
||||
*/
|
||||
default boolean containsStrings(List<String> strings) {
|
||||
|
||||
return strings.stream().allMatch(this::containsString);
|
||||
return strings.stream()
|
||||
.allMatch(this::containsString);
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +317,8 @@ public interface SemanticNode {
|
||||
*/
|
||||
default boolean containsAnyString(List<String> strings) {
|
||||
|
||||
return strings.stream().anyMatch(this::containsString);
|
||||
return strings.stream()
|
||||
.anyMatch(this::containsString);
|
||||
}
|
||||
|
||||
|
||||
@@ -315,7 +330,8 @@ public interface SemanticNode {
|
||||
*/
|
||||
default boolean containsAnyStringIgnoreCase(List<String> strings) {
|
||||
|
||||
return strings.stream().anyMatch(this::containsStringIgnoreCase);
|
||||
return strings.stream()
|
||||
.anyMatch(this::containsStringIgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
@@ -323,19 +339,19 @@ public interface SemanticNode {
|
||||
* This function is used during insertion of EntityNodes into the graph, it checks if the boundary of the RedactionEntity intersects or even contains the RedactionEntity.
|
||||
* It sets the fields accordingly and recursively calls this function on all its children.
|
||||
*
|
||||
* @param redactionEntity RedactionEntity, which is being inserted into the graph
|
||||
* @param textEntity RedactionEntity, which is being inserted into the graph
|
||||
*/
|
||||
default void addThisToEntityIfIntersects(RedactionEntity redactionEntity) {
|
||||
default void addThisToEntityIfIntersects(TextEntity textEntity) {
|
||||
|
||||
TextBlock textBlock = getTextBlock();
|
||||
if (textBlock.getBoundary().intersects(redactionEntity.getBoundary())) {
|
||||
if (textBlock.containsBoundary(redactionEntity.getBoundary())) {
|
||||
redactionEntity.setDeepestFullyContainingNode(this);
|
||||
if (textBlock.getTextRange().intersects(textEntity.getTextRange())) {
|
||||
if (textBlock.containsBoundary(textEntity.getTextRange())) {
|
||||
textEntity.setDeepestFullyContainingNode(this);
|
||||
}
|
||||
|
||||
redactionEntity.addIntersectingNode(this);
|
||||
streamChildren().filter(semanticNode -> semanticNode.getBoundary().intersects(redactionEntity.getBoundary()))
|
||||
.forEach(node -> node.addThisToEntityIfIntersects(redactionEntity));
|
||||
textEntity.addIntersectingNode(this);
|
||||
streamChildren().filter(semanticNode -> semanticNode.getBoundary().intersects(textEntity.getTextRange()))
|
||||
.forEach(node -> node.addThisToEntityIfIntersects(textEntity));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +402,8 @@ public interface SemanticNode {
|
||||
*/
|
||||
default Stream<SemanticNode> streamAllSubNodes() {
|
||||
|
||||
return getDocumentTree().allSubEntriesInOrder(getTreeId()).map(DocumentTree.Entry::getNode);
|
||||
return getDocumentTree().allSubEntriesInOrder(getTreeId())
|
||||
.map(DocumentTree.Entry::getNode);
|
||||
}
|
||||
|
||||
|
||||
@@ -397,7 +414,9 @@ public interface SemanticNode {
|
||||
*/
|
||||
default Stream<SemanticNode> streamAllSubNodesOfType(NodeType nodeType) {
|
||||
|
||||
return getDocumentTree().allSubEntriesInOrder(getTreeId()).filter(entry -> entry.getType().equals(nodeType)).map(DocumentTree.Entry::getNode);
|
||||
return getDocumentTree().allSubEntriesInOrder(getTreeId())
|
||||
.filter(entry -> entry.getType().equals(nodeType))
|
||||
.map(DocumentTree.Entry::getNode);
|
||||
}
|
||||
|
||||
|
||||
@@ -406,9 +425,9 @@ public interface SemanticNode {
|
||||
*
|
||||
* @return Boundary of this Node's TextBlock
|
||||
*/
|
||||
default Boundary getBoundary() {
|
||||
default TextRange getBoundary() {
|
||||
|
||||
return getTextBlock().getBoundary();
|
||||
return getTextBlock().getTextRange();
|
||||
}
|
||||
|
||||
|
||||
@@ -453,17 +472,19 @@ public interface SemanticNode {
|
||||
*/
|
||||
private Map<Page, Rectangle2D> getBBoxFromChildren() {
|
||||
|
||||
Map<Page, Rectangle2D> bBoxPerPage = new HashMap<>();
|
||||
List<Map<Page, Rectangle2D>> childrenBBoxes = streamChildren().map(SemanticNode::getBBox).toList();
|
||||
Set<Page> pages = childrenBBoxes.stream().flatMap(map -> map.keySet().stream()).collect(Collectors.toSet());
|
||||
for (Page page : pages) {
|
||||
Rectangle2D bBoxOnPage = childrenBBoxes.stream()
|
||||
.filter(childBboxPerPage -> childBboxPerPage.containsKey(page))
|
||||
.map(childBboxPerPage -> childBboxPerPage.get(page))
|
||||
.collect(RectangleTransformations.collectBBox());
|
||||
bBoxPerPage.put(page, bBoxOnPage);
|
||||
List<Map<Page, Rectangle2D>> childrenBBoxes = streamChildren().filter(child -> !isFullPageImage(child))
|
||||
.map(SemanticNode::getBBox)
|
||||
.toList();
|
||||
return BBoxMergingUtility.mergeBBoxes(childrenBBoxes);
|
||||
}
|
||||
|
||||
|
||||
private static boolean isFullPageImage(SemanticNode child) {
|
||||
|
||||
if (!child.getType().equals(NodeType.IMAGE)) {
|
||||
return false;
|
||||
}
|
||||
return bBoxPerPage;
|
||||
return ((Image) child).isFullPageImage();
|
||||
}
|
||||
|
||||
|
||||
@@ -473,9 +494,14 @@ public interface SemanticNode {
|
||||
private Map<Page, Rectangle2D> getBBoxFromLeafTextBlock() {
|
||||
|
||||
Map<Page, Rectangle2D> bBoxPerPage = new HashMap<>();
|
||||
Map<Page, List<AtomicTextBlock>> atomicTextBlockPerPage = getTextBlock().getAtomicTextBlocks().stream().collect(Collectors.groupingBy(AtomicTextBlock::getPage));
|
||||
Map<Page, List<AtomicTextBlock>> atomicTextBlockPerPage = getTextBlock().getAtomicTextBlocks()
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(AtomicTextBlock::getPage));
|
||||
atomicTextBlockPerPage.forEach((page, atbs) -> bBoxPerPage.put(page, RectangleTransformations.bBoxUnionAtomicTextBlock(atbs)));
|
||||
return bBoxPerPage;
|
||||
}
|
||||
|
||||
|
||||
void accept(NodeVisitor visitor);
|
||||
|
||||
}
|
||||
+9
-2
@@ -1,6 +1,7 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -26,8 +27,14 @@ public class SuperSection extends AbstractSemanticNode {
|
||||
public Headline getHeadline() {
|
||||
|
||||
return streamChildrenOfType(NodeType.HEADLINE).map(node -> (Headline) node)
|
||||
.findFirst()
|
||||
.orElseGet(() -> getParent().getHeadline());
|
||||
.findFirst().orElseGet(() -> getParent().getHeadline());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+18
-11
@@ -15,7 +15,8 @@ import java.util.stream.Stream;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.RedactionEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.entity.TextEntity;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlockCollector;
|
||||
|
||||
@@ -43,7 +44,7 @@ public class Table implements SemanticNode {
|
||||
|
||||
@Builder.Default
|
||||
@EqualsAndHashCode.Exclude
|
||||
Set<RedactionEntity> entities = new HashSet<>();
|
||||
Set<TextEntity> entities = new HashSet<>();
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
Map<Page, Rectangle2D> bBoxCache;
|
||||
@@ -54,7 +55,7 @@ public class Table implements SemanticNode {
|
||||
* @param strings Strings to check whether a row contains them
|
||||
* @return Stream of all entities in this table, that appear in a row, which contains any of the provided strings
|
||||
*/
|
||||
public Stream<RedactionEntity> streamEntitiesWhereRowContainsStringsIgnoreCase(List<String> strings) {
|
||||
public Stream<TextEntity> streamEntitiesWhereRowContainsStringsIgnoreCase(List<String> strings) {
|
||||
|
||||
return IntStream.range(0, numberOfRows).boxed()
|
||||
.filter(row -> rowContainsStringsIgnoreCase(row, strings))
|
||||
@@ -88,7 +89,7 @@ public class Table implements SemanticNode {
|
||||
* @param value the string which the table cell should contain
|
||||
* @return a stream of all entities, which appear in a row where at least one cell has the provided header and the provided value.
|
||||
*/
|
||||
public Stream<RedactionEntity> streamEntitiesWhereRowHasHeaderAndValue(String header, String value) {
|
||||
public Stream<TextEntity> streamEntitiesWhereRowHasHeaderAndValue(String header, String value) {
|
||||
|
||||
List<Integer> vertebrateStudyCols = streamHeaders().filter(headerNode -> headerNode.containsString(header))
|
||||
.map(TableCell::getCol)
|
||||
@@ -107,7 +108,7 @@ public class Table implements SemanticNode {
|
||||
* @param values the strings which the table cell should contain
|
||||
* @return a stream of all entities, which appear in a row where at least one cell has the provided header and any provided value.
|
||||
*/
|
||||
public Stream<RedactionEntity> streamEntitiesWhereRowHasHeaderAndAnyValue(String header, List<String> values) {
|
||||
public Stream<TextEntity> streamEntitiesWhereRowHasHeaderAndAnyValue(String header, List<String> values) {
|
||||
|
||||
List<Integer> colsWithHeader = streamHeaders().filter(headerNode -> headerNode.containsString(header))
|
||||
.map(TableCell::getCol)
|
||||
@@ -125,7 +126,7 @@ public class Table implements SemanticNode {
|
||||
* @param types type strings to check whether a row contains an entity like them
|
||||
* @return Stream of all entities in this table, that appear in a row, which contains at least one entity with any of the provided types.
|
||||
*/
|
||||
public Stream<RedactionEntity> streamEntitiesWhereRowContainsEntitiesOfType(List<String> types) {
|
||||
public Stream<TextEntity> streamEntitiesWhereRowContainsEntitiesOfType(List<String> types) {
|
||||
|
||||
List<Integer> rowsWithEntityOfType = IntStream.range(0, numberOfRows).boxed()
|
||||
.filter(rowNumber -> streamEntityTypesInRow(rowNumber).anyMatch(existingType -> types.stream()
|
||||
@@ -145,7 +146,7 @@ public class Table implements SemanticNode {
|
||||
* @param types type strings to check whether a row doesn't contain an entity like it
|
||||
* @return Stream of all entities in this table, that appear in a row, which does not contain any entity with any of the provided types.
|
||||
*/
|
||||
public Stream<RedactionEntity> streamEntitiesWhereRowContainsNoEntitiesOfType(List<String> types) {
|
||||
public Stream<TextEntity> streamEntitiesWhereRowContainsNoEntitiesOfType(List<String> types) {
|
||||
|
||||
List<Integer> rowsWithNoEntityOfType = IntStream.range(0, numberOfRows).boxed()
|
||||
.filter(rowNumber -> streamEntityTypesInRow(rowNumber).noneMatch(existingType -> types.stream()
|
||||
@@ -163,7 +164,7 @@ public class Table implements SemanticNode {
|
||||
|
||||
return streamRow(rowNumber).map(TableCell::getEntities)
|
||||
.flatMap(Collection::stream)
|
||||
.map(RedactionEntity::getType)
|
||||
.map(TextEntity::getType)
|
||||
.distinct();
|
||||
}
|
||||
|
||||
@@ -304,12 +305,12 @@ public class Table implements SemanticNode {
|
||||
* Finds all entities of the provided type, which appear in the same row that the provided entity appears in.
|
||||
*
|
||||
* @param type the type of entities to search for
|
||||
* @param redactionEntity the entity, which appears in the row to search
|
||||
* @param textEntity the entity, which appears in the row to search
|
||||
* @return List of all entities of the provided type, which appear in the same row that the provided entity appears in.
|
||||
*/
|
||||
public List<RedactionEntity> getEntitiesOfTypeInSameRow(String type, RedactionEntity redactionEntity) {
|
||||
public List<TextEntity> getEntitiesOfTypeInSameRow(String type, TextEntity textEntity) {
|
||||
|
||||
return redactionEntity.getIntersectingNodes()
|
||||
return textEntity.getIntersectingNodes()
|
||||
.stream()
|
||||
.filter(node -> node instanceof TableCell)
|
||||
.map(node -> (TableCell) node)
|
||||
@@ -353,4 +354,10 @@ public class Table implements SemanticNode {
|
||||
return bBoxCache;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
@@ -5,6 +5,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.NodeVisitor;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlockCollector;
|
||||
|
||||
@@ -42,6 +43,13 @@ public class TableCell extends AbstractSemanticNode {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(NodeVisitor visitor) {
|
||||
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
|
||||
|
||||
+64
-28
@@ -10,10 +10,12 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionData;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentTextData;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
@@ -36,14 +38,14 @@ public class AtomicTextBlock implements TextBlock {
|
||||
Page page;
|
||||
|
||||
//string coordinates
|
||||
Boundary boundary;
|
||||
TextRange textRange;
|
||||
String searchText;
|
||||
@Builder.Default
|
||||
List<Integer> lineBreaks = new ArrayList<>();
|
||||
@Builder.Default
|
||||
List<Boundary> boldTextBoundaries = new ArrayList<>();
|
||||
List<TextRange> boldTextBoundaries = new ArrayList<>();
|
||||
@Builder.Default
|
||||
List<Boundary> italicTextBoundaries = new ArrayList<>();
|
||||
List<TextRange> italicTextBoundaries = new ArrayList<>();
|
||||
String orientation;
|
||||
int textDirection;
|
||||
|
||||
@@ -64,10 +66,44 @@ public class AtomicTextBlock implements TextBlock {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String subSequenceWithLineBreaks(TextRange stringTextRange) {
|
||||
|
||||
if (stringTextRange.length() == 0 || !getTextRange().contains(stringTextRange)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Set<Integer> lbInBoundary = lineBreaks.stream()
|
||||
.map(i -> i + this.textRange.start())
|
||||
.filter(stringTextRange::contains)
|
||||
.collect(Collectors.toSet());
|
||||
if (stringTextRange.end() == getTextRange().end()) {
|
||||
lbInBoundary.add(getTextRange().end());
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = stringTextRange.start(); i < stringTextRange.end(); i++) {
|
||||
char character = this.charAt(i);
|
||||
if (lbInBoundary.contains(i + 1)) {
|
||||
// always plus one, due to the linebreaks being an exclusive end index
|
||||
if (!Character.isWhitespace(character)) {
|
||||
lbInBoundary.remove(i + 1);
|
||||
lbInBoundary.add(i + 2);
|
||||
sb.append(character);
|
||||
continue;
|
||||
}
|
||||
sb.append("\n");
|
||||
} else {
|
||||
sb.append(character);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
public static AtomicTextBlock fromSearchTextWithTextPosition(String searchText,
|
||||
List<Integer> lineBreaks,
|
||||
List<Boundary> boldTextBoundaries,
|
||||
List<Boundary> italicTextBoundaries,
|
||||
List<TextRange> boldTextBoundaries,
|
||||
List<TextRange> italicTextBoundaries,
|
||||
List<Rectangle2D> positions,
|
||||
List<Integer> stringIdxToPositionIdx,
|
||||
long idx,
|
||||
@@ -89,7 +125,7 @@ public class AtomicTextBlock implements TextBlock {
|
||||
.italicTextBoundaries(italicTextBoundaries)
|
||||
.positions(positions)
|
||||
.stringIdxToPositionIdx(stringIdxToPositionIdx)
|
||||
.boundary(new Boundary(offset, offset + searchText.length()))
|
||||
.textRange(new TextRange(offset, offset + searchText.length()))
|
||||
.textDirection(textDirection)
|
||||
.orientation(orientation)
|
||||
.build();
|
||||
@@ -100,7 +136,7 @@ public class AtomicTextBlock implements TextBlock {
|
||||
|
||||
return AtomicTextBlock.builder()
|
||||
.id(textBlockIdx)
|
||||
.boundary(new Boundary(stringOffset, stringOffset))
|
||||
.textRange(new TextRange(stringOffset, stringOffset))
|
||||
.searchText("")
|
||||
.page(page)
|
||||
.numberOnPage(numberOnPage)
|
||||
@@ -118,7 +154,7 @@ public class AtomicTextBlock implements TextBlock {
|
||||
.id(documentTextData.getId())
|
||||
.numberOnPage(documentTextData.getNumberOnPage())
|
||||
.page(page)
|
||||
.boundary(new Boundary(documentTextData.getStart(), documentTextData.getEnd()))
|
||||
.textRange(new TextRange(documentTextData.getStart(), documentTextData.getEnd()))
|
||||
.searchText(documentTextData.getSearchText())
|
||||
.lineBreaks(Arrays.stream(documentTextData.getLineBreaks()).boxed().toList())
|
||||
.stringIdxToPositionIdx(Arrays.stream(documentPositionData.getStringIdxToPositionIdx()).boxed().toList())
|
||||
@@ -140,11 +176,11 @@ public class AtomicTextBlock implements TextBlock {
|
||||
throw new IndexOutOfBoundsException(format("line %d out of range for AtomicTextBlock with %d lines", lineNumber, numberOfLines()));
|
||||
}
|
||||
if (lineNumber == 0) {
|
||||
return subSequence(boundary.start(), lineBreaks.get(0) + boundary.start());
|
||||
return subSequence(textRange.start(), lineBreaks.get(0) + textRange.start());
|
||||
} else if (lineNumber == numberOfLines() - 1) {
|
||||
return subSequence(lineBreaks.get(lineBreaks.size() - 1) + boundary.start(), boundary.end());
|
||||
return subSequence(lineBreaks.get(lineBreaks.size() - 1) + textRange.start(), textRange.end());
|
||||
}
|
||||
return subSequence(lineBreaks.get(lineNumber - 1) + boundary.start(), lineBreaks.get(lineNumber) + boundary.start());
|
||||
return subSequence(lineBreaks.get(lineNumber - 1) + textRange.start(), lineBreaks.get(lineNumber) + textRange.start());
|
||||
}
|
||||
|
||||
|
||||
@@ -159,9 +195,9 @@ public class AtomicTextBlock implements TextBlock {
|
||||
public int getNextLinebreak(int fromIndex) {
|
||||
|
||||
return lineBreaks.stream()//
|
||||
.filter(linebreak -> linebreak > fromIndex - boundary.start()) //
|
||||
.filter(linebreak -> linebreak > fromIndex - textRange.start()) //
|
||||
.findFirst() //
|
||||
.orElse(searchText.length()) + boundary.start();
|
||||
.orElse(searchText.length()) + textRange.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -169,43 +205,43 @@ public class AtomicTextBlock implements TextBlock {
|
||||
public int getPreviousLinebreak(int fromIndex) {
|
||||
|
||||
return lineBreaks.stream()//
|
||||
.filter(linebreak -> linebreak <= fromIndex - boundary.start())//
|
||||
.filter(linebreak -> linebreak <= fromIndex - textRange.start())//
|
||||
.reduce((a, b) -> b)//
|
||||
.orElse(0) + boundary.start();
|
||||
.orElse(0) + textRange.start();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Rectangle2D getPosition(int stringIdx) {
|
||||
|
||||
return positions.get(stringIdxToPositionIdx.get(stringIdx - boundary.start()));
|
||||
return positions.get(stringIdxToPositionIdx.get(stringIdx - textRange.start()));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<Rectangle2D> getPositions(Boundary stringBoundary) {
|
||||
public List<Rectangle2D> getPositions(TextRange stringTextRange) {
|
||||
|
||||
if (!containsBoundary(stringBoundary)) {
|
||||
throw new IndexOutOfBoundsException(format("%s is out of bounds for %s", stringBoundary, this.boundary));
|
||||
if (!containsBoundary(stringTextRange)) {
|
||||
throw new IndexOutOfBoundsException(format("%s is out of bounds for %s", stringTextRange, this.textRange));
|
||||
}
|
||||
if (stringBoundary.length() == 0) {
|
||||
if (stringTextRange.length() == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int startPositionIdx = stringIdxToPositionIdx.get(stringBoundary.start() - this.boundary.start());
|
||||
int startPositionIdx = stringIdxToPositionIdx.get(stringTextRange.start() - this.textRange.start());
|
||||
|
||||
if (stringBoundary.end() == this.boundary.end()) {
|
||||
if (stringTextRange.end() == this.textRange.end()) {
|
||||
return positions.subList(startPositionIdx, positions.size());
|
||||
}
|
||||
|
||||
return positions.subList(startPositionIdx, stringIdxToPositionIdx.get(stringBoundary.end() - this.boundary.start()));
|
||||
return positions.subList(startPositionIdx, stringIdxToPositionIdx.get(stringTextRange.end() - this.textRange.start()));
|
||||
|
||||
}
|
||||
|
||||
|
||||
public Map<Page, List<Rectangle2D>> getPositionsPerPage(Boundary stringBoundary) {
|
||||
public Map<Page, List<Rectangle2D>> getPositionsPerPage(TextRange stringTextRange) {
|
||||
|
||||
List<Rectangle2D> rectanglesPerLine = stringBoundary.split(getAllLineBreaksInBoundary(stringBoundary))
|
||||
List<Rectangle2D> rectanglesPerLine = stringTextRange.split(getAllLineBreaksInBoundary(stringTextRange))
|
||||
.stream()
|
||||
.map(this::getPositions)
|
||||
.map(RectangleTransformations::rectangleBBoxWithGaps)
|
||||
@@ -217,9 +253,9 @@ public class AtomicTextBlock implements TextBlock {
|
||||
}
|
||||
|
||||
|
||||
private List<Integer> getAllLineBreaksInBoundary(Boundary boundary) {
|
||||
protected List<Integer> getAllLineBreaksInBoundary(TextRange textRange) {
|
||||
|
||||
return getLineBreaks().stream().map(linebreak -> linebreak + this.boundary.start()).filter(boundary::contains).toList();
|
||||
return getLineBreaks().stream().map(linebreak -> linebreak + this.textRange.start()).filter(textRange::contains).toList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
+82
-33
@@ -11,7 +11,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
@@ -24,7 +24,7 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
|
||||
List<AtomicTextBlock> atomicTextBlocks;
|
||||
String searchText;
|
||||
Boundary boundary;
|
||||
TextRange textRange;
|
||||
|
||||
|
||||
public static ConcatenatedTextBlock empty() {
|
||||
@@ -37,29 +37,30 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
|
||||
this.atomicTextBlocks = new LinkedList<>();
|
||||
if (atomicTextBlocks.isEmpty()) {
|
||||
boundary = new Boundary(-1, -1);
|
||||
textRange = new TextRange(-1, -1);
|
||||
return;
|
||||
}
|
||||
var firstTextBlock = atomicTextBlocks.get(0);
|
||||
this.atomicTextBlocks.add(firstTextBlock);
|
||||
boundary = new Boundary(firstTextBlock.getBoundary().start(), firstTextBlock.getBoundary().end());
|
||||
textRange = new TextRange(firstTextBlock.getTextRange().start(), firstTextBlock.getTextRange().end());
|
||||
|
||||
atomicTextBlocks.subList(1, atomicTextBlocks.size()).forEach(this::concat);
|
||||
atomicTextBlocks.subList(1, atomicTextBlocks.size())
|
||||
.forEach(this::concat);
|
||||
}
|
||||
|
||||
|
||||
public ConcatenatedTextBlock concat(TextBlock textBlock) {
|
||||
|
||||
int start = textBlock.getBoundary().start();
|
||||
int end = textBlock.getBoundary().end();
|
||||
int start = textBlock.getTextRange().start();
|
||||
int end = textBlock.getTextRange().end();
|
||||
if (this.atomicTextBlocks.isEmpty()) {
|
||||
boundary.setStart(start);
|
||||
boundary.setEnd(end);
|
||||
} else if (boundary.end() != start) {
|
||||
throw new UnsupportedOperationException(format("Can only concat consecutive TextBlocks, trying to concat %s and %s", boundary, textBlock.getBoundary()));
|
||||
textRange.setStart(start);
|
||||
textRange.setEnd(end);
|
||||
} else if (textRange.end() != start) {
|
||||
throw new UnsupportedOperationException(format("Can only concat consecutive TextBlocks, trying to concat %s and %s", textRange, textBlock.getTextRange()));
|
||||
}
|
||||
this.atomicTextBlocks.addAll(textBlock.getAtomicTextBlocks());
|
||||
boundary.setEnd(end);
|
||||
textRange.setEnd(end);
|
||||
this.searchText = null;
|
||||
return this;
|
||||
}
|
||||
@@ -67,13 +68,18 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
|
||||
private AtomicTextBlock getAtomicTextBlockByStringIndex(int stringIdx) {
|
||||
|
||||
return atomicTextBlocks.stream().filter(textBlock -> textBlock.getBoundary().contains(stringIdx)).findAny().orElseThrow(IndexOutOfBoundsException::new);
|
||||
return atomicTextBlocks.stream()
|
||||
.filter(textBlock -> textBlock.getTextRange().containsExclusive(stringIdx))
|
||||
.findAny()
|
||||
.orElseThrow(IndexOutOfBoundsException::new);
|
||||
}
|
||||
|
||||
|
||||
private List<AtomicTextBlock> getAllAtomicTextBlocksPartiallyInStringBoundary(Boundary boundary) {
|
||||
private List<AtomicTextBlock> getAllAtomicTextBlocksPartiallyInStringBoundary(TextRange textRange) {
|
||||
|
||||
return atomicTextBlocks.stream().filter(tb -> tb.getBoundary().intersects(boundary)).toList();
|
||||
return atomicTextBlocks.stream()
|
||||
.filter(tb -> tb.getTextRange().intersects(textRange))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +98,9 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
@Override
|
||||
public int numberOfLines() {
|
||||
|
||||
return atomicTextBlocks.stream().map(AtomicTextBlock::getLineBreaks).mapToInt(List::size).sum();
|
||||
return atomicTextBlocks.stream()
|
||||
.map(AtomicTextBlock::getLineBreaks)
|
||||
.mapToInt(List::size).sum();
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +121,10 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
@Override
|
||||
public List<Integer> getLineBreaks() {
|
||||
|
||||
return getAtomicTextBlocks().stream().flatMap(atomicTextBlock -> atomicTextBlock.getLineBreaks().stream()).toList();
|
||||
return getAtomicTextBlocks().stream()
|
||||
.flatMap(atomicTextBlock -> atomicTextBlock.getLineBreaks()
|
||||
.stream())
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -125,47 +136,48 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
|
||||
|
||||
@Override
|
||||
public List<Rectangle2D> getPositions(Boundary stringBoundary) {
|
||||
public List<Rectangle2D> getPositions(TextRange stringTextRange) {
|
||||
|
||||
List<AtomicTextBlock> textBlocks = getAllAtomicTextBlocksPartiallyInStringBoundary(stringBoundary);
|
||||
List<AtomicTextBlock> textBlocks = getAllAtomicTextBlocksPartiallyInStringBoundary(stringTextRange);
|
||||
|
||||
if (textBlocks.size() == 1) {
|
||||
return textBlocks.get(0).getPositions(stringBoundary);
|
||||
return textBlocks.get(0).getPositions(stringTextRange);
|
||||
}
|
||||
|
||||
AtomicTextBlock firstTextBlock = textBlocks.get(0);
|
||||
List<Rectangle2D> positions = new LinkedList<>(firstTextBlock.getPositions(new Boundary(stringBoundary.start(), firstTextBlock.getBoundary().end())));
|
||||
List<Rectangle2D> positions = new LinkedList<>(firstTextBlock.getPositions(new TextRange(stringTextRange.start(), firstTextBlock.getTextRange().end())));
|
||||
|
||||
for (AtomicTextBlock textBlock : textBlocks.subList(1, textBlocks.size() - 1)) {
|
||||
positions.addAll(textBlock.getPositions());
|
||||
}
|
||||
|
||||
var lastTextBlock = textBlocks.get(textBlocks.size() - 1);
|
||||
positions.addAll(lastTextBlock.getPositions(new Boundary(lastTextBlock.getBoundary().start(), stringBoundary.end())));
|
||||
positions.addAll(lastTextBlock.getPositions(new TextRange(lastTextBlock.getTextRange().start(), stringTextRange.end())));
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Page, List<Rectangle2D>> getPositionsPerPage(Boundary stringBoundary) {
|
||||
public Map<Page, List<Rectangle2D>> getPositionsPerPage(TextRange stringTextRange) {
|
||||
|
||||
List<AtomicTextBlock> textBlocks = getAllAtomicTextBlocksPartiallyInStringBoundary(stringBoundary);
|
||||
List<AtomicTextBlock> textBlocks = getAllAtomicTextBlocksPartiallyInStringBoundary(stringTextRange);
|
||||
|
||||
if (textBlocks.size() == 1) {
|
||||
return textBlocks.get(0).getPositionsPerPage(stringBoundary);
|
||||
return textBlocks.get(0).getPositionsPerPage(stringTextRange);
|
||||
}
|
||||
|
||||
AtomicTextBlock firstTextBlock = textBlocks.get(0);
|
||||
Map<Page, List<Rectangle2D>> rectanglesPerLinePerPage = firstTextBlock.getPositionsPerPage(new Boundary(stringBoundary.start(), firstTextBlock.getBoundary().end()));
|
||||
Map<Page, List<Rectangle2D>> rectanglesPerLinePerPage = firstTextBlock.getPositionsPerPage(new TextRange(stringTextRange.start(), firstTextBlock.getTextRange().end()));
|
||||
|
||||
for (AtomicTextBlock textBlock : textBlocks.subList(1, textBlocks.size() - 1)) {
|
||||
rectanglesPerLinePerPage = mergeEntityPositionsWithSamePageNode(rectanglesPerLinePerPage, textBlock.getPositionsPerPage(textBlock.getBoundary()));
|
||||
rectanglesPerLinePerPage = mergeEntityPositionsWithSamePageNode(rectanglesPerLinePerPage, textBlock.getPositionsPerPage(textBlock.getTextRange()));
|
||||
}
|
||||
|
||||
AtomicTextBlock lastTextBlock = textBlocks.get(textBlocks.size() - 1);
|
||||
rectanglesPerLinePerPage = mergeEntityPositionsWithSamePageNode(rectanglesPerLinePerPage,
|
||||
lastTextBlock.getPositionsPerPage(new Boundary(lastTextBlock.getBoundary().start(), stringBoundary.end())));
|
||||
lastTextBlock.getPositionsPerPage(new TextRange(lastTextBlock.getTextRange().start(),
|
||||
stringTextRange.end())));
|
||||
|
||||
return rectanglesPerLinePerPage;
|
||||
}
|
||||
@@ -174,11 +186,42 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
private Map<Page, List<Rectangle2D>> mergeEntityPositionsWithSamePageNode(Map<Page, List<Rectangle2D>> map1, Map<Page, List<Rectangle2D>> map2) {
|
||||
|
||||
Map<Page, List<Rectangle2D>> mergedMap = new HashMap<>(map1);
|
||||
map2.forEach((pageNode, rectangles) -> mergedMap.merge(pageNode, rectangles, (l1, l2) -> Stream.concat(l1.stream(), l2.stream()).toList()));
|
||||
map2.forEach((pageNode, rectangles) -> mergedMap.merge(pageNode,
|
||||
rectangles,
|
||||
(l1, l2) -> Stream.concat(l1.stream(), l2.stream())
|
||||
.toList()));
|
||||
return mergedMap;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String subSequenceWithLineBreaks(TextRange stringTextRange) {
|
||||
|
||||
if (stringTextRange.length() == 0 || !getTextRange().contains(stringTextRange)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<AtomicTextBlock> textBlocks = getAllAtomicTextBlocksPartiallyInStringBoundary(stringTextRange);
|
||||
|
||||
if (textBlocks.size() == 1) {
|
||||
return textBlocks.get(0).subSequenceWithLineBreaks(stringTextRange);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
AtomicTextBlock firstTextBlock = textBlocks.get(0);
|
||||
sb.append(firstTextBlock.subSequenceWithLineBreaks(new TextRange(stringTextRange.start(), firstTextBlock.getTextRange().end())));
|
||||
|
||||
for (AtomicTextBlock textBlock : textBlocks.subList(1, textBlocks.size() - 1)) {
|
||||
sb.append(textBlock.searchTextWithLineBreaks());
|
||||
}
|
||||
|
||||
var lastTextBlock = textBlocks.get(textBlocks.size() - 1);
|
||||
sb.append(lastTextBlock.subSequenceWithLineBreaks(new TextRange(lastTextBlock.getTextRange().start(), stringTextRange.end())));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -187,16 +230,22 @@ public class ConcatenatedTextBlock implements TextBlock {
|
||||
|
||||
|
||||
@Override
|
||||
public List<Boundary> getBoldTextBoundaries() {
|
||||
public List<TextRange> getBoldTextBoundaries() {
|
||||
|
||||
return getAtomicTextBlocks().stream().map(AtomicTextBlock::getBoldTextBoundaries).flatMap(Collection::stream).toList();
|
||||
return getAtomicTextBlocks().stream()
|
||||
.map(AtomicTextBlock::getBoldTextBoundaries)
|
||||
.flatMap(Collection::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<Boundary> getItalicTextBoundaries() {
|
||||
public List<TextRange> getItalicTextBoundaries() {
|
||||
|
||||
return getAtomicTextBlocks().stream().map(AtomicTextBlock::getItalicTextBoundaries).flatMap(Collection::stream).toList();
|
||||
return getAtomicTextBlocks().stream()
|
||||
.map(AtomicTextBlock::getItalicTextBoundaries)
|
||||
.flatMap(Collection::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
+33
-23
@@ -10,7 +10,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
|
||||
public interface TextBlock extends CharSequence {
|
||||
@@ -21,10 +21,10 @@ public interface TextBlock extends CharSequence {
|
||||
List<AtomicTextBlock> getAtomicTextBlocks();
|
||||
|
||||
|
||||
List<Boundary> getBoldTextBoundaries();
|
||||
List<TextRange> getBoldTextBoundaries();
|
||||
|
||||
|
||||
List<Boundary> getItalicTextBoundaries();
|
||||
List<TextRange> getItalicTextBoundaries();
|
||||
|
||||
|
||||
String getOrientation();
|
||||
@@ -33,7 +33,7 @@ public interface TextBlock extends CharSequence {
|
||||
int getTextDirection();
|
||||
|
||||
|
||||
Boundary getBoundary();
|
||||
TextRange getTextRange();
|
||||
|
||||
|
||||
int getNextLinebreak(int fromIndex);
|
||||
@@ -48,31 +48,41 @@ public interface TextBlock extends CharSequence {
|
||||
Rectangle2D getPosition(int stringIdx);
|
||||
|
||||
|
||||
List<Rectangle2D> getPositions(Boundary stringBoundary);
|
||||
List<Rectangle2D> getPositions(TextRange stringTextRange);
|
||||
|
||||
|
||||
Map<Page, List<Rectangle2D>> getPositionsPerPage(Boundary stringBoundary);
|
||||
Map<Page, List<Rectangle2D>> getPositionsPerPage(TextRange stringTextRange);
|
||||
|
||||
|
||||
int numberOfLines();
|
||||
|
||||
|
||||
String subSequenceWithLineBreaks(TextRange stringTextRange);
|
||||
|
||||
|
||||
default String searchTextWithLineBreaks() {
|
||||
|
||||
return subSequenceWithLineBreaks(getTextRange());
|
||||
}
|
||||
|
||||
default int indexOf(String searchTerm) {
|
||||
|
||||
return indexOf(searchTerm, getBoundary().start());
|
||||
return indexOf(searchTerm, getTextRange().start());
|
||||
}
|
||||
|
||||
|
||||
default Set<Page> getPages() {
|
||||
|
||||
return getAtomicTextBlocks().stream().map(AtomicTextBlock::getPage).collect(Collectors.toUnmodifiableSet());
|
||||
return getAtomicTextBlocks().stream()
|
||||
.map(AtomicTextBlock::getPage)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
|
||||
default Set<Page> getPages(Boundary boundary) {
|
||||
default Set<Page> getPages(TextRange textRange) {
|
||||
|
||||
return getAtomicTextBlocks().stream()
|
||||
.filter(atomicTextBlock -> atomicTextBlock.getBoundary().intersects(boundary))
|
||||
.filter(atomicTextBlock -> atomicTextBlock.getTextRange().intersects(textRange))
|
||||
.map(AtomicTextBlock::getPage)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
@@ -80,38 +90,38 @@ public interface TextBlock extends CharSequence {
|
||||
|
||||
default int indexOf(String searchTerm, int startOffset) {
|
||||
|
||||
int start = getSearchText().indexOf(searchTerm, startOffset - getBoundary().start());
|
||||
int start = getSearchText().indexOf(searchTerm, startOffset - getTextRange().start());
|
||||
if (start == -1) {
|
||||
return -1;
|
||||
}
|
||||
return start + getBoundary().start();
|
||||
return start + getTextRange().start();
|
||||
}
|
||||
|
||||
|
||||
default CharSequence getFirstLine() {
|
||||
|
||||
return subSequence(getBoundary().start(), getNextLinebreak(getBoundary().start()));
|
||||
return subSequence(getTextRange().start(), getNextLinebreak(getTextRange().start()));
|
||||
}
|
||||
|
||||
|
||||
default boolean containsBoundary(Boundary boundary) {
|
||||
default boolean containsBoundary(TextRange textRange) {
|
||||
|
||||
if (boundary.end() < boundary.start()) {
|
||||
throw new IllegalArgumentException(format("Invalid %s, StartIndex must be smaller than EndIndex", boundary));
|
||||
if (textRange.end() < textRange.start()) {
|
||||
throw new IllegalArgumentException(format("Invalid %s, StartIndex must be smaller than EndIndex", textRange));
|
||||
}
|
||||
return getBoundary().contains(boundary);
|
||||
return getTextRange().contains(textRange);
|
||||
}
|
||||
|
||||
|
||||
default boolean containsIndex(int stringIndex) {
|
||||
|
||||
return getBoundary().contains(stringIndex);
|
||||
return getTextRange().containsExclusive(stringIndex);
|
||||
}
|
||||
|
||||
|
||||
default CharSequence subSequence(Boundary boundary) {
|
||||
default CharSequence subSequence(TextRange textRange) {
|
||||
|
||||
return subSequence(boundary.start(), boundary.end());
|
||||
return subSequence(textRange.start(), textRange.end());
|
||||
}
|
||||
|
||||
|
||||
@@ -128,21 +138,21 @@ public interface TextBlock extends CharSequence {
|
||||
@Override
|
||||
default CharSequence subSequence(int start, int end) {
|
||||
|
||||
return getSearchText().substring(start - getBoundary().start(), end - getBoundary().start());
|
||||
return getSearchText().substring(start - getTextRange().start(), end - getTextRange().start());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
default int length() {
|
||||
|
||||
return getBoundary().length();
|
||||
return getTextRange().length();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
default char charAt(int index) {
|
||||
|
||||
return getSearchText().charAt(index - getBoundary().start());
|
||||
return getSearchText().charAt(index - getTextRange().start());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -24,5 +24,7 @@ public class ClassifiedImage {
|
||||
private boolean hasTransparency;
|
||||
@NonNull
|
||||
private int page;
|
||||
@NonNull
|
||||
private String representation;
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -12,12 +12,14 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OutlineValidationService {
|
||||
|
||||
@Observed(name = "OutlineValidationService", contextualName = "create-toc")
|
||||
public TableOfContents createToC(List<TextPageBlock> headlines) {
|
||||
|
||||
List<TableOfContentItem> mainSections = new ArrayList<>();
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ public class ImageServiceResponseAdapter {
|
||||
.add(new ClassifiedImage(new Rectangle2D.Double(imageMetadata.getPosition().getX1(),
|
||||
imageMetadata.getPosition().getY1(),
|
||||
imageMetadata.getGeometry().getWidth(),
|
||||
imageMetadata.getGeometry().getHeight()), classification, imageMetadata.isAlpha(), imageMetadata.getPosition().getPageNumber()));
|
||||
imageMetadata.getGeometry().getHeight()), classification, imageMetadata.isAlpha(), imageMetadata.getPosition().getPageNumber(),imageMetadata.getRepresentation()));
|
||||
});
|
||||
|
||||
// Currently This is a copy but, it will be changed later because i don' t think that we should unclassified images.
|
||||
@@ -44,7 +44,7 @@ public class ImageServiceResponseAdapter {
|
||||
.add(new ClassifiedImage(new Rectangle2D.Double(imageMetadata.getPosition().getX1(),
|
||||
imageMetadata.getPosition().getY1(),
|
||||
imageMetadata.getGeometry().getWidth(),
|
||||
imageMetadata.getGeometry().getHeight()), classification, imageMetadata.isAlpha(), imageMetadata.getPosition().getPageNumber()));
|
||||
imageMetadata.getGeometry().getHeight()), classification, imageMetadata.isAlpha(), imageMetadata.getPosition().getPageNumber(),imageMetadata.getRepresentation()));
|
||||
});
|
||||
|
||||
return images;
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ public class VisualLayoutParsingAdapter {
|
||||
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()), ImageType.SIGNATURE, true, false, false, pageNumber);
|
||||
t.getBox().getY2() - t.getBox().getY1()), ImageType.SIGNATURE, true, false, false, pageNumber,"");
|
||||
|
||||
signatures.add(signature);
|
||||
}
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import lombok.NoArgsConstructor;
|
||||
public class ImageMetadata {
|
||||
|
||||
private Classification classification;
|
||||
private String representation;
|
||||
private Position position;
|
||||
private Geometry geometry;
|
||||
private Filters filters;
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ public class BodyTextFrameService {
|
||||
for (ClassificationPage page : classificationDocument.getPages()) {
|
||||
var updatedBodyTextFrame = getBodyTextFrameFromRulings(page, bodyTextFrame, landscapeBodyTextFrame);
|
||||
setBodyTextFrameAdjustedToPage(page, bodyTextFrame, landscapeBodyTextFrame);
|
||||
classificationDocument.getVisualizations().addMainBodyVisualization(page.isLandscape() ? landscapeBodyTextFrame : bodyTextFrame, page.getPageNumber());
|
||||
classificationDocument.getLayoutDebugLayer().addMainBodyVisualization(page.isLandscape() ? landscapeBodyTextFrame : bodyTextFrame, page.getPageNumber());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -42,7 +42,9 @@ public class SimplifiedSectionTextService {
|
||||
|
||||
return SimplifiedSectionText.builder()
|
||||
.sectionNumber(section.getTreeId()
|
||||
.get(0))
|
||||
.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(".")))
|
||||
.text(section.getTextBlock().getSearchText())
|
||||
.build();
|
||||
}
|
||||
|
||||
+15
-3
@@ -12,6 +12,7 @@ import java.util.function.Function;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.SectionIdentifier;
|
||||
@@ -204,21 +205,28 @@ public class BlockificationPostprocessingService {
|
||||
return false;
|
||||
}
|
||||
if (minDistance == distanceToDirectMatch) {
|
||||
directMatch.setClassification(headlineType);
|
||||
setClassificationAndAddOutlineEngine(directMatch, headlineType);
|
||||
} else if (minDistance == distanceToSplitCandidate) {
|
||||
SplitBlockResult splitBlockResult = splitBlock(classificationPage, splitCandidate, context.sectionIdentifier, outlineObject.getTitle());
|
||||
if (splitBlockResult.modifiedBlockToSplit) {
|
||||
splitCandidate.setClassification(headlineType);
|
||||
setClassificationAndAddOutlineEngine(splitCandidate, headlineType);
|
||||
}
|
||||
splitBlockResult.otherBlocks.forEach(other -> other.setClassification(null));
|
||||
} else {
|
||||
var merged = mergeBlocks(classificationPage, bestMergeCandidateCombination);
|
||||
merged.setClassification(headlineType);
|
||||
setClassificationAndAddOutlineEngine(merged, headlineType);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static void setClassificationAndAddOutlineEngine(TextPageBlock block, PageBlockType headlineType) {
|
||||
|
||||
block.setClassification(headlineType);
|
||||
block.getEngines().add(LayoutEngine.OUTLINE);
|
||||
}
|
||||
|
||||
|
||||
private SplitBlockResult splitBlock(ClassificationPage classificationPage, TextPageBlock blockToSplit, SectionIdentifier sectionIdentifier, String title) {
|
||||
|
||||
List<TextPageBlock> otherBlocks = new ArrayList<>();
|
||||
@@ -265,6 +273,10 @@ public class BlockificationPostprocessingService {
|
||||
List<TextPositionSequence> postSequence = new ArrayList<>();
|
||||
StringBuilder currentSequence = new StringBuilder();
|
||||
|
||||
if (target.isBlank()){
|
||||
return new WordSequenceResult();
|
||||
}
|
||||
|
||||
for (TextPositionSequence sequence : textPositionSequences) {
|
||||
|
||||
currentSequence.append(sanitizeString(sequence.toString()));
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRul
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutDebugLayer;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -35,7 +35,7 @@ public class DocstrumBlockificationService {
|
||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions,
|
||||
CleanRulings rulings,
|
||||
boolean xyOrder,
|
||||
LayoutparsingVisualizations visualizations,
|
||||
LayoutDebugLayer visualizations,
|
||||
LayoutParsingType layoutParsingType) {
|
||||
|
||||
CleanRulings usedRulings = rulings.withoutTextRulings();
|
||||
|
||||
+2
-7
@@ -1,9 +1,6 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.blockification;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
@@ -11,13 +8,11 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.FloatFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.Orientation;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.StringFrequencyCounter;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutDebugLayer;
|
||||
|
||||
@SuppressWarnings("all")
|
||||
@Service
|
||||
@@ -35,7 +30,7 @@ public class RedactManagerBlockificationService {
|
||||
* @param visualizations
|
||||
* @return Page object that contains the Textblock and text statistics.
|
||||
*/
|
||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions, CleanRulings cleanRulings, LayoutparsingVisualizations visualizations) {
|
||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions, CleanRulings cleanRulings, LayoutDebugLayer visualizations) {
|
||||
|
||||
CleanRulings usedRulings = cleanRulings.withoutTextRulings();
|
||||
|
||||
|
||||
+5
-7
@@ -21,33 +21,31 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class ClarifyndClassificationService {
|
||||
|
||||
private final HeadlineClassificationService headlineClassificationService;
|
||||
|
||||
public void classifyDocument(ClassificationDocument document) {
|
||||
|
||||
List<Float> headlineFontSizes = document.getFontSizeCounter().getHighterThanMostPopular();
|
||||
|
||||
log.debug("Document FontSize counters are: {}", document.getFontSizeCounter().getCountPerValue());
|
||||
|
||||
headlineClassificationService.resetContext();
|
||||
HeadlineClassificationService headlineClassificationService = new HeadlineClassificationService();
|
||||
|
||||
for (ClassificationPage page : document.getPages()) {
|
||||
classifyPage(page, document, headlineFontSizes);
|
||||
classifyPage(headlineClassificationService, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyPage(ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyPage(HeadlineClassificationService headlineClassificationService, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
for (AbstractPageBlock textBlock : page.getTextBlocks()) {
|
||||
if (textBlock instanceof TextPageBlock) {
|
||||
classifyBlock((TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
classifyBlock(headlineClassificationService, (TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyBlock(TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyBlock(HeadlineClassificationService headlineClassificationService, TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
var bodyTextFrame = page.getBodyTextFrame();
|
||||
|
||||
|
||||
+35
-27
@@ -24,10 +24,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class DocuMineClassificationService {
|
||||
|
||||
private final HeadlineClassificationService headlineClassificationService;
|
||||
private static final Pattern pattern = Pattern.compile("^([1-9]\\d?\\.){1,3}\\d{1,2}\\.?\\s[0-9A-Za-z \\[\\]]{2,50}", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern pattern2 = Pattern.compile("\\p{L}{3,}", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern pattern3 = Pattern.compile("^(\\d{1,1}\\.){1,3}\\d{1,2}\\.?\\s[a-z]{1,2}\\/[a-z]{1,2}.*");
|
||||
private static final Pattern HEADLINE_WITH_IDENTIFER_PATTERN = Pattern.compile("^([1-9]\\d?\\.){1,3}\\d{1,2}\\.?\\s[0-9A-Za-z \\[\\]]{2,50}", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern AT_LEAST_3_PATTERN = Pattern.compile("\\p{L}{3,}", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern HEADLINE_PATTTERN_WITH_SLASHES = Pattern.compile("^(\\d{1,1}\\.){1,3}\\d{1,2}\\.?\\s[a-z]{1,2}\\/[a-z]{1,2}.*");
|
||||
|
||||
|
||||
public void classifyDocument(ClassificationDocument document) {
|
||||
@@ -36,32 +35,39 @@ public class DocuMineClassificationService {
|
||||
|
||||
log.debug("Document FontSize counters are: {}", document.getFontSizeCounter().getCountPerValue());
|
||||
|
||||
headlineClassificationService.resetContext();
|
||||
HeadlineClassificationService headlineClassificationService = new HeadlineClassificationService();
|
||||
|
||||
for (ClassificationPage page : document.getPages()) {
|
||||
classifyPage(page, document, headlineFontSizes);
|
||||
classifyPage(headlineClassificationService, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyPage(ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyPage(HeadlineClassificationService headlineClassificationService,
|
||||
ClassificationPage page,
|
||||
ClassificationDocument document,
|
||||
List<Float> headlineFontSizes) {
|
||||
|
||||
for (AbstractPageBlock textBlock : page.getTextBlocks()) {
|
||||
if (textBlock instanceof TextPageBlock) {
|
||||
classifyBlock((TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
classifyBlock(headlineClassificationService, (TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyBlock(TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyBlock(HeadlineClassificationService headlineClassificationService,
|
||||
TextPageBlock textBlock,
|
||||
ClassificationPage page,
|
||||
ClassificationDocument document,
|
||||
List<Float> headlineFontSizes) {
|
||||
|
||||
log.debug("headlineFontSizes: {}", headlineFontSizes);
|
||||
var bodyTextFrame = page.getBodyTextFrame();
|
||||
|
||||
Matcher matcher = pattern.matcher(textBlock.toString());
|
||||
Matcher matcher2 = pattern2.matcher(textBlock.toString());
|
||||
Matcher matcher3 = pattern3.matcher(textBlock.toString());
|
||||
Matcher headlineWithIdentifierMatcher = HEADLINE_WITH_IDENTIFER_PATTERN.matcher(textBlock.toString());
|
||||
Matcher atLeast3Matcher = AT_LEAST_3_PATTERN.matcher(textBlock.toString());
|
||||
Matcher headlineWithSlashesMatcher = HEADLINE_PATTTERN_WITH_SLASHES.matcher(textBlock.toString());
|
||||
|
||||
if (textBlock.getClassification() != null && textBlock.getClassification().isHeadline()) {
|
||||
headlineClassificationService.setLastHeadlineFromOutline(textBlock);
|
||||
@@ -71,14 +77,10 @@ public class DocuMineClassificationService {
|
||||
textBlock.setClassification(PageBlockType.OTHER);
|
||||
return;
|
||||
}
|
||||
if (MarkedContentUtils.intersects(textBlock, page.getMarkedContentBboxPerType(), MarkedContentUtils.HEADER)
|
||||
|| (PositionUtils.isOverBodyTextFrame(bodyTextFrame,
|
||||
textBlock,
|
||||
page.getRotation()) && (document.getFontSizeCounter().getMostPopular()
|
||||
== null
|
||||
|| textBlock.getHighestFontSize()
|
||||
<= document.getFontSizeCounter()
|
||||
.getMostPopular()))) {
|
||||
if (MarkedContentUtils.intersects(textBlock, page.getMarkedContentBboxPerType(), MarkedContentUtils.HEADER) //
|
||||
|| (PositionUtils.isOverBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) //
|
||||
&& (document.getFontSizeCounter().getMostPopular() == null //
|
||||
|| textBlock.getHighestFontSize() <= document.getFontSizeCounter().getMostPopular()))) {
|
||||
textBlock.setClassification(PageBlockType.HEADER);
|
||||
|
||||
} else if (MarkedContentUtils.intersects(textBlock, page.getMarkedContentBboxPerType(), MarkedContentUtils.FOOTER)
|
||||
@@ -102,19 +104,25 @@ public class DocuMineClassificationService {
|
||||
|| textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter().getMostPopular())
|
||||
&& PositionUtils.getApproxLineCount(textBlock) < 5.9
|
||||
|
||||
&& (textBlock.getMostPopularWordStyle().contains("bold") && Character.isDigit(textBlock.toString().charAt(0)) && matcher2.reset().find() && !textBlock.toString()
|
||||
.contains(":")
|
||||
|| textBlock.toString().equals(textBlock.toString().toUpperCase(Locale.ROOT)) && matcher2.reset().find() && !textBlock.toString().contains(":")
|
||||
|| textBlock.toString().startsWith("APPENDIX")
|
||||
|| textBlock.toString().startsWith("FIGURE")
|
||||
&& (textBlock.getMostPopularWordStyle().contains("bold")
|
||||
&& Character.isDigit(textBlock.toString().charAt(0))
|
||||
&& atLeast3Matcher.reset().find()
|
||||
&& !textBlock.toString().contains(":") //
|
||||
|| textBlock.toString().equals(textBlock.toString().toUpperCase(Locale.ROOT)) && atLeast3Matcher.reset().find() && !textBlock.toString().contains(":") //
|
||||
|| textBlock.toString().startsWith("APPENDIX") //
|
||||
|| textBlock.toString().startsWith("FIGURE") //
|
||||
|| textBlock.toString().startsWith("Continued TABLE") //
|
||||
|| textBlock.toString().startsWith("TABLE"))
|
||||
&& !textBlock.toString().endsWith(":")
|
||||
&& matcher2.reset().find()) {
|
||||
&& atLeast3Matcher.reset().find()) {
|
||||
PageBlockType headlineType = PageBlockType.getHeadlineType(1);
|
||||
headlineClassificationService.classifyHeadline(textBlock, headlineType);
|
||||
document.setHeadlines(true);
|
||||
|
||||
} else if (matcher.reset().find() && PositionUtils.getApproxLineCount(textBlock) < 2.9 && matcher2.reset().find() && !matcher3.reset().matches()) {
|
||||
} else if (headlineWithIdentifierMatcher.reset().find()
|
||||
&& PositionUtils.getApproxLineCount(textBlock) < 2.9
|
||||
&& atLeast3Matcher.reset().find()
|
||||
&& !headlineWithSlashesMatcher.reset().matches()) {
|
||||
PageBlockType headlineType = PageBlockType.getHeadlineType(2);
|
||||
headlineClassificationService.classifyHeadline(textBlock, headlineType);
|
||||
document.setHeadlines(true);
|
||||
|
||||
-10
@@ -2,15 +2,12 @@ package com.knecon.fforesight.service.layoutparser.processor.services.classifica
|
||||
|
||||
import static com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType.getHeadlineNumber;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Service
|
||||
@Getter
|
||||
@Setter
|
||||
public class HeadlineClassificationService {
|
||||
@@ -19,13 +16,6 @@ public class HeadlineClassificationService {
|
||||
PageBlockType originalClassifiedBlockType;
|
||||
TextPageBlock lastHeadlineFromOutline;
|
||||
|
||||
public void resetContext() {
|
||||
setLastHeadline(null);
|
||||
setOriginalClassifiedBlockType(null);
|
||||
setLastHeadlineFromOutline(null);
|
||||
}
|
||||
|
||||
|
||||
public void setLastHeadlineFromOutline(TextPageBlock lastHeadlineFromOutline) {
|
||||
|
||||
this.lastHeadlineFromOutline = lastHeadlineFromOutline;
|
||||
|
||||
+5
-7
@@ -22,8 +22,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class RedactManagerClassificationService {
|
||||
|
||||
private final HeadlineClassificationService headlineClassificationService;
|
||||
|
||||
|
||||
public void classifyDocument(ClassificationDocument document) {
|
||||
|
||||
@@ -31,25 +29,25 @@ public class RedactManagerClassificationService {
|
||||
|
||||
log.debug("Document FontSize counters are: {}", document.getFontSizeCounter().getCountPerValue());
|
||||
|
||||
headlineClassificationService.resetContext();
|
||||
HeadlineClassificationService headlineClassificationService = new HeadlineClassificationService();
|
||||
|
||||
for (ClassificationPage page : document.getPages()) {
|
||||
classifyPage(page, document, headlineFontSizes);
|
||||
classifyPage(headlineClassificationService, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyPage(ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyPage(HeadlineClassificationService headlineClassificationService, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
for (AbstractPageBlock textBlock : page.getTextBlocks()) {
|
||||
if (textBlock instanceof TextPageBlock) {
|
||||
classifyBlock((TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
classifyBlock(headlineClassificationService, (TextPageBlock) textBlock, page, document, headlineFontSizes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void classifyBlock(TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
private void classifyBlock(HeadlineClassificationService headlineClassificationService, TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
||||
|
||||
var bodyTextFrame = page.getBodyTextFrame();
|
||||
|
||||
|
||||
+8
-3
@@ -22,6 +22,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.Classification
|
||||
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.graph.DocumentTree;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.AbstractSemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.DuplicatedParagraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Footer;
|
||||
@@ -31,7 +32,6 @@ import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.He
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Image;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.AbstractSemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.outline.TableOfContentItem;
|
||||
@@ -45,15 +45,17 @@ import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@UtilityClass
|
||||
@Slf4j
|
||||
public class DocumentGraphFactory {
|
||||
|
||||
public Document buildDocumentGraph(LayoutParsingType layoutParsingType, ClassificationDocument document) {
|
||||
|
||||
Document documentGraph = new Document();
|
||||
|
||||
documentGraph.setVisualizations(document.getVisualizations());
|
||||
documentGraph.setLayoutDebugLayer(document.getLayoutDebugLayer());
|
||||
|
||||
Context context = new Context(documentGraph);
|
||||
|
||||
@@ -122,6 +124,7 @@ public class DocumentGraphFactory {
|
||||
List<Integer> treeId = context.documentTree.createNewChildEntryAndReturnId(parentNode, node);
|
||||
node.setLeafTextBlock(textBlock);
|
||||
node.setTreeId(treeId);
|
||||
node.getEngines().addAll(originalTextBlock.getEngines());
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +147,7 @@ public class DocumentGraphFactory {
|
||||
.position(position)
|
||||
.transparent(image.isHasTransparency())
|
||||
.page(page)
|
||||
.representationHash(image.getRepresentation())
|
||||
.documentTree(context.getDocumentTree())
|
||||
.build();
|
||||
page.getMainBody().add(imageNode);
|
||||
@@ -276,7 +280,8 @@ public class DocumentGraphFactory {
|
||||
return pages.keySet()
|
||||
.stream()
|
||||
.filter(page -> page.getNumber() == pageIndex)
|
||||
.findFirst().orElseThrow(() -> new NoSuchElementException(format("ClassificationPage with number %d not found", pageIndex)));
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new NoSuchElementException(format("ClassificationPage with number %d not found", pageIndex)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import java.awt.geom.Rectangle2D;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
@@ -19,8 +19,8 @@ public class SearchTextWithTextPositionDto {
|
||||
String searchText;
|
||||
List<Integer> lineBreaks;
|
||||
List<Integer> stringIdxToPositionIdx;
|
||||
List<Boundary> boldTextBoundaries;
|
||||
List<Boundary> italicTextBoundaries;
|
||||
List<TextRange> boldTextBoundaries;
|
||||
List<TextRange> italicTextBoundaries;
|
||||
List<Rectangle2D> positions;
|
||||
|
||||
|
||||
|
||||
+8
-7
@@ -9,7 +9,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextDirection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
@@ -26,6 +26,7 @@ public class SearchTextWithTextPositionFactory {
|
||||
// This is why, we need to initialize this to < -2, otherwise, if the very first symbol is a \n we would detect a hyphen linebreak that isn't there.
|
||||
// Also, Integer.MIN_VALUE is a bad idea due to potential overflow during arithmetic operations. This is why the default should be -3.
|
||||
public final int MAX_HYPHEN_LINEBREAK_DISTANCE = 3;
|
||||
public static final double LINEBREAK_DELTA_TOLERANCE = 1.05;
|
||||
|
||||
|
||||
public SearchTextWithTextPositionDto buildSearchTextToTextPositionDto(List<TextPositionSequence> sequences) {
|
||||
@@ -118,23 +119,23 @@ public class SearchTextWithTextPositionFactory {
|
||||
}
|
||||
|
||||
|
||||
private static List<Boundary> mergeToBoundaries(List<Integer> integers) {
|
||||
private static List<TextRange> mergeToBoundaries(List<Integer> integers) {
|
||||
|
||||
if (integers.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Boundary> boundaries = new LinkedList<>();
|
||||
List<TextRange> boundaries = new LinkedList<>();
|
||||
int start = integers.get(0);
|
||||
int end = integers.get(0) + 1;
|
||||
for (int current : integers) {
|
||||
if (current > end + 1) {
|
||||
boundaries.add(new Boundary(start, end));
|
||||
boundaries.add(new TextRange(start, end));
|
||||
start = current;
|
||||
}
|
||||
end = current + 1;
|
||||
}
|
||||
if (boundaries.isEmpty()) {
|
||||
boundaries.add(new Boundary(start, end));
|
||||
boundaries.add(new TextRange(start, end));
|
||||
}
|
||||
return boundaries;
|
||||
}
|
||||
@@ -160,8 +161,8 @@ public class SearchTextWithTextPositionFactory {
|
||||
return false;
|
||||
}
|
||||
|
||||
double deltaY = Math.abs(currentPosition.getYDirAdj() - previousPosition.getYDirAdj());
|
||||
return deltaY >= currentPosition.getHeightDir();
|
||||
double deltaY = Math.abs(currentPosition.getYDirAdj() - previousPosition.getYDirAdj()) * LINEBREAK_DELTA_TOLERANCE;
|
||||
return deltaY >= currentPosition.getHeightDir() || deltaY >= previousPosition.getHeightDir();
|
||||
}
|
||||
|
||||
|
||||
|
||||
+3
-2
@@ -55,7 +55,8 @@ public class SectionNodeFactory {
|
||||
.collect(groupingBy(AbstractPageBlock::getPage));
|
||||
|
||||
AbstractSemanticNode section;
|
||||
if (isLeaf) {
|
||||
boolean containsTablesAndTextBlocks = containsTablesAndTextBlocks(pageBlocks);
|
||||
if (isLeaf && !containsTablesAndTextBlocks) {
|
||||
section = Section.builder().documentTree(context.getDocumentTree()).build();
|
||||
} else {
|
||||
section = SuperSection.builder().documentTree(context.getDocumentTree()).build();
|
||||
@@ -68,7 +69,7 @@ public class SectionNodeFactory {
|
||||
section.setTreeId(getTreeId(parentNode, context, section));
|
||||
|
||||
addFirstHeadlineDirectlyToSection(layoutParsingType, pageBlocks, context, section, document);
|
||||
if (containsTablesAndTextBlocks(pageBlocks)) {
|
||||
if (containsTablesAndTextBlocks) {
|
||||
splitPageBlocksIntoSubSections(pageBlocks).forEach(subSectionPageBlocks -> addSection(layoutParsingType,
|
||||
section,
|
||||
true,
|
||||
|
||||
+2
-2
@@ -116,8 +116,8 @@ public class DocumentDataMapper {
|
||||
.page(atomicTextBlock.getPage().getNumber().longValue())
|
||||
.searchText(atomicTextBlock.getSearchText())
|
||||
.numberOnPage(atomicTextBlock.getNumberOnPage())
|
||||
.start(atomicTextBlock.getBoundary().start())
|
||||
.end(atomicTextBlock.getBoundary().end())
|
||||
.start(atomicTextBlock.getTextRange().start())
|
||||
.end(atomicTextBlock.getTextRange().end())
|
||||
.lineBreaks(toPrimitiveIntArray(atomicTextBlock.getLineBreaks()))
|
||||
.build();
|
||||
}
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ public class PropertiesMapper {
|
||||
properties.put(DocumentStructure.ImageProperties.TRANSPARENT, String.valueOf(image.isTransparent()));
|
||||
properties.put(DocumentStructure.ImageProperties.POSITION, toString(image.getPosition()));
|
||||
properties.put(DocumentStructure.ImageProperties.ID, image.getId());
|
||||
properties.put(DocumentStructure.ImageProperties.REPRESENTATION_HASH, image.getRepresentationHash());
|
||||
return properties;
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -13,7 +13,7 @@ import com.knecon.fforesight.service.layoutparser.internal.api.data.taas.Researc
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.taas.RowData;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.taas.StructureObject;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.taas.TableData;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
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.SemanticNode;
|
||||
@@ -82,15 +82,15 @@ public class TaasDocumentDataMapper {
|
||||
}
|
||||
|
||||
|
||||
private static Range toRange(Boundary boundary) {
|
||||
private static Range toRange(TextRange textRange) {
|
||||
|
||||
return new Range(boundary.start(), boundary.end());
|
||||
return new Range(textRange.start(), textRange.end());
|
||||
}
|
||||
|
||||
|
||||
private static List<Range> toRange(List<Boundary> boundary) {
|
||||
private static List<Range> toRange(List<TextRange> textRange) {
|
||||
|
||||
return boundary.stream().map(TaasDocumentDataMapper::toRange).toList();
|
||||
return textRange.stream().map(TaasDocumentDataMapper::toRange).toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ public class TaasDocumentDataMapper {
|
||||
Page page = semanticNode.getFirstPage();
|
||||
Rectangle2D bBox = semanticNode.getBBox().get(page);
|
||||
return StructureObject.builder()
|
||||
.treeId(semanticNode.getTreeId())
|
||||
.structureObjectNumber(structureObjectNumber)
|
||||
.boundingBox(toFloatArray(bBox))
|
||||
.stringOffset(semanticNode.getBoundary().start())
|
||||
@@ -114,6 +115,7 @@ public class TaasDocumentDataMapper {
|
||||
Page page = table.getFirstPage();
|
||||
Rectangle2D bBox = table.getBBox().get(page);
|
||||
return StructureObject.builder()
|
||||
.treeId(table.getTreeId())
|
||||
.structureObjectNumber(structureObjectNumber)
|
||||
.boundingBox(toFloatArray(bBox))
|
||||
.stringOffset(table.getBoundary().start())
|
||||
|
||||
+19
-3
@@ -82,6 +82,7 @@ public class LegacyPDFStreamEngine extends PDFStreamEngine {
|
||||
|
||||
private int pageRotation;
|
||||
private PDRectangle pageSize;
|
||||
private Matrix translateMatrix;
|
||||
private final GlyphList glyphList;
|
||||
private final Map<COSDictionary, Float> fontHeightMap = new WeakHashMap<COSDictionary, Float>();
|
||||
|
||||
@@ -133,6 +134,12 @@ public class LegacyPDFStreamEngine extends PDFStreamEngine {
|
||||
this.pageRotation = page.getRotation();
|
||||
this.pageSize = page.getCropBox();
|
||||
|
||||
if (pageSize.getLowerLeftX() == 0 && pageSize.getLowerLeftY() == 0) {
|
||||
translateMatrix = null;
|
||||
} else {
|
||||
// translation matrix for cropbox
|
||||
translateMatrix = Matrix.getTranslateInstance(-pageSize.getLowerLeftX(), -pageSize.getLowerLeftY());
|
||||
}
|
||||
super.processPage(page);
|
||||
}
|
||||
|
||||
@@ -257,13 +264,22 @@ public class LegacyPDFStreamEngine extends PDFStreamEngine {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// adjust for cropbox if needed
|
||||
Matrix translatedTextRenderingMatrix;
|
||||
if (translateMatrix == null) {
|
||||
translatedTextRenderingMatrix = textRenderingMatrix;
|
||||
} else {
|
||||
translatedTextRenderingMatrix = Matrix.concatenate(translateMatrix, textRenderingMatrix);
|
||||
nextX -= pageSize.getLowerLeftX();
|
||||
nextY -= pageSize.getLowerLeftY();
|
||||
}
|
||||
|
||||
// This is a hack for unicode letter with 2 chars e.g. RA see unicodeProblem.pdf
|
||||
if (unicodeMapping.length() == 2) {
|
||||
processTextPosition(new TextPosition(pageRotation,
|
||||
pageSize.getWidth(),
|
||||
pageSize.getHeight(),
|
||||
textRenderingMatrix,
|
||||
translatedTextRenderingMatrix,
|
||||
nextX,
|
||||
nextY,
|
||||
Math.abs(dyDisplay),
|
||||
@@ -277,7 +293,7 @@ public class LegacyPDFStreamEngine extends PDFStreamEngine {
|
||||
processTextPosition(new TextPosition(pageRotation,
|
||||
pageSize.getWidth(),
|
||||
pageSize.getHeight(),
|
||||
textRenderingMatrix,
|
||||
translatedTextRenderingMatrix,
|
||||
nextX,
|
||||
nextY,
|
||||
Math.abs(dyDisplay),
|
||||
@@ -293,7 +309,7 @@ public class LegacyPDFStreamEngine extends PDFStreamEngine {
|
||||
processTextPosition(new TextPosition(pageRotation,
|
||||
pageSize.getWidth(),
|
||||
pageSize.getHeight(),
|
||||
textRenderingMatrix,
|
||||
translatedTextRenderingMatrix,
|
||||
nextX,
|
||||
nextY,
|
||||
Math.abs(dyDisplay),
|
||||
|
||||
+23
-363
@@ -1,39 +1,17 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.services.visualization;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RectangularShape;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
|
||||
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.DuplicatedParagraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Headline;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Image;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Table;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.TableCell;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
import com.knecon.fforesight.service.viewerdoc.ContentStreams;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.ColoredLine;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.ColoredRectangle;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.LayoutGrid;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.PlacedText;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Standard14EmbeddableFont;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
import com.knecon.fforesight.service.viewerdoc.service.ViewerDocumentService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutGrid;
|
||||
import com.knecon.fforesight.service.viewerdoc.service.PDFTronViewerDocumentService;
|
||||
|
||||
import io.micrometer.observation.annotation.Observed;
|
||||
import lombok.AccessLevel;
|
||||
@@ -46,359 +24,41 @@ import lombok.experimental.FieldDefaults;
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
|
||||
public class LayoutGridService {
|
||||
|
||||
ViewerDocumentService viewerDocumentService;
|
||||
|
||||
static float FONT_SIZE = 10f;
|
||||
static float LINE_WIDTH = 1f;
|
||||
static Standard14EmbeddableFont FONT = Standard14EmbeddableFont.helvetica();
|
||||
|
||||
static Color INNER_LINES_COLOR = new Color(255, 175, 175);
|
||||
static Color PARAGRAPH_COLOR = new Color(70, 130, 180);
|
||||
|
||||
static Color DUPLICATE_PARAGRAPH_COLOR = new Color(70, 180, 101);
|
||||
static Color TABLE_COLOR = new Color(102, 205, 170);
|
||||
static Color SECTION_COLOR = new Color(50, 50, 50);
|
||||
static Color HEADLINE_COLOR = new Color(162, 56, 56);
|
||||
static Color HEADER_COLOR = new Color(171, 131, 6);
|
||||
static Color IMAGE_COLOR = new Color(253, 63, 146);
|
||||
PDFTronViewerDocumentService viewerDocumentService;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Observed(name = "ViewerDocumentService", contextualName = "create-viewer-document")
|
||||
public void addLayoutGrid(File originFile, Document document, File destinationFile, boolean layerVisibilityDefaultValue, boolean writeVisualLayoutParsingGrid) {
|
||||
|
||||
List<Visualizations> allVisualizations;
|
||||
Visualizations layoutGrid = this.addLayoutGrid(document, layerVisibilityDefaultValue, false);
|
||||
if (writeVisualLayoutParsingGrid) {
|
||||
Visualizations visualLayoutGrid = this.addLayoutGrid(document, layerVisibilityDefaultValue, true);
|
||||
allVisualizations = Stream.concat(Stream.of(layoutGrid, visualLayoutGrid), document.getVisualizations().streamAll())
|
||||
.toList();
|
||||
LayoutGrid layoutGrid = createLayoutGrid(document);
|
||||
layoutGrid.setVisibleByDefault(layerVisibilityDefaultValue);
|
||||
// Visualizations visualLayoutGrid = this.addLayoutGrid(document, layerVisibilityDefaultValue, true);
|
||||
if (document.getLayoutDebugLayer().isActive()) {
|
||||
viewerDocumentService.addLayerGroups(originFile, destinationFile, List.of(layoutGrid, document.getLayoutDebugLayer()));
|
||||
} else {
|
||||
allVisualizations = Stream.concat(Stream.of(layoutGrid), document.getVisualizations().streamAll())
|
||||
.toList();
|
||||
viewerDocumentService.addLayerGroups(originFile, destinationFile, List.of(layoutGrid));
|
||||
|
||||
}
|
||||
|
||||
viewerDocumentService.addVisualizationsOnPage(originFile, destinationFile, allVisualizations);
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Observed(name = "ViewerDocumentService", contextualName = "create-viewer-document")
|
||||
public Visualizations addLayoutGrid(Document document, boolean layerVisibilityDefaultValue, boolean visualParsingGrid) {
|
||||
private LayoutGrid createLayoutGrid(Document document) {
|
||||
|
||||
LayoutGrid layoutGrid = createLayoutGrid(document, visualParsingGrid);
|
||||
|
||||
return Visualizations.builder()
|
||||
.layer(visualParsingGrid ? ContentStreams.KNECON_VISUAL_PARSING : ContentStreams.KNECON_LAYOUT)
|
||||
.visualizationsOnPages(layoutGrid.getVisualizationsPerPages())
|
||||
.layerVisibilityDefaultValue(layerVisibilityDefaultValue)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private LayoutGrid createLayoutGrid(Document document, boolean visualParsingGrid) {
|
||||
|
||||
LayoutGrid layoutGrid = new LayoutGrid(document.getNumberOfPages());
|
||||
LayoutGrid layoutGrid = new LayoutGrid();
|
||||
document.streamAllSubNodes()
|
||||
.filter(node -> (node.getEngines().contains(LayoutEngine.AI) && visualParsingGrid) || (node.getEngines().contains(LayoutEngine.ALGORITHM) && !visualParsingGrid))
|
||||
.peek(layoutGrid::addTreeId)
|
||||
.forEach(semanticNode -> {
|
||||
Color color = switch (semanticNode.getType()) {
|
||||
case PARAGRAPH -> PARAGRAPH_COLOR;
|
||||
case TABLE -> TABLE_COLOR;
|
||||
case SECTION, SUPER_SECTION -> SECTION_COLOR;
|
||||
case HEADLINE -> HEADLINE_COLOR;
|
||||
case HEADER, FOOTER -> HEADER_COLOR;
|
||||
case IMAGE -> IMAGE_COLOR;
|
||||
default -> null;
|
||||
};
|
||||
|
||||
if (semanticNode instanceof DuplicatedParagraph) {
|
||||
color = DUPLICATE_PARAGRAPH_COLOR;
|
||||
}
|
||||
|
||||
if (isNotSectionOrTableCellOrDocument(semanticNode)) {
|
||||
addAsRectangle(semanticNode, layoutGrid, color);
|
||||
}
|
||||
if (semanticNode.getType().equals(NodeType.SECTION) || semanticNode.getType().equals(NodeType.SUPER_SECTION)) {
|
||||
addSection(semanticNode, layoutGrid, color);
|
||||
}
|
||||
if (semanticNode.getType().equals(NodeType.TABLE)) {
|
||||
Table table = (Table) semanticNode;
|
||||
addInnerTableLines(table, layoutGrid);
|
||||
switch (semanticNode.getType()) {
|
||||
case SECTION, SUPER_SECTION -> layoutGrid.addSection(semanticNode);
|
||||
case HEADLINE -> layoutGrid.addHeadline((Headline) semanticNode);
|
||||
case PARAGRAPH -> layoutGrid.addParagraph((Paragraph) semanticNode);
|
||||
case TABLE -> layoutGrid.addTable((Table) semanticNode);
|
||||
case IMAGE -> layoutGrid.addImage((Image) semanticNode);
|
||||
case HEADER, FOOTER -> layoutGrid.addHeaderOrFooter(semanticNode);
|
||||
}
|
||||
});
|
||||
return layoutGrid;
|
||||
}
|
||||
|
||||
|
||||
private void addInnerTableLines(Table table, LayoutGrid layoutGrid) {
|
||||
|
||||
if (table.getNumberOfCols() < 1 || table.getNumberOfRows() < 1) {
|
||||
return;
|
||||
}
|
||||
for (Page page : table.getPages()) {
|
||||
|
||||
Optional<Integer> optionalFirstRowOnPage = table.streamCol(0)
|
||||
.filter(tableCell -> tableCell.isOnPage(page.getNumber()))
|
||||
.map(TableCell::getRow)
|
||||
.findFirst();
|
||||
if (optionalFirstRowOnPage.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int firstRowOnPage = optionalFirstRowOnPage.get();
|
||||
|
||||
Stream<Double> xStream = switch (page.getRotation()) {
|
||||
case 90 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMinX);
|
||||
case 180 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMaxX);
|
||||
case 270 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMaxX);
|
||||
default -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMinX);
|
||||
};
|
||||
List<Double> xs = xStream.collect(Collectors.toList());
|
||||
xs.remove(0);
|
||||
|
||||
Stream<Double> yStream = switch (page.getRotation()) {
|
||||
case 90 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMinY);
|
||||
case 180 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMinY);
|
||||
case 270 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMaxY);
|
||||
default -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMaxY);
|
||||
};
|
||||
List<Double> ys = yStream.collect(Collectors.toList());
|
||||
ys.remove(0);
|
||||
|
||||
Rectangle2D tableBBox = table.getBBox()
|
||||
.get(page);
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages()
|
||||
.get(page.getNumber() - 1).getColoredLines();
|
||||
xs.forEach(x -> {
|
||||
Line2D line = new Line2D.Double(new Point2D.Double(x, tableBBox.getMaxY()), new Point2D.Double(x, tableBBox.getMinY()));
|
||||
coloredLines.add(new ColoredLine(line, INNER_LINES_COLOR, LINE_WIDTH));
|
||||
});
|
||||
ys.forEach(y -> {
|
||||
Line2D line = new Line2D.Double(new Point2D.Double(tableBBox.getMinX(), y), new Point2D.Double(tableBBox.getMaxX(), y));
|
||||
coloredLines.add(new ColoredLine(line, INNER_LINES_COLOR, LINE_WIDTH));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Stream<Rectangle2D> streamBBoxOfCellsOnPage(Stream<TableCell> table, Page page) {
|
||||
|
||||
return table.filter(tableCell -> tableCell.isOnPage(page.getNumber()))
|
||||
.map(TableCell::getBBox)
|
||||
.map(bBoxMap -> bBoxMap.get(page));
|
||||
}
|
||||
|
||||
|
||||
private void addSection(SemanticNode semanticNode, LayoutGrid layoutGrid, Color color) {
|
||||
|
||||
Map<Page, Rectangle2D> bBoxMap = semanticNode.getBBox();
|
||||
List<SemanticNode> subSections = semanticNode.streamAllSubNodesOfType(NodeType.SECTION)
|
||||
.toList();
|
||||
float lineWidthModifier = (subSections.stream()
|
||||
.map(node -> node.getTreeId().size())
|
||||
.max(Integer::compareTo)
|
||||
.orElse(semanticNode.getTreeId().size()) - semanticNode.getTreeId().size());
|
||||
|
||||
Page firstPage = semanticNode.getFirstPage();
|
||||
String treeIdString = buildTreeIdString(semanticNode);
|
||||
if (!subSections.isEmpty()) {
|
||||
addPlacedText(firstPage, bBoxMap.get(firstPage), treeIdString, layoutGrid);
|
||||
} else {
|
||||
bBoxMap.forEach(((page, textBBox) -> addPlacedText(page, textBBox, treeIdString, layoutGrid)));
|
||||
}
|
||||
if (bBoxMap.values().size() == 1) {
|
||||
Rectangle2D r = RectangleTransformations.pad(bBoxMap.get(firstPage), LINE_WIDTH * (1 + lineWidthModifier), LINE_WIDTH * (1 + lineWidthModifier));
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages()
|
||||
.get(firstPage.getNumber() - 1).getColoredLines();
|
||||
List<Line2D> lines = createLinesFromRectangle(r, firstPage.getRotation());
|
||||
// add string to top line
|
||||
var firstLine = lines.remove(0);
|
||||
coloredLines.add(new ColoredLine(firstLine, color, LINE_WIDTH));
|
||||
for (Line2D line : lines) {
|
||||
coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
return;
|
||||
}
|
||||
List<Page> pagesInOrder = bBoxMap.keySet()
|
||||
.stream()
|
||||
.sorted(Comparator.comparingInt(Page::getNumber))
|
||||
.collect(Collectors.toList());
|
||||
pagesInOrder.remove(0);
|
||||
addLinesForFirstPageOfSection(semanticNode, color, firstPage, layoutGrid, lineWidthModifier);
|
||||
var lastPage = pagesInOrder.remove(pagesInOrder.size() - 1);
|
||||
addLinesForLastPageOfSection(semanticNode, color, lastPage, layoutGrid, lineWidthModifier);
|
||||
for (Page middlePage : pagesInOrder) {
|
||||
addLinesForMiddlePageOfSection(semanticNode, color, middlePage, layoutGrid, lineWidthModifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private void addPlacedText(Page page, Rectangle2D textBBox, String s, LayoutGrid layoutGrid) {
|
||||
|
||||
// translates text, such that its right edge is a bit to the left of the drawn box
|
||||
float translationAmount = ((FONT.getStringWidth(s) / 1000) * FONT_SIZE + (2 * LINE_WIDTH) + 4);
|
||||
|
||||
Point2D upperLeftCorner;
|
||||
Point2D translationVector;
|
||||
switch (page.getRotation()) {
|
||||
case 90 -> {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMinY());
|
||||
translationVector = new Point2D.Double(FONT_SIZE, -translationAmount);
|
||||
}
|
||||
case 180 -> {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMaxX(), textBBox.getMinY());
|
||||
translationVector = new Point2D.Double(translationAmount, FONT_SIZE);
|
||||
}
|
||||
case 270 -> {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMaxX(), textBBox.getMaxY());
|
||||
translationVector = new Point2D.Double(-FONT_SIZE, translationAmount);
|
||||
}
|
||||
default -> {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMaxY());
|
||||
translationVector = new Point2D.Double(-translationAmount, -FONT_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
upperLeftCorner = add(upperLeftCorner, translationVector);
|
||||
|
||||
var placedTexts = layoutGrid.getVisualizationsPerPages()
|
||||
.get(page.getNumber() - 1).getPlacedTexts();
|
||||
placedTexts.add(PlacedText.textFacingUp(s, upperLeftCorner, FONT_SIZE, Color.BLACK, FONT));
|
||||
}
|
||||
|
||||
|
||||
private void addLinesForMiddlePageOfSection(SemanticNode semanticNode, Color color, Page middlePage, LayoutGrid layoutGrid, float lineWidthModifier) {
|
||||
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages()
|
||||
.get(middlePage.getNumber() - 1).getColoredLines();
|
||||
Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox()
|
||||
.get(middlePage), LINE_WIDTH * (1 + lineWidthModifier), LINE_WIDTH * (1 + lineWidthModifier));
|
||||
var midPageLines = createLinesFromRectangle(r, middlePage.getRotation());
|
||||
// remove top line
|
||||
midPageLines.remove(0);
|
||||
// remove top line
|
||||
midPageLines.remove(1);
|
||||
// add string to left line
|
||||
var leftLine = midPageLines.remove(1);
|
||||
coloredLines.add(new ColoredLine(leftLine, color, LINE_WIDTH));
|
||||
for (Line2D line : midPageLines) {
|
||||
coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void addLinesForLastPageOfSection(SemanticNode semanticNode, Color color, Page lastPage, LayoutGrid layoutGrid, float lineWidthModifier) {
|
||||
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages()
|
||||
.get(lastPage.getNumber() - 1).getColoredLines();
|
||||
Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox()
|
||||
.get(lastPage), LINE_WIDTH * (1 + lineWidthModifier), LINE_WIDTH * (1 + lineWidthModifier));
|
||||
var lastPageLines = createLinesFromRectangle(r, lastPage.getRotation());
|
||||
// remove top line
|
||||
lastPageLines.remove(0);
|
||||
// add string to left line
|
||||
var leftLine = lastPageLines.remove(2);
|
||||
coloredLines.add(new ColoredLine(leftLine, color, LINE_WIDTH));
|
||||
for (Line2D line : lastPageLines) {
|
||||
coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void addLinesForFirstPageOfSection(SemanticNode semanticNode, Color color, Page firstPage, LayoutGrid layoutGrid, float lineWidthModifier) {
|
||||
|
||||
List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages()
|
||||
.get(firstPage.getNumber() - 1).getColoredLines();
|
||||
Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox()
|
||||
.get(firstPage), LINE_WIDTH * (1 + lineWidthModifier), LINE_WIDTH * (1 + lineWidthModifier));
|
||||
var firstPageLines = createLinesFromRectangle(r, firstPage.getRotation());
|
||||
// remove bottom line
|
||||
firstPageLines.remove(2);
|
||||
// add string to top line
|
||||
var firstLine = firstPageLines.remove(0);
|
||||
coloredLines.add(new ColoredLine(firstLine, color, LINE_WIDTH));
|
||||
for (Line2D line : firstPageLines) {
|
||||
coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String buildTreeIdString(SemanticNode semanticNode) {
|
||||
|
||||
return semanticNode.getTreeId()
|
||||
.stream()
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.joining("."));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
A __________________ B
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
D|__________________| C
|
||||
The returned List are the lines [AB, BC, DC, AD]
|
||||
The List is reordered, such that the order of the returned lines are always as viewed on the page.
|
||||
*/
|
||||
private List<Line2D> createLinesFromRectangle(Rectangle2D r, int pageRotation) {
|
||||
// +0.5 to join the lines
|
||||
List<Line2D> lines = new ArrayList<>(4);
|
||||
float lineWidthCorrection = LINE_WIDTH * 0.5f;
|
||||
Point2D.Float a = new Point2D.Float((float) r.getMinX(), (float) r.getMaxY());
|
||||
Point2D.Float a1 = new Point2D.Float((float) r.getMinX() - lineWidthCorrection, (float) r.getMaxY());
|
||||
Point2D.Float b = new Point2D.Float((float) r.getMaxX(), (float) r.getMaxY());
|
||||
Point2D.Float b1 = new Point2D.Float((float) r.getMaxX() + lineWidthCorrection, (float) r.getMaxY());
|
||||
Point2D.Float c = new Point2D.Float((float) r.getMaxX(), (float) r.getMinY());
|
||||
Point2D.Float c1 = new Point2D.Float((float) r.getMaxX() + lineWidthCorrection, (float) r.getMinY());
|
||||
Point2D.Float d = new Point2D.Float((float) r.getMinX(), (float) r.getMinY());
|
||||
Point2D.Float d1 = new Point2D.Float((float) r.getMinX() - lineWidthCorrection, (float) r.getMinY());
|
||||
lines.add(new Line2D.Float(a1, b1));
|
||||
lines.add(new Line2D.Float(b, c));
|
||||
lines.add(new Line2D.Float(d1, c1));
|
||||
lines.add(new Line2D.Float(a, d));
|
||||
|
||||
return switch (pageRotation) {
|
||||
case 90 -> {
|
||||
Collections.rotate(lines, 1);
|
||||
yield lines;
|
||||
}
|
||||
case 180 -> {
|
||||
Collections.rotate(lines, 2);
|
||||
yield lines;
|
||||
}
|
||||
case 270 -> {
|
||||
Collections.rotate(lines, 3);
|
||||
yield lines;
|
||||
}
|
||||
|
||||
default -> lines;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static boolean isNotSectionOrTableCellOrDocument(SemanticNode semanticNode) {
|
||||
|
||||
return !(semanticNode.getType().equals(NodeType.DOCUMENT)
|
||||
|| semanticNode.getType().equals(NodeType.SECTION)
|
||||
|| semanticNode.getType().equals(NodeType.SUPER_SECTION)
|
||||
|| semanticNode.getType().equals(NodeType.TABLE_CELL));
|
||||
}
|
||||
|
||||
|
||||
private void addAsRectangle(SemanticNode semanticNode, LayoutGrid layoutGrid, Color color) {
|
||||
|
||||
semanticNode.getBBox()
|
||||
.forEach((page, textBBox) -> layoutGrid.getVisualizationsPerPages()
|
||||
.get(page.getNumber() - 1).getColoredRectangles().add(new ColoredRectangle(textBBox, color, LINE_WIDTH)));
|
||||
}
|
||||
|
||||
|
||||
private Point2D add(Point2D a, Point2D b) {
|
||||
|
||||
return new Point2D.Double(a.getX() + b.getX(), a.getY() + b.getY());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.utils;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class BBoxMergingUtility {
|
||||
|
||||
public Map<Page, Rectangle2D> mergeBBoxes(List<Map<Page, Rectangle2D>> bboxesToMerge) {
|
||||
|
||||
Map<Page, Rectangle2D> bBoxPerPage = new HashMap<>();
|
||||
Set<Page> pages = bboxesToMerge.stream()
|
||||
.flatMap(map -> map.keySet()
|
||||
.stream())
|
||||
.collect(Collectors.toSet());
|
||||
for (Page page : pages) {
|
||||
Rectangle2D bBoxOnPage = bboxesToMerge.stream()
|
||||
.filter(childBboxPerPage -> childBboxPerPage.containsKey(page))
|
||||
.map(childBboxPerPage -> childBboxPerPage.get(page))
|
||||
.collect(RectangleTransformations.collectBBox());
|
||||
bBoxPerPage.put(page, bBoxOnPage);
|
||||
}
|
||||
return bBoxPerPage;
|
||||
}
|
||||
|
||||
}
|
||||
+42
-7
@@ -51,13 +51,48 @@ public class TextPositionSequenceComparator implements Comparator<TextPositionSe
|
||||
|
||||
double yDifference = Math.abs(pos1YBottom - pos2YBottom);
|
||||
|
||||
// we will do a simple tolerance comparison
|
||||
if (yDifference < .1 || pos2YBottom >= pos1YTop && pos2YBottom <= pos1YBottom || pos1YBottom >= pos2YTop && pos1YBottom <= pos2YBottom) {
|
||||
return Double.compare(x1, x2);
|
||||
} else if (pos1YBottom < pos2YBottom) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
|
||||
// Adjust for text rotation
|
||||
switch (pos1.getRotation()) {
|
||||
case 0:
|
||||
// 0 degrees (horizontal, top to bottom and left to right): Sort primarily by y-coordinates from top to bottom (pos1YBottom < pos2YBottom).
|
||||
if (yDifference < .1 || (pos2YBottom >= pos1YTop && pos2YBottom <= pos1YBottom) || (pos1YBottom >= pos2YTop && pos1YBottom <= pos2YBottom)) {
|
||||
return Double.compare(x1, x2);
|
||||
} else if (pos1YBottom < pos2YBottom) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
case 90:
|
||||
// 90 degrees (vertical, right to left): Sort by x-coordinates first (x1 > x2), then by y-coordinates from top to bottom (pos1YBottom < pos2YBottom).
|
||||
if (x1 > x2) {
|
||||
return -1;
|
||||
} else if (x1 < x2) {
|
||||
return 1;
|
||||
} else {
|
||||
return Double.compare(pos1YBottom, pos2YBottom);
|
||||
}
|
||||
case 180:
|
||||
// 180 degrees (horizontal, bottom to top and right to left): Sort primarily by y-coordinates from bottom to top (pos1YBottom > pos2YBottom).
|
||||
if (yDifference < .1 || (pos2YBottom >= pos1YTop && pos2YBottom <= pos1YBottom) || (pos1YBottom >= pos2YTop && pos1YBottom <= pos2YBottom)) {
|
||||
return Double.compare(x2, x1);
|
||||
|
||||
} else if (pos1YBottom > pos2YBottom) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
case 270:
|
||||
// 270 degrees (vertical, left to right): Sort by x-coordinates in reverse (x2 > x1), then by y-coordinates from bottom to top (pos2YBottom > pos1YBottom).
|
||||
if (x2 > x1) {
|
||||
return -1;
|
||||
} else if (x2 < x1) {
|
||||
return 1;
|
||||
} else {
|
||||
return Double.compare(pos2YBottom, pos1YBottom);
|
||||
}
|
||||
default:
|
||||
throw new RuntimeException("Rotation not supported. Only 0/90/180/270 degree rotation is supported.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.visualization;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class ConnectionLineUtil {
|
||||
|
||||
public static Line2D[] splitRectangleIntoLines(Rectangle2D rect) {
|
||||
|
||||
double x = rect.getX();
|
||||
double y = rect.getY();
|
||||
double width = rect.getWidth();
|
||||
double height = rect.getHeight();
|
||||
|
||||
Line2D[] lines = new Line2D[4];
|
||||
lines[0] = new Line2D.Double(x, y, x + width, y); // Top
|
||||
lines[1] = new Line2D.Double(x + width, y, x + width, y + height); // Right
|
||||
lines[2] = new Line2D.Double(x + width, y + height, x, y + height); // Bottom
|
||||
lines[3] = new Line2D.Double(x, y + height, x, y); // Left
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
||||
public static Line2D transform(Line2D line2D, AffineTransform affineTransform) {
|
||||
|
||||
var p1 = affineTransform.transform(line2D.getP1(), null);
|
||||
var p2 = affineTransform.transform(line2D.getP2(), null);
|
||||
return new Line2D.Double(p1, p2);
|
||||
}
|
||||
|
||||
|
||||
public static double length(Line2D line2D) {
|
||||
|
||||
return line2D.getP1().distance(line2D.getP2());
|
||||
}
|
||||
|
||||
|
||||
public static Line2D findClosestMidpointLine(Rectangle2D rect1, Rectangle2D rect2) {
|
||||
|
||||
Line2D[] lines1 = splitRectangleIntoLines(rect1);
|
||||
Line2D[] lines2 = splitRectangleIntoLines(rect2);
|
||||
|
||||
Line2D closestLine1 = null;
|
||||
Line2D closestLine2 = null;
|
||||
double minDistance = Double.MAX_VALUE;
|
||||
|
||||
for (Line2D line1 : lines1) {
|
||||
for (Line2D line2 : lines2) {
|
||||
double distance = lineDistance(line1, line2);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestLine1 = line1;
|
||||
closestLine2 = line2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closestLine1 == null || closestLine2 == null) {
|
||||
throw new IllegalStateException("Could not find closest lines");
|
||||
}
|
||||
|
||||
Point2D midpoint1 = getMidpoint(closestLine1);
|
||||
Point2D midpoint2 = getMidpoint(closestLine2);
|
||||
|
||||
return new Line2D.Double(midpoint1, midpoint2);
|
||||
}
|
||||
|
||||
|
||||
private static double lineDistance(Line2D line1, Line2D line2) {
|
||||
|
||||
return Math.abs(getMidpoint(line1).distance(getMidpoint(line2)));
|
||||
}
|
||||
|
||||
|
||||
private static Point2D getMidpoint(Line2D line) {
|
||||
|
||||
double x = (line.getX1() + line.getX2()) / 2;
|
||||
double y = (line.getY1() + line.getY2()) / 2;
|
||||
return new Point2D.Double(x, y);
|
||||
}
|
||||
|
||||
|
||||
public static Line2D[] createArrowHead(Line2D line, double arrowLength) {
|
||||
|
||||
Point2D start = line.getP1();
|
||||
Point2D end = line.getP2();
|
||||
|
||||
// Calculate the angle of the line
|
||||
double angle = Math.atan2(end.getY() - start.getY(), end.getX() - start.getX());
|
||||
|
||||
// Calculate the points for the two arrow lines
|
||||
double arrowHeadAngle = Math.PI / 6;
|
||||
double x1 = end.getX() - arrowLength * Math.cos(angle - arrowHeadAngle);
|
||||
double y1 = end.getY() - arrowLength * Math.sin(angle - arrowHeadAngle);
|
||||
double x2 = end.getX() - arrowLength * Math.cos(angle + arrowHeadAngle);
|
||||
double y2 = end.getY() - arrowLength * Math.sin(angle + arrowHeadAngle);
|
||||
|
||||
// Create and return the two arrow lines
|
||||
Line2D arrow1 = new Line2D.Double(end, new Point2D.Double(x1, y1));
|
||||
Line2D arrow2 = new Line2D.Double(end, new Point2D.Double(x2, y2));
|
||||
|
||||
return new Line2D[]{arrow1, arrow2};
|
||||
}
|
||||
|
||||
}
|
||||
+5
-77
@@ -7,7 +7,6 @@ import java.awt.geom.Rectangle2D;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent;
|
||||
|
||||
@@ -21,12 +20,10 @@ import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageB
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.MarkedContentUtils;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
import com.knecon.fforesight.service.viewerdoc.ContentStreams;
|
||||
import com.knecon.fforesight.service.viewerdoc.layers.LayoutDebugLayerConfig;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.ColoredLine;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.ColoredRectangle;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.PlacedText;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Standard14EmbeddableFont;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.VisualizationsOnPage;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
@@ -36,72 +33,15 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class LayoutparsingVisualizations {
|
||||
public class LayoutDebugLayer extends LayoutDebugLayerConfig {
|
||||
|
||||
static final Standard14EmbeddableFont FONT = Standard14EmbeddableFont.helvetica();
|
||||
|
||||
static final Color WORDS_COLOR = new Color(68, 84, 147);
|
||||
static final Color LINES_COLOR = new Color(152, 45, 179);
|
||||
static final Color ZONES_COLOR = new Color(131, 38, 38);
|
||||
|
||||
static final Color RULINGS_COLOR = new Color(21, 221, 174);
|
||||
static final Color TABLE_RULINGS_COLOR = new Color(255, 175, 175);
|
||||
static final Color HEADER_RULING_COLOR = new Color(171, 131, 6);
|
||||
static final Color FOOTER_RULING_COLOR = new Color(106, 82, 2);
|
||||
static final Color UNDERLINE_RULING_COLOR = new Color(6, 39, 171);
|
||||
static final Color STRIKETROUGH_RULING_COLOR = new Color(171, 6, 6);
|
||||
|
||||
static final Color CELLS_COLOR = new Color(31, 214, 27);
|
||||
|
||||
static final Color MAIN_BODY_COLOR = new Color(171, 131, 6);
|
||||
static final Color MARKED_CONTENT_COLOR = new Color(171, 131, 6);
|
||||
|
||||
static final List<Color> ROTATING_CHARACTER_COLOR = List.of(new Color(255, 87, 51),
|
||||
new Color(255, 195, 0),
|
||||
new Color(76, 175, 80),
|
||||
new Color(33, 150, 243),
|
||||
new Color(155, 89, 182),
|
||||
new Color(233, 30, 99),
|
||||
new Color(0, 188, 212),
|
||||
new Color(121, 85, 72));
|
||||
|
||||
@Setter
|
||||
boolean active;
|
||||
|
||||
final Visualizations words = Visualizations.builder().layer(ContentStreams.WORDS).build();
|
||||
final Visualizations lines = Visualizations.builder().layer(ContentStreams.LINES).build();
|
||||
final Visualizations zones = Visualizations.builder().layer(ContentStreams.ZONES).build();
|
||||
final Visualizations mainBody = Visualizations.builder().layer(ContentStreams.MAIN_BODY).build();
|
||||
final Visualizations clean_rulings = Visualizations.builder().layer(ContentStreams.CLEAN_RULINGS).build();
|
||||
final Visualizations rulings = Visualizations.builder().layer(ContentStreams.RULINGS).build();
|
||||
final Visualizations cells = Visualizations.builder().layer(ContentStreams.CELLS).build();
|
||||
final Visualizations markedContent = Visualizations.builder().layer(ContentStreams.MARKED_CONTENT).build();
|
||||
final Visualizations neighbours = Visualizations.builder().layer(ContentStreams.NEIGHBOURS).build();
|
||||
final Visualizations characters = Visualizations.builder().layer(ContentStreams.CHARACTERS).build();
|
||||
|
||||
|
||||
public Stream<Visualizations> streamAll() {
|
||||
|
||||
if (!active) {
|
||||
return Stream.empty();
|
||||
}
|
||||
return Stream.of(characters, //
|
||||
neighbours,//
|
||||
words, //
|
||||
lines, //
|
||||
zones, //
|
||||
rulings, //
|
||||
clean_rulings, //
|
||||
cells, //
|
||||
mainBody, //
|
||||
markedContent //
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void addTextVisualizations(List<TextPositionSequence> textPositionSequences, int pageNumber) {
|
||||
|
||||
@@ -130,6 +70,7 @@ public class LayoutparsingVisualizations {
|
||||
.toList());
|
||||
}
|
||||
|
||||
|
||||
public void addRulingVisualization(List<Ruling> rulings, int pageNumber) {
|
||||
|
||||
if (!active) {
|
||||
@@ -137,8 +78,7 @@ public class LayoutparsingVisualizations {
|
||||
}
|
||||
VisualizationsOnPage visualizationsOnPage = getOrCreateVisualizationsOnPage(pageNumber, this.rulings);
|
||||
visualizationsOnPage.getColoredLines()
|
||||
.addAll(rulings
|
||||
.stream()
|
||||
.addAll(rulings.stream()
|
||||
.map(ruling -> new ColoredLine(ruling, decideOnRulingColor(ruling), 0.5f))
|
||||
.toList());
|
||||
}
|
||||
@@ -295,16 +235,4 @@ public class LayoutparsingVisualizations {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private VisualizationsOnPage getOrCreateVisualizationsOnPage(int page, Visualizations visualizations) {
|
||||
|
||||
if (visualizations.getVisualizationsOnPages().containsKey(page - 1)) {
|
||||
return visualizations.getVisualizationsOnPages()
|
||||
.get(page - 1);
|
||||
}
|
||||
VisualizationsOnPage visualizationsOnPage = VisualizationsOnPage.builder().build();
|
||||
visualizations.getVisualizationsOnPages().put(page - 1, visualizationsOnPage);
|
||||
return visualizationsOnPage;
|
||||
}
|
||||
|
||||
}
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
package com.knecon.fforesight.service.layoutparser.processor.visualization;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RectangularShape;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.DuplicatedParagraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Headline;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Image;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SuperSection;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Table;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.TableCell;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
|
||||
import com.knecon.fforesight.service.viewerdoc.layers.LayoutGridLayerConfig;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.ColoredLine;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.ColoredRectangle;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.FilledRectangle;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.PlacedText;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class LayoutGrid extends LayoutGridLayerConfig {
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
boolean visibleByDefault;
|
||||
|
||||
final HashMap<RectangleIdentifier, Rectangle2D> rectangleMap = new HashMap<>();
|
||||
|
||||
|
||||
public void addParagraph(Paragraph paragraph) {
|
||||
|
||||
if (paragraph instanceof DuplicatedParagraph) {
|
||||
addAsRectangle(paragraph, paragraphs, DUPLICATE_PARAGRAPH_COLOR);
|
||||
} else {
|
||||
addAsRectangle(paragraph, paragraphs, PARAGRAPH_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addImage(Image image) {
|
||||
|
||||
if (image.isFullPageImage()) {
|
||||
addAsRectangle(image, images, IMAGE_COLOR);
|
||||
} else {
|
||||
addAsRectangle(image, figures, IMAGE_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addHeadline(Headline headline) {
|
||||
|
||||
addAsRectangle(headline, headlines, HEADLINE_COLOR);
|
||||
}
|
||||
|
||||
|
||||
public void addHeaderOrFooter(SemanticNode header) {
|
||||
|
||||
addAsRectangle(header, headerFooter, HEADER_COLOR);
|
||||
}
|
||||
|
||||
|
||||
public void addTreeId(SemanticNode semanticNode) {
|
||||
|
||||
Page page = semanticNode.getFirstPage();
|
||||
addPlacedText(page, semanticNode.getBBox().get(page), semanticNode.getBBox().get(page), buildTreeIdString(semanticNode), 1, treeIds, TREEID_COLOR);
|
||||
}
|
||||
|
||||
|
||||
public void addTable(Table table) {
|
||||
|
||||
addAsRectangle(table, tables, TABLE_COLOR);
|
||||
addInnerTableLines(table);
|
||||
addHeaderCells(table);
|
||||
}
|
||||
|
||||
|
||||
private void addHeaderCells(Table table) {
|
||||
|
||||
table.streamHeaders()
|
||||
.map(TableCell::getBBox)
|
||||
.forEach(map -> map.forEach((page, textBBox) -> getOrCreateVisualizationsOnPage(page.getNumber(), tables).getFilledRectangles()
|
||||
.add(new FilledRectangle(textBBox, HEADER_CELL_COLOR, 0.1f))));
|
||||
}
|
||||
|
||||
|
||||
public void addSection(SemanticNode section) {
|
||||
|
||||
Map<Page, Rectangle2D> bBoxMap = section.getBBox();
|
||||
|
||||
List<SemanticNode> subSections = section.streamAllSubNodesOfType(NodeType.SECTION)
|
||||
.toList();
|
||||
Integer maxChildDepth = subSections.stream()
|
||||
.map(node -> node.getTreeId().size())
|
||||
.max(Integer::compareTo).orElse(section.getTreeId().size());
|
||||
int ownDepth = section.getTreeId().size();
|
||||
|
||||
Page firstPage = section.getFirstPage();
|
||||
String treeIdString = buildTreeIdString(section);
|
||||
|
||||
if (bBoxMap.values().size() == 1) {
|
||||
handleSinglePage(section, firstPage, bBoxMap.get(firstPage), treeIdString, maxChildDepth, ownDepth);
|
||||
return;
|
||||
}
|
||||
List<Page> pagesInOrder = bBoxMap.keySet()
|
||||
.stream()
|
||||
.sorted(Comparator.comparingInt(Page::getNumber))
|
||||
.collect(Collectors.toList());
|
||||
pagesInOrder.remove(0);
|
||||
handleFirstPageOfSection(section, firstPage, bBoxMap.get(firstPage), treeIdString, maxChildDepth, ownDepth);
|
||||
if (section instanceof SuperSection) {
|
||||
return;
|
||||
}
|
||||
for (Page middlePage : pagesInOrder.subList(0, pagesInOrder.size() - 1)) {
|
||||
handleForMiddlePageOfSection(section, middlePage, bBoxMap.get(middlePage), treeIdString, maxChildDepth, ownDepth);
|
||||
}
|
||||
var lastPage = pagesInOrder.remove(pagesInOrder.size() - 1);
|
||||
handleLastPageOfSection(section, lastPage, bBoxMap.get(lastPage), treeIdString, maxChildDepth, ownDepth);
|
||||
}
|
||||
|
||||
|
||||
private String buildTreeIdString(SemanticNode semanticNode) {
|
||||
|
||||
return semanticNode.getTreeId()
|
||||
.stream()
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.joining("."));
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private void addPlacedText(Page page, Rectangle2D textBBox, Rectangle2D highestParentRect, String s, Integer maxChildDepth, Visualizations visualizations, Color color) {
|
||||
|
||||
// translates text, such that its right edge is a bit to the left of the drawn box
|
||||
float translationAmount = ((FONT.getStringWidth(s) / 1000) * FONT_SIZE + LINE_WIDTH + 2 * maxChildDepth);
|
||||
|
||||
Point2D upperLeftCorner;
|
||||
Point2D translationVector;
|
||||
switch (page.getRotation()) {
|
||||
case 90 -> {
|
||||
if (highestParentRect != null) {
|
||||
upperLeftCorner = new Point2D.Double(highestParentRect.getMinX(), textBBox.getMinY());
|
||||
} else {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMinY());
|
||||
}
|
||||
translationVector = new Point2D.Double(FONT_SIZE, -translationAmount);
|
||||
}
|
||||
case 180 -> {
|
||||
if (highestParentRect != null) {
|
||||
upperLeftCorner = new Point2D.Double(highestParentRect.getMaxX(), textBBox.getMinY());
|
||||
} else {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMaxX(), textBBox.getMinY());
|
||||
}
|
||||
translationVector = new Point2D.Double(translationAmount, FONT_SIZE);
|
||||
}
|
||||
case 270 -> {
|
||||
|
||||
if (highestParentRect != null) {
|
||||
upperLeftCorner = new Point2D.Double(highestParentRect.getMaxX(), textBBox.getMaxY());
|
||||
} else {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMaxX(), textBBox.getMaxY());
|
||||
}
|
||||
translationVector = new Point2D.Double(-FONT_SIZE, translationAmount);
|
||||
}
|
||||
default -> {
|
||||
|
||||
if (highestParentRect != null) {
|
||||
upperLeftCorner = new Point2D.Double(highestParentRect.getMinX(), textBBox.getMaxY());
|
||||
} else {
|
||||
upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMaxY());
|
||||
}
|
||||
translationVector = new Point2D.Double(-translationAmount, -FONT_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
upperLeftCorner = add(upperLeftCorner, translationVector);
|
||||
|
||||
List<PlacedText> placedTexts = getOrCreateVisualizationsOnPage(page.getNumber(), visualizations).getPlacedTexts();
|
||||
|
||||
PlacedText newText = PlacedText.textFacingUp(s, upperLeftCorner, FONT_SIZE, color, FONT);
|
||||
|
||||
Optional<PlacedText> conflictingText = placedTexts.stream()
|
||||
.filter(pt -> Math.abs(pt.lineStart().getY() - newText.lineStart().getY()) <= FONT_SIZE)
|
||||
.findFirst();
|
||||
|
||||
if (conflictingText.isPresent()) {
|
||||
PlacedText existingText = conflictingText.get();
|
||||
if (newText.text().length() > existingText.text().length()) {
|
||||
placedTexts.remove(existingText);
|
||||
placedTexts.add(newText);
|
||||
}
|
||||
} else {
|
||||
placedTexts.add(newText);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void handleSinglePage(SemanticNode semanticNode, Page page, Rectangle2D rectangle2D, String treeIdString, Integer maxChildDepth, Integer ownDepth) {
|
||||
|
||||
RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, page, rectangle2D, treeIdString, maxChildDepth, ownDepth);
|
||||
// add string to top line
|
||||
var firstLine = result.pageLines().remove(0);
|
||||
result.coloredLines().add(new ColoredLine(firstLine, SECTION_COLOR, LINE_WIDTH));
|
||||
for (Line2D line : result.pageLines()) {
|
||||
result.coloredLines().add(new ColoredLine(line, SECTION_COLOR, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void handleFirstPageOfSection(SemanticNode semanticNode, Page firstPage, Rectangle2D rectangle2D, String treeIdString, Integer maxChildDepth, Integer ownDepth) {
|
||||
|
||||
RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, firstPage, rectangle2D, treeIdString, maxChildDepth, ownDepth);
|
||||
// remove bottom line
|
||||
result.pageLines().remove(2);
|
||||
// add string to top line
|
||||
var firstLine = result.pageLines().remove(0);
|
||||
result.coloredLines().add(new ColoredLine(firstLine, SECTION_COLOR, LINE_WIDTH));
|
||||
for (Line2D line : result.pageLines()) {
|
||||
result.coloredLines().add(new ColoredLine(line, SECTION_COLOR, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void handleForMiddlePageOfSection(SemanticNode semanticNode, Page middlePage, Rectangle2D rectangle2D, String treeIdString, Integer maxChildDepth, Integer ownDepth) {
|
||||
|
||||
RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, middlePage, rectangle2D, treeIdString, maxChildDepth, ownDepth);
|
||||
// remove top line
|
||||
result.pageLines().remove(0);
|
||||
// remove bottom line
|
||||
result.pageLines().remove(1);
|
||||
// add string to left line
|
||||
var leftLine = result.pageLines().remove(1);
|
||||
result.coloredLines().add(new ColoredLine(leftLine, SECTION_COLOR, LINE_WIDTH));
|
||||
for (Line2D line : result.pageLines()) {
|
||||
result.coloredLines().add(new ColoredLine(line, SECTION_COLOR, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void handleLastPageOfSection(SemanticNode semanticNode, Page lastPage, Rectangle2D rectangle2D, String treeIdString, Integer maxChildDepth, Integer ownDepth) {
|
||||
|
||||
RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, lastPage, rectangle2D, treeIdString, maxChildDepth, ownDepth);
|
||||
// remove top line
|
||||
result.pageLines().remove(0);
|
||||
// add string to left line
|
||||
var leftLine = result.pageLines().remove(2);
|
||||
result.coloredLines().add(new ColoredLine(leftLine, SECTION_COLOR, LINE_WIDTH));
|
||||
for (Line2D line : result.pageLines()) {
|
||||
result.coloredLines().add(new ColoredLine(line, SECTION_COLOR, LINE_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private RectangleAndLinesResult createLinesAndPlaceText(SemanticNode semanticNode,
|
||||
Page page,
|
||||
Rectangle2D rectangle2D,
|
||||
String treeIdString,
|
||||
Integer maxChildDepth,
|
||||
Integer ownDepth) {
|
||||
|
||||
List<ColoredLine> coloredLines = getOrCreateVisualizationsOnPage(page.getNumber(), sections).getColoredLines();
|
||||
int lineWidthModifier = maxChildDepth - ownDepth;
|
||||
Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox().get(page), LINE_WIDTH * (1 + lineWidthModifier), LINE_WIDTH * (1 + lineWidthModifier));
|
||||
|
||||
SemanticNode highestParent = semanticNode.getHighestParent();
|
||||
Rectangle2D highestParentRect = rectangleMap.get(new RectangleIdentifier(highestParent.getTreeId(), page.getNumber()));
|
||||
addPlacedText(page, rectangle2D, highestParentRect, treeIdString, maxChildDepth, sections, SECTION_COLOR);
|
||||
var lastPageLines = createLinesFromRectangle(r, page.getRotation());
|
||||
|
||||
if (semanticNode instanceof SuperSection) {
|
||||
rectangleMap.put(new RectangleIdentifier(semanticNode.getTreeId(), page.getNumber()), r);
|
||||
}
|
||||
|
||||
return new RectangleAndLinesResult(coloredLines, r, lastPageLines);
|
||||
}
|
||||
|
||||
|
||||
private void addInnerTableLines(Table table) {
|
||||
|
||||
if (table.getNumberOfCols() < 1 || table.getNumberOfRows() < 1) {
|
||||
return;
|
||||
}
|
||||
for (Page page : table.getPages()) {
|
||||
|
||||
Optional<Integer> optionalFirstRowOnPage = table.streamCol(0)
|
||||
.filter(tableCell -> tableCell.isOnPage(page.getNumber()))
|
||||
.map(TableCell::getRow)
|
||||
.findFirst();
|
||||
if (optionalFirstRowOnPage.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int firstRowOnPage = optionalFirstRowOnPage.get();
|
||||
|
||||
Stream<Double> xStream = switch (page.getRotation()) {
|
||||
case 90 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMinX);
|
||||
case 180 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMaxX);
|
||||
case 270 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMaxX);
|
||||
default -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMinX);
|
||||
};
|
||||
List<Double> xs = xStream.collect(Collectors.toList());
|
||||
xs.remove(0);
|
||||
|
||||
Stream<Double> yStream = switch (page.getRotation()) {
|
||||
case 90 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMinY);
|
||||
case 180 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMinY);
|
||||
case 270 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMaxY);
|
||||
default -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMaxY);
|
||||
};
|
||||
List<Double> ys = yStream.collect(Collectors.toList());
|
||||
ys.remove(0);
|
||||
|
||||
Rectangle2D tableBBox = table.getBBox().get(page);
|
||||
List<ColoredLine> coloredLines = getOrCreateVisualizationsOnPage(page.getNumber(), tables).getColoredLines();
|
||||
|
||||
xs.forEach(x -> {
|
||||
Line2D line = new Line2D.Double(new Point2D.Double(x, tableBBox.getMaxY()), new Point2D.Double(x, tableBBox.getMinY()));
|
||||
coloredLines.add(new ColoredLine(line, INNER_LINES_COLOR, LINE_WIDTH));
|
||||
});
|
||||
ys.forEach(y -> {
|
||||
Line2D line = new Line2D.Double(new Point2D.Double(tableBBox.getMinX(), y), new Point2D.Double(tableBBox.getMaxX(), y));
|
||||
coloredLines.add(new ColoredLine(line, INNER_LINES_COLOR, LINE_WIDTH));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Stream<Rectangle2D> streamBBoxOfCellsOnPage(Stream<TableCell> table, Page page) {
|
||||
|
||||
return table.filter(tableCell -> tableCell.isOnPage(page.getNumber()))
|
||||
.map(TableCell::getBBox)
|
||||
.map(bBoxMap -> bBoxMap.get(page));
|
||||
}
|
||||
|
||||
|
||||
private void addAsRectangle(SemanticNode semanticNode, Visualizations visualizations, Color color) {
|
||||
|
||||
addAsRectangle(semanticNode.getBBox(), visualizations, color);
|
||||
}
|
||||
|
||||
|
||||
private void addAsRectangle(Map<Page, Rectangle2D> bbox, Visualizations visualizations, Color color) {
|
||||
|
||||
bbox.forEach((page, textBBox) -> getOrCreateVisualizationsOnPage(page.getNumber(), visualizations).getColoredRectangles()
|
||||
.add(new ColoredRectangle(textBBox, color, LINE_WIDTH)));
|
||||
}
|
||||
|
||||
|
||||
private record RectangleAndLinesResult(List<ColoredLine> coloredLines, Rectangle2D rectangle, List<Line2D> pageLines) {
|
||||
|
||||
}
|
||||
|
||||
private record RectangleIdentifier(List<Integer> treeId, Integer pageNumber) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
A __________________ B
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
D|__________________| C
|
||||
The returned List are the lines [AB, BC, DC, AD]
|
||||
The List is reordered, such that the order of the returned lines are always as viewed on the page.
|
||||
*/
|
||||
private List<Line2D> createLinesFromRectangle(Rectangle2D r, int pageRotation) {
|
||||
// +0.5 to join the lines
|
||||
List<Line2D> lines = new ArrayList<>(4);
|
||||
float lineWidthCorrection = LINE_WIDTH * 0.5f;
|
||||
Point2D.Float a = new Point2D.Float((float) r.getMinX(), (float) r.getMaxY());
|
||||
Point2D.Float a1 = new Point2D.Float((float) r.getMinX() - lineWidthCorrection, (float) r.getMaxY());
|
||||
Point2D.Float b = new Point2D.Float((float) r.getMaxX(), (float) r.getMaxY());
|
||||
Point2D.Float b1 = new Point2D.Float((float) r.getMaxX() + lineWidthCorrection, (float) r.getMaxY());
|
||||
Point2D.Float c = new Point2D.Float((float) r.getMaxX(), (float) r.getMinY());
|
||||
Point2D.Float c1 = new Point2D.Float((float) r.getMaxX() + lineWidthCorrection, (float) r.getMinY());
|
||||
Point2D.Float d = new Point2D.Float((float) r.getMinX(), (float) r.getMinY());
|
||||
Point2D.Float d1 = new Point2D.Float((float) r.getMinX() - lineWidthCorrection, (float) r.getMinY());
|
||||
lines.add(new Line2D.Float(a1, b1));
|
||||
lines.add(new Line2D.Float(b, c));
|
||||
lines.add(new Line2D.Float(d1, c1));
|
||||
lines.add(new Line2D.Float(a, d));
|
||||
|
||||
return switch (pageRotation) {
|
||||
case 90 -> {
|
||||
Collections.rotate(lines, 1);
|
||||
yield lines;
|
||||
}
|
||||
case 180 -> {
|
||||
Collections.rotate(lines, 2);
|
||||
yield lines;
|
||||
}
|
||||
case 270 -> {
|
||||
Collections.rotate(lines, 3);
|
||||
yield lines;
|
||||
}
|
||||
|
||||
default -> lines;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private Point2D add(Point2D a, Point2D b) {
|
||||
|
||||
return new Point2D.Double(a.getX() + b.getX(), a.getY() + b.getY());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,11 +32,13 @@ dependencies {
|
||||
implementation("com.iqser.red.commons:storage-commons:2.45.0")
|
||||
implementation("com.knecon.fforesight:tenant-commons:0.21.0")
|
||||
implementation("com.knecon.fforesight:tracing-commons:0.5.0")
|
||||
implementation("com.knecon.fforesight:lifecycle-commons:0.6.0")
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator:${springBootStarterVersion}")
|
||||
implementation("org.springframework.boot:spring-boot-starter-amqp:${springBootStarterVersion}")
|
||||
implementation("com.amazonaws:aws-java-sdk-s3:1.12.536")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.0.4")
|
||||
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
|
||||
implementation("com.pdftron:PDFNet:10.7.0")
|
||||
|
||||
// for integration testing only
|
||||
testImplementation(project(":viewer-doc-processor"))
|
||||
|
||||
+4
-1
@@ -6,19 +6,22 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import com.amazonaws.services.s3.model.metrics.MetricsConfiguration;
|
||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||
import com.knecon.fforesight.lifecyclecommons.LifecycleAutoconfiguration;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutParsingServiceProcessorConfiguration;
|
||||
import com.knecon.fforesight.tenantcommons.MultiTenancyAutoConfiguration;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.aop.ObservedAspect;
|
||||
|
||||
@ImportAutoConfiguration({MultiTenancyAutoConfiguration.class})
|
||||
@ImportAutoConfiguration({MultiTenancyAutoConfiguration.class, LifecycleAutoconfiguration.class})
|
||||
@Import({MetricsConfiguration.class, StorageAutoConfiguration.class, LayoutParsingServiceProcessorConfiguration.class})
|
||||
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class})
|
||||
@EnableAspectJAutoProxy
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.knecon.fforesight.service.layoutparser.server;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutparserSettings;
|
||||
import com.pdftron.pdf.PDFNet;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PDFNetInitializer {
|
||||
|
||||
@Value("${pdftron.license:}")
|
||||
private String pdftronLicense;
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@PostConstruct
|
||||
// Do not change back to application runner, if it is application runner it takes messages from the queue before PDFNet is initialized, that leads to UnsatisfiedLinkError.
|
||||
public void init() {
|
||||
|
||||
if (Strings.isNullOrEmpty(pdftronLicense)) {
|
||||
return;
|
||||
}
|
||||
log.info("Initializing Native Libraries");
|
||||
log.info("Setting pdftron license: {}", pdftronLicense);
|
||||
PDFNet.setTempPath("/tmp/pdftron");
|
||||
PDFNet.initialize(pdftronLicense);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@PreDestroy
|
||||
public void terminate() {
|
||||
|
||||
PDFNet.terminate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,9 @@ project.version: 1.0-SNAPSHOT
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
lifecycle:
|
||||
base-package: com.knecon.fforesight.service.layoutparser
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: layoutparser-service
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ public class BdrJsonBuildTest extends AbstractTest {
|
||||
protected Document buildGraph(File file) {
|
||||
|
||||
return DocumentGraphFactory.buildDocumentGraph(LayoutParsingType.CLARIFYND,
|
||||
layoutParsingPipeline.parseLayout(LayoutParsingType.CLARIFYND,
|
||||
layoutParsingPipeline.parseLayout(LayoutParsingType.CLARIFYND,
|
||||
file,
|
||||
new ImageServiceResponse(),
|
||||
new TableServiceResponse(),
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ public class HeadlinesGoldStandardIntegrationTest {
|
||||
goldStandardLog.getRedactionLogEntry().forEach(e -> goldStandardHeadlines.add(new Headline(e.getPositions().get(0).getPage(), e.getValue())));
|
||||
|
||||
Document documentGraph = DocumentGraphFactory.buildDocumentGraph(LayoutParsingType.DOCUMINE,
|
||||
layoutParsingPipeline.parseLayout(LayoutParsingType.DOCUMINE,
|
||||
layoutParsingPipeline.parseLayout(LayoutParsingType.DOCUMINE,
|
||||
pdfFileResource.getFile(),
|
||||
new ImageServiceResponse(),
|
||||
new TableServiceResponse(),
|
||||
|
||||
+8
-3
@@ -30,11 +30,11 @@ public class LayoutparserEnd2EndTest extends AbstractTest {
|
||||
@Autowired
|
||||
private LayoutParsingPipeline layoutParsingPipeline;
|
||||
|
||||
@Disabled
|
||||
@Test
|
||||
@Disabled
|
||||
public void testLayoutParserEndToEnd() {
|
||||
|
||||
String filePath = "files/Minimal Examples/RotateTextWithRulingsTestFile.pdf";
|
||||
String filePath = "files/syngenta/CustomerFiles/Documine/Flora/425_F.1.1.1 - A13617AV - Acute Oral Toxicity Study.pdf";
|
||||
|
||||
runForFile(filePath);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class LayoutparserEnd2EndTest extends AbstractTest {
|
||||
@SneakyThrows
|
||||
public void testLayoutParserEndToEndWithFolder() {
|
||||
|
||||
String folder = "/home/kschuettler/Dokumente/TestFiles/large number of prod files";
|
||||
String folder = "/Users/maverickstuder/Documents/Fforesight/layoutparser/layoutparser-service/layoutparser-service-server/src/test";
|
||||
List<Path> pdfFiles = Files.walk(Path.of(folder))
|
||||
.filter(path -> path.getFileName().toString().endsWith(".pdf"))
|
||||
.sorted(Comparator.comparing(Path::getFileName))
|
||||
@@ -82,6 +82,11 @@ public class LayoutparserEnd2EndTest extends AbstractTest {
|
||||
assert tmpFile.getParentFile().exists() || tmpFile.getParentFile().mkdirs();
|
||||
|
||||
storageService.downloadTo(TENANT_ID, layoutParsingRequest.viewerDocumentStorageId(), tmpFile);
|
||||
|
||||
tmpFile = new File("/tmp/layoutparserEND2END/" + fileName + "_MARKDOWN.md");
|
||||
assert tmpFile.getParentFile().exists() || tmpFile.getParentFile().mkdirs();
|
||||
|
||||
storageService.downloadTo(TENANT_ID, layoutParsingRequest.documentMarkdownFileStorageId().get(), tmpFile);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+36
-52
@@ -12,6 +12,7 @@ import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
@@ -32,18 +33,29 @@ import com.knecon.fforesight.service.layoutparser.processor.python_api.model.tab
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.visualization.LayoutGridService;
|
||||
import com.knecon.fforesight.service.layoutparser.server.utils.AbstractTest;
|
||||
import com.knecon.fforesight.service.viewerdoc.service.ViewerDocumentService;
|
||||
import com.knecon.fforesight.service.viewerdoc.service.PDFTronViewerDocumentService;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
public class OutlineDetectionTest extends AbstractTest {
|
||||
|
||||
ViewerDocumentService viewerDocumentService = new ViewerDocumentService(null);
|
||||
PDFTronViewerDocumentService viewerDocumentService = new PDFTronViewerDocumentService(null);
|
||||
LayoutGridService layoutGridService = new LayoutGridService(viewerDocumentService);
|
||||
|
||||
@Autowired
|
||||
protected LayoutParsingPipeline layoutParsingPipeline;
|
||||
|
||||
@Autowired
|
||||
PDFNetInitializer pdfNetInitializer;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
|
||||
pdfNetInitializer.init();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
@@ -60,28 +72,17 @@ public class OutlineDetectionTest extends AbstractTest {
|
||||
|
||||
OutlineObjectTree outlineObjectTree = classificationDocument.getOutlineObjectTree();
|
||||
assertEquals(outlineObjectTree.getRootNodes().size(), 8);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(1).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(3).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(4).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(5).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(6).size(), 2);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(7).size(), 3);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(8).size(), 2);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(10).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(11).size(), 4);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(12).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage()
|
||||
.get(13).size(), 2);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(1).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(3).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(4).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(5).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(6).size(), 2);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(7).size(), 3);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(8).size(), 2);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(10).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(11).size(), 4);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(12).size(), 1);
|
||||
assertEquals(outlineObjectTree.getOutlineObjectsPerPage().get(13).size(), 2);
|
||||
assertTrue(outlineObjectTree.getOutlineObjectsPerPage().values()
|
||||
.stream()
|
||||
.flatMap(Collection::stream)
|
||||
@@ -98,29 +99,15 @@ public class OutlineDetectionTest extends AbstractTest {
|
||||
.stream()
|
||||
.map(outlineObjectTreeNode -> sanitizeString(outlineObjectTreeNode.getOutlineObject().getTitle()))
|
||||
.toList());
|
||||
assertEquals(tableOfContents.getMainSections()
|
||||
.get(5).getChildren().size(), 6);
|
||||
assertEquals(tableOfContents.getMainSections()
|
||||
.get(7).getChildren().size(), 3);
|
||||
assertEquals(tableOfContents.getMainSections()
|
||||
.get(8).getChildren().size(), 3);
|
||||
assertEquals(tableOfContents.getMainSections()
|
||||
.get(8).getChildren()
|
||||
.get(2).getChildren().size(), 1);
|
||||
assertEquals(tableOfContents.getMainSections()
|
||||
.get(8).getChildren()
|
||||
.get(2).getChildren()
|
||||
.get(0).getChildren().size(), 3);
|
||||
assertEquals(tableOfContents.getMainSections().get(5).getChildren().size(), 6);
|
||||
assertEquals(tableOfContents.getMainSections().get(7).getChildren().size(), 3);
|
||||
assertEquals(tableOfContents.getMainSections().get(8).getChildren().size(), 3);
|
||||
assertEquals(tableOfContents.getMainSections().get(8).getChildren().get(2).getChildren().size(), 1);
|
||||
assertEquals(tableOfContents.getMainSections().get(8).getChildren().get(2).getChildren().get(0).getChildren().size(), 3);
|
||||
|
||||
assertEquals(tableOfContents.getMainSections()
|
||||
.get(0).getImages().size(), 1);
|
||||
assertEquals(tableOfContents.getMainSections()
|
||||
.get(6).getImages().size(), 1);
|
||||
assertEquals(tableOfContents.getMainSections()
|
||||
.get(8).getChildren()
|
||||
.get(2).getChildren()
|
||||
.get(0).getChildren()
|
||||
.get(2).getImages().size(), 1);
|
||||
assertEquals(tableOfContents.getMainSections().get(0).getImages().size(), 1);
|
||||
assertEquals(tableOfContents.getMainSections().get(6).getImages().size(), 1);
|
||||
assertEquals(tableOfContents.getMainSections().get(8).getChildren().get(2).getChildren().get(0).getChildren().get(2).getImages().size(), 1);
|
||||
|
||||
Document document = buildGraph(fileName, classificationDocument);
|
||||
|
||||
@@ -159,17 +146,14 @@ public class OutlineDetectionTest extends AbstractTest {
|
||||
.count(), 3 + 1);
|
||||
assertEquals(childrenOfTypeSectionOrSuperSection.get(8).streamChildren()
|
||||
.filter(isSectionOrSuperSection)
|
||||
.toList()
|
||||
.get(3).streamChildren()
|
||||
.toList().get(3).streamChildren()
|
||||
.filter(isSectionOrSuperSection)
|
||||
.count(), 1 + 1);
|
||||
assertEquals(childrenOfTypeSectionOrSuperSection.get(8).streamChildren()
|
||||
.filter(isSectionOrSuperSection)
|
||||
.toList()
|
||||
.get(3).streamChildren()
|
||||
.toList().get(3).streamChildren()
|
||||
.filter(isSectionOrSuperSection)
|
||||
.toList()
|
||||
.get(1).streamChildren()
|
||||
.toList().get(1).streamChildren()
|
||||
.filter(isSectionOrSuperSection)
|
||||
.count(), 3 + 1);
|
||||
|
||||
|
||||
-8
@@ -3,11 +3,8 @@ package com.knecon.fforesight.service.layoutparser.server;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -16,12 +13,8 @@ import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.SimplifiedSectionText;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.SimplifiedText;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingFinishedEvent;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingRequest;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutParsingPipeline;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.LayoutParsingStorageService;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.image.ImageServiceResponse;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.TableServiceResponse;
|
||||
@@ -30,7 +23,6 @@ import com.knecon.fforesight.service.layoutparser.processor.services.SimplifiedS
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory;
|
||||
import com.knecon.fforesight.service.layoutparser.server.utils.AbstractTest;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
package com.knecon.fforesight.service.layoutparser.server.graph;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
|
||||
|
||||
class BoundaryTest {
|
||||
|
||||
Boundary startBoundary;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
|
||||
startBoundary = new Boundary(10, 100);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testContains() {
|
||||
|
||||
assertTrue(startBoundary.contains(11));
|
||||
assertTrue(startBoundary.contains(50));
|
||||
assertFalse(startBoundary.contains(9));
|
||||
assertFalse(startBoundary.contains(100));
|
||||
assertFalse(startBoundary.contains(150));
|
||||
assertFalse(startBoundary.contains(-123));
|
||||
assertTrue(startBoundary.contains(new Boundary(11, 99)));
|
||||
assertTrue(startBoundary.contains(new Boundary(10, 100)));
|
||||
assertTrue(startBoundary.contains(new Boundary(11, 11)));
|
||||
assertFalse(startBoundary.contains(9, 100));
|
||||
assertTrue(startBoundary.contains(100, 100));
|
||||
assertFalse(startBoundary.contains(100, 101));
|
||||
assertFalse(startBoundary.contains(150, 151));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testIntersects() {
|
||||
|
||||
assertTrue(startBoundary.intersects(new Boundary(1, 11)));
|
||||
assertTrue(startBoundary.intersects(new Boundary(11, 12)));
|
||||
assertTrue(startBoundary.intersects(new Boundary(11, 100)));
|
||||
assertFalse(startBoundary.intersects(new Boundary(100, 101)));
|
||||
assertTrue(startBoundary.intersects(new Boundary(99, 101)));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testSplit() {
|
||||
|
||||
assertEquals(4, startBoundary.split(List.of(12, 40, 90)).size());
|
||||
assertEquals(List.of(new Boundary(10, 12), new Boundary(12, 40), new Boundary(40, 90), new Boundary(90, 100)), startBoundary.split(List.of(12, 40, 90)));
|
||||
assertEquals(List.of(new Boundary(10, 40), new Boundary(40, 100)), startBoundary.split(List.of(40)));
|
||||
assertEquals(1, startBoundary.split(Collections.emptyList()).size());
|
||||
assertEquals(1, startBoundary.split(List.of(startBoundary.start())).size());
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> startBoundary.split(Collections.singletonList(0)));
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> startBoundary.split(Collections.singletonList(100)));
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> startBoundary.split(List.of(12, 40, 100)));
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@ public class DocumentGraphJsonWritingTest extends BuildDocumentTest {
|
||||
private void writeJsons(Path filename) {
|
||||
|
||||
Document documentGraph = DocumentGraphFactory.buildDocumentGraph(LayoutParsingType.REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH,
|
||||
layoutParsingPipeline.parseLayout(LayoutParsingType.REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH,
|
||||
layoutParsingPipeline.parseLayout(LayoutParsingType.REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH,
|
||||
filename.toFile(),
|
||||
new ImageServiceResponse(),
|
||||
new TableServiceResponse(),
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.knecon.fforesight.service.layoutparser.server.graph;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
|
||||
|
||||
class TextRangeTest {
|
||||
|
||||
TextRange startTextRange;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
|
||||
startTextRange = new TextRange(10, 100);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testContains() {
|
||||
|
||||
assertTrue(startTextRange.contains(11));
|
||||
assertTrue(startTextRange.contains(50));
|
||||
assertFalse(startTextRange.contains(9));
|
||||
assertTrue(startTextRange.contains(100));
|
||||
assertFalse(startTextRange.contains(101));
|
||||
assertFalse(startTextRange.contains(150));
|
||||
assertFalse(startTextRange.contains(-123));
|
||||
assertTrue(startTextRange.contains(new TextRange(11, 99)));
|
||||
assertTrue(startTextRange.contains(new TextRange(10, 100)));
|
||||
assertTrue(startTextRange.contains(new TextRange(11, 11)));
|
||||
assertFalse(startTextRange.contains(9, 100));
|
||||
assertTrue(startTextRange.contains(100, 100));
|
||||
assertFalse(startTextRange.contains(100, 101));
|
||||
assertFalse(startTextRange.contains(150, 151));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testContainsExclusive() {
|
||||
|
||||
assertTrue(startTextRange.containsExclusive(11));
|
||||
assertTrue(startTextRange.containsExclusive(50));
|
||||
assertFalse(startTextRange.containsExclusive(9));
|
||||
assertFalse(startTextRange.containsExclusive(100));
|
||||
assertFalse(startTextRange.containsExclusive(150));
|
||||
assertFalse(startTextRange.containsExclusive(-123));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testIntersects() {
|
||||
|
||||
assertTrue(startTextRange.intersects(new TextRange(1, 11)));
|
||||
assertTrue(startTextRange.intersects(new TextRange(11, 12)));
|
||||
assertTrue(startTextRange.intersects(new TextRange(11, 100)));
|
||||
assertFalse(startTextRange.intersects(new TextRange(100, 101)));
|
||||
assertTrue(startTextRange.intersects(new TextRange(99, 101)));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testSplit() {
|
||||
|
||||
assertEquals(4, startTextRange.split(List.of(12, 40, 90)).size());
|
||||
assertEquals(List.of(new TextRange(10, 12), new TextRange(12, 40), new TextRange(40, 90), new TextRange(90, 100)), startTextRange.split(List.of(12, 40, 90)));
|
||||
assertEquals(List.of(new TextRange(10, 40), new TextRange(40, 100)), startTextRange.split(List.of(40)));
|
||||
assertEquals(1, startTextRange.split(Collections.emptyList()).size());
|
||||
assertEquals(1, startTextRange.split(List.of(startTextRange.start())).size());
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> startTextRange.split(Collections.singletonList(0)));
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> startTextRange.split(Collections.singletonList(100)));
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> startTextRange.split(List.of(12, 40, 100)));
|
||||
}
|
||||
|
||||
}
|
||||
+23
-3
@@ -4,11 +4,18 @@ import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import com.iqser.red.commons.jackson.ObjectMapperFactory;
|
||||
import com.iqser.red.storage.commons.service.StorageService;
|
||||
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.image.ImageServiceResponse;
|
||||
@@ -16,17 +23,30 @@ import com.knecon.fforesight.service.layoutparser.processor.python_api.model.tab
|
||||
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.VisualLayoutParsingResponse;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory;
|
||||
import com.knecon.fforesight.service.layoutparser.processor.services.visualization.LayoutGridService;
|
||||
import com.knecon.fforesight.service.layoutparser.server.PDFNetInitializer;
|
||||
import com.knecon.fforesight.service.layoutparser.server.utils.BuildDocumentTest;
|
||||
import com.knecon.fforesight.service.viewerdoc.service.ViewerDocumentService;
|
||||
import com.knecon.fforesight.service.viewerdoc.service.PDFTronViewerDocumentService;
|
||||
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
||||
import com.pdftron.pdf.PDFNet;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
public class ViewerDocumentTest extends BuildDocumentTest {
|
||||
|
||||
ViewerDocumentService viewerDocumentService = new ViewerDocumentService(null);
|
||||
@Autowired
|
||||
PDFNetInitializer pdfNetInitializer;
|
||||
PDFTronViewerDocumentService viewerDocumentService = new PDFTronViewerDocumentService(null);
|
||||
LayoutGridService layoutGridService = new LayoutGridService(viewerDocumentService);
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
|
||||
pdfNetInitializer.init();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void testViewerDocument() {
|
||||
@@ -63,7 +83,7 @@ public class ViewerDocumentTest extends BuildDocumentTest {
|
||||
tableResponse,
|
||||
new VisualLayoutParsingResponse(),
|
||||
Map.of("file", Path.of(fileName).getFileName().toFile().toString()));
|
||||
ViewerDocumentService viewerDocumentService = new ViewerDocumentService(null);
|
||||
PDFTronViewerDocumentService viewerDocumentService = new PDFTronViewerDocumentService(null);
|
||||
LayoutGridService layoutGridService = new LayoutGridService(viewerDocumentService);
|
||||
Document document = DocumentGraphFactory.buildDocumentGraph(LayoutParsingType.DOCUMINE_OLD, classificationDocument);
|
||||
|
||||
|
||||
+2
-1
@@ -190,7 +190,8 @@ public class PdfSegmentationServiceTest extends AbstractTest {
|
||||
imageMetadata.getGeometry().getHeight()),
|
||||
ImageType.valueOf(imageMetadata.getClassification().getLabel().toUpperCase(Locale.ROOT)),
|
||||
imageMetadata.isAlpha(),
|
||||
imageMetadata.getPosition().getPageNumber())));
|
||||
imageMetadata.getPosition().getPageNumber(),
|
||||
"")));
|
||||
|
||||
System.out.println("object");
|
||||
}
|
||||
|
||||
+3
-1
@@ -62,6 +62,7 @@ public abstract class AbstractTest {
|
||||
protected final static String TEXT_FILE_ID = "texts";
|
||||
protected final static String POSITION_FILE_ID = "positions";
|
||||
protected final static String PAGES_FILE_ID = "pages";
|
||||
protected final static String MARKDOWN_FILE_ID = "markdown";
|
||||
protected final static String TENANT_ID = "tenant";
|
||||
protected final static String VIEWER_DOCUMENT_ID = "viewer";
|
||||
protected final static String SIMPLIFIED_ID = "simplified";
|
||||
@@ -105,7 +106,7 @@ public abstract class AbstractTest {
|
||||
}
|
||||
|
||||
|
||||
protected LayoutParsingRequest buildDefaultLayoutParsingRequest(String fileName, LayoutParsingType layoutParsingType, boolean debug) {
|
||||
public static LayoutParsingRequest buildDefaultLayoutParsingRequest(String fileName, LayoutParsingType layoutParsingType, boolean debug) {
|
||||
|
||||
var identifier = debug ? Map.of("fileId", fileName, "debug", "true") : Map.of("fileId", fileName);
|
||||
return LayoutParsingRequest.builder()
|
||||
@@ -121,6 +122,7 @@ public abstract class AbstractTest {
|
||||
.pageFileStorageId(fileName + PAGES_FILE_ID)
|
||||
.simplifiedTextStorageId(fileName + SIMPLIFIED_ID)
|
||||
.viewerDocumentStorageId(fileName + VIEWER_DOCUMENT_ID)
|
||||
.documentMarkdownFileStorageId(Optional.of(fileName + MARKDOWN_FILE_ID))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -9,3 +9,5 @@ storage:
|
||||
key: minioadmin
|
||||
secret: minioadmin
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@ spring:
|
||||
max-interval: 15000
|
||||
prefetch: 1
|
||||
|
||||
layoutparser:
|
||||
debug: true
|
||||
|
||||
pdftron.license: demo:1650351709282:7bd235e003000000004ec28a6743e1163a085e2115de2536ab6e2cfe5a
|
||||
|
||||
management:
|
||||
endpoint:
|
||||
metrics.enabled: ${monitoring.enabled:false}
|
||||
|
||||
BIN
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04f3fc00d7e0851c6ee0663ce749562234cc95123ffdd643df88d621e4323ede
|
||||
size 238546
|
||||
@@ -12,6 +12,7 @@ dependencies {
|
||||
implementation("org.apache.pdfbox:pdfbox-tools:${pdfBoxVersion}")
|
||||
implementation("org.slf4j:slf4j-api:1.7.25")
|
||||
implementation("com.knecon.fforesight:tracing-commons:0.5.0")
|
||||
implementation("com.pdftron:PDFNet:10.7.0")
|
||||
|
||||
testImplementation("org.apache.logging.log4j:log4j-slf4j-impl:2.22.1")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
package com.knecon.fforesight.service.viewerdoc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PUBLIC)
|
||||
public class ContentStreams {
|
||||
|
||||
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_TEXT_DEBUG = new Identifier("OCR Text", COSName.getPDFName("KNECON_OCR_TEXT_DEBUG"), true);
|
||||
|
||||
public static Identifier KNECON_OCR_BBOX_DEBUG = new Identifier("OCR Boxes", COSName.getPDFName("KNECON_OCR_BBOX_DEBUG"), true);
|
||||
|
||||
public static Identifier OTHER = new Identifier("other", COSName.getPDFName("OTHER"), false);
|
||||
|
||||
public static Identifier ESCAPE_START = new Identifier("escape start", COSName.getPDFName("ESCAPE_START"), false);
|
||||
|
||||
public static Identifier ESCAPE_END = new Identifier("escape start", COSName.getPDFName("ESCAPE_END"), false);
|
||||
|
||||
public static Identifier CLEAN_RULINGS = new Identifier("Cleaned Rulings", COSName.getPDFName("KNECON_CLEAN_RULINGS"), true);
|
||||
|
||||
public static Identifier RULINGS = new Identifier("Rulings", COSName.getPDFName("KNECON_RULINGS"), true);
|
||||
|
||||
public static Identifier WORDS = new Identifier("Words", COSName.getPDFName("KNECON_WORDS"), true);
|
||||
|
||||
public static Identifier ZONES = new Identifier("Text Zones", COSName.getPDFName("KNECON_ZONES"), true);
|
||||
|
||||
public static Identifier LINES = new Identifier("Text Lines", COSName.getPDFName("KNECON_LINES"), true);
|
||||
|
||||
public static Identifier CELLS = new Identifier("Cells", COSName.getPDFName("KNECON_CELLS"), true);
|
||||
|
||||
public static Identifier MAIN_BODY = new Identifier("Main Text Body", COSName.getPDFName("KNECON_MAIN_BODY"), true);
|
||||
|
||||
public static Identifier MARKED_CONTENT = new Identifier("Marked content", COSName.getPDFName("KNECON_MARKED_CONTENT"), true);
|
||||
|
||||
public static Identifier NEIGHBOURS = new Identifier("Neighbours", COSName.getPDFName("KNECON_NEIGHBOURS"), true);
|
||||
|
||||
public static Identifier CHARACTERS = new Identifier("Characters", COSName.getPDFName("KNECON_CHARACTERS"), true);
|
||||
|
||||
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,
|
||||
RULINGS,
|
||||
CLEAN_RULINGS,
|
||||
WORDS,
|
||||
ZONES,
|
||||
LINES,
|
||||
MAIN_BODY,
|
||||
MARKED_CONTENT,
|
||||
NEIGHBOURS,
|
||||
CHARACTERS,
|
||||
CELLS);
|
||||
|
||||
public record Identifier(String name, COSName cosName, boolean optionalContent) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.knecon.fforesight.service.viewerdoc;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
|
||||
/*
|
||||
These identifiers are used to mark content in the pdf, such that it may be found later. The markedContentName must therefore be unique.
|
||||
The String "name" is only used to display optional content in the optional content view in the pdf.
|
||||
Therefore, it may be null, if optionalContent is false.
|
||||
If optionalContent is false, the layer will not be created as a OCG, and will not be listed in the OCG view.
|
||||
*/
|
||||
public record LayerIdentifier(String name, String markedContentName) {
|
||||
|
||||
public String markedContentName() {
|
||||
// The prefix KNECON_ is used to identify marked contents as knecon contents later on
|
||||
return KNECON_IDENTIFIER_PREFIX + markedContentName;
|
||||
}
|
||||
|
||||
|
||||
public COSName cosName() {
|
||||
|
||||
return COSName.getPDFName(markedContentName);
|
||||
}
|
||||
|
||||
|
||||
public static final String KNECON_IDENTIFIER_PREFIX = "KNECON_";
|
||||
|
||||
public static final LayerIdentifier KNECON_OCR = new LayerIdentifier(null, "OCR");
|
||||
public static final LayerIdentifier KNECON_OCR_TEXT = new LayerIdentifier(null, "OCR_TEXT");
|
||||
public static final LayerIdentifier KNECON_OCR_LINES = new LayerIdentifier(null, "OCR_LINES");
|
||||
|
||||
// layers
|
||||
// layout grid
|
||||
public static final LayerIdentifier KNECON_LAYOUT = new LayerIdentifier("Layout grid", "LAYOUT");
|
||||
public static final LayerIdentifier KNECON_LAYOUT_SECTION = new LayerIdentifier("Section", "LAYOUT_SECTION");
|
||||
public static final LayerIdentifier KNECON_LAYOUT_PARAGRAPH = new LayerIdentifier("Paragraph ", "LAYOUT_PARAGRAPH");
|
||||
public static final LayerIdentifier KNECON_LAYOUT_KEY_VALUE = new LayerIdentifier("Key-Value Pairs ", "LAYOUT_KEY_VALUE");
|
||||
public static final LayerIdentifier KNECON_LAYOUT_HEADLINE = new LayerIdentifier("Headline", "LAYOUT_HEADLINE");
|
||||
public static final LayerIdentifier KNECON_LAYOUT_HEADER_FOOTER = new LayerIdentifier("Header/Footer", "LAYOUT_HEADER_FOOTER");
|
||||
public static final LayerIdentifier KNECON_LAYOUT_TABLE = new LayerIdentifier("Tables", "LAYOUT_TABLE");
|
||||
public static final LayerIdentifier KNECON_LAYOUT_FIGURES = new LayerIdentifier("Figures", "LAYOUT_FIGURES");
|
||||
public static final LayerIdentifier KNECON_LAYOUT_IMAGES = new LayerIdentifier("Images", "LAYOUT_IMAGES");
|
||||
public static final LayerIdentifier KNECON_LAYOUT_TREE_IDs = new LayerIdentifier("Tree IDs", "LAYOUT_TREE_IDs");
|
||||
|
||||
//layout grid debug
|
||||
public static final LayerIdentifier KNECON_LAYOUT_DEBUG = new LayerIdentifier("Layout elements", "DEBUG_LAYOUT");
|
||||
public static final LayerIdentifier CLEAN_RULINGS = new LayerIdentifier("Classified Rulings", "CLEAN_RULINGS");
|
||||
public static final LayerIdentifier RULINGS = new LayerIdentifier("Rulings", "RULINGS");
|
||||
public static final LayerIdentifier WORDS = new LayerIdentifier("Words", "WORDS");
|
||||
public static final LayerIdentifier ZONES = new LayerIdentifier("Text Zones", "ZONES");
|
||||
public static final LayerIdentifier LINES = new LayerIdentifier("Text Lines", "LINES");
|
||||
public static final LayerIdentifier CELLS = new LayerIdentifier("Cells", "CELLS");
|
||||
public static final LayerIdentifier MAIN_BODY = new LayerIdentifier("Main Text Body", "MAIN_BODY");
|
||||
public static final LayerIdentifier MARKED_CONTENT = new LayerIdentifier("Marked content", "MARKED_CONTENT");
|
||||
public static final LayerIdentifier NEIGHBOURS = new LayerIdentifier("Neighbours", "NEIGHBOURS");
|
||||
public static final LayerIdentifier CHARACTERS = new LayerIdentifier("Characters", "CHARACTERS");
|
||||
|
||||
public static final LayerIdentifier KNECON_VISUAL_PARSING = new LayerIdentifier("Visual Layout Parser", "VISUAL_PARSING");
|
||||
|
||||
//ocr
|
||||
public static final LayerIdentifier KNECON_OCR_DEBUG = new LayerIdentifier("OCR", "OCR_DEBUG");
|
||||
public static final LayerIdentifier KNECON_OCR_TEXT_DEBUG = new LayerIdentifier("OCR Text", "OCR_TEXT_DEBUG");
|
||||
public static final LayerIdentifier KNECON_OCR_BBOX_DEBUG = new LayerIdentifier("OCR Words", "OCR_BBOX_DEBUG");
|
||||
public static final LayerIdentifier KNECON_OCR_LINE_DEBUG = new LayerIdentifier("OCR Lines", "OCR_LINE_DEBUG");
|
||||
public static final LayerIdentifier KNECON_OCR_OVERLAPPED_TEXT = new LayerIdentifier("OCR overlapped Text", "OCR_OVERLAPPED_TEXT_DEBUG");
|
||||
|
||||
//azure idp
|
||||
public static final LayerIdentifier KNECON_AZURE_IDP = new LayerIdentifier("IDP", "IDP");
|
||||
public static final LayerIdentifier IDP_FIGURES = new LayerIdentifier("IDP Figures", "IDP_FIGURES");
|
||||
public static final LayerIdentifier IDP_TABLES = new LayerIdentifier("IDP Tables", "IDP_TABLES");
|
||||
public static final LayerIdentifier IDP_KV_PAIRS = new LayerIdentifier("IDP Key Value Pair", "IDP_KV_PAIRS");
|
||||
public static final LayerIdentifier IDP_SECTIONS = new LayerIdentifier("IDP Sections", "IDP_SECTIONS");
|
||||
public static final LayerIdentifier IDP_LINES = new LayerIdentifier("IDP Lines", "IDP_LINES");
|
||||
public static final LayerIdentifier IDP_PARAGRAPHS = new LayerIdentifier("IDP Paragraphs", "IDP_PARAGRAPHS");
|
||||
public static final LayerIdentifier IDP_LIST = new LayerIdentifier("IDP Lists", "IDP_LISTS");
|
||||
public static final LayerIdentifier IDP_BARCODES = new LayerIdentifier("IDP Barcodes", "IDP_BARCODES");
|
||||
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.layers;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.VisualizationsOnPage;
|
||||
|
||||
public abstract class AbstractLayerGroup implements LayerGroup {
|
||||
|
||||
protected VisualizationsOnPage getOrCreateVisualizationsOnPage(int page, Visualizations visualizations) {
|
||||
|
||||
if (visualizations.getVisualizationsOnPages().containsKey(page)) {
|
||||
return visualizations.getVisualizationsOnPages()
|
||||
.get(page);
|
||||
}
|
||||
VisualizationsOnPage visualizationsOnPage = VisualizationsOnPage.builder().build();
|
||||
visualizations.getVisualizationsOnPages().put(page, visualizationsOnPage);
|
||||
return visualizationsOnPage;
|
||||
}
|
||||
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.layers;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class IdpLayerConfig extends AbstractLayerGroup {
|
||||
|
||||
@Getter
|
||||
public final LayerIdentifier groupIdentifier = LayerIdentifier.KNECON_AZURE_IDP;
|
||||
|
||||
public static final LayerGroup CONFIG_INSTANCE = new IdpLayerConfig();
|
||||
|
||||
protected final Visualizations figures = Visualizations.builder().layer(LayerIdentifier.IDP_FIGURES).visibleByDefault(true).build();
|
||||
protected final Visualizations tables = Visualizations.builder().layer(LayerIdentifier.IDP_TABLES).visibleByDefault(true).build();
|
||||
protected final Visualizations keyValuePairs = Visualizations.builder().layer(LayerIdentifier.IDP_KV_PAIRS).visibleByDefault(true).build();
|
||||
protected final Visualizations paragraphs = Visualizations.builder().layer(LayerIdentifier.IDP_PARAGRAPHS).build();
|
||||
protected final Visualizations sections = Visualizations.builder().layer(LayerIdentifier.IDP_SECTIONS).build();
|
||||
protected final Visualizations lines = Visualizations.builder().layer(LayerIdentifier.IDP_LINES).build();
|
||||
protected final Visualizations lists = Visualizations.builder().layer(LayerIdentifier.IDP_LIST).visibleByDefault(true).build();
|
||||
protected final Visualizations barcodes = Visualizations.builder().layer(LayerIdentifier.IDP_BARCODES).visibleByDefault(true).build();
|
||||
|
||||
protected static final Color TABLE_COLOR = new Color(102, 205, 170);
|
||||
protected static final Color INNER_LINES_COLOR = new Color(255, 175, 175);
|
||||
protected static final Color SECTION_COLOR = new Color(50, 50, 50);
|
||||
protected static final Color SECTION_HEADING_COLOR = new Color(162, 56, 56);
|
||||
protected static final Color TITLE_COLOR = new Color(221, 25, 25);
|
||||
protected static final Color HEADER_FOOTER_COLOR = new Color(171, 131, 6);
|
||||
protected static final Color FOOTNOTE_COLOR = new Color(6, 64, 171);
|
||||
protected static final Color FORMULA_COLOR = new Color(80, 171, 6);
|
||||
protected static final Color PARAGRAPH_COLOR = new Color(70, 130, 180);
|
||||
protected static final Color IMAGE_COLOR = new Color(253, 63, 146);
|
||||
protected static final Color KEY_VALUE_BBOX_COLOR = new Color(0, 39, 85);
|
||||
protected static final Color KEY_COLOR = new Color(30, 92, 172);
|
||||
protected static final Color VALUE_COLOR = new Color(30, 172, 146);
|
||||
protected static final Color LINES_COLOR = new Color(152, 45, 179);
|
||||
|
||||
|
||||
@Override
|
||||
public List<Visualizations> getVisualizations() {
|
||||
|
||||
return List.of(paragraphs, sections, figures, tables, keyValuePairs, lines, lists, barcodes);
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.layers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
|
||||
public interface LayerGroup {
|
||||
|
||||
LayerIdentifier getGroupIdentifier();
|
||||
|
||||
|
||||
List<Visualizations> getVisualizations();
|
||||
|
||||
|
||||
default List<LayerIdentifier> getSubLayers() {
|
||||
|
||||
return getVisualizations().stream()
|
||||
.map(Visualizations::getLayer)
|
||||
.toList();
|
||||
|
||||
}
|
||||
|
||||
|
||||
default boolean isVisibleByDefault() {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
indicates the sub layers are all optional content
|
||||
*/
|
||||
default boolean subLayersAreOptionalContent() {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
indicates the LayerGroup is also a optional content group, and should be displayed as such:
|
||||
layer
|
||||
- sublayer0
|
||||
- sublayer1
|
||||
|
||||
see note in specification 8.11.4.3
|
||||
*/
|
||||
default boolean isOptionalContent() {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
default boolean isEmpty() {
|
||||
|
||||
return getVisualizations().isEmpty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.layers;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Standard14EmbeddableFont;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class LayoutDebugLayerConfig extends AbstractLayerGroup {
|
||||
|
||||
@Getter
|
||||
public final LayerIdentifier groupIdentifier = LayerIdentifier.KNECON_LAYOUT_DEBUG;
|
||||
|
||||
public static final LayerGroup CONFIG_INSTANCE = new LayoutDebugLayerConfig();
|
||||
|
||||
protected static final Standard14EmbeddableFont FONT = Standard14EmbeddableFont.helvetica();
|
||||
|
||||
protected static final Color WORDS_COLOR = new Color(68, 84, 147);
|
||||
protected static final Color LINES_COLOR = new Color(152, 45, 179);
|
||||
protected static final Color ZONES_COLOR = new Color(131, 38, 38);
|
||||
|
||||
protected static final Color RULINGS_COLOR = new Color(21, 221, 174);
|
||||
protected static final Color TABLE_RULINGS_COLOR = new Color(255, 175, 175);
|
||||
protected static final Color HEADER_RULING_COLOR = new Color(171, 131, 6);
|
||||
protected static final Color FOOTER_RULING_COLOR = new Color(106, 82, 2);
|
||||
protected static final Color UNDERLINE_RULING_COLOR = new Color(6, 39, 171);
|
||||
protected static final Color STRIKETROUGH_RULING_COLOR = new Color(171, 6, 6);
|
||||
|
||||
protected static final Color CELLS_COLOR = new Color(31, 214, 27);
|
||||
|
||||
protected static final Color MAIN_BODY_COLOR = new Color(171, 131, 6);
|
||||
protected static final Color MARKED_CONTENT_COLOR = new Color(171, 131, 6);
|
||||
|
||||
protected static final List<Color> ROTATING_CHARACTER_COLOR = List.of(new Color(255, 87, 51),
|
||||
new Color(255, 195, 0),
|
||||
new Color(76, 175, 80),
|
||||
new Color(33, 150, 243),
|
||||
new Color(155, 89, 182),
|
||||
new Color(233, 30, 99),
|
||||
new Color(0, 188, 212),
|
||||
new Color(121, 85, 72));
|
||||
|
||||
protected final Visualizations words = Visualizations.builder().layer(LayerIdentifier.WORDS).visibleByDefault(true).build();
|
||||
protected final Visualizations lines = Visualizations.builder().layer(LayerIdentifier.LINES).build();
|
||||
protected final Visualizations zones = Visualizations.builder().layer(LayerIdentifier.ZONES).build();
|
||||
protected final Visualizations mainBody = Visualizations.builder().layer(LayerIdentifier.MAIN_BODY).build();
|
||||
protected final Visualizations clean_rulings = Visualizations.builder().layer(LayerIdentifier.CLEAN_RULINGS).build();
|
||||
protected final Visualizations rulings = Visualizations.builder().layer(LayerIdentifier.RULINGS).build();
|
||||
protected final Visualizations cells = Visualizations.builder().layer(LayerIdentifier.CELLS).build();
|
||||
protected final Visualizations markedContent = Visualizations.builder().layer(LayerIdentifier.MARKED_CONTENT).build();
|
||||
protected final Visualizations neighbours = Visualizations.builder().layer(LayerIdentifier.NEIGHBOURS).build();
|
||||
protected final Visualizations characters = Visualizations.builder().layer(LayerIdentifier.CHARACTERS).build();
|
||||
|
||||
|
||||
public List<Visualizations> getVisualizations() {
|
||||
|
||||
return List.of(characters, //
|
||||
neighbours,//
|
||||
words, //
|
||||
lines, //
|
||||
zones, //
|
||||
rulings, //
|
||||
clean_rulings, //
|
||||
cells, //
|
||||
mainBody, //
|
||||
markedContent //
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.layers;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Standard14EmbeddableFont;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class LayoutGridLayerConfig extends AbstractLayerGroup {
|
||||
|
||||
@Getter
|
||||
public final LayerIdentifier groupIdentifier = LayerIdentifier.KNECON_LAYOUT;
|
||||
|
||||
public static final LayerGroup CONFIG_INSTANCE = new LayoutGridLayerConfig();
|
||||
|
||||
protected static final float FONT_SIZE = 10f;
|
||||
protected static final float LINE_WIDTH = 1f;
|
||||
protected static final Standard14EmbeddableFont FONT = Standard14EmbeddableFont.helvetica();
|
||||
|
||||
protected static final Color INNER_LINES_COLOR = new Color(255, 175, 175);
|
||||
protected static final Color HEADER_CELL_COLOR = new Color(156, 21, 48);
|
||||
protected static final Color PARAGRAPH_COLOR = new Color(70, 130, 180);
|
||||
|
||||
protected static final Color DUPLICATE_PARAGRAPH_COLOR = new Color(70, 180, 101);
|
||||
protected static final Color TABLE_COLOR = new Color(102, 205, 170);
|
||||
protected static final Color SECTION_COLOR = new Color(50, 50, 50);
|
||||
protected static final Color HEADLINE_COLOR = new Color(162, 56, 56);
|
||||
protected static final Color HEADER_COLOR = new Color(171, 131, 6);
|
||||
protected static final Color IMAGE_COLOR = new Color(253, 63, 146);
|
||||
protected static final Color TREEID_COLOR = new Color(53, 53, 53);
|
||||
protected static final Color KEY_VALUE_BBOX_COLOR = new Color(0, 39, 85);
|
||||
protected static final Color KEY_COLOR = new Color(30, 92, 172);
|
||||
protected static final Color VALUE_COLOR = new Color(30, 172, 146);
|
||||
|
||||
protected final Visualizations sections = Visualizations.builder().layer(LayerIdentifier.KNECON_LAYOUT_SECTION).visibleByDefault(true).build();
|
||||
protected final Visualizations paragraphs = Visualizations.builder().layer(LayerIdentifier.KNECON_LAYOUT_PARAGRAPH).visibleByDefault(true).build();
|
||||
protected final Visualizations headlines = Visualizations.builder().layer(LayerIdentifier.KNECON_LAYOUT_HEADLINE).visibleByDefault(true).build();
|
||||
protected final Visualizations tables = Visualizations.builder().layer(LayerIdentifier.KNECON_LAYOUT_TABLE).visibleByDefault(true).build();
|
||||
protected final Visualizations figures = Visualizations.builder().layer(LayerIdentifier.KNECON_LAYOUT_FIGURES).visibleByDefault(true).build();
|
||||
protected final Visualizations headerFooter = Visualizations.builder().layer(LayerIdentifier.KNECON_LAYOUT_HEADER_FOOTER).visibleByDefault(true).build();
|
||||
protected final Visualizations images = Visualizations.builder().layer(LayerIdentifier.KNECON_LAYOUT_IMAGES).build();
|
||||
protected final Visualizations keyValue = Visualizations.builder().layer(LayerIdentifier.KNECON_LAYOUT_KEY_VALUE).build();
|
||||
protected final Visualizations treeIds = Visualizations.builder().layer(LayerIdentifier.KNECON_LAYOUT_TREE_IDs).build();
|
||||
|
||||
|
||||
@Override
|
||||
public List<Visualizations> getVisualizations() {
|
||||
|
||||
return List.of(headlines, paragraphs, tables, sections, headerFooter, keyValue, figures, images, treeIds);
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.layers;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class OcrDebugLayerConfig extends AbstractLayerGroup {
|
||||
|
||||
@Getter
|
||||
public final LayerIdentifier groupIdentifier = LayerIdentifier.KNECON_OCR_DEBUG;
|
||||
|
||||
public static final LayerGroup CONFIG_INSTANCE = new OcrDebugLayerConfig();
|
||||
|
||||
protected static final Color REGULAR_COLOR = new Color(6, 39, 171);
|
||||
protected static final Color BOLD_COLOR = new Color(50, 246, 246);
|
||||
protected static final Color ITALIC_COLOR = new Color(171, 105, 6);
|
||||
protected static final Color BOLD_ITALIC_COLOR = new Color(6, 171, 102);
|
||||
protected static final Color HANDWRITTEN_COLOR = new Color(171, 64, 6);
|
||||
protected static final Color OVERLAPPED_COLOR = new Color(142, 8, 8);
|
||||
protected static final Color TABLE_LINES_COLOR = new Color(21, 221, 174);
|
||||
|
||||
protected final Visualizations debugText = Visualizations.builder().layer(LayerIdentifier.KNECON_OCR_TEXT_DEBUG).visibleByDefault(true).build();
|
||||
protected final Visualizations tableLines = Visualizations.builder().layer(LayerIdentifier.KNECON_OCR_LINE_DEBUG).visibleByDefault(true).build();
|
||||
protected final Visualizations overlappedText = Visualizations.builder().layer(LayerIdentifier.KNECON_OCR_OVERLAPPED_TEXT).visibleByDefault(false).build();
|
||||
protected final Visualizations debugBBox = Visualizations.builder().layer(LayerIdentifier.KNECON_OCR_BBOX_DEBUG).visibleByDefault(false).build();
|
||||
|
||||
|
||||
@Override
|
||||
public List<Visualizations> getVisualizations() {
|
||||
|
||||
return List.of(debugText, tableLines, debugBBox, overlappedText);
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.layers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
|
||||
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
|
||||
|
||||
public class OcrTextLayerConfig extends AbstractLayerGroup {
|
||||
|
||||
protected final Visualizations ocrText = Visualizations.builder().layer(LayerIdentifier.KNECON_OCR_TEXT).build();
|
||||
protected final Visualizations tableLines = Visualizations.builder().layer(LayerIdentifier.KNECON_OCR_LINES).build();
|
||||
|
||||
|
||||
@Override
|
||||
public LayerIdentifier getGroupIdentifier() {
|
||||
|
||||
return LayerIdentifier.KNECON_OCR;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<Visualizations> getVisualizations() {
|
||||
|
||||
return List.of(ocrText, tableLines);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean subLayersAreOptionalContent() {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOptionalContent() {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
+5
@@ -3,8 +3,13 @@ package com.knecon.fforesight.service.viewerdoc.model;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
|
||||
import com.pdftron.pdf.Font;
|
||||
import com.pdftron.pdf.PDFDoc;
|
||||
|
||||
public interface EmbeddableFont {
|
||||
|
||||
PDFont embed(PDDocument document);
|
||||
|
||||
Font embed(PDFDoc doc);
|
||||
|
||||
}
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Getter
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class LayoutGrid {
|
||||
|
||||
int numberOfPages;
|
||||
Map<Integer, VisualizationsOnPage> visualizationsPerPages;
|
||||
|
||||
|
||||
public LayoutGrid(int numberOfPages) {
|
||||
|
||||
this.numberOfPages = numberOfPages;
|
||||
this.visualizationsPerPages = new HashMap<>();
|
||||
for (int i = 0; i < numberOfPages; i++) {
|
||||
this.visualizationsPerPages.put(i, VisualizationsOnPage.builder().build());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.model;
|
||||
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Set;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
|
||||
|
||||
public class MarkedContentStack {
|
||||
|
||||
private final Deque<MarkedContent> stack = new LinkedList<>();
|
||||
|
||||
|
||||
public void enterMarkedContent(String name) {
|
||||
|
||||
stack.push(new MarkedContent(name));
|
||||
}
|
||||
|
||||
|
||||
public void leaveMarkedContent() {
|
||||
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
|
||||
public String currentMarkedContent() {
|
||||
|
||||
if (stack.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return stack.peek().name();
|
||||
}
|
||||
|
||||
|
||||
public boolean currentMarkedContentContains(String name) {
|
||||
|
||||
Iterator<MarkedContent> markedContentIterator = stack.descendingIterator();
|
||||
while (markedContentIterator.hasNext()) {
|
||||
var markedContent = markedContentIterator.next();
|
||||
if (markedContent.name().equals(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public boolean currentMarkedContentContainsNone(Set<String> names) {
|
||||
|
||||
if (stack.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
Iterator<MarkedContent> markedContentIterator = stack.descendingIterator();
|
||||
while (markedContentIterator.hasNext()) {
|
||||
var markedContent = markedContentIterator.next();
|
||||
if (names.contains(markedContent.name())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public boolean currentMarkedContentIsKneconContent() {
|
||||
|
||||
if (stack.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Iterator<MarkedContent> markedContentIterator = stack.descendingIterator();
|
||||
while (markedContentIterator.hasNext()) {
|
||||
var markedContent = markedContentIterator.next();
|
||||
if (markedContent.name().startsWith(LayerIdentifier.KNECON_IDENTIFIER_PREFIX)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void clear() {
|
||||
|
||||
stack.clear();
|
||||
}
|
||||
|
||||
|
||||
private record MarkedContent(String name) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.contentstream.operator.Operator;
|
||||
import org.apache.pdfbox.cos.COSBase;
|
||||
|
||||
public record OperatorWithArguments(Operator operator, List<COSBase> arguments) {
|
||||
|
||||
}
|
||||
+2
-1
@@ -1,13 +1,14 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.model;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
|
||||
public record PlacedText(String text, Point2D lineStart, Color color, float fontSize, EmbeddableFont font, Optional<Matrix> textMatrix, Optional<RenderingMode> renderingMode) {
|
||||
public record PlacedText(String text, Point2D lineStart, Color color, float fontSize, EmbeddableFont font, Optional<AffineTransform> textMatrix, Optional<RenderingMode> renderingMode) {
|
||||
|
||||
public static PlacedText textFacingUp(String text, Point2D lineStart, float fontSize, Color color, EmbeddableFont font) {
|
||||
|
||||
|
||||
+15
-1
@@ -1,10 +1,15 @@
|
||||
package com.knecon.fforesight.service.viewerdoc.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
|
||||
import com.pdftron.pdf.Font;
|
||||
import com.pdftron.pdf.PDFDoc;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
@@ -13,10 +18,11 @@ public class Standard14EmbeddableFont implements EmbeddableFont {
|
||||
|
||||
private final PDType1Font font;
|
||||
|
||||
private final int pdfTronIdentifier;
|
||||
|
||||
public static Standard14EmbeddableFont helvetica() {
|
||||
|
||||
return new Standard14EmbeddableFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA));
|
||||
return new Standard14EmbeddableFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), Font.e_helvetica);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,4 +40,12 @@ public class Standard14EmbeddableFont implements EmbeddableFont {
|
||||
return font;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Font embed(PDFDoc document) {
|
||||
|
||||
return Font.create(document, pdfTronIdentifier);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-3
@@ -3,7 +3,7 @@ package com.knecon.fforesight.service.viewerdoc.model;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.knecon.fforesight.service.viewerdoc.ContentStreams;
|
||||
import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -17,9 +17,10 @@ import lombok.experimental.FieldDefaults;
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class Visualizations {
|
||||
|
||||
ContentStreams.Identifier layer;
|
||||
LayerIdentifier layer;
|
||||
@Builder.Default
|
||||
Map<Integer, VisualizationsOnPage> visualizationsOnPages = new LinkedHashMap<>();
|
||||
boolean layerVisibilityDefaultValue;
|
||||
|
||||
boolean visibleByDefault;
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user