Compare commits

...
13 Commits
Author SHA1 Message Date
Kilian Schüttler e44fc3b536 Merge branch 'RM-231-bp' into 'release/0.192.x'
RM-231: missing whitespace in name

See merge request fforesight/layout-parser!266
2025-01-14 13:07:49 +01:00
Kilian Schuettler 237034b696 RM-231: missing whitespace in name 2025-01-14 13:01:09 +01:00
Kilian Schuettler 85432a2511 RED-10714: fix IndexOutOfBoundsException
(cherry picked from commit 0b6a292c75)
2025-01-13 13:22:59 +01:00
Kilian Schüttler a821570065 Merge branch 'RED-9139-bp' into 'main'
RED-9139: more robust TOC detection

See merge request fforesight/layout-parser!254
2024-11-13 10:54:39 +01:00
Kilian Schüttler 7ee1f9e360 RED-9139: more robust TOC detection 2024-11-13 10:54:39 +01:00
Kilian Schüttler f9b25c8157 Merge branch 'RED-10249' into 'main'
RED-10249: regex found incorrectly due to wrong text sorting

See merge request fforesight/layout-parser!252
2024-11-04 12:51:38 +01:00
Kilian Schüttler c90874da7a RED-10249: regex found incorrectly due to wrong text sorting 2024-11-04 12:51:37 +01:00
Kilian Schüttler 4683c696a5 Merge branch 'RED-10247' into 'main'
RED-10247: dictionary entry not found in footer due to wrong text sorting

See merge request fforesight/layout-parser!251
2024-10-25 18:30:35 +02:00
Kilian Schuettler 95c02ce3cf RED-10247: dictionary entry not found in footer due to wrong text sorting 2024-10-25 17:18:14 +02:00
Kilian Schüttler b2d62e32fe Merge branch 'RED-10270-fp' into 'main'
RED-10270: fix NumberFormatException

See merge request fforesight/layout-parser!248
2024-10-24 17:14:47 +02:00
Kilian Schuettler 65c1f03ea3 RED-10270: fix NumberFormatException 2024-10-24 10:59:05 +02:00
Kilian Schüttler 2219519a2b Merge branch 'RED-10127' into 'main'
RED-10127: rename TextPositionSequence to Word

See merge request fforesight/layout-parser!244
2024-10-18 12:20:15 +02:00
Kilian Schüttler af05218e37 RED-10127: rename TextPositionSequence to Word 2024-10-18 12:20:15 +02:00
52 changed files with 701 additions and 609 deletions
@@ -26,10 +26,11 @@ import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.No
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingFinishedEvent;
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingRequest;
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Character;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextDirection;
import com.knecon.fforesight.service.layoutparser.processor.services.classification.ClassificationService;
import com.knecon.fforesight.service.layoutparser.processor.services.classification.TableOfContentsClassificationService;
import com.knecon.fforesight.service.layoutparser.processor.services.mapper.MarkdownMapper;
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
@@ -45,7 +46,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.outline.TableO
import com.knecon.fforesight.service.layoutparser.processor.model.table.Cell;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.python_api.adapter.CvTableParsingAdapter;
import com.knecon.fforesight.service.layoutparser.processor.python_api.adapter.ImageServiceResponseAdapter;
import com.knecon.fforesight.service.layoutparser.processor.python_api.adapter.VisualLayoutParsingAdapter;
@@ -53,7 +54,6 @@ import com.knecon.fforesight.service.layoutparser.processor.python_api.model.ima
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.TableCells;
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.TableServiceResponse;
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.VisualLayoutParsingResponse;
import com.knecon.fforesight.service.layoutparser.processor.services.BodyTextFrameService;
import com.knecon.fforesight.service.layoutparser.processor.services.RulingCleaningService;
import com.knecon.fforesight.service.layoutparser.processor.services.SectionsBuilderService;
import com.knecon.fforesight.service.layoutparser.processor.services.SimplifiedSectionTextService;
@@ -63,9 +63,6 @@ import com.knecon.fforesight.service.layoutparser.processor.services.blockificat
import com.knecon.fforesight.service.layoutparser.processor.services.blockification.DocstrumBlockificationService;
import com.knecon.fforesight.service.layoutparser.processor.services.blockification.DocuMineBlockificationService;
import com.knecon.fforesight.service.layoutparser.processor.services.blockification.RedactManagerBlockificationService;
import com.knecon.fforesight.service.layoutparser.processor.services.classification.ClarifyndClassificationService;
import com.knecon.fforesight.service.layoutparser.processor.services.classification.DocuMineClassificationService;
import com.knecon.fforesight.service.layoutparser.processor.services.classification.RedactManagerClassificationService;
import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory;
import com.knecon.fforesight.service.layoutparser.processor.services.graphics.Box;
import com.knecon.fforesight.service.layoutparser.processor.services.graphics.GraphicExtractorService;
@@ -163,9 +160,9 @@ public class LayoutParsingPipeline {
}
if (!viewerDocumentFile.equals(originFile)) {
viewerDocumentFile.delete();
assert !viewerDocumentFile.exists() || viewerDocumentFile.delete();
}
originFile.delete();
assert !originFile.exists() || originFile.delete();
return LayoutParsingFinishedEvent.builder()
.identifier(layoutParsingRequest.identifier())
@@ -245,12 +242,8 @@ public class LayoutParsingPipeline {
}
List<ClassificationPage> classificationPages = new ArrayList<>();
OutlineObject lastProcessedOutlineObject = null;
// parsing the structure elements could be useful as well
if (layoutParsingType != LayoutParsingType.REDACT_MANAGER_OLD) {
classificationDocument.setOutlineObjectTree(outlineExtractorService.getOutlineObjectTree(originDocument));
}
classificationDocument.setOutlineObjectTree(outlineExtractorService.getOutlineObjectTree(originDocument));
long pageCount = originDocument.getNumberOfPages();
@@ -275,25 +268,22 @@ public class LayoutParsingPipeline {
stripper.setEndPage(pageNumber);
stripper.setPdpage(pdPage);
stripper.getText(originDocument);
List<TextPositionSequence> words = stripper.getTextPositionSequences();
List<Word> words = stripper.getWords();
// rotateDirAdjExactly(words, pdPage); // works really well for many highly rotated documents (e.g. VV-331340.pdf), but it decreases the headline performance by 1.3%, so I am leaving it out for now
if (layoutParsingType.equals(LayoutParsingType.DOCUMINE_OLD)) {
var lines = TextPositionOperations.groupByLine(new HashSet<>(words));
classificationDocument.getLayoutDebugLayer().addLineVisualizationsFromNestedTextPosition(lines, pageNumber);
words = TextPositionOperations.sortLines(lines);
words = TextPositionOperations.sortWords(lines);
}
classificationDocument.getLayoutDebugLayer().addTextVisualizations(words, pageNumber);
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();
classificationDocument.getLayoutDebugLayer().addRulingVisualization(stripper.getRulings(), pageNumber);
CleanRulings cleanRulings = rulingCleaningService.deduplicateAndStraightenRulings(pdfTableCells.get(pageNumber), stripper.getRulings());
List<Ruling> rulings = stripper.getRulings();
classificationDocument.getLayoutDebugLayer().addRulingVisualization(rulings, pageNumber);
CleanRulings cleanRulings = rulingCleaningService.deduplicateAndStraightenRulings(pdfTableCells.get(pageNumber), rulings);
PageInformation pageInformation = PageInformation.fromPDPage(pageNumber, pdPage);
List<Cell> emptyTableCells = TableExtractionService.findCells(cleanRulings.getHorizontals(), cleanRulings.getVerticals(), pageInformation);
@@ -301,7 +291,7 @@ public class LayoutParsingPipeline {
TextRulingsClassifier.classifyUnderlinedAndStrikethroughText(words, cleanRulings);
List<Box> graphics = graphicExtractorService.extractPathElementGraphics(originDocument, pdPage, pageNumber, cleanRulings, stripper.getTextPositionSequences(), false);
List<Box> graphics = graphicExtractorService.extractPathElementGraphics(originDocument, pdPage, pageNumber, cleanRulings, stripper.getWords(), false);
pdfImages.computeIfAbsent(pageNumber, x -> new ArrayList<>())
.addAll(graphics.stream()
@@ -313,8 +303,7 @@ public class LayoutParsingPipeline {
.toList());
ClassificationPage classificationPage = switch (layoutParsingType) {
case REDACT_MANAGER_OLD ->
redactManagerBlockificationService.blockify(stripper.getTextPositionSequences(), cleanRulings, classificationDocument.getLayoutDebugLayer());
case REDACT_MANAGER_OLD -> redactManagerBlockificationService.blockify(stripper.getWords(), cleanRulings, classificationDocument.getLayoutDebugLayer());
case DOCUMINE_OLD -> docuMineBlockificationService.blockify(words, cleanRulings);
case DOCUMINE, REDACT_MANAGER, REDACT_MANAGER_PARAGRAPH_DEBUG, REDACT_MANAGER_WITHOUT_DUPLICATE_PARAGRAPH ->
docstrumBlockificationService.blockify(words, cleanRulings, true, classificationDocument.getLayoutDebugLayer(), layoutParsingType);
@@ -322,27 +311,9 @@ public class LayoutParsingPipeline {
docstrumBlockificationService.blockify(words, cleanRulings, false, classificationDocument.getLayoutDebugLayer(), layoutParsingType);
};
classificationPage.setCleanRulings(cleanRulings);
classificationPage.setRotation(rotation);
classificationPage.setLandscape(isLandscape);
classificationPage.setPageNumber(pageNumber);
classificationPage.setPageWidth(cropbox.getWidth());
classificationPage.setPageHeight(cropbox.getHeight());
updateClassificationPage(pdPage, pdr, classificationPage, cleanRulings, pageNumber, pageInformation);
if (layoutParsingType != LayoutParsingType.REDACT_MANAGER_OLD) {
List<OutlineObject> outlineObjects = classificationDocument.getOutlineObjectTree().getOutlineObjectsPerPage().getOrDefault(pageNumber, new ArrayList<>());
OutlineObject notFoundOutlineObject = null;
if (lastProcessedOutlineObject != null && !lastProcessedOutlineObject.isFound()) {
lastProcessedOutlineObject.resetPoint();
notFoundOutlineObject = lastProcessedOutlineObject;
}
if (!outlineObjects.isEmpty()) {
classificationPage.setOutlineObjects(outlineObjects);
lastProcessedOutlineObject = blockificationPostprocessingService.sanitizeOutlineBlocks(classificationPage, notFoundOutlineObject);
}
classificationDocument.getLayoutDebugLayer().addOutlineObjects(outlineObjects, pageInformation);
}
blockificationPostprocessingService.findHeadlinesFromOutline(classificationDocument, pageNumber, classificationPage, pageInformation);
classificationDocument.getLayoutDebugLayer().addMarkedContentVisualizations(stripper.getMarkedContents(), pageNumber);
// MarkedContent needs to be converted at this point, otherwise it leads to GC Problems in Pdfbox.
@@ -388,13 +359,31 @@ public class LayoutParsingPipeline {
}
private static void rotateDirAdjExactly(List<TextPositionSequence> words, PDPage pdPage) {
private static void updateClassificationPage(PDPage pdPage,
PDRectangle pdr,
ClassificationPage classificationPage,
CleanRulings cleanRulings,
int pageNumber,
PageInformation pageInformation) {
int rotation = pdPage.getRotation();
boolean isLandscape = pdr.getWidth() > pdr.getHeight() && (rotation == 0 || rotation == 180) || pdr.getHeight() > pdr.getWidth() && (rotation == 90 || rotation == 270);
classificationPage.setCleanRulings(cleanRulings);
classificationPage.setRotation(rotation);
classificationPage.setLandscape(isLandscape);
classificationPage.setPageNumber(pageNumber);
classificationPage.setPageWidth((float) pageInformation.width());
classificationPage.setPageHeight((float) pageInformation.height());
}
private static void rotateDirAdjExactly(List<Word> words, PDPage pdPage) {
for (TextDirection dir : TextDirection.values()) {
double averageRotation = words.stream()
.map(TextPositionSequence::getTextPositions)
.map(Word::getCharacters)
.flatMap(Collection::stream)
.map(Character::getTextPosition)
.filter(pos -> pos.getDir().equals(dir))
.mapToDouble(RedTextPosition::getExactDir).average().orElse(0);
@@ -404,7 +393,7 @@ public class LayoutParsingPipeline {
AffineTransform rotateInstance = AffineTransform.getRotateInstance(averageRotation, pdPage.getMediaBox().getWidth() / 2, pdPage.getMediaBox().getHeight() / 2);
for (TextPositionSequence word : words) {
for (Word word : words) {
if (!dir.equals(word.getDir())) {
continue;
}
@@ -455,10 +444,10 @@ public class LayoutParsingPipeline {
// Collect all statistics for the classificationPage, except from blocks inside tables, as tables will always be added to BodyTextFrame.
for (AbstractPageBlock textBlock : classificationPage.getTextBlocks()) {
if (textBlock instanceof TextPageBlock) {
if (((TextPageBlock) textBlock).getSequences() == null) {
if (((TextPageBlock) textBlock).getWords() == null) {
continue;
}
for (TextPositionSequence word : ((TextPageBlock) textBlock).getSequences()) {
for (Word word : ((TextPageBlock) textBlock).getWords()) {
classificationPage.getTextHeightCounter().add(word.getTextHeight());
classificationPage.getFontCounter().add(word.getFont());
classificationPage.getFontSizeCounter().add(word.getFontSize());
@@ -18,7 +18,7 @@ import com.knecon.fforesight.service.layoutparser.processor.docstrum.service.Zon
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextDirection;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutDebugLayer;
import lombok.RequiredArgsConstructor;
@@ -35,7 +35,7 @@ public class DocstrumSegmentationService {
private final ReadingOrderService readingOrderService;
public List<Zone> segmentPage(List<TextPositionSequence> textPositions, boolean xyOrder, CleanRulings usedRulings, LayoutDebugLayer visualizations) {
public List<Zone> segmentPage(List<Word> textPositions, boolean xyOrder, CleanRulings usedRulings, LayoutDebugLayer visualizations) {
EnumMap<TextDirection, Integer> directionCounts = new EnumMap<>(TextDirection.class);
@@ -78,18 +78,14 @@ public class DocstrumSegmentationService {
}
private List<Zone> computeZones(List<TextPositionSequence> textPositions, CleanRulings rulings, LayoutDebugLayer visualizations, TextDirection direction) {
private List<Zone> computeZones(List<Word> textPositions, CleanRulings rulings, LayoutDebugLayer visualizations, TextDirection direction) {
List<RedTextPosition> positions = textPositions.stream()
List<Character> characters = textPositions.stream()
.filter(t -> t.getDir() == direction)
.map(TextPositionSequence::getTextPositions)
.map(Word::getCharacters)
.flatMap(List::stream)
.toList();
List<Character> characters = positions.stream()
.map(Character::new)
.collect(Collectors.toList());
nearestNeighbourService.findNearestNeighbors(characters);
double characterSpacing = spacingService.computeCharacterSpacing(characters);
@@ -1,9 +1,9 @@
package com.knecon.fforesight.service.layoutparser.processor.docstrum.model;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence.BOLD;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence.BOLD_ITALIC;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence.ITALIC;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence.STANDARD;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.Word.BOLD;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.Word.BOLD_ITALIC;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.Word.ITALIC;
import static com.knecon.fforesight.service.layoutparser.processor.model.text.Word.STANDARD;
import java.util.ArrayList;
import java.util.Arrays;
@@ -14,7 +14,7 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.utils.FastAtan2;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.model.text.FontStyle;
import lombok.Data;
@@ -24,7 +24,7 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class Line extends TextBoundingBox {
private static final double WORD_DISTANCE_MULTIPLIER = 0.18;
private static final double WORD_DISTANCE_MULTIPLIER = 0.17;
@EqualsAndHashCode.Include
private final double x0;
@@ -36,18 +36,13 @@ public class Line extends TextBoundingBox {
@EqualsAndHashCode.Include
private final double y1;
private final double height;
private FontStyle fontStyle;
private final List<Character> characters;
private final List<TextPositionSequence> words = new ArrayList<>();
private final List<Word> words;
public Line(List<Character> characters, double wordSpacing) {
this.characters = characters;
if (characters.size() >= 2) {
// linear regression
double sx = 0.0;
@@ -76,20 +71,32 @@ public class Line extends TextBoundingBox {
this.y0 = character.getY() - dy;
this.y1 = character.getY() + dy;
}
height = computeHeight();
computeWords(wordSpacing * WORD_DISTANCE_MULTIPLIER);
this.words = new ArrayList<>();
computeWords(characters, wordSpacing * WORD_DISTANCE_MULTIPLIER);
buildBBox();
computeFontStyle();
}
public Line(List<Word> words) {
this.words = words;
buildBBox();
x0 = getMinX();
y0 = getMinY();
x1 = getMaxX();
y1 = getMaxY();
computeFontStyle();
}
private void computeFontStyle() {
EnumMap<FontStyle, AtomicInteger> fontStyleCounter = new EnumMap<>(FontStyle.class);
for (FontStyle fontStyle : FontStyle.values()) {
fontStyleCounter.put(fontStyle, new AtomicInteger(0));
}
for (TextPositionSequence word : words) {
for (Word word : words) {
switch (word.getFontStyle()) {
case STANDARD -> fontStyleCounter.get(FontStyle.REGULAR).getAndIncrement();
case BOLD -> fontStyleCounter.get(FontStyle.BOLD).getAndIncrement();
@@ -100,8 +107,7 @@ public class Line extends TextBoundingBox {
fontStyle = fontStyleCounter.entrySet()
.stream()
.max(Comparator.comparing(entry -> entry.getValue().get()))
.map(Map.Entry::getKey)
.orElse(FontStyle.REGULAR);
.map(Map.Entry::getKey).orElse(FontStyle.REGULAR);
}
@@ -117,14 +123,6 @@ public class Line extends TextBoundingBox {
}
private double computeHeight() {
return characters.stream()
.map(Character::getHeight)
.reduce(0d, Double::sum) / characters.size();
}
public double angularDifference(Line j) {
double diff = Math.abs(getAngle() - j.getAngle());
@@ -157,19 +155,22 @@ public class Line extends TextBoundingBox {
}
private void computeWords(double wordSpacing) {
private void computeWords(List<Character> characters, double wordSpacing) {
TextPositionSequence word = new TextPositionSequence();
// Imo, the width of space should be scaled with the font size, but it only depends on the median distance between horizontal neighbours.
// If there are large differences in fontsize on a page, this might lead to missing spaces for the smaller fonts and too many for larger fonts.
// I've just now changed the scaling factor. If you come across this comment with missing whitespaces again, try scaling the fontsize instead of simply changing the factor again.
Word word = new Word();
Character previous = null;
for (Character current : characters) {
if (previous != null) {
double dist = current.getTextPosition().getXDirAdj() - previous.getTextPosition().getXDirAdj() - previous.getTextPosition().getWidthDirAdj();
if (dist > wordSpacing) {
words.add(word);
word = new TextPositionSequence();
word = new Word();
}
}
word.getTextPositions().add(current.getTextPosition());
word.add(current);
previous = current;
}
words.add(word);
@@ -178,9 +179,7 @@ public class Line extends TextBoundingBox {
private void buildBBox() {
this.setToBBoxOfComponents(characters.stream()
.map(Character::getTextPosition)
.toList());
this.setToBBoxOfComponents(words);
}
@@ -18,7 +18,6 @@ public class Zone extends TextBoundingBox {
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public Zone(List<Line> lines) {
lines.sort(Comparator.comparingDouble(Line::getY0));
this.lines = lines;
setToBBoxOfComponents(lines);
}
@@ -17,7 +17,7 @@ public class LineBuilderService {
private static final double CHARACTER_SPACING_DISTANCE_MULTIPLIER = 3.5;
private static final double LINE_SPACING_THRESHOLD_MULTIPLIER = 0.67;
private static final double ANGLE_TOLERANCE = Math.PI / 6;
private static final double ANGLE_TOLERANCE = Math.toRadians(5);
public List<Line> buildLines(List<Character> characters, double characterSpacing, double lineSpacing, CleanRulings rulings) {
@@ -1,9 +1,7 @@
package com.knecon.fforesight.service.layoutparser.processor.docstrum.service;
import static com.knecon.fforesight.service.layoutparser.processor.model.SectionIdentifier.numericalIdentifierPattern;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -11,11 +9,12 @@ import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Character;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Line;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.UnionFind;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Zone;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.utils.TextPositionOperations;
@Service
public class ZoneBuilderService {
@@ -31,7 +30,7 @@ public class ZoneBuilderService {
private static final double MAX_LINE_SIZE_SCALE = 2.5;
private static final double ANGLE_TOLERANCE = Math.PI / 6;
private static final double ANGLE_TOLERANCE = Math.toRadians(5);
private static final double MAX_VERTICAL_MERGE_DISTANCE = 0.5;
@@ -114,64 +113,14 @@ public class ZoneBuilderService {
private Zone mergeLinesInZone(List<Line> lines, double characterSpacing, double lineSpacing) {
double maxHorizontalDistance = 0;
double minVerticalDistance = 0;
double maxVerticalDistance = lineSpacing * MAX_VERTICAL_MERGE_DISTANCE;
Set<Word> words = lines.stream()
.map(Line::getWords)
.flatMap(Collection::stream)
.collect(Collectors.toSet());
Collection<Set<Word>> groupedLines = TextPositionOperations.groupByLine(words);
UnionFind<Line> unionFind = new UnionFind<>(new HashSet<>(lines));
lines.forEach(outer -> {
lines.forEach(inner -> {
if (inner == outer) {
return;
}
double horizontalDistance = outer.horizontalDistance(inner);
double verticalDistance = outer.verticalDistance(inner);
if (horizontalDistance <= maxHorizontalDistance && minVerticalDistance <= verticalDistance && verticalDistance <= maxVerticalDistance) {
unionFind.union(outer, inner);
} else if (minVerticalDistance <= verticalDistance
&& verticalDistance <= maxVerticalDistance
&& Math.abs(horizontalDistance - Math.min(outer.getLength(), inner.getLength())) < 0.1) {
boolean characterOverlap = false;
int overlappingCount = 0;
for (Character outerCharacter : outer.getCharacters()) {
for (Character innerCharacter : inner.getCharacters()) {
double characterOverlapDistance = outerCharacter.overlappingDistance(innerCharacter);
if (characterOverlapDistance > 2) {
characterOverlap = true;
}
if (characterOverlapDistance > 0) {
overlappingCount++;
}
}
}
if (!characterOverlap && overlappingCount <= 2) {
unionFind.union(outer, inner);
}
}
});
});
List<Line> outputZone = new ArrayList<>();
for (Set<Line> group : unionFind.getGroups()) {
List<Character> characters = new ArrayList<>();
for (Line line : group) {
characters.addAll(line.getCharacters());
}
characters.sort(Comparator.comparingDouble(Character::getX));
outputZone.add(new Line(characters, characterSpacing));
}
return new Zone(outputZone.stream()
.sorted(Comparator.comparing(Line::getY0))
.collect(Collectors.toList()));
List<Line> sortedLines = TextPositionOperations.sortLines(groupedLines);
return new Zone(sortedLines);
}
}
@@ -3,7 +3,7 @@ package com.knecon.fforesight.service.layoutparser.processor.model;
import java.awt.geom.Rectangle2D;
import java.util.List;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -16,8 +16,8 @@ import lombok.experimental.FieldDefaults;
public class LineInformation {
List<Rectangle2D> lineBBox;
List<List<TextPositionSequence>> sequencesByLines;
List<List<Word>> sequencesByLines;
List<List<Rectangle2D>> bBoxWithGapsByLines;
List<List<List<TextPositionSequence>>> sequencesWithGapsByLines;
List<List<List<Word>>> sequencesWithGapsByLines;
}
@@ -4,7 +4,7 @@ import java.awt.geom.Rectangle2D;
import java.util.List;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -15,7 +15,7 @@ import lombok.Getter;
@AllArgsConstructor
public class PageContents {
List<TextPositionSequence> sortedTextPositionSequences;
List<Word> sortedWords;
Rectangle2D cropBox;
Rectangle2D mediaBox;
List<Ruling> rulings;
@@ -16,8 +16,8 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class SectionIdentifier {
public static Pattern numericalIdentifierPattern = Pattern.compile("^[\\s]?(\\d+)[\\s.,;]?(\\d+)?[\\s.,;]?(\\d+)?[\\s.,;]?(\\d+)?");
public static Pattern alphanumericIdentifierPattern = Pattern.compile("^[\\s]?[A-Za-z][\\s.,;]?(\\d+)[\\s.,;]?(\\d+)?[\\s.,;]?(\\d+)?[\\s.,;]?(\\d+)?[\\s.,;]?");
public static Pattern numericalIdentifierPattern = Pattern.compile("^[\\s]?(\\d{1,2})(?:[\\s.,;](\\d{1,2}))?(?:[\\s.,;](\\d{1,2}))?(?:[\\s.,;](\\d{1,2}))?");
public static Pattern alphanumericIdentifierPattern = Pattern.compile("^[\\s]?[A-Za-z][\\s.,;]?(\\d{1,2})[\\s.,;]?(\\d{1,2})?[\\s.,;]?(\\d{1,2})?[\\s.,;]?(\\d{1,2})?[\\s.,;]?");
public enum Format {
EMPTY,
@@ -9,7 +9,7 @@ import java.util.List;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.utils.TextNormalizationUtilities;
import lombok.Data;
@@ -68,12 +68,12 @@ public class Cell extends BoundingBox {
StringBuilder sb = new StringBuilder();
Iterator<TextPageBlock> itty = textBlocks.iterator();
TextPositionSequence previous = null;
Word previous = null;
while (itty.hasNext()) {
TextPageBlock textBlock = itty.next();
for (TextPositionSequence word : textBlock.getSequences()) {
for (Word word : textBlock.getWords()) {
if (previous != null) {
if (Math.abs(previous.getMaxYDirAdj() - word.getMaxYDirAdj()) > word.getTextHeight()) {
sb.append('\n');
@@ -14,16 +14,17 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class ListIdentifier {
public static final Pattern NUMBER_WITH_DOT = Pattern.compile("^\\s*([1-9]+)\\.\\s+");
public static final Pattern NUMBER_IN_PARENTHESES = Pattern.compile("^\\s*\\(([1-9]+)\\)\\s+");
public static final Pattern NUMBER_WITH_DOT = Pattern.compile("^\\s*([1-9]{1,4})\\.\\s+");
public static final Pattern NUMBER_IN_PARENTHESES = Pattern.compile("^\\s*\\(([1-9]{1,4})\\)\\s+");
enum Format {
NUMBER_WITH_DOT, NUMBER_IN_PARENTHESES
NUMBER_WITH_DOT,
NUMBER_IN_PARENTHESES
}
Format format;
@Getter
TextPositionSequence word;
Word word;
@Getter
int page;
int representation;
@@ -31,14 +32,14 @@ public class ListIdentifier {
public static Optional<ListIdentifier> parse(TextPageBlock textPageBlock, int page) {
return parse(textPageBlock.getSequences().subList(0, Math.min(5, textPageBlock.getSequences().size())), page);
return parse(textPageBlock.getWords().subList(0, Math.min(5, textPageBlock.getWords().size())), page);
}
public static Optional<ListIdentifier> parse(List<TextPositionSequence> sequences, int page) {
public static Optional<ListIdentifier> parse(List<Word> sequences, int page) {
StringBuilder sb = new StringBuilder();
for (TextPositionSequence sequence : sequences) {
for (Word sequence : sequences) {
sb.append(sequence.toString());
sb.append(" ");
}
@@ -48,17 +49,33 @@ public class ListIdentifier {
Matcher numberMatcher = NUMBER_WITH_DOT.matcher(text);
if (numberMatcher.find()) {
return Optional.of(new ListIdentifier(Format.NUMBER_WITH_DOT, sequences.get(0), page, Integer.parseInt(numberMatcher.group(1))));
Optional<Integer> representation = parseInteger(numberMatcher.group(1));
if (representation.isPresent()) {
return Optional.of(new ListIdentifier(Format.NUMBER_WITH_DOT, sequences.get(0), page, representation.get()));
}
}
Matcher parenthesisMatcher = NUMBER_IN_PARENTHESES.matcher(text);
if (parenthesisMatcher.find()) {
return Optional.of(new ListIdentifier(Format.NUMBER_IN_PARENTHESES, sequences.get(0), page, Integer.parseInt(parenthesisMatcher.group(1))));
Optional<Integer> representation = parseInteger(parenthesisMatcher.group(1));
if (representation.isPresent()) {
return Optional.of(new ListIdentifier(Format.NUMBER_IN_PARENTHESES, sequences.get(0), page, representation.get()));
}
}
return Optional.empty();
}
private static Optional<Integer> parseInteger(String text) {
try {
return Optional.of(Integer.parseInt(text));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
public static boolean isInOrder(List<ListIdentifier> listIdentifiers) {
if (listIdentifiers.size() <= 1) {
@@ -10,18 +10,18 @@ import lombok.Getter;
@Getter
public class SearchableText {
private final List<TextPositionSequence> sequences = new ArrayList<>();
private final List<Word> sequences = new ArrayList<>();
public void add(TextPositionSequence textPositionSequence) {
public void add(Word word) {
sequences.add(textPositionSequence);
sequences.add(word);
}
public void addAll(List<TextPositionSequence> textPositionSequences) {
public void addAll(List<Word> words) {
sequences.addAll(textPositionSequences);
sequences.addAll(words);
}
@@ -32,10 +32,10 @@ public class SearchableText {
}
public static String buildString(List<TextPositionSequence> sequences) {
public static String buildString(List<Word> sequences) {
StringBuilder sb = new StringBuilder();
for (TextPositionSequence word : sequences) {
for (Word word : sequences) {
sb.append(word);
sb.append(' ');
}
@@ -25,7 +25,7 @@ import lombok.NoArgsConstructor;
public class TextPageBlock extends AbstractPageBlock {
@Builder.Default
private List<TextPositionSequence> sequences = new ArrayList<>();
private List<Word> words = new ArrayList<>();
@Builder.Default
private FrequencyCounters frequencyCounters = new FrequencyCounters();
@@ -41,43 +41,43 @@ public class TextPageBlock extends AbstractPageBlock {
private boolean changed;
public TextPageBlock(List<TextPositionSequence> sequences) {
public TextPageBlock(List<Word> words) {
this.sequences = new ArrayList<>(sequences);
this.words = new ArrayList<>(words);
this.frequencyCounters = new FrequencyCounters();
if (!sequences.isEmpty()) {
addToFrequencyCounters(sequences);
if (!words.isEmpty()) {
addToFrequencyCounters(words);
}
calculateBBox();
}
public List<TextPositionSequence> getSequences() {
public List<Word> getWords() {
return Collections.unmodifiableList(sequences);
return Collections.unmodifiableList(words);
}
public TextDirection getDir() {
return sequences.get(0).getDir();
return words.get(0).getDir();
}
private void calculateBBox() {
if (sequences == null) {
if (words == null) {
this.bBox = new Rectangle2D.Double();
this.bBoxPdf = new Rectangle2D.Double();
this.bBoxDirAdj = new Rectangle2D.Double();
return;
}
this.bBoxDirAdj = sequences.stream()
.map(TextPositionSequence::getBBoxDirAdj)
this.bBoxDirAdj = words.stream()
.map(Word::getBBoxDirAdj)
.collect(RectangleTransformations.collectBBox());
setToBBoxOfComponents(sequences);
setToBBoxOfComponents(words);
}
@@ -99,8 +99,8 @@ public class TextPageBlock extends AbstractPageBlock {
throw new IllegalArgumentException("Cannot merge textBlocks on different pages.");
}
List<TextPositionSequence> sequences = textBlocksToMerge.stream()
.map(TextPageBlock::getSequences)
List<Word> sequences = textBlocksToMerge.stream()
.map(TextPageBlock::getWords)
.flatMap(java.util.Collection::stream)
.toList();
sequences = new ArrayList<>(sequences);
@@ -109,9 +109,9 @@ public class TextPageBlock extends AbstractPageBlock {
}
private void addToFrequencyCounters(List<TextPositionSequence> sequences) {
private void addToFrequencyCounters(List<Word> sequences) {
for (TextPositionSequence wordBlock : sequences) {
for (Word wordBlock : sequences) {
frequencyCounters.getLineHeightFrequencyCounter().add(wordBlock.getTextHeight());
frequencyCounters.getFontSizeFrequencyCounter().add(wordBlock.getFontSize());
@@ -120,12 +120,12 @@ public class TextPageBlock extends AbstractPageBlock {
frequencyCounters.getStyleFrequencyCounter().add(wordBlock.getFontStyle());
}
setUnderlined(this.sequences.stream()
.allMatch(TextPositionSequence::isUnderline));
setUnderlined(this.words.stream()
.allMatch(Word::isUnderline));
}
public TextPageBlock union(TextPositionSequence r) {
public TextPageBlock union(Word r) {
TextPageBlock union = this.copy();
union.add(r);
@@ -138,8 +138,8 @@ public class TextPageBlock extends AbstractPageBlock {
public TextPageBlock union(TextPageBlock r) {
TextPageBlock union = this.copy();
union.addAll(r.getSequences());
addToFrequencyCounters(r.getSequences());
union.addAll(r.getWords());
addToFrequencyCounters(r.getWords());
calculateBBox();
return union;
}
@@ -148,33 +148,33 @@ public class TextPageBlock extends AbstractPageBlock {
public void add(TextPageBlock textPageBlock) {
changed = true;
sequences.addAll(textPageBlock.getSequences());
addToFrequencyCounters(textPageBlock.getSequences());
words.addAll(textPageBlock.getWords());
addToFrequencyCounters(textPageBlock.getWords());
calculateBBox();
}
public void add(TextPositionSequence textPositionSequence) {
public void add(Word word) {
changed = true;
sequences.add(textPositionSequence);
addToFrequencyCounters(List.of(textPositionSequence));
words.add(word);
addToFrequencyCounters(List.of(word));
calculateBBox();
}
public void addAll(List<TextPositionSequence> textPositionSequences) {
public void addAll(List<Word> words) {
changed = true;
sequences.addAll(textPositionSequences);
addToFrequencyCounters(textPositionSequences);
this.words.addAll(words);
addToFrequencyCounters(words);
calculateBBox();
}
public TextPageBlock copy() {
return new TextPageBlock(new ArrayList<>(sequences));
return new TextPageBlock(new ArrayList<>(words));
}
@@ -193,8 +193,8 @@ public class TextPageBlock extends AbstractPageBlock {
StringBuilder sb = new StringBuilder();
TextPositionSequence previous = null;
for (TextPositionSequence word : sequences) {
Word previous = null;
for (Word word : words) {
if (previous != null) {
if (Math.abs(previous.getMaxYDirAdj() - word.getMaxYDirAdj()) > word.getTextHeight()) {
sb.append('\n');
@@ -217,8 +217,8 @@ public class TextPageBlock extends AbstractPageBlock {
public int getNumberOfLines() {
int numberOfLines = 1;
TextPositionSequence previous = null;
for (TextPositionSequence word : sequences) {
Word previous = null;
for (Word word : words) {
if (previous != null) {
if (word.getMaxYDirAdj() - previous.getMaxYDirAdj() > word.getTextHeight()) {
numberOfLines++;
@@ -270,7 +270,7 @@ public class TextPageBlock extends AbstractPageBlock {
@Override
public boolean isEmpty() {
return sequences.isEmpty();
return words.isEmpty();
}
}
@@ -1,34 +0,0 @@
package com.knecon.fforesight.service.layoutparser.processor.model.text;
import java.util.Comparator;
import java.util.HashMap;
public class TextPositionSequenceComparator implements Comparator<TextPositionSequence> {
private HashMap<TextPositionSequence, TextBlockOnPage> lookup;
public TextPositionSequenceComparator(HashMap<TextPositionSequence, TextBlockOnPage> lookup) {
this.lookup = lookup;
}
@Override
public int compare(TextPositionSequence number1, TextPositionSequence number2) {
int page1 = lookup.get(number1).page().getPageNumber();
int page2 = lookup.get(number2).page().getPageNumber();
if (page1 != page2) {
return Integer.compare(page1, page2);
}
if (number1.getY() != number2.getY()) {
return Double.compare(number1.getY(), number2.getY());
}
return Integer.compare(Integer.parseInt(number1.toString()), Integer.parseInt(number2.toString()));
}
}
@@ -0,0 +1,36 @@
package com.knecon.fforesight.service.layoutparser.processor.model.text;
import java.util.Comparator;
import java.util.HashMap;
import com.knecon.fforesight.service.layoutparser.processor.services.classification.NumberWord;
public class TocNumberComparator implements Comparator<NumberWord> {
private HashMap<NumberWord, TextBlockOnPage> lookup;
public TocNumberComparator(HashMap<NumberWord, TextBlockOnPage> lookup) {
this.lookup = lookup;
}
@Override
public int compare(NumberWord number1, NumberWord number2) {
int page1 = lookup.get(number1).page().getPageNumber();
int page2 = lookup.get(number2).page().getPageNumber();
if (page1 != page2) {
return Integer.compare(page1, page2);
}
if (number1.word().getY() != number2.word().getY()) {
return Double.compare(number1.word().getY(), number2.word().getY());
}
return Integer.compare(number1.number(), number2.number());
}
}
@@ -13,6 +13,7 @@ import java.util.stream.Collectors;
import org.apache.pdfbox.text.TextPosition;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Character;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.TextBoundingBox;
import lombok.AllArgsConstructor;
@@ -27,7 +28,7 @@ import lombok.extern.slf4j.Slf4j;
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("pmd")
public class TextPositionSequence extends TextBoundingBox implements CharSequence {
public class Word extends TextBoundingBox implements CharSequence {
public static final String STANDARD = "standard";
public static final String BOLD_ITALIC = "bold, italic";
@@ -38,8 +39,7 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
private int page;
@Builder.Default
private List<RedTextPosition> textPositions = new ArrayList<>();
private List<Character> characters = new ArrayList<>();
private boolean isParagraphStart;
private boolean strikethrough;
private boolean underline;
@@ -47,10 +47,11 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
private Integer hashcodeCache;
public TextPositionSequence(List<TextPosition> textPositions, int pageNumber, boolean isParagraphStart) {
public Word(List<TextPosition> textPositions, int pageNumber, boolean isParagraphStart) {
this.textPositions = textPositions.stream()
this.characters = textPositions.stream()
.map(RedTextPosition::fromTextPosition)
.map(Character::new)
.collect(Collectors.toList());
this.page = pageNumber;
this.isParagraphStart = isParagraphStart;
@@ -65,9 +66,9 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
}
public TextPositionSequence(List<RedTextPosition> textPositions, int page) {
public Word(List<Character> textPositions, int page) {
this.textPositions = textPositions;
this.characters = new ArrayList<>(textPositions);
this.page = page;
calculateBBoxAndHashcode();
}
@@ -76,7 +77,7 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
@Override
public int length() {
return textPositions.size();
return characters.size();
}
@@ -98,10 +99,10 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
@Override
public TextPositionSequence subSequence(int start, int end) {
public Word subSequence(int start, int end) {
var textPositionSequence = new TextPositionSequence();
textPositionSequence.textPositions = textPositions.subList(start, end);
var textPositionSequence = new Word();
textPositionSequence.characters = characters.subList(start, end);
textPositionSequence.page = page;
textPositionSequence.dir = dir;
textPositionSequence.setToBBoxOfComponents(getTextPositions());
@@ -122,53 +123,59 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
public RedTextPosition textPositionAt(int index) {
return textPositions.get(index);
return characters.get(index).getTextPosition();
}
public void add(TextPositionSequence textPositionSequence, RedTextPosition textPosition) {
public void add(Word word, RedTextPosition textPosition) {
this.textPositions.add(textPosition);
this.page = textPositionSequence.getPage();
this.characters.add(new Character(textPosition));
this.page = word.getPage();
calculateBBoxAndHashcode();
}
public void add(Character current) {
characters.add(current);
calculateBBoxAndHashcode();
}
public void add(TextPosition textPosition) {
this.textPositions.add(RedTextPosition.fromTextPosition(textPosition));
calculateBBoxAndHashcode();
add(new Character(RedTextPosition.fromTextPosition(textPosition)));
}
public double getTextHeightNoPadding() {
return textPositions.get(0).getHeightDirAdj();
return characters.get(0).getTextPosition().getHeightDirAdj();
}
public double getTextHeight() {
return textPositions.get(0).getHeightDirAdj() + HEIGHT_PADDING;
return characters.get(0).getTextPosition().getHeightDirAdj() + HEIGHT_PADDING;
}
public String getFont() {
if (textPositions.get(0).getFontName() == null) {
if (characters.get(0).getTextPosition().getFontName() == null) {
return "none";
}
return FONT_CLEANER.matcher(textPositions.get(0).getFontName().toLowerCase(Locale.ROOT)).replaceAll("");
return FONT_CLEANER.matcher(characters.get(0).getTextPosition().getFontName().toLowerCase(Locale.ROOT)).replaceAll("");
}
public String getFontStyle() {
if (textPositions.get(0).getFontName() == null) {
if (characters.get(0).getTextPosition().getFontName() == null) {
return STANDARD;
}
String lowercaseFontName = textPositions.get(0).getFontName().toLowerCase(Locale.ROOT);
String lowercaseFontName = characters.get(0).getTextPosition().getFontName().toLowerCase(Locale.ROOT);
if (lowercaseFontName.contains(BOLD) && lowercaseFontName.contains(ITALIC)) {
return BOLD_ITALIC;
@@ -184,13 +191,13 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
public float getFontSize() {
return textPositions.get(0).getFontSizeInPt();
return characters.get(0).getTextPosition().getFontSizeInPt();
}
public float getSpaceWidth() {
return textPositions.get(0).getWidthOfSpace();
return characters.get(0).getTextPosition().getWidthOfSpace();
}
@@ -199,7 +206,7 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
if (o == this) {
return true;
}
if (!(o instanceof TextPositionSequence other)) {
if (!(o instanceof Word other)) {
return false;
}
if (!other.canEqual((Object) this)) {
@@ -220,7 +227,7 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
}
protected boolean canEqual(final Object other) {return other instanceof TextPositionSequence;}
protected boolean canEqual(final Object other) {return other instanceof Word;}
public int hashCode() {
@@ -244,6 +251,14 @@ public class TextPositionSequence extends TextBoundingBox implements CharSequenc
}
private List<RedTextPosition> getTextPositions() {
return characters.stream()
.map(Character::getTextPosition)
.toList();
}
public void transform(AffineTransform rotateInstance) {
for (RedTextPosition textPosition : getTextPositions()) {
@@ -23,11 +23,11 @@ public class DividingColumnDetectionService {
public List<Rectangle2D> detectColumns(PageContents pageContents) {
if (pageContents.getSortedTextPositionSequences().size() < 2) {
if (pageContents.getSortedWords().size() < 2) {
return List.of(pageContents.getCropBox());
}
GapInformation linesWithGapInformation = GapDetectionService.findGapsInLines(pageContents.getSortedTextPositionSequences(), pageContents.getCropBox());
GapInformation linesWithGapInformation = GapDetectionService.findGapsInLines(pageContents.getSortedWords(), pageContents.getCropBox());
return detectColumnsFromLines(linesWithGapInformation.getXGaps(), pageContents.getCropBox());
}
@@ -5,7 +5,7 @@ import java.util.LinkedList;
import java.util.List;
import com.knecon.fforesight.service.layoutparser.processor.model.GapInformation;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.AllArgsConstructor;
import lombok.experimental.UtilityClass;
@@ -18,23 +18,23 @@ public class GapDetectionService {
private static final double NEW_LINE_FACTOR = 0.2;
public static GapInformation findGapsInLines(List<TextPositionSequence> sortedTextPositionSequences, Rectangle2D mainBodyTextFrame) {
public static GapInformation findGapsInLines(List<Word> sortedWords, Rectangle2D mainBodyTextFrame) {
if (sortedTextPositionSequences.isEmpty()) {
if (sortedWords.isEmpty()) {
return new GapInformation();
}
final double avgTextPositionHeight = getAvgTextPositionHeight(sortedTextPositionSequences);
final double avgTextPositionHeight = getAvgTextPositionHeight(sortedWords);
XGapsContext xGapContext = XGapsContext.init(mainBodyTextFrame);
YGapsContext yGapContext = YGapsContext.init(mainBodyTextFrame);
var previousTextPosition = sortedTextPositionSequences.get(0);
var previousTextPosition = sortedWords.get(0);
Rectangle2D rectangle = toRectangle2D(previousTextPosition);
xGapContext.addGapFromLeftEdgeOfMainBody(rectangle);
for (TextPositionSequence currentTextPosition : sortedTextPositionSequences.subList(1, sortedTextPositionSequences.size())) {
for (Word currentTextPosition : sortedWords.subList(1, sortedWords.size())) {
double yDifference = Math.abs(currentTextPosition.getMaxYDirAdj() - previousTextPosition.getMaxYDirAdj());
double xGap = Math.abs(previousTextPosition.getMaxXDirAdj() - currentTextPosition.getXDirAdj());
@@ -59,14 +59,14 @@ public class GapDetectionService {
}
previousTextPosition = currentTextPosition;
}
xGapContext.addGapToRightEdgeOfMainBody(toRectangle2D(sortedTextPositionSequences.get(sortedTextPositionSequences.size() - 1)));
xGapContext.addGapToRightEdgeOfMainBody(toRectangle2D(sortedWords.get(sortedWords.size() - 1)));
xGapContext.gapsPerLine.add(xGapContext.gapsInCurrentLine);
return new GapInformation(xGapContext.gapsPerLine, yGapContext.gapsPerLine);
}
private static Rectangle2D toRectangle2D(TextPositionSequence textPosition) {
private static Rectangle2D toRectangle2D(Word textPosition) {
return mirrorY(textPosition.getBBox());
}
@@ -87,18 +87,18 @@ public class GapDetectionService {
}
private static void assertAllTextPositionsHaveSameDir(List<TextPositionSequence> textPositionSequences) {
private static void assertAllTextPositionsHaveSameDir(List<Word> words) {
assert textPositionSequences.stream()
.map(TextPositionSequence::getDir)
.allMatch(a -> a.equals(textPositionSequences.get(0).getDir()));
assert words.stream()
.map(Word::getDir)
.allMatch(a -> a.equals(words.get(0).getDir()));
}
private static double getAvgTextPositionHeight(List<TextPositionSequence> textPositionSequences) {
private static double getAvgTextPositionHeight(List<Word> words) {
return textPositionSequences.stream()
.mapToDouble(TextPositionSequence::getHeight).average().orElseThrow();
return words.stream()
.mapToDouble(Word::getHeight).average().orElseThrow();
}
@@ -7,17 +7,17 @@ import java.util.List;
import com.knecon.fforesight.service.layoutparser.processor.model.GapInformation;
import com.knecon.fforesight.service.layoutparser.processor.model.LineInformation;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.experimental.UtilityClass;
@UtilityClass
public class InvisibleTableDetectionService {
public List<List<Rectangle2D>> detectTable(List<TextPositionSequence> textPositionSequences, Rectangle2D tableBBox) {
public List<List<Rectangle2D>> detectTable(List<Word> words, Rectangle2D tableBBox) {
LineInformation lineInformation = LineDetectionService.calculateLineInformation(textPositionSequences);
GapInformation gaps = GapDetectionService.findGapsInLines(textPositionSequences, tableBBox);
LineInformation lineInformation = LineDetectionService.calculateLineInformation(words);
GapInformation gaps = GapDetectionService.findGapsInLines(words, tableBBox);
List<Rectangle2D> gapsAcrossLines = GapsAcrossLinesService.detectXGapsAcrossLines(gaps, tableBBox);
List<Double> columnXCoords = gapsAcrossLines.stream().map(RectangularShape::getCenterX).toList();
int colCount = gapsAcrossLines.size();
@@ -7,7 +7,7 @@ import java.util.List;
import com.knecon.fforesight.service.layoutparser.processor.model.GapInformation;
import com.knecon.fforesight.service.layoutparser.processor.model.LineInformation;
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -19,37 +19,37 @@ 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 LineInformation calculateLineInformation(List<TextPositionSequence> sortedTextPositionSequences) {
public LineInformation calculateLineInformation(List<Word> sortedWords) {
if (sortedTextPositionSequences.isEmpty()) {
if (sortedWords.isEmpty()) {
return LineFactory.init().build();
}
return buildLineInformation(sortedTextPositionSequences);
return buildLineInformation(sortedWords);
}
public List<List<Rectangle2D>> findLinesWithGaps(List<TextPositionSequence> sortedTextPositionSequences, Rectangle2D mainBodyTextFrame) {
public List<List<Rectangle2D>> findLinesWithGaps(List<Word> sortedWords, Rectangle2D mainBodyTextFrame) {
return calculateLineInformation(sortedTextPositionSequences).getBBoxWithGapsByLines();
return calculateLineInformation(sortedWords).getBBoxWithGapsByLines();
}
public List<List<TextPositionSequence>> orderByLines(List<TextPositionSequence> sortedTextPositionSequences, Rectangle2D mainBodyTextFrame) {
public List<List<Word>> orderByLines(List<Word> sortedWords, Rectangle2D mainBodyTextFrame) {
return calculateLineInformation(sortedTextPositionSequences).getSequencesByLines();
return calculateLineInformation(sortedWords).getSequencesByLines();
}
private static LineInformation buildLineInformation(List<TextPositionSequence> sortedTextPositionSequences) {
private static LineInformation buildLineInformation(List<Word> sortedWords) {
final double avgTextPositionHeight = getAvgTextPositionHeight(sortedTextPositionSequences);
final double avgTextPositionHeight = getAvgTextPositionHeight(sortedWords);
LineFactory lineFactory = LineFactory.init();
var previousTextPosition = sortedTextPositionSequences.get(0);
var previousTextPosition = sortedWords.get(0);
lineFactory.addToCurrentLine(previousTextPosition);
for (TextPositionSequence currentTextPosition : sortedTextPositionSequences.subList(1, sortedTextPositionSequences.size())) {
for (Word currentTextPosition : sortedWords.subList(1, sortedWords.size())) {
if (isNewLine(currentTextPosition, previousTextPosition, avgTextPositionHeight) || isSplitByOrientation(currentTextPosition, previousTextPosition)) {
lineFactory.startNewLine();
} else if (isXGap(currentTextPosition, previousTextPosition, avgTextPositionHeight)) {
@@ -63,25 +63,25 @@ public class LineDetectionService {
}
private static double getAvgTextPositionHeight(List<TextPositionSequence> textPositionSequences) {
private static double getAvgTextPositionHeight(List<Word> words) {
return textPositionSequences.stream().mapToDouble(TextPositionSequence::getHeight).average().orElseThrow();
return words.stream().mapToDouble(Word::getHeight).average().orElseThrow();
}
private static boolean isXGap(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition, double avgTextPositionHeight) {
private static boolean isXGap(Word currentTextPosition, Word previousTextPosition, double avgTextPositionHeight) {
return Math.abs(previousTextPosition.getMaxXDirAdj() - currentTextPosition.getXDirAdj()) > (avgTextPositionHeight * X_GAP_FACTOR);
}
private static boolean isSplitByOrientation(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition) {
private static boolean isSplitByOrientation(Word currentTextPosition, Word previousTextPosition) {
return !previousTextPosition.getDir().equals(currentTextPosition.getDir());
}
private static boolean isNewLine(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition, double avgTextPositionHeight) {
private static boolean isNewLine(Word currentTextPosition, Word previousTextPosition, double avgTextPositionHeight) {
return Math.abs(previousTextPosition.getYDirAdj() - currentTextPosition.getYDirAdj()) > avgTextPositionHeight;
}
@@ -96,13 +96,13 @@ public class LineDetectionService {
List<List<Rectangle2D>> bBoxWithGapsByLines;
List<Rectangle2D> bBoxWithGapsInCurrentLine;
List<List<List<TextPositionSequence>>> sequencesWithGapsByLines;
List<List<TextPositionSequence>> sequencesWithGapsInCurrentLine;
List<List<List<Word>>> sequencesWithGapsByLines;
List<List<Word>> sequencesWithGapsInCurrentLine;
List<TextPositionSequence> currentSequencesWithoutGaps;
List<Word> currentSequencesWithoutGaps;
List<List<TextPositionSequence>> sequencesByLines;
List<TextPositionSequence> sequencesInCurrentLine;
List<List<Word>> sequencesByLines;
List<Word> sequencesInCurrentLine;
List<List<Rectangle2D>> xGaps;
List<List<Rectangle2D>> yGaps;
@@ -116,14 +116,14 @@ public class LineDetectionService {
List<Rectangle2D> bBoxWithGapsInCurrentLine = new LinkedList<>();
bBoxWithGapsByLines.add(bBoxWithGapsInCurrentLine);
List<List<List<TextPositionSequence>>> sequencesWithGapsByLines = new LinkedList<>();
List<List<TextPositionSequence>> sequencesWithGapsInCurrentLine = new LinkedList<>();
List<List<List<Word>>> sequencesWithGapsByLines = new LinkedList<>();
List<List<Word>> sequencesWithGapsInCurrentLine = new LinkedList<>();
sequencesWithGapsByLines.add(sequencesWithGapsInCurrentLine);
List<TextPositionSequence> currentSequencesWithoutGaps = new LinkedList<>();
List<Word> currentSequencesWithoutGaps = new LinkedList<>();
sequencesWithGapsInCurrentLine.add(currentSequencesWithoutGaps);
List<List<TextPositionSequence>> sequencesByLines = new LinkedList<>();
List<TextPositionSequence> sequencesInCurrentLine = new LinkedList<>();
List<List<Word>> sequencesByLines = new LinkedList<>();
List<Word> sequencesInCurrentLine = new LinkedList<>();
sequencesByLines.add(sequencesInCurrentLine);
return new LineFactory(lineBBox,
@@ -178,13 +178,13 @@ public class LineDetectionService {
}
private Rectangle2D textPositionBBox(List<TextPositionSequence> textPositionSequences) {
private Rectangle2D textPositionBBox(List<Word> words) {
return RectangleTransformations.rectangle2DBBox(textPositionSequences.stream().map(TextPositionSequence::getBBox).toList());
return RectangleTransformations.rectangle2DBBox(words.stream().map(Word::getBBox).toList());
}
public void addToCurrentLine(TextPositionSequence current) {
public void addToCurrentLine(Word current) {
sequencesInCurrentLine.add(current);
currentSequencesWithoutGaps.add(current);
@@ -13,7 +13,7 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.springframework.core.io.ClassPathResource;
import com.knecon.fforesight.service.layoutparser.processor.model.PageContents;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.services.parsing.PDFLinesTextStripper;
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
@@ -40,7 +40,7 @@ public class PageContentExtractor {
stripper.setPdpage(pdPage);
stripper.getText(pdDocument);
Map<Float, List<TextPositionSequence>> sortedTextPositionSequencesPerDir = stripper.getTextPositionSequences()
Map<Float, List<Word>> sortedTextPositionSequencesPerDir = stripper.getWords()
.stream()
.collect(Collectors.groupingBy(textPositionSequence -> textPositionSequence.getDir().getDegrees()));
@@ -57,7 +57,7 @@ public class PageContentExtractor {
}
public List<TextPositionSequence> sortByDirAccordingToPageRotation(Map<Float, List<TextPositionSequence>> sortedTextPositionSequencesPerDir, int rotation) {
public List<Word> sortByDirAccordingToPageRotation(Map<Float, List<Word>> sortedTextPositionSequencesPerDir, int rotation) {
LinkedList<Float> sortedKeys = new LinkedList<>(sortedTextPositionSequencesPerDir.keySet().stream().sorted().toList());
@@ -14,9 +14,9 @@ public class PageInformationService {
public PageInformation build(PageContents pageContents) {
LineInformation lineInformation = LineDetectionService.calculateLineInformation(pageContents.getSortedTextPositionSequences());
LineInformation lineInformation = LineDetectionService.calculateLineInformation(pageContents.getSortedWords());
Rectangle2D mainBodyTextFrame = MainBodyTextFrameExtractionService.calculateMainBodyTextFrame(lineInformation);
GapInformation gapInformation = GapDetectionService.findGapsInLines(pageContents.getSortedTextPositionSequences(), mainBodyTextFrame);
GapInformation gapInformation = GapDetectionService.findGapsInLines(pageContents.getSortedWords(), mainBodyTextFrame);
return new PageInformation(pageContents, lineInformation, mainBodyTextFrame, gapInformation);
}
@@ -5,7 +5,7 @@ import java.util.List;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextDirection;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.experimental.UtilityClass;
@@ -17,9 +17,9 @@ public class TextRulingsClassifier {
private final static double TEXT_BBOX_THRESHOLD_FACTOR = 0.15; // multiplied with text width then subtracted from word width. If ruling covers this width, it is considered as strikethrough/underline.
public static void classifyUnderlinedAndStrikethroughText(List<TextPositionSequence> words, CleanRulings cleanRulings) {
public static void classifyUnderlinedAndStrikethroughText(List<Word> words, CleanRulings cleanRulings) {
for (TextPositionSequence word : words) {
for (Word word : words) {
if (word.getDir().equals(TextDirection.ZERO) || word.getDir().equals(TextDirection.HALF_CIRCLE)) {
handleHorizontalText(cleanRulings, word);
} else {
@@ -29,7 +29,7 @@ public class TextRulingsClassifier {
}
private static void handleVerticalText(CleanRulings cleanRulings, TextPositionSequence word) {
private static void handleVerticalText(CleanRulings cleanRulings, Word word) {
float lowerY = (float) (word.getBBoxPdf().getMinY() + TEXT_BBOX_THRESHOLD_FACTOR * word.getWidth());
float upperY = (float) (word.getBBoxPdf().getMaxY() - TEXT_BBOX_THRESHOLD_FACTOR * word.getWidth());
@@ -63,7 +63,7 @@ public class TextRulingsClassifier {
}
private static void handleHorizontalText(CleanRulings cleanRulings, TextPositionSequence word) {
private static void handleHorizontalText(CleanRulings cleanRulings, Word word) {
float leftX = (float) (word.getBBoxPdf().getMinX() + TEXT_BBOX_THRESHOLD_FACTOR * word.getWidth());
float rightX = (float) (word.getBBoxPdf().getMaxX() - TEXT_BBOX_THRESHOLD_FACTOR * word.getWidth());
@@ -11,12 +11,14 @@ import org.apache.commons.text.similarity.LevenshteinDistance;
import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngineProto.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
import com.knecon.fforesight.service.layoutparser.processor.model.SectionIdentifier;
import com.knecon.fforesight.service.layoutparser.processor.model.outline.OutlineObject;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.utils.PageInformation;
import com.knecon.fforesight.service.layoutparser.processor.utils.TextNormalizationUtilities;
import lombok.Data;
@@ -27,6 +29,24 @@ public class BlockificationPostprocessingService {
private static final float STRING_SIMILARITY_THRESHOLD = 0.1f;
public void findHeadlinesFromOutline(ClassificationDocument classificationDocument, int pageNumber, ClassificationPage classificationPage, PageInformation pageInformation) {
OutlineObject lastProcessedOutlineObject = null;
List<OutlineObject> outlineObjects = classificationDocument.getOutlineObjectTree().getOutlineObjectsPerPage().getOrDefault(pageNumber, new ArrayList<>());
OutlineObject notFoundOutlineObject = null;
if (lastProcessedOutlineObject != null && !lastProcessedOutlineObject.isFound()) {
lastProcessedOutlineObject.resetPoint();
notFoundOutlineObject = lastProcessedOutlineObject;
}
if (!outlineObjects.isEmpty()) {
classificationPage.setOutlineObjects(outlineObjects);
lastProcessedOutlineObject = sanitizeOutlineBlocks(classificationPage, notFoundOutlineObject);
}
classificationDocument.getLayoutDebugLayer().addOutlineObjects(outlineObjects, pageInformation);
}
public OutlineObject sanitizeOutlineBlocks(ClassificationPage classificationPage, OutlineObject notFoundOutlineObject) {
List<OutlineObject> outlineObjects = classificationPage.getOutlineObjects();
@@ -222,14 +242,14 @@ public class BlockificationPostprocessingService {
headline = sectionIdentifier + headline;
}
WordSequenceResult wordSequenceResult = findWordSequence(blockToSplit.getSequences(), headline);
WordSequenceResult wordSequenceResult = findWordSequence(blockToSplit.getWords(), headline);
if (wordSequenceResult.inSequence.isEmpty() && !headline.equals(title)) {
wordSequenceResult = findWordSequence(blockToSplit.getSequences(), title);
wordSequenceResult = findWordSequence(blockToSplit.getWords(), title);
}
boolean modifiedBlockToSplit = false;
if (!wordSequenceResult.inSequence.isEmpty()) {
blockToSplit.setSequences(wordSequenceResult.inSequence);
blockToSplit.setWords(wordSequenceResult.inSequence);
blockToSplit.recalculateBBox();
modifiedBlockToSplit = true;
}
@@ -250,19 +270,19 @@ public class BlockificationPostprocessingService {
}
private static WordSequenceResult findWordSequence(List<TextPositionSequence> textPositionSequences, String text) {
private static WordSequenceResult findWordSequence(List<Word> words, String text) {
String target = sanitizeString(text);
List<TextPositionSequence> inSequence = new ArrayList<>();
List<TextPositionSequence> preSequence = new ArrayList<>();
List<TextPositionSequence> postSequence = new ArrayList<>();
List<Word> inSequence = new ArrayList<>();
List<Word> preSequence = new ArrayList<>();
List<Word> postSequence = new ArrayList<>();
StringBuilder currentSequence = new StringBuilder();
if (target.isBlank()) {
return new WordSequenceResult();
}
for (TextPositionSequence sequence : textPositionSequences) {
for (Word sequence : words) {
currentSequence.append(sanitizeString(sequence.toString()));
inSequence.add(sequence);
@@ -274,10 +294,10 @@ public class BlockificationPostprocessingService {
int index = 0;
String toRemove = currentSequence.substring(0, currentSequence.length() - target.length());
TextPositionSequence next = inSequence.get(index);
Word next = inSequence.get(index);
while (currentSequence.length() - next.length() >= target.length()) {
TextPositionSequence removed = inSequence.remove(index);
Word removed = inSequence.remove(index);
currentSequence.delete(0, removed.toString().length());
preSequence.add(removed);
@@ -306,7 +326,7 @@ public class BlockificationPostprocessingService {
}
if (currentSequence.toString().equals(target)) {
postSequence.addAll(textPositionSequences.subList(textPositionSequences.indexOf(sequence) + 1, textPositionSequences.size()));
postSequence.addAll(words.subList(words.indexOf(sequence) + 1, words.size()));
return new WordSequenceResult(inSequence, preSequence, postSequence);
}
}
@@ -316,10 +336,10 @@ public class BlockificationPostprocessingService {
}
private static SplitSequenceResult splitSequence(TextPositionSequence sequence, String toRemove) {
private static SplitSequenceResult splitSequence(Word sequence, String toRemove) {
TextPositionSequence in = null;
TextPositionSequence out;
Word in = null;
Word out;
String currentSequence = sequence.toString().toLowerCase(Locale.ROOT);
int index = currentSequence.indexOf(toRemove);
@@ -329,17 +349,17 @@ public class BlockificationPostprocessingService {
if (index > 0) {
in = createSubSequence(sequence, 0, index);
} else if (endIndex < sequence.getTextPositions().size()) {
in = createSubSequence(sequence, endIndex, sequence.getTextPositions().size());
} else if (endIndex < sequence.length()) {
in = createSubSequence(sequence, endIndex, sequence.length());
}
return new SplitSequenceResult(in, out);
}
private static TextPositionSequence createSubSequence(TextPositionSequence sequence, int start, int end) {
private static Word createSubSequence(Word sequence, int start, int end) {
TextPositionSequence newSeq = new TextPositionSequence(new ArrayList<>(sequence.getTextPositions().subList(start, end)), sequence.getPage());
Word newSeq = new Word(new ArrayList<>(sequence.getCharacters().subList(start, end)), sequence.getPage());
newSeq.setParagraphStart(sequence.isParagraphStart());
return newSeq;
}
@@ -354,10 +374,10 @@ public class BlockificationPostprocessingService {
List<TextPageBlock> mergedBlocks = new ArrayList<>();
for (TextPageBlock textPageBlock : blocksToMerge.subList(1, blocksToMerge.size())) {
if (firstBlock != null && !firstBlock.getSequences().isEmpty()) {
if (firstBlock != null && !firstBlock.getWords().isEmpty()) {
if (textPageBlock.getDir() == firstBlock.getDir()) {
firstBlock.addAll(textPageBlock.getSequences());
firstBlock.addAll(textPageBlock.getWords());
mergedBlocks.add(textPageBlock);
}
}
@@ -496,12 +516,12 @@ public class BlockificationPostprocessingService {
public static class WordSequenceResult {
public List<TextPositionSequence> inSequence;
public List<TextPositionSequence> preSequence;
public List<TextPositionSequence> postSequence;
public List<Word> inSequence;
public List<Word> preSequence;
public List<Word> postSequence;
public WordSequenceResult(List<TextPositionSequence> inSequence, List<TextPositionSequence> preSequence, List<TextPositionSequence> postSequence) {
public WordSequenceResult(List<Word> inSequence, List<Word> preSequence, List<Word> postSequence) {
this.inSequence = inSequence;
this.preSequence = preSequence;
@@ -522,7 +542,7 @@ public class BlockificationPostprocessingService {
}
public record SplitSequenceResult(TextPositionSequence in, TextPositionSequence out) {
public record SplitSequenceResult(Word in, Word out) {
}
@@ -14,7 +14,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.Classification
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutDebugLayer;
import lombok.RequiredArgsConstructor;
@@ -30,7 +30,7 @@ public class DocstrumBlockificationService {
static final float THRESHOLD = 1f;
public ClassificationPage blockify(List<TextPositionSequence> textPositions,
public ClassificationPage blockify(List<Word> textPositions,
CleanRulings rulings,
boolean xyOrder,
LayoutDebugLayer visualizations,
@@ -72,16 +72,16 @@ public class DocstrumBlockificationService {
List<AbstractPageBlock> abstractPageBlocks = new ArrayList<>();
zones.forEach(zone -> {
List<TextPositionSequence> textPositionSequences = new ArrayList<>();
List<Word> words = new ArrayList<>();
zone.getLines()
.forEach(line -> {
line.getWords()
.forEach(word -> {
textPositionSequences.add(new TextPositionSequence(word.getTextPositions(), word.getPage()));
words.add(new Word(word.getCharacters(), word.getPage()));
});
});
abstractPageBlocks.add(buildTextBlock(textPositionSequences, 0));
abstractPageBlocks.add(buildTextBlock(words, 0));
});
return abstractPageBlocks;
@@ -102,7 +102,7 @@ public class DocstrumBlockificationService {
}
TextPageBlock current = (TextPageBlock) block;
if (previous != null && !previous.getSequences().isEmpty()) {
if (previous != null && !previous.getWords().isEmpty()) {
if (current.getDir() != previous.getDir() || usedRulings.lineBetween(current, previous)) {
previous = current;
@@ -182,8 +182,8 @@ public class DocstrumBlockificationService {
private TextPageBlock combineBlocksAndResetIterator(TextPageBlock previous, TextPageBlock current, ListIterator<AbstractPageBlock> itty, boolean toDuplicate) {
previous.addAll(current.getSequences());
previous = buildTextBlock(previous.getSequences(), 0);
previous.addAll(current.getWords());
previous = buildTextBlock(previous.getWords(), 0);
previous.setToDuplicate(toDuplicate);
if (current.getClassification() != null && previous.getClassification() == null) {
previous.setClassification(current.getClassification());
@@ -283,8 +283,8 @@ public class DocstrumBlockificationService {
if (current.getDir() == inner.getDir() && current.intersects(inner, yThreshold, xThreshold)) {
boolean toDuplicate = current.isToDuplicate() || inner.isToDuplicate();
current.addAll(inner.getSequences());
current = buildTextBlock(current.getSequences(), 0);
current.addAll(inner.getWords());
current = buildTextBlock(current.getWords(), 0);
current.setToDuplicate(toDuplicate);
blocks.set(i, null);
@@ -301,7 +301,7 @@ public class DocstrumBlockificationService {
}
public static TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList, int indexOnPage) {
public static TextPageBlock buildTextBlock(List<Word> wordBlockList, int indexOnPage) {
return new TextPageBlock(wordBlockList);
}
@@ -16,13 +16,14 @@ import com.knecon.fforesight.service.layoutparser.processor.model.Orientation;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
@SuppressWarnings("all")
@Service
public class DocuMineBlockificationService {
static final float THRESHOLD = 1f;
public static final double FONT_SIZE_CHANGE_RATIO = 0.15;
Pattern pattern = Pattern.compile("^(\\d{1,2}\\.){1,3}\\d{1,2}\\.?\\s[0-9A-Za-z ()-]{2,50}", Pattern.CASE_INSENSITIVE);
@@ -36,9 +37,9 @@ public class DocuMineBlockificationService {
* @param cleanRulings All rulings on a page
* @return Page object that contains the Textblock and text statistics.
*/
public ClassificationPage blockify(List<TextPositionSequence> textPositions, CleanRulings cleanRulings) {
public ClassificationPage blockify(List<Word> textPositions, CleanRulings cleanRulings) {
List<TextPositionSequence> chunkWords = new ArrayList<>();
List<Word> chunkWords = new ArrayList<>();
List<AbstractPageBlock> textPageBlocks = new ArrayList<>();
CleanRulings usedRulings = cleanRulings.withoutTextRulings();
@@ -47,11 +48,11 @@ public class DocuMineBlockificationService {
double maxX = 0;
double minY = 1000;
double maxY = 0;
TextPositionSequence prev = null;
Word prev = null;
boolean wasSplitted = false;
Double splitX1 = null;
for (TextPositionSequence word : textPositions) {
for (Word word : textPositions) {
boolean lineSeparation = prev != null && word.getYDirAdj() - prev.getMaxYDirAdj() > Math.min(word.getHeight(), prev.getHeight()) * 1.1;
boolean startFromTop = prev != null && word.getYDirAdj() < prev.getYDirAdj() - prev.getTextHeight();
@@ -60,11 +61,7 @@ public class DocuMineBlockificationService {
boolean newLineAfterSplit = prev != null && word.getYDirAdj() != prev.getYDirAdj() && wasSplitted && splitX1 != word.getXDirAdj();
boolean isSplitByRuling = prev != null && usedRulings.lineBetween(prev, word);
boolean splitByDir = prev != null && !prev.getDir().equals(word.getDir());
boolean splitByOtherFontAndOtherY = prev != null && Math.abs(prev.getMaxYDirAdj() - word.getMaxYDirAdj()) > word.getTextHeight() * 0.2 //
&& (word.getFontStyle().contains("bold") && !prev.getFontStyle().contains("bold")
|| prev.getFontStyle().contains("bold") && !word.getFontStyle().contains("bold")
|| Math.abs(prev.getFontSize() - word.getFontSize()) >= 1
|| Math.abs(word.getTextHeight() - prev.getTextHeight()) > 0.8);
boolean splitByOtherFontAndOtherY = prev != null && Math.abs(prev.getMaxYDirAdj() - word.getMaxYDirAdj()) > word.getTextHeight() * 0.2 && isFontChange(word, prev);
Matcher matcher = pattern.matcher(chunkWords.stream()
.collect(Collectors.joining(" ")).toString());
@@ -127,6 +124,15 @@ public class DocuMineBlockificationService {
}
private static boolean isFontChange(Word word, Word prev) {
return word.getFontStyle().contains("bold") && !prev.getFontStyle().contains("bold")
|| prev.getFontStyle().contains("bold") && !word.getFontStyle().contains("bold")
|| Math.abs(prev.getFontSize() - word.getFontSize()) >= FONT_SIZE_CHANGE_RATIO * Math.min(prev.getFontSize(), word.getFontSize())
|| Math.abs(word.getTextHeight() - prev.getTextHeight()) >= FONT_SIZE_CHANGE_RATIO * Math.min(prev.getTextHeight(), word.getTextHeight());
}
public void mergeblocks(ClassificationPage page, CleanRulings usedRulings, float xThreshold, float yThreshold) {
var blocks = page.getTextBlocks();
@@ -169,8 +175,8 @@ public class DocuMineBlockificationService {
.equals(inner.getClassification()))) {
boolean toDuplicate = current.isToDuplicate() || inner.isToDuplicate();
current.addAll(inner.getSequences());
current = buildTextBlock(current.getSequences(), 0);
current.addAll(inner.getWords());
current = buildTextBlock(current.getWords(), 0);
current.setClassification(inner.getClassification());
current.setToDuplicate(toDuplicate);
blocks.set(i, null);
@@ -193,7 +199,7 @@ public class DocuMineBlockificationService {
}
public static TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList, int indexOnPage) {
public static TextPageBlock buildTextBlock(List<Word> wordBlockList, int indexOnPage) {
return new TextPageBlock(wordBlockList);
}
@@ -11,7 +11,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.Classification
import com.knecon.fforesight.service.layoutparser.processor.model.Orientation;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutDebugLayer;
@SuppressWarnings("all")
@@ -30,20 +30,20 @@ public class RedactManagerBlockificationService {
* @param visualizations
* @return Page object that contains the Textblock and text statistics.
*/
public ClassificationPage blockify(List<TextPositionSequence> textPositions, CleanRulings cleanRulings, LayoutDebugLayer visualizations) {
public ClassificationPage blockify(List<Word> textPositions, CleanRulings cleanRulings, LayoutDebugLayer visualizations) {
CleanRulings usedRulings = cleanRulings.withoutTextRulings();
int indexOnPage = 0;
List<TextPositionSequence> chunkWords = new ArrayList<>();
List<Word> chunkWords = new ArrayList<>();
List<AbstractPageBlock> chunkBlockList = new ArrayList<>();
double minX = 1000, maxX = 0, minY = 1000, maxY = 0;
TextPositionSequence prev = null;
Word prev = null;
boolean wasSplitted = false;
Double splitX1 = null;
for (TextPositionSequence word : textPositions) {
for (Word word : textPositions) {
boolean lineSeparation = word.getYDirAdj() - maxY > word.getHeight() * 1.25;
boolean startFromTop = prev != null && word.getYDirAdj() < prev.getYDirAdj() - prev.getTextHeight();
@@ -81,7 +81,7 @@ public class ClarifyndClassificationService {
&& (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()) {
&& textBlock.getWords().get(0).getFontSize()>= textBlock.getMostPopularWordFontSize()) {
PageBlockType headlineType = HeadlineClassificationService.headlineClassByFontSize(textBlock, headlineFontSizes);
headlineClassificationService.classifyHeadline(textBlock, headlineType);
@@ -91,7 +91,7 @@ public class ClarifyndClassificationService {
&& 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.getWords().get(0).getFontSize() >= textBlock.getMostPopularWordFontSize()) {
PageBlockType headlineType = HeadlineClassificationService.headlineClassByFontSize(textBlock, headlineFontSizes);
headlineClassificationService.classifyHeadline(textBlock, headlineType);
@@ -15,7 +15,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;
@@ -153,6 +152,16 @@ public class DocuMineClassificationService {
&& !headlineWithSlashesMatches) {
setAsHeadline(headlineClassificationService, textBlock, document, headlineFontSizes);
// } else if (textBlock.getMostPopularWordFont().contains("bold")
// && greaterOrEqualFontThanPageAverage(textBlock, page)
// && textBlock.getWords().size() <= 6
// && PositionUtils.getApproxLineCount(textBlock) < 2.9
// && isAtLeast3Characters
// && charCount > textBlock.getText().length() * 0.75
// && !textBlock.getText().contains(":")
// && textBlock.getWidth() < page.getBodyTextFrame().getWidth() * 0.7) {
//
// setAsHeadline(headlineClassificationService, textBlock, document, headlineFontSizes);
} else if (!listIdentifiers.isEmpty()) {
textBlock.setClassification(PageBlockType.LIST_ITEM);
@@ -11,7 +11,7 @@ import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.layoutparser.processor.model.text.AbstractBlockOnPage;
import com.knecon.fforesight.service.layoutparser.processor.model.text.ListIdentifier;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
@Service
public class ListItemClassificationService {
@@ -71,7 +71,7 @@ public class ListItemClassificationService {
List<ListIdentifier> result = new LinkedList<>();
if (block.block() instanceof TextPageBlock textBlock) {
List<TextPositionSequence> sequences = textBlock.getSequences();
List<Word> sequences = textBlock.getWords();
for (int i = 0; i < sequences.size(); i++) {
if (i != 0 && sequences.get(i - 1).getXDirAdj() < sequences.get(i).getXDirAdj()) {
@@ -79,8 +79,8 @@ public class ListItemClassificationService {
continue;
}
TextPositionSequence sequence = sequences.get(i);
List<TextPositionSequence> wordsAtStartOfLine = new ArrayList<>(3);
Word sequence = sequences.get(i);
List<Word> wordsAtStartOfLine = new ArrayList<>(3);
int end = Math.min(sequences.size(), i + 3);
for (int j = i; j < end; j++) {
if (sequences.get(j).intersectsYDirAdj(sequence, 2)) {
@@ -0,0 +1,7 @@
package com.knecon.fforesight.service.layoutparser.processor.services.classification;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
public record NumberWord(Word word, int number) {
}
@@ -11,7 +11,6 @@ import com.knecon.fforesight.service.layoutparser.processor.model.Classification
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.ImageType;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.utils.MarkedContentUtils;
import com.knecon.fforesight.service.layoutparser.processor.utils.PositionUtils;
import lombok.RequiredArgsConstructor;
@@ -95,7 +94,7 @@ public class RedactManagerClassificationService {
&& (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()) {
&& textBlock.getWords().get(0).getFontSize() >= textBlock.getMostPopularWordFontSize()) {
PageBlockType headlineType = HeadlineClassificationService.headlineClassByFontSize(textBlock, headlineFontSizes);
headlineClassificationService.classifyHeadline(textBlock, headlineType);
@@ -105,7 +104,7 @@ public class RedactManagerClassificationService {
&& 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.getWords().get(0).getFontSize() >= textBlock.getMostPopularWordFontSize()) {
PageBlockType headlineType = HeadlineClassificationService.headlineClassByFontSize(textBlock, headlineFontSizes);
headlineClassificationService.classifyHeadline(textBlock, headlineType);
@@ -14,6 +14,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
@@ -23,10 +24,11 @@ import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBl
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
import com.knecon.fforesight.service.layoutparser.processor.model.SectionIdentifier;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextBlockOnPage;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequenceComparator;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TocNumberComparator;
import com.knecon.fforesight.service.layoutparser.processor.utils.TextNormalizationUtilities;
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutDebugLayer;
@@ -69,11 +71,14 @@ public class TableOfContentsClassificationService {
private int identifyTOCItems(int start, List<TextBlockOnPage> textBlocks, ClassificationDocument document) {
if (start >= textBlocks.size()) {
return start;
}
ClassificationPage startPage = textBlocks.get(start).page();
List<TextBlockOnPage> initialLookAhead = textBlocks.subList(start, Math.min(start + SURROUNDING_BLOCKS_RADIUS, textBlocks.size()));
HashMap<TextPositionSequence, TextBlockOnPage> lookup = new HashMap<>();
List<TextPositionSequence> numbers = extractNumbers(initialLookAhead, lookup, document.getPages().size());
TocNumberFinder tocNumberFinder = new TocNumberFinder(numbers, lookup);
HashMap<NumberWord, TextBlockOnPage> numberToBlockLookup = new HashMap<>();
List<NumberWord> numbers = extractNumbers(initialLookAhead, numberToBlockLookup, document.getPages().size());
TocNumberFinder tocNumberFinder = new TocNumberFinder(numbers, numberToBlockLookup);
int lastCandidate = start;
for (int i = start; i < Math.min(lastCandidate + SURROUNDING_BLOCKS_RADIUS, textBlocks.size()); i++) {
@@ -93,28 +98,28 @@ public class TableOfContentsClassificationService {
break;
}
List<TextPositionSequence> numbersFromBlock = extractNumbers(textBlockOnPage, lookup, document.getPages().size());
List<NumberWord> numbersFromBlock = extractNumbers(textBlockOnPage, numberToBlockLookup, document.getPages().size());
List<TextPositionSequence> currentRightmostCluster = tocNumberFinder.getCurrentRightmostCluster();
List<NumberWord> currentRightmostCluster = tocNumberFinder.getCurrentRightmostCluster();
if (currentRightmostCluster.size() < MINIMUM_MATCHES) {
log.debug("No numbers indicating a table of contents here.");
return start;
}
if (anyIntersection(currentRightmostCluster, numbersFromBlock, lookup)) {
if (anyIntersection(currentRightmostCluster, numbersFromBlock, numberToBlockLookup)) {
lastCandidate = i;
numbersFromBlock.forEach(tocNumberFinder::add);
}
}
addVisualization(document.getLayoutDebugLayer(), tocNumberFinder, lookup);
Set<TextBlockOnPage> blocksWithNumberInCluster = tocNumberFinder.getCurrentRightmostCluster()
.stream()
.map(lookup::get)
.map(numberToBlockLookup::get)
.collect(Collectors.toSet());
addVisualization(document.getLayoutDebugLayer(), tocNumberFinder, numberToBlockLookup, blocksWithNumberInCluster, textBlocks.get(start - 1));
int lastConfirmed = start;
for (int i = start; i < lastCandidate + 1; i++) {
TextBlockOnPage textBlockOnPage = textBlocks.get(i);
@@ -132,18 +137,22 @@ public class TableOfContentsClassificationService {
}
private static void addVisualization(LayoutDebugLayer layoutDebugLayer, TocNumberFinder tocNumberFinder, Map<TextPositionSequence, TextBlockOnPage> lookup) {
private static void addVisualization(LayoutDebugLayer layoutDebugLayer,
TocNumberFinder tocNumberFinder,
Map<NumberWord, TextBlockOnPage> lookup,
Set<TextBlockOnPage> blocksWithNumberInCluster,
TextBlockOnPage startingHeadline) {
tocNumberFinder.getCurrentRightmostCluster()
.stream()
.collect(Collectors.groupingBy(key -> lookup.get(key).page().getPageNumber()))
.forEach((pageNumber, number) -> layoutDebugLayer.addTocPages(number, pageNumber));
layoutDebugLayer.addTocBlocks(blocksWithNumberInCluster);
layoutDebugLayer.addTocBlocks(Set.of(startingHeadline));
}
private static boolean anyIntersection(Collection<TextPositionSequence> numbers1,
Collection<TextPositionSequence> numbers2,
Map<TextPositionSequence, TextBlockOnPage> lookup) {
private static boolean anyIntersection(Collection<NumberWord> numbers1, Collection<NumberWord> numbers2, Map<NumberWord, TextBlockOnPage> lookup) {
return numbers1.stream()
.anyMatch(numberFromCluster -> numbers2.stream()
@@ -151,9 +160,9 @@ public class TableOfContentsClassificationService {
}
private static List<TextPositionSequence> extractNumbers(List<TextBlockOnPage> textBlocks, Map<TextPositionSequence, TextBlockOnPage> lookup, int numberOfPages) {
private static List<NumberWord> extractNumbers(List<TextBlockOnPage> textBlocks, Map<NumberWord, TextBlockOnPage> lookup, int numberOfPages) {
List<TextPositionSequence> blocks = new LinkedList<>();
List<NumberWord> blocks = new LinkedList<>();
for (TextBlockOnPage textBlock : textBlocks) {
blocks.addAll(extractNumbers(textBlock, lookup, numberOfPages));
}
@@ -161,30 +170,40 @@ public class TableOfContentsClassificationService {
}
private static List<TextPositionSequence> extractNumbers(TextBlockOnPage textBlock, Map<TextPositionSequence, TextBlockOnPage> lookup, int numberOfPages) {
private static List<NumberWord> extractNumbers(TextBlockOnPage textBlock, Map<NumberWord, TextBlockOnPage> lookup, int numberOfPages) {
List<TextPositionSequence> blocks = new LinkedList<>();
List<NumberWord> blocks = new LinkedList<>();
TextPageBlock block = textBlock.textBlock();
List<TextPositionSequence> sequences = block.getSequences();
for (int i = 0; i < sequences.size(); i++) {
List<Word> words = block.getWords();
for (int i = 0; i < words.size(); i++) {
TextPositionSequence word = sequences.get(i);
if (!NUMERIC.matcher(word).matches()) {
Word word = words.get(i);
if (!wordIsEndOfLine(i, words)) {
continue;
}
if (AMOUNT_PATTERN.matcher(getSurroundingString(i, sequences)).matches()) {
if (AMOUNT_PATTERN.matcher(getSurroundingString(i, words)).matches()) {
continue;
}
Matcher matcher = SectionIdentifier.numericalIdentifierPattern.matcher(word.toString());
if (matcher.find() && matcher.group(2) != null) {
continue;
}
Matcher numberFinder = NUMERIC.matcher(word);
if (!numberFinder.find() || word.length() > 5) {
continue;
}
try {
int pageNumber = Integer.parseInt(word.toString());
int pageNumber = Integer.parseInt(numberFinder.group());
if (0 >= pageNumber || pageNumber > numberOfPages) {
continue;
}
lookup.put(word, textBlock);
blocks.add(word);
NumberWord numberWord = new NumberWord(word, pageNumber);
lookup.put(numberWord, textBlock);
blocks.add(numberWord);
} catch (NumberFormatException e) {
log.debug("That wasn't a number! Should not happen, due to numeric check beforehand.");
}
@@ -193,23 +212,34 @@ public class TableOfContentsClassificationService {
}
private static CharSequence getSurroundingString(int i, List<TextPositionSequence> sequences) {
private static boolean wordIsEndOfLine(int i, List<Word> words) {
if (i == words.size() - 1) {
return true;
}
Word word = words.get(i);
Word nextWord = words.get(i + 1);
return !nextWord.rightOf(word);
}
private static CharSequence getSurroundingString(int i, List<Word> sequences) {
int end = Math.min(i + 5, sequences.size());
return sequences.subList(i, end)
.stream()
.map(TextPositionSequence::toString)
.map(Word::toString)
.collect(Collectors.joining(" "));
}
private static boolean matches(TextPositionSequence number1, TextPositionSequence number2, Map<TextPositionSequence, TextBlockOnPage> lookup) {
private static boolean matches(NumberWord number1, NumberWord number2, Map<NumberWord, TextBlockOnPage> lookup) {
if (number1.getDir() != number2.getDir()) {
if (number1.word().getDir() != number2.word().getDir()) {
return false;
}
return number1.intersectsXDirAdj(number2, INTERSECTION_TOLERANCE);
return number1.word().intersectsXDirAdj(number2.word(), INTERSECTION_TOLERANCE);
}
@@ -247,11 +277,11 @@ public class TableOfContentsClassificationService {
private static class TocNumberFinder {
final UnionFind<TextPositionSequence> numberClusters;
final HashMap<TextPositionSequence, TextBlockOnPage> lookup;
final UnionFind<NumberWord> numberClusters;
final HashMap<NumberWord, TextBlockOnPage> lookup;
TocNumberFinder(List<TextPositionSequence> blocks, HashMap<TextPositionSequence, TextBlockOnPage> lookup) {
TocNumberFinder(List<NumberWord> blocks, HashMap<NumberWord, TextBlockOnPage> lookup) {
this.numberClusters = new UnionFind<>(new HashSet<>(blocks));
for (int i = 0; i < blocks.size(); i++) {
@@ -265,14 +295,14 @@ public class TableOfContentsClassificationService {
}
public void add(TextPositionSequence number) {
public void add(NumberWord number) {
if (numberClusters.getElements().contains(number)) {
return;
}
numberClusters.addElement(number);
for (TextPositionSequence element : numberClusters.getElements()) {
for (NumberWord element : numberClusters.getElements()) {
if (matches(number, element, lookup)) {
numberClusters.union(element, number);
}
@@ -280,73 +310,100 @@ public class TableOfContentsClassificationService {
}
public List<TextPositionSequence> getCurrentRightmostCluster() {
public List<NumberWord> getCurrentRightmostCluster() {
return numberClusters.getGroups()
.stream()
.filter(cluster -> cluster.size() > MINIMUM_MATCHES)
.map(cluster -> cluster.stream()
.sorted(new TextPositionSequenceComparator(lookup))
.sorted(new TocNumberComparator(lookup))
.toList())
.map(this::removeOutliers)
// .map(this::filterByMinimumDensity)
.map(this::removeOnNonConsecutivePages)
.map(this::filterByWordNearTopOfPage)
.filter(cluster -> cluster.size() > MINIMUM_MATCHES)
.max(Comparator.comparingDouble(cluster -> cluster.get(0).getBBox().getMaxX())).orElse(Collections.emptyList());
.max(Comparator.comparingDouble(cluster -> cluster.get(0).word().getBBox().getMaxX())).orElse(Collections.emptyList());
}
// does not seem to be doing much, ideally instead of using the height of the blocks i would like to use the height, beginning from the MainBody top,
// but as the MainBody is often wrong, this results in some numbers being discarded even though they are correct.
// private List<TextPositionSequence> filterByMinimumDensity(List<TextPositionSequence> numbers) {
//
// Map<ClassificationPage, List<TextPositionSequence>> clustersPerPage = numbers.stream()
// .collect(Collectors.groupingBy(number -> lookup.get(number).page()));
//
// List<TextPositionSequence> result = new ArrayList<>(numbers.size());
// clustersPerPage.keySet()
// .stream()
// .sorted(Comparator.comparingInt(ClassificationPage::getPageNumber))
// .forEach(page -> {
// var numbersOnPage = clustersPerPage.get(page);
//
// double height = numbersOnPage.stream()
// .map(BoundingBox::getBBox)
// .collect(RectangleTransformations.collectBBox()).getHeight();
//
// double count = numbersOnPage.size();
//
// if ((count / height) >= (DENSITY_THRESHOLD_COUNT / page.getPageHeight())) {
// result.addAll(numbers);
// }
// });
// return result;
// }
private List<NumberWord> removeOnNonConsecutivePages(List<NumberWord> numbers) {
public List<TextPositionSequence> removeOutliers(List<TextPositionSequence> numbers) {
List<TextPositionSequence> result = new ArrayList<>();
List<NumberWord> result = new ArrayList<>();
result.add(numbers.get(0));
for (int i = 1; i < numbers.size(); i++) {
int prev = getPageNumber(numbers, i - 1);
int curr = getPageNumber(numbers, i);
if (Math.abs(prev - curr) > 1) {
break;
} else {
result.add(numbers.get(i));
}
}
return result;
}
private int getPageNumber(List<NumberWord> numbers, int i) {
return lookup.get(numbers.get(i)).page().getPageNumber();
}
private List<NumberWord> filterByWordNearTopOfPage(List<NumberWord> numbers) {
List<NumberWord> result = new ArrayList<>();
result.add(numbers.get(0));
for (int i = 1; i < numbers.size(); i++) {
NumberWord prev = numbers.get(i - 1);
NumberWord curr = numbers.get(i);
ClassificationPage prevPage = lookup.get(prev).page();
ClassificationPage currPage = lookup.get(curr).page();
if (prevPage.equals(currPage)) {
result.add(curr);
} else if (curr.word().getBBox().getMinY() < currPage.getPageHeight() * 0.33) {
result.add(curr);
}
}
return result;
}
public List<NumberWord> removeOutliers(List<NumberWord> numbers) {
List<NumberWord> confirmedClusterNumbers = new ArrayList<>();
confirmedClusterNumbers.add(numbers.get(0));
for (int i = 1; i < numbers.size() - 1; i++) {
int prev = getNumberAsInt(numbers, i - 1);
int curr = getNumberAsInt(numbers, i);
int next = getNumberAsInt(numbers, i + 1);
if (!(curr <= prev || curr >= next) || !isBetterWithout(numbers, i)) {
result.add(numbers.get(i));
confirmedClusterNumbers.add(numbers.get(i));
}
}
if (getNumberAsInt(numbers, numbers.size() - 1) >= getNumberAsInt(numbers, Math.max(0, numbers.size() - 2))) {
result.add(numbers.get(numbers.size() - 1));
if (getNumberAsInt(numbers, numbers.size() - 1) >= getLatestNumber(confirmedClusterNumbers)) {
confirmedClusterNumbers.add(numbers.get(numbers.size() - 1));
}
return result;
return confirmedClusterNumbers;
}
private static int getLatestNumber(List<NumberWord> confirmedClusterNumbers) {
return confirmedClusterNumbers.get(confirmedClusterNumbers.size() - 1).number();
}
// Helper method to check if removing the current number results in a better order
public static boolean isBetterWithout(List<TextPositionSequence> numbers, int i) {
public static boolean isBetterWithout(List<NumberWord> numbers, int i) {
if (i == 0 || i == numbers.size() - 1) {
return false;
@@ -362,9 +419,9 @@ public class TableOfContentsClassificationService {
}
private static int getNumberAsInt(List<TextPositionSequence> numbers, int i) {
private static int getNumberAsInt(List<NumberWord> numbers, int i) {
return Integer.parseInt(numbers.get(i).toString());
return numbers.get(i).number();
}
}
@@ -16,7 +16,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeTypeProto;
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
@@ -82,9 +82,9 @@ public class DocumentGraphFactory {
documentGraph.streamAllSubNodes()
.filter(SemanticNode::isLeaf)
.filter(node -> !node.getType().equals(NodeType.HEADER))
.filter(node -> !node.getType().equals(NodeType.FOOTER))
.filter(node -> !node.getType().equals(NodeType.IMAGE))
.filter(node -> !node.getType().equals(NodeTypeProto.NodeType.HEADER))
.filter(node -> !node.getType().equals(NodeTypeProto.NodeType.FOOTER))
.filter(node -> !node.getType().equals(NodeTypeProto.NodeType.IMAGE))
.map(SemanticNode::getTextBlock)
.map(TextBlock::getAtomicTextBlocks)
.flatMap(Collection::stream)
@@ -97,12 +97,12 @@ public class DocumentGraphFactory {
for (TableOfContentItem tocItem : classificationDocument.getTableOfContents()) {
GenericSemanticNode parent = tocItem.getParent() == null ? null : tocItem.getParent().getSection();
Optional<GenericSemanticNode> section = SectionNodeFactory.addSection(layoutParsingType,
parent,
tocItem.getChildren().isEmpty(),
tocItem.getNonEmptySectionBlocks(),
tocItem.getImages(),
context,
document);
parent,
tocItem.getChildren().isEmpty(),
tocItem.getNonEmptySectionBlocks(),
tocItem.getImages(),
context,
document);
tocItem.setSection(section.orElse(null));
}
}
@@ -133,9 +133,9 @@ public class DocumentGraphFactory {
if (node instanceof DuplicatedParagraph duplicatedParagraph) {
AtomicTextBlock unsortedTextBlock = context.textBlockFactory.buildAtomicTextBlock(textBlocks.stream()
.flatMap(tb -> tb.getSequences()
.stream())
.collect(Collectors.toList()), node, context, page);
.flatMap(tb -> tb.getWords()
.stream())
.collect(Collectors.toList()), node, context, page);
duplicatedParagraph.setUnsortedLeafTextBlock(unsortedTextBlock);
}
@@ -10,9 +10,10 @@ import java.util.List;
import java.util.Locale;
import java.util.Objects;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Character;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.TextRange;
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.experimental.UtilityClass;
@@ -28,22 +29,22 @@ public class SearchTextWithTextPositionFactory {
public static final double LINEBREAK_DELTA_TOLERANCE = 1.5;
public SearchTextWithTextPositionDto buildSearchTextToTextPositionDto(List<TextPositionSequence> sequences) {
public SearchTextWithTextPositionDto buildSearchTextToTextPositionDto(List<Word> sequences) {
if (sequences.isEmpty() || sequences.stream()
.allMatch(sequence -> sequence.getTextPositions().isEmpty())) {
.allMatch(sequence -> sequence.getCharacters().isEmpty())) {
return SearchTextWithTextPositionDto.empty();
}
Context context = new Context();
RedTextPosition currentTextPosition = sequences.get(0).getTextPositions().get(0);
RedTextPosition currentTextPosition = sequences.get(0).getCharacters().get(0).getTextPosition();
RedTextPosition previousTextPosition = RedTextPosition.builder().unicode(" ").bBoxDirAdj(currentTextPosition.getBBoxDirAdj()).build();
for (TextPositionSequence word : sequences) {
for (int i = 0; i < word.getTextPositions().size(); ++i) {
for (Word word : sequences) {
for (int i = 0; i < word.getCharacters().size(); ++i) {
currentTextPosition = word.getTextPositions().get(i);
currentTextPosition = word.getCharacters().get(i).getTextPosition();
if (isLineBreak(currentTextPosition, previousTextPosition)) {
removeHyphenLinebreaks(context);
context.lineBreaksStringIdx.add(context.stringIdx);
@@ -66,8 +67,9 @@ public class SearchTextWithTextPositionFactory {
}
List<Rectangle2D> positions = sequences.stream()
.map(TextPositionSequence::getTextPositions)
.map(Word::getCharacters)
.flatMap(Collection::stream)
.map(Character::getTextPosition)
.map(RedTextPosition::getBBoxPdf)
.toList();
@@ -16,7 +16,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Ta
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Cell;
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.utils.TextPositionOperations;
import lombok.experimental.UtilityClass;
@@ -115,7 +115,7 @@ public class TableNodeFactory {
if (cell.getTextBlocks().isEmpty()) {
tableCell.setLeafTextBlock(context.getTextBlockFactory().emptyTextBlock(tableNode, context, page));
} else if (cell.getTextBlocks().size() == 1) {
textBlock = context.getTextBlockFactory().buildAtomicTextBlock(cell.getTextBlocks().get(0).getSequences(), tableCell, context, page);
textBlock = context.getTextBlockFactory().buildAtomicTextBlock(cell.getTextBlocks().get(0).getWords(), tableCell, context, page);
tableCell.setLeafTextBlock(textBlock);
} else if (firstTextBlockIsHeadline(cell)) {
SectionNodeFactory.addSection(layoutParsingType,
@@ -129,7 +129,7 @@ public class TableNodeFactory {
context,
document);
} else if (cellAreaIsSmallerThanPageAreaTimesThreshold(cell, page)) {
List<TextPositionSequence> sequences = TextPositionOperations.mergeAndSort(cell.getTextBlocks());
List<Word> sequences = TextPositionOperations.mergeAndSort(cell.getTextBlocks());
textBlock = context.getTextBlockFactory().buildAtomicTextBlock(sequences, tableCell, context, page);
tableCell.setLeafTextBlock(textBlock);
} else {
@@ -2,7 +2,7 @@ package com.knecon.fforesight.service.layoutparser.processor.services.factory;
import java.util.List;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
@@ -17,14 +17,14 @@ public class TextBlockFactory {
long textBlockIdx;
public AtomicTextBlock buildAtomicTextBlock(List<TextPositionSequence> sequences, SemanticNode parent, DocumentGraphFactory.Context context, Page page) {
public AtomicTextBlock buildAtomicTextBlock(List<Word> sequences, SemanticNode parent, DocumentGraphFactory.Context context, Page page) {
Integer numberOnPage = context.getAndIncrementTextBlockNumberOnPage(page);
return buildAtomicTextBlock(sequences, parent, numberOnPage, page);
}
public AtomicTextBlock buildAtomicTextBlock(List<TextPositionSequence> sequences, SemanticNode parent, Integer numberOnPage, Page page) {
public AtomicTextBlock buildAtomicTextBlock(List<Word> sequences, SemanticNode parent, Integer numberOnPage, Page page) {
SearchTextWithTextPositionDto searchTextWithTextPositionDto = SearchTextWithTextPositionFactory.buildSearchTextToTextPositionDto(sequences);
int offset = stringOffset;
@@ -11,7 +11,7 @@ import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.utils.PageInformation;
import lombok.RequiredArgsConstructor;
@@ -33,10 +33,10 @@ public class GraphicExtractorService {
PDPage pdPage,
int pageNumber,
CleanRulings cleanRulings,
List<TextPositionSequence> textPositionSequences,
List<Word> words,
boolean graphicsRaster) {
List<Box> characterBBoxes = getCharacterBBoxes(textPositionSequences);
List<Box> characterBBoxes = getCharacterBBoxes(words);
List<Box> classifiedRulingsBoxes = getLineBBoxesOfAllClassifiedRulings(cleanRulings);
GraphicBBDetector graphicBBDetector = new GraphicBBDetector(pdPage, true);
@@ -63,9 +63,9 @@ public class GraphicExtractorService {
}
private List<Box> getCharacterBBoxes(List<TextPositionSequence> textPositionSequences) {
private List<Box> getCharacterBBoxes(List<Word> words) {
return textPositionSequences.stream()
return words.stream()
.map(BoundingBox::getBBoxPdf)
.map(Box::new)
.collect(Collectors.toList());
@@ -40,7 +40,7 @@ import org.apache.pdfbox.text.TextPosition;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.Getter;
import lombok.Setter;
@@ -52,7 +52,7 @@ import lombok.extern.slf4j.Slf4j;
public class PDFLinesTextStripper extends PDFTextStripper {
private final static Set<String> DOT_LIKE_CHARACTERS = Set.of(".", "·", "•", "․", "‧", "∙", "⋅", "・", ".", "・", "…", "⸱", "﹒", "ꞏ");
private final List<TextPositionSequence> textPositionSequences = new ArrayList<>();
private final List<Word> words = new ArrayList<>();
private final List<Ruling> rulings = new ArrayList<>();
private final List<Ruling> graphicsPath = new ArrayList<>();
@Setter
@@ -246,10 +246,10 @@ public class PDFLinesTextStripper extends PDFTextStripper {
direction = textPositions.get(i).getDir();
}
if (!textPositionSequences.isEmpty()) {
previous = textPositionSequences.get(textPositionSequences.size() - 1)
.getTextPositions()
.get(textPositionSequences.get(textPositionSequences.size() - 1).getTextPositions().size() - 1);
if (!words.isEmpty()) {
previous = words.get(words.size() - 1)
.getCharacters()
.get(words.get(words.size() - 1).getCharacters().size() - 1).getTextPosition();
}
if (i == 0 && (textPositions.get(i).getUnicode().equals(" ") || textPositions.get(i).getUnicode().equals("\u00A0") || textPositions.get(i).getUnicode().equals("\t"))) {
@@ -259,7 +259,7 @@ public class PDFLinesTextStripper extends PDFTextStripper {
if (textPositions.get(i).getDir() != direction && startIndex != i) {
List<TextPosition> sublist = textPositions.subList(startIndex, i);
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
words.add(new Word(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
startIndex = i;
direction = textPositions.get(i).getDir();
}
@@ -268,7 +268,7 @@ public class PDFLinesTextStripper extends PDFTextStripper {
if (checkIfCurrentPositionIsToTheRightOfPreviousPosition(i, textPositions)) {
List<TextPosition> sublist = textPositions.subList(startIndex, i);
if (checkIfSequenceContainsOnlyWhitespaces(sublist)) {
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
words.add(new Word(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
}
startIndex = i;
}
@@ -276,7 +276,7 @@ public class PDFLinesTextStripper extends PDFTextStripper {
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 (checkIfSequenceContainsOnlyWhitespaces(sublist)) {
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
words.add(new Word(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
}
startIndex = i;
}
@@ -290,22 +290,22 @@ public class PDFLinesTextStripper extends PDFTextStripper {
// Remove false sequence ends (whitespaces)
if (checkIfGapSizeBetweenCharactersSmallerThanMaximum(previous, sublist, 0.01f)) {
for (TextPosition t : sublist) {
textPositionSequences.get(textPositionSequences.size() - 1).add(t);
words.get(words.size() - 1).add(t);
}
} else {
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
words.add(new Word(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
}
}
startIndex = i + 1;
}
if (isDottedLineFollowedByWord(textPositions, i, startIndex)) {
List<TextPosition> sublist = textPositions.subList(startIndex, i);
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
words.add(new Word(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
startIndex = i;
}
if (isWordFollowedByDottedLine(textPositions, i, startIndex)) {
List<TextPosition> sublist = textPositions.subList(startIndex, i - 2);
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
words.add(new Word(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
startIndex = i - 2;
}
}
@@ -324,10 +324,10 @@ public class PDFLinesTextStripper extends PDFTextStripper {
&& 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);
words.get(words.size() - 1).add(t);
}
} else {
textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, isParagraphStart));
words.add(new Word(sublist, pageNumber, isParagraphStart));
}
}
@@ -392,7 +392,7 @@ public class PDFLinesTextStripper extends PDFTextStripper {
@Override
public String getText(PDDocument doc) throws IOException {
textPositionSequences.clear();
words.clear();
rulings.clear();
graphicsPath.clear();
path_x = 0.0f;
@@ -13,7 +13,7 @@ import org.apache.pdfbox.text.TextPosition;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.BoundingBox;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.experimental.UtilityClass;
@@ -48,7 +48,7 @@ public class MarkedContentUtils {
return markedContentByYPosition.values()
.stream()
.map(textPositions -> new TextPositionSequence(textPositions, 0, true).getBBoxPdf())
.map(textPositions -> new Word(textPositions, 0, true).getBBoxPdf())
.map(t -> new Rectangle2D.Double(t.getX(), t.getY() - Math.abs(t.getHeight()), t.getWidth(), Math.abs(t.getHeight())))
.collect(Collectors.toList());
}
@@ -89,7 +89,7 @@ public class MarkedContentUtils {
.filter(content -> content instanceof TextPosition)
.map(content -> (TextPosition) content)
.filter(content -> !content.getUnicode().equals(" "))
.map(textPositions -> new TextPositionSequence(List.of(textPositions), 0, true))
.map(textPositions -> new Word(List.of(textPositions), 0, true))
.map(BoundingBox::getBBoxPdf)
.collect(Collectors.toList());
}
@@ -11,12 +11,13 @@ import java.util.Set;
import java.util.stream.Collectors;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.AngleFilter;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Line;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.TextBoundingBox;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.UnionFind;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.utils.DoubleUtils;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.utils.FastAtan2;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import lombok.experimental.UtilityClass;
@@ -36,33 +37,42 @@ public class TextPositionOperations {
.thenComparing(TextBoundingBox::getXDirAdj, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, X_THRESHOLD));
public List<TextPositionSequence> mergeAndSort(List<TextPageBlock> textBlocks) {
public List<Word> mergeAndSort(List<TextPageBlock> textBlocks) {
var sequences = textBlocks.stream()
.flatMap(tb -> tb.getSequences()
.flatMap(tb -> tb.getWords()
.stream())
.collect(Collectors.toSet());
return sortUsingLineDetection(sequences);
}
public List<TextPositionSequence> sort(List<TextPositionSequence> sequences) {
public List<Word> sort(List<Word> sequences) {
return sortUsingLineDetection(new HashSet<>(sequences));
}
private List<TextPositionSequence> sortUsingLineDetection(Set<TextPositionSequence> sequences) {
private List<Word> sortUsingLineDetection(Set<Word> sequences) {
return sortLines(groupByLine(sequences));
return sortWords(groupByLine(sequences));
}
public List<TextPositionSequence> sortLines(Collection<Set<TextPositionSequence>> lines) {
public List<Word> sortWords(Collection<Set<Word>> lines) {
List<List<TextPositionSequence>> lineBlocks = new ArrayList<>();
for (Set<TextPositionSequence> line : lines) {
List<TextPositionSequence> sortedLine = sortByXDirAdj(line);
return sortLines(lines).stream()
.map(Line::getWords)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
public List<Line> sortLines(Collection<Set<Word>> lines) {
List<List<Word>> lineBlocks = new ArrayList<>();
for (Set<Word> line : lines) {
List<Word> sortedLine = sortByXDirAdj(line);
if (!sortedLine.isEmpty()) {
lineBlocks.add(sortedLine);
}
@@ -70,35 +80,41 @@ public class TextPositionOperations {
// need to use old sorting, since COMPARATOR_DIR_ADJ is not transitive
QuickSort.sort(lineBlocks, Comparator.comparing(line -> line.get(0), COMPARATOR_DIR_ADJ));
List<TextPositionSequence> list = new ArrayList<>();
for (List<TextPositionSequence> textPositionSequences : lineBlocks) {
list.addAll(textPositionSequences);
List<Line> list = new ArrayList<>();
for (List<Word> lineBlock : lineBlocks) {
list.add(new Line(lineBlock));
}
return list;
}
private List<TextPositionSequence> sortByXDirAdj(Set<TextPositionSequence> line) {
private List<Word> sortByXDirAdj(Set<Word> line) {
return line.stream()
.sorted(Comparator.comparing(TextPositionSequence::getXDirAdj))
.sorted(Comparator.comparing(Word::getXDirAdj))
.toList();
}
public Collection<Set<TextPositionSequence>> groupByLine(Set<TextPositionSequence> sequences) {
public Collection<Set<Word>> groupByLine(Set<Word> sequences) {
double maxLineDistance = sequences.stream()
.map(TextPositionSequence::getBBoxDirAdj)
.map(Word::getBBoxDirAdj)
.mapToDouble(RectangularShape::getHeight).average().orElse(10) * MAX_LINE_HEIGHT_FACTOR;
double maxXGap = sequences.stream()
.map(TextPositionSequence::getBBoxDirAdj)
.map(Word::getBBoxDirAdj)
.mapToDouble(RectangularShape::getWidth).average().orElse(75) * MAX_WORD_DISTANCE_FACTOR;
UnionFind<TextPositionSequence> unionFind = new UnionFind<>(sequences);
return groupByLine(sequences, maxLineDistance, maxXGap);
}
for (TextPositionSequence sequence : sequences) {
for (TextPositionSequence sequence2 : sequences) {
public Collection<Set<Word>> groupByLine(Set<Word> sequences, double maxLineDistance, double maxXGap) {
UnionFind<Word> unionFind = new UnionFind<>(sequences);
for (Word sequence : sequences) {
for (Word sequence2 : sequences) {
if (sequence.equals(sequence2)) { // || unionFind.inSameSet(sequence, sequence2)) doing this is actually slower than not doing it
continue;
@@ -144,10 +160,10 @@ public class TextPositionOperations {
}
public List<TextPositionSequence> merge(List<TextPageBlock> textBlocks) {
public List<Word> merge(List<TextPageBlock> textBlocks) {
return textBlocks.stream()
.map(TextPageBlock::getSequences)
.map(TextPageBlock::getWords)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
@@ -27,7 +27,9 @@ import com.knecon.fforesight.service.layoutparser.processor.model.outline.Outlin
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.model.text.ListIdentifier;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextBlockOnPage;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.services.classification.NumberWord;
import com.knecon.fforesight.service.layoutparser.processor.utils.CoordinateTransforms;
import com.knecon.fforesight.service.layoutparser.processor.utils.MarkedContentUtils;
import com.knecon.fforesight.service.layoutparser.processor.utils.PageInformation;
@@ -58,14 +60,14 @@ public class LayoutDebugLayer extends LayoutDebugLayerConfig {
Map<Integer, AtomicInteger> outlineObjectsWithoutPointsPerPage = new HashMap<>();
public void addTextVisualizations(List<TextPositionSequence> textPositionSequences, int pageNumber) {
public void addTextVisualizations(List<Word> words, int pageNumber) {
if (!active) {
return;
}
VisualizationsOnPage visualizationsOnPage = getOrCreateVisualizationsOnPage(pageNumber, this.words);
visualizationsOnPage.getColoredRectangles()
.addAll(textPositionSequences.stream()
.addAll(words.stream()
.map(BoundingBox::getBBoxPdf)
.map(rect -> new ColoredRectangle(rect, WORDS_COLOR, 1))
.toList());
@@ -188,7 +190,7 @@ public class LayoutDebugLayer extends LayoutDebugLayerConfig {
}
public void addLineVisualizationsFromNestedTextPosition(Collection<Set<TextPositionSequence>> lines, int pageNumber) {
public void addLineVisualizationsFromNestedTextPosition(Collection<Set<Word>> lines, int pageNumber) {
if (!active) {
return;
@@ -273,7 +275,9 @@ public class LayoutDebugLayer extends LayoutDebugLayerConfig {
AtomicInteger index = new AtomicInteger(0);
zones.forEach(zone -> zone.getLines()
.stream()
.map(Line::getCharacters)
.map(Line::getWords)
.flatMap(Collection::stream)
.map(Word::getCharacters)
.flatMap(Collection::stream)
.forEach(character -> {
Color color = getRotatingColor(index);
@@ -291,7 +295,7 @@ public class LayoutDebugLayer extends LayoutDebugLayerConfig {
}
public void addTocPages(List<TextPositionSequence> numbers, int page) {
public void addTocPages(List<NumberWord> numbers, int page) {
if (!active) {
return;
@@ -300,13 +304,10 @@ public class LayoutDebugLayer extends LayoutDebugLayerConfig {
VisualizationsOnPage visualizationsOnPage = getOrCreateVisualizationsOnPage(page, this.tocPages);
visualizationsOnPage.getColoredRectangles()
.addAll(numbers.stream()
.map(NumberWord::word)
.map(BoundingBox::getBBoxPdf)
.map(line -> new ColoredRectangle(line, LINES_COLOR, LINE_WIDTH))
.toList());
visualizationsOnPage.getColoredRectangles()
.add(new ColoredRectangle(numbers.stream()
.map(BoundingBox::getBBoxPdf)
.collect(RectangleTransformations.collectBBox()), LINES_COLOR, LINE_WIDTH));
}
@@ -330,8 +331,10 @@ public class LayoutDebugLayer extends LayoutDebugLayerConfig {
private void addOutlineObject(OutlineObject outlineObject, PageInformation pageInformation) {
if (!active) {
return;
}
int rectSize = 5;
Point2D point2D;
if (outlineObject.getPoint().isPresent()) {
point2D = outlineObject.getPoint().get();
@@ -355,10 +358,25 @@ public class LayoutDebugLayer extends LayoutDebugLayerConfig {
public void addListIdentifiers(List<ListIdentifier> listIdentifiers) {
if (!active) {
return;
}
for (ListIdentifier listIdentifier : listIdentifiers) {
getOrCreateVisualizationsOnPage(listIdentifier.getPage(), this.listIdentifiers).getColoredRectangles()
.add(new ColoredRectangle(listIdentifier.getWord().getBBoxPdf(), WORDS_COLOR, LINE_WIDTH));
}
}
public void addTocBlocks(Set<TextBlockOnPage> blocksWithNumberInCluster) {
if (!active) {
return;
}
for (TextBlockOnPage textBlockOnPage : blocksWithNumberInCluster) {
getOrCreateVisualizationsOnPage(textBlockOnPage.page().getPageNumber(), this.tocBlocks).getColoredRectangles()
.add(new ColoredRectangle(textBlockOnPage.textBlock().getBBoxPdf(), TOC_COLOR, LINE_WIDTH));
}
}
}
@@ -83,4 +83,11 @@ class SectionIdentifierTest {
assertEquals(List.of(4, 1, 2, 4), identifier.getIdentifiers());
}
@Test
void testFalsePositive111() {
SectionIdentifier identifier = SectionIdentifier.fromSearchText("111: Headline");
assertEquals(SectionIdentifier.Format.NUMERICAL, identifier.getFormat());
assertEquals(1, identifier.level());
}
}
@@ -26,7 +26,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.Classification
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory;
import com.knecon.fforesight.service.layoutparser.processor.utils.TextPositionOperations;
import com.knecon.fforesight.service.layoutparser.server.PDFNetInitializer;
@@ -387,7 +387,7 @@ public class DocumentReadingOrderTest extends BuildDocumentTest {
for (AbstractPageBlock abstractBlock : classificationDocumentPage.getTextBlocks()) {
if (abstractBlock instanceof TextPageBlock textBlock) {
for (TextPositionSequence sequence : TextPositionOperations.mergeAndSort(List.of(textBlock))) {
for (Word sequence : TextPositionOperations.mergeAndSort(List.of(textBlock))) {
float stringWidth;
try {
@@ -32,7 +32,7 @@ import com.knecon.fforesight.service.layoutparser.processor.model.graph.textbloc
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Cell;
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.image.ImageServiceResponse;
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.TableServiceResponse;
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.VisualLayoutParsingResponse;
@@ -105,9 +105,9 @@ public class PdfSegmentationServiceTest extends AbstractTest {
List<PageContents> textPositionPerPage = PageContentExtractor.getSortedPageContents(fileName);
var textPositions = textPositionPerPage.stream()
.flatMap(t -> t.getSortedTextPositionSequences()
.flatMap(t -> t.getSortedWords()
.stream()
.map(TextPositionSequence::toString))
.map(Word::toString))
.collect(Collectors.joining(" "));
assertThat(textPositions.contains(textToSearch)).isFalse();
@@ -117,7 +117,7 @@ public class PdfSegmentationServiceTest extends AbstractTest {
.get(0).getTextBlocks().size()).isEqualTo(3);
assertThat(classificationDocument.getHeaders()
.get(0).getTextBlocks()
.get(0).getSequences().size()).isEqualTo(8);
.get(0).getWords().size()).isEqualTo(8);
assertThat(classificationDocument.getHeaders()
.get(0).getTextBlocks()
.get(0).toString()).contains(textToSearch);
@@ -36,7 +36,7 @@ class GapAcrossLinesDetectionServiceTest {
System.out.println("start column detection");
start = System.currentTimeMillis();
for (PageInformation pageInformation : pageInformations) {
GapInformation gapInformation = GapDetectionService.findGapsInLines(pageInformation.getPageContents().getSortedTextPositionSequences(), pageInformation.getMainBodyTextFrame());
GapInformation gapInformation = GapDetectionService.findGapsInLines(pageInformation.getPageContents().getSortedWords(), pageInformation.getMainBodyTextFrame());
columnsPerPage.add(GapsAcrossLinesService.detectXGapsAcrossLines(gapInformation, pageInformation.getMainBodyTextFrame()));
}
System.out.printf("Finished column detection in %d ms%n", System.currentTimeMillis() - start);
@@ -10,7 +10,7 @@ import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import com.knecon.fforesight.service.layoutparser.processor.model.PageInformation;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.services.InvisibleTableDetectionService;
import com.knecon.fforesight.service.layoutparser.processor.services.PageInformationService;
import com.knecon.fforesight.service.layoutparser.processor.services.PageContentExtractor;
@@ -34,18 +34,18 @@ class InvisibleTableDetectionServiceTest {
.collect(Collectors.toList());
int pageNumber = 1;
Rectangle2D tableBBox = pageContents.get(0).getPageContents().getSortedTextPositionSequences().subList(45, 152)
Rectangle2D tableBBox = pageContents.get(0).getPageContents().getSortedWords().subList(45, 152)
.stream()
.map(TextPositionSequence::getBBox)
.map(Word::getBBox)
.map(this::mirrorY)
.collect(RectangleTransformations.collectBBox());
List<TextPositionSequence> textPositionSequences = pageContents.get(0).getPageContents().getSortedTextPositionSequences()
List<Word> words = pageContents.get(0).getPageContents().getSortedWords()
.stream()
.filter(textPositionSequence -> tableBBox.contains(mirrorY(textPositionSequence.getBBox())))
.toList();
var table = InvisibleTableDetectionService.detectTable(textPositionSequences, tableBBox);
var table = InvisibleTableDetectionService.detectTable(words, tableBBox);
PdfDraw.drawRectanglesPerPage(fileName,
List.of(table.stream()
@@ -6,7 +6,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import com.knecon.fforesight.service.layoutparser.processor.model.PageContents;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.services.PageContentExtractor;
import com.knecon.fforesight.service.layoutparser.server.utils.visualizations.PdfDraw;
@@ -25,9 +25,9 @@ class PageContentExtractorTest {
PdfDraw.drawRectanglesPerPageNumberedByLine(fileName,
textPositionPerPage.stream()
.map(t -> t.getSortedTextPositionSequences()
.map(t -> t.getSortedWords()
.stream()
.map(TextPositionSequence::getBBoxPdf)
.map(Word::getBBoxPdf)
.map(List::of)
.toList())
.toList(), tmpFileName);
@@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test;
import com.knecon.fforesight.service.layoutparser.processor.model.PageContents;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.model.text.Word;
import com.knecon.fforesight.service.layoutparser.processor.services.PageContentExtractor;
import com.knecon.fforesight.service.layoutparser.processor.services.RulingCleaningService;
import com.knecon.fforesight.service.layoutparser.processor.services.TextRulingsClassifier;
@@ -32,16 +32,16 @@ public class RulingsClassifierTest {
for (PageContents pageContent : pageContents) {
CleanRulings cleanRulings = rulingCleaningService.deduplicateAndStraightenRulings(Collections.emptyList(), pageContent.getRulings());
RectangularIntersectionFinder.find(cleanRulings.getHorizontals(), cleanRulings.getVerticals());
TextRulingsClassifier.classifyUnderlinedAndStrikethroughText(pageContent.getSortedTextPositionSequences(), cleanRulings);
TextRulingsClassifier.classifyUnderlinedAndStrikethroughText(pageContent.getSortedWords(), cleanRulings);
assertTrue(pageContent.getSortedTextPositionSequences()
assertTrue(pageContent.getSortedWords()
.stream()
.filter(word -> word.toString().equals("Underlined"))
.allMatch(TextPositionSequence::isUnderline));
assertTrue(pageContent.getSortedTextPositionSequences()
.allMatch(Word::isUnderline));
assertTrue(pageContent.getSortedWords()
.stream()
.filter(word -> word.toString().equals("Striketrough"))
.allMatch(TextPositionSequence::isStrikethrough));
.allMatch(Word::isStrikethrough));
assertEquals(4,
cleanRulings.buildAll()
@@ -70,7 +70,7 @@ public class RulingsClassifierTest {
for (PageContents pageContent : pageContents) {
CleanRulings cleanRulings = rulingCleaningService.deduplicateAndStraightenRulings(Collections.emptyList(), pageContent.getRulings());
RectangularIntersectionFinder.find(cleanRulings.getHorizontals(), cleanRulings.getVerticals());
TextRulingsClassifier.classifyUnderlinedAndStrikethroughText(pageContent.getSortedTextPositionSequences(), cleanRulings);
TextRulingsClassifier.classifyUnderlinedAndStrikethroughText(pageContent.getSortedWords(), cleanRulings);
assertEquals(30, cleanRulings.getHorizontals().size());
assertEquals(30, cleanRulings.getTableLines().getHorizontals().size());
@@ -1,8 +1,6 @@
package com.knecon.fforesight.service.layoutparser.server.utils;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Path;
import java.util.Map;
@@ -12,27 +10,11 @@ import org.springframework.core.io.ClassPathResource;
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.model.AbstractPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.TableServiceResponse;
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.VisualLayoutParsingResponse;
import com.knecon.fforesight.service.layoutparser.processor.services.factory.DocumentGraphFactory;
import com.knecon.fforesight.service.viewerdoc.model.Standard14EmbeddableFont;
import com.pdftron.common.Matrix2D;
import com.pdftron.pdf.ColorPt;
import com.pdftron.pdf.ColorSpace;
import com.pdftron.pdf.Element;
import com.pdftron.pdf.ElementBuilder;
import com.pdftron.pdf.ElementWriter;
import com.pdftron.pdf.Font;
import com.pdftron.pdf.GState;
import com.pdftron.pdf.PDFDoc;
import com.pdftron.pdf.Page;
import com.pdftron.sdf.SDFDoc;
import lombok.SneakyThrows;
@@ -57,6 +57,7 @@ public record LayerIdentifier(String name, String markedContentName) {
public static final LayerIdentifier OUTLINE_OBJECTS = new LayerIdentifier("Outline Positions", "OUTLINE_OBJECTS");
public static final LayerIdentifier SENTENCES = new LayerIdentifier("Sentences", "SENTENCES");
public static final LayerIdentifier TOC_PAGES = new LayerIdentifier("TOC pages", "TOC_PAGES");
public static final LayerIdentifier TOC_BLOCKS = new LayerIdentifier("TOC blocks", "TOC_BLOCKS");
public static final LayerIdentifier LIST_IDENTIFIERS = new LayerIdentifier("List identifiers", "LIST_IDENTIFIERS");
// Visual layout parser
@@ -22,6 +22,7 @@ public class LayoutDebugLayerConfig extends AbstractLayerGroup {
protected static final Color WORDS_COLOR = new Color(68, 84, 147);
protected static final Color LINES_COLOR = new Color(152, 45, 179);
protected static final Color TOC_COLOR = new Color(33, 159, 144);
protected static final Color ZONES_COLOR = new Color(131, 38, 38);
protected static final Color RULINGS_COLOR = new Color(21, 221, 174);
@@ -46,9 +47,9 @@ public class LayoutDebugLayerConfig extends AbstractLayerGroup {
new Color(0, 188, 212),
new Color(121, 85, 72));
protected final Visualizations words = Visualizations.builder().layer(LayerIdentifier.WORDS).visibleByDefault(true).build();
protected final Visualizations words = Visualizations.builder().layer(LayerIdentifier.WORDS).build();
protected final Visualizations lines = Visualizations.builder().layer(LayerIdentifier.LINES).build();
protected final Visualizations zones = Visualizations.builder().layer(LayerIdentifier.ZONES).build();
protected final Visualizations zones = Visualizations.builder().layer(LayerIdentifier.ZONES).visibleByDefault(true).build();
protected final Visualizations mainBody = Visualizations.builder().layer(LayerIdentifier.MAIN_BODY).build();
protected final Visualizations clean_rulings = Visualizations.builder().layer(LayerIdentifier.CLEAN_RULINGS).build();
protected final Visualizations rulings = Visualizations.builder().layer(LayerIdentifier.RULINGS).build();
@@ -59,6 +60,7 @@ public class LayoutDebugLayerConfig extends AbstractLayerGroup {
protected final Visualizations outlineObjects = Visualizations.builder().layer(LayerIdentifier.OUTLINE_OBJECTS).build();
protected final Visualizations sentences = Visualizations.builder().layer(LayerIdentifier.SENTENCES).build();
protected final Visualizations tocPages = Visualizations.builder().layer(LayerIdentifier.TOC_PAGES).build();
protected final Visualizations tocBlocks = Visualizations.builder().layer(LayerIdentifier.TOC_BLOCKS).build();
protected final Visualizations listIdentifiers = Visualizations.builder().layer(LayerIdentifier.LIST_IDENTIFIERS).build();
@@ -77,9 +79,9 @@ public class LayoutDebugLayerConfig extends AbstractLayerGroup {
markedContent, //
outlineObjects, //
tocPages, //
tocBlocks, //
listIdentifiers //
);
}
}