fixed all test and pmd or checkstyle errors
This commit is contained in:
parent
afc051592b
commit
daf221a52f
@ -1,24 +1,16 @@
|
|||||||
plugins {
|
plugins {
|
||||||
java
|
`java-library`
|
||||||
|
`maven-publish`
|
||||||
pmd
|
pmd
|
||||||
checkstyle
|
checkstyle
|
||||||
jacoco
|
jacoco
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.iqser.red"
|
group = "com.iqser.red"
|
||||||
version = "4.0-SNAPSHOT"
|
|
||||||
|
|
||||||
java.sourceCompatibility = JavaVersion.VERSION_17
|
java.sourceCompatibility = JavaVersion.VERSION_17
|
||||||
java.targetCompatibility = JavaVersion.VERSION_17
|
java.targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
|
||||||
tasks.jacocoTestReport {
|
|
||||||
reports {
|
|
||||||
xml.required.set(false)
|
|
||||||
csv.required.set(false)
|
|
||||||
html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.pmdMain {
|
tasks.pmdMain {
|
||||||
pmd.ruleSetFiles = files("${rootDir}/config/pmd/pmd.xml")
|
pmd.ruleSetFiles = files("${rootDir}/config/pmd/pmd.xml")
|
||||||
}
|
}
|
||||||
@ -29,6 +21,9 @@ tasks.pmdTest {
|
|||||||
|
|
||||||
tasks.named<Test>("test") {
|
tasks.named<Test>("test") {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
reports {
|
||||||
|
junitXml.outputLocation.set(layout.buildDirectory.dir("reports/junit"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
@ -40,6 +35,26 @@ tasks.jacocoTestReport {
|
|||||||
reports {
|
reports {
|
||||||
xml.required.set(true)
|
xml.required.set(true)
|
||||||
csv.required.set(false)
|
csv.required.set(false)
|
||||||
|
html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>(name) {
|
||||||
|
from(components["java"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url = uri("https://nexus.knecon.com/repository/red-platform-releases/")
|
||||||
|
credentials {
|
||||||
|
username = providers.gradleProperty("mavenUser").getOrNull();
|
||||||
|
password = providers.gradleProperty("mavenPassword").getOrNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,6 @@
|
|||||||
<exclude name="DataflowAnomalyAnalysis"/>
|
<exclude name="DataflowAnomalyAnalysis"/>
|
||||||
<exclude name="MissingSerialVersionUID"/>
|
<exclude name="MissingSerialVersionUID"/>
|
||||||
<exclude name="NullAssignment"/>
|
<exclude name="NullAssignment"/>
|
||||||
<exclude name="BeanMembersShouldSerialize"/>
|
|
||||||
<exclude name="AvoidLiteralsInIfCondition"/>
|
<exclude name="AvoidLiteralsInIfCondition"/>
|
||||||
<exclude name="AvoidDuplicateLiterals"/>
|
<exclude name="AvoidDuplicateLiterals"/>
|
||||||
<exclude name="AvoidFieldNameMatchingMethodName"/>
|
<exclude name="AvoidFieldNameMatchingMethodName"/>
|
||||||
|
|||||||
@ -13,12 +13,12 @@
|
|||||||
<exclude name="DataflowAnomalyAnalysis"/>
|
<exclude name="DataflowAnomalyAnalysis"/>
|
||||||
<exclude name="MissingSerialVersionUID"/>
|
<exclude name="MissingSerialVersionUID"/>
|
||||||
<exclude name="NullAssignment"/>
|
<exclude name="NullAssignment"/>
|
||||||
<exclude name="BeanMembersShouldSerialize"/>
|
|
||||||
<exclude name="AvoidLiteralsInIfCondition"/>
|
<exclude name="AvoidLiteralsInIfCondition"/>
|
||||||
<exclude name="AvoidDuplicateLiterals"/>
|
<exclude name="AvoidDuplicateLiterals"/>
|
||||||
<exclude name="AvoidFieldNameMatchingMethodName"/>
|
<exclude name="AvoidFieldNameMatchingMethodName"/>
|
||||||
<exclude name="AvoidFieldNameMatchingTypeName"/>
|
<exclude name="AvoidFieldNameMatchingTypeName"/>
|
||||||
<exclude name="AssignmentInOperand"/>
|
<exclude name="AssignmentInOperand"/>
|
||||||
|
<exclude name="TestClassWithoutTestCases"/>
|
||||||
</rule>
|
</rule>
|
||||||
|
|
||||||
</ruleset>
|
</ruleset>
|
||||||
1
gradle.properties.kts
Normal file
1
gradle.properties.kts
Normal file
@ -0,0 +1 @@
|
|||||||
|
version = 4.0-SNAPSHOT
|
||||||
@ -1,13 +1,14 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.iqser.red.service.java-conventions")
|
id("com.iqser.red.service.java-conventions")
|
||||||
id("io.freefair.lombok") version "8.0.1"
|
id("io.freefair.lombok") version "8.1.0"
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("org.springframework:spring-web:6.0.6")
|
|
||||||
implementation("com.iqser.red.service:persistence-service-internal-api-v1:2.79.0")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
description = "redaction-service-api-v1"
|
description = "redaction-service-api-v1"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.springframework:spring-web:6.0.6")
|
||||||
|
implementation("com.iqser.red.service:persistence-service-internal-api-v1:RED-6725")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,15 +6,21 @@ plugins {
|
|||||||
id("org.springframework.boot") version "3.0.6"
|
id("org.springframework.boot") version "3.0.6"
|
||||||
id("io.spring.dependency-management") version "1.1.0"
|
id("io.spring.dependency-management") version "1.1.0"
|
||||||
id("org.sonarqube") version "4.2.1.3168"
|
id("org.sonarqube") version "4.2.1.3168"
|
||||||
id("io.freefair.lombok") version "8.0.1"
|
id("io.freefair.lombok") version "8.1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
description = "redaction-service-server-v1"
|
||||||
|
|
||||||
|
extra["layoutparserVersion"] = "0.8.0"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":redaction-service-api-v1"))
|
implementation(project(":redaction-service-api-v1"))
|
||||||
implementation("com.iqser.red.service:persistence-service-internal-api-v1:2.79.0")
|
implementation("com.iqser.red.service:persistence-service-internal-api-v1:RED-6725")
|
||||||
|
implementation("com.knecon.fforesight:layoutparser-service-internal-api:0.12.0")
|
||||||
|
|
||||||
implementation("com.iqser.red.commons:spring-commons:2.1.0") { exclude(group = "org.springframework.boot", module = "spring-boot-starter-log4j2") }
|
implementation("com.iqser.red.commons:spring-commons:2.1.0") { exclude(group = "org.springframework.boot", module = "spring-boot-starter-log4j2") }
|
||||||
implementation("com.iqser.red.commons:metric-commons:2.1.0") { exclude(group = "org.springframework.boot", module = "spring-boot-starter-log4j2") }
|
implementation("com.iqser.red.commons:metric-commons:2.1.0") { exclude(group = "org.springframework.boot", module = "spring-boot-starter-log4j2") }
|
||||||
|
|
||||||
implementation("com.iqser.red.commons:dictionary-merge-commons:1.3.0")
|
implementation("com.iqser.red.commons:dictionary-merge-commons:1.3.0")
|
||||||
implementation("com.iqser.red.commons:storage-commons:2.1.0")
|
implementation("com.iqser.red.commons:storage-commons:2.1.0")
|
||||||
implementation("com.knecon.fforesight:tenant-commons:0.10.0")
|
implementation("com.knecon.fforesight:tenant-commons:0.10.0")
|
||||||
@ -31,15 +37,15 @@ dependencies {
|
|||||||
|
|
||||||
implementation("org.locationtech.jts:jts-core:1.19.0")
|
implementation("org.locationtech.jts:jts-core:1.19.0")
|
||||||
|
|
||||||
implementation("org.apache.pdfbox:pdfbox:2.0.24")
|
testImplementation("org.apache.pdfbox:pdfbox:3.0.0-alpha2")
|
||||||
implementation("org.apache.pdfbox:pdfbox-tools:2.0.24")
|
testImplementation("org.apache.pdfbox:pdfbox-tools:3.0.0-alpha2")
|
||||||
|
|
||||||
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.0.1")
|
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.0.1")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-amqp:3.0.4")
|
implementation("org.springframework.boot:spring-boot-starter-amqp:3.0.4")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test:3.0.4")
|
testImplementation("org.springframework.boot:spring-boot-starter-test:3.0.4")
|
||||||
|
testImplementation("com.knecon.fforesight:layoutparser-service-processor:0.12.0") { exclude(group = "com.iqser.red.service", module = "persistence-service-shared-api-v1") }
|
||||||
}
|
}
|
||||||
|
|
||||||
description = "redaction-service-server-v1"
|
|
||||||
|
|
||||||
|
|
||||||
tasks.named<BootBuildImage>("bootBuildImage") {
|
tasks.named<BootBuildImage>("bootBuildImage") {
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.data;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
|
||||||
public class AtomicPositionBlockData {
|
|
||||||
|
|
||||||
Long id;
|
|
||||||
int[] stringIdxToPositionIdx;
|
|
||||||
float[][] positions;
|
|
||||||
|
|
||||||
|
|
||||||
public static AtomicPositionBlockData fromAtomicTextBlock(AtomicTextBlock atomicTextBlock) {
|
|
||||||
|
|
||||||
return AtomicPositionBlockData.builder()
|
|
||||||
.id(atomicTextBlock.getId())
|
|
||||||
.positions(toPrimitiveFloatMatrix(atomicTextBlock.getPositions()))
|
|
||||||
.stringIdxToPositionIdx(atomicTextBlock.getStringIdxToPositionIdx().stream().mapToInt(Integer::intValue).toArray())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static float[][] toPrimitiveFloatMatrix(List<Rectangle2D> positions) {
|
|
||||||
|
|
||||||
float[][] positionMatrix = new float[positions.size()][];
|
|
||||||
for (int i = 0; i < positions.size(); i++) {
|
|
||||||
positionMatrix[i] = toArray(positions.get(i));
|
|
||||||
}
|
|
||||||
return positionMatrix;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static float[] toArray(Rectangle2D positions) {
|
|
||||||
|
|
||||||
return new float[]{(float) positions.getMinX(), (float) positions.getMinY(), (float) positions.getWidth(), (float) positions.getHeight()};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.data;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
|
||||||
public class AtomicTextBlockData {
|
|
||||||
|
|
||||||
Long id;
|
|
||||||
Long page;
|
|
||||||
String searchText;
|
|
||||||
int numberOnPage;
|
|
||||||
int start;
|
|
||||||
int end;
|
|
||||||
int[] lineBreaks;
|
|
||||||
|
|
||||||
|
|
||||||
public static AtomicTextBlockData fromAtomicTextBlock(AtomicTextBlock atomicTextBlock) {
|
|
||||||
|
|
||||||
return AtomicTextBlockData.builder()
|
|
||||||
.id(atomicTextBlock.getId())
|
|
||||||
.page(atomicTextBlock.getPage().getNumber().longValue())
|
|
||||||
.searchText(atomicTextBlock.getSearchText())
|
|
||||||
.numberOnPage(atomicTextBlock.getNumberOnPage())
|
|
||||||
.start(atomicTextBlock.getBoundary().start())
|
|
||||||
.end(atomicTextBlock.getBoundary().end())
|
|
||||||
.lineBreaks(atomicTextBlock.getLineBreaks().stream().mapToInt(Integer::intValue).toArray())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,6 +1,9 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.data;
|
package com.iqser.red.service.redaction.v1.server.document.data;
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Document;
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPage;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionData;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentStructure;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentTextData;
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@ -14,30 +17,9 @@ import lombok.experimental.FieldDefaults;
|
|||||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||||
public class DocumentData {
|
public class DocumentData {
|
||||||
|
|
||||||
PageData[] pages;
|
DocumentPage[] documentPages;
|
||||||
AtomicTextBlockData[] atomicTextBlocks;
|
DocumentTextData[] documentTextData;
|
||||||
AtomicPositionBlockData[] atomicPositionBlocks;
|
DocumentPositionData[] documentPositionData;
|
||||||
DocumentTreeData documentTreeData;
|
DocumentStructure documentStructure;
|
||||||
|
|
||||||
|
|
||||||
public static DocumentData fromDocument(Document document) {
|
|
||||||
|
|
||||||
var atomicPositionBlocks = document.streamTerminalTextBlocksInOrder()
|
|
||||||
.flatMap(textBlock -> textBlock.getAtomicTextBlocks().stream())
|
|
||||||
.distinct()
|
|
||||||
.map(AtomicPositionBlockData::fromAtomicTextBlock)
|
|
||||||
.toArray(AtomicPositionBlockData[]::new);
|
|
||||||
|
|
||||||
var atomicTextBlocks = document.streamTerminalTextBlocksInOrder()
|
|
||||||
.flatMap(textBlock -> textBlock.getAtomicTextBlocks().stream())
|
|
||||||
.distinct()
|
|
||||||
.map(AtomicTextBlockData::fromAtomicTextBlock)
|
|
||||||
.toArray(AtomicTextBlockData[]::new);
|
|
||||||
|
|
||||||
var pages = document.getPages().stream().map(PageData::fromPage).toArray(PageData[]::new);
|
|
||||||
|
|
||||||
var documentTreeData = new DocumentTreeData(DocumentTreeData.EntryData.fromEntry(document.getDocumentTree().getRoot()));
|
|
||||||
return new DocumentData(pages, atomicTextBlocks, atomicPositionBlocks, documentTreeData);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.data;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.mapper.PropertiesMapper;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentTree;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Image;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableCell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Table;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class DocumentTreeData {
|
|
||||||
|
|
||||||
EntryData root;
|
|
||||||
|
|
||||||
|
|
||||||
public EntryData get(List<Integer> tocId) {
|
|
||||||
|
|
||||||
if (tocId.isEmpty()) {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
EntryData entry = root.children.get(tocId.get(0));
|
|
||||||
for (int id : tocId.subList(1, tocId.size())) {
|
|
||||||
entry = entry.children.get(id);
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Stream<EntryData> streamAllEntries() {
|
|
||||||
|
|
||||||
return Stream.concat(Stream.of(root), root.children.stream()).flatMap(DocumentTreeData::flatten);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
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(DocumentTreeData::flatten));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
|
||||||
public static class EntryData {
|
|
||||||
|
|
||||||
NodeType type;
|
|
||||||
int[] treeId;
|
|
||||||
Long[] atomicBlockIds;
|
|
||||||
Long[] pageNumbers;
|
|
||||||
Map<String, String> properties;
|
|
||||||
List<EntryData> children;
|
|
||||||
|
|
||||||
|
|
||||||
public static EntryData fromEntry(DocumentTree.Entry entry) {
|
|
||||||
|
|
||||||
Long[] atomicBlockIds = toAtomicTextBlockIds(entry);
|
|
||||||
|
|
||||||
Map<String, String> properties = switch (entry.getType()) {
|
|
||||||
case TABLE -> PropertiesMapper.buildTableProperties((Table) entry.getNode());
|
|
||||||
case TABLE_CELL -> PropertiesMapper.buildTableCellProperties((TableCell) entry.getNode());
|
|
||||||
case IMAGE -> PropertiesMapper.buildImageProperties((Image) entry.getNode());
|
|
||||||
default -> new HashMap<>();
|
|
||||||
};
|
|
||||||
var treeId = entry.getTreeId().stream().mapToInt(Integer::intValue).toArray();
|
|
||||||
var pageNumbers = entry.getNode().getPages().stream().map(Page::getNumber).map(Integer::longValue).toArray(Long[]::new);
|
|
||||||
var subEntries = entry.getChildren().stream().map(EntryData::fromEntry).toList();
|
|
||||||
return new EntryData(entry.getType(), treeId, atomicBlockIds, pageNumbers, properties, subEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static Long[] toAtomicTextBlockIds(DocumentTree.Entry entry) {
|
|
||||||
|
|
||||||
if (entry.getNode().isLeaf()) {
|
|
||||||
return entry.getNode().getLeafTextBlock().getAtomicTextBlocks().stream().map(AtomicTextBlock::getId).toArray(Long[]::new);
|
|
||||||
} else {
|
|
||||||
return new Long[]{};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("[");
|
|
||||||
for (int i : treeId) {
|
|
||||||
sb.append(i);
|
|
||||||
sb.append(",");
|
|
||||||
}
|
|
||||||
sb.delete(sb.length() - 1, sb.length());
|
|
||||||
sb.append("]: ");
|
|
||||||
|
|
||||||
sb.append(type);
|
|
||||||
sb.append(" atbs = ");
|
|
||||||
sb.append(atomicBlockIds.length);
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.data;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
|
||||||
public class PageData {
|
|
||||||
|
|
||||||
int number;
|
|
||||||
int height;
|
|
||||||
int width;
|
|
||||||
int rotation;
|
|
||||||
|
|
||||||
|
|
||||||
public static PageData fromPage(Page page) {
|
|
||||||
|
|
||||||
return new PageData(page.getNumber(), page.getHeight(), page.getWidth(), page.getRotation());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -19,14 +19,14 @@ import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableCell;
|
|||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.AtomicPositionBlockData;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.AtomicTextBlockData;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.DocumentData;
|
import com.iqser.red.service.redaction.v1.server.document.data.DocumentData;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.DocumentTreeData;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.PageData;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Document;
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Document;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Headline;
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Headline;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Table;
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Table;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPage;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionData;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentStructure;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentTextData;
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
@ -39,23 +39,23 @@ public class DocumentGraphMapper {
|
|||||||
DocumentTree documentTree = new DocumentTree(document);
|
DocumentTree documentTree = new DocumentTree(document);
|
||||||
Context context = new Context(documentData, documentTree);
|
Context context = new Context(documentData, documentTree);
|
||||||
|
|
||||||
context.pages.addAll(Arrays.stream(documentData.getPages()).map(DocumentGraphMapper::buildPage).toList());
|
context.pageData.addAll(Arrays.stream(documentData.getDocumentPages()).map(DocumentGraphMapper::buildPage).toList());
|
||||||
|
|
||||||
context.documentTree.getRoot().getChildren().addAll(buildEntries(documentData.getDocumentTreeData().getRoot().getChildren(), context));
|
context.documentTree.getRoot().getChildren().addAll(buildEntries(documentData.getDocumentStructure().getRoot().getChildren(), context));
|
||||||
|
|
||||||
document.setDocumentTree(context.documentTree);
|
document.setDocumentTree(context.documentTree);
|
||||||
document.setPages(new HashSet<>(context.pages));
|
document.setPages(new HashSet<>(context.pageData));
|
||||||
document.setNumberOfPages(documentData.getPages().length);
|
document.setNumberOfPages(documentData.getDocumentPages().length);
|
||||||
|
|
||||||
document.setTextBlock(document.getTextBlock());
|
document.setTextBlock(document.getTextBlock());
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private List<DocumentTree.Entry> buildEntries(List<DocumentTreeData.EntryData> entries, Context context) {
|
private List<DocumentTree.Entry> buildEntries(List<DocumentStructure.EntryData> entries, Context context) {
|
||||||
|
|
||||||
List<DocumentTree.Entry> newEntries = new LinkedList<>();
|
List<DocumentTree.Entry> newEntries = new LinkedList<>();
|
||||||
for (DocumentTreeData.EntryData entryData : entries) {
|
for (DocumentStructure.EntryData entryData : entries) {
|
||||||
|
|
||||||
List<Page> pages = Arrays.stream(entryData.getPageNumbers()).map(pageNumber -> getPage(pageNumber, context)).toList();
|
List<Page> pages = Arrays.stream(entryData.getPageNumbers()).map(pageNumber -> getPage(pageNumber, context)).toList();
|
||||||
|
|
||||||
@ -154,14 +154,14 @@ public class DocumentGraphMapper {
|
|||||||
|
|
||||||
private AtomicTextBlock getAtomicTextBlock(Context context, SemanticNode parent, Long atomicTextBlockId) {
|
private AtomicTextBlock getAtomicTextBlock(Context context, SemanticNode parent, Long atomicTextBlockId) {
|
||||||
|
|
||||||
return AtomicTextBlock.fromAtomicTextBlockData(context.atomicTextBlockData.get(Math.toIntExact(atomicTextBlockId)),
|
return AtomicTextBlock.fromAtomicTextBlockData(context.documentTextData.get(Math.toIntExact(atomicTextBlockId)),
|
||||||
context.atomicPositionBlockData.get(Math.toIntExact(atomicTextBlockId)),
|
context.documentPositionData.get(Math.toIntExact(atomicTextBlockId)),
|
||||||
parent,
|
parent,
|
||||||
getPage(context.atomicTextBlockData.get(Math.toIntExact(atomicTextBlockId)).getPage(), context));
|
getPage(context.documentTextData.get(Math.toIntExact(atomicTextBlockId)).getPage(), context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Page buildPage(PageData p) {
|
private Page buildPage(DocumentPage p) {
|
||||||
|
|
||||||
return Page.builder().rotation(p.getRotation()).height(p.getHeight()).width(p.getWidth()).number(p.getNumber()).mainBody(new LinkedList<>()).build();
|
return Page.builder().rotation(p.getRotation()).height(p.getHeight()).width(p.getWidth()).number(p.getNumber()).mainBody(new LinkedList<>()).build();
|
||||||
}
|
}
|
||||||
@ -169,7 +169,7 @@ public class DocumentGraphMapper {
|
|||||||
|
|
||||||
private Page getPage(Long pageIndex, Context context) {
|
private Page getPage(Long pageIndex, Context context) {
|
||||||
|
|
||||||
return context.pages.stream()
|
return context.pageData.stream()
|
||||||
.filter(page -> page.getNumber() == Math.toIntExact(pageIndex))
|
.filter(page -> page.getNumber() == Math.toIntExact(pageIndex))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow(() -> new NoSuchElementException(String.format("ClassificationPage with number %d not found", pageIndex)));
|
.orElseThrow(() -> new NoSuchElementException(String.format("ClassificationPage with number %d not found", pageIndex)));
|
||||||
@ -179,17 +179,17 @@ public class DocumentGraphMapper {
|
|||||||
static final class Context {
|
static final class Context {
|
||||||
|
|
||||||
private final DocumentTree documentTree;
|
private final DocumentTree documentTree;
|
||||||
private final List<Page> pages;
|
private final List<Page> pageData;
|
||||||
private final List<AtomicTextBlockData> atomicTextBlockData;
|
private final List<DocumentTextData> documentTextData;
|
||||||
private final List<AtomicPositionBlockData> atomicPositionBlockData;
|
private final List<DocumentPositionData> documentPositionData;
|
||||||
|
|
||||||
|
|
||||||
Context(DocumentData documentData, DocumentTree documentTree) {
|
Context(DocumentData documentData, DocumentTree documentTree) {
|
||||||
|
|
||||||
this.documentTree = documentTree;
|
this.documentTree = documentTree;
|
||||||
this.pages = new LinkedList<>();
|
this.pageData = new LinkedList<>();
|
||||||
this.atomicTextBlockData = Arrays.stream(documentData.getAtomicTextBlocks()).toList();
|
this.documentTextData = Arrays.stream(documentData.getDocumentTextData()).toList();
|
||||||
this.atomicPositionBlockData = Arrays.stream(documentData.getAtomicPositionBlocks()).toList();
|
this.documentPositionData = Arrays.stream(documentData.getDocumentPositionData()).toList();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,242 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.factory;
|
|
||||||
|
|
||||||
import static java.lang.String.format;
|
|
||||||
import static java.util.stream.Collectors.groupingBy;
|
|
||||||
import static java.util.stream.Collectors.toList;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Footer;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.GenericSemanticNode;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Paragraph;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Section;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationDocument;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationFooter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationHeader;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image.ClassifiedImage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentTree;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Document;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Header;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Headline;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Image;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.utils.TextPositionOperations;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class DocumentGraphFactory {
|
|
||||||
|
|
||||||
public Document buildDocumentGraph(ClassificationDocument document) {
|
|
||||||
|
|
||||||
Document documentGraph = new Document();
|
|
||||||
Context context = new Context(documentGraph);
|
|
||||||
|
|
||||||
document.getPages().forEach(context::buildAndAddPageWithCounter);
|
|
||||||
document.getSections().stream().flatMap(section -> section.getImages().stream()).forEach(image -> context.getImages().add(image));
|
|
||||||
addSections(document, context);
|
|
||||||
addHeaderAndFooterToEachPage(document, context);
|
|
||||||
|
|
||||||
documentGraph.setNumberOfPages(context.pages.size());
|
|
||||||
documentGraph.setPages(context.pages.keySet());
|
|
||||||
documentGraph.setDocumentTree(context.documentTree);
|
|
||||||
documentGraph.setTextBlock(documentGraph.getTextBlock());
|
|
||||||
return documentGraph;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addSections(ClassificationDocument document, Context context) {
|
|
||||||
|
|
||||||
document.getSections().forEach(section -> SectionNodeFactory.addSection(null, section.getPageBlocks(), section.getImages(), context));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void addParagraphOrHeadline(GenericSemanticNode parentNode, TextPageBlock originalTextBlock, Context context, List<TextPageBlock> textBlocksToMerge) {
|
|
||||||
|
|
||||||
Page page = context.getPage(originalTextBlock.getPage());
|
|
||||||
|
|
||||||
GenericSemanticNode node;
|
|
||||||
if (originalTextBlock.isHeadline()) {
|
|
||||||
node = Headline.builder().documentTree(context.getDocumentTree()).build();
|
|
||||||
} else {
|
|
||||||
node = Paragraph.builder().documentTree(context.getDocumentTree()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
page.getMainBody().add(node);
|
|
||||||
|
|
||||||
List<TextPageBlock> textBlocks = new ArrayList<>(textBlocksToMerge);
|
|
||||||
AtomicTextBlock textBlock = context.textBlockFactory.fromContext(TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX(textBlocks), node, context, page);
|
|
||||||
List<Integer> treeId = context.documentTree.createNewChildEntryAndReturnId(parentNode, node);
|
|
||||||
node.setLeafTextBlock(textBlock);
|
|
||||||
node.setTreeId(treeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void addImage(Section section, ClassifiedImage image, Context context) {
|
|
||||||
|
|
||||||
Rectangle2D position = image.getPosition();
|
|
||||||
Page page = context.getPage(image.getPage());
|
|
||||||
Image imageNode = Image.builder()
|
|
||||||
.id(IdBuilder.buildId(Set.of(page), List.of(position), image.getImageType().name(), "image"))
|
|
||||||
.imageType(image.getImageType())
|
|
||||||
.position(position)
|
|
||||||
.transparent(image.isHasTransparency())
|
|
||||||
.page(page)
|
|
||||||
.documentTree(context.getDocumentTree())
|
|
||||||
.build();
|
|
||||||
page.getMainBody().add(imageNode);
|
|
||||||
|
|
||||||
List<Integer> tocId = context.getDocumentTree().createNewChildEntryAndReturnId(section, imageNode);
|
|
||||||
imageNode.setTreeId(tocId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addHeaderAndFooterToEachPage(ClassificationDocument document, Context context) {
|
|
||||||
|
|
||||||
Map<Integer, List<TextPageBlock>> headers = document.getHeaders()
|
|
||||||
.stream()
|
|
||||||
.map(ClassificationHeader::getTextBlocks)
|
|
||||||
.flatMap(List::stream)
|
|
||||||
.collect(groupingBy(AbstractPageBlock::getPage, toList()));
|
|
||||||
|
|
||||||
Map<Integer, List<TextPageBlock>> footers = document.getFooters()
|
|
||||||
.stream()
|
|
||||||
.map(ClassificationFooter::getTextBlocks)
|
|
||||||
.flatMap(List::stream)
|
|
||||||
.collect(groupingBy(AbstractPageBlock::getPage, toList()));
|
|
||||||
|
|
||||||
for (int pageIndex = 1; pageIndex <= document.getPages().size(); pageIndex++) {
|
|
||||||
if (headers.containsKey(pageIndex)) {
|
|
||||||
addHeader(headers.get(pageIndex), context);
|
|
||||||
} else {
|
|
||||||
addEmptyHeader(pageIndex, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int pageIndex = 1; pageIndex <= document.getPages().size(); pageIndex++) {
|
|
||||||
if (footers.containsKey(pageIndex)) {
|
|
||||||
addFooter(footers.get(pageIndex), context);
|
|
||||||
} else {
|
|
||||||
addEmptyFooter(pageIndex, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addFooter(List<TextPageBlock> textBlocks, Context context) {
|
|
||||||
|
|
||||||
Page page = context.getPage(textBlocks.get(0).getPage());
|
|
||||||
Footer footer = Footer.builder().documentTree(context.getDocumentTree()).build();
|
|
||||||
AtomicTextBlock textBlock = context.textBlockFactory.fromContext(TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX(textBlocks), footer, context, page);
|
|
||||||
List<Integer> tocId = context.getDocumentTree().createNewMainEntryAndReturnId(footer);
|
|
||||||
footer.setTreeId(tocId);
|
|
||||||
footer.setLeafTextBlock(textBlock);
|
|
||||||
page.setFooter(footer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void addHeader(List<TextPageBlock> textBlocks, Context context) {
|
|
||||||
|
|
||||||
Page page = context.getPage(textBlocks.get(0).getPage());
|
|
||||||
Header header = Header.builder().documentTree(context.getDocumentTree()).build();
|
|
||||||
AtomicTextBlock textBlock = context.textBlockFactory.fromNumberOnPage(TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX(textBlocks), header, 0, page);
|
|
||||||
List<Integer> tocId = context.getDocumentTree().createNewMainEntryAndReturnId(header);
|
|
||||||
header.setTreeId(tocId);
|
|
||||||
header.setLeafTextBlock(textBlock);
|
|
||||||
page.setHeader(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addEmptyFooter(int pageIndex, Context context) {
|
|
||||||
|
|
||||||
Page page = context.getPage(pageIndex);
|
|
||||||
Footer footer = Footer.builder().documentTree(context.getDocumentTree()).build();
|
|
||||||
AtomicTextBlock textBlock = context.textBlockFactory.emptyTextBlock(footer, context, page);
|
|
||||||
List<Integer> tocId = context.getDocumentTree().createNewMainEntryAndReturnId(footer);
|
|
||||||
footer.setTreeId(tocId);
|
|
||||||
footer.setLeafTextBlock(textBlock);
|
|
||||||
page.setFooter(footer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addEmptyHeader(int pageIndex, Context context) {
|
|
||||||
|
|
||||||
Page page = context.getPage(pageIndex);
|
|
||||||
Header header = Header.builder().documentTree(context.getDocumentTree()).build();
|
|
||||||
AtomicTextBlock textBlock = context.textBlockFactory.emptyTextBlockFromInteger(header, 0, page);
|
|
||||||
List<Integer> tocId = context.getDocumentTree().createNewMainEntryAndReturnId(header);
|
|
||||||
header.setTreeId(tocId);
|
|
||||||
header.setLeafTextBlock(textBlock);
|
|
||||||
page.setHeader(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
|
||||||
public final class Context {
|
|
||||||
|
|
||||||
DocumentTree documentTree;
|
|
||||||
Map<Page, Integer> pages;
|
|
||||||
List<Section> sections;
|
|
||||||
List<ClassifiedImage> images;
|
|
||||||
TextBlockFactory textBlockFactory;
|
|
||||||
|
|
||||||
|
|
||||||
public Context(Document document) {
|
|
||||||
|
|
||||||
documentTree = new DocumentTree(document);
|
|
||||||
pages = new HashMap<>();
|
|
||||||
sections = new LinkedList<>();
|
|
||||||
images = new LinkedList<>();
|
|
||||||
textBlockFactory = new TextBlockFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void buildAndAddPageWithCounter(ClassificationPage classificationPage) {
|
|
||||||
|
|
||||||
Page page = Page.fromClassificationPage(classificationPage);
|
|
||||||
//this counter counts the TextBlocks per page
|
|
||||||
//initial value is set to 1, because 0 is reserved for Header
|
|
||||||
pages.put(page, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int getAndIncrementTextBlockNumberOnPage(Page page) {
|
|
||||||
|
|
||||||
Integer textBlockNumberOnPage = pages.get(page);
|
|
||||||
pages.merge(page, 1, Integer::sum);
|
|
||||||
return textBlockNumberOnPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Page getPage(int pageIndex) {
|
|
||||||
|
|
||||||
return pages.keySet()
|
|
||||||
.stream()
|
|
||||||
.filter(page -> page.getNumber() == pageIndex)
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow(() -> new NoSuchElementException(format("ClassificationPage with number %d not found", pageIndex)));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.factory;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
@Getter
|
|
||||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
|
||||||
public class SearchTextWithTextPositionDto {
|
|
||||||
|
|
||||||
String searchText;
|
|
||||||
List<Integer> lineBreaks;
|
|
||||||
List<Integer> stringCoordsToPositionCoords;
|
|
||||||
List<Rectangle2D> positions;
|
|
||||||
|
|
||||||
|
|
||||||
public static SearchTextWithTextPositionDto empty() {
|
|
||||||
|
|
||||||
return SearchTextWithTextPositionDto.builder()
|
|
||||||
.searchText("")
|
|
||||||
.lineBreaks(Collections.emptyList())
|
|
||||||
.positions(Collections.emptyList())
|
|
||||||
.stringCoordsToPositionCoords(Collections.emptyList())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.factory;
|
|
||||||
|
|
||||||
import java.awt.geom.AffineTransform;
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.RedTextPosition;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextDirection;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class SearchTextWithTextPositionFactory {
|
|
||||||
|
|
||||||
public final int HEIGHT_PADDING = 2;
|
|
||||||
// when checking for a hyphen linebreak, we need to check after a linebreak if the last hyphen was less than three symbols away.
|
|
||||||
// We detect a linebreak as either a "\n" character or if two adjacent symbol's position differ in y-coordinates by at least one character height.
|
|
||||||
// If there is a hyphen linebreak, the hyphen will be 1 position in front of a "\n" or 2 positions in front of the character which has a lower y-coordinate
|
|
||||||
// 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 SearchTextWithTextPositionDto buildSearchTextToTextPositionModel(List<TextPositionSequence> sequences) {
|
|
||||||
|
|
||||||
if (sequences.isEmpty() || sequences.stream().allMatch(sequence -> sequence.getTextPositions().isEmpty())) {
|
|
||||||
return SearchTextWithTextPositionDto.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Context context = new Context();
|
|
||||||
|
|
||||||
RedTextPosition currentTextPosition = sequences.get(0).getTextPositions().get(0);
|
|
||||||
RedTextPosition previousTextPosition = RedTextPosition.builder().unicode(" ").position(currentTextPosition.getPosition()).build();
|
|
||||||
|
|
||||||
for (TextPositionSequence word : sequences) {
|
|
||||||
for (int i = 0; i < word.getTextPositions().size(); ++i) {
|
|
||||||
|
|
||||||
currentTextPosition = word.getTextPositions().get(i);
|
|
||||||
if (isLineBreak(currentTextPosition, previousTextPosition)) {
|
|
||||||
removeHyphenLinebreaks(context);
|
|
||||||
context.lineBreaksStringIdx.add(context.stringIdx);
|
|
||||||
}
|
|
||||||
if (!isRepeatedWhitespace(currentTextPosition.getUnicode(), previousTextPosition.getUnicode())) {
|
|
||||||
if (isHyphen(currentTextPosition.getUnicode())) {
|
|
||||||
context.lastHyphenIdx = context.stringIdx;
|
|
||||||
}
|
|
||||||
appendCurrentTextPosition(context, currentTextPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
previousTextPosition = currentTextPosition;
|
|
||||||
++context.positionIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
previousTextPosition = RedTextPosition.builder().unicode(" ").position(previousTextPosition.getPosition()).build();
|
|
||||||
context.stringBuilder.append(" ");
|
|
||||||
context.stringIdxToPositionIdx.add(context.positionIdx);
|
|
||||||
++context.stringIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert context.stringBuilder.length() == context.stringIdxToPositionIdx.size();
|
|
||||||
|
|
||||||
List<Rectangle2D> positions = sequences.stream()
|
|
||||||
.flatMap(sequence -> sequence.getTextPositions().stream().map(textPosition -> mapRedTextPositionToInitialUserSpace(textPosition, sequence)))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return SearchTextWithTextPositionDto.builder()
|
|
||||||
.searchText(context.stringBuilder.toString())
|
|
||||||
.lineBreaks(context.lineBreaksStringIdx)
|
|
||||||
.stringCoordsToPositionCoords(context.stringIdxToPositionIdx)
|
|
||||||
.positions(positions)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void appendCurrentTextPosition(Context context, RedTextPosition currentTextPosition) {
|
|
||||||
|
|
||||||
context.stringBuilder.append(currentTextPosition.getUnicode());
|
|
||||||
|
|
||||||
// unicode characters with more than 16-bit encoding have a length > 1 in java strings
|
|
||||||
for (int j = 0; j < currentTextPosition.getUnicode().length(); j++) {
|
|
||||||
context.stringIdxToPositionIdx.add(context.positionIdx);
|
|
||||||
}
|
|
||||||
context.stringIdx += currentTextPosition.getUnicode().length();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void removeHyphenLinebreaks(Context context) {
|
|
||||||
|
|
||||||
if (lastHyphenDirectlyBeforeLineBreak(context)) {
|
|
||||||
context.stringBuilder.delete(context.lastHyphenIdx, context.stringBuilder.length());
|
|
||||||
context.stringIdxToPositionIdx = context.stringIdxToPositionIdx.subList(0, context.lastHyphenIdx);
|
|
||||||
context.stringIdx = context.lastHyphenIdx;
|
|
||||||
context.lastHyphenIdx = -MAX_HYPHEN_LINEBREAK_DISTANCE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean lastHyphenDirectlyBeforeLineBreak(Context context) {
|
|
||||||
|
|
||||||
return context.stringIdx - context.lastHyphenIdx < MAX_HYPHEN_LINEBREAK_DISTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isLineBreak(RedTextPosition currentTextPosition, RedTextPosition previousTextPosition) {
|
|
||||||
|
|
||||||
return Objects.equals(currentTextPosition.getUnicode(), "\n") || isDeltaYLargerThanTextHeight(currentTextPosition, previousTextPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isDeltaYLargerThanTextHeight(RedTextPosition currentPosition, RedTextPosition previousPosition) {
|
|
||||||
|
|
||||||
if (previousPosition == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
float deltaY = Math.abs(currentPosition.getYDirAdj() - previousPosition.getYDirAdj());
|
|
||||||
return deltaY >= currentPosition.getHeightDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isRepeatedWhitespace(String currentUnicode, String previousUnicode) {
|
|
||||||
|
|
||||||
return Objects.equals(previousUnicode, " ") && Objects.equals(currentUnicode, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isHyphen(String unicodeCharacter) {
|
|
||||||
|
|
||||||
return Objects.equals(unicodeCharacter, "-") || //
|
|
||||||
Objects.equals(unicodeCharacter, "~") || //
|
|
||||||
Objects.equals(unicodeCharacter, "‐") || //
|
|
||||||
Objects.equals(unicodeCharacter, "‒") || //
|
|
||||||
Objects.equals(unicodeCharacter, "⁻") || //
|
|
||||||
Objects.equals(unicodeCharacter, "−") || //
|
|
||||||
Objects.equals(unicodeCharacter, "﹣") || //
|
|
||||||
Objects.equals(unicodeCharacter, "゠") || //
|
|
||||||
Objects.equals(unicodeCharacter, "⁓") || //
|
|
||||||
Objects.equals(unicodeCharacter, "‑") || //
|
|
||||||
Objects.equals(unicodeCharacter, "\u00AD");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Rectangle2D mapRedTextPositionToInitialUserSpace(RedTextPosition textPosition, TextPositionSequence sequence) {
|
|
||||||
|
|
||||||
float textHeight = sequence.getTextHeight() + HEIGHT_PADDING;
|
|
||||||
Rectangle2D rectangle2D = new Rectangle2D.Double(textPosition.getXDirAdj(),
|
|
||||||
textPosition.getYDirAdj() - textHeight,
|
|
||||||
textPosition.getWidthDirAdj(),
|
|
||||||
textHeight + HEIGHT_PADDING);
|
|
||||||
|
|
||||||
AffineTransform transform = new AffineTransform();
|
|
||||||
|
|
||||||
if (sequence.getDir() == TextDirection.ZERO || sequence.getDir() == TextDirection.HALF_CIRCLE) {
|
|
||||||
transform.rotate(sequence.getDir().getRadians(), sequence.getPageWidth() / 2f, sequence.getPageHeight() / 2f);
|
|
||||||
transform.translate(0f, sequence.getPageHeight());
|
|
||||||
} else if (sequence.getDir() == TextDirection.QUARTER_CIRCLE) {
|
|
||||||
transform.rotate(sequence.getDir().getRadians(), sequence.getPageWidth() / 2f, sequence.getPageWidth() / 2f);
|
|
||||||
transform.translate(0f, sequence.getPageWidth());
|
|
||||||
} else {
|
|
||||||
transform.rotate(sequence.getDir().getRadians(), sequence.getPageHeight() / 2f, sequence.getPageHeight() / 2f);
|
|
||||||
transform.translate(0f, sequence.getPageWidth());
|
|
||||||
}
|
|
||||||
transform.scale(1., -1.);
|
|
||||||
|
|
||||||
return transform.createTransformedShape(rectangle2D).getBounds2D();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class Context {
|
|
||||||
|
|
||||||
List<Integer> stringIdxToPositionIdx = new LinkedList<>();
|
|
||||||
List<Integer> lineBreaksStringIdx = new LinkedList<>();
|
|
||||||
StringBuilder stringBuilder = new StringBuilder();
|
|
||||||
|
|
||||||
int stringIdx;
|
|
||||||
int positionIdx;
|
|
||||||
|
|
||||||
int lastHyphenIdx = -MAX_HYPHEN_LINEBREAK_DISTANCE;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,204 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.factory;
|
|
||||||
|
|
||||||
import static java.lang.String.format;
|
|
||||||
import static java.util.Collections.emptyList;
|
|
||||||
import static java.util.stream.Collectors.groupingBy;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.GenericSemanticNode;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Section;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image.ClassifiedImage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.TablePageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.utils.TableMergingUtility;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class SectionNodeFactory {
|
|
||||||
|
|
||||||
public void addSection(GenericSemanticNode parentNode, List<AbstractPageBlock> pageBlocks, List<ClassifiedImage> images, DocumentGraphFactory.Context context) {
|
|
||||||
|
|
||||||
if (pageBlocks.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Map<Integer, List<AbstractPageBlock>> blocksPerPage = pageBlocks.stream().collect(groupingBy(AbstractPageBlock::getPage));
|
|
||||||
Section section = Section.builder().documentTree(context.getDocumentTree()).build();
|
|
||||||
|
|
||||||
context.getSections().add(section);
|
|
||||||
blocksPerPage.keySet().forEach(pageNumber -> addSectionNodeToPageNode(context, section, pageNumber));
|
|
||||||
|
|
||||||
section.setTreeId(getTreeId(parentNode, context, section));
|
|
||||||
|
|
||||||
addFirstHeadlineDirectlyToSection(pageBlocks, context, section);
|
|
||||||
if (containsTablesAndTextBlocks(pageBlocks)) {
|
|
||||||
splitPageBlocksIntoSubSections(pageBlocks).forEach(subSectionPageBlocks -> addSection(section, subSectionPageBlocks, emptyList(), context));
|
|
||||||
} else {
|
|
||||||
addTablesAndParagraphsAndHeadlinesToSection(pageBlocks, context, section);
|
|
||||||
}
|
|
||||||
|
|
||||||
images.stream().distinct().forEach(image -> DocumentGraphFactory.addImage(section, image, context));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Integer> getTreeId(GenericSemanticNode parentNode, DocumentGraphFactory.Context context, Section section) {
|
|
||||||
|
|
||||||
if (parentNode == null) {
|
|
||||||
return context.getDocumentTree().createNewMainEntryAndReturnId(section);
|
|
||||||
} else {
|
|
||||||
return context.getDocumentTree().createNewChildEntryAndReturnId(parentNode, section);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addFirstHeadlineDirectlyToSection(List<AbstractPageBlock> pageBlocks, DocumentGraphFactory.Context context, Section section) {
|
|
||||||
|
|
||||||
if (pageBlocks.get(0).isHeadline()) {
|
|
||||||
addTablesAndParagraphsAndHeadlinesToSection(List.of(pageBlocks.get(0)), context, section);
|
|
||||||
pageBlocks.remove(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addTablesAndParagraphsAndHeadlinesToSection(List<AbstractPageBlock> pageBlocks, DocumentGraphFactory.Context context, Section section) {
|
|
||||||
|
|
||||||
Set<AbstractPageBlock> alreadyMerged = new HashSet<>();
|
|
||||||
List<AbstractPageBlock> remainingBlocks = new LinkedList<>(pageBlocks);
|
|
||||||
for (AbstractPageBlock abstractPageBlock : pageBlocks) {
|
|
||||||
|
|
||||||
if (alreadyMerged.contains(abstractPageBlock)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingBlocks.removeAll(alreadyMerged);
|
|
||||||
|
|
||||||
if (abstractPageBlock instanceof TextPageBlock) {
|
|
||||||
// List<TextPageBlock> textBlocksToMerge = findTextBlocksWithSameClassificationAndAlignsYAndSameOrientationUntilConvergence((TextPageBlock) abstractPageBlock, remainingBlocks);
|
|
||||||
List<TextPageBlock> textBlocksToMerge = findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(List.of((TextPageBlock) abstractPageBlock),
|
|
||||||
remainingBlocks);
|
|
||||||
alreadyMerged.addAll(textBlocksToMerge);
|
|
||||||
DocumentGraphFactory.addParagraphOrHeadline(section, (TextPageBlock) abstractPageBlock, context, textBlocksToMerge);
|
|
||||||
} else if (abstractPageBlock instanceof TablePageBlock tablePageBlock) {
|
|
||||||
List<TablePageBlock> tablesToMerge = TableMergingUtility.findConsecutiveTablesWithSameColCountAndSameHeaders(tablePageBlock, remainingBlocks);
|
|
||||||
alreadyMerged.addAll(tablesToMerge);
|
|
||||||
TableNodeFactory.addTable(section, tablesToMerge, context);
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException(format("Unhandled AbstractPageBlockType %s!", abstractPageBlock.getClass()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean containsTablesAndTextBlocks(List<AbstractPageBlock> pageBlocks) {
|
|
||||||
|
|
||||||
return pageBlocks.stream().anyMatch(pageBlock -> pageBlock instanceof TablePageBlock) && pageBlocks.stream().anyMatch(pageBlock -> pageBlock instanceof TextPageBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function splits the list of PageBlocks around TablePageBlocks, such that SubSections can be created, that don't include tables.
|
|
||||||
* This is needed so we can execute rules on sections, that do not contain tables.
|
|
||||||
* See: <a href="https://knecon.atlassian.net/wiki/spaces/RED/pages/14765218/Document+Structure">document structure wiki</a>
|
|
||||||
*
|
|
||||||
* @param pageBlocks a List of AbstractPageBlocks, which have at least one TablePageBlock and one ClassificationTextBlock
|
|
||||||
* @return List of Lists of AbstractPageBlocks, which include either a single Headline ClassificationTextBlock and a TablePageBlock or only ClassificationTextBlocks.
|
|
||||||
*/
|
|
||||||
private List<List<AbstractPageBlock>> splitPageBlocksIntoSubSections(List<AbstractPageBlock> pageBlocks) {
|
|
||||||
|
|
||||||
List<List<AbstractPageBlock>> splitList = splitIntoCoherentList(pageBlocks);
|
|
||||||
movePrecedingHeadlineToTableList(splitList);
|
|
||||||
return splitList.stream().filter(list -> !list.isEmpty()).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void movePrecedingHeadlineToTableList(List<List<AbstractPageBlock>> splitList) {
|
|
||||||
|
|
||||||
for (int i = 0; i < splitList.size(); i++) {
|
|
||||||
if (listIsTablesOnly(splitList.get(i)) && i > 0) {
|
|
||||||
List<AbstractPageBlock> previousList = splitList.get(i - 1);
|
|
||||||
AbstractPageBlock lastPageBlockInPreviousList = previousList.get(previousList.size() - 1);
|
|
||||||
if (lastPageBlockInPreviousList.isHeadline()) {
|
|
||||||
previousList.remove(previousList.size() - 1);
|
|
||||||
splitList.get(i).add(0, lastPageBlockInPreviousList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean listIsTablesOnly(List<AbstractPageBlock> abstractPageBlocks) {
|
|
||||||
|
|
||||||
return abstractPageBlocks.stream().allMatch(abstractPageBlock -> abstractPageBlock instanceof TablePageBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param pageBlocks a List of AbstractPageBlocks, which have at least one TablePageBlock and one ClassificationTextBlock
|
|
||||||
* @return List of Lists of AbstractPageBlocks, which are exclusively of type ClassificationTextBlock or TablePageBlock
|
|
||||||
*/
|
|
||||||
private List<List<AbstractPageBlock>> splitIntoCoherentList(List<AbstractPageBlock> pageBlocks) {
|
|
||||||
|
|
||||||
List<List<AbstractPageBlock>> splitList = new LinkedList<>();
|
|
||||||
List<AbstractPageBlock> currentList = new LinkedList<>();
|
|
||||||
splitList.add(currentList);
|
|
||||||
|
|
||||||
Class<? extends AbstractPageBlock> lastPageBlockClass = pageBlocks.get(0).getClass();
|
|
||||||
for (AbstractPageBlock pageBlock : pageBlocks) {
|
|
||||||
if (lastPageBlockClass.isInstance(pageBlock)) {
|
|
||||||
currentList.add(pageBlock);
|
|
||||||
} else {
|
|
||||||
currentList = new LinkedList<>();
|
|
||||||
currentList.add(pageBlock);
|
|
||||||
splitList.add(currentList);
|
|
||||||
lastPageBlockClass = pageBlock.getClass();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return splitList;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("PMD")
|
|
||||||
// experimental feature to be used later
|
|
||||||
private List<TextPageBlock> findTextBlocksWithSameClassificationAndAlignsYAndSameOrientationUntilConvergence(TextPageBlock originalTextBlocks,
|
|
||||||
List<AbstractPageBlock> pageBlocks) {
|
|
||||||
|
|
||||||
int previousCount = 1;
|
|
||||||
List<TextPageBlock> alignedBlocks = findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(List.of(originalTextBlocks), pageBlocks);
|
|
||||||
while (previousCount < alignedBlocks.size()) {
|
|
||||||
alignedBlocks = findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(alignedBlocks, pageBlocks);
|
|
||||||
previousCount = alignedBlocks.size();
|
|
||||||
}
|
|
||||||
return alignedBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static List<TextPageBlock> findTextBlocksWithSameClassificationAndAlignsYAndSameOrientation(List<TextPageBlock> textBlocksToMerge, List<AbstractPageBlock> pageBlocks) {
|
|
||||||
|
|
||||||
return Stream.concat(pageBlocks.stream()
|
|
||||||
.filter(abstractPageBlock -> !textBlocksToMerge.contains(abstractPageBlock))
|
|
||||||
.filter(abstractPageBlock -> textBlocksToMerge.stream().allMatch(textBlockToMerge -> abstractPageBlock.getPage() == textBlockToMerge.getPage()))
|
|
||||||
.filter(abstractPageBlock -> textBlocksToMerge.stream().allMatch(textBlockToMerge -> abstractPageBlock.getOrientation().equals(textBlockToMerge.getOrientation())))
|
|
||||||
.filter(abstractPageBlock -> textBlocksToMerge.stream().anyMatch(abstractPageBlock::intersectsY))
|
|
||||||
//.filter(abstractPageBlock -> textBlocksToMerge.stream().anyMatch(abstractPageBlock::intersectsX))
|
|
||||||
.filter(abstractPageBlock -> abstractPageBlock instanceof TextPageBlock)
|
|
||||||
.map(abstractPageBlock -> (TextPageBlock) abstractPageBlock), //
|
|
||||||
textBlocksToMerge.stream())//
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addSectionNodeToPageNode(DocumentGraphFactory.Context context, Section section, Integer pageNumber) {
|
|
||||||
|
|
||||||
Page page = context.getPage(pageNumber);
|
|
||||||
page.getMainBody().add(section);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.factory;
|
|
||||||
|
|
||||||
import static java.util.Collections.emptyList;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.GenericSemanticNode;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Cell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.TablePageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Table;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableCell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.utils.TextPositionOperations;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class TableNodeFactory {
|
|
||||||
|
|
||||||
public final double TABLE_CELL_MERGE_CONTENTS_SIZE_THRESHOLD = 0.05;
|
|
||||||
|
|
||||||
|
|
||||||
public void addTable(GenericSemanticNode parentNode, List<TablePageBlock> tablesToMerge, DocumentGraphFactory.Context context) {
|
|
||||||
|
|
||||||
setPageNumberInCells(tablesToMerge);
|
|
||||||
Set<Page> pages = tablesToMerge.stream().map(AbstractPageBlock::getPage).map(context::getPage).collect(Collectors.toSet());
|
|
||||||
List<List<Cell>> mergedRows = tablesToMerge.stream().map(TablePageBlock::getRows).flatMap(Collection::stream).toList();
|
|
||||||
|
|
||||||
Table table = Table.builder()
|
|
||||||
.documentTree(context.getDocumentTree())
|
|
||||||
.numberOfCols(mergedRows.isEmpty() ? 0 : mergedRows.get(0).size())
|
|
||||||
.numberOfRows(mergedRows.size())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
pages.forEach(page -> addTableToPage(page, parentNode, table));
|
|
||||||
|
|
||||||
List<Integer> treeId = context.getDocumentTree().createNewChildEntryAndReturnId(parentNode, table);
|
|
||||||
table.setTreeId(treeId);
|
|
||||||
addTableCells(mergedRows, table, context);
|
|
||||||
|
|
||||||
ifTableHasNoHeadersSetFirstRowAsHeaders(table);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void setPageNumberInCells(List<TablePageBlock> tablesToMerge) {
|
|
||||||
|
|
||||||
// For some reason I can't figure out, in some table cells, the ClassificationTextBlocks have 0 as page number
|
|
||||||
// So I am fixing this here, but this should actually be fixed upstream.
|
|
||||||
tablesToMerge.forEach(table -> table.getRows()
|
|
||||||
.stream()
|
|
||||||
.flatMap(Collection::stream)
|
|
||||||
.peek(cell -> cell.setPageNumber(table.getPage()))
|
|
||||||
.forEach(cell -> setPageNumberInTextBlocksWithPageNumberSetTo0(table, cell)));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void setPageNumberInTextBlocksWithPageNumberSetTo0(TablePageBlock table, Cell cell) {
|
|
||||||
|
|
||||||
cell.getTextBlocks().stream()//
|
|
||||||
.filter(tb -> tb.getPage() == 0)//
|
|
||||||
.forEach(tb -> tb.setPage(table.getPage()));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("PMD.UnusedPrivateMethod") // PMD actually flags this wrong
|
|
||||||
private void addTableToPage(Page page, SemanticNode parentNode, Table table) {
|
|
||||||
|
|
||||||
if (!page.getMainBody().contains(parentNode)) {
|
|
||||||
parentNode.getPages().add(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
page.getMainBody().add(table);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void ifTableHasNoHeadersSetFirstRowAsHeaders(Table table) {
|
|
||||||
|
|
||||||
if (table.streamHeaders().findAny().isEmpty()) {
|
|
||||||
table.streamRow(0).forEach(tableCellNode -> tableCellNode.setHeader(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addTableCells(List<List<Cell>> rows, Table table, DocumentGraphFactory.Context context) {
|
|
||||||
|
|
||||||
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
|
|
||||||
for (int colIndex = 0; colIndex < rows.get(rowIndex).size(); colIndex++) {
|
|
||||||
addTableCell(rows.get(rowIndex).get(colIndex), rowIndex, colIndex, table, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("PMD.UnusedPrivateMethod") // PMD actually flags this wrong
|
|
||||||
private void addTableCell(Cell cell, int rowIndex, int colIndex, Table tableNode, DocumentGraphFactory.Context context) {
|
|
||||||
|
|
||||||
Page page = context.getPage(cell.getPageNumber());
|
|
||||||
|
|
||||||
TableCell tableCell = TableCell.builder().documentTree(context.getDocumentTree()).row(rowIndex).col(colIndex).header(cell.isHeaderCell()).bBox(cell.getBounds2D()).build();
|
|
||||||
page.getMainBody().add(tableCell);
|
|
||||||
|
|
||||||
List<Integer> treeId = context.getDocumentTree().createNewTableChildEntryAndReturnId(tableNode, tableCell);
|
|
||||||
tableCell.setTreeId(treeId);
|
|
||||||
|
|
||||||
TextBlock textBlock;
|
|
||||||
if (cell.getTextBlocks().isEmpty()) {
|
|
||||||
tableCell.setLeafTextBlock(context.getTextBlockFactory().emptyTextBlock(tableNode, context, page));
|
|
||||||
} else if (cell.getTextBlocks().size() == 1) {
|
|
||||||
textBlock = context.getTextBlockFactory().fromContext(cell.getTextBlocks().get(0).getSequences(), tableCell, context, page);
|
|
||||||
tableCell.setLeafTextBlock(textBlock);
|
|
||||||
} else if (firstTextBlockIsHeadline(cell)) {
|
|
||||||
SectionNodeFactory.addSection(tableCell, cell.getTextBlocks().stream().map(tb -> (AbstractPageBlock) tb).toList(), emptyList(), context);
|
|
||||||
} else if (cellAreaIsSmallerThanPageAreaTimesThreshold(cell, page)) {
|
|
||||||
List<TextPositionSequence> sequences = TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX(cell.getTextBlocks());
|
|
||||||
textBlock = context.getTextBlockFactory().fromContext(sequences, tableCell, context, page);
|
|
||||||
tableCell.setLeafTextBlock(textBlock);
|
|
||||||
} else {
|
|
||||||
cell.getTextBlocks().forEach(tb -> DocumentGraphFactory.addParagraphOrHeadline(tableCell, tb, context, List.of(tb)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean cellAreaIsSmallerThanPageAreaTimesThreshold(Cell cell, Page page) {
|
|
||||||
|
|
||||||
return cell.getArea() < TABLE_CELL_MERGE_CONTENTS_SIZE_THRESHOLD * page.getHeight() * page.getWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean firstTextBlockIsHeadline(Cell cell) {
|
|
||||||
|
|
||||||
return cell.getTextBlocks().get(0).isHeadline();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.factory;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class TextBlockFactory {
|
|
||||||
|
|
||||||
int stringOffset;
|
|
||||||
long textBlockIdx;
|
|
||||||
|
|
||||||
|
|
||||||
public AtomicTextBlock fromContext(List<TextPositionSequence> sequences, SemanticNode parent, DocumentGraphFactory.Context context, Page page) {
|
|
||||||
|
|
||||||
Integer numberOnPage = context.getAndIncrementTextBlockNumberOnPage(page);
|
|
||||||
return fromNumberOnPage(sequences, parent, numberOnPage, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public AtomicTextBlock fromNumberOnPage(List<TextPositionSequence> sequences, SemanticNode parent, Integer numberOnPage, Page page) {
|
|
||||||
|
|
||||||
SearchTextWithTextPositionDto searchTextWithTextPositionDto = SearchTextWithTextPositionFactory.buildSearchTextToTextPositionModel(sequences);
|
|
||||||
int offset = stringOffset;
|
|
||||||
stringOffset += searchTextWithTextPositionDto.getSearchText().length();
|
|
||||||
long idx = textBlockIdx;
|
|
||||||
textBlockIdx++;
|
|
||||||
return AtomicTextBlock.fromSearchTextWithTextPositionDto(searchTextWithTextPositionDto, parent, offset, idx, numberOnPage, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public AtomicTextBlock emptyTextBlock(SemanticNode parent, DocumentGraphFactory.Context context, Page page) {
|
|
||||||
|
|
||||||
long idx = textBlockIdx;
|
|
||||||
textBlockIdx++;
|
|
||||||
return AtomicTextBlock.empty(idx, stringOffset, page, context.getAndIncrementTextBlockNumberOnPage(page), parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public AtomicTextBlock emptyTextBlockFromInteger(SemanticNode parent, Integer numberOnPage, Page page) {
|
|
||||||
|
|
||||||
long idx = textBlockIdx;
|
|
||||||
textBlockIdx++;
|
|
||||||
return AtomicTextBlock.empty(idx, stringOffset, page, numberOnPage, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -6,7 +6,6 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.entity.RedactionEntity;
|
import com.iqser.red.service.redaction.v1.server.document.graph.entity.RedactionEntity;
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
|
||||||
|
|
||||||
@ -48,18 +47,6 @@ public class Page {
|
|||||||
Set<Image> images = new HashSet<>();
|
Set<Image> images = new HashSet<>();
|
||||||
|
|
||||||
|
|
||||||
public static Page fromClassificationPage(ClassificationPage classificationPage) {
|
|
||||||
|
|
||||||
return Page.builder()
|
|
||||||
.height((int) classificationPage.getPageHeight())
|
|
||||||
.width((int) classificationPage.getPageWidth())
|
|
||||||
.number(classificationPage.getPageNumber())
|
|
||||||
.rotation(classificationPage.getRotation())
|
|
||||||
.mainBody(new LinkedList<>())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TextBlock getMainBodyTextBlock() {
|
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());
|
||||||
|
|||||||
@ -12,13 +12,12 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.factory.SearchTextWithTextPositionDto;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.utils.RectangleTransformations;
|
import com.iqser.red.service.redaction.v1.server.document.utils.RectangleTransformations;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.AtomicPositionBlockData;
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionData;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.AtomicTextBlockData;
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentTextData;
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@ -58,28 +57,6 @@ public class AtomicTextBlock implements TextBlock {
|
|||||||
return lineBreaks.size() + 1;
|
return lineBreaks.size() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static AtomicTextBlock fromSearchTextWithTextPositionDto(SearchTextWithTextPositionDto searchTextWithTextPositionDto,
|
|
||||||
SemanticNode parent,
|
|
||||||
int stringOffset,
|
|
||||||
Long textBlockIdx,
|
|
||||||
Integer numberOnPage,
|
|
||||||
Page page) {
|
|
||||||
|
|
||||||
return AtomicTextBlock.builder()
|
|
||||||
.id(textBlockIdx)
|
|
||||||
.parent(parent)
|
|
||||||
.searchText(searchTextWithTextPositionDto.getSearchText())
|
|
||||||
.numberOnPage(numberOnPage)
|
|
||||||
.page(page)
|
|
||||||
.lineBreaks(searchTextWithTextPositionDto.getLineBreaks())
|
|
||||||
.positions(searchTextWithTextPositionDto.getPositions())
|
|
||||||
.stringIdxToPositionIdx(searchTextWithTextPositionDto.getStringCoordsToPositionCoords())
|
|
||||||
.boundary(new Boundary(stringOffset, stringOffset + searchTextWithTextPositionDto.getSearchText().length()))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static AtomicTextBlock empty(Long textBlockIdx, int stringOffset, Page page, int numberOnPage, SemanticNode parent) {
|
public static AtomicTextBlock empty(Long textBlockIdx, int stringOffset, Page page, int numberOnPage, SemanticNode parent) {
|
||||||
|
|
||||||
return AtomicTextBlock.builder()
|
return AtomicTextBlock.builder()
|
||||||
@ -96,8 +73,8 @@ public class AtomicTextBlock implements TextBlock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static AtomicTextBlock fromAtomicTextBlockData(AtomicTextBlockData atomicTextBlockData,
|
public static AtomicTextBlock fromAtomicTextBlockData(DocumentTextData atomicTextBlockData,
|
||||||
AtomicPositionBlockData atomicPositionBlockData,
|
DocumentPositionData atomicPositionBlockData,
|
||||||
SemanticNode parent,
|
SemanticNode parent,
|
||||||
Page page) {
|
Page page) {
|
||||||
|
|
||||||
|
|||||||
@ -12,11 +12,9 @@ import java.util.function.Function;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collector;
|
import java.util.stream.Collector;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|
||||||
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@ -24,27 +22,6 @@ import lombok.NoArgsConstructor;
|
|||||||
|
|
||||||
public class RectangleTransformations {
|
public class RectangleTransformations {
|
||||||
|
|
||||||
public static PDRectangle toPDRectangleBBox(List<Rectangle> rectangles) {
|
|
||||||
|
|
||||||
Rectangle2D rectangle2D = RectangleTransformations.rectangleBBox(rectangles);
|
|
||||||
|
|
||||||
PDRectangle annotationPosition = new PDRectangle();
|
|
||||||
annotationPosition.setLowerLeftX((float) rectangle2D.getMinX());
|
|
||||||
annotationPosition.setLowerLeftY((float) rectangle2D.getMinY());
|
|
||||||
annotationPosition.setUpperRightX((float) rectangle2D.getMaxX());
|
|
||||||
annotationPosition.setUpperRightY((float) rectangle2D.getMaxY());
|
|
||||||
return annotationPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Rectangle2D abstractPageBlockBBox(List<AbstractPageBlock> abstractPageBlocks) {
|
|
||||||
|
|
||||||
return abstractPageBlocks.stream()
|
|
||||||
.map(abstractPageBlock -> new Rectangle2D.Double(abstractPageBlock.getMinX(),
|
|
||||||
abstractPageBlock.getMinY(),
|
|
||||||
abstractPageBlock.getWidth(),
|
|
||||||
abstractPageBlock.getHeight())).collect(new Rectangle2DBBoxCollector());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static Rectangle2D atomicTextBlockBBox(List<AtomicTextBlock> atomicTextBlocks) {
|
public static Rectangle2D atomicTextBlockBBox(List<AtomicTextBlock> atomicTextBlocks) {
|
||||||
|
|
||||||
@ -114,12 +91,6 @@ public class RectangleTransformations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static Rectangle2D toRectangle2D(PDRectangle cropBox) {
|
|
||||||
|
|
||||||
return new Rectangle2D.Double(cropBox.getLowerLeftX(), cropBox.getLowerLeftY(), cropBox.getWidth(), cropBox.getHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static class Rectangle2DBBoxCollector implements Collector<Rectangle2D, Rectangle2DBBoxCollector.BBox, Rectangle2D> {
|
private static class Rectangle2DBBoxCollector implements Collector<Rectangle2D, Rectangle2DBBoxCollector.BBox, Rectangle2D> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.utils;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Cell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.TablePageBlock;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class TableMergingUtility {
|
|
||||||
|
|
||||||
private static final double TABLE_ALIGNMENT_THRESHOLD = 2d;
|
|
||||||
|
|
||||||
|
|
||||||
public List<TablePageBlock> findConsecutiveTablesWithSameColCountAndSameHeaders(TablePageBlock originalTablePageBlock, List<AbstractPageBlock> pageBlocks) {
|
|
||||||
|
|
||||||
List<TablePageBlock> consecutiveTables = pageBlocks.stream()
|
|
||||||
.map(abstractPageBlock -> (TablePageBlock) abstractPageBlock)
|
|
||||||
.filter(tablePageBlock -> !tablePageBlock.equals(originalTablePageBlock))
|
|
||||||
.toList();
|
|
||||||
assert consecutiveTables.size() == pageBlocks.size() - 1;
|
|
||||||
|
|
||||||
List<TablePageBlock> consecutiveTablesWithSameColCountAndHeaders = new LinkedList<>();
|
|
||||||
for (TablePageBlock consecutiveTable : consecutiveTables) {
|
|
||||||
if (consecutiveTable.getColCount() == originalTablePageBlock.getColCount() && !hasTableHeader(consecutiveTable) && outerBoundaryAlignsX(originalTablePageBlock,
|
|
||||||
consecutiveTable)) {
|
|
||||||
consecutiveTablesWithSameColCountAndHeaders.add(consecutiveTable);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Stream.concat(Stream.of(originalTablePageBlock), consecutiveTablesWithSameColCountAndHeaders.stream()).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static boolean outerBoundaryAlignsX(TablePageBlock originalTablePageBlock, TablePageBlock consecutiveTable) {
|
|
||||||
|
|
||||||
return Math.abs(consecutiveTable.getMinX() - originalTablePageBlock.getMinX()) < TABLE_ALIGNMENT_THRESHOLD && Math.abs(consecutiveTable.getMaxX() - originalTablePageBlock.getMaxX()) < TABLE_ALIGNMENT_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean hasTableHeader(TablePageBlock table) {
|
|
||||||
|
|
||||||
return table.getRows().stream().flatMap(Collection::stream).anyMatch(Cell::isHeaderCell);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.utils;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.util.QuickSort;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
|
|
||||||
public class TextPositionOperations {
|
|
||||||
|
|
||||||
private static final TextPositionSequenceComparator comparator = new TextPositionSequenceComparator();
|
|
||||||
|
|
||||||
|
|
||||||
public static List<TextPositionSequence> mergeAndSortTextPositionSequenceByYThenX(List<TextPageBlock> textBlocks) {
|
|
||||||
|
|
||||||
var sequence = textBlocks.stream().flatMap(tb -> tb.getSequences().stream()).collect(Collectors.toList());
|
|
||||||
|
|
||||||
// because the TextPositionSequenceComparator is not transitive, but
|
|
||||||
// JDK7+ enforces transitivity on comparators, we need to use
|
|
||||||
// a custom quicksort implementation (which is slower, unfortunately).
|
|
||||||
QuickSort.sort(sequence, comparator);
|
|
||||||
return sequence;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.iqser.red.service.redaction.v1.server.document.utils;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is a comparator for TextPosition operators. It handles
|
|
||||||
* pages with text in different directions by grouping the text based
|
|
||||||
* on direction and sorting in that direction. This allows continuous text
|
|
||||||
* in a given direction to be more easily grouped together.
|
|
||||||
*
|
|
||||||
* @author Ben Litchfield
|
|
||||||
*/
|
|
||||||
public class TextPositionSequenceComparator implements Comparator<TextPositionSequence>
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public int compare(TextPositionSequence pos1, TextPositionSequence pos2)
|
|
||||||
{
|
|
||||||
// only compare text that is in the same direction
|
|
||||||
int cmp1 = Float.compare(pos1.getDir().getDegrees(), pos2.getDir().getDegrees());
|
|
||||||
if (cmp1 != 0)
|
|
||||||
{
|
|
||||||
return cmp1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the text direction adjusted coordinates
|
|
||||||
float x1 = pos1.getMinXDirAdj();
|
|
||||||
float x2 = pos2.getMinXDirAdj();
|
|
||||||
|
|
||||||
float pos1YBottom = pos1.getMaxYDirAdj();
|
|
||||||
float pos2YBottom = pos2.getMaxYDirAdj();
|
|
||||||
|
|
||||||
// note that the coordinates have been adjusted so 0,0 is in upper left
|
|
||||||
float pos1YTop = pos1YBottom - pos1.getTextHeight();
|
|
||||||
float pos2YTop = pos2YBottom - pos2.getTextHeight();
|
|
||||||
|
|
||||||
float yDifference = Math.abs(pos1YBottom - pos2YBottom);
|
|
||||||
|
|
||||||
// we will do a simple tolerance comparison
|
|
||||||
if (yDifference < .1 ||
|
|
||||||
pos2YBottom >= pos1YTop && pos2YBottom <= pos1YBottom ||
|
|
||||||
pos1YBottom >= pos2YTop && pos1YBottom <= pos2YBottom)
|
|
||||||
{
|
|
||||||
return Float.compare(x1, x2);
|
|
||||||
}
|
|
||||||
else if (pos1YBottom < pos2YBottom)
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image.ImageServiceResponse;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image.ClassifiedImage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ImageType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ImageServiceResponseAdapter {
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
private final RedactionStorageService redactionStorageService;
|
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
public Map<Integer, List<ClassifiedImage>> convertImages(String dossierId, String fileId) {
|
|
||||||
|
|
||||||
var imageClassificationStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(dossierId, fileId, FileType.IMAGE_INFO));
|
|
||||||
|
|
||||||
ImageServiceResponse imageServiceResponse = objectMapper.readValue(imageClassificationStream, ImageServiceResponse.class);
|
|
||||||
|
|
||||||
Map<Integer, List<ClassifiedImage>> images = new HashMap<>();
|
|
||||||
imageServiceResponse.getData().forEach(imageMetadata -> {
|
|
||||||
var classification = imageMetadata.getFilters().isAllPassed() ? ImageType.valueOf(imageMetadata.getClassification()
|
|
||||||
.getLabel()
|
|
||||||
.toUpperCase(Locale.ROOT)) : ImageType.OTHER;
|
|
||||||
images.computeIfAbsent(imageMetadata.getPosition().getPageNumber(), x -> new ArrayList<>())
|
|
||||||
.add(new ClassifiedImage(new Rectangle2D.Double(imageMetadata.getPosition().getX1(),
|
|
||||||
imageMetadata.getPosition().getY1(),
|
|
||||||
imageMetadata.getGeometry().getWidth(),
|
|
||||||
imageMetadata.getGeometry().getHeight()), classification, imageMetadata.isAlpha(), imageMetadata.getPosition().getPageNumber()));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Currently This is a copy but, it will be changed later because i don' t think that we should unclassified images.
|
|
||||||
imageServiceResponse.getDataCV().forEach(imageMetadata -> {
|
|
||||||
var classification = imageMetadata.getFilters().isAllPassed() ? ImageType.valueOf(imageMetadata.getClassification()
|
|
||||||
.getLabel()
|
|
||||||
.toUpperCase(Locale.ROOT)) : ImageType.OTHER;
|
|
||||||
images.computeIfAbsent(imageMetadata.getPosition().getPageNumber(), x -> new ArrayList<>())
|
|
||||||
.add(new ClassifiedImage(new Rectangle2D.Double(imageMetadata.getPosition().getX1(),
|
|
||||||
imageMetadata.getPosition().getY1(),
|
|
||||||
imageMetadata.getGeometry().getWidth(),
|
|
||||||
imageMetadata.getGeometry().getHeight()), classification, imageMetadata.isAlpha(), imageMetadata.getPosition().getPageNumber()));
|
|
||||||
});
|
|
||||||
|
|
||||||
return images;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void findOcr(ClassificationPage page) {
|
|
||||||
|
|
||||||
page.getImages().forEach(image -> {
|
|
||||||
if (image.getImageType().equals(ImageType.OTHER)) {
|
|
||||||
page.getTextBlocks().forEach(textblock -> {
|
|
||||||
if (image.getPosition().contains(textblock.getMinX(), textblock.getMinY(), textblock.getWidth(), textblock.getHeight())) {
|
|
||||||
image.setImageType(ImageType.OCR);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table.PdfTableCell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table.TableCells;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table.TableServiceResponse;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class TableServiceResponseAdapter {
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
private final RedactionStorageService redactionStorageService;
|
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
public Map<Integer, List<PdfTableCell>> convertTables(String dossierId, String fileId) {
|
|
||||||
|
|
||||||
var tableClassificationStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(dossierId, fileId, FileType.TABLES));
|
|
||||||
|
|
||||||
TableServiceResponse tableServiceResponse = objectMapper.readValue(tableClassificationStream, TableServiceResponse.class);
|
|
||||||
|
|
||||||
Map<Integer, List<PdfTableCell>> tableCells = new HashMap<>();
|
|
||||||
tableServiceResponse.getData()
|
|
||||||
.forEach(tableData -> tableCells.computeIfAbsent(tableData.getPageInfo().getNumber(), tableCell -> new ArrayList<>())
|
|
||||||
.addAll(convertTableCells(tableData.getTableCells())));
|
|
||||||
|
|
||||||
return tableCells;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Collection<? extends PdfTableCell> convertTableCells(List<TableCells> tableCells) {
|
|
||||||
|
|
||||||
List<PdfTableCell> pdfTableCells = new ArrayList<>();
|
|
||||||
|
|
||||||
tableCells.forEach(t -> pdfTableCells.add(PdfTableCell.builder()
|
|
||||||
.y0(t.getY0())
|
|
||||||
.x1(t.getX1())
|
|
||||||
.y1(t.getY1())
|
|
||||||
.x0(t.getX0())
|
|
||||||
.width(t.getWidth())
|
|
||||||
.height(t.getHeight())
|
|
||||||
.build()));
|
|
||||||
|
|
||||||
return pdfTableCells;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class Classification {
|
|
||||||
|
|
||||||
private Map<String, Float> probabilities = new HashMap<>();
|
|
||||||
private String label;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class FilterGeometry {
|
|
||||||
|
|
||||||
private ImageSize imageSize;
|
|
||||||
private ImageFormat imageFormat;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class Filters {
|
|
||||||
|
|
||||||
private FilterGeometry geometry;
|
|
||||||
private Probability probability;
|
|
||||||
private boolean allPassed;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class Geometry {
|
|
||||||
|
|
||||||
private float width;
|
|
||||||
private float height;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class ImageFormat {
|
|
||||||
|
|
||||||
private float quotient;
|
|
||||||
private boolean tooTall;
|
|
||||||
private boolean tooWide;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class ImageMetadata {
|
|
||||||
|
|
||||||
private Classification classification;
|
|
||||||
private Position position;
|
|
||||||
private Geometry geometry;
|
|
||||||
private Filters filters;
|
|
||||||
private boolean alpha;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class ImageServiceResponse {
|
|
||||||
|
|
||||||
private String dossierId;
|
|
||||||
private String fileId;
|
|
||||||
|
|
||||||
@JsonProperty(value = "imageMetadata")
|
|
||||||
@JsonAlias("data")
|
|
||||||
private List<ImageMetadata> data = new ArrayList<>();
|
|
||||||
|
|
||||||
private List<ImageMetadata> dataCV = new ArrayList<>();
|
|
||||||
|
|
||||||
|
|
||||||
@JsonProperty(value = "imageMetadata")
|
|
||||||
@JsonAlias("data")
|
|
||||||
public void setData(List<ImageMetadata> data) {this.data = data;}
|
|
||||||
|
|
||||||
|
|
||||||
public List<ImageMetadata> getData() {
|
|
||||||
|
|
||||||
if (this.data == null) {
|
|
||||||
this.data = new ArrayList<>();
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public List<ImageMetadata> getDataCV() {
|
|
||||||
|
|
||||||
if (this.dataCV == null) {
|
|
||||||
this.dataCV = new ArrayList<>();
|
|
||||||
}
|
|
||||||
return dataCV;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class ImageSize {
|
|
||||||
|
|
||||||
private float quotient;
|
|
||||||
private boolean tooLarge;
|
|
||||||
private boolean tooSmall;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class Position {
|
|
||||||
|
|
||||||
private float x1;
|
|
||||||
private float x2;
|
|
||||||
private float y1;
|
|
||||||
private float y2;
|
|
||||||
private int pageNumber;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.image;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class Probability {
|
|
||||||
|
|
||||||
private boolean unconfident;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class PageInfo {
|
|
||||||
|
|
||||||
private int number;
|
|
||||||
private int rotation;
|
|
||||||
private float width;
|
|
||||||
private float height;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class PdfTableCell {
|
|
||||||
|
|
||||||
private float x0;
|
|
||||||
private float y0;
|
|
||||||
private float x1;
|
|
||||||
private float y1;
|
|
||||||
private float width;
|
|
||||||
private float height;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class TableCells {
|
|
||||||
|
|
||||||
private float x0;
|
|
||||||
private float y0;
|
|
||||||
private float x1;
|
|
||||||
private float y1;
|
|
||||||
private float width;
|
|
||||||
private float height;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class TableData {
|
|
||||||
|
|
||||||
private PageInfo pageInfo;
|
|
||||||
private List<TableCells> tableCells = new ArrayList<>();
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class TableServiceResponse {
|
|
||||||
|
|
||||||
private String dossierId;
|
|
||||||
private String fileId;
|
|
||||||
private String operation;
|
|
||||||
private String targetFileExtension;
|
|
||||||
private String responseFileExtension;
|
|
||||||
|
|
||||||
private List<TableData> data = new ArrayList<>();
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
public abstract class AbstractPageBlock {
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
protected float minX;
|
|
||||||
@JsonIgnore
|
|
||||||
protected float maxX;
|
|
||||||
@JsonIgnore
|
|
||||||
protected float minY;
|
|
||||||
@JsonIgnore
|
|
||||||
protected float maxY;
|
|
||||||
@JsonIgnore
|
|
||||||
protected PageBlockType classification;
|
|
||||||
@JsonIgnore
|
|
||||||
protected int page;
|
|
||||||
|
|
||||||
int columnIndex;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private Orientation orientation = Orientation.NONE;
|
|
||||||
|
|
||||||
|
|
||||||
public abstract String getText();
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isHeadline() {
|
|
||||||
|
|
||||||
return this instanceof TextPageBlock && this.getClassification() != null && this.getClassification().isHeadline();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean containsBlock(TextPageBlock other) {
|
|
||||||
|
|
||||||
return this.minX <= other.getPdfMinX() && this.maxX >= other.getPdfMaxX() && this.minY >= other.getPdfMinY() && this.maxY <= other.getPdfMaxY();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean contains(AbstractPageBlock other) {
|
|
||||||
|
|
||||||
return this.minX <= other.minX && this.maxX >= other.maxX && this.minY >= other.minY && this.maxY <= other.maxY;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean contains(Rectangle other) {
|
|
||||||
|
|
||||||
return page == other.getPage() && this.minX <= other.getTopLeft().getX() && this.maxX >= other.getTopLeft().getX() + other.getWidth() && this.minY <= other.getTopLeft()
|
|
||||||
.getY() && this.maxY >= other.getTopLeft().getY() + other.getHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getHeight() {
|
|
||||||
|
|
||||||
return maxY - minY;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getWidth() {
|
|
||||||
|
|
||||||
return maxX - minX;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean intersectsY(AbstractPageBlock atc) {
|
|
||||||
|
|
||||||
return this.minY <= atc.getMaxY() && this.maxY >= atc.getMinY();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean intersectsX(AbstractPageBlock atc) {
|
|
||||||
|
|
||||||
return this.minX <= atc.getMaxX() && this.maxX >= atc.getMinX();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionGrid;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.StringFrequencyCounter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.UnclassifiedText;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryVersion;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class ClassificationDocument {
|
|
||||||
|
|
||||||
private List<ClassificationPage> pages = new ArrayList<>();
|
|
||||||
private List<ClassificationSection> sections = new ArrayList<>();
|
|
||||||
private List<ClassificationHeader> headers = new ArrayList<>();
|
|
||||||
private List<ClassificationFooter> footers = new ArrayList<>();
|
|
||||||
private List<UnclassifiedText> unclassifiedTexts = new ArrayList<>();
|
|
||||||
private FloatFrequencyCounter textHeightCounter = new FloatFrequencyCounter();
|
|
||||||
private FloatFrequencyCounter fontSizeCounter = new FloatFrequencyCounter();
|
|
||||||
private StringFrequencyCounter fontCounter = new StringFrequencyCounter();
|
|
||||||
private StringFrequencyCounter fontStyleCounter = new StringFrequencyCounter();
|
|
||||||
private boolean headlines;
|
|
||||||
|
|
||||||
private SectionGrid sectionGrid = new SectionGrid();
|
|
||||||
private DictionaryVersion dictionaryVersion;
|
|
||||||
private long rulesVersion;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class ClassificationFooter {
|
|
||||||
|
|
||||||
private List<TextPageBlock> textBlocks;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class ClassificationHeader {
|
|
||||||
|
|
||||||
private List<TextPageBlock> textBlocks;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image.ClassifiedImage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.StringFrequencyCounter;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ClassificationPage {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private List<AbstractPageBlock> textBlocks;
|
|
||||||
|
|
||||||
private List<ClassifiedImage> images = new ArrayList<>();
|
|
||||||
|
|
||||||
private Rectangle bodyTextFrame;
|
|
||||||
|
|
||||||
private boolean landscape;
|
|
||||||
private int rotation;
|
|
||||||
|
|
||||||
private int pageNumber;
|
|
||||||
|
|
||||||
private FloatFrequencyCounter textHeightCounter = new FloatFrequencyCounter();
|
|
||||||
private FloatFrequencyCounter fontSizeCounter = new FloatFrequencyCounter();
|
|
||||||
private StringFrequencyCounter fontCounter = new StringFrequencyCounter();
|
|
||||||
private StringFrequencyCounter fontStyleCounter = new StringFrequencyCounter();
|
|
||||||
|
|
||||||
private float pageWidth;
|
|
||||||
private float pageHeight;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image.ClassifiedImage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.TablePageBlock;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class ClassificationSection {
|
|
||||||
|
|
||||||
private List<AbstractPageBlock> pageBlocks = new ArrayList<>();
|
|
||||||
private List<ClassifiedImage> images = new ArrayList<>();
|
|
||||||
private String headline;
|
|
||||||
|
|
||||||
|
|
||||||
public List<TablePageBlock> getTables() {
|
|
||||||
|
|
||||||
List<TablePageBlock> tables = new ArrayList<>();
|
|
||||||
pageBlocks.forEach(block -> {
|
|
||||||
if (block instanceof TablePageBlock) {
|
|
||||||
tables.add((TablePageBlock) block);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return tables;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class Column {
|
|
||||||
|
|
||||||
int index;
|
|
||||||
ColumnType columnType;
|
|
||||||
Rectangle2D bBox;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
public enum ColumnType {
|
|
||||||
RULING,
|
|
||||||
DISTANCE
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
public class FloatFrequencyCounter {
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
Map<Float, Integer> countPerValue = new HashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
public void add(float value) {
|
|
||||||
|
|
||||||
if (!countPerValue.containsKey(value)) {
|
|
||||||
countPerValue.put(value, 1);
|
|
||||||
} else {
|
|
||||||
countPerValue.put(value, countPerValue.get(value) + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void addAll(Map<Float, Integer> otherCounter) {
|
|
||||||
|
|
||||||
for (Map.Entry<Float, Integer> entry : otherCounter.entrySet()) {
|
|
||||||
if (countPerValue.containsKey(entry.getKey())) {
|
|
||||||
countPerValue.put(entry.getKey(), countPerValue.get(entry.getKey()) + entry.getValue());
|
|
||||||
} else {
|
|
||||||
countPerValue.put(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Float getMostPopular() {
|
|
||||||
|
|
||||||
Map.Entry<Float, Integer> mostPopular = null;
|
|
||||||
for (Map.Entry<Float, Integer> entry : countPerValue.entrySet()) {
|
|
||||||
if (mostPopular == null || entry.getValue() >= mostPopular.getValue()) {
|
|
||||||
mostPopular = entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mostPopular != null ? mostPopular.getKey() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public List<Float> getHighterThanMostPopular() {
|
|
||||||
|
|
||||||
Float mostPopular = getMostPopular();
|
|
||||||
List<Float> higher = new ArrayList<>();
|
|
||||||
for (Float value : countPerValue.keySet()) {
|
|
||||||
if (value > mostPopular) {
|
|
||||||
higher.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return higher.stream().sorted(Collections.reverseOrder()).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Float getHighest() {
|
|
||||||
|
|
||||||
Float highest = null;
|
|
||||||
for (Float value : countPerValue.keySet()) {
|
|
||||||
if (highest == null || value > highest) {
|
|
||||||
highest = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return highest;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
public enum Orientation {
|
|
||||||
|
|
||||||
NONE,
|
|
||||||
LEFT,
|
|
||||||
RIGHT
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model;
|
|
||||||
|
|
||||||
public enum PageBlockType {
|
|
||||||
H1,
|
|
||||||
H2,
|
|
||||||
H3,
|
|
||||||
H4,
|
|
||||||
H5,
|
|
||||||
H6,
|
|
||||||
HEADER,
|
|
||||||
FOOTER,
|
|
||||||
TITLE,
|
|
||||||
PARAGRAPH,
|
|
||||||
PARAGRAPH_BOLD,
|
|
||||||
PARAGRAPH_ITALIC,
|
|
||||||
PARAGRAPH_UNKNOWN,
|
|
||||||
OTHER,
|
|
||||||
TABLE;
|
|
||||||
|
|
||||||
|
|
||||||
public static PageBlockType getHeadlineType(int i) {
|
|
||||||
|
|
||||||
return switch (i) {
|
|
||||||
case 1 -> PageBlockType.H1;
|
|
||||||
case 2 -> PageBlockType.H2;
|
|
||||||
case 3 -> PageBlockType.H3;
|
|
||||||
case 4 -> PageBlockType.H4;
|
|
||||||
case 5 -> PageBlockType.H5;
|
|
||||||
default -> PageBlockType.H6;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isHeadline() {
|
|
||||||
|
|
||||||
return this.equals(H1) || this.equals(H2) || this.equals(H3) || this.equals(H4) || this.equals(H5) || this.equals(H6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ImageType;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ClassifiedImage {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Rectangle2D position;
|
|
||||||
@NonNull
|
|
||||||
private ImageType imageType;
|
|
||||||
private boolean isAppendedToSection;
|
|
||||||
@NonNull
|
|
||||||
private boolean hasTransparency;
|
|
||||||
@NonNull
|
|
||||||
private int page;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table;
|
|
||||||
|
|
||||||
import java.awt.geom.Point2D;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class Cell extends Rectangle {
|
|
||||||
|
|
||||||
private List<TextPageBlock> textBlocks = new ArrayList<>();
|
|
||||||
|
|
||||||
private List<Cell> headerCells = new ArrayList<>();
|
|
||||||
|
|
||||||
private boolean isHeaderCell;
|
|
||||||
|
|
||||||
private static final int MIN_SIZE = 1;
|
|
||||||
|
|
||||||
private int pageNumber;
|
|
||||||
|
|
||||||
|
|
||||||
public Cell(Point2D topLeft, Point2D bottomRight) {
|
|
||||||
|
|
||||||
super((float) topLeft.getY(), (float) topLeft.getX(), (float) (bottomRight.getX() - topLeft.getX()), (float) (bottomRight.getY() - topLeft.getY()));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void addTextBlock(TextPageBlock textBlock) {
|
|
||||||
|
|
||||||
textBlocks.add(textBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
Iterator<TextPageBlock> itty = textBlocks.iterator();
|
|
||||||
TextPositionSequence previous = null;
|
|
||||||
while (itty.hasNext()) {
|
|
||||||
|
|
||||||
TextPageBlock textBlock = itty.next();
|
|
||||||
|
|
||||||
for (TextPositionSequence word : textBlock.getSequences()) {
|
|
||||||
if (previous != null) {
|
|
||||||
if (Math.abs(previous.getMaxYDirAdj() - word.getMaxYDirAdj()) > word.getTextHeight()) {
|
|
||||||
sb.append('\n');
|
|
||||||
} else {
|
|
||||||
sb.append(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.append(word.toString());
|
|
||||||
previous = word;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return TextNormalizationUtilities.removeHyphenLineBreaks(sb.toString()).replaceAll("\n", " ").replaceAll(" {2}", " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean hasMinimumSize() {
|
|
||||||
|
|
||||||
return this.getHeight() >= MIN_SIZE && this.getWidth() >= MIN_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.Value;
|
|
||||||
|
|
||||||
@Value
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class CellPosition implements Comparable<CellPosition> {
|
|
||||||
|
|
||||||
int row;
|
|
||||||
|
|
||||||
int col;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(CellPosition other) {
|
|
||||||
|
|
||||||
int rowDiff = row - other.row;
|
|
||||||
return rowDiff != 0 ? rowDiff : col - other.col;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
public class CleanRulings {
|
|
||||||
|
|
||||||
List<Ruling> horizontal;
|
|
||||||
List<Ruling> vertical;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table;
|
|
||||||
|
|
||||||
import java.awt.geom.Point2D;
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@SuppressWarnings("all")
|
|
||||||
public class Rectangle extends Rectangle2D.Float {
|
|
||||||
|
|
||||||
protected static final float VERTICAL_COMPARISON_THRESHOLD = 0.4f;
|
|
||||||
/**
|
|
||||||
* Ill-defined comparator, from when Rectangle was Comparable.
|
|
||||||
* <p>
|
|
||||||
* see https://github.com/tabulapdf/tabula-java/issues/116
|
|
||||||
*
|
|
||||||
* @deprecated with no replacement
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static final Comparator<Rectangle> ILL_DEFINED_ORDER = new Comparator<Rectangle>() {
|
|
||||||
@Override
|
|
||||||
public int compare(Rectangle o1, Rectangle o2) {
|
|
||||||
|
|
||||||
if (o1.equals(o2)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (o1.verticalOverlap(o2) > VERTICAL_COMPARISON_THRESHOLD) {
|
|
||||||
return o1.isLtrDominant() == -1 && o2.isLtrDominant() == -1 ? -java.lang.Double.compare(o1.getX(), o2.getX()) : java.lang.Double.compare(o1.getX(), o2.getX());
|
|
||||||
} else {
|
|
||||||
return java.lang.Float.compare(o1.getBottom(), o2.getBottom());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
public Rectangle() {
|
|
||||||
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Rectangle(float top, float left, float width, float height) {
|
|
||||||
|
|
||||||
super();
|
|
||||||
this.setRect(left, top, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param rectangles
|
|
||||||
* @return minimum bounding box that contains all the rectangles
|
|
||||||
*/
|
|
||||||
public static Rectangle boundingBoxOf(List<? extends Rectangle> rectangles) {
|
|
||||||
|
|
||||||
float minx = java.lang.Float.MAX_VALUE;
|
|
||||||
float miny = java.lang.Float.MAX_VALUE;
|
|
||||||
float maxx = java.lang.Float.MIN_VALUE;
|
|
||||||
float maxy = java.lang.Float.MIN_VALUE;
|
|
||||||
|
|
||||||
for (Rectangle r : rectangles) {
|
|
||||||
minx = (float) Math.min(r.getMinX(), minx);
|
|
||||||
miny = (float) Math.min(r.getMinY(), miny);
|
|
||||||
maxx = (float) Math.max(r.getMaxX(), maxx);
|
|
||||||
maxy = (float) Math.max(r.getMaxY(), maxy);
|
|
||||||
}
|
|
||||||
return new Rectangle(miny, minx, maxx - minx, maxy - miny);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int compareTo(Rectangle other) {
|
|
||||||
|
|
||||||
return ILL_DEFINED_ORDER.compare(this, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// I'm bad at Java and need this for fancy sorting in
|
|
||||||
// technology.tabula.TextChunk.
|
|
||||||
public int isLtrDominant() {
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getArea() {
|
|
||||||
|
|
||||||
return this.width * this.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float verticalOverlap(Rectangle other) {
|
|
||||||
|
|
||||||
return Math.max(0, Math.min(this.getBottom(), other.getBottom()) - Math.max(this.getTop(), other.getTop()));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean verticallyOverlaps(Rectangle other) {
|
|
||||||
|
|
||||||
return verticalOverlap(other) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float horizontalOverlap(Rectangle other) {
|
|
||||||
|
|
||||||
return Math.max(0, Math.min(this.getRight(), other.getRight()) - Math.max(this.getLeft(), other.getLeft()));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean horizontallyOverlaps(Rectangle other) {
|
|
||||||
|
|
||||||
return horizontalOverlap(other) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float verticalOverlapRatio(Rectangle other) {
|
|
||||||
|
|
||||||
float rv = 0, delta = Math.min(this.getBottom() - this.getTop(), other.getBottom() - other.getTop());
|
|
||||||
|
|
||||||
if (other.getTop() <= this.getTop() && this.getTop() <= other.getBottom() && other.getBottom() <= this.getBottom()) {
|
|
||||||
rv = (other.getBottom() - this.getTop()) / delta;
|
|
||||||
} else if (this.getTop() <= other.getTop() && other.getTop() <= this.getBottom() && this.getBottom() <= other.getBottom()) {
|
|
||||||
rv = (this.getBottom() - other.getTop()) / delta;
|
|
||||||
} else if (this.getTop() <= other.getTop() && other.getTop() <= other.getBottom() && other.getBottom() <= this.getBottom()) {
|
|
||||||
rv = (other.getBottom() - other.getTop()) / delta;
|
|
||||||
} else if (other.getTop() <= this.getTop() && this.getTop() <= this.getBottom() && this.getBottom() <= other.getBottom()) {
|
|
||||||
rv = (this.getBottom() - this.getTop()) / delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float overlapRatio(Rectangle other) {
|
|
||||||
|
|
||||||
double intersectionWidth = Math.max(0, Math.min(this.getRight(), other.getRight()) - Math.max(this.getLeft(), other.getLeft()));
|
|
||||||
double intersectionHeight = Math.max(0, Math.min(this.getBottom(), other.getBottom()) - Math.max(this.getTop(), other.getTop()));
|
|
||||||
double intersectionArea = Math.max(0, intersectionWidth * intersectionHeight);
|
|
||||||
double unionArea = this.getArea() + other.getArea() - intersectionArea;
|
|
||||||
|
|
||||||
return (float) (intersectionArea / unionArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Rectangle merge(Rectangle other) {
|
|
||||||
|
|
||||||
this.setRect(this.createUnion(other));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getTop() {
|
|
||||||
|
|
||||||
return (float) this.getMinY();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setTop(float top) {
|
|
||||||
|
|
||||||
float deltaHeight = top - this.y;
|
|
||||||
this.setRect(this.x, top, this.width, this.height - deltaHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getRight() {
|
|
||||||
|
|
||||||
return (float) this.getMaxX();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setRight(float right) {
|
|
||||||
|
|
||||||
this.setRect(this.x, this.y, right - this.x, this.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getLeft() {
|
|
||||||
|
|
||||||
return (float) this.getMinX();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setLeft(float left) {
|
|
||||||
|
|
||||||
float deltaWidth = left - this.x;
|
|
||||||
this.setRect(left, this.y, this.width - deltaWidth, this.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getBottom() {
|
|
||||||
|
|
||||||
return (float) this.getMaxY();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setBottom(float bottom) {
|
|
||||||
|
|
||||||
this.setRect(this.x, this.y, this.width, bottom - this.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Point2D[] getPoints() {
|
|
||||||
|
|
||||||
return new Point2D[]{new Point2D.Float(this.getLeft(), this.getTop()), new Point2D.Float(this.getRight(), this.getTop()), new Point2D.Float(this.getRight(),
|
|
||||||
this.getBottom()), new Point2D.Float(this.getLeft(), this.getBottom())};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
String s = super.toString();
|
|
||||||
sb.append(s.substring(0, s.length() - 1));
|
|
||||||
sb.append(String.format(",bottom=%f,right=%f]", this.getBottom(), this.getRight()));
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,437 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table;
|
|
||||||
|
|
||||||
import java.awt.geom.Line2D;
|
|
||||||
import java.awt.geom.Point2D;
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Formatter;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.CohenSutherlandClipping;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.Utils;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@SuppressWarnings("all")
|
|
||||||
public class Ruling extends Line2D.Float {
|
|
||||||
|
|
||||||
private static int PERPENDICULAR_PIXEL_EXPAND_AMOUNT = 2;
|
|
||||||
|
|
||||||
|
|
||||||
public Ruling(Point2D p1, Point2D p2) {
|
|
||||||
|
|
||||||
super(p1, p2);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static List<Ruling> cropRulingsToArea(List<Ruling> rulings, Rectangle2D area) {
|
|
||||||
|
|
||||||
ArrayList<Ruling> rv = new ArrayList<>();
|
|
||||||
for (Ruling r : rulings) {
|
|
||||||
if (r.intersects(area)) {
|
|
||||||
rv.add(r.intersect(area));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// log(n) implementation of find_intersections
|
|
||||||
// based on http://people.csail.mit.edu/indyk/6.838-old/handouts/lec2.pdf
|
|
||||||
public static Map<Point2D, Ruling[]> findIntersections(List<Ruling> horizontals, List<Ruling> verticals) {
|
|
||||||
|
|
||||||
class SortObject {
|
|
||||||
|
|
||||||
protected SOType type;
|
|
||||||
protected float position;
|
|
||||||
protected Ruling ruling;
|
|
||||||
|
|
||||||
|
|
||||||
public SortObject(SOType type, float position, Ruling ruling) {
|
|
||||||
|
|
||||||
this.type = type;
|
|
||||||
this.position = position;
|
|
||||||
this.ruling = ruling;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
List<SortObject> sos = new ArrayList<>();
|
|
||||||
|
|
||||||
TreeMap<Ruling, Boolean> tree = new TreeMap<>(new Comparator<Ruling>() {
|
|
||||||
@Override
|
|
||||||
public int compare(Ruling o1, Ruling o2) {
|
|
||||||
|
|
||||||
return java.lang.Double.compare(o1.getTop(), o2.getTop());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
TreeMap<Point2D, Ruling[]> rv = new TreeMap<>(new Comparator<Point2D>() {
|
|
||||||
@Override
|
|
||||||
public int compare(Point2D o1, Point2D o2) {
|
|
||||||
|
|
||||||
if (o1.getY() > o2.getY()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (o1.getY() < o2.getY()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (o1.getX() > o2.getX()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (o1.getX() < o2.getX()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (Ruling h : horizontals) {
|
|
||||||
sos.add(new SortObject(SOType.HLEFT, h.getLeft() - PERPENDICULAR_PIXEL_EXPAND_AMOUNT, h));
|
|
||||||
sos.add(new SortObject(SOType.HRIGHT, h.getRight() + PERPENDICULAR_PIXEL_EXPAND_AMOUNT, h));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Ruling v : verticals) {
|
|
||||||
sos.add(new SortObject(SOType.VERTICAL, v.getLeft(), v));
|
|
||||||
}
|
|
||||||
|
|
||||||
Collections.sort(sos, new Comparator<SortObject>() {
|
|
||||||
@Override
|
|
||||||
public int compare(SortObject a, SortObject b) {
|
|
||||||
|
|
||||||
int rv;
|
|
||||||
if (Utils.feq(a.position, b.position)) {
|
|
||||||
if (a.type == SOType.VERTICAL && b.type == SOType.HLEFT) {
|
|
||||||
rv = 1;
|
|
||||||
} else if (a.type == SOType.VERTICAL && b.type == SOType.HRIGHT) {
|
|
||||||
rv = -1;
|
|
||||||
} else if (a.type == SOType.HLEFT && b.type == SOType.VERTICAL) {
|
|
||||||
rv = -1;
|
|
||||||
} else if (a.type == SOType.HRIGHT && b.type == SOType.VERTICAL) {
|
|
||||||
rv = 1;
|
|
||||||
} else {
|
|
||||||
rv = java.lang.Double.compare(a.position, b.position);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return java.lang.Double.compare(a.position, b.position);
|
|
||||||
}
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (SortObject so : sos) {
|
|
||||||
switch (so.type) {
|
|
||||||
case VERTICAL:
|
|
||||||
for (Map.Entry<Ruling, Boolean> h : tree.entrySet()) {
|
|
||||||
try {
|
|
||||||
Point2D i = h.getKey().intersectionPoint(so.ruling);
|
|
||||||
if (i == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
rv.put(i, new Ruling[]{h.getKey().expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT), so.ruling.expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT)});
|
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
log.info("Some line are oblique, ignoring...");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case HRIGHT:
|
|
||||||
tree.remove(so.ruling);
|
|
||||||
break;
|
|
||||||
case HLEFT:
|
|
||||||
tree.put(so.ruling, true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean vertical() {
|
|
||||||
|
|
||||||
return this.length() > 0 && Utils.feq(this.x1, this.x2); //diff < ORIENTATION_CHECK_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean horizontal() {
|
|
||||||
|
|
||||||
return this.length() > 0 && Utils.feq(this.y1, this.y2); //diff < ORIENTATION_CHECK_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
// attributes that make sense only for non-oblique lines
|
|
||||||
// these are used to have a single collapse method (in page, currently)
|
|
||||||
|
|
||||||
|
|
||||||
public boolean oblique() {
|
|
||||||
|
|
||||||
return !(this.vertical() || this.horizontal());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getPosition() {
|
|
||||||
|
|
||||||
if (this.oblique()) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
return this.vertical() ? this.getLeft() : this.getTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getStart() {
|
|
||||||
|
|
||||||
if (this.oblique()) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
return this.vertical() ? this.getTop() : this.getLeft();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setStart(float v) {
|
|
||||||
|
|
||||||
if (this.oblique()) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
if (this.vertical()) {
|
|
||||||
this.setTop(v);
|
|
||||||
} else {
|
|
||||||
this.setLeft(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getEnd() {
|
|
||||||
|
|
||||||
if (this.oblique()) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
return this.vertical() ? this.getBottom() : this.getRight();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setEnd(float v) {
|
|
||||||
|
|
||||||
if (this.oblique()) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
if (this.vertical()) {
|
|
||||||
this.setBottom(v);
|
|
||||||
} else {
|
|
||||||
this.setRight(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setStartEnd(float start, float end) {
|
|
||||||
|
|
||||||
if (this.oblique()) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
if (this.vertical()) {
|
|
||||||
this.setTop(start);
|
|
||||||
this.setBottom(end);
|
|
||||||
} else {
|
|
||||||
this.setLeft(start);
|
|
||||||
this.setRight(end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean perpendicularTo(Ruling other) {
|
|
||||||
|
|
||||||
return this.vertical() == other.horizontal();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean nearlyIntersects(Ruling another, int colinearOrParallelExpandAmount) {
|
|
||||||
|
|
||||||
if (this.intersectsLine(another)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean rv = false;
|
|
||||||
|
|
||||||
if (this.perpendicularTo(another)) {
|
|
||||||
rv = this.expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT).intersectsLine(another);
|
|
||||||
} else {
|
|
||||||
rv = this.expand(colinearOrParallelExpandAmount).intersectsLine(another.expand(colinearOrParallelExpandAmount));
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public double length() {
|
|
||||||
|
|
||||||
return Math.sqrt(Math.pow(this.x1 - this.x2, 2) + Math.pow(this.y1 - this.y2, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Ruling intersect(Rectangle2D clip) {
|
|
||||||
|
|
||||||
Float clipee = (Float) this.clone();
|
|
||||||
boolean clipped = new CohenSutherlandClipping(clip).clip(clipee);
|
|
||||||
|
|
||||||
if (clipped) {
|
|
||||||
return new Ruling(clipee.getP1(), clipee.getP2());
|
|
||||||
} else {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Ruling expand(float amount) {
|
|
||||||
|
|
||||||
Ruling r = (Ruling) this.clone();
|
|
||||||
try {
|
|
||||||
r.setStart(this.getStart() - amount);
|
|
||||||
r.setEnd(this.getEnd() + amount);
|
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
log.warn("Could not expand ruling!");
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Point2D intersectionPoint(Ruling other) {
|
|
||||||
|
|
||||||
Ruling this_l = this.expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT);
|
|
||||||
Ruling other_l = other.expand(PERPENDICULAR_PIXEL_EXPAND_AMOUNT);
|
|
||||||
Ruling horizontal, vertical;
|
|
||||||
|
|
||||||
if (!this_l.intersectsLine(other_l)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this_l.horizontal() && other_l.vertical()) {
|
|
||||||
horizontal = this_l;
|
|
||||||
vertical = other_l;
|
|
||||||
} else if (this_l.vertical() && other_l.horizontal()) {
|
|
||||||
vertical = this_l;
|
|
||||||
horizontal = other_l;
|
|
||||||
} else {
|
|
||||||
log.warn("lines must be orthogonal, vertical and horizontal");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Point2D.Float(vertical.getLeft(), horizontal.getTop());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object other) {
|
|
||||||
|
|
||||||
if (this == other) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(other instanceof Ruling)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ruling o = (Ruling) other;
|
|
||||||
return this.getP1().equals(o.getP1()) && this.getP2().equals(o.getP2());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
|
|
||||||
return super.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getTop() {
|
|
||||||
|
|
||||||
return this.y1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setTop(float v) {
|
|
||||||
|
|
||||||
setLine(this.getLeft(), v, this.getRight(), this.getBottom());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getLeft() {
|
|
||||||
|
|
||||||
return this.x1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setLeft(float v) {
|
|
||||||
|
|
||||||
setLine(v, this.getTop(), this.getRight(), this.getBottom());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getBottom() {
|
|
||||||
|
|
||||||
return this.y2;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setBottom(float v) {
|
|
||||||
|
|
||||||
setLine(this.getLeft(), this.getTop(), this.getRight(), v);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getRight() {
|
|
||||||
|
|
||||||
return this.x2;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setRight(float v) {
|
|
||||||
|
|
||||||
setLine(this.getLeft(), this.getTop(), v, this.getBottom());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getWidth() {
|
|
||||||
|
|
||||||
return this.getRight() - this.getLeft();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getHeight() {
|
|
||||||
|
|
||||||
return this.getBottom() - this.getTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public double getAngle() {
|
|
||||||
|
|
||||||
double angle = Math.toDegrees(Math.atan2(this.getP2().getY() - this.getP1().getY(), this.getP2().getX() - this.getP1().getX()));
|
|
||||||
|
|
||||||
if (angle < 0) {
|
|
||||||
angle += 360;
|
|
||||||
}
|
|
||||||
return angle;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
Formatter formatter = new Formatter(sb);
|
|
||||||
String rv = formatter.format("%s[minX=%f minY=%f maxX=%f maxY=%f]", this.getClass().toString(), this.x1, this.y1, this.x2, this.y2).toString();
|
|
||||||
formatter.close();
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private enum SOType {
|
|
||||||
VERTICAL,
|
|
||||||
HRIGHT,
|
|
||||||
HLEFT
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,342 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table;
|
|
||||||
|
|
||||||
import java.awt.geom.Point2D;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.PageBlockType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class TablePageBlock extends AbstractPageBlock {
|
|
||||||
|
|
||||||
private final TreeMap<CellPosition, Cell> cells = new TreeMap<>();
|
|
||||||
|
|
||||||
private final int rotation;
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private String headline;
|
|
||||||
private int unrotatedRowCount;
|
|
||||||
private int unrotatedColCount;
|
|
||||||
private List<List<Cell>> rows;
|
|
||||||
|
|
||||||
|
|
||||||
public TablePageBlock(List<Cell> cells, Rectangle area, int rotation) {
|
|
||||||
|
|
||||||
addCells(cells);
|
|
||||||
minX = area.getLeft();
|
|
||||||
minY = area.getBottom();
|
|
||||||
maxX = area.getRight();
|
|
||||||
maxY = area.getTop();
|
|
||||||
classification = PageBlockType.TABLE;
|
|
||||||
this.rotation = rotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public List<List<Cell>> getRows() {
|
|
||||||
|
|
||||||
if (rows == null) {
|
|
||||||
rows = computeRows();
|
|
||||||
|
|
||||||
// Ignore rows that does not contain any cells and values.
|
|
||||||
List<List<Cell>> rowsToRemove = new ArrayList<>();
|
|
||||||
for (List<Cell> row : rows) {
|
|
||||||
if (row.size() == 1 && row.get(0).getTextBlocks().isEmpty()) {
|
|
||||||
rowsToRemove.add(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rows.removeAll(rowsToRemove);
|
|
||||||
|
|
||||||
computeHeaders();
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int getRowCount() {
|
|
||||||
|
|
||||||
return getRows().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int getColCount() {
|
|
||||||
|
|
||||||
return getRows().stream().mapToInt(List::size).max().orElse(0);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect header cells (either first row or first column):
|
|
||||||
* Column is marked as header if cell text is bold and row cell text is not bold.
|
|
||||||
* Defaults to row.
|
|
||||||
*/
|
|
||||||
private void computeHeaders() {
|
|
||||||
|
|
||||||
if (rows == null) {
|
|
||||||
rows = computeRows();
|
|
||||||
}
|
|
||||||
// A bold cell is a header cell as long as every cell to the left/top is bold, too
|
|
||||||
// we move from left to right and top to bottom
|
|
||||||
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
|
|
||||||
List<Cell> rowCells = rows.get(rowIndex);
|
|
||||||
if (rowCells.size() == 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int colIndex = 0; colIndex < rowCells.size(); colIndex++) {
|
|
||||||
Cell cell = rowCells.get(colIndex);
|
|
||||||
List<Cell> cellsToTheLeft = rowCells.subList(0, colIndex);
|
|
||||||
Cell lastHeaderCell = null;
|
|
||||||
for (Cell leftCell : cellsToTheLeft) {
|
|
||||||
if (leftCell.isHeaderCell()) {
|
|
||||||
lastHeaderCell = leftCell;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lastHeaderCell != null) {
|
|
||||||
cell.getHeaderCells().add(lastHeaderCell);
|
|
||||||
}
|
|
||||||
List<Cell> cellsToTheTop = new ArrayList<>();
|
|
||||||
for (int i = 0; i < rowIndex; i++) {
|
|
||||||
try {
|
|
||||||
cellsToTheTop.add(rows.get(i).get(colIndex));
|
|
||||||
} catch (IndexOutOfBoundsException e) {
|
|
||||||
log.debug("No cell {} in row {}, ignoring.", colIndex, rowIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Cell topCell : cellsToTheTop) {
|
|
||||||
if (topCell.isHeaderCell()) {
|
|
||||||
lastHeaderCell = topCell;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lastHeaderCell != null) {
|
|
||||||
cell.getHeaderCells().add(lastHeaderCell);
|
|
||||||
}
|
|
||||||
if (CollectionUtils.isNotEmpty(cell.getTextBlocks()) && cell.getTextBlocks().get(0).getMostPopularWordStyle().equals("bold")) {
|
|
||||||
cell.setHeaderCell(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<List<Cell>> computeRows() {
|
|
||||||
|
|
||||||
List<List<Cell>> rows = new ArrayList<>();
|
|
||||||
if (rotation == 90) {
|
|
||||||
for (int i = 0; i < unrotatedColCount; i++) { // rows
|
|
||||||
List<Cell> lastRow = new ArrayList<>();
|
|
||||||
for (int j = unrotatedRowCount - 1; j >= 0; j--) { // cols
|
|
||||||
Cell cell = cells.get(new CellPosition(j, i));
|
|
||||||
if (cell != null) {
|
|
||||||
lastRow.add(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rows.add(lastRow);
|
|
||||||
}
|
|
||||||
} else if (rotation == 270) {
|
|
||||||
for (int i = unrotatedColCount - 1; i >= 0; i--) { // rows
|
|
||||||
List<Cell> lastRow = new ArrayList<>();
|
|
||||||
for (int j = 0; j < unrotatedRowCount; j++) { // cols
|
|
||||||
Cell cell = cells.get(new CellPosition(j, i));
|
|
||||||
if (cell != null) {
|
|
||||||
lastRow.add(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rows.add(lastRow);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < unrotatedRowCount; i++) {
|
|
||||||
List<Cell> lastRow = new ArrayList<>();
|
|
||||||
for (int j = 0; j < unrotatedColCount; j++) {
|
|
||||||
Cell cell = cells.get(new CellPosition(i, j)); // JAVA_8 use getOrDefault()
|
|
||||||
if (cell != null) {
|
|
||||||
lastRow.add(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rows.add(lastRow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void add(Cell chunk, int row, int col) {
|
|
||||||
|
|
||||||
unrotatedRowCount = Math.max(unrotatedRowCount, row + 1);
|
|
||||||
unrotatedColCount = Math.max(unrotatedColCount, col + 1);
|
|
||||||
|
|
||||||
CellPosition cp = new CellPosition(row, col);
|
|
||||||
cells.put(cp, chunk);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addCells(List<Cell> cells) {
|
|
||||||
|
|
||||||
if (cells.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cells.removeIf(cell -> cell.getWidth() < 1.1 || cell.getHeight() < 1.1);
|
|
||||||
|
|
||||||
List<List<Cell>> rowsOfCells = calculateStructure(cells);
|
|
||||||
|
|
||||||
for (int i = 0; i < rowsOfCells.size(); i++) {
|
|
||||||
for (int j = 0; j < rowsOfCells.get(i).size(); j++) {
|
|
||||||
add(rowsOfCells.get(i).get(j), i, j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the structure of the table. For spanning rows and columns multiple cells with the same values will be inserted.
|
|
||||||
*
|
|
||||||
* @param cells The found cells
|
|
||||||
* @return TablePageBlock Structure
|
|
||||||
*/
|
|
||||||
private List<List<Cell>> calculateStructure(List<Cell> cells) {
|
|
||||||
|
|
||||||
List<List<Cell>> matrix = new ArrayList<>();
|
|
||||||
|
|
||||||
if (cells.isEmpty()) {
|
|
||||||
return matrix;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<Float> uniqueX = new HashSet<>();
|
|
||||||
Set<Float> uniqueY = new HashSet<>();
|
|
||||||
cells.stream().filter(c -> !c.getTextBlocks().isEmpty() || c.getHeight() > 3 && c.getWidth() > 3).forEach(c -> {
|
|
||||||
uniqueX.add(c.getLeft());
|
|
||||||
uniqueX.add(c.getRight());
|
|
||||||
uniqueY.add(c.getBottom());
|
|
||||||
uniqueY.add(c.getTop());
|
|
||||||
});
|
|
||||||
|
|
||||||
var sortedUniqueX = uniqueX.stream().sorted().toList();
|
|
||||||
var sortedUniqueY = uniqueY.stream().sorted().toList();
|
|
||||||
|
|
||||||
Float prevY = null;
|
|
||||||
for (Float y : sortedUniqueY) {
|
|
||||||
|
|
||||||
List<Cell> row = new ArrayList<>();
|
|
||||||
|
|
||||||
Float prevX = null;
|
|
||||||
for (Float x : sortedUniqueX) {
|
|
||||||
|
|
||||||
if (prevY != null && prevX != null) {
|
|
||||||
var cell = new Cell(new Point2D.Float(prevX, prevY), new Point2D.Float(x, y));
|
|
||||||
|
|
||||||
var intersectionCell = cells.stream().filter(c -> cell.intersects(c) && cell.overlapRatio(c) > 0.1f).findFirst();
|
|
||||||
intersectionCell.ifPresent(value -> cell.getTextBlocks().addAll(value.getTextBlocks()));
|
|
||||||
if (cell.hasMinimumSize()) {
|
|
||||||
row.add(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prevX = x;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevY != null && prevX != null && !row.isEmpty()) {
|
|
||||||
matrix.add(row);
|
|
||||||
}
|
|
||||||
prevY = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
Collections.reverse(matrix);
|
|
||||||
|
|
||||||
return matrix;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getText() {
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
List<List<Cell>> rows = getRows();
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
for (List<Cell> row : rows) {
|
|
||||||
if (i != 0) {
|
|
||||||
sb.append("\n");
|
|
||||||
}
|
|
||||||
if (!row.isEmpty()) {
|
|
||||||
boolean firstColumn = true;
|
|
||||||
for (Cell column : row) {
|
|
||||||
if (!firstColumn) {
|
|
||||||
sb.append(",");
|
|
||||||
}
|
|
||||||
if (column != null && column.getTextBlocks() != null) {
|
|
||||||
boolean first = true;
|
|
||||||
for (TextPageBlock textBlock : column.getTextBlocks()) {
|
|
||||||
if (!first) {
|
|
||||||
sb.append("\n");
|
|
||||||
}
|
|
||||||
sb.append('\"').append(textBlock.getText().replaceAll("\"", "\\\"")).append('\"');
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
firstColumn = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public String getTextAsHtml() {
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
List<List<Cell>> rows = getRows();
|
|
||||||
|
|
||||||
sb.append("<table border=\"1\">");
|
|
||||||
int i = 0;
|
|
||||||
for (List<Cell> row : rows) {
|
|
||||||
sb.append("\n<tr>");
|
|
||||||
if (!row.isEmpty()) {
|
|
||||||
for (Cell column : row) {
|
|
||||||
sb.append(i == 0 ? "\n<th>" : "\n<td>");
|
|
||||||
if (column != null && column.getTextBlocks() != null) {
|
|
||||||
boolean first = true;
|
|
||||||
for (TextPageBlock textBlock : column.getTextBlocks()) {
|
|
||||||
if (!first) {
|
|
||||||
sb.append("<br />");
|
|
||||||
}
|
|
||||||
sb.append(textBlock.getText().replaceAll("\\n", "<br />"));
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.append(i == 0 ? "</th>" : "</td>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.append("</tr>");
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
sb.append("</table>");
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
|
||||||
import org.springframework.beans.BeanUtils;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class RedTextPosition {
|
|
||||||
|
|
||||||
private String textMatrix;
|
|
||||||
private float[] position;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private int rotation;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private float pageHeight;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private float pageWidth;
|
|
||||||
|
|
||||||
private String unicode;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private float dir;
|
|
||||||
|
|
||||||
// not used in reanalysis
|
|
||||||
@JsonIgnore
|
|
||||||
private float widthOfSpace;
|
|
||||||
|
|
||||||
// not used in reanalysis
|
|
||||||
@JsonIgnore
|
|
||||||
private float fontSizeInPt;
|
|
||||||
|
|
||||||
// not used in reanalysis
|
|
||||||
@JsonIgnore
|
|
||||||
private String fontName;
|
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
public static RedTextPosition fromTextPosition(TextPosition textPosition) {
|
|
||||||
|
|
||||||
var pos = new RedTextPosition();
|
|
||||||
BeanUtils.copyProperties(textPosition, pos);
|
|
||||||
pos.setFontName(textPosition.getFont().getName());
|
|
||||||
|
|
||||||
pos.setFontSizeInPt(textPosition.getFontSizeInPt());
|
|
||||||
|
|
||||||
pos.setTextMatrix(textPosition.getTextMatrix().toString());
|
|
||||||
|
|
||||||
var position = new float[4];
|
|
||||||
|
|
||||||
position[0] = textPosition.getXDirAdj();
|
|
||||||
position[1] = textPosition.getYDirAdj();
|
|
||||||
position[2] = textPosition.getWidthDirAdj();
|
|
||||||
position[3] = textPosition.getHeightDir();
|
|
||||||
|
|
||||||
pos.setPosition(position);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getXDirAdj() {
|
|
||||||
|
|
||||||
return position[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getYDirAdj() {
|
|
||||||
|
|
||||||
return position[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getWidthDirAdj() {
|
|
||||||
|
|
||||||
return position[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getHeightDir() {
|
|
||||||
|
|
||||||
return position[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
public class SearchableText {
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final List<TextPositionSequence> sequences = new ArrayList<>();
|
|
||||||
|
|
||||||
|
|
||||||
public void add(TextPositionSequence textPositionSequence) {
|
|
||||||
|
|
||||||
sequences.add(textPositionSequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void addAll(List<TextPositionSequence> textPositionSequences) {
|
|
||||||
|
|
||||||
sequences.addAll(textPositionSequences);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
return buildString(sequences);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static String buildString(List<TextPositionSequence> sequences) {
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (TextPositionSequence word : sequences) {
|
|
||||||
sb.append(word);
|
|
||||||
sb.append(' ');
|
|
||||||
}
|
|
||||||
String text = sb.toString();
|
|
||||||
text = TextNormalizationUtilities.removeHyphenLineBreaks(text);
|
|
||||||
text = TextNormalizationUtilities.removeLineBreaks(text);
|
|
||||||
text = TextNormalizationUtilities.removeRepeatingWhitespaces(text);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class SimplifiedSectionText {
|
|
||||||
|
|
||||||
private int sectionNumber;
|
|
||||||
private String text;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class SimplifiedText {
|
|
||||||
|
|
||||||
private int numberOfPages;
|
|
||||||
private List<SimplifiedSectionText> sectionTexts = new ArrayList<>();
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
public class StringFrequencyCounter {
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final Map<String, Integer> countPerValue = new HashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
public void add(String value) {
|
|
||||||
|
|
||||||
if (!countPerValue.containsKey(value)) {
|
|
||||||
countPerValue.put(value, 1);
|
|
||||||
} else {
|
|
||||||
countPerValue.put(value, countPerValue.get(value) + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void addAll(Map<String, Integer> otherCounter) {
|
|
||||||
|
|
||||||
for (Map.Entry<String, Integer> entry : otherCounter.entrySet()) {
|
|
||||||
if (countPerValue.containsKey(entry.getKey())) {
|
|
||||||
countPerValue.put(entry.getKey(), countPerValue.get(entry.getKey()) + entry.getValue());
|
|
||||||
} else {
|
|
||||||
countPerValue.put(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public String getMostPopular() {
|
|
||||||
|
|
||||||
Map.Entry<String, Integer> mostPopular = null;
|
|
||||||
for (Map.Entry<String, Integer> entry : countPerValue.entrySet()) {
|
|
||||||
if (mostPopular == null || entry.getValue() > mostPopular.getValue()) {
|
|
||||||
mostPopular = entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mostPopular != null ? mostPopular.getKey() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonValue;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public enum TextDirection {
|
|
||||||
ZERO(0f),
|
|
||||||
QUARTER_CIRCLE(90f),
|
|
||||||
HALF_CIRCLE(180f),
|
|
||||||
THREE_QUARTER_CIRCLE(270f);
|
|
||||||
|
|
||||||
public static final String VALUE_STRING_SUFFIX = "°";
|
|
||||||
|
|
||||||
@JsonValue
|
|
||||||
private final float degrees;
|
|
||||||
private final float radians;
|
|
||||||
|
|
||||||
|
|
||||||
TextDirection(float degreeValue) {
|
|
||||||
|
|
||||||
degrees = degreeValue;
|
|
||||||
radians = (float) Math.toRadians(degreeValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
return degrees + VALUE_STRING_SUFFIX;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
|
||||||
public static TextDirection fromDegrees(float degrees) {
|
|
||||||
|
|
||||||
for (var dir : TextDirection.values()) {
|
|
||||||
if (degrees == dir.degrees) {
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalArgumentException(String.format("A value of %f is not supported by TextDirection", degrees));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,302 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.PageBlockType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class TextPageBlock extends AbstractPageBlock {
|
|
||||||
|
|
||||||
@Builder.Default
|
|
||||||
private List<TextPositionSequence> sequences = new ArrayList<>();
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private int rotation;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private String mostPopularWordFont;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private String mostPopularWordStyle;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private float mostPopularWordFontSize;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private float mostPopularWordHeight;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private float mostPopularWordSpaceWidth;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private float highestFontSize;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private PageBlockType classification;
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public TextDirection getDir() {
|
|
||||||
|
|
||||||
return sequences.get(0).getDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private float getPageHeight() {
|
|
||||||
|
|
||||||
return sequences.get(0).getPageHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private float getPageWidth() {
|
|
||||||
|
|
||||||
return sequences.get(0).getPageWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the minX value in pdf coordinate system.
|
|
||||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
|
||||||
* 0 -> LowerLeft
|
|
||||||
* 90 -> UpperLeft
|
|
||||||
* 180 -> UpperRight
|
|
||||||
* 270 -> LowerRight
|
|
||||||
*
|
|
||||||
* @return the minX value in pdf coordinate system
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
public float getPdfMinX() {
|
|
||||||
|
|
||||||
if (getDir().getDegrees() == 90) {
|
|
||||||
return minY;
|
|
||||||
} else if (getDir().getDegrees() == 180) {
|
|
||||||
return getPageWidth() - maxX;
|
|
||||||
|
|
||||||
} else if (getDir().getDegrees() == 270) {
|
|
||||||
|
|
||||||
return getPageWidth() - maxY;
|
|
||||||
} else {
|
|
||||||
return minX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the maxX value in pdf coordinate system.
|
|
||||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
|
||||||
* 0 -> LowerLeft
|
|
||||||
* 90 -> UpperLeft
|
|
||||||
* 180 -> UpperRight
|
|
||||||
* 270 -> LowerRight
|
|
||||||
*
|
|
||||||
* @return the maxX value in pdf coordinate system
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
public float getPdfMaxX() {
|
|
||||||
|
|
||||||
if (getDir().getDegrees() == 90) {
|
|
||||||
return maxY;
|
|
||||||
} else if (getDir().getDegrees() == 180) {
|
|
||||||
return getPageWidth() - minX;
|
|
||||||
} else if (getDir().getDegrees() == 270) {
|
|
||||||
return getPageWidth() - minY;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return maxX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the minY value in pdf coordinate system.
|
|
||||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
|
||||||
* 0 -> LowerLeft
|
|
||||||
* 90 -> UpperLeft
|
|
||||||
* 180 -> UpperRight
|
|
||||||
* 270 -> LowerRight
|
|
||||||
*
|
|
||||||
* @return the minY value in pdf coordinate system
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
public float getPdfMinY() {
|
|
||||||
|
|
||||||
if (getDir().getDegrees() == 90) {
|
|
||||||
return minX;
|
|
||||||
} else if (getDir().getDegrees() == 180) {
|
|
||||||
return maxY;
|
|
||||||
|
|
||||||
} else if (getDir().getDegrees() == 270) {
|
|
||||||
return getPageHeight() - maxX;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return getPageHeight() - maxY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the maxY value in pdf coordinate system.
|
|
||||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
|
||||||
* 0 -> LowerLeft
|
|
||||||
* 90 -> UpperLeft
|
|
||||||
* 180 -> UpperRight
|
|
||||||
* 270 -> LowerRight
|
|
||||||
*
|
|
||||||
* @return the maxY value in pdf coordinate system
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
public float getPdfMaxY() {
|
|
||||||
|
|
||||||
if (getDir().getDegrees() == 90) {
|
|
||||||
return maxX;
|
|
||||||
} else if (getDir().getDegrees() == 180) {
|
|
||||||
|
|
||||||
return minY;
|
|
||||||
} else if (getDir().getDegrees() == 270) {
|
|
||||||
return getPageHeight() - minX;
|
|
||||||
} else {
|
|
||||||
return getPageHeight() - minY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TextPageBlock(float minX, float maxX, float minY, float maxY, List<TextPositionSequence> sequences, int rotation) {
|
|
||||||
|
|
||||||
this.minX = minX;
|
|
||||||
this.maxX = maxX;
|
|
||||||
this.minY = minY;
|
|
||||||
this.maxY = maxY;
|
|
||||||
this.sequences = sequences;
|
|
||||||
this.rotation = rotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TextPageBlock union(TextPositionSequence r) {
|
|
||||||
|
|
||||||
TextPageBlock union = this.copy();
|
|
||||||
union.add(r);
|
|
||||||
return union;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TextPageBlock union(TextPageBlock r) {
|
|
||||||
|
|
||||||
TextPageBlock union = this.copy();
|
|
||||||
union.add(r);
|
|
||||||
return union;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void add(TextPageBlock r) {
|
|
||||||
|
|
||||||
if (r.getMinX() < minX) {
|
|
||||||
minX = r.getMinX();
|
|
||||||
}
|
|
||||||
if (r.getMaxX() > maxX) {
|
|
||||||
maxX = r.getMaxX();
|
|
||||||
}
|
|
||||||
if (r.getMinY() < minY) {
|
|
||||||
minY = r.getMinY();
|
|
||||||
}
|
|
||||||
if (r.getMaxY() > maxY) {
|
|
||||||
maxY = r.getMaxY();
|
|
||||||
}
|
|
||||||
sequences.addAll(r.getSequences());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void add(TextPositionSequence r) {
|
|
||||||
|
|
||||||
if (r.getMinXDirAdj() < minX) {
|
|
||||||
minX = r.getMinXDirAdj();
|
|
||||||
}
|
|
||||||
if (r.getMaxXDirAdj() > maxX) {
|
|
||||||
maxX = r.getMaxXDirAdj();
|
|
||||||
}
|
|
||||||
if (r.getMinYDirAdj() < minY) {
|
|
||||||
minY = r.getMinYDirAdj();
|
|
||||||
}
|
|
||||||
if (r.getMaxYDirAdj() > maxY) {
|
|
||||||
maxY = r.getMaxYDirAdj();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TextPageBlock copy() {
|
|
||||||
|
|
||||||
return new TextPageBlock(minX, maxX, minY, maxY, sequences, rotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void resize(float x1, float y1, float width, float height) {
|
|
||||||
|
|
||||||
set(x1, y1, x1 + width, y1 + height);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void set(float x1, float y1, float x2, float y2) {
|
|
||||||
|
|
||||||
this.minX = Math.min(x1, x2);
|
|
||||||
this.maxX = Math.max(x1, x2);
|
|
||||||
this.minY = Math.min(y1, y2);
|
|
||||||
this.maxY = Math.max(y1, y2);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
|
|
||||||
for (int i = 0; i < sequences.size(); i++) {
|
|
||||||
String sequenceAsString = sequences.get(i).toString();
|
|
||||||
// Fix for missing Whitespace. This is recognized in getSequences method. See PDFTextStripper Line 1730.
|
|
||||||
if (i != 0 && sequences.get(i - 1).charAt(sequences.get(i - 1).length() - 1) != ' ' && sequenceAsString.charAt(0) != ' ') {
|
|
||||||
builder.append(' ');
|
|
||||||
}
|
|
||||||
builder.append(sequenceAsString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.toString();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@JsonIgnore
|
|
||||||
public String getText() {
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
TextPositionSequence previous = null;
|
|
||||||
for (TextPositionSequence word : sequences) {
|
|
||||||
if (previous != null) {
|
|
||||||
if (Math.abs(previous.getMaxYDirAdj() - word.getMaxYDirAdj()) > word.getTextHeight()) {
|
|
||||||
sb.append('\n');
|
|
||||||
} else {
|
|
||||||
sb.append(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.append(word.toString());
|
|
||||||
previous = word;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TextNormalizationUtilities.removeHyphenLineBreaks(sb.toString());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,302 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text;
|
|
||||||
|
|
||||||
import java.awt.geom.AffineTransform;
|
|
||||||
import java.awt.geom.Point2D;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@JsonIgnoreProperties({"empty"})
|
|
||||||
public class TextPositionSequence implements CharSequence {
|
|
||||||
|
|
||||||
public static final int HEIGHT_PADDING = 2;
|
|
||||||
private int page;
|
|
||||||
private List<RedTextPosition> textPositions = new ArrayList<>();
|
|
||||||
|
|
||||||
private TextDirection dir;
|
|
||||||
private int rotation;
|
|
||||||
private float pageHeight;
|
|
||||||
private float pageWidth;
|
|
||||||
|
|
||||||
|
|
||||||
public TextPositionSequence(int page) {
|
|
||||||
|
|
||||||
this.page = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TextPositionSequence(List<TextPosition> textPositions, int page) {
|
|
||||||
|
|
||||||
this.textPositions = textPositions.stream().map(RedTextPosition::fromTextPosition).collect(Collectors.toList());
|
|
||||||
this.page = page;
|
|
||||||
this.dir = TextDirection.fromDegrees(textPositions.get(0).getDir());
|
|
||||||
this.rotation = textPositions.get(0).getRotation();
|
|
||||||
this.pageHeight = textPositions.get(0).getPageHeight();
|
|
||||||
this.pageWidth = textPositions.get(0).getPageWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int length() {
|
|
||||||
|
|
||||||
return textPositions.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public char charAt(int index) {
|
|
||||||
|
|
||||||
RedTextPosition textPosition = textPositionAt(index);
|
|
||||||
String text = textPosition.getUnicode();
|
|
||||||
return text.charAt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public char charAt(int index, boolean caseInSensitive) {
|
|
||||||
|
|
||||||
RedTextPosition textPosition = textPositionAt(index);
|
|
||||||
String text = textPosition.getUnicode();
|
|
||||||
return caseInSensitive ? text.toLowerCase(Locale.ROOT).charAt(0) : text.charAt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextPositionSequence subSequence(int start, int end) {
|
|
||||||
|
|
||||||
var textPositionSequence = new TextPositionSequence();
|
|
||||||
textPositionSequence.textPositions = textPositions.subList(start, end);
|
|
||||||
textPositionSequence.page = page;
|
|
||||||
textPositionSequence.dir = dir;
|
|
||||||
textPositionSequence.rotation = rotation;
|
|
||||||
textPositionSequence.pageHeight = pageHeight;
|
|
||||||
textPositionSequence.pageWidth = pageWidth;
|
|
||||||
|
|
||||||
return textPositionSequence;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder(length());
|
|
||||||
for (int i = 0; i < length(); i++) {
|
|
||||||
builder.append(charAt(i));
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public RedTextPosition textPositionAt(int index) {
|
|
||||||
|
|
||||||
return textPositions.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void add(TextPositionSequence textPositionSequence, RedTextPosition textPosition) {
|
|
||||||
|
|
||||||
this.textPositions.add(textPosition);
|
|
||||||
this.page = textPositionSequence.getPage();
|
|
||||||
this.dir = textPositionSequence.getDir();
|
|
||||||
this.rotation = textPositionSequence.getRotation();
|
|
||||||
this.pageHeight = textPositionSequence.getPageHeight();
|
|
||||||
this.pageWidth = textPositionSequence.getPageWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void add(TextPosition textPosition) {
|
|
||||||
|
|
||||||
this.textPositions.add(RedTextPosition.fromTextPosition(textPosition));
|
|
||||||
|
|
||||||
this.dir = TextDirection.fromDegrees(textPositions.get(0).getDir());
|
|
||||||
this.rotation = textPositions.get(0).getRotation();
|
|
||||||
this.pageHeight = textPositions.get(0).getPageHeight();
|
|
||||||
this.pageWidth = textPositions.get(0).getPageWidth();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This value is adjusted so that 0,0 is upper left and it is adjusted based on the text direction.
|
|
||||||
* This method ignores the page rotation but takes the text rotation and adjusts the coordinates to awt.
|
|
||||||
*
|
|
||||||
* @return the text direction adjusted minX value
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
public float getMinXDirAdj() {
|
|
||||||
|
|
||||||
return textPositions.get(0).getXDirAdj();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This value is adjusted so that 0,0 is upper left and it is adjusted based on the text direction.
|
|
||||||
* This method ignores the page rotation but takes the text rotation and adjusts the coordinates to awt.
|
|
||||||
*
|
|
||||||
* @return the text direction adjusted maxX value
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
public float getMaxXDirAdj() {
|
|
||||||
|
|
||||||
return textPositions.get(textPositions.size() - 1).getXDirAdj() + textPositions.get(textPositions.size() - 1).getWidthDirAdj() + HEIGHT_PADDING;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This value is adjusted so that 0,0 is upper left and it is adjusted based on the text direction.
|
|
||||||
* This method ignores the page rotation but takes the text rotation and adjusts the coordinates to awt.
|
|
||||||
*
|
|
||||||
* @return the text direction adjusted minY value. The upper border of the bounding box of the word.
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
public float getMinYDirAdj() {
|
|
||||||
|
|
||||||
return textPositions.get(0).getYDirAdj() - getTextHeight();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This value is adjusted so that 0,0 is upper left and it is adjusted based on the text direction.
|
|
||||||
* This method ignores the page rotation but takes the text rotation and adjusts the coordinates to awt.
|
|
||||||
*
|
|
||||||
* @return the text direction adjusted maxY value. The lower border of the bounding box of the word.
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
public float getMaxYDirAdj() {
|
|
||||||
|
|
||||||
return textPositions.get(0).getYDirAdj();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getTextHeight() {
|
|
||||||
|
|
||||||
return textPositions.get(0).getHeightDir() + HEIGHT_PADDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getHeight() {
|
|
||||||
|
|
||||||
return getMaxYDirAdj() - getMinYDirAdj();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getWidth() {
|
|
||||||
|
|
||||||
return getMaxXDirAdj() - getMinXDirAdj();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public String getFont() {
|
|
||||||
|
|
||||||
return textPositions.get(0).getFontName().toLowerCase(Locale.ROOT).replaceAll(",bold", "").replaceAll(",italic", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public String getFontStyle() {
|
|
||||||
|
|
||||||
String lowercaseFontName = textPositions.get(0).getFontName().toLowerCase(Locale.ROOT);
|
|
||||||
|
|
||||||
if (lowercaseFontName.contains("bold") && lowercaseFontName.contains("italic")) {
|
|
||||||
return "bold, italic";
|
|
||||||
} else if (lowercaseFontName.contains("bold")) {
|
|
||||||
return "bold";
|
|
||||||
} else if (lowercaseFontName.contains("italic")) {
|
|
||||||
return "italic";
|
|
||||||
} else {
|
|
||||||
return "standard";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getFontSize() {
|
|
||||||
|
|
||||||
return textPositions.get(0).getFontSizeInPt();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public float getSpaceWidth() {
|
|
||||||
|
|
||||||
return textPositions.get(0).getWidthOfSpace();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This returns the bounding box of the word in Pdf Coordinate System where {0,0} rotated with the page rotation.
|
|
||||||
* 0 -> LowerLeft
|
|
||||||
* 90 -> UpperLeft
|
|
||||||
* 180 -> UpperRight
|
|
||||||
* 270 -> LowerRight
|
|
||||||
*
|
|
||||||
* @return bounding box of the word in Pdf Coordinate System
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
@SneakyThrows
|
|
||||||
public Rectangle getRectangle() {
|
|
||||||
|
|
||||||
log.debug("Page: '{}', Word: '{}', Rotation: '{}', textRotation {}", page, this, rotation, dir);
|
|
||||||
|
|
||||||
float textHeight = getTextHeight();
|
|
||||||
|
|
||||||
RedTextPosition firstTextPos = textPositions.get(0);
|
|
||||||
RedTextPosition lastTextPos = textPositions.get(textPositions.size() - 1);
|
|
||||||
|
|
||||||
Point2D bottomLeft = new Point2D.Double(firstTextPos.getXDirAdj(), firstTextPos.getYDirAdj() - HEIGHT_PADDING);
|
|
||||||
Point2D topRight = new Point2D.Double(lastTextPos.getXDirAdj() + lastTextPos.getWidthDirAdj(), lastTextPos.getYDirAdj() + textHeight + HEIGHT_PADDING);
|
|
||||||
|
|
||||||
AffineTransform transform = new AffineTransform();
|
|
||||||
if (dir == TextDirection.ZERO || dir == TextDirection.HALF_CIRCLE) {
|
|
||||||
transform.rotate(dir.getRadians(), pageWidth / 2f, pageHeight / 2f);
|
|
||||||
transform.translate(0f, pageHeight + textHeight);
|
|
||||||
transform.scale(1., -1.);
|
|
||||||
} else if (dir == TextDirection.QUARTER_CIRCLE) {
|
|
||||||
transform.rotate(dir.getRadians(), pageWidth / 2f, pageWidth / 2f);
|
|
||||||
transform.translate(0f, pageWidth + textHeight);
|
|
||||||
transform.scale(1., -1.);
|
|
||||||
} else {
|
|
||||||
transform.rotate(dir.getRadians(), pageHeight / 2f, pageHeight / 2f);
|
|
||||||
transform.translate(0f, pageWidth + textHeight);
|
|
||||||
transform.scale(1., -1.);
|
|
||||||
}
|
|
||||||
|
|
||||||
bottomLeft = transform.transform(bottomLeft, null);
|
|
||||||
topRight = transform.transform(topRight, null);
|
|
||||||
|
|
||||||
return new Rectangle( //
|
|
||||||
new Point((float) bottomLeft.getX(), (float) bottomLeft.getY()),
|
|
||||||
(float) (topRight.getX() - bottomLeft.getX()),
|
|
||||||
(float) (topRight.getY() - bottomLeft.getY()),
|
|
||||||
page);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class UnclassifiedText {
|
|
||||||
|
|
||||||
private List<TextPageBlock> textBlocks;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,384 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.parsing;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.WeakHashMap;
|
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
import org.apache.fontbox.ttf.TrueTypeFont;
|
|
||||||
import org.apache.fontbox.util.BoundingBox;
|
|
||||||
import org.apache.pdfbox.contentstream.PDFStreamEngine;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.DrawObject;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.Concatenate;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.Restore;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.Save;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.SetGraphicsStateParameters;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.SetMatrix;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.BeginText;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.EndText;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.MoveText;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.MoveTextSetLeading;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.NextLine;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.SetCharSpacing;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.SetFontAndSize;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.SetTextHorizontalScaling;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.SetTextLeading;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.SetTextRenderingMode;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.SetTextRise;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.SetWordSpacing;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.ShowText;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.ShowTextAdjusted;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.ShowTextLine;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.ShowTextLineAndSpace;
|
|
||||||
import org.apache.pdfbox.cos.COSDictionary;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDCIDFont;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDCIDFontType2;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDSimpleFont;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDType3Font;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.encoding.GlyphList;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState;
|
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
|
||||||
import org.apache.pdfbox.util.Matrix;
|
|
||||||
import org.apache.pdfbox.util.Vector;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LEGACY text calculations which are known to be incorrect but are depended on by PDFTextStripper.
|
|
||||||
* <p>
|
|
||||||
* This class exists only so that we don't break the code of users who have their own subclasses of
|
|
||||||
* PDFTextStripper. It replaces the mostly empty implementation of showGlyph() in PDFStreamEngine
|
|
||||||
* with a heuristic implementation which is backwards compatible.
|
|
||||||
* <p>
|
|
||||||
* DO NOT USE THIS CODE UNLESS YOU ARE WORKING WITH PDFTextStripper.
|
|
||||||
* THIS CODE IS DELIBERATELY INCORRECT, USE PDFStreamEngine INSTEAD.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"PMD", "checkstyle:all"})
|
|
||||||
class LegacyPDFStreamEngine extends PDFStreamEngine {
|
|
||||||
|
|
||||||
private static final Log LOG = LogFactory.getLog(LegacyPDFStreamEngine.class);
|
|
||||||
|
|
||||||
private int pageRotation;
|
|
||||||
private PDRectangle pageSize;
|
|
||||||
private Matrix translateMatrix;
|
|
||||||
private final GlyphList glyphList;
|
|
||||||
private final Map<COSDictionary, Float> fontHeightMap = new WeakHashMap<COSDictionary, Float>();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*/
|
|
||||||
LegacyPDFStreamEngine() throws IOException {
|
|
||||||
|
|
||||||
addOperator(new BeginText());
|
|
||||||
addOperator(new Concatenate());
|
|
||||||
addOperator(new DrawObject()); // special text version
|
|
||||||
addOperator(new EndText());
|
|
||||||
addOperator(new SetGraphicsStateParameters());
|
|
||||||
addOperator(new Save());
|
|
||||||
addOperator(new Restore());
|
|
||||||
addOperator(new NextLine());
|
|
||||||
addOperator(new SetCharSpacing());
|
|
||||||
addOperator(new MoveText());
|
|
||||||
addOperator(new MoveTextSetLeading());
|
|
||||||
addOperator(new SetFontAndSize());
|
|
||||||
addOperator(new ShowText());
|
|
||||||
addOperator(new ShowTextAdjusted());
|
|
||||||
addOperator(new SetTextLeading());
|
|
||||||
addOperator(new SetMatrix());
|
|
||||||
addOperator(new SetTextRenderingMode());
|
|
||||||
addOperator(new SetTextRise());
|
|
||||||
addOperator(new SetWordSpacing());
|
|
||||||
addOperator(new SetTextHorizontalScaling());
|
|
||||||
addOperator(new ShowTextLine());
|
|
||||||
addOperator(new ShowTextLineAndSpace());
|
|
||||||
|
|
||||||
// load additional glyph list for Unicode mapping
|
|
||||||
String path = "/org/apache/pdfbox/resources/glyphlist/additional.txt";
|
|
||||||
InputStream input = GlyphList.class.getResourceAsStream(path);
|
|
||||||
glyphList = new GlyphList(GlyphList.getAdobeGlyphList(), input);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will initialize and process the contents of the stream.
|
|
||||||
*
|
|
||||||
* @param page the page to process
|
|
||||||
* @throws java.io.IOException if there is an error accessing the stream.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void processPage(PDPage page) throws IOException {
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a glyph is to be processed. The heuristic calculations here were originally
|
|
||||||
* written by Ben Litchfield for PDFStreamEngine.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, String unicode, Vector displacement) throws IOException {
|
|
||||||
//
|
|
||||||
// legacy calculations which were previously in PDFStreamEngine
|
|
||||||
//
|
|
||||||
// DO NOT USE THIS CODE UNLESS YOU ARE WORKING WITH PDFTextStripper.
|
|
||||||
// THIS CODE IS DELIBERATELY INCORRECT
|
|
||||||
//
|
|
||||||
|
|
||||||
PDGraphicsState state = getGraphicsState();
|
|
||||||
Matrix ctm = state.getCurrentTransformationMatrix();
|
|
||||||
float fontSize = state.getTextState().getFontSize();
|
|
||||||
float horizontalScaling = state.getTextState().getHorizontalScaling() / 100f;
|
|
||||||
Matrix textMatrix = getTextMatrix();
|
|
||||||
|
|
||||||
float displacementX = displacement.getX();
|
|
||||||
// the sorting algorithm is based on the width of the character. As the displacement
|
|
||||||
// for vertical characters doesn't provide any suitable value for it, we have to
|
|
||||||
// calculate our own
|
|
||||||
if (font.isVertical()) {
|
|
||||||
displacementX = font.getWidth(code) / 1000;
|
|
||||||
// there may be an additional scaling factor for true type fonts
|
|
||||||
TrueTypeFont ttf = null;
|
|
||||||
if (font instanceof PDTrueTypeFont) {
|
|
||||||
ttf = ((PDTrueTypeFont) font).getTrueTypeFont();
|
|
||||||
} else if (font instanceof PDType0Font) {
|
|
||||||
PDCIDFont cidFont = ((PDType0Font) font).getDescendantFont();
|
|
||||||
if (cidFont instanceof PDCIDFontType2) {
|
|
||||||
ttf = ((PDCIDFontType2) cidFont).getTrueTypeFont();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ttf != null && ttf.getUnitsPerEm() != 1000) {
|
|
||||||
displacementX *= 1000f / ttf.getUnitsPerEm();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// legacy calculations which were previously in PDFStreamEngine
|
|
||||||
//
|
|
||||||
// DO NOT USE THIS CODE UNLESS YOU ARE WORKING WITH PDFTextStripper.
|
|
||||||
// THIS CODE IS DELIBERATELY INCORRECT
|
|
||||||
//
|
|
||||||
|
|
||||||
// (modified) combined displacement, this is calculated *without* taking the character
|
|
||||||
// spacing and word spacing into account, due to legacy code in TextStripper
|
|
||||||
float tx = displacementX * fontSize * horizontalScaling;
|
|
||||||
float ty = displacement.getY() * fontSize;
|
|
||||||
|
|
||||||
// (modified) combined displacement matrix
|
|
||||||
Matrix td = Matrix.getTranslateInstance(tx, ty);
|
|
||||||
|
|
||||||
// (modified) text rendering matrix
|
|
||||||
Matrix nextTextRenderingMatrix = td.multiply(textMatrix).multiply(ctm); // text space -> device space
|
|
||||||
float nextX = nextTextRenderingMatrix.getTranslateX();
|
|
||||||
float nextY = nextTextRenderingMatrix.getTranslateY();
|
|
||||||
|
|
||||||
// (modified) width and height calculations
|
|
||||||
float dxDisplay = nextX - textRenderingMatrix.getTranslateX();
|
|
||||||
Float fontHeight = fontHeightMap.get(font.getCOSObject());
|
|
||||||
if (fontHeight == null) {
|
|
||||||
fontHeight = computeFontHeight(font);
|
|
||||||
fontHeightMap.put(font.getCOSObject(), fontHeight);
|
|
||||||
}
|
|
||||||
float dyDisplay = fontHeight * textRenderingMatrix.getScalingFactorY();
|
|
||||||
|
|
||||||
//
|
|
||||||
// start of the original method
|
|
||||||
//
|
|
||||||
|
|
||||||
// Note on variable names. There are three different units being used in this code.
|
|
||||||
// Character sizes are given in glyph units, text locations are initially given in text
|
|
||||||
// units, and we want to save the data in display units. The variable names should end with
|
|
||||||
// Text or Disp to represent if the values are in text or disp units (no glyph units are
|
|
||||||
// saved).
|
|
||||||
|
|
||||||
float glyphSpaceToTextSpaceFactor = 1 / 1000f;
|
|
||||||
if (font instanceof PDType3Font) {
|
|
||||||
glyphSpaceToTextSpaceFactor = font.getFontMatrix().getScaleX();
|
|
||||||
}
|
|
||||||
|
|
||||||
float spaceWidthText = 0;
|
|
||||||
try {
|
|
||||||
// to avoid crash as described in PDFBOX-614, see what the space displacement should be
|
|
||||||
spaceWidthText = font.getSpaceWidth() * glyphSpaceToTextSpaceFactor;
|
|
||||||
} catch (Throwable exception) {
|
|
||||||
LOG.warn(exception, exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spaceWidthText == 0) {
|
|
||||||
spaceWidthText = font.getAverageFontWidth() * glyphSpaceToTextSpaceFactor;
|
|
||||||
// the average space width appears to be higher than necessary so make it smaller
|
|
||||||
spaceWidthText *= .80f;
|
|
||||||
}
|
|
||||||
if (spaceWidthText == 0) {
|
|
||||||
spaceWidthText = 1.0f; // if could not find font, use a generic value
|
|
||||||
}
|
|
||||||
|
|
||||||
// the space width has to be transformed into display units
|
|
||||||
float spaceWidthDisplay = spaceWidthText * textRenderingMatrix.getScalingFactorX();
|
|
||||||
|
|
||||||
// use our additional glyph list for Unicode mapping
|
|
||||||
String unicodeMapping = font.toUnicode(code, glyphList);
|
|
||||||
|
|
||||||
// when there is no Unicode mapping available, Acrobat simply coerces the character code
|
|
||||||
// into Unicode, so we do the same. Subclasses of PDFStreamEngine don't necessarily want
|
|
||||||
// this, which is why we leave it until this point in PDFTextStreamEngine.
|
|
||||||
if (unicodeMapping == null) {
|
|
||||||
if (font instanceof PDSimpleFont) {
|
|
||||||
char c = (char) code;
|
|
||||||
unicodeMapping = new String(new char[]{c});
|
|
||||||
} else {
|
|
||||||
// Acrobat doesn't seem to coerce composite font's character codes, instead it
|
|
||||||
// skips them. See the "allah2.pdf" TestTextStripper file.
|
|
||||||
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(),
|
|
||||||
translatedTextRenderingMatrix,
|
|
||||||
nextX,
|
|
||||||
nextY,
|
|
||||||
Math.abs(dyDisplay),
|
|
||||||
dxDisplay,
|
|
||||||
Math.abs(spaceWidthDisplay),
|
|
||||||
Character.toString(unicodeMapping.charAt(0)),
|
|
||||||
new int[]{code},
|
|
||||||
font,
|
|
||||||
fontSize,
|
|
||||||
(int) (fontSize * textMatrix.getScalingFactorX())));
|
|
||||||
processTextPosition(new TextPosition(pageRotation,
|
|
||||||
pageSize.getWidth(),
|
|
||||||
pageSize.getHeight(),
|
|
||||||
translatedTextRenderingMatrix,
|
|
||||||
nextX,
|
|
||||||
nextY,
|
|
||||||
Math.abs(dyDisplay),
|
|
||||||
dxDisplay,
|
|
||||||
Math.abs(spaceWidthDisplay),
|
|
||||||
Character.toString(unicodeMapping.charAt(1)),
|
|
||||||
new int[]{code},
|
|
||||||
font,
|
|
||||||
fontSize,
|
|
||||||
(int) (fontSize * textMatrix.getScalingFactorX())));
|
|
||||||
} else {
|
|
||||||
|
|
||||||
processTextPosition(new TextPosition(pageRotation,
|
|
||||||
pageSize.getWidth(),
|
|
||||||
pageSize.getHeight(),
|
|
||||||
translatedTextRenderingMatrix,
|
|
||||||
nextX,
|
|
||||||
nextY,
|
|
||||||
Math.abs(dyDisplay),
|
|
||||||
dxDisplay,
|
|
||||||
Math.abs(spaceWidthDisplay),
|
|
||||||
unicodeMapping,
|
|
||||||
new int[]{code},
|
|
||||||
font,
|
|
||||||
fontSize,
|
|
||||||
(int) (fontSize * textMatrix.getScalingFactorX())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the font height. Override this if you want to use own calculations.
|
|
||||||
*
|
|
||||||
* @param font the font.
|
|
||||||
* @return the font height.
|
|
||||||
* @throws IOException if there is an error while getting the font bounding box.
|
|
||||||
*/
|
|
||||||
protected float computeFontHeight(PDFont font) throws IOException {
|
|
||||||
|
|
||||||
BoundingBox bbox = font.getBoundingBox();
|
|
||||||
if (bbox.getLowerLeftY() < Short.MIN_VALUE) {
|
|
||||||
// PDFBOX-2158 and PDFBOX-3130
|
|
||||||
// files by Salmat eSolutions / ClibPDF Library
|
|
||||||
bbox.setLowerLeftY(-(bbox.getLowerLeftY() + 65536));
|
|
||||||
}
|
|
||||||
// 1/2 the bbox is used as the height todo: why?
|
|
||||||
float glyphHeight = bbox.getHeight() / 2;
|
|
||||||
|
|
||||||
// sometimes the bbox has very high values, but CapHeight is OK
|
|
||||||
PDFontDescriptor fontDescriptor = font.getFontDescriptor();
|
|
||||||
if (fontDescriptor != null) {
|
|
||||||
float capHeight = fontDescriptor.getCapHeight();
|
|
||||||
if (Float.compare(capHeight, 0) != 0 && (capHeight < glyphHeight || Float.compare(glyphHeight, 0) == 0)) {
|
|
||||||
glyphHeight = capHeight;
|
|
||||||
}
|
|
||||||
// PDFBOX-3464, PDFBOX-4480, PDFBOX-4553:
|
|
||||||
// sometimes even CapHeight has very high value, but Ascent and Descent are ok
|
|
||||||
float ascent = fontDescriptor.getAscent();
|
|
||||||
float descent = fontDescriptor.getDescent();
|
|
||||||
if (capHeight > ascent && ascent > 0 && descent < 0 && ((ascent - descent) / 2 < glyphHeight || Float.compare(glyphHeight, 0) == 0)) {
|
|
||||||
glyphHeight = (ascent - descent) / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// transformPoint from glyph space -> text space
|
|
||||||
float height;
|
|
||||||
if (font instanceof PDType3Font) {
|
|
||||||
height = font.getFontMatrix().transformPoint(0, glyphHeight).y;
|
|
||||||
} else {
|
|
||||||
height = glyphHeight / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A method provided as an event interface to allow a subclass to perform some specific
|
|
||||||
* functionality when text needs to be processed.
|
|
||||||
*
|
|
||||||
* @param text The text to be processed.
|
|
||||||
*/
|
|
||||||
protected void processTextPosition(TextPosition text) {
|
|
||||||
// subclasses can override to provide specific functionality
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,335 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.parsing;
|
|
||||||
|
|
||||||
import java.awt.geom.Point2D;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.contentstream.operator.Operator;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.OperatorName;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColor;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColorN;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColorSpace;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingDeviceCMYKColor;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingDeviceGrayColor;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingDeviceRGBColor;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetStrokingColor;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetStrokingColorN;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetStrokingColorSpace;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetStrokingDeviceCMYKColor;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetStrokingDeviceGrayColor;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.color.SetStrokingDeviceRGBColor;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.SetFlatness;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.SetLineCapStyle;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.SetLineDashPattern;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.SetLineJoinStyle;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.SetLineMiterLimit;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.SetLineWidth;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.state.SetRenderingIntent;
|
|
||||||
import org.apache.pdfbox.contentstream.operator.text.SetFontAndSize;
|
|
||||||
import org.apache.pdfbox.cos.COSBase;
|
|
||||||
import org.apache.pdfbox.cos.COSNumber;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Ruling;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.RedTextPosition;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class PDFLinesTextStripper extends PDFTextStripper {
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final List<TextPositionSequence> textPositionSequences = new ArrayList<>();
|
|
||||||
@Getter
|
|
||||||
private final List<Ruling> rulings = new ArrayList<>();
|
|
||||||
private final List<Ruling> graphicsPath = new ArrayList<>();
|
|
||||||
@Setter
|
|
||||||
protected PDPage pdpage;
|
|
||||||
@Getter
|
|
||||||
private int minCharWidth;
|
|
||||||
@Getter
|
|
||||||
private int maxCharWidth;
|
|
||||||
@Getter
|
|
||||||
private int minCharHeight;
|
|
||||||
@Getter
|
|
||||||
private int maxCharHeight;
|
|
||||||
|
|
||||||
private float path_x;
|
|
||||||
private float path_y;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
private int pageNumber;
|
|
||||||
|
|
||||||
|
|
||||||
public PDFLinesTextStripper() throws IOException {
|
|
||||||
|
|
||||||
super();
|
|
||||||
this.addOperator(new SetStrokingColorSpace());
|
|
||||||
this.addOperator(new SetNonStrokingColorSpace());
|
|
||||||
this.addOperator(new SetLineDashPattern());
|
|
||||||
this.addOperator(new SetStrokingDeviceGrayColor());
|
|
||||||
this.addOperator(new SetNonStrokingDeviceGrayColor());
|
|
||||||
this.addOperator(new SetFlatness());
|
|
||||||
this.addOperator(new SetLineJoinStyle());
|
|
||||||
this.addOperator(new SetLineCapStyle());
|
|
||||||
this.addOperator(new SetStrokingDeviceCMYKColor());
|
|
||||||
this.addOperator(new SetNonStrokingDeviceCMYKColor());
|
|
||||||
this.addOperator(new SetLineMiterLimit());
|
|
||||||
this.addOperator(new SetStrokingDeviceRGBColor());
|
|
||||||
this.addOperator(new SetNonStrokingDeviceRGBColor());
|
|
||||||
this.addOperator(new SetRenderingIntent());
|
|
||||||
this.addOperator(new SetStrokingColor());
|
|
||||||
this.addOperator(new SetNonStrokingColor());
|
|
||||||
this.addOperator(new SetStrokingColorN());
|
|
||||||
this.addOperator(new SetNonStrokingColorN());
|
|
||||||
this.addOperator(new SetFontAndSize());
|
|
||||||
this.addOperator(new SetLineWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void processOperator(Operator operator, List<COSBase> arguments) throws IOException {
|
|
||||||
|
|
||||||
String operation = operator.getName();
|
|
||||||
|
|
||||||
//move
|
|
||||||
switch (operation) {
|
|
||||||
case OperatorName.MOVE_TO:
|
|
||||||
if (arguments.size() == 2) {
|
|
||||||
Point2D.Float pos = transformPosition(floatValue(arguments.get(0)), floatValue(arguments.get(1)));
|
|
||||||
path_x = (float) pos.getX();
|
|
||||||
path_y = (float) pos.getY();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
//line
|
|
||||||
case OperatorName.LINE_TO:
|
|
||||||
if (arguments.size() == 2) {
|
|
||||||
Point2D.Float pos = transformPosition(floatValue(arguments.get(0)), floatValue(arguments.get(1)));
|
|
||||||
|
|
||||||
// The direction of vertical lines must always be from bottom to top for the table extraction algorithm.
|
|
||||||
if (pos.getY() > path_y) {
|
|
||||||
graphicsPath.add(new Ruling(new Point2D.Float(path_x, path_y), new Point2D.Float((float) pos.getX(), (float) pos.getY())));
|
|
||||||
} else {
|
|
||||||
graphicsPath.add(new Ruling(new Point2D.Float(path_x, (float) pos.getY()), new Point2D.Float((float) pos.getX(), path_y)));
|
|
||||||
}
|
|
||||||
|
|
||||||
path_x = (float) pos.getX();
|
|
||||||
path_y = (float) pos.getY();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
//rectangle
|
|
||||||
case OperatorName.APPEND_RECT:
|
|
||||||
|
|
||||||
if (arguments.size() == 4) {
|
|
||||||
float x = floatValue(arguments.get(0));
|
|
||||||
float y = floatValue(arguments.get(1));
|
|
||||||
float width = floatValue(arguments.get(2));
|
|
||||||
float height = floatValue(arguments.get(3));
|
|
||||||
|
|
||||||
Point2D p1 = transformPosition(x, y);
|
|
||||||
Point2D p2 = transformPosition(x + width, y + height);
|
|
||||||
|
|
||||||
// Horizontal lines
|
|
||||||
graphicsPath.add(new Ruling(new Point2D.Float((float) p1.getX(), (float) p1.getY()), new Point2D.Float((float) p2.getX(), (float) p1.getY())));
|
|
||||||
graphicsPath.add(new Ruling(new Point2D.Float((float) p1.getX(), (float) p2.getY()), new Point2D.Float((float) p2.getX(), (float) p2.getY())));
|
|
||||||
|
|
||||||
// Vertical lines, direction must always be from bottom to top for the table extraction algorithm.
|
|
||||||
if (p2.getY() > p1.getY()) {
|
|
||||||
graphicsPath.add(new Ruling(new Point2D.Float((float) p2.getX(), (float) p1.getY()), new Point2D.Float((float) p2.getX(), (float) p2.getY())));
|
|
||||||
} else {
|
|
||||||
graphicsPath.add(new Ruling(new Point2D.Float((float) p2.getX(), (float) p2.getY()), new Point2D.Float((float) p2.getX(), (float) p1.getY())));
|
|
||||||
}
|
|
||||||
if (p2.getY() > p1.getY()) {
|
|
||||||
graphicsPath.add(new Ruling(new Point2D.Float((float) p1.getX(), (float) p1.getY()), new Point2D.Float((float) p1.getX(), (float) p2.getY())));
|
|
||||||
} else {
|
|
||||||
graphicsPath.add(new Ruling(new Point2D.Float((float) p1.getX(), (float) p2.getY()), new Point2D.Float((float) p1.getX(), (float) p1.getY())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
//fill
|
|
||||||
case OperatorName.FILL_NON_ZERO:
|
|
||||||
case OperatorName.LEGACY_FILL_NON_ZERO:
|
|
||||||
case OperatorName.FILL_EVEN_ODD:
|
|
||||||
addVisibleRulings(graphicsPath, false);
|
|
||||||
graphicsPath.clear();
|
|
||||||
break;
|
|
||||||
|
|
||||||
//stroke
|
|
||||||
case OperatorName.STROKE_PATH:
|
|
||||||
addVisibleRulings(graphicsPath, true);
|
|
||||||
graphicsPath.clear();
|
|
||||||
break;
|
|
||||||
|
|
||||||
//cancel path
|
|
||||||
case OperatorName.ENDPATH:
|
|
||||||
graphicsPath.clear();
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
super.processOperator(operator, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private float floatValue(COSBase value) {
|
|
||||||
|
|
||||||
if (value instanceof COSNumber) {
|
|
||||||
return ((COSNumber) value).floatValue();
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Point2D.Float transformPosition(float x, float y) {
|
|
||||||
|
|
||||||
return super.transformedPoint(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void addVisibleRulings(List<Ruling> path, boolean stroke) throws IOException {
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (stroke && !getGraphicsState().getStrokingColor().isPattern() && getGraphicsState().getStrokingColor()
|
|
||||||
.toRGB() == 0 || !stroke && !getGraphicsState().getNonStrokingColor().isPattern() && getGraphicsState().getNonStrokingColor().toRGB() == 0) {
|
|
||||||
rulings.addAll(path);
|
|
||||||
}
|
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
log.debug("UnsupportedOperationException: " + getGraphicsState().getStrokingColor().getColorSpace().getName() + " or " + getGraphicsState().getNonStrokingColor()
|
|
||||||
.getColorSpace()
|
|
||||||
.getName() + " does not support toRGB");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeString(String text, List<TextPosition> textPositions) throws IOException {
|
|
||||||
|
|
||||||
int startIndex = 0;
|
|
||||||
RedTextPosition previous = null;
|
|
||||||
|
|
||||||
textPositions.sort(Comparator.comparing(TextPosition::getXDirAdj));
|
|
||||||
|
|
||||||
for (int i = 0; i <= textPositions.size() - 1; i++) {
|
|
||||||
|
|
||||||
if (!textPositionSequences.isEmpty()) {
|
|
||||||
previous = textPositionSequences.get(textPositionSequences.size() - 1)
|
|
||||||
.getTextPositions()
|
|
||||||
.get(textPositionSequences.get(textPositionSequences.size() - 1).getTextPositions().size() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int charWidth = (int) textPositions.get(i).getWidthDirAdj();
|
|
||||||
if (charWidth < minCharWidth) {
|
|
||||||
minCharWidth = charWidth;
|
|
||||||
}
|
|
||||||
if (charWidth > maxCharWidth) {
|
|
||||||
maxCharWidth = charWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
int charHeight = (int) textPositions.get(i).getHeightDir();
|
|
||||||
if (charHeight < minCharHeight) {
|
|
||||||
minCharHeight = charHeight;
|
|
||||||
}
|
|
||||||
if (charWidth > maxCharHeight) {
|
|
||||||
maxCharHeight = charHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i == 0 && (textPositions.get(i).getUnicode().equals(" ") || textPositions.get(i).getUnicode().equals("\u00A0") || textPositions.get(i).getUnicode().equals("\t"))) {
|
|
||||||
startIndex++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strange but sometimes this is happening, for example: Metolachlor2.pdf
|
|
||||||
if (i > 0 && textPositions.get(i).getXDirAdj() < textPositions.get(i - 1).getXDirAdj()) {
|
|
||||||
List<TextPosition> sublist = textPositions.subList(startIndex, i);
|
|
||||||
if (!(sublist.isEmpty() || sublist.size() == 1 && (sublist.get(0).getUnicode().equals(" ") || sublist.get(0).getUnicode().equals("\u00A0") || sublist.get(0)
|
|
||||||
.getUnicode()
|
|
||||||
.equals("\t")))) {
|
|
||||||
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber));
|
|
||||||
}
|
|
||||||
startIndex = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textPositions.get(i).getRotation() == 0 && i > 0 && textPositions.get(i).getX() > textPositions.get(i - 1).getEndX() + 1) {
|
|
||||||
List<TextPosition> sublist = textPositions.subList(startIndex, i);
|
|
||||||
if (!(sublist.isEmpty() || sublist.size() == 1 && (sublist.get(0).getUnicode().equals(" ") || sublist.get(0).getUnicode().equals("\u00A0") || sublist.get(0)
|
|
||||||
.getUnicode()
|
|
||||||
.equals("\t")))) {
|
|
||||||
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber));
|
|
||||||
}
|
|
||||||
startIndex = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i > 0 && (textPositions.get(i).getUnicode().equals(" ") || textPositions.get(i).getUnicode().equals("\u00A0") || textPositions.get(i)
|
|
||||||
.getUnicode()
|
|
||||||
.equals("\t")) && i <= textPositions.size() - 2) {
|
|
||||||
List<TextPosition> sublist = textPositions.subList(startIndex, i);
|
|
||||||
if (!(sublist.isEmpty() || sublist.size() == 1 && (sublist.get(0).getUnicode().equals(" ") || sublist.get(0).getUnicode().equals("\u00A0") || sublist.get(0)
|
|
||||||
.getUnicode()
|
|
||||||
.equals("\t")))) {
|
|
||||||
|
|
||||||
// Remove false sequence ends (whitespaces)
|
|
||||||
if (previous != null && sublist.get(0).getYDirAdj() == previous.getYDirAdj() && sublist.get(0)
|
|
||||||
.getXDirAdj() - (previous.getXDirAdj() + previous.getWidthDirAdj()) < 0.01) {
|
|
||||||
for (TextPosition textPosition : sublist) {
|
|
||||||
textPositionSequences.get(textPositionSequences.size() - 1).add(textPosition);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startIndex = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<TextPosition> sublist = textPositions.subList(startIndex, textPositions.size());
|
|
||||||
if (!sublist.isEmpty() && (sublist.get(sublist.size() - 1).getUnicode().equals(" ") || sublist.get(sublist.size() - 1)
|
|
||||||
.getUnicode()
|
|
||||||
.equals("\u00A0") || sublist.get(sublist.size() - 1).getUnicode().equals("\t"))) {
|
|
||||||
sublist = sublist.subList(0, sublist.size() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(sublist.isEmpty() || sublist.size() == 1 && (sublist.get(0).getUnicode().equals(" ") || sublist.get(0).getUnicode().equals("\u00A0") || sublist.get(0)
|
|
||||||
.getUnicode()
|
|
||||||
.equals("\t")))) {
|
|
||||||
if (previous != null && sublist.get(0).getYDirAdj() == previous.getYDirAdj() && sublist.get(0)
|
|
||||||
.getXDirAdj() - (previous.getXDirAdj() + previous.getWidthDirAdj()) < 0.01) {
|
|
||||||
for (TextPosition t : sublist) {
|
|
||||||
textPositionSequences.get(textPositionSequences.size() - 1).add(t);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.writeString(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getText(PDDocument doc) throws IOException {
|
|
||||||
|
|
||||||
minCharWidth = Integer.MAX_VALUE;
|
|
||||||
maxCharWidth = 0;
|
|
||||||
minCharHeight = Integer.MAX_VALUE;
|
|
||||||
maxCharHeight = 0;
|
|
||||||
textPositionSequences.clear();
|
|
||||||
rulings.clear();
|
|
||||||
graphicsPath.clear();
|
|
||||||
path_x = 0.0f;
|
|
||||||
path_y = 0.0f;
|
|
||||||
|
|
||||||
return super.getText(doc);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,25 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import static java.util.stream.Collectors.toSet;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.FloatFrequencyCounter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.Orientation;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Ruling;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.StringFrequencyCounter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.RulingTextDirAdjustUtil;
|
|
||||||
|
|
||||||
public interface BlockificationService {
|
|
||||||
|
|
||||||
ClassificationPage blockify(List<TextPositionSequence> textPositions, List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines);
|
|
||||||
}
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.FloatFrequencyCounter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Cell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.TablePageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.PositionUtils;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class BodyTextFrameService {
|
|
||||||
|
|
||||||
@Value("${application.type:RedactManager}")
|
|
||||||
private String applicationType;
|
|
||||||
|
|
||||||
private static final float APPROXIMATE_HEADER_LINE_COUNT = 2.9f;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjusts and sets the body text frame to a page.
|
|
||||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
|
||||||
* 0 -> LowerLeft
|
|
||||||
* 90 -> UpperLeft
|
|
||||||
* 180 -> UpperRight
|
|
||||||
* 270 -> LowerRight
|
|
||||||
* The aspect ratio of the page is also regarded.
|
|
||||||
*
|
|
||||||
* @param page The page
|
|
||||||
* @param bodyTextFrame frame that contains the main text on portrait pages
|
|
||||||
* @param landscapeBodyTextFrame frame that contains the main text on landscape pages
|
|
||||||
*/
|
|
||||||
public void setBodyTextFrameAdjustedToPage(ClassificationPage page, Rectangle bodyTextFrame, Rectangle landscapeBodyTextFrame) {
|
|
||||||
|
|
||||||
Rectangle textFrame = page.isLandscape() ? landscapeBodyTextFrame : bodyTextFrame;
|
|
||||||
|
|
||||||
if (page.getPageWidth() > page.getPageHeight() && page.getRotation() == 270) {
|
|
||||||
textFrame = new Rectangle(new Point(textFrame.getTopLeft().getY(), page.getPageHeight() - textFrame.getTopLeft().getX() - textFrame.getWidth()),
|
|
||||||
textFrame.getHeight(),
|
|
||||||
textFrame.getWidth(),
|
|
||||||
0);
|
|
||||||
} else if (page.getPageWidth() > page.getPageHeight() && page.getRotation() != 0) {
|
|
||||||
textFrame = new Rectangle(new Point(textFrame.getTopLeft().getY(), textFrame.getTopLeft().getX()), textFrame.getHeight(), textFrame.getWidth(), page.getPageNumber());
|
|
||||||
} else if (page.getRotation() == 180) {
|
|
||||||
textFrame = new Rectangle(new Point(textFrame.getTopLeft().getX(), page.getPageHeight() - textFrame.getTopLeft().getY() - textFrame.getHeight()),
|
|
||||||
textFrame.getWidth(),
|
|
||||||
textFrame.getHeight(),
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
page.setBodyTextFrame(textFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the frame that contains the main text, text outside the frame will be e.g. headers or footers.
|
|
||||||
* Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
|
|
||||||
* 0 -> LowerLeft
|
|
||||||
* 90 -> UpperLeft
|
|
||||||
* 180 -> UpperRight
|
|
||||||
* 270 -> LowerRight
|
|
||||||
* The aspect ratio of the page is also regarded.
|
|
||||||
*
|
|
||||||
* @param pages List of all pages
|
|
||||||
* @param documentFontSizeCounter Statistics of the document
|
|
||||||
* @param landscape Calculate for landscape or portrait
|
|
||||||
* @return Rectangle of the text frame
|
|
||||||
*/
|
|
||||||
public Rectangle calculateBodyTextFrame(List<ClassificationPage> pages, FloatFrequencyCounter documentFontSizeCounter, boolean landscape) {
|
|
||||||
|
|
||||||
BodyTextFrameExpansionsRectangle expansionsRectangle = new BodyTextFrameExpansionsRectangle();
|
|
||||||
|
|
||||||
for (ClassificationPage page : pages) {
|
|
||||||
|
|
||||||
if (page.getTextBlocks().isEmpty() || landscape != page.isLandscape()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (AbstractPageBlock container : page.getTextBlocks()) {
|
|
||||||
|
|
||||||
if (container instanceof TextPageBlock) {
|
|
||||||
TextPageBlock textBlock = (TextPageBlock) container;
|
|
||||||
if (textBlock.getMostPopularWordFont() == null || textBlock.getMostPopularWordStyle() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
float approxLineCount = PositionUtils.getApproxLineCount(textBlock);
|
|
||||||
if (applicationType.equals("DocuMine") && approxLineCount < APPROXIMATE_HEADER_LINE_COUNT && textBlock.getMaxY() >= page.getPageHeight() - (page.getPageHeight() / 10)
|
|
||||||
|| applicationType.equals("RedactManager") && approxLineCount < APPROXIMATE_HEADER_LINE_COUNT){
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (documentFontSizeCounter.getMostPopular() != null && textBlock.getMostPopularWordFontSize() >= documentFontSizeCounter.getMostPopular()) {
|
|
||||||
|
|
||||||
expandRectangle(textBlock, page, expansionsRectangle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (container instanceof TablePageBlock) {
|
|
||||||
TablePageBlock table = (TablePageBlock) container;
|
|
||||||
for (List<Cell> row : table.getRows()) {
|
|
||||||
for (Cell cell : row) {
|
|
||||||
|
|
||||||
if (cell == null || cell.getTextBlocks() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (TextPageBlock textBlock : cell.getTextBlocks()) {
|
|
||||||
expandRectangle(textBlock, page, expansionsRectangle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Rectangle(new Point(expansionsRectangle.minX, expansionsRectangle.minY),
|
|
||||||
expansionsRectangle.maxX - expansionsRectangle.minX,
|
|
||||||
expansionsRectangle.maxY - expansionsRectangle.minY,
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void expandRectangle(TextPageBlock textBlock, ClassificationPage page, BodyTextFrameExpansionsRectangle expansionsRectangle) {
|
|
||||||
|
|
||||||
if (page.getPageWidth() > page.getPageHeight() && page.getRotation() != 0) {
|
|
||||||
if (textBlock.getPdfMinY() < expansionsRectangle.minX) {
|
|
||||||
expansionsRectangle.minX = textBlock.getPdfMinY();
|
|
||||||
}
|
|
||||||
if (textBlock.getPdfMaxY() > expansionsRectangle.maxX) {
|
|
||||||
expansionsRectangle.maxX = textBlock.getPdfMaxY();
|
|
||||||
}
|
|
||||||
if (textBlock.getPdfMinX() < expansionsRectangle.minY) {
|
|
||||||
expansionsRectangle.minY = textBlock.getPdfMinX();
|
|
||||||
}
|
|
||||||
if (textBlock.getPdfMaxX() > expansionsRectangle.maxY) {
|
|
||||||
expansionsRectangle.maxY = textBlock.getPdfMaxX();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (textBlock.getPdfMinX() < expansionsRectangle.minX) {
|
|
||||||
expansionsRectangle.minX = textBlock.getPdfMinX();
|
|
||||||
}
|
|
||||||
if (textBlock.getPdfMaxX() > expansionsRectangle.maxX) {
|
|
||||||
expansionsRectangle.maxX = textBlock.getPdfMaxX();
|
|
||||||
}
|
|
||||||
if (textBlock.getPdfMinY() < expansionsRectangle.minY) {
|
|
||||||
expansionsRectangle.minY = textBlock.getPdfMinY();
|
|
||||||
}
|
|
||||||
if (textBlock.getPdfMaxY() > expansionsRectangle.maxY) {
|
|
||||||
expansionsRectangle.maxY = textBlock.getPdfMaxY();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class BodyTextFrameExpansionsRectangle {
|
|
||||||
|
|
||||||
float minX = 10000;
|
|
||||||
float maxX = -100;
|
|
||||||
float minY = 10000;
|
|
||||||
float maxY = -100;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationDocument;
|
|
||||||
|
|
||||||
public interface ClassificationService {
|
|
||||||
void classifyDocument(ClassificationDocument document);
|
|
||||||
}
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class ColumnDetectionService {
|
|
||||||
|
|
||||||
private static final double SPLITTABLE_LINE_PERCENTAGE_THRESHOLD = 0.6;
|
|
||||||
private static final int MAX_NUMBER_OF_COLUMNS = 4;
|
|
||||||
|
|
||||||
|
|
||||||
public List<Rectangle2D> detectColumns(List<TextPositionSequence> textPositionSequences, Rectangle2D mainBodyTextFrame) {
|
|
||||||
|
|
||||||
if (textPositionSequences.size() < 2) {
|
|
||||||
return List.of(mainBodyTextFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<Rectangle2D>> linesWithGaps = LineDetectionService.findLinesWithGaps(textPositionSequences);
|
|
||||||
|
|
||||||
Map<Integer, List<Integer>> linesWithMatchingGapIndices = new HashMap<>();
|
|
||||||
for (int numberOfColumns = 2; numberOfColumns <= MAX_NUMBER_OF_COLUMNS; numberOfColumns++) {
|
|
||||||
linesWithMatchingGapIndices.put(numberOfColumns, findConsecutiveLinesWithMatchingGaps(linesWithGaps, mainBodyTextFrame.getWidth(), numberOfColumns));
|
|
||||||
}
|
|
||||||
|
|
||||||
int optimalNumberOfColumns = findOptimalNumberOfColumns(linesWithMatchingGapIndices, linesWithGaps.size());
|
|
||||||
if (optimalNumberOfColumns == 1) {
|
|
||||||
return List.of(mainBodyTextFrame);
|
|
||||||
}
|
|
||||||
return buildColumns(mainBodyTextFrame, getLinesWithMatchingGaps(linesWithMatchingGapIndices.get(optimalNumberOfColumns), linesWithGaps), optimalNumberOfColumns);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static List<Integer> findConsecutiveLinesWithMatchingGaps(List<List<Rectangle2D>> linesWithGaps, double width, int numberOfColumns) {
|
|
||||||
|
|
||||||
List<Boolean> booleans = lineHasMatchingGap(linesWithGaps, width, numberOfColumns);
|
|
||||||
return findConsecutiveTrueIndicesWithMaxLengthRun(booleans);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Boolean> lineHasMatchingGap(List<List<Rectangle2D>> linesWithGaps, double width, int numberOfColumns) {
|
|
||||||
|
|
||||||
return linesWithGaps.stream()
|
|
||||||
.map(blocksWithGaps -> IntStream.range(1, numberOfColumns)
|
|
||||||
.allMatch(columnIndex -> noBlocksIntersectX(blocksWithGaps, calculateGapLocation(width, numberOfColumns, columnIndex))))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Integer> findConsecutiveTrueIndicesWithMaxLengthRun(List<Boolean> booleans) {
|
|
||||||
|
|
||||||
List<Integer> maxConsecutiveTrueIndices = new LinkedList<>();
|
|
||||||
List<Integer> currentConsecutiveTrueIndices = new LinkedList<>();
|
|
||||||
for (int i = 0; i < booleans.size(); i++) {
|
|
||||||
if (!booleans.get(i)) {
|
|
||||||
if (currentConsecutiveTrueIndices.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (currentConsecutiveTrueIndices.size() > maxConsecutiveTrueIndices.size()) {
|
|
||||||
maxConsecutiveTrueIndices = currentConsecutiveTrueIndices;
|
|
||||||
}
|
|
||||||
currentConsecutiveTrueIndices = new LinkedList<>();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
currentConsecutiveTrueIndices.add(i);
|
|
||||||
}
|
|
||||||
if (currentConsecutiveTrueIndices.size() > maxConsecutiveTrueIndices.size()) {
|
|
||||||
return currentConsecutiveTrueIndices;
|
|
||||||
}
|
|
||||||
return maxConsecutiveTrueIndices;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static int findOptimalNumberOfColumns(Map<Integer, List<Integer>> linesWithMatchingGapIndices, Integer numberOfLines) {
|
|
||||||
|
|
||||||
return linesWithMatchingGapIndices.entrySet()
|
|
||||||
.stream()
|
|
||||||
.max(comparePercentages(numberOfLines))
|
|
||||||
.filter(entry -> percentageIsAboveThreshold(entry, numberOfLines))
|
|
||||||
.map(Map.Entry::getKey)
|
|
||||||
.orElse(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Rectangle2D> buildColumns(Rectangle2D mainBodyTextFrame, List<Rectangle2D> rectanglesToMerge, int optimalColumnCount) {
|
|
||||||
|
|
||||||
if (optimalColumnCount == 1 || rectanglesToMerge.isEmpty()) {
|
|
||||||
return List.of(mainBodyTextFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
double maxY = rectanglesToMerge.get(0).getMaxY();
|
|
||||||
double minY = rectanglesToMerge.get(rectanglesToMerge.size() - 1).getMinY();
|
|
||||||
|
|
||||||
List<Rectangle2D> columns = new LinkedList<>();
|
|
||||||
double width = mainBodyTextFrame.getWidth() / optimalColumnCount;
|
|
||||||
double height = maxY - minY;
|
|
||||||
for (int i = 0; i < optimalColumnCount; i++) {
|
|
||||||
columns.add(new Rectangle2D.Double(mainBodyTextFrame.getMinY() + i * width, minY, width, height));
|
|
||||||
}
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Comparator<Map.Entry<Integer, List<Integer>>> comparePercentages(Integer numberOfLines) {
|
|
||||||
|
|
||||||
return Comparator.comparingDouble(entry -> calculatePercentage(entry.getValue().size(), numberOfLines));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Rectangle2D> getLinesWithMatchingGaps(List<Integer> linesWithMatchingGapIndices, List<List<Rectangle2D>> linesWithGaps) {
|
|
||||||
|
|
||||||
return linesWithMatchingGapIndices.stream().map(linesWithGaps::get).flatMap(Collection::stream).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean percentageIsAboveThreshold(Map.Entry<Integer, List<Integer>> entry, Integer numberOfLines) {
|
|
||||||
|
|
||||||
return calculatePercentage(entry.getValue().size(), numberOfLines) > SPLITTABLE_LINE_PERCENTAGE_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private double calculatePercentage(Integer numberOfMatchingLines, Integer numberOfLines) {
|
|
||||||
|
|
||||||
return ((double) numberOfMatchingLines) / ((double) numberOfLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private double calculateGapLocation(double pageWidth, int numberOfColumns, int columnIndex) {
|
|
||||||
|
|
||||||
return (pageWidth / numberOfColumns) * columnIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Boolean noBlocksIntersectX(List<Rectangle2D> blocksWithGaps, double x) {
|
|
||||||
|
|
||||||
return blocksWithGaps.stream().noneMatch(rect -> rect.getMaxX() > x && rect.getMinX() < x);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,207 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import static java.util.stream.Collectors.toSet;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.FloatFrequencyCounter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.Orientation;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Ruling;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.StringFrequencyCounter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.RulingTextDirAdjustUtil;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.Patterns;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@ConditionalOnProperty(prefix = "application", name = "type", havingValue = "DocuMine")
|
|
||||||
@SuppressWarnings("all")
|
|
||||||
public class DocuMineBlockificationService implements BlockificationService{
|
|
||||||
|
|
||||||
static final float THRESHOLD = 1f;
|
|
||||||
|
|
||||||
Pattern pattern = Patterns.getCompiledPattern("^(\\d{1,2}\\.){1,3}\\d{1,2}\\.?\\s[0-9A-Za-z ()-]{2,50}", true);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is building blocks by expanding the minX/maxX and minY/maxY value on each word that is not split by the conditions.
|
|
||||||
* This method must use text direction adjusted postions (DirAdj). Where {0,0} is on the upper left. Never try to change this!
|
|
||||||
* Rulings (Table lines) must be adjusted to the text directions as well, when checking if a block is split by a ruling.
|
|
||||||
* @param textPositions The words of a page.
|
|
||||||
* @param horizontalRulingLines Horizontal table lines.
|
|
||||||
* @param verticalRulingLines Vertical table lines.
|
|
||||||
* @return Page object that contains the Textblock and text statistics.
|
|
||||||
*/
|
|
||||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions, List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {
|
|
||||||
|
|
||||||
List<TextPositionSequence> chunkWords = new ArrayList<>();
|
|
||||||
List<AbstractPageBlock> chunkBlockList1 = new ArrayList<>();
|
|
||||||
|
|
||||||
float minX = 1000, maxX = 0, minY = 1000, maxY = 0;
|
|
||||||
TextPositionSequence prev = null;
|
|
||||||
|
|
||||||
boolean wasSplitted = false;
|
|
||||||
Float splitX1 = null;
|
|
||||||
for (TextPositionSequence word : textPositions) {
|
|
||||||
|
|
||||||
boolean lineSeparation = prev != null && word.getMinYDirAdj() - maxY > Math.min(word.getHeight(), prev.getHeight()) * 1.25;
|
|
||||||
boolean startFromTop = prev != null && word.getMinYDirAdj() < prev.getMinYDirAdj() - prev.getTextHeight();
|
|
||||||
boolean splitByX = prev != null && maxX + 50 < word.getMinXDirAdj() && prev.getMinYDirAdj() == word.getMinYDirAdj();
|
|
||||||
boolean negativeXGap = prev != null && word.getMinXDirAdj() - minX < -5;
|
|
||||||
boolean newLineAfterSplit = prev != null && word.getMinYDirAdj() != prev.getMinYDirAdj() && wasSplitted && splitX1 != word.getMinXDirAdj();
|
|
||||||
boolean isSplitByRuling = isSplitByRuling(minX, minY, maxX, maxY, word, horizontalRulingLines, verticalRulingLines);
|
|
||||||
boolean splitByDir = prev != null && !prev.getDir().equals(word.getDir());
|
|
||||||
boolean splitByOtherFontAndOtherY = prev != null && prev.getMaxYDirAdj() != word.getMaxYDirAdj() && (word.getFontStyle().contains("bold") && !prev.getFontStyle().contains("bold") || prev.getFontStyle().contains("bold") && !word.getFontStyle().contains("bold"));
|
|
||||||
|
|
||||||
Matcher matcher = pattern.matcher(chunkWords.stream().collect(Collectors.joining(" ")).toString());
|
|
||||||
boolean startsOnSameX = Math.abs(minX - word.getMinXDirAdj()) < 5 && matcher.matches();
|
|
||||||
|
|
||||||
if (prev != null && (lineSeparation || startFromTop || splitByDir || isSplitByRuling || splitByOtherFontAndOtherY || negativeXGap || startsOnSameX)) {
|
|
||||||
|
|
||||||
Orientation prevOrientation = null;
|
|
||||||
if (!chunkBlockList1.isEmpty()) {
|
|
||||||
prevOrientation = chunkBlockList1.get(chunkBlockList1.size() - 1).getOrientation();
|
|
||||||
}
|
|
||||||
|
|
||||||
TextPageBlock cb1 = buildTextBlock(chunkWords);
|
|
||||||
chunkBlockList1.add(cb1);
|
|
||||||
chunkWords = new ArrayList<>();
|
|
||||||
|
|
||||||
if (splitByX && !isSplitByRuling) {
|
|
||||||
wasSplitted = true;
|
|
||||||
cb1.setOrientation(Orientation.LEFT);
|
|
||||||
splitX1 = word.getMinXDirAdj();
|
|
||||||
} else if (newLineAfterSplit && !isSplitByRuling) {
|
|
||||||
wasSplitted = false;
|
|
||||||
cb1.setOrientation(Orientation.RIGHT);
|
|
||||||
splitX1 = null;
|
|
||||||
} else if (prevOrientation != null && prevOrientation.equals(Orientation.RIGHT) && (lineSeparation || !startFromTop || !splitByX || !newLineAfterSplit || !isSplitByRuling)) {
|
|
||||||
cb1.setOrientation(Orientation.LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
minX = 1000;
|
|
||||||
maxX = 0;
|
|
||||||
minY = 1000;
|
|
||||||
maxY = 0;
|
|
||||||
prev = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunkWords.add(word);
|
|
||||||
|
|
||||||
prev = word;
|
|
||||||
if (word.getMinXDirAdj() < minX) {
|
|
||||||
minX = word.getMinXDirAdj();
|
|
||||||
}
|
|
||||||
if (word.getMaxXDirAdj() > maxX) {
|
|
||||||
maxX = word.getMaxXDirAdj();
|
|
||||||
}
|
|
||||||
if (word.getMinYDirAdj() < minY) {
|
|
||||||
minY = word.getMinYDirAdj();
|
|
||||||
}
|
|
||||||
if (word.getMaxYDirAdj() > maxY) {
|
|
||||||
maxY = word.getMaxYDirAdj();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextPageBlock cb1 = buildTextBlock(chunkWords);
|
|
||||||
if (cb1 != null) {
|
|
||||||
chunkBlockList1.add(cb1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ClassificationPage(chunkBlockList1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean equalsWithThreshold(float f1, float f2) {
|
|
||||||
|
|
||||||
return Math.abs(f1 - f2) < THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList) {
|
|
||||||
|
|
||||||
TextPageBlock textBlock = null;
|
|
||||||
|
|
||||||
FloatFrequencyCounter lineHeightFrequencyCounter = new FloatFrequencyCounter();
|
|
||||||
FloatFrequencyCounter fontSizeFrequencyCounter = new FloatFrequencyCounter();
|
|
||||||
FloatFrequencyCounter spaceFrequencyCounter = new FloatFrequencyCounter();
|
|
||||||
StringFrequencyCounter fontFrequencyCounter = new StringFrequencyCounter();
|
|
||||||
StringFrequencyCounter styleFrequencyCounter = new StringFrequencyCounter();
|
|
||||||
|
|
||||||
for (TextPositionSequence wordBlock : wordBlockList) {
|
|
||||||
|
|
||||||
lineHeightFrequencyCounter.add(wordBlock.getTextHeight());
|
|
||||||
fontSizeFrequencyCounter.add(wordBlock.getFontSize());
|
|
||||||
spaceFrequencyCounter.add(wordBlock.getSpaceWidth());
|
|
||||||
fontFrequencyCounter.add(wordBlock.getFont());
|
|
||||||
styleFrequencyCounter.add(wordBlock.getFontStyle());
|
|
||||||
|
|
||||||
if (textBlock == null) {
|
|
||||||
textBlock = new TextPageBlock(wordBlock.getMinXDirAdj(), wordBlock.getMaxXDirAdj(), wordBlock.getMinYDirAdj(), wordBlock.getMaxYDirAdj(), wordBlockList, wordBlock.getRotation());
|
|
||||||
} else {
|
|
||||||
TextPageBlock spatialEntity = textBlock.union(wordBlock);
|
|
||||||
textBlock.resize(spatialEntity.getMinX(), spatialEntity.getMinY(), spatialEntity.getWidth(), spatialEntity.getHeight());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textBlock != null) {
|
|
||||||
textBlock.setMostPopularWordFont(fontFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setMostPopularWordStyle(styleFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setMostPopularWordFontSize(fontSizeFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setMostPopularWordHeight(lineHeightFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setMostPopularWordSpaceWidth(spaceFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setHighestFontSize(fontSizeFrequencyCounter.getHighest());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textBlock != null && textBlock.getSequences() != null && textBlock.getSequences().stream().map(t -> round(t.getMinYDirAdj(), 3)).collect(toSet()).size() == 1) {
|
|
||||||
textBlock.getSequences().sort(Comparator.comparing(TextPositionSequence::getMinXDirAdj));
|
|
||||||
}
|
|
||||||
return textBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isSplitByRuling(float minX,
|
|
||||||
float minY,
|
|
||||||
float maxX,
|
|
||||||
float maxY,
|
|
||||||
TextPositionSequence word,
|
|
||||||
List<Ruling> horizontalRulingLines,
|
|
||||||
List<Ruling> verticalRulingLines) {
|
|
||||||
|
|
||||||
return isSplitByRuling(maxX, minY, word.getMinXDirAdj(), word.getMinYDirAdj(), verticalRulingLines, word.getDir().getDegrees(), word.getPageWidth(), word.getPageHeight()) //
|
|
||||||
|| isSplitByRuling(minX, minY, word.getMinXDirAdj(), word.getMaxYDirAdj(), horizontalRulingLines, word.getDir().getDegrees(), word.getPageWidth(), word.getPageHeight()) //
|
|
||||||
|| isSplitByRuling(maxX, minY, word.getMinXDirAdj(), word.getMinYDirAdj(), horizontalRulingLines, word.getDir().getDegrees(), word.getPageWidth(), word.getPageHeight()) //
|
|
||||||
|| isSplitByRuling(minX, minY, word.getMinXDirAdj(), word.getMaxYDirAdj(), verticalRulingLines, word.getDir().getDegrees(), word.getPageWidth(), word.getPageHeight()); //
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isSplitByRuling(float previousX2, float previousY1, float currentX1, float currentY1, List<Ruling> rulingLines, float dir, float pageWidth, float pageHeight) {
|
|
||||||
|
|
||||||
for (Ruling ruling : rulingLines) {
|
|
||||||
var line = RulingTextDirAdjustUtil.convertToDirAdj(ruling, dir, pageWidth, pageHeight);
|
|
||||||
if (line.intersectsLine(previousX2, previousY1, currentX1, currentY1)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private double round(float value, int decimalPoints) {
|
|
||||||
|
|
||||||
var d = Math.pow(10, decimalPoints);
|
|
||||||
return Math.round(value * d) / d;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationDocument;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.PageBlockType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.PositionUtils;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.Patterns;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@ConditionalOnProperty(prefix = "application", name = "type", havingValue = "DocuMine")
|
|
||||||
public class DocuMineClassificationService implements ClassificationService {
|
|
||||||
|
|
||||||
private final BodyTextFrameService bodyTextFrameService;
|
|
||||||
|
|
||||||
|
|
||||||
public void classifyDocument(ClassificationDocument document) {
|
|
||||||
|
|
||||||
Rectangle bodyTextFrame = bodyTextFrameService.calculateBodyTextFrame(document.getPages(), document.getFontSizeCounter(), false);
|
|
||||||
Rectangle landscapeBodyTextFrame = bodyTextFrameService.calculateBodyTextFrame(document.getPages(), document.getFontSizeCounter(), true);
|
|
||||||
|
|
||||||
log.debug("Document FontSize counters are: {}", document.getFontSizeCounter().getCountPerValue());
|
|
||||||
|
|
||||||
for (ClassificationPage page : document.getPages()) {
|
|
||||||
bodyTextFrameService.setBodyTextFrameAdjustedToPage(page, bodyTextFrame, landscapeBodyTextFrame);
|
|
||||||
classifyPage(page, document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void classifyPage(ClassificationPage page, ClassificationDocument document) {
|
|
||||||
|
|
||||||
for (AbstractPageBlock textBlock : page.getTextBlocks()) {
|
|
||||||
if (textBlock instanceof TextPageBlock) {
|
|
||||||
classifyBlock((TextPageBlock) textBlock, page, document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void classifyBlock(TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document) {
|
|
||||||
|
|
||||||
var bodyTextFrame = page.getBodyTextFrame();
|
|
||||||
|
|
||||||
var pattern = Patterns.getCompiledPattern("^(\\d{1,2}\\.){1,3}\\d{1,2}\\.?\\s[0-9A-Za-z \\[\\]]{2,50}", true);
|
|
||||||
var pattern2 = Patterns.getCompiledPattern("\\p{L}{3,}", true);
|
|
||||||
var pattern3 = Patterns.getCompiledPattern("^(\\d{1,1}\\.){1,3}\\d{1,2}\\.?\\s[a-z]{1,2}\\/[a-z]{1,2}.*", false);
|
|
||||||
|
|
||||||
Matcher matcher = pattern.matcher(textBlock.toString());
|
|
||||||
Matcher matcher2 = pattern2.matcher(textBlock.toString());
|
|
||||||
Matcher matcher3 = pattern3.matcher(textBlock.toString());
|
|
||||||
|
|
||||||
if (document.getFontSizeCounter().getMostPopular() == null) {
|
|
||||||
textBlock.setClassification(PageBlockType.OTHER);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (PositionUtils.isOverBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter()
|
|
||||||
.getMostPopular() == null || textBlock.getHighestFontSize() <= document.getFontSizeCounter().getMostPopular())) {
|
|
||||||
textBlock.setClassification(PageBlockType.HEADER);
|
|
||||||
|
|
||||||
} else if (PositionUtils.isUnderBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter()
|
|
||||||
.getMostPopular() == null || textBlock.getHighestFontSize() <= document.getFontSizeCounter().getMostPopular())) {
|
|
||||||
textBlock.setClassification(PageBlockType.FOOTER);
|
|
||||||
} else if (page.getPageNumber() == 1 && (PositionUtils.getHeightDifferenceBetweenChunkWordAndDocumentWord(textBlock,
|
|
||||||
document.getTextHeightCounter().getMostPopular()) > 2.5 && textBlock.getHighestFontSize() > document.getFontSizeCounter().getMostPopular() || page.getTextBlocks()
|
|
||||||
.size() == 1)) {
|
|
||||||
if (!Pattern.matches("[0-9]+", textBlock.toString())) {
|
|
||||||
textBlock.setClassification(PageBlockType.TITLE);
|
|
||||||
}
|
|
||||||
} else if (textBlock.getText().length() > 5 && (textBlock.getMostPopularWordHeight() > document.getTextHeightCounter()
|
|
||||||
.getMostPopular() || textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter().getMostPopular()) && PositionUtils.getApproxLineCount(textBlock) < 5.9
|
|
||||||
|
|
||||||
&& (textBlock.getMostPopularWordStyle().contains("bold") && Character.isDigit(textBlock.toString().charAt(0)) && !matcher2.matches() && !textBlock.toString()
|
|
||||||
.contains(":") || textBlock.toString().equals(textBlock.toString().toUpperCase(Locale.ROOT)) && !matcher2.matches() && !textBlock.toString()
|
|
||||||
.contains(":") || textBlock.toString().startsWith("APPENDIX") || textBlock.toString().startsWith("FIGURE") || textBlock.toString()
|
|
||||||
.startsWith("TABLE")) && !textBlock.toString().endsWith(":") && matcher2.find()) {
|
|
||||||
textBlock.setClassification(PageBlockType.getHeadlineType(1));
|
|
||||||
document.setHeadlines(true);
|
|
||||||
|
|
||||||
} else if (matcher.find() && PositionUtils.getApproxLineCount(textBlock) < 2.9 && matcher2.find() && !matcher3.matches()) {
|
|
||||||
textBlock.setClassification(PageBlockType.getHeadlineType(2));
|
|
||||||
document.setHeadlines(true);
|
|
||||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter()
|
|
||||||
.getMostPopular() && textBlock.getMostPopularWordStyle().equals("bold") && !document.getFontStyleCounter().getMostPopular().equals("bold")) {
|
|
||||||
textBlock.setClassification(PageBlockType.PARAGRAPH_BOLD);
|
|
||||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFont()
|
|
||||||
.equals(document.getFontCounter().getMostPopular()) && textBlock.getMostPopularWordStyle()
|
|
||||||
.equals(document.getFontStyleCounter().getMostPopular()) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()) {
|
|
||||||
textBlock.setClassification(PageBlockType.PARAGRAPH);
|
|
||||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter()
|
|
||||||
.getMostPopular() && textBlock.getMostPopularWordStyle().equals("italic") && !document.getFontStyleCounter()
|
|
||||||
.getMostPopular()
|
|
||||||
.equals("italic") && PositionUtils.getApproxLineCount(textBlock) < 2.9) {
|
|
||||||
textBlock.setClassification(PageBlockType.PARAGRAPH_ITALIC);
|
|
||||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)) {
|
|
||||||
textBlock.setClassification(PageBlockType.PARAGRAPH_UNKNOWN);
|
|
||||||
} else {
|
|
||||||
textBlock.setClassification(PageBlockType.OTHER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.utils.RectangleTransformations;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.utils.TextPositionSequenceComparator;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class LineDetectionService {
|
|
||||||
|
|
||||||
private static final double X_GAP_FACTOR = 1; // multiplied with average text height, determines the minimum distance of gaps in lines
|
|
||||||
|
|
||||||
|
|
||||||
public static List<List<Rectangle2D>> findLinesWithGaps(List<TextPositionSequence> textPositionSequences) {
|
|
||||||
|
|
||||||
if (textPositionSequences.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
final double avgTextPositionHeight = textPositionSequences.stream().mapToDouble(TextPositionSequence::getHeight).average().orElseThrow();
|
|
||||||
|
|
||||||
Context context = Context.init();
|
|
||||||
|
|
||||||
List<TextPositionSequence> sortedTextPositionSequence = textPositionSequences.stream().sorted(new TextPositionSequenceComparator()).toList();
|
|
||||||
|
|
||||||
var previousTextPosition = sortedTextPositionSequence.get(0);
|
|
||||||
context.textPositionsToMerge.add(previousTextPosition);
|
|
||||||
for (TextPositionSequence currentTextPosition : sortedTextPositionSequence.subList(1, sortedTextPositionSequence.size())) {
|
|
||||||
if (isNewLine(currentTextPosition, previousTextPosition, avgTextPositionHeight) || isSplitByOrientation(currentTextPosition, previousTextPosition)) {
|
|
||||||
addBlockToLine(context);
|
|
||||||
startNewLine(currentTextPosition, context);
|
|
||||||
} else if (isXGap(currentTextPosition, previousTextPosition, avgTextPositionHeight)) {
|
|
||||||
addBlockToLine(context);
|
|
||||||
startNewBlock(currentTextPosition, context);
|
|
||||||
} else {
|
|
||||||
context.textPositionsToMerge.add(currentTextPosition);
|
|
||||||
}
|
|
||||||
previousTextPosition = currentTextPosition;
|
|
||||||
}
|
|
||||||
addBlockToLine(context);
|
|
||||||
return context.linesWithGaps;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static boolean isXGap(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition, double avgTextPositionHeight) {
|
|
||||||
|
|
||||||
return Math.abs(previousTextPosition.getMaxXDirAdj() - currentTextPosition.getMinXDirAdj()) > (avgTextPositionHeight * X_GAP_FACTOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static boolean isSplitByOrientation(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition) {
|
|
||||||
|
|
||||||
return !previousTextPosition.getDir().equals(currentTextPosition.getDir());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static boolean isNewLine(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition, double avgTextPositionHeight) {
|
|
||||||
|
|
||||||
return Math.abs(previousTextPosition.getMinYDirAdj() - currentTextPosition.getMinYDirAdj()) > avgTextPositionHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static void startNewBlock(TextPositionSequence currentTextPosition, Context context) {
|
|
||||||
|
|
||||||
context.textPositionsToMerge = new LinkedList<>();
|
|
||||||
context.textPositionsToMerge.add(currentTextPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static void addBlockToLine(Context context) {
|
|
||||||
|
|
||||||
context.blocksInLine.add(textPositionBBox(context.textPositionsToMerge));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static void startNewLine(TextPositionSequence current, Context context) {
|
|
||||||
|
|
||||||
context.blocksInLine = new LinkedList<>();
|
|
||||||
startNewBlock(current, context);
|
|
||||||
context.linesWithGaps.add(context.blocksInLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Rectangle2D textPositionBBox(List<TextPositionSequence> textPositionSequences) {
|
|
||||||
|
|
||||||
return RectangleTransformations.rectangleBBox(textPositionSequences.stream().map(TextPositionSequence::getRectangle).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
private class Context {
|
|
||||||
|
|
||||||
List<List<Rectangle2D>> linesWithGaps;
|
|
||||||
List<Rectangle2D> blocksInLine;
|
|
||||||
List<TextPositionSequence> textPositionsToMerge;
|
|
||||||
|
|
||||||
|
|
||||||
public static Context init() {
|
|
||||||
|
|
||||||
List<List<Rectangle2D>> initialLinesWithGaps = new LinkedList<>();
|
|
||||||
List<Rectangle2D> initialBlocksInLine = new LinkedList<>();
|
|
||||||
initialLinesWithGaps.add(initialBlocksInLine);
|
|
||||||
return new Context(initialLinesWithGaps, initialBlocksInLine, new LinkedList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
import org.apache.pdfbox.io.MemoryUsageSetting;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.ImageServiceResponseAdapter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.TableServiceResponseAdapter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table.PdfTableCell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationDocument;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image.ClassifiedImage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.CleanRulings;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.parsing.PDFLinesTextStripper;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.FileUtils;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class PdfSegmentationService {
|
|
||||||
|
|
||||||
private final RedactionServiceSettings redactionServiceSettings;
|
|
||||||
private final RulingCleaningService rulingCleaningService;
|
|
||||||
private final TableExtractionService tableExtractionService;
|
|
||||||
private final BlockificationService blockificationService;
|
|
||||||
private final ClassificationService classificationService;
|
|
||||||
private final SectionsBuilderService sectionsBuilderService;
|
|
||||||
private final ImageServiceResponseAdapter imageServiceResponseAdapter;
|
|
||||||
private final TableServiceResponseAdapter tableServiceResponseAdapter;
|
|
||||||
|
|
||||||
@Value("${application.type:RedactManager}")
|
|
||||||
private String applicationType;
|
|
||||||
|
|
||||||
|
|
||||||
public ClassificationDocument parseDocument(String dossierId,
|
|
||||||
String fileId,
|
|
||||||
InputStream documentInputStream,
|
|
||||||
Map<Integer, List<ClassifiedImage>> pdfImages) throws IOException {
|
|
||||||
|
|
||||||
PDDocument pdDocument = null;
|
|
||||||
File tempFile = null;
|
|
||||||
try {
|
|
||||||
Map<Integer, List<PdfTableCell>> pdfTableCells = new HashMap<>();
|
|
||||||
if (redactionServiceSettings.isCvTableParsingEnabled()) {
|
|
||||||
pdfTableCells = tableServiceResponseAdapter.convertTables(dossierId, fileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
tempFile = FileUtils.createTempFile("document", ".pdf");
|
|
||||||
try (var fos = new FileOutputStream(tempFile)) {
|
|
||||||
IOUtils.copy(documentInputStream, fos);
|
|
||||||
|
|
||||||
// initialize required variables
|
|
||||||
ClassificationDocument document = new ClassificationDocument();
|
|
||||||
List<ClassificationPage> pages = new ArrayList<>();
|
|
||||||
|
|
||||||
pdDocument = PDDocument.load(tempFile, MemoryUsageSetting.setupMixed(67108864L));
|
|
||||||
pdDocument.setAllSecurityToBeRemoved(true);
|
|
||||||
long pageCount = pdDocument.getNumberOfPages();
|
|
||||||
|
|
||||||
for (int pageNumber = 1; pageNumber <= pageCount; pageNumber++) {
|
|
||||||
processPage(pdfImages, pdDocument, pdfTableCells, document, pages, pageNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.setPages(pages);
|
|
||||||
|
|
||||||
classificationService.classifyDocument(document);
|
|
||||||
sectionsBuilderService.buildSections(document);
|
|
||||||
sectionsBuilderService.addImagesToSections(document);
|
|
||||||
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (pdDocument != null) {
|
|
||||||
pdDocument.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
FileUtils.deleteFile(tempFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void processPage(Map<Integer, List<ClassifiedImage>> pdfImages,
|
|
||||||
PDDocument pdDocument,
|
|
||||||
Map<Integer, List<PdfTableCell>> pdfTableCells,
|
|
||||||
ClassificationDocument document,
|
|
||||||
List<ClassificationPage> pages,
|
|
||||||
int pageNumber) throws IOException {
|
|
||||||
|
|
||||||
PDFLinesTextStripper stripper = new PDFLinesTextStripper();
|
|
||||||
PDPage pdPage = pdDocument.getPage(pageNumber - 1);
|
|
||||||
stripper.setPageNumber(pageNumber);
|
|
||||||
stripper.setStartPage(pageNumber);
|
|
||||||
stripper.setEndPage(pageNumber);
|
|
||||||
stripper.setPdpage(pdPage);
|
|
||||||
|
|
||||||
if(applicationType.equals("DocuMine")){
|
|
||||||
stripper.setSortByPosition(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
stripper.getText(pdDocument);
|
|
||||||
|
|
||||||
PDRectangle pdr = pdPage.getMediaBox();
|
|
||||||
|
|
||||||
int rotation = pdPage.getRotation();
|
|
||||||
boolean isLandscape = pdr.getWidth() > pdr.getHeight() && (rotation == 0 || rotation == 180) || pdr.getHeight() > pdr.getWidth() && (rotation == 90 || rotation == 270);
|
|
||||||
|
|
||||||
PDRectangle cropbox = pdPage.getCropBox();
|
|
||||||
CleanRulings cleanRulings = rulingCleaningService.getCleanRulings(pdfTableCells.get(pageNumber),
|
|
||||||
stripper.getRulings(),
|
|
||||||
stripper.getMinCharWidth(),
|
|
||||||
stripper.getMaxCharHeight());
|
|
||||||
// var columns = ColumnDetectionService.detectColumns(stripper.getTextPositionSequences(), RectangleTransformations.toRectangle2D(pdPage.getCropBox()));
|
|
||||||
ClassificationPage page = blockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
|
||||||
|
|
||||||
page.setRotation(rotation);
|
|
||||||
page.setLandscape(isLandscape);
|
|
||||||
page.setPageNumber(pageNumber);
|
|
||||||
page.setPageWidth(cropbox.getWidth());
|
|
||||||
page.setPageHeight(cropbox.getHeight());
|
|
||||||
|
|
||||||
// If images is ocr needs to be calculated before textBlocks are moved into tables, otherwise findOcr algorithm needs to be adopted.
|
|
||||||
if (pdfImages != null && pdfImages.containsKey(pageNumber)) {
|
|
||||||
page.setImages(pdfImages.get(pageNumber));
|
|
||||||
imageServiceResponseAdapter.findOcr(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
tableExtractionService.extractTables(cleanRulings, page);
|
|
||||||
buildPageStatistics(page);
|
|
||||||
increaseDocumentStatistics(page, document);
|
|
||||||
|
|
||||||
pages.add(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void increaseDocumentStatistics(ClassificationPage page, ClassificationDocument document) {
|
|
||||||
|
|
||||||
if (!page.isLandscape()) {
|
|
||||||
document.getFontSizeCounter().addAll(page.getFontSizeCounter().getCountPerValue());
|
|
||||||
}
|
|
||||||
document.getFontCounter().addAll(page.getFontCounter().getCountPerValue());
|
|
||||||
document.getTextHeightCounter().addAll(page.getTextHeightCounter().getCountPerValue());
|
|
||||||
document.getFontStyleCounter().addAll(page.getFontStyleCounter().getCountPerValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void buildPageStatistics(ClassificationPage page) {
|
|
||||||
|
|
||||||
// Collect all statistics for the page, except from blocks inside tables, as tables will always be added to BodyTextFrame.
|
|
||||||
for (AbstractPageBlock textBlock : page.getTextBlocks()) {
|
|
||||||
if (textBlock instanceof TextPageBlock) {
|
|
||||||
if (((TextPageBlock) textBlock).getSequences() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (TextPositionSequence word : ((TextPageBlock) textBlock).getSequences()) {
|
|
||||||
page.getTextHeightCounter().add(word.getTextHeight());
|
|
||||||
page.getFontCounter().add(word.getFont());
|
|
||||||
page.getFontSizeCounter().add(word.getFontSize());
|
|
||||||
page.getFontStyleCounter().add(word.getFontStyle());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,280 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import static java.util.stream.Collectors.toSet;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.FloatFrequencyCounter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.Orientation;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Ruling;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.StringFrequencyCounter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPositionSequence;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.RulingTextDirAdjustUtil;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@SuppressWarnings("all")
|
|
||||||
@ConditionalOnProperty(prefix = "application", name = "type", havingValue = "RedactManager")
|
|
||||||
public class RedactManagerBlockificationService implements BlockificationService {
|
|
||||||
|
|
||||||
static final float THRESHOLD = 1f;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is building blocks by expanding the minX/maxX and minY/maxY value on each word that is not split by the conditions.
|
|
||||||
* This method must use text direction adjusted postions (DirAdj). Where {0,0} is on the upper left. Never try to change this!
|
|
||||||
* Rulings (TablePageBlock lines) must be adjusted to the text directions as well, when checking if a block is split by a ruling.
|
|
||||||
*
|
|
||||||
* @param textPositions The words of a page.
|
|
||||||
* @param horizontalRulingLines Horizontal table lines.
|
|
||||||
* @param verticalRulingLines Vertical table lines.
|
|
||||||
* @return Page object that contains the Textblock and text statistics.
|
|
||||||
*/
|
|
||||||
public ClassificationPage blockify(List<TextPositionSequence> textPositions, List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {
|
|
||||||
|
|
||||||
int indexOnPage = 0;
|
|
||||||
List<TextPositionSequence> chunkWords = new ArrayList<>();
|
|
||||||
List<AbstractPageBlock> chunkBlockList = new ArrayList<>();
|
|
||||||
|
|
||||||
float minX = 1000, maxX = 0, minY = 1000, maxY = 0;
|
|
||||||
TextPositionSequence prev = null;
|
|
||||||
|
|
||||||
boolean wasSplitted = false;
|
|
||||||
Float splitX1 = null;
|
|
||||||
for (TextPositionSequence word : textPositions) {
|
|
||||||
|
|
||||||
boolean lineSeparation = word.getMinYDirAdj() - maxY > word.getHeight() * 1.25;
|
|
||||||
boolean startFromTop = prev != null && word.getMinYDirAdj() < prev.getMinYDirAdj() - prev.getTextHeight();
|
|
||||||
boolean splitByX = prev != null && maxX + 50 < word.getMinXDirAdj() && prev.getMinYDirAdj() == word.getMinYDirAdj();
|
|
||||||
boolean xIsBeforeFirstX = prev != null && word.getMinXDirAdj() < minX;
|
|
||||||
boolean newLineAfterSplit = prev != null && word.getMinYDirAdj() != prev.getMinYDirAdj() && wasSplitted && splitX1 != word.getMinXDirAdj();
|
|
||||||
boolean isSplitByRuling = isSplitByRuling(minX, minY, maxX, maxY, word, horizontalRulingLines, verticalRulingLines);
|
|
||||||
boolean splitByDir = prev != null && !prev.getDir().equals(word.getDir());
|
|
||||||
|
|
||||||
if (prev != null && (lineSeparation || startFromTop || splitByX || splitByDir || isSplitByRuling)) {
|
|
||||||
|
|
||||||
Orientation prevOrientation = null;
|
|
||||||
if (!chunkBlockList.isEmpty()) {
|
|
||||||
prevOrientation = chunkBlockList.get(chunkBlockList.size() - 1).getOrientation();
|
|
||||||
}
|
|
||||||
|
|
||||||
TextPageBlock cb1 = buildTextBlock(chunkWords, indexOnPage);
|
|
||||||
indexOnPage++;
|
|
||||||
|
|
||||||
chunkBlockList.add(cb1);
|
|
||||||
chunkWords = new ArrayList<>();
|
|
||||||
|
|
||||||
if (splitByX && !isSplitByRuling) {
|
|
||||||
wasSplitted = true;
|
|
||||||
cb1.setOrientation(Orientation.LEFT);
|
|
||||||
splitX1 = word.getMinXDirAdj();
|
|
||||||
} else if (newLineAfterSplit && !isSplitByRuling) {
|
|
||||||
wasSplitted = false;
|
|
||||||
cb1.setOrientation(Orientation.RIGHT);
|
|
||||||
splitX1 = null;
|
|
||||||
} else if (prevOrientation != null && prevOrientation.equals(Orientation.RIGHT) && (lineSeparation || !startFromTop || !splitByX || !newLineAfterSplit || !isSplitByRuling)) {
|
|
||||||
cb1.setOrientation(Orientation.LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
minX = 1000;
|
|
||||||
maxX = 0;
|
|
||||||
minY = 1000;
|
|
||||||
maxY = 0;
|
|
||||||
prev = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunkWords.add(word);
|
|
||||||
|
|
||||||
prev = word;
|
|
||||||
if (word.getMinXDirAdj() < minX) {
|
|
||||||
minX = word.getMinXDirAdj();
|
|
||||||
}
|
|
||||||
if (word.getMaxXDirAdj() > maxX) {
|
|
||||||
maxX = word.getMaxXDirAdj();
|
|
||||||
}
|
|
||||||
if (word.getMinYDirAdj() < minY) {
|
|
||||||
minY = word.getMinYDirAdj();
|
|
||||||
}
|
|
||||||
if (word.getMaxYDirAdj() > maxY) {
|
|
||||||
maxY = word.getMaxYDirAdj();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextPageBlock cb1 = buildTextBlock(chunkWords, indexOnPage);
|
|
||||||
if (cb1 != null) {
|
|
||||||
chunkBlockList.add(cb1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterator<AbstractPageBlock> itty = chunkBlockList.iterator();
|
|
||||||
|
|
||||||
TextPageBlock previousLeft = null;
|
|
||||||
TextPageBlock previousRight = null;
|
|
||||||
while (itty.hasNext()) {
|
|
||||||
TextPageBlock block = (TextPageBlock) itty.next();
|
|
||||||
|
|
||||||
if (previousLeft != null && block.getOrientation().equals(Orientation.LEFT)) {
|
|
||||||
if (previousLeft.getMinY() > block.getMinY() && block.getMaxY() + block.getMostPopularWordHeight() > previousLeft.getMinY()) {
|
|
||||||
previousLeft.add(block);
|
|
||||||
itty.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousRight != null && block.getOrientation().equals(Orientation.RIGHT)) {
|
|
||||||
if (previousRight.getMinY() > block.getMinY() && block.getMaxY() + block.getMostPopularWordHeight() > previousRight.getMinY()) {
|
|
||||||
previousRight.add(block);
|
|
||||||
itty.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block.getOrientation().equals(Orientation.LEFT)) {
|
|
||||||
previousLeft = block;
|
|
||||||
} else if (block.getOrientation().equals(Orientation.RIGHT)) {
|
|
||||||
previousRight = block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itty = chunkBlockList.iterator();
|
|
||||||
TextPageBlock previous = null;
|
|
||||||
while (itty.hasNext()) {
|
|
||||||
TextPageBlock block = (TextPageBlock) itty.next();
|
|
||||||
|
|
||||||
if (previous != null && previous.getOrientation().equals(Orientation.LEFT) && block.getOrientation().equals(Orientation.LEFT) && equalsWithThreshold(block.getMaxY(),
|
|
||||||
previous.getMaxY()) || previous != null && previous.getOrientation().equals(Orientation.LEFT) && block.getOrientation()
|
|
||||||
.equals(Orientation.RIGHT) && equalsWithThreshold(block.getMaxY(), previous.getMaxY())) {
|
|
||||||
previous.add(block);
|
|
||||||
itty.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
previous = block;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ClassificationPage(chunkBlockList);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean equalsWithThreshold(float f1, float f2) {
|
|
||||||
|
|
||||||
return Math.abs(f1 - f2) < THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList, int indexOnPage) {
|
|
||||||
|
|
||||||
TextPageBlock textBlock = null;
|
|
||||||
|
|
||||||
FloatFrequencyCounter lineHeightFrequencyCounter = new FloatFrequencyCounter();
|
|
||||||
FloatFrequencyCounter fontSizeFrequencyCounter = new FloatFrequencyCounter();
|
|
||||||
FloatFrequencyCounter spaceFrequencyCounter = new FloatFrequencyCounter();
|
|
||||||
StringFrequencyCounter fontFrequencyCounter = new StringFrequencyCounter();
|
|
||||||
StringFrequencyCounter styleFrequencyCounter = new StringFrequencyCounter();
|
|
||||||
|
|
||||||
for (TextPositionSequence wordBlock : wordBlockList) {
|
|
||||||
|
|
||||||
lineHeightFrequencyCounter.add(wordBlock.getTextHeight());
|
|
||||||
fontSizeFrequencyCounter.add(wordBlock.getFontSize());
|
|
||||||
spaceFrequencyCounter.add(wordBlock.getSpaceWidth());
|
|
||||||
fontFrequencyCounter.add(wordBlock.getFont());
|
|
||||||
styleFrequencyCounter.add(wordBlock.getFontStyle());
|
|
||||||
|
|
||||||
if (textBlock == null) {
|
|
||||||
textBlock = new TextPageBlock(wordBlock.getMinXDirAdj(),
|
|
||||||
wordBlock.getMaxXDirAdj(),
|
|
||||||
wordBlock.getMinYDirAdj(),
|
|
||||||
wordBlock.getMaxYDirAdj(),
|
|
||||||
wordBlockList,
|
|
||||||
wordBlock.getRotation());
|
|
||||||
} else {
|
|
||||||
TextPageBlock spatialEntity = textBlock.union(wordBlock);
|
|
||||||
textBlock.resize(spatialEntity.getMinX(), spatialEntity.getMinY(), spatialEntity.getWidth(), spatialEntity.getHeight());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textBlock != null) {
|
|
||||||
textBlock.setMostPopularWordFont(fontFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setMostPopularWordStyle(styleFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setMostPopularWordFontSize(fontSizeFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setMostPopularWordHeight(lineHeightFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setMostPopularWordSpaceWidth(spaceFrequencyCounter.getMostPopular());
|
|
||||||
textBlock.setHighestFontSize(fontSizeFrequencyCounter.getHighest());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textBlock != null && textBlock.getSequences() != null && textBlock.getSequences().stream().map(t -> round(t.getMinYDirAdj(), 3)).collect(toSet()).size() == 1) {
|
|
||||||
textBlock.getSequences().sort(Comparator.comparing(TextPositionSequence::getMinXDirAdj));
|
|
||||||
}
|
|
||||||
return textBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isSplitByRuling(float minX,
|
|
||||||
float minY,
|
|
||||||
float maxX,
|
|
||||||
float maxY,
|
|
||||||
TextPositionSequence word,
|
|
||||||
List<Ruling> horizontalRulingLines,
|
|
||||||
List<Ruling> verticalRulingLines) {
|
|
||||||
|
|
||||||
return isSplitByRuling(maxX,
|
|
||||||
minY,
|
|
||||||
word.getMinXDirAdj(),
|
|
||||||
word.getMinYDirAdj(),
|
|
||||||
verticalRulingLines,
|
|
||||||
word.getDir().getDegrees(),
|
|
||||||
word.getPageWidth(),
|
|
||||||
word.getPageHeight()) //
|
|
||||||
|| isSplitByRuling(minX,
|
|
||||||
minY,
|
|
||||||
word.getMinXDirAdj(),
|
|
||||||
word.getMaxYDirAdj(),
|
|
||||||
horizontalRulingLines,
|
|
||||||
word.getDir().getDegrees(),
|
|
||||||
word.getPageWidth(),
|
|
||||||
word.getPageHeight()) //
|
|
||||||
|| isSplitByRuling(maxX,
|
|
||||||
minY,
|
|
||||||
word.getMinXDirAdj(),
|
|
||||||
word.getMinYDirAdj(),
|
|
||||||
horizontalRulingLines,
|
|
||||||
word.getDir().getDegrees(),
|
|
||||||
word.getPageWidth(),
|
|
||||||
word.getPageHeight()) //
|
|
||||||
|| isSplitByRuling(minX,
|
|
||||||
minY,
|
|
||||||
word.getMinXDirAdj(),
|
|
||||||
word.getMaxYDirAdj(),
|
|
||||||
verticalRulingLines,
|
|
||||||
word.getDir().getDegrees(),
|
|
||||||
word.getPageWidth(),
|
|
||||||
word.getPageHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isSplitByRuling(float previousX2, float previousY1, float currentX1, float currentY1, List<Ruling> rulingLines, float dir, float pageWidth, float pageHeight) {
|
|
||||||
|
|
||||||
for (Ruling ruling : rulingLines) {
|
|
||||||
var line = RulingTextDirAdjustUtil.convertToDirAdj(ruling, dir, pageWidth, pageHeight);
|
|
||||||
if (line.intersectsLine(previousX2, previousY1, currentX1, currentY1)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private double round(float value, int decimalPoints) {
|
|
||||||
|
|
||||||
var d = Math.pow(10, decimalPoints);
|
|
||||||
return Math.round(value * d) / d;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationDocument;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.PageBlockType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.PositionUtils;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@ConditionalOnProperty(prefix = "application", name = "type", havingValue = "RedactManager")
|
|
||||||
public class RedactManagerClassificationService implements ClassificationService {
|
|
||||||
|
|
||||||
private final BodyTextFrameService bodyTextFrameService;
|
|
||||||
|
|
||||||
|
|
||||||
public void classifyDocument(ClassificationDocument document) {
|
|
||||||
|
|
||||||
Rectangle bodyTextFrame = bodyTextFrameService.calculateBodyTextFrame(document.getPages(), document.getFontSizeCounter(), false);
|
|
||||||
Rectangle landscapeBodyTextFrame = bodyTextFrameService.calculateBodyTextFrame(document.getPages(), document.getFontSizeCounter(), true);
|
|
||||||
List<Float> headlineFontSizes = document.getFontSizeCounter().getHighterThanMostPopular();
|
|
||||||
|
|
||||||
log.debug("Document FontSize counters are: {}", document.getFontSizeCounter().getCountPerValue());
|
|
||||||
|
|
||||||
for (ClassificationPage page : document.getPages()) {
|
|
||||||
bodyTextFrameService.setBodyTextFrameAdjustedToPage(page, bodyTextFrame, landscapeBodyTextFrame);
|
|
||||||
classifyPage(page, document, headlineFontSizes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void classifyPage(ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
|
||||||
|
|
||||||
for (AbstractPageBlock textBlock : page.getTextBlocks()) {
|
|
||||||
if (textBlock instanceof TextPageBlock) {
|
|
||||||
classifyBlock((TextPageBlock) textBlock, page, document, headlineFontSizes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void classifyBlock(TextPageBlock textBlock, ClassificationPage page, ClassificationDocument document, List<Float> headlineFontSizes) {
|
|
||||||
|
|
||||||
var bodyTextFrame = page.getBodyTextFrame();
|
|
||||||
|
|
||||||
if (document.getFontSizeCounter().getMostPopular() == null) {
|
|
||||||
textBlock.setClassification(PageBlockType.OTHER);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (PositionUtils.isOverBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter()
|
|
||||||
.getMostPopular() == null || textBlock.getHighestFontSize() <= document.getFontSizeCounter().getMostPopular())) {
|
|
||||||
textBlock.setClassification(PageBlockType.HEADER);
|
|
||||||
|
|
||||||
} else if (PositionUtils.isUnderBodyTextFrame(bodyTextFrame, textBlock, page.getRotation()) && (document.getFontSizeCounter()
|
|
||||||
.getMostPopular() == null || textBlock.getHighestFontSize() <= document.getFontSizeCounter().getMostPopular())) {
|
|
||||||
textBlock.setClassification(PageBlockType.FOOTER);
|
|
||||||
} else if (page.getPageNumber() == 1 && (PositionUtils.getHeightDifferenceBetweenChunkWordAndDocumentWord(textBlock,
|
|
||||||
document.getTextHeightCounter().getMostPopular()) > 2.5 && textBlock.getHighestFontSize() > document.getFontSizeCounter().getMostPopular() || page.getTextBlocks()
|
|
||||||
.size() == 1)) {
|
|
||||||
if (!Pattern.matches("[0-9]+", textBlock.toString())) {
|
|
||||||
textBlock.setClassification(PageBlockType.TITLE);
|
|
||||||
}
|
|
||||||
} else if (textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter()
|
|
||||||
.getMostPopular() && PositionUtils.getApproxLineCount(textBlock) < 4.9 && (textBlock.getMostPopularWordStyle().equals("bold") || !document.getFontStyleCounter()
|
|
||||||
.getCountPerValue()
|
|
||||||
.containsKey("bold") && textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter().getMostPopular() + 1) && textBlock.getSequences()
|
|
||||||
.get(0)
|
|
||||||
.getTextPositions()
|
|
||||||
.get(0)
|
|
||||||
.getFontSizeInPt() >= textBlock.getMostPopularWordFontSize()) {
|
|
||||||
|
|
||||||
for (int i = 1; i <= headlineFontSizes.size(); i++) {
|
|
||||||
if (textBlock.getMostPopularWordFontSize() == headlineFontSizes.get(i - 1)) {
|
|
||||||
textBlock.setClassification(PageBlockType.getHeadlineType(i));
|
|
||||||
document.setHeadlines(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!textBlock.getText().startsWith("Figure ") && PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordStyle()
|
|
||||||
.equals("bold") && !document.getFontStyleCounter().getMostPopular().equals("bold") && PositionUtils.getApproxLineCount(textBlock) < 2.9 && textBlock.getSequences()
|
|
||||||
.get(0)
|
|
||||||
.getTextPositions()
|
|
||||||
.get(0)
|
|
||||||
.getFontSizeInPt() >= textBlock.getMostPopularWordFontSize()) {
|
|
||||||
textBlock.setClassification(PageBlockType.getHeadlineType(headlineFontSizes.size() + 1));
|
|
||||||
document.setHeadlines(true);
|
|
||||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter()
|
|
||||||
.getMostPopular() && textBlock.getMostPopularWordStyle().equals("bold") && !document.getFontStyleCounter().getMostPopular().equals("bold")) {
|
|
||||||
textBlock.setClassification(PageBlockType.PARAGRAPH_BOLD);
|
|
||||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFont()
|
|
||||||
.equals(document.getFontCounter().getMostPopular()) && textBlock.getMostPopularWordStyle()
|
|
||||||
.equals(document.getFontStyleCounter().getMostPopular()) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()) {
|
|
||||||
textBlock.setClassification(PageBlockType.PARAGRAPH);
|
|
||||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter()
|
|
||||||
.getMostPopular() && textBlock.getMostPopularWordStyle().equals("italic") && !document.getFontStyleCounter()
|
|
||||||
.getMostPopular()
|
|
||||||
.equals("italic") && PositionUtils.getApproxLineCount(textBlock) < 2.9) {
|
|
||||||
textBlock.setClassification(PageBlockType.PARAGRAPH_ITALIC);
|
|
||||||
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)) {
|
|
||||||
textBlock.setClassification(PageBlockType.PARAGRAPH_UNKNOWN);
|
|
||||||
} else {
|
|
||||||
textBlock.setClassification(PageBlockType.OTHER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,235 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import java.awt.geom.Line2D;
|
|
||||||
import java.awt.geom.Point2D;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.table.PdfTableCell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.CleanRulings;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Ruling;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.Utils;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class RulingCleaningService {
|
|
||||||
|
|
||||||
private final RedactionServiceSettings redactionServiceSettings;
|
|
||||||
|
|
||||||
|
|
||||||
public CleanRulings getCleanRulings(List<PdfTableCell> pdfTableCells, List<Ruling> rulings, float minCharWidth, float maxCharHeight) {
|
|
||||||
|
|
||||||
if (!rulings.isEmpty()) {
|
|
||||||
snapPoints(rulings, minCharWidth, maxCharHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Ruling> vrs = new ArrayList<>();
|
|
||||||
for (Ruling vr : rulings) {
|
|
||||||
if (vr.vertical()) {
|
|
||||||
vrs.add(vr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (vrs.isEmpty() && redactionServiceSettings.isCvTableParsingEnabled()) {
|
|
||||||
vrs.addAll(extractVerticalRulings(pdfTableCells));
|
|
||||||
}
|
|
||||||
List<Ruling> verticalRulingLines = collapseOrientedRulings(vrs);
|
|
||||||
|
|
||||||
List<Ruling> hrs = new ArrayList<>();
|
|
||||||
for (Ruling hr : rulings) {
|
|
||||||
if (hr.horizontal()) {
|
|
||||||
hrs.add(hr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hrs.isEmpty() && redactionServiceSettings.isCvTableParsingEnabled()) {
|
|
||||||
hrs.addAll(extractHorizontalRulings(pdfTableCells));
|
|
||||||
}
|
|
||||||
List<Ruling> horizontalRulingLines = collapseOrientedRulings(hrs);
|
|
||||||
|
|
||||||
return CleanRulings.builder().vertical(verticalRulingLines).horizontal(horizontalRulingLines).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void snapPoints(List<? extends Line2D.Float> rulings, float xThreshold, float yThreshold) {
|
|
||||||
|
|
||||||
// collect points and keep a Line -> p1,p2 map
|
|
||||||
Map<Line2D.Float, Point2D[]> linesToPoints = new HashMap<>();
|
|
||||||
List<Point2D> points = new ArrayList<>();
|
|
||||||
for (Line2D.Float r : rulings) {
|
|
||||||
Point2D p1 = r.getP1();
|
|
||||||
Point2D p2 = r.getP2();
|
|
||||||
linesToPoints.put(r, new Point2D[]{p1, p2});
|
|
||||||
points.add(p1);
|
|
||||||
points.add(p2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// snap by X
|
|
||||||
points.sort(Comparator.comparingDouble(Point2D::getX));
|
|
||||||
|
|
||||||
List<List<Point2D>> groupedPoints = new ArrayList<>();
|
|
||||||
groupedPoints.add(new ArrayList<>(Collections.singletonList(points.get(0))));
|
|
||||||
|
|
||||||
for (Point2D p : points.subList(1, points.size() - 1)) {
|
|
||||||
List<Point2D> last = groupedPoints.get(groupedPoints.size() - 1);
|
|
||||||
if (Math.abs(p.getX() - last.get(0).getX()) < xThreshold) {
|
|
||||||
groupedPoints.get(groupedPoints.size() - 1).add(p);
|
|
||||||
} else {
|
|
||||||
groupedPoints.add(new ArrayList<>(Collections.singletonList(p)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (List<Point2D> group : groupedPoints) {
|
|
||||||
float avgLoc = 0;
|
|
||||||
for (Point2D p : group) {
|
|
||||||
avgLoc += p.getX();
|
|
||||||
}
|
|
||||||
avgLoc /= group.size();
|
|
||||||
for (Point2D p : group) {
|
|
||||||
p.setLocation(avgLoc, p.getY());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ---
|
|
||||||
|
|
||||||
// snap by Y
|
|
||||||
points.sort(Comparator.comparingDouble(Point2D::getY));
|
|
||||||
|
|
||||||
groupedPoints = new ArrayList<>();
|
|
||||||
groupedPoints.add(new ArrayList<>(Collections.singletonList(points.get(0))));
|
|
||||||
|
|
||||||
for (Point2D p : points.subList(1, points.size() - 1)) {
|
|
||||||
List<Point2D> last = groupedPoints.get(groupedPoints.size() - 1);
|
|
||||||
if (Math.abs(p.getY() - last.get(0).getY()) < yThreshold) {
|
|
||||||
groupedPoints.get(groupedPoints.size() - 1).add(p);
|
|
||||||
} else {
|
|
||||||
groupedPoints.add(new ArrayList<>(Collections.singletonList(p)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (List<Point2D> group : groupedPoints) {
|
|
||||||
float avgLoc = 0;
|
|
||||||
for (Point2D p : group) {
|
|
||||||
avgLoc += p.getY();
|
|
||||||
}
|
|
||||||
avgLoc /= group.size();
|
|
||||||
for (Point2D p : group) {
|
|
||||||
p.setLocation(p.getX(), avgLoc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ---
|
|
||||||
|
|
||||||
// finally, modify lines
|
|
||||||
for (Map.Entry<Line2D.Float, Point2D[]> ltp : linesToPoints.entrySet()) {
|
|
||||||
Point2D[] p = ltp.getValue();
|
|
||||||
ltp.getKey().setLine(p[0], p[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Collection<? extends Ruling> extractVerticalRulings(List<PdfTableCell> pdfTableCells) {
|
|
||||||
|
|
||||||
List<Ruling> vrs = new ArrayList<>();
|
|
||||||
|
|
||||||
if (pdfTableCells != null) {
|
|
||||||
for (PdfTableCell pdfTableCell : pdfTableCells) {
|
|
||||||
Ruling leftLine = createRuling(pdfTableCell.getX0(), pdfTableCell.getX0(), pdfTableCell.getY0(), pdfTableCell.getY1());
|
|
||||||
Ruling rightLine = createRuling(pdfTableCell.getX1(), pdfTableCell.getX1(), pdfTableCell.getY0(), pdfTableCell.getY1());
|
|
||||||
vrs.add(leftLine);
|
|
||||||
vrs.add(rightLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Collection<? extends Ruling> extractHorizontalRulings(List<PdfTableCell> pdfTableCells) {
|
|
||||||
|
|
||||||
List<Ruling> hrs = new ArrayList<>();
|
|
||||||
|
|
||||||
if (pdfTableCells != null) {
|
|
||||||
for (PdfTableCell pdfTableCell : pdfTableCells) {
|
|
||||||
Ruling topLine = createRuling(pdfTableCell.getX0(), pdfTableCell.getX1(), pdfTableCell.getY1(), pdfTableCell.getY1());
|
|
||||||
Ruling baseLine = createRuling(pdfTableCell.getX0(), pdfTableCell.getX1(), pdfTableCell.getY0(), pdfTableCell.getY0());
|
|
||||||
hrs.add(topLine);
|
|
||||||
hrs.add(baseLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Ruling createRuling(float tableCellX0, float tableCellX1, float tableCellY0, float tableCellY1) {
|
|
||||||
|
|
||||||
float x0 = tableCellX0;
|
|
||||||
float x1 = tableCellX1;
|
|
||||||
float y0 = tableCellY0;
|
|
||||||
float y1 = tableCellY1;
|
|
||||||
|
|
||||||
if (x1 < x0) {
|
|
||||||
x0 = tableCellX1;
|
|
||||||
x1 = tableCellX0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y1 < y0) {
|
|
||||||
y0 = tableCellY1;
|
|
||||||
y1 = tableCellY0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Ruling(new Point2D.Float(x0, y0), new Point2D.Float(x1, y1));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Ruling> collapseOrientedRulings(List<Ruling> lines) {
|
|
||||||
|
|
||||||
int COLINEAR_OR_PARALLEL_PIXEL_EXPAND_AMOUNT = 1;
|
|
||||||
return collapseOrientedRulings(lines, COLINEAR_OR_PARALLEL_PIXEL_EXPAND_AMOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Ruling> collapseOrientedRulings(List<Ruling> lines, int expandAmount) {
|
|
||||||
|
|
||||||
ArrayList<Ruling> rv = new ArrayList<>();
|
|
||||||
lines.sort((a, b) -> {
|
|
||||||
final float diff = a.getPosition() - b.getPosition();
|
|
||||||
return Float.compare(diff == 0 ? a.getStart() - b.getStart() : diff, 0f);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (Ruling next_line : lines) {
|
|
||||||
Ruling last = rv.isEmpty() ? null : rv.get(rv.size() - 1);
|
|
||||||
// if current line colinear with next, and are "close enough": expand current line
|
|
||||||
if (last != null && Utils.feq(next_line.getPosition(), last.getPosition()) && last.nearlyIntersects(next_line, expandAmount)) {
|
|
||||||
final float lastStart = last.getStart();
|
|
||||||
final float lastEnd = last.getEnd();
|
|
||||||
|
|
||||||
final boolean lastFlipped = lastStart > lastEnd;
|
|
||||||
final boolean nextFlipped = next_line.getStart() > next_line.getEnd();
|
|
||||||
|
|
||||||
boolean differentDirections = nextFlipped != lastFlipped;
|
|
||||||
float nextS = differentDirections ? next_line.getEnd() : next_line.getStart();
|
|
||||||
float nextE = differentDirections ? next_line.getStart() : next_line.getEnd();
|
|
||||||
|
|
||||||
final float newStart = lastFlipped ? Math.max(nextS, lastStart) : Math.min(nextS, lastStart);
|
|
||||||
final float newEnd = lastFlipped ? Math.min(nextE, lastEnd) : Math.max(nextE, lastEnd);
|
|
||||||
last.setStartEnd(newStart, newEnd);
|
|
||||||
assert !last.oblique();
|
|
||||||
} else if (next_line.length() == 0) {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
rv.add(next_line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,304 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationDocument;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationFooter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationHeader;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationSection;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.PageBlockType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image.ClassifiedImage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Cell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.TablePageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.UnclassifiedText;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class SectionsBuilderService {
|
|
||||||
|
|
||||||
public void buildSections(ClassificationDocument document) {
|
|
||||||
|
|
||||||
List<AbstractPageBlock> chunkWords = new ArrayList<>();
|
|
||||||
List<ClassificationSection> chunkBlockList = new ArrayList<>();
|
|
||||||
List<ClassificationHeader> headers = new ArrayList<>();
|
|
||||||
List<ClassificationFooter> footers = new ArrayList<>();
|
|
||||||
List<UnclassifiedText> unclassifiedTexts = new ArrayList<>();
|
|
||||||
|
|
||||||
AbstractPageBlock prev = null;
|
|
||||||
|
|
||||||
String lastHeadline = "";
|
|
||||||
TablePageBlock previousTable = null;
|
|
||||||
for (ClassificationPage page : document.getPages()) {
|
|
||||||
List<TextPageBlock> header = new ArrayList<>();
|
|
||||||
List<TextPageBlock> footer = new ArrayList<>();
|
|
||||||
List<TextPageBlock> unclassifiedText = new ArrayList<>();
|
|
||||||
for (AbstractPageBlock current : page.getTextBlocks()) {
|
|
||||||
|
|
||||||
if (current.getClassification() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
current.setPage(page.getPageNumber());
|
|
||||||
|
|
||||||
if (current.getClassification().equals(PageBlockType.HEADER)) {
|
|
||||||
header.add((TextPageBlock) current);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.getClassification().equals(PageBlockType.FOOTER)) {
|
|
||||||
footer.add((TextPageBlock) current);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.getClassification().equals(PageBlockType.OTHER)) {
|
|
||||||
unclassifiedText.add((TextPageBlock) current);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prev != null && current.getClassification().isHeadline() && !prev.getClassification().isHeadline() || !document.isHeadlines()) {
|
|
||||||
ClassificationSection chunkBlock = buildTextBlock(chunkWords, lastHeadline);
|
|
||||||
chunkBlock.setHeadline(lastHeadline);
|
|
||||||
if (document.isHeadlines()) {
|
|
||||||
lastHeadline = current.getText();
|
|
||||||
}
|
|
||||||
chunkBlockList.add(chunkBlock);
|
|
||||||
chunkWords = new ArrayList<>();
|
|
||||||
if (CollectionUtils.isNotEmpty(chunkBlock.getTables())) {
|
|
||||||
previousTable = chunkBlock.getTables().get(chunkBlock.getTables().size() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current instanceof TablePageBlock table) {
|
|
||||||
// Distribute header information for subsequent tables
|
|
||||||
mergeTableMetadata(table, previousTable);
|
|
||||||
previousTable = table;
|
|
||||||
}
|
|
||||||
chunkWords.add(current);
|
|
||||||
prev = current;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!header.isEmpty()) {
|
|
||||||
headers.add(new ClassificationHeader(header));
|
|
||||||
}
|
|
||||||
if (!footer.isEmpty()) {
|
|
||||||
footers.add(new ClassificationFooter(footer));
|
|
||||||
}
|
|
||||||
if (!unclassifiedText.isEmpty()) {
|
|
||||||
unclassifiedTexts.add(new UnclassifiedText(unclassifiedText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClassificationSection chunkBlock = buildTextBlock(chunkWords, lastHeadline);
|
|
||||||
chunkBlock.setHeadline(lastHeadline);
|
|
||||||
chunkBlockList.add(chunkBlock);
|
|
||||||
|
|
||||||
document.setSections(chunkBlockList);
|
|
||||||
document.setHeaders(headers);
|
|
||||||
document.setFooters(footers);
|
|
||||||
document.setUnclassifiedTexts(unclassifiedTexts);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void addImagesToSections(ClassificationDocument document) {
|
|
||||||
|
|
||||||
Map<Integer, List<ClassificationSection>> sectionMap = new HashMap<>();
|
|
||||||
for (ClassificationSection section : document.getSections()) {
|
|
||||||
for (AbstractPageBlock container : section.getPageBlocks()) {
|
|
||||||
|
|
||||||
List<ClassificationSection> sectionsOnPage = sectionMap.computeIfAbsent(container.getPage(), c -> new ArrayList<>());
|
|
||||||
if (sectionsOnPage.contains(section)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sectionsOnPage.add(section);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sectionMap.isEmpty()) {
|
|
||||||
ClassificationSection section = new ClassificationSection();
|
|
||||||
document.getSections().add(section);
|
|
||||||
sectionMap.computeIfAbsent(1, x -> new ArrayList<>()).add(section);
|
|
||||||
}
|
|
||||||
|
|
||||||
// first page is always a paragraph, else we can't process pages 1..N,
|
|
||||||
// where N is the first found page with a paragraph
|
|
||||||
if (sectionMap.get(1) == null) {
|
|
||||||
ClassificationSection section = new ClassificationSection();
|
|
||||||
document.getSections().add(section);
|
|
||||||
sectionMap.computeIfAbsent(1, x -> new ArrayList<>()).add(section);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ClassificationPage page : document.getPages()) {
|
|
||||||
for (ClassifiedImage image : page.getImages()) {
|
|
||||||
List<ClassificationSection> sectionsOnPage = sectionMap.get(page.getPageNumber());
|
|
||||||
if (sectionsOnPage == null) {
|
|
||||||
int i = page.getPageNumber();
|
|
||||||
while (sectionsOnPage == null) {
|
|
||||||
sectionsOnPage = sectionMap.get(i);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (ClassificationSection section : sectionsOnPage) {
|
|
||||||
Float xMin = null;
|
|
||||||
Float yMin = null;
|
|
||||||
Float xMax = null;
|
|
||||||
Float yMax = null;
|
|
||||||
|
|
||||||
for (AbstractPageBlock abs : section.getPageBlocks()) {
|
|
||||||
if (abs.getPage() != page.getPageNumber()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abs.getMinX() < abs.getMaxX()) {
|
|
||||||
if (xMin == null || abs.getMinX() < xMin) {
|
|
||||||
xMin = abs.getMinX();
|
|
||||||
}
|
|
||||||
if (xMax == null || abs.getMaxX() > xMax) {
|
|
||||||
xMax = abs.getMaxX();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (xMin == null || abs.getMaxX() < xMin) {
|
|
||||||
xMin = abs.getMaxX();
|
|
||||||
}
|
|
||||||
if (xMax == null || abs.getMinX() > xMax) {
|
|
||||||
xMax = abs.getMinX();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abs.getMinY() < abs.getMaxY()) {
|
|
||||||
if (yMin == null || abs.getMinY() < yMin) {
|
|
||||||
yMin = abs.getMinY();
|
|
||||||
}
|
|
||||||
if (yMax == null || abs.getMaxY() > yMax) {
|
|
||||||
yMax = abs.getMaxY();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (yMin == null || abs.getMaxY() < yMin) {
|
|
||||||
yMin = abs.getMaxY();
|
|
||||||
}
|
|
||||||
if (yMax == null || abs.getMinY() > yMax) {
|
|
||||||
yMax = abs.getMinY();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Image position x: {}, y: {}", image.getPosition().getX(), image.getPosition().getY());
|
|
||||||
log.debug("Paragraph position xMin: {}, xMax: {}, yMin: {}, yMax: {}", xMin, xMax, yMin, yMax);
|
|
||||||
|
|
||||||
if (xMin != null && xMax != null && yMin != null && yMax != null && image.getPosition().getX() >= xMin && image.getPosition()
|
|
||||||
.getX() <= xMax && image.getPosition().getY() >= yMin && image.getPosition().getY() <= yMax) {
|
|
||||||
section.getImages().add(image);
|
|
||||||
image.setAppendedToSection(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!image.isAppendedToSection()) {
|
|
||||||
log.debug("Image uses first paragraph");
|
|
||||||
sectionsOnPage.get(0).getImages().add(image);
|
|
||||||
image.setAppendedToSection(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void mergeTableMetadata(TablePageBlock currentTable, TablePageBlock previousTable) {
|
|
||||||
|
|
||||||
// Distribute header information for subsequent tables
|
|
||||||
if (previousTable != null && hasInvalidHeaderInformation(currentTable) && hasValidHeaderInformation(previousTable)) {
|
|
||||||
List<Cell> previousTableNonHeaderRow = getRowWithNonHeaderCells(previousTable);
|
|
||||||
List<Cell> tableNonHeaderRow = getRowWithNonHeaderCells(currentTable);
|
|
||||||
// Allow merging of tables if header row is separated from first logical non-header row
|
|
||||||
if (previousTableNonHeaderRow.isEmpty() && previousTable.getRowCount() == 1 && previousTable.getRows().get(0).size() == tableNonHeaderRow.size()) {
|
|
||||||
previousTableNonHeaderRow = previousTable.getRows().get(0).stream().map(cell -> {
|
|
||||||
Cell fakeCell = new Cell(cell.getPoints()[0], cell.getPoints()[2]);
|
|
||||||
fakeCell.setHeaderCells(Collections.singletonList(cell));
|
|
||||||
return fakeCell;
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
if (previousTableNonHeaderRow.size() == tableNonHeaderRow.size()) {
|
|
||||||
for (int i = currentTable.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
|
|
||||||
List<Cell> row = currentTable.getRows().get(i);
|
|
||||||
if (row.size() == tableNonHeaderRow.size() && row.stream().allMatch(cell -> cell.getHeaderCells().isEmpty())) {
|
|
||||||
for (int j = 0; j < row.size(); j++) {
|
|
||||||
row.get(j).setHeaderCells(previousTableNonHeaderRow.get(j).getHeaderCells());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private ClassificationSection buildTextBlock(List<AbstractPageBlock> wordBlockList, String lastHeadline) {
|
|
||||||
|
|
||||||
ClassificationSection section = new ClassificationSection();
|
|
||||||
|
|
||||||
for (AbstractPageBlock container : wordBlockList) {
|
|
||||||
if (container instanceof TablePageBlock table) {
|
|
||||||
|
|
||||||
if (lastHeadline == null || lastHeadline.isEmpty()) {
|
|
||||||
table.setHeadline("Text in table");
|
|
||||||
} else {
|
|
||||||
table.setHeadline("TablePageBlock in: " + lastHeadline);
|
|
||||||
}
|
|
||||||
|
|
||||||
section.getPageBlocks().add(table);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextPageBlock wordBlock = (TextPageBlock) container;
|
|
||||||
section.getPageBlocks().add(wordBlock);
|
|
||||||
}
|
|
||||||
return section;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean hasValidHeaderInformation(TablePageBlock table) {
|
|
||||||
|
|
||||||
return !hasInvalidHeaderInformation(table);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean hasInvalidHeaderInformation(TablePageBlock table) {
|
|
||||||
|
|
||||||
return table.getRows().stream().flatMap(row -> row.stream().filter(cell -> CollectionUtils.isNotEmpty(cell.getHeaderCells()))).findAny().isEmpty();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Cell> getRowWithNonHeaderCells(TablePageBlock table) {
|
|
||||||
|
|
||||||
for (int i = table.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
|
|
||||||
List<Cell> row = table.getRows().get(i);
|
|
||||||
if (row.size() == 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
boolean allNonHeader = true;
|
|
||||||
for (Cell cell : row) {
|
|
||||||
if (cell.isHeaderCell()) {
|
|
||||||
allNonHeader = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (allNonHeader) {
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Collections.emptyList();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,337 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service;
|
|
||||||
|
|
||||||
import java.awt.geom.Point2D;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationPage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Cell;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.CleanRulings;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Rectangle;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Ruling;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.TablePageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils.Utils;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class TableExtractionService {
|
|
||||||
|
|
||||||
private static final Comparator<Point2D> X_FIRST_POINT_COMPARATOR = (arg0, arg1) -> {
|
|
||||||
|
|
||||||
int rv = 0;
|
|
||||||
float arg0X = Utils.round(arg0.getX(), 2);
|
|
||||||
float arg0Y = Utils.round(arg0.getY(), 2);
|
|
||||||
float arg1X = Utils.round(arg1.getX(), 2);
|
|
||||||
float arg1Y = Utils.round(arg1.getY(), 2);
|
|
||||||
|
|
||||||
if (arg0X > arg1X) {
|
|
||||||
rv = 1;
|
|
||||||
} else if (arg0X < arg1X) {
|
|
||||||
rv = -1;
|
|
||||||
} else if (arg0Y > arg1Y) {
|
|
||||||
rv = 1;
|
|
||||||
} else if (arg0Y < arg1Y) {
|
|
||||||
rv = -1;
|
|
||||||
}
|
|
||||||
return rv;
|
|
||||||
};
|
|
||||||
private static final Comparator<Point2D> POINT_COMPARATOR = (arg0, arg1) -> {
|
|
||||||
|
|
||||||
int rv = 0;
|
|
||||||
float arg0X = Utils.round(arg0.getX(), 2);
|
|
||||||
float arg0Y = Utils.round(arg0.getY(), 2);
|
|
||||||
float arg1X = Utils.round(arg1.getX(), 2);
|
|
||||||
float arg1Y = Utils.round(arg1.getY(), 2);
|
|
||||||
|
|
||||||
if (arg0Y > arg1Y) {
|
|
||||||
rv = 1;
|
|
||||||
} else if (arg0Y < arg1Y) {
|
|
||||||
rv = -1;
|
|
||||||
} else if (arg0X > arg1X) {
|
|
||||||
rv = 1;
|
|
||||||
} else if (arg0X < arg1X) {
|
|
||||||
rv = -1;
|
|
||||||
}
|
|
||||||
return rv;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds tables on a page and moves textblocks into cells of the found tables.
|
|
||||||
* Note: This algorithm uses Pdf Coordinate System where {0,0} rotated with the page rotation.
|
|
||||||
* 0 -> LowerLeft
|
|
||||||
* 90 -> UpperLeft
|
|
||||||
* 180 -> UpperRight
|
|
||||||
* 270 -> LowerRight
|
|
||||||
* <p>
|
|
||||||
* DirAdj (Text direction adjusted) values can not be used here.
|
|
||||||
*
|
|
||||||
* @param cleanRulings The lines used to build the table.
|
|
||||||
* @param page Page object that contains textblocks and statistics.
|
|
||||||
*/
|
|
||||||
public void extractTables(CleanRulings cleanRulings, ClassificationPage page) {
|
|
||||||
|
|
||||||
List<Cell> cells = findCells(cleanRulings.getHorizontal(), cleanRulings.getVertical());
|
|
||||||
|
|
||||||
List<TextPageBlock> toBeRemoved = new ArrayList<>();
|
|
||||||
|
|
||||||
for (AbstractPageBlock abstractPageBlock : page.getTextBlocks()) {
|
|
||||||
TextPageBlock textBlock = (TextPageBlock) abstractPageBlock;
|
|
||||||
for (Cell cell : cells) {
|
|
||||||
if (cell.hasMinimumSize() && cell.intersects(textBlock.getPdfMinX(),
|
|
||||||
textBlock.getPdfMinY(),
|
|
||||||
textBlock.getPdfMaxX() - textBlock.getPdfMinX(),
|
|
||||||
textBlock.getPdfMaxY() - textBlock.getPdfMinY())) {
|
|
||||||
cell.addTextBlock(textBlock);
|
|
||||||
toBeRemoved.add(textBlock);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cells = new ArrayList<>(new HashSet<>(cells));
|
|
||||||
Utils.sort(cells, Rectangle.ILL_DEFINED_ORDER);
|
|
||||||
|
|
||||||
List<Rectangle> spreadsheetAreas = findSpreadsheetsFromCells(cells).stream().filter(r -> r.getWidth() > 0f && r.getHeight() > 0f).collect(Collectors.toList());
|
|
||||||
|
|
||||||
List<TablePageBlock> tables = new ArrayList<>();
|
|
||||||
for (Rectangle area : spreadsheetAreas) {
|
|
||||||
|
|
||||||
List<Cell> overlappingCells = new ArrayList<>();
|
|
||||||
for (Cell c : cells) {
|
|
||||||
if (c.hasMinimumSize() && c.intersects(area)) {
|
|
||||||
overlappingCells.add(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tables.add(new TablePageBlock(overlappingCells, area, page.getRotation()));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (TablePageBlock table : tables) {
|
|
||||||
int position = -1;
|
|
||||||
|
|
||||||
Iterator<AbstractPageBlock> itty = page.getTextBlocks().iterator();
|
|
||||||
while (itty.hasNext()) {
|
|
||||||
AbstractPageBlock textBlock = itty.next();
|
|
||||||
if (textBlock instanceof TextPageBlock ? table.containsBlock((TextPageBlock) textBlock) : table.contains(textBlock) && position == -1) {
|
|
||||||
position = page.getTextBlocks().indexOf(textBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (position != -1) {
|
|
||||||
page.getTextBlocks().add(position, table);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.getTextBlocks().removeAll(toBeRemoved);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public List<Cell> findCells(List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {
|
|
||||||
|
|
||||||
List<Cell> cellsFound = new ArrayList<>();
|
|
||||||
Map<Point2D, Ruling[]> intersectionPoints = Ruling.findIntersections(horizontalRulingLines, verticalRulingLines);
|
|
||||||
List<Point2D> intersectionPointsList = new ArrayList<>(intersectionPoints.keySet());
|
|
||||||
intersectionPointsList.sort(POINT_COMPARATOR);
|
|
||||||
|
|
||||||
for (int i = 0; i < intersectionPointsList.size(); i++) {
|
|
||||||
Point2D topLeft = intersectionPointsList.get(i);
|
|
||||||
Ruling[] hv = intersectionPoints.get(topLeft);
|
|
||||||
|
|
||||||
// CrossingPointsDirectlyBelow( topLeft );
|
|
||||||
List<Point2D> xPoints = new ArrayList<>();
|
|
||||||
// CrossingPointsDirectlyToTheRight( topLeft );
|
|
||||||
List<Point2D> yPoints = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Point2D p : intersectionPointsList.subList(i, intersectionPointsList.size())) {
|
|
||||||
if (p.getX() == topLeft.getX() && p.getY() > topLeft.getY()) {
|
|
||||||
xPoints.add(p);
|
|
||||||
}
|
|
||||||
if (p.getY() == topLeft.getY() && p.getX() > topLeft.getX()) {
|
|
||||||
yPoints.add(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outer:
|
|
||||||
for (Point2D xPoint : xPoints) {
|
|
||||||
// is there a vertical edge b/w topLeft and xPoint?
|
|
||||||
if (!hv[1].equals(intersectionPoints.get(xPoint)[1])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (Point2D yPoint : yPoints) {
|
|
||||||
// is there an horizontal edge b/w topLeft and yPoint ?
|
|
||||||
if (!hv[0].equals(intersectionPoints.get(yPoint)[0])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Point2D btmRight = new Point2D.Float((float) yPoint.getX(), (float) xPoint.getY());
|
|
||||||
if (intersectionPoints.containsKey(btmRight) && intersectionPoints.get(btmRight)[0].equals(intersectionPoints.get(xPoint)[0]) && intersectionPoints.get(btmRight)[1].equals(
|
|
||||||
intersectionPoints.get(yPoint)[1])) {
|
|
||||||
cellsFound.add(new Cell(topLeft, btmRight));
|
|
||||||
break outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO create cells for vertical ruling lines with aligned endpoints at the top/bottom of a grid
|
|
||||||
// that aren't connected with an horizontal ruler?
|
|
||||||
// see: https://github.com/jazzido/tabula-extractor/issues/78#issuecomment-41481207
|
|
||||||
|
|
||||||
return cellsFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Rectangle> findSpreadsheetsFromCells(List<? extends Rectangle> cells) {
|
|
||||||
// via: http://stackoverflow.com/questions/13746284/merging-multiple-adjacent-rectangles-into-one-polygon
|
|
||||||
List<Rectangle> rectangles = new ArrayList<>();
|
|
||||||
Set<Point2D> pointSet = new HashSet<>();
|
|
||||||
Map<Point2D, Point2D> edgesH = new HashMap<>();
|
|
||||||
Map<Point2D, Point2D> edgesV = new HashMap<>();
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
for (Rectangle cell : cells) {
|
|
||||||
for (Point2D pt : cell.getPoints()) {
|
|
||||||
if (pointSet.contains(pt)) { // shared vertex, remove it
|
|
||||||
pointSet.remove(pt);
|
|
||||||
} else {
|
|
||||||
pointSet.add(pt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// X first sort
|
|
||||||
List<Point2D> pointsSortX = new ArrayList<>(pointSet);
|
|
||||||
pointsSortX.sort(X_FIRST_POINT_COMPARATOR);
|
|
||||||
// Y first sort
|
|
||||||
List<Point2D> pointsSortY = new ArrayList<>(pointSet);
|
|
||||||
pointsSortY.sort(POINT_COMPARATOR);
|
|
||||||
|
|
||||||
while (i < pointSet.size()) {
|
|
||||||
float currY = (float) pointsSortY.get(i).getY();
|
|
||||||
while (i < pointSet.size() && Utils.feq(pointsSortY.get(i).getY(), currY)) {
|
|
||||||
edgesH.put(pointsSortY.get(i), pointsSortY.get(i + 1));
|
|
||||||
edgesH.put(pointsSortY.get(i + 1), pointsSortY.get(i));
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i = 0;
|
|
||||||
while (i < pointSet.size()) {
|
|
||||||
float currX = (float) pointsSortX.get(i).getX();
|
|
||||||
while (i < pointSet.size() && Utils.feq(pointsSortX.get(i).getX(), currX)) {
|
|
||||||
edgesV.put(pointsSortX.get(i), pointsSortX.get(i + 1));
|
|
||||||
edgesV.put(pointsSortX.get(i + 1), pointsSortX.get(i));
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all the polygons
|
|
||||||
List<List<PolygonVertex>> polygons = new ArrayList<>();
|
|
||||||
Point2D nextVertex;
|
|
||||||
while (!edgesH.isEmpty()) {
|
|
||||||
ArrayList<PolygonVertex> polygon = new ArrayList<>();
|
|
||||||
Point2D first = edgesH.keySet().iterator().next();
|
|
||||||
polygon.add(new PolygonVertex(first, Direction.HORIZONTAL));
|
|
||||||
edgesH.remove(first);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
PolygonVertex curr = polygon.get(polygon.size() - 1);
|
|
||||||
PolygonVertex lastAddedVertex;
|
|
||||||
if (curr.direction == Direction.HORIZONTAL) {
|
|
||||||
nextVertex = edgesV.get(curr.point);
|
|
||||||
edgesV.remove(curr.point);
|
|
||||||
lastAddedVertex = new PolygonVertex(nextVertex, Direction.VERTICAL);
|
|
||||||
} else {
|
|
||||||
nextVertex = edgesH.get(curr.point);
|
|
||||||
edgesH.remove(curr.point);
|
|
||||||
lastAddedVertex = new PolygonVertex(nextVertex, Direction.HORIZONTAL);
|
|
||||||
}
|
|
||||||
polygon.add(lastAddedVertex);
|
|
||||||
|
|
||||||
if (lastAddedVertex.equals(polygon.get(0))) {
|
|
||||||
// closed polygon
|
|
||||||
polygon.remove(polygon.size() - 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (PolygonVertex vertex : polygon) {
|
|
||||||
edgesH.remove(vertex.point);
|
|
||||||
edgesV.remove(vertex.point);
|
|
||||||
}
|
|
||||||
polygons.add(polygon);
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate grid-aligned minimum area rectangles for each found polygon
|
|
||||||
for (List<PolygonVertex> poly : polygons) {
|
|
||||||
float top = Float.MAX_VALUE;
|
|
||||||
float left = Float.MAX_VALUE;
|
|
||||||
float bottom = Float.MIN_VALUE;
|
|
||||||
float right = Float.MIN_VALUE;
|
|
||||||
for (PolygonVertex pt : poly) {
|
|
||||||
top = (float) Math.min(top, pt.point.getY());
|
|
||||||
left = (float) Math.min(left, pt.point.getX());
|
|
||||||
bottom = (float) Math.max(bottom, pt.point.getY());
|
|
||||||
right = (float) Math.max(right, pt.point.getX());
|
|
||||||
}
|
|
||||||
rectangles.add(new Rectangle(top, left, right - left, bottom - top));
|
|
||||||
}
|
|
||||||
|
|
||||||
return rectangles;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private enum Direction {
|
|
||||||
HORIZONTAL,
|
|
||||||
VERTICAL
|
|
||||||
}
|
|
||||||
|
|
||||||
static class PolygonVertex {
|
|
||||||
|
|
||||||
Point2D point;
|
|
||||||
Direction direction;
|
|
||||||
|
|
||||||
|
|
||||||
PolygonVertex(Point2D point, Direction direction) {
|
|
||||||
|
|
||||||
this.direction = direction;
|
|
||||||
this.point = point;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object other) {
|
|
||||||
|
|
||||||
if (this == other) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!(other instanceof PolygonVertex)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.point.equals(((PolygonVertex) other).point);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
|
|
||||||
return this.point.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
return String.format("%s[point=%s,direction=%s]", this.getClass().getName(), this.point.toString(), this.direction.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
/*
|
|
||||||
* CohenSutherland.java
|
|
||||||
* --------------------
|
|
||||||
* (c) 2007 by Intevation GmbH
|
|
||||||
*
|
|
||||||
* @author Sascha L. Teichmann (teichmann@intevation.de)
|
|
||||||
* @author Ludwig Reiter (ludwig@intevation.de)
|
|
||||||
*
|
|
||||||
* This program is free software under the LGPL (>=v2.1)
|
|
||||||
* Read the file LICENSE.txt coming with the sources for details.
|
|
||||||
*/
|
|
||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils;
|
|
||||||
|
|
||||||
import java.awt.geom.Line2D;
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements the well known Cohen Sutherland line
|
|
||||||
* clipping algorithm (line against clip rectangle).
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("all")
|
|
||||||
public final class CohenSutherlandClipping {
|
|
||||||
|
|
||||||
private static final int INSIDE = 0;
|
|
||||||
private static final int LEFT = 1;
|
|
||||||
private static final int RIGHT = 2;
|
|
||||||
private static final int BOTTOM = 4;
|
|
||||||
private static final int TOP = 8;
|
|
||||||
private double xMin;
|
|
||||||
private double yMin;
|
|
||||||
private double xMax;
|
|
||||||
private double yMax;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Cohen Sutherland clipper with clip rect (0, 0, 0, 0).
|
|
||||||
*/
|
|
||||||
public CohenSutherlandClipping() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Cohen Sutherland clipper with the given clip rectangle.
|
|
||||||
*
|
|
||||||
* @param clip the clip rectangle to use
|
|
||||||
*/
|
|
||||||
public CohenSutherlandClipping(Rectangle2D clip) {
|
|
||||||
|
|
||||||
setClip(clip);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the clip rectangle.
|
|
||||||
*
|
|
||||||
* @param clip the clip rectangle
|
|
||||||
*/
|
|
||||||
public void setClip(Rectangle2D clip) {
|
|
||||||
|
|
||||||
xMin = clip.getX();
|
|
||||||
xMax = xMin + clip.getWidth();
|
|
||||||
yMin = clip.getY();
|
|
||||||
yMax = yMin + clip.getHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private final int regionCode(double x, double y) {
|
|
||||||
|
|
||||||
int code = x < xMin ? LEFT : x > xMax ? RIGHT : INSIDE;
|
|
||||||
if (y < yMin) {
|
|
||||||
code |= BOTTOM;
|
|
||||||
} else if (y > yMax) {
|
|
||||||
code |= TOP;
|
|
||||||
}
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clips a given line against the clip rectangle.
|
|
||||||
* The modification (if needed) is done in place.
|
|
||||||
*
|
|
||||||
* @param line the line to clip
|
|
||||||
* @return true if line is clipped, false if line is
|
|
||||||
* totally outside the clip rect.
|
|
||||||
*/
|
|
||||||
public boolean clip(Line2D.Float line) {
|
|
||||||
|
|
||||||
double p1x = line.getX1();
|
|
||||||
double p1y = line.getY1();
|
|
||||||
double p2x = line.getX2();
|
|
||||||
double p2y = line.getY2();
|
|
||||||
|
|
||||||
double qx = 0d;
|
|
||||||
double qy = 0d;
|
|
||||||
|
|
||||||
boolean vertical = p1x == p2x;
|
|
||||||
|
|
||||||
double slope = vertical ? 0d : (p2y - p1y) / (p2x - p1x);
|
|
||||||
|
|
||||||
int c1 = regionCode(p1x, p1y);
|
|
||||||
int c2 = regionCode(p2x, p2y);
|
|
||||||
|
|
||||||
while (c1 != INSIDE || c2 != INSIDE) {
|
|
||||||
|
|
||||||
if ((c1 & c2) != INSIDE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int c = c1 == INSIDE ? c2 : c1;
|
|
||||||
|
|
||||||
if ((c & LEFT) != INSIDE) {
|
|
||||||
qx = xMin;
|
|
||||||
qy = (Utils.feq(qx, p1x) ? 0 : qx - p1x) * slope + p1y;
|
|
||||||
} else if ((c & RIGHT) != INSIDE) {
|
|
||||||
qx = xMax;
|
|
||||||
qy = (Utils.feq(qx, p1x) ? 0 : qx - p1x) * slope + p1y;
|
|
||||||
} else if ((c & BOTTOM) != INSIDE) {
|
|
||||||
qy = yMin;
|
|
||||||
qx = vertical ? p1x : (Utils.feq(qy, p1y) ? 0 : qy - p1y) / slope + p1x;
|
|
||||||
} else if ((c & TOP) != INSIDE) {
|
|
||||||
qy = yMax;
|
|
||||||
qx = vertical ? p1x : (Utils.feq(qy, p1y) ? 0 : qy - p1y) / slope + p1x;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c == c1) {
|
|
||||||
p1x = qx;
|
|
||||||
p1y = qy;
|
|
||||||
c1 = regionCode(p1x, p1y);
|
|
||||||
} else {
|
|
||||||
p2x = qx;
|
|
||||||
p2y = qy;
|
|
||||||
c2 = regionCode(p2x, p2y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line.setLine(p1x, p1y, p2x, p2y);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
// end of file
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@UtilityClass
|
|
||||||
public class FileUtils {
|
|
||||||
|
|
||||||
public File createTempFile(String filenamePrefix, String filenameSuffix) throws IOException {
|
|
||||||
|
|
||||||
File tempFile = Files.createTempFile(filenamePrefix, filenameSuffix).toFile();
|
|
||||||
setRWPermissionsOnlyForOwner(tempFile);
|
|
||||||
|
|
||||||
return tempFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a file; logs a message with the reason if the deletion fails.
|
|
||||||
* This method is null-safe.
|
|
||||||
*
|
|
||||||
* @param file The file to delete. Can be null.
|
|
||||||
*/
|
|
||||||
public void deleteFile(File file) {
|
|
||||||
|
|
||||||
if (file != null) {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(file.toPath());
|
|
||||||
} catch (IOException ex) {
|
|
||||||
log.warn("Could not delete file!", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// We don't need to check the results of the permission setters below,
|
|
||||||
// since we're manipulating a file we created ourselves.
|
|
||||||
@SuppressWarnings({"ResultOfMethodCallIgnored", "squid:S899"})
|
|
||||||
private void setRWPermissionsOnlyForOwner(File tempFile) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
tempFile.setReadable(true, true);
|
|
||||||
tempFile.setWritable(true, true);
|
|
||||||
tempFile.setExecutable(false);
|
|
||||||
} catch (SecurityException ex) {
|
|
||||||
// This should never happen since we're creating a temp file ourselves.
|
|
||||||
log.warn("Caught an exception during temp file creation. This should not happend. Check the code.", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils;
|
|
||||||
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.TextPageBlock;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
@SuppressWarnings("all")
|
|
||||||
public final class PositionUtils {
|
|
||||||
|
|
||||||
// TODO This currently uses pdf coord system. In the futher this should use java coord system.
|
|
||||||
// Note: DirAdj (TextDirection Adjusted) can not be user for this.
|
|
||||||
public boolean isWithinBodyTextFrame(Rectangle btf, TextPageBlock textBlock) {
|
|
||||||
|
|
||||||
if (btf == null || textBlock == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
double threshold = textBlock.getMostPopularWordHeight() * 3;
|
|
||||||
|
|
||||||
if (textBlock.getPdfMinX() + threshold > btf.getTopLeft().getX() && textBlock.getPdfMaxX() - threshold < btf.getTopLeft()
|
|
||||||
.getX() + btf.getWidth() && textBlock.getPdfMinY() + threshold > btf.getTopLeft().getY() && textBlock.getPdfMaxY() - threshold < btf.getTopLeft()
|
|
||||||
.getY() + btf.getHeight()) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO This currently uses pdf coord system. In the futher this should use java coord system.
|
|
||||||
// Note: DirAdj (TextDirection Adjusted) can not be user for this.
|
|
||||||
public boolean isOverBodyTextFrame(Rectangle btf, TextPageBlock textBlock, int rotation) {
|
|
||||||
|
|
||||||
if (btf == null || textBlock == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rotation == 90 && textBlock.getPdfMaxX() < btf.getTopLeft().getX()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rotation == 180 && textBlock.getPdfMaxY() < btf.getTopLeft().getY()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rotation == 270 && textBlock.getPdfMinX() > btf.getTopLeft().getX() + btf.getWidth()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rotation == 0 && textBlock.getPdfMinY() > btf.getTopLeft().getY() + btf.getHeight()) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO This currently uses pdf coord system. In the futher this should use java coord system.
|
|
||||||
// Note: DirAdj (TextDirection Adjusted) can not be user for this.
|
|
||||||
public boolean isUnderBodyTextFrame(Rectangle btf, TextPageBlock textBlock, int rotation) {
|
|
||||||
|
|
||||||
if (btf == null || textBlock == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rotation == 90 && textBlock.getPdfMinX() > btf.getTopLeft().getX() + btf.getWidth()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rotation == 180 && textBlock.getPdfMinY() > btf.getTopLeft().getY() + btf.getHeight()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rotation == 270 && textBlock.getPdfMaxX() < btf.getTopLeft().getX()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rotation == 0 && textBlock.getPdfMaxY() < btf.getTopLeft().getY()) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO This currently uses pdf coord system. In the futher this should use java coord system.
|
|
||||||
// Note: DirAdj (TextDirection Adjusted) can not be user for this.
|
|
||||||
public boolean isTouchingUnderBodyTextFrame(Rectangle btf, TextPageBlock textBlock) {
|
|
||||||
|
|
||||||
//TODO Currently this is not working for rotated pages.
|
|
||||||
|
|
||||||
if (btf == null || textBlock == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textBlock.getMinY() < btf.getTopLeft().getY()) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public float getHeightDifferenceBetweenChunkWordAndDocumentWord(TextPageBlock textBlock, Float documentMostPopularWordHeight) {
|
|
||||||
|
|
||||||
return textBlock.getMostPopularWordHeight() - documentMostPopularWordHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Float getApproxLineCount(TextPageBlock textBlock) {
|
|
||||||
|
|
||||||
return textBlock.getHeight() / textBlock.getMostPopularWordHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils;
|
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copied and minimal modified from PDFBox.
|
|
||||||
*/
|
|
||||||
@UtilityClass
|
|
||||||
public final class QuickSort {
|
|
||||||
|
|
||||||
private static final Comparator<? extends Comparable> OBJCOMP = new Comparator<Comparable>() {
|
|
||||||
@Override
|
|
||||||
public int compare(Comparable object1, Comparable object2) {
|
|
||||||
|
|
||||||
return object1.compareTo(object2);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts the given list using the given comparator.
|
|
||||||
*
|
|
||||||
* @param <T> type of the objects to be sorted.
|
|
||||||
* @param list list to be sorted
|
|
||||||
* @param cmp comparator used to compare the objects within the list
|
|
||||||
*/
|
|
||||||
public static <T> void sort(List<T> list, Comparator<? super T> cmp) {
|
|
||||||
|
|
||||||
int size = list.size();
|
|
||||||
if (size < 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
quicksort(list, cmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts the given list using compareTo as comparator.
|
|
||||||
*
|
|
||||||
* @param <T> type of the objects to be sorted.
|
|
||||||
* @param list list to be sorted
|
|
||||||
*/
|
|
||||||
public static <T extends Comparable> void sort(List<T> list) {
|
|
||||||
|
|
||||||
sort(list, (Comparator<T>) OBJCOMP);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static <T> void quicksort(List<T> list, Comparator<? super T> cmp) {
|
|
||||||
|
|
||||||
Deque<Integer> stack = new ArrayDeque<Integer>();
|
|
||||||
stack.push(0);
|
|
||||||
stack.push(list.size());
|
|
||||||
while (!stack.isEmpty()) {
|
|
||||||
int right = stack.pop();
|
|
||||||
int left = stack.pop();
|
|
||||||
if (right - left < 2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int p = left + ((right - left) / 2);
|
|
||||||
p = partition(list, cmp, p, left, right);
|
|
||||||
|
|
||||||
stack.push(p + 1);
|
|
||||||
stack.push(right);
|
|
||||||
|
|
||||||
stack.push(left);
|
|
||||||
stack.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static <T> int partition(List<T> list, Comparator<? super T> cmp, int p, int start, int end) {
|
|
||||||
|
|
||||||
int l = start;
|
|
||||||
int h = end - 2;
|
|
||||||
T piv = list.get(p);
|
|
||||||
swap(list, p, end - 1);
|
|
||||||
|
|
||||||
while (l < h) {
|
|
||||||
if (cmp.compare(list.get(l), piv) <= 0) {
|
|
||||||
l++;
|
|
||||||
} else if (cmp.compare(piv, list.get(h)) <= 0) {
|
|
||||||
h--;
|
|
||||||
} else {
|
|
||||||
swap(list, l, h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int idx = h;
|
|
||||||
if (cmp.compare(list.get(h), piv) < 0) {
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
swap(list, end - 1, idx);
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static <T> void swap(List<T> list, int i, int j) {
|
|
||||||
|
|
||||||
T tmp = list.get(i);
|
|
||||||
list.set(i, list.get(j));
|
|
||||||
list.set(j, tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils;
|
|
||||||
|
|
||||||
import java.awt.geom.Line2D;
|
|
||||||
import java.awt.geom.Point2D;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Ruling;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public final class RulingTextDirAdjustUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ruling (line of a table) the same way TextPositions are converted in PDFBox.
|
|
||||||
* This will get the y position of the text, adjusted so that 0,0 is upper left and it is adjusted based on the text direction.
|
|
||||||
* <p>
|
|
||||||
* See org.apache.pdfbox.text.TextPosition
|
|
||||||
*/
|
|
||||||
public Line2D.Float convertToDirAdj(Ruling ruling, float dir, float pageWidth, float pageHeight) {
|
|
||||||
|
|
||||||
return new Line2D.Float(convertPoint(ruling.x1, ruling.y1, dir, pageWidth, pageHeight), convertPoint(ruling.x2, ruling.y2, dir, pageWidth, pageHeight));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Point2D convertPoint(float x, float y, float dir, float pageWidth, float pageHeight) {
|
|
||||||
|
|
||||||
var xAdj = getXRot(x, y, dir, pageWidth, pageHeight);
|
|
||||||
var yLowerLeftRot = getYLowerLeftRot(x, y, dir, pageWidth, pageHeight);
|
|
||||||
var yAdj = dir == 0 || dir == 180 ? pageHeight - yLowerLeftRot : pageWidth - yLowerLeftRot;
|
|
||||||
return new Point2D.Float(xAdj, yAdj);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
|
||||||
private float getXRot(float x, float y, float dir, float pageWidth, float pageHeight) {
|
|
||||||
|
|
||||||
if (dir == 0) {
|
|
||||||
return x;
|
|
||||||
} else if (dir == 90) {
|
|
||||||
return y;
|
|
||||||
} else if (dir == 180) {
|
|
||||||
return pageWidth - x;
|
|
||||||
} else if (dir == 270) {
|
|
||||||
return pageHeight - y;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private float getYLowerLeftRot(float x, float y, float dir, float pageWidth, float pageHeight) {
|
|
||||||
|
|
||||||
if (dir == 0) {
|
|
||||||
return y;
|
|
||||||
} else if (dir == 90) {
|
|
||||||
return pageWidth - x;
|
|
||||||
} else if (dir == 180) {
|
|
||||||
return pageHeight - y;
|
|
||||||
} else if (dir == 270) {
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.layoutparsing.classification.utils;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@UtilityClass
|
|
||||||
@SuppressWarnings("all")
|
|
||||||
public final class Utils {
|
|
||||||
|
|
||||||
private final static float EPSILON = 0.1f;
|
|
||||||
|
|
||||||
|
|
||||||
public static boolean feq(double f1, double f2) {
|
|
||||||
|
|
||||||
return (Math.abs(f1 - f2) < EPSILON);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static float round(double d, int decimalPlace) {
|
|
||||||
|
|
||||||
BigDecimal bd = BigDecimal.valueOf(d);
|
|
||||||
bd = bd.setScale(decimalPlace, BigDecimal.ROUND_HALF_UP);
|
|
||||||
return bd.floatValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static <T> void sort(List<T> list, Comparator<? super T> comparator) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
QuickSort.sort(list, comparator);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// This should not happen since we use QuickSort from PDFBox
|
|
||||||
log.warn(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -14,7 +14,6 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
import com.iqser.gin4.commons.metrics.meters.FunctionTimerValues;
|
import com.iqser.gin4.commons.metrics.meters.FunctionTimerValues;
|
||||||
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntryModel;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;
|
||||||
@ -24,29 +23,15 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlo
|
|||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogChanges;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogChanges;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogLegalBasis;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogLegalBasis;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionGrid;
|
|
||||||
import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient;
|
import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient;
|
||||||
import com.iqser.red.service.redaction.v1.server.client.model.NerEntitiesModel;
|
import com.iqser.red.service.redaction.v1.server.client.model.NerEntitiesModel;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.DocumentData;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.mapper.DocumentGraphMapper;
|
import com.iqser.red.service.redaction.v1.server.document.data.mapper.DocumentGraphMapper;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.factory.DocumentGraphFactory;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Document;
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Document;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Section;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
|
||||||
import com.iqser.red.service.redaction.v1.server.exception.RedactionException;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.adapter.ImageServiceResponseAdapter;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.ClassificationDocument;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.image.ClassifiedImage;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.SimplifiedSectionText;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.text.SimplifiedText;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.service.PdfSegmentationService;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntities;
|
import com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntities;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntitiesAdapter;
|
import com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntitiesAdapter;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.Dictionary;
|
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.Dictionary;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryIncrement;
|
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryIncrement;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryModel;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryVersion;
|
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryVersion;
|
||||||
import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings;
|
import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings;
|
||||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||||
@ -71,63 +56,15 @@ public class AnalyzeService {
|
|||||||
EntityRedactionService entityRedactionService;
|
EntityRedactionService entityRedactionService;
|
||||||
RedactionLogCreatorService redactionLogCreatorService;
|
RedactionLogCreatorService redactionLogCreatorService;
|
||||||
RedactionStorageService redactionStorageService;
|
RedactionStorageService redactionStorageService;
|
||||||
PdfSegmentationService pdfSegmentationService;
|
|
||||||
RedactionChangeLogService redactionChangeLogService;
|
RedactionChangeLogService redactionChangeLogService;
|
||||||
LegalBasisClient legalBasisClient;
|
LegalBasisClient legalBasisClient;
|
||||||
RedactionServiceSettings redactionServiceSettings;
|
RedactionServiceSettings redactionServiceSettings;
|
||||||
SectionGridCreatorService sectionGridCreatorService;
|
|
||||||
ImageServiceResponseAdapter imageServiceResponseAdapter;
|
|
||||||
ImportedRedactionService importedRedactionService;
|
ImportedRedactionService importedRedactionService;
|
||||||
SectionFinderService sectionFinderService;
|
SectionFinderService sectionFinderService;
|
||||||
|
|
||||||
FunctionTimerValues redactmanagerAnalyzePagewiseValues;
|
FunctionTimerValues redactmanagerAnalyzePagewiseValues;
|
||||||
|
|
||||||
|
|
||||||
@Timed("redactmanager_analyzeDocumentStructure")
|
|
||||||
public AnalyzeResult analyzeDocumentStructure(StructureAnalyzeRequest analyzeRequest) {
|
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
ClassificationDocument classifiedDoc;
|
|
||||||
|
|
||||||
try {
|
|
||||||
var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(analyzeRequest.getDossierId(),
|
|
||||||
analyzeRequest.getFileId(),
|
|
||||||
FileType.ORIGIN));
|
|
||||||
log.info("Loaded PDF for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
|
||||||
Map<Integer, List<ClassifiedImage>> pdfImages = null;
|
|
||||||
if (redactionServiceSettings.isEnableImageClassification()) {
|
|
||||||
pdfImages = imageServiceResponseAdapter.convertImages(analyzeRequest.getDossierId(), analyzeRequest.getFileId());
|
|
||||||
log.info("Loaded image service response for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
|
||||||
}
|
|
||||||
|
|
||||||
classifiedDoc = pdfSegmentationService.parseDocument(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), storedObjectStream, pdfImages);
|
|
||||||
log.info("Parsed document for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RedactionException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Document document = DocumentGraphFactory.buildDocumentGraph(classifiedDoc);
|
|
||||||
log.info("Built Document Graph for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
|
||||||
|
|
||||||
SectionGrid sectionGrid = sectionGridCreatorService.createSectionGrid(document);
|
|
||||||
log.info("Built section grid for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
|
||||||
|
|
||||||
redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.TEXT, DocumentData.fromDocument(document));
|
|
||||||
redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.SIMPLIFIED_TEXT, toSimplifiedText(document));
|
|
||||||
redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.SECTION_GRID, sectionGrid);
|
|
||||||
log.info("Stored document graph, text, simplified text, and section grid for file {} in dossier {}", analyzeRequest.getFileId(), analyzeRequest.getDossierId());
|
|
||||||
|
|
||||||
return AnalyzeResult.builder()
|
|
||||||
.dossierId(analyzeRequest.getDossierId())
|
|
||||||
.fileId(analyzeRequest.getFileId())
|
|
||||||
.duration(System.currentTimeMillis() - startTime)
|
|
||||||
.numberOfPages(document.getNumberOfPages())
|
|
||||||
.analysisVersion(redactionServiceSettings.getAnalysisVersion())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Timed("redactmanager_analyze")
|
@Timed("redactmanager_analyze")
|
||||||
public AnalyzeResult analyze(AnalyzeRequest analyzeRequest) {
|
public AnalyzeResult analyze(AnalyzeRequest analyzeRequest) {
|
||||||
|
|
||||||
@ -367,18 +304,4 @@ public class AnalyzeService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private SimplifiedText toSimplifiedText(Document document) {
|
|
||||||
|
|
||||||
List<SimplifiedSectionText> simplifiedSectionTexts = document.getMainSections().stream().map(this::toSimplifiedSectionText).toList();
|
|
||||||
return SimplifiedText.builder().numberOfPages(document.getNumberOfPages()).sectionTexts(simplifiedSectionTexts).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private SimplifiedSectionText toSimplifiedSectionText(Section section) {
|
|
||||||
|
|
||||||
return SimplifiedSectionText.builder().sectionNumber(section.getTreeId().get(0)).text(section.getTextBlock().getSearchText()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,145 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.redaction.service;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.function.BinaryOperator;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.Collector;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.CellRectangle;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionGrid;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionRectangle;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Document;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Page;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.Table;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableCell;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class SectionGridCreatorService {
|
|
||||||
|
|
||||||
public SectionGrid createSectionGrid(Document document) {
|
|
||||||
|
|
||||||
Map<Integer, List<SectionRectangle>> sectionBBox = document.streamAllSubNodesOfType(NodeType.SECTION).map(SemanticNode::getBBox).collect(new SectionGridCollector());
|
|
||||||
Map<Integer, List<SectionRectangle>> paragraphBBox = document.streamAllSubNodesOfType(NodeType.PARAGRAPH).map(SemanticNode::getBBox).collect(new SectionGridCollector());
|
|
||||||
Map<Integer, List<SectionRectangle>> headlineBBox = document.streamAllSubNodesOfType(NodeType.HEADLINE).map(SemanticNode::getBBox).collect(new SectionGridCollector());
|
|
||||||
Map<Integer, List<SectionRectangle>> tableBBox = document.streamAllSubNodesOfType(NodeType.TABLE).map(node -> (Table) node).collect(new TableGridCollector());
|
|
||||||
var sectionGrid = new SectionGrid();
|
|
||||||
|
|
||||||
sectionGrid.setRectanglesPerPage(mergeMapsByConcatenatingLists(//
|
|
||||||
mergeMapsByConcatenatingLists(paragraphBBox, headlineBBox), //
|
|
||||||
mergeMapsByConcatenatingLists(sectionBBox, tableBBox)));
|
|
||||||
return sectionGrid;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static abstract class GridCollector<T> implements Collector<T, Map<Integer, List<SectionRectangle>>, Map<Integer, List<SectionRectangle>>> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Supplier<Map<Integer, List<SectionRectangle>>> supplier() {
|
|
||||||
|
|
||||||
return HashMap::new;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Function<Map<Integer, List<SectionRectangle>>, Map<Integer, List<SectionRectangle>>> finisher() {
|
|
||||||
|
|
||||||
return Function.identity();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BinaryOperator<Map<Integer, List<SectionRectangle>>> combiner() {
|
|
||||||
|
|
||||||
return SectionGridCreatorService::mergeMapsByConcatenatingLists;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<Characteristics> characteristics() {
|
|
||||||
|
|
||||||
return Set.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT, Characteristics.UNORDERED);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class TableGridCollector extends GridCollector<Table> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BiConsumer<Map<Integer, List<SectionRectangle>>, Table> accumulator() {
|
|
||||||
|
|
||||||
return (map, table) -> table.getPages()
|
|
||||||
.forEach(page -> map.merge(page.getNumber(), List.of(toSectionRectangle(table, page, table.getPages().size())), SectionGridCreatorService::concatLists));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static SectionRectangle toSectionRectangle(Table table, Page page, int numberOfParts) {
|
|
||||||
|
|
||||||
Rectangle2D rect = table.getBBox().get(page);
|
|
||||||
List<CellRectangle> tableCellRectangles = table.streamTableCells()
|
|
||||||
.map(TableCell::getBBox)
|
|
||||||
.map(map -> map.get(page))
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.map(rectangle2D -> new CellRectangle(new Point((float) rectangle2D.getX(), (float) rectangle2D.getY()),
|
|
||||||
(float) rectangle2D.getWidth(),
|
|
||||||
(float) rectangle2D.getHeight()))
|
|
||||||
.toList();
|
|
||||||
return new SectionRectangle(new Point((float) rect.getX(), (float) rect.getY()),
|
|
||||||
(float) rect.getWidth(),
|
|
||||||
(float) rect.getHeight(),
|
|
||||||
1,
|
|
||||||
numberOfParts,
|
|
||||||
tableCellRectangles);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SectionGridCollector extends GridCollector<Map<Page, Rectangle2D>> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BiConsumer<Map<Integer, List<SectionRectangle>>, Map<Page, Rectangle2D>> accumulator() {
|
|
||||||
|
|
||||||
return (mapToKeep, mapToMerge) -> mapToMerge.forEach((page, rectangle) -> mapToKeep.merge(page.getNumber(),
|
|
||||||
List.of(toSectionRectangle(rectangle, mapToMerge.values().size())),
|
|
||||||
SectionGridCreatorService::concatLists));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static SectionRectangle toSectionRectangle(Rectangle2D rect, int numberOfParts) {
|
|
||||||
|
|
||||||
return new SectionRectangle(new Point((float) rect.getX(), (float) rect.getY()), (float) rect.getWidth(), (float) rect.getHeight(), 1, numberOfParts, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static Map<Integer, List<SectionRectangle>> mergeMapsByConcatenatingLists(Map<Integer, List<SectionRectangle>> mapToKeep,
|
|
||||||
Map<Integer, List<SectionRectangle>> mapToMerge) {
|
|
||||||
|
|
||||||
mapToMerge.forEach((page, rectangle) -> mapToKeep.merge(page, rectangle, SectionGridCreatorService::concatLists));
|
|
||||||
return mapToKeep;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static List<SectionRectangle> concatLists(List<SectionRectangle> l1, List<SectionRectangle> l2) {
|
|
||||||
|
|
||||||
return Stream.concat(l1.stream(), l2.stream()).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -13,6 +13,10 @@ import com.iqser.red.service.redaction.v1.server.exception.NotFoundException;
|
|||||||
import com.iqser.red.service.redaction.v1.server.document.data.DocumentData;
|
import com.iqser.red.service.redaction.v1.server.document.data.DocumentData;
|
||||||
import com.iqser.red.storage.commons.exception.StorageObjectDoesNotExist;
|
import com.iqser.red.storage.commons.exception.StorageObjectDoesNotExist;
|
||||||
import com.iqser.red.storage.commons.service.StorageService;
|
import com.iqser.red.storage.commons.service.StorageService;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPage;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentPositionData;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentStructure;
|
||||||
|
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentTextData;
|
||||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
@ -82,7 +86,12 @@ public class RedactionStorageService {
|
|||||||
public DocumentData getDocumentData(String dossierId, String fileId) {
|
public DocumentData getDocumentData(String dossierId, String fileId) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.TEXT), DocumentData.class);
|
return DocumentData.builder()
|
||||||
|
.documentStructure(storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_STRUCTURE), DocumentStructure.class))
|
||||||
|
.documentTextData(storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_TEXT), DocumentTextData[].class))
|
||||||
|
.documentPositionData(storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_POSITION), DocumentPositionData[].class))
|
||||||
|
.documentPages(storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_PAGES), DocumentPage[].class))
|
||||||
|
.build();
|
||||||
} catch (StorageObjectDoesNotExist e) {
|
} catch (StorageObjectDoesNotExist e) {
|
||||||
log.debug("DocumentData not available.");
|
log.debug("DocumentData not available.");
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import java.util.HashMap;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntry;
|
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntry;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.configuration.Colors;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.configuration.Colors;
|
||||||
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
||||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotationService;
|
import com.iqser.red.service.redaction.v1.server.annotate.AnnotationService;
|
||||||
@ -41,6 +43,10 @@ import com.iqser.red.service.redaction.v1.server.redaction.utils.ResourceLoader;
|
|||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
|
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
|
||||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||||
import com.iqser.red.storage.commons.service.StorageService;
|
import com.iqser.red.storage.commons.service.StorageService;
|
||||||
|
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.tenantcommons.TenantContext;
|
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||||
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
||||||
|
|
||||||
@ -112,6 +118,9 @@ public abstract class AbstractRedactionIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
protected ManualRedactionSurroundingTextService manualRedactionSurroundingTextService;
|
protected ManualRedactionSurroundingTextService manualRedactionSurroundingTextService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LayoutParsingPipeline layoutParsingPipeline;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
protected AmazonS3 amazonS3;
|
protected AmazonS3 amazonS3;
|
||||||
|
|
||||||
@ -437,6 +446,32 @@ public abstract class AbstractRedactionIntegrationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
protected LayoutParsingFinishedEvent analyzeDocumentStructure(LayoutParsingType layoutParsingType, AnalyzeRequest request) {
|
||||||
|
|
||||||
|
var originId = RedactionStorageService.StorageIdUtils.getStorageId(request.getDossierId(), request.getFileId(), FileType.ORIGIN);
|
||||||
|
var tableId = RedactionStorageService.StorageIdUtils.getStorageId(request.getDossierId(), request.getFileId(), FileType.TABLES);
|
||||||
|
var imageId = RedactionStorageService.StorageIdUtils.getStorageId(request.getDossierId(), request.getFileId(), FileType.IMAGE_INFO);
|
||||||
|
var sectionGridId = RedactionStorageService.StorageIdUtils.getStorageId(request.getDossierId(), request.getFileId(), FileType.SECTION_GRID);
|
||||||
|
var structureId = RedactionStorageService.StorageIdUtils.getStorageId(request.getDossierId(), request.getFileId(), FileType.DOCUMENT_STRUCTURE);
|
||||||
|
var textId = RedactionStorageService.StorageIdUtils.getStorageId(request.getDossierId(), request.getFileId(), FileType.DOCUMENT_TEXT);
|
||||||
|
var positionID = RedactionStorageService.StorageIdUtils.getStorageId(request.getDossierId(), request.getFileId(), FileType.DOCUMENT_POSITION);
|
||||||
|
var pagesID = RedactionStorageService.StorageIdUtils.getStorageId(request.getDossierId(), request.getFileId(), FileType.DOCUMENT_PAGES);
|
||||||
|
var layoutParsingRequest = LayoutParsingRequest.builder()
|
||||||
|
.layoutParsingType(layoutParsingType)
|
||||||
|
.originFileStorageId(originId)
|
||||||
|
.tablesFileStorageId(Optional.of(tableId))
|
||||||
|
.imagesFileStorageId(Optional.of(imageId))
|
||||||
|
.structureFileStorageId(structureId)
|
||||||
|
.textBlockFileStorageId(textId)
|
||||||
|
.positionBlockFileStorageId(positionID)
|
||||||
|
.pageFileStorageId(pagesID)
|
||||||
|
.sectionGridStorageId(sectionGridId)
|
||||||
|
.build();
|
||||||
|
return layoutParsingPipeline.parseLayoutAndSaveFilesToStorage(layoutParsingRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
protected AnalyzeRequest prepareStorage(String file, String cvServiceResponseFile) {
|
protected AnalyzeRequest prepareStorage(String file, String cvServiceResponseFile) {
|
||||||
|
|
||||||
|
|||||||
@ -25,12 +25,13 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequ
|
|||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSONPrimitive;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSONPrimitive;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
||||||
import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest;
|
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest;
|
||||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils;
|
import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils;
|
||||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||||
import com.iqser.red.storage.commons.service.StorageService;
|
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.LayoutParsingServiceProcessorConfiguration;
|
||||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||||
|
|
||||||
@ExtendWith(SpringExtension.class)
|
@ExtendWith(SpringExtension.class)
|
||||||
@ -51,7 +52,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
|||||||
// "files/Documine/Flora/ProblemDocs/SOLICITA_VICTRATO-GOLD-II_Item 21_Mutacao_Genica (1).TABLES.json");
|
// "files/Documine/Flora/ProblemDocs/SOLICITA_VICTRATO-GOLD-II_Item 21_Mutacao_Genica (1).TABLES.json");
|
||||||
|
|
||||||
System.out.println("Start Full integration test");
|
System.out.println("Start Full integration test");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.DOCUMINE, request);
|
||||||
System.out.println("Finished structure analysis");
|
System.out.println("Finished structure analysis");
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
System.out.println("Finished analysis");
|
System.out.println("Finished analysis");
|
||||||
@ -77,7 +78,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
|||||||
"files/Documine/Flora/A8591B/2009773e2e05919bb9e46aeedcc8b924.ORIGIN.TABLES.json");
|
"files/Documine/Flora/A8591B/2009773e2e05919bb9e46aeedcc8b924.ORIGIN.TABLES.json");
|
||||||
|
|
||||||
System.out.println("Start Full integration test");
|
System.out.println("Start Full integration test");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.DOCUMINE, request);
|
||||||
System.out.println("Finished structure analysis");
|
System.out.println("Finished structure analysis");
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
System.out.println("Finished analysis");
|
System.out.println("Finished analysis");
|
||||||
@ -104,7 +105,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
|||||||
AnalyzeRequest request = uploadFileToStorage("files/Documine/Flora/ProblemDocs/SOLICITA_VICTRATO-GOLD-II_Item 15_Toxicidade Oral Aguda.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/Documine/Flora/ProblemDocs/SOLICITA_VICTRATO-GOLD-II_Item 15_Toxicidade Oral Aguda.pdf");
|
||||||
|
|
||||||
System.out.println("Start Full integration test");
|
System.out.println("Start Full integration test");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.DOCUMINE, request);
|
||||||
System.out.println("Finished structure analysis");
|
System.out.println("Finished structure analysis");
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
System.out.println("Finished analysis");
|
System.out.println("Finished analysis");
|
||||||
@ -123,6 +124,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
||||||
|
@Import(LayoutParsingServiceProcessorConfiguration.class)
|
||||||
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
||||||
static class RedactionIntegrationTestConfiguration {
|
static class RedactionIntegrationTestConfiguration {
|
||||||
|
|
||||||
|
|||||||
@ -1,217 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.assertj.core.api.Assertions;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.context.annotation.FilterType;
|
|
||||||
import org.springframework.context.annotation.Import;
|
|
||||||
import org.springframework.context.annotation.Primary;
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.ChangeType;
|
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
|
||||||
import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.mapper.DocumentGraphMapper;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.service.AnalyzeService;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
|
||||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
|
||||||
import com.iqser.red.storage.commons.service.StorageService;
|
|
||||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
|
||||||
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
@ExtendWith(SpringExtension.class)
|
|
||||||
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
||||||
@Import(HeadlinesGoldStandardIntegrationTest.RedactionIntegrationTestConfiguration.class)
|
|
||||||
public class HeadlinesGoldStandardIntegrationTest {
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private TenantsClient tenantsClient;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AnalyzeService analyzeService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RedactionStorageService redactionStorageService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private StorageService storageService;
|
|
||||||
|
|
||||||
private final static String TEST_DOSSIER_TEMPLATE_ID = "123";
|
|
||||||
private final static String TEST_DOSSIER_ID = "123";
|
|
||||||
private final static String TEST_FILE_ID = "123";
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHeadlineDetection() {
|
|
||||||
|
|
||||||
List<Metrics> metrics = new ArrayList<>();
|
|
||||||
metrics.add(getMetrics("files/RSS/01 - CGA100251 - Acute Oral Toxicity (Up and Down Procedure) - Rat (1).pdf",
|
|
||||||
"files/Headlines/01 - CGA100251 - Acute Oral Toxicity (Up and Down Procedure) - Rat (1)_REDACTION_LOG.json"));
|
|
||||||
metrics.add(getMetrics("files/Trinexapac/91 Trinexapac-ethyl_RAR_01_Volume_1_2018-02-23.pdf",
|
|
||||||
"files/Headlines/91 Trinexapac-ethyl_RAR_01_Volume_1_2018-02-23_REDACTION_LOG.json"));
|
|
||||||
metrics.add(getMetrics("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06.pdf", "files/Headlines/S-Metolachlor_RAR_01_Volume_1_2018-09-06_REDACTION_LOG.json"));
|
|
||||||
|
|
||||||
double precision = metrics.stream().mapToDouble(Metrics::getPrecision).average().orElse(1.0);
|
|
||||||
double recall = metrics.stream().mapToDouble(Metrics::getRecall).average().orElse(1.0);
|
|
||||||
|
|
||||||
System.out.println("Precision is: " + precision + " recall is: " + recall);
|
|
||||||
|
|
||||||
Assertions.assertThat(precision).isGreaterThanOrEqualTo(0.44f);
|
|
||||||
Assertions.assertThat(recall).isGreaterThanOrEqualTo(0.69f);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
private Metrics getMetrics(String fileUrl, String redactionLogUrl) {
|
|
||||||
|
|
||||||
ClassPathResource redactionLogResource = new ClassPathResource(redactionLogUrl);
|
|
||||||
|
|
||||||
Set<Headline> goldStandardHeadlines = new HashSet<>();
|
|
||||||
var goldStandardLog = objectMapper.readValue(redactionLogResource.getInputStream(), RedactionLog.class);
|
|
||||||
goldStandardLog.getRedactionLogEntry().removeIf(r -> !r.isRedacted() || r.getChanges().get(r.getChanges().size() - 1).getType().equals(ChangeType.REMOVED));
|
|
||||||
goldStandardLog.getRedactionLogEntry().forEach(e -> goldStandardHeadlines.add(new Headline(e.getPositions().get(0).getPage(), e.getValue())));
|
|
||||||
|
|
||||||
AnalyzeRequest request = prepareStorage(fileUrl);
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
|
||||||
|
|
||||||
var documentGraph = DocumentGraphMapper.toDocumentGraph(redactionStorageService.getDocumentData(TEST_DOSSIER_ID, TEST_FILE_ID));
|
|
||||||
var foundHeadlines = documentGraph.streamAllSubNodes()
|
|
||||||
.map(SemanticNode::getHeadline)
|
|
||||||
.distinct()
|
|
||||||
.map(headlineNode -> new Headline(headlineNode.getPages().stream().findFirst().get().getNumber(), headlineNode.getTextBlock().getSearchText().stripTrailing()))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
Set<Headline> correct = new HashSet<>();
|
|
||||||
Set<Headline> missing;
|
|
||||||
Set<Headline> falsePositive = new HashSet<>();
|
|
||||||
for (Headline headline : foundHeadlines) {
|
|
||||||
if (goldStandardHeadlines.contains(headline)) {
|
|
||||||
correct.add(headline);
|
|
||||||
} else {
|
|
||||||
falsePositive.add(headline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
missing = goldStandardHeadlines.stream().filter(h -> !correct.contains(h)).collect(Collectors.toSet());
|
|
||||||
|
|
||||||
float precision = (float) correct.size() / (float) foundHeadlines.size();
|
|
||||||
float recall = (float) correct.size() / ((float) correct.size() + (float) missing.size());
|
|
||||||
|
|
||||||
return new Metrics(precision, recall);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class/*, StorageAutoConfiguration.class*/})
|
|
||||||
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
|
||||||
public static class RedactionIntegrationTestConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@Primary
|
|
||||||
public StorageService inmemoryStorage() {
|
|
||||||
|
|
||||||
return new FileSystemBackedStorageService();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
public void cleanupStorage() {
|
|
||||||
|
|
||||||
if (this.storageService instanceof FileSystemBackedStorageService) {
|
|
||||||
((FileSystemBackedStorageService) this.storageService).clearStorage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
private AnalyzeRequest prepareStorage(String file) {
|
|
||||||
|
|
||||||
return prepareStorage(file, "files/cv_service_empty_response.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
private AnalyzeRequest prepareStorage(String file, String cvServiceResponseFile) {
|
|
||||||
|
|
||||||
ClassPathResource pdfFileResource = new ClassPathResource(file);
|
|
||||||
ClassPathResource cvServiceResponseFileResource = new ClassPathResource(cvServiceResponseFile);
|
|
||||||
|
|
||||||
return prepareStorage(pdfFileResource.getInputStream(), cvServiceResponseFileResource.getInputStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
private AnalyzeRequest prepareStorage(InputStream fileStream, InputStream cvServiceResponseFileStream) {
|
|
||||||
|
|
||||||
AnalyzeRequest request = AnalyzeRequest.builder()
|
|
||||||
.dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID)
|
|
||||||
.dossierId(TEST_DOSSIER_ID)
|
|
||||||
.fileId(TEST_FILE_ID)
|
|
||||||
.lastProcessed(OffsetDateTime.now())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
storageService.storeObject(TenantContext.getTenantId(),
|
|
||||||
RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.TABLES),
|
|
||||||
cvServiceResponseFileStream);
|
|
||||||
storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.ORIGIN), fileStream);
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode
|
|
||||||
@AllArgsConstructor
|
|
||||||
@ToString
|
|
||||||
private class Metrics {
|
|
||||||
|
|
||||||
private float precision;
|
|
||||||
private float recall;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode
|
|
||||||
@AllArgsConstructor
|
|
||||||
@ToString
|
|
||||||
private class Headline {
|
|
||||||
|
|
||||||
private int page;
|
|
||||||
private String headline;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server;
|
package com.iqser.red.service.redaction.v1.server;
|
||||||
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.wildfly.common.Assert.assertFalse;
|
|
||||||
import static org.wildfly.common.Assert.assertTrue;
|
import static org.wildfly.common.Assert.assertTrue;
|
||||||
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
@ -34,12 +33,13 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSON
|
|||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||||
import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest;
|
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest;
|
||||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils;
|
import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils;
|
||||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||||
import com.iqser.red.storage.commons.service.StorageService;
|
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.LayoutParsingServiceProcessorConfiguration;
|
||||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||||
|
|
||||||
@ExtendWith(SpringExtension.class)
|
@ExtendWith(SpringExtension.class)
|
||||||
@ -51,6 +51,7 @@ public class RedactionAcceptanceTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
||||||
|
@Import(LayoutParsingServiceProcessorConfiguration.class)
|
||||||
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
||||||
public static class RedactionIntegrationTestConfiguration {
|
public static class RedactionIntegrationTestConfiguration {
|
||||||
|
|
||||||
@ -101,7 +102,7 @@ public class RedactionAcceptanceTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1_moreSections.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1_moreSections.pdf");
|
||||||
System.out.println("Start Full integration test");
|
System.out.println("Start Full integration test");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
System.out.println("Finished structure analysis");
|
System.out.println("Finished structure analysis");
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
System.out.println("Finished analysis");
|
System.out.println("Finished analysis");
|
||||||
@ -158,7 +159,7 @@ public class RedactionAcceptanceTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1_moreSections.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1_moreSections.pdf");
|
||||||
System.out.println("Start Full integration test");
|
System.out.println("Start Full integration test");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
System.out.println("Finished structure analysis");
|
System.out.println("Finished structure analysis");
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
System.out.println("Finished analysis");
|
System.out.println("Finished analysis");
|
||||||
|
|||||||
@ -66,7 +66,6 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlo
|
|||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||||
import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest;
|
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest;
|
||||||
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.data.mapper.DocumentGraphMapper;
|
import com.iqser.red.service.redaction.v1.server.document.data.mapper.DocumentGraphMapper;
|
||||||
@ -81,6 +80,8 @@ import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils;
|
|||||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||||
import com.iqser.red.storage.commons.service.StorageService;
|
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.LayoutParsingServiceProcessorConfiguration;
|
||||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||||
|
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
@ -94,8 +95,10 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private EntityEnrichmentService entityEnrichmentService;
|
private EntityEnrichmentService entityEnrichmentService;
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
||||||
|
@Import({LayoutParsingServiceProcessorConfiguration.class})
|
||||||
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
||||||
public static class RedactionIntegrationTestConfiguration {
|
public static class RedactionIntegrationTestConfiguration {
|
||||||
|
|
||||||
@ -145,7 +148,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
public void test270Rotated() {
|
public void test270Rotated() {
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/270Rotated.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/270Rotated.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
}
|
}
|
||||||
@ -156,7 +159,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
public void testLargeScannedFileOOM() {
|
public void testLargeScannedFileOOM() {
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("scanned/VV-377031.pdf");
|
AnalyzeRequest request = uploadFileToStorage("scanned/VV-377031.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
}
|
}
|
||||||
@ -168,7 +171,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/merge_images.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/merge_images.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
Map<String, List<RedactionLogEntry>> duplicates = new HashMap<>();
|
Map<String, List<RedactionLogEntry>> duplicates = new HashMap<>();
|
||||||
@ -209,7 +212,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
// Lastname M., Doe J., Mustermann M.
|
// Lastname M., Doe J., Mustermann M.
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/ExpansionTest.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/ExpansionTest.pdf");
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -225,7 +228,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1_moreSections.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/new/SYNGENTA_EFSA_sanitisation_GFL_v1_moreSections.pdf");
|
||||||
System.out.println("Start Full integration test");
|
System.out.println("Start Full integration test");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
System.out.println("Finished structure analysis");
|
System.out.println("Finished structure analysis");
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
System.out.println("Finished analysis");
|
System.out.println("Finished analysis");
|
||||||
@ -247,7 +250,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/RSS/01 - CGA100251 - Acute Oral Toxicity (Up and Down Procedure) - Rat (1).pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/RSS/01 - CGA100251 - Acute Oral Toxicity (Up and Down Procedure) - Rat (1).pdf");
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
assertThat(result.getAddedFileAttributes().size()).isEqualTo(1);
|
assertThat(result.getAddedFileAttributes().size()).isEqualTo(1);
|
||||||
@ -264,7 +267,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/new/test-ignore-hint.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/new/test-ignore-hint.pdf");
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
analyzeService.analyze(request);
|
analyzeService.analyze(request);
|
||||||
|
|
||||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -298,8 +301,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
System.out.println("noExceptionShouldBeThrownForAnyFiles");
|
System.out.println("noExceptionShouldBeThrownForAnyFiles");
|
||||||
ClassLoader loader = getClass().getClassLoader();
|
URL url = Thread.currentThread().getContextClassLoader().getResource("files");
|
||||||
URL url = loader.getResource("files");
|
|
||||||
Path path = Paths.get(URI.create(url.toString()));
|
Path path = Paths.get(URI.create(url.toString()));
|
||||||
when(dictionaryClient.getDictionaryForType(anyString(), anyLong())).thenReturn(new Type());
|
when(dictionaryClient.getDictionaryForType(anyString(), anyLong())).thenReturn(new Type());
|
||||||
Files.walk(path)//
|
Files.walk(path)//
|
||||||
@ -308,7 +310,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
.forEach(currentPath -> {
|
.forEach(currentPath -> {
|
||||||
AnalyzeRequest request = uploadFileToStorage(currentPath.toString());
|
AnalyzeRequest request = uploadFileToStorage(currentPath.toString());
|
||||||
System.out.println("Redacting file : " + currentPath.getFileName());
|
System.out.println("Redacting file : " + currentPath.getFileName());
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
|
|
||||||
long fstart = System.currentTimeMillis();
|
long fstart = System.currentTimeMillis();
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
@ -360,7 +362,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
.value("true")
|
.value("true")
|
||||||
.build()));
|
.build()));
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -414,13 +416,12 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
ManualRedactions manualRedactions = new ManualRedactions();
|
ManualRedactions manualRedactions = new ManualRedactions();
|
||||||
|
|
||||||
manualRedactions.setEntriesToAdd(Set.of(ManualRedactionEntry.builder()
|
manualRedactions.setEntriesToAdd(Set.of(ManualRedactionEntry.builder()
|
||||||
.value("Redact")
|
.value("Redact")
|
||||||
.addToDictionary(true)
|
.addToDictionary(true)
|
||||||
.addToDossierDictionary(true)
|
.addToDossierDictionary(true)
|
||||||
.positions(List.of(new Rectangle(new Point(95.96979999999999f, 515.7984f), 19.866899999999987f, 46.953f, 2)
|
.positions(List.of(new Rectangle(new Point(95.96979999999999f, 515.7984f), 19.866899999999987f, 46.953f, 2)))
|
||||||
)).type("dossier_redaction").build()));
|
.type("dossier_redaction")
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
|
||||||
request.setManualRedactions(manualRedactions);
|
request.setManualRedactions(manualRedactions);
|
||||||
|
|
||||||
@ -460,7 +461,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage(fileName);
|
AnalyzeRequest request = uploadFileToStorage(fileName);
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
analyzeService.analyze(request);
|
analyzeService.analyze(request);
|
||||||
|
|
||||||
dictionary.get(DICTIONARY_AUTHOR).add("report");
|
dictionary.get(DICTIONARY_AUTHOR).add("report");
|
||||||
@ -516,7 +517,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
.value("true")
|
.value("true")
|
||||||
.build()));
|
.build()));
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -614,7 +615,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
List<Type> types = objectMapper.readValue(typeResource.getInputStream(), typeRefForTypes);
|
List<Type> types = objectMapper.readValue(typeResource.getInputStream(), typeRefForTypes);
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/new/PublishedInformationTest.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/new/PublishedInformationTest.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
ManualRedactions manualRedactions = new ManualRedactions();
|
ManualRedactions manualRedactions = new ManualRedactions();
|
||||||
manualRedactions.getIdsToRemove()
|
manualRedactions.getIdsToRemove()
|
||||||
.add(IdRemoval.builder()
|
.add(IdRemoval.builder()
|
||||||
@ -673,7 +674,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
@ -694,7 +695,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
String filePath = "files/new/crafted document.pdf";
|
String filePath = "files/new/crafted document.pdf";
|
||||||
AnalyzeRequest request = uploadFileToStorage(filePath);
|
AnalyzeRequest request = uploadFileToStorage(filePath);
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
String testEntityValue1 = "Desiree";
|
String testEntityValue1 = "Desiree";
|
||||||
@ -766,7 +767,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
String tableServiceResponseFile = "files/cv_table_response_VV-511309.json";
|
String tableServiceResponseFile = "files/cv_table_response_VV-511309.json";
|
||||||
|
|
||||||
AnalyzeRequest request = prepareStorage(fileName, tableServiceResponseFile);
|
AnalyzeRequest request = prepareStorage(fileName, tableServiceResponseFile);
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
@ -790,7 +791,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
String tableServiceResponseFile = "files/cv_table_response_VV-511309.json";
|
String tableServiceResponseFile = "files/cv_table_response_VV-511309.json";
|
||||||
|
|
||||||
AnalyzeRequest request = prepareStorage(fileName, tableServiceResponseFile);
|
AnalyzeRequest request = prepareStorage(fileName, tableServiceResponseFile);
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
@ -811,7 +812,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/new/unicodeProblem.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/new/unicodeProblem.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
@ -833,7 +834,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/new/RotateTestFile.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/new/RotateTestFile.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
@ -859,7 +860,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/new/RotateTestFileSimple.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/new/RotateTestFileSimple.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
@ -881,7 +882,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/NoHeaderTable.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/NoHeaderTable.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
@ -934,7 +935,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
request.setManualRedactions(manualRedactions);
|
request.setManualRedactions(manualRedactions);
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -993,7 +994,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage(pdfFile);
|
AnalyzeRequest request = uploadFileToStorage(pdfFile);
|
||||||
request.setManualRedactions(manualRedactions);
|
request.setManualRedactions(manualRedactions);
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
manualRedactions.getEntriesToAdd().add(manualRedactionEntry);
|
manualRedactions.getEntriesToAdd().add(manualRedactionEntry);
|
||||||
@ -1026,7 +1027,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/Phantom Cells.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/Phantom Cells.pdf");
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -1046,7 +1047,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/sponsor_companies.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/sponsor_companies.pdf");
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
@ -1108,7 +1109,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage(pdfFile);
|
AnalyzeRequest request = uploadFileToStorage(pdfFile);
|
||||||
request.setManualRedactions(manualRedactions);
|
request.setManualRedactions(manualRedactions);
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
manualRedactions.getEntriesToAdd().add(manualRedactionEntry);
|
manualRedactions.getEntriesToAdd().add(manualRedactionEntry);
|
||||||
@ -1163,7 +1164,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf");
|
AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf");
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
@ -1219,7 +1220,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage(pdfFile);
|
AnalyzeRequest request = uploadFileToStorage(pdfFile);
|
||||||
request.setManualRedactions(manualRedactions);
|
request.setManualRedactions(manualRedactions);
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder()
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder()
|
||||||
@ -1252,7 +1253,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMPORTED_REDACTIONS),
|
RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMPORTED_REDACTIONS),
|
||||||
importedRedactions.getInputStream());
|
importedRedactions.getInputStream());
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
AnalyzeResult result = analyzeService.analyze(request);
|
AnalyzeResult result = analyzeService.analyze(request);
|
||||||
|
|
||||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -1292,7 +1293,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest {
|
|||||||
|
|
||||||
AnalyzeRequest request = uploadFileToStorage(fileName);
|
AnalyzeRequest request = uploadFileToStorage(fileName);
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
analyzeService.analyze(request);
|
analyzeService.analyze(request);
|
||||||
|
|
||||||
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build());
|
||||||
|
|||||||
@ -24,9 +24,10 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSON
|
|||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||||
import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest;
|
|
||||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||||
import com.iqser.red.storage.commons.service.StorageService;
|
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.LayoutParsingServiceProcessorConfiguration;
|
||||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||||
|
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
@ -40,6 +41,7 @@ public class RedactionIntegrationV2Test extends AbstractRedactionIntegrationTest
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
||||||
|
@Import(LayoutParsingServiceProcessorConfiguration.class)
|
||||||
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
||||||
static class RedactionIntegrationTestConfiguration {
|
static class RedactionIntegrationTestConfiguration {
|
||||||
|
|
||||||
@ -112,7 +114,7 @@ public class RedactionIntegrationV2Test extends AbstractRedactionIntegrationTest
|
|||||||
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(3L);
|
when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(3L);
|
||||||
mockDictionaryCalls(0L);
|
mockDictionaryCalls(0L);
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
analyzeService.analyze(request);
|
analyzeService.analyze(request);
|
||||||
|
|
||||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -150,7 +152,7 @@ public class RedactionIntegrationV2Test extends AbstractRedactionIntegrationTest
|
|||||||
dictionary.put(DICTIONARY_AUTHOR, List.of(entryAuthorDictionary));
|
dictionary.put(DICTIONARY_AUTHOR, List.of(entryAuthorDictionary));
|
||||||
falsePositive.put(DICTIONARY_PII, List.of("Dr. Alan Miller COMPLETION DATE:"));
|
falsePositive.put(DICTIONARY_PII, List.of("Dr. Alan Miller COMPLETION DATE:"));
|
||||||
|
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request);
|
||||||
analyzeService.analyze(request);
|
analyzeService.analyze(request);
|
||||||
|
|
||||||
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import java.util.HashMap;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -72,18 +73,22 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlo
|
|||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogComment;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogComment;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
|
||||||
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogLegalBasis;
|
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogLegalBasis;
|
||||||
import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.client.DictionaryClient;
|
import com.iqser.red.service.redaction.v1.server.client.DictionaryClient;
|
||||||
import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient;
|
import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient;
|
||||||
import com.iqser.red.service.redaction.v1.server.client.RulesClient;
|
import com.iqser.red.service.redaction.v1.server.client.RulesClient;
|
||||||
import com.iqser.red.service.redaction.v1.server.controller.RedactionController;
|
import com.iqser.red.service.redaction.v1.server.controller.RedactionController;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.service.ManualRedactionSurroundingTextService;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.service.AnalyzeService;
|
import com.iqser.red.service.redaction.v1.server.redaction.service.AnalyzeService;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.service.ManualRedactionSurroundingTextService;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.ResourceLoader;
|
import com.iqser.red.service.redaction.v1.server.redaction.utils.ResourceLoader;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
|
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
|
||||||
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
|
||||||
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
import com.iqser.red.storage.commons.StorageAutoConfiguration;
|
||||||
import com.iqser.red.storage.commons.service.StorageService;
|
import com.iqser.red.storage.commons.service.StorageService;
|
||||||
|
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.LayoutParsingServiceProcessorConfiguration;
|
||||||
import com.knecon.fforesight.tenantcommons.TenantContext;
|
import com.knecon.fforesight.tenantcommons.TenantContext;
|
||||||
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
import com.knecon.fforesight.tenantcommons.TenantsClient;
|
||||||
|
|
||||||
@ -237,6 +242,8 @@ public class RulesTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RedactionStorageService redactionStorageService;
|
private RedactionStorageService redactionStorageService;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private LayoutParsingPipeline layoutParsingPipeline;
|
||||||
|
@Autowired
|
||||||
private StorageService storageService;
|
private StorageService storageService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ManualRedactionSurroundingTextService manualRedactionSurroundingTextService;
|
private ManualRedactionSurroundingTextService manualRedactionSurroundingTextService;
|
||||||
@ -399,7 +406,7 @@ public class RulesTest {
|
|||||||
loadNerForTest();
|
loadNerForTest();
|
||||||
|
|
||||||
AnalyzeRequest request = prepareStorage(fileName);
|
AnalyzeRequest request = prepareStorage(fileName);
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
|
||||||
analyzeService.analyze(request);
|
analyzeService.analyze(request);
|
||||||
|
|
||||||
RedactionLog redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
RedactionLog redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -422,7 +429,7 @@ public class RulesTest {
|
|||||||
loadNerForTest();
|
loadNerForTest();
|
||||||
|
|
||||||
AnalyzeRequest request = prepareStorage(fileName);
|
AnalyzeRequest request = prepareStorage(fileName);
|
||||||
analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId()));
|
analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER);
|
||||||
analyzeService.analyze(request);
|
analyzeService.analyze(request);
|
||||||
|
|
||||||
RedactionLog redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
RedactionLog redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID);
|
||||||
@ -814,9 +821,35 @@ public class RulesTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
protected LayoutParsingFinishedEvent analyzeDocumentStructure(LayoutParsingType layoutParsingType) {
|
||||||
|
|
||||||
|
var originId = RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.ORIGIN);
|
||||||
|
var tableId = RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.TABLES);
|
||||||
|
var imageId = RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMAGE_INFO);
|
||||||
|
var sectionGridId = RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.SECTION_GRID);
|
||||||
|
var structureId = RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.DOCUMENT_STRUCTURE);
|
||||||
|
var textId = RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.DOCUMENT_TEXT);
|
||||||
|
var positionID = RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.DOCUMENT_POSITION);
|
||||||
|
var pagesID = RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.DOCUMENT_PAGES);
|
||||||
|
var layoutParsingRequest = LayoutParsingRequest.builder()
|
||||||
|
.layoutParsingType(layoutParsingType)
|
||||||
|
.originFileStorageId(originId)
|
||||||
|
.tablesFileStorageId(Optional.of(tableId))
|
||||||
|
.imagesFileStorageId(Optional.of(imageId))
|
||||||
|
.structureFileStorageId(structureId)
|
||||||
|
.textBlockFileStorageId(textId)
|
||||||
|
.positionBlockFileStorageId(positionID)
|
||||||
|
.pageFileStorageId(pagesID)
|
||||||
|
.sectionGridStorageId(sectionGridId)
|
||||||
|
.build();
|
||||||
|
return layoutParsingPipeline.parseLayoutAndSaveFilesToStorage(layoutParsingRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
@EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class})
|
||||||
|
@Import(LayoutParsingServiceProcessorConfiguration.class)
|
||||||
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
|
||||||
public static class RulesTestConfiguration {
|
public static class RulesTestConfiguration {
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user