RED-6369: Rules Refactor

*fixed DocumentData Mapping
This commit is contained in:
Kilian Schuettler 2023-03-23 15:05:07 +01:00 committed by Kilian Schuettler
parent 1fd483a6d4
commit d9c27d70d1
9 changed files with 233 additions and 54 deletions

View File

@ -11,11 +11,14 @@ import lombok.experimental.FieldDefaults;
@AllArgsConstructor @AllArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class AtomicTextBlockData { public class AtomicTextBlockData {
Long id; Long id;
Long page;
String searchText; String searchText;
int start; int start;
int end; int end;
int[] lineBreaks; int[] lineBreaks;
int[] stringIdxToPositionIdx; int[] stringIdxToPositionIdx;
float[][] positions; float[][] positions;
} }

View File

@ -4,6 +4,8 @@ import static java.lang.String.format;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import javax.management.openmbean.InvalidKeyException; import javax.management.openmbean.InvalidKeyException;
@ -26,7 +28,6 @@ public class TableOfContentsData {
public EntryData get(List<Integer> tocId) { public EntryData get(List<Integer> tocId) {
if (tocId.size() < 1) { if (tocId.size() < 1) {
throw new InvalidKeyException(format("Section Identifier: \"%s\" is not valid.", tocId)); throw new InvalidKeyException(format("Section Identifier: \"%s\" is not valid.", tocId));
} }
@ -38,14 +39,51 @@ public class TableOfContentsData {
} }
public Stream<EntryData> streamAllEntries() {
return entries.stream().flatMap(TableOfContentsData::flatten);
}
private static List<Integer> getIds(String idsAsString) { private static List<Integer> getIds(String idsAsString) {
return Arrays.stream(idsAsString.split("\\.")).map(Integer::valueOf).toList(); return Arrays.stream(idsAsString.split("\\.")).map(Integer::valueOf).toList();
} }
public String toString() {
return String.join("\n", streamAllEntries().map(EntryData::toString).toList());
}
private static Stream<EntryData> flatten(EntryData entry) {
return Stream.concat(Stream.of(entry), entry.subEntries().stream().flatMap(TableOfContentsData::flatten));
}
@Builder @Builder
public record EntryData(int[] tocId, List<EntryData> subEntries, NodeType type, Long[] atomicTextBlocks, Long[] pages, int numberOnPage) { public record EntryData(int[] tocId, List<EntryData> subEntries, NodeType type, Long[] atomicTextBlocks, Long[] pages, int numberOnPage, Map<String, String> properties) {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i : tocId) {
sb.append(i);
sb.append(",");
}
sb.delete(sb.length() - 1, sb.length());
sb.append("]: ");
sb.append(type);
sb.append(" atbs = ");
sb.append(atomicTextBlocks.length);
return sb.toString();
}
} }

View File

@ -1,7 +1,9 @@
package com.iqser.red.service.redaction.v1.server.document.data.mapper; package com.iqser.red.service.redaction.v1.server.document.data.mapper;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -11,7 +13,11 @@ import com.iqser.red.service.redaction.v1.server.document.data.PageData;
import com.iqser.red.service.redaction.v1.server.document.data.TableOfContentsData; import com.iqser.red.service.redaction.v1.server.document.data.TableOfContentsData;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph;
import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.ImageNode;
import com.iqser.red.service.redaction.v1.server.document.graph.factory.RectangleTransformations;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableCellNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableNode;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
@ -40,23 +46,68 @@ public class DocumentDataMapper {
private TableOfContentsData.EntryData toEntryData(TableOfContents.Entry entry) { private TableOfContentsData.EntryData toEntryData(TableOfContents.Entry entry) {
Long[] atomicTextBlocks; Long[] atomicTextBlocks;
if (entry.node().isTerminal()) { if (entry.node().isTerminal()) {
atomicTextBlocks = toAtomicTextBlockIds(entry.node().getTerminalTextBlock()); atomicTextBlocks = toAtomicTextBlockIds(entry.node().getTerminalTextBlock());
} else { } else {
atomicTextBlocks = new Long[]{-1L}; atomicTextBlocks = new Long[]{};
} }
Map<String, String> properties = switch (entry.type()) {
case TABLE -> buildTableProperties((TableNode) entry.node());
case TABLE_CELL -> buildTableCellProperties((TableCellNode) entry.node());
case IMAGE -> buildImageProperties((ImageNode) entry.node());
default -> new HashMap<>();
};
return TableOfContentsData.EntryData.builder() return TableOfContentsData.EntryData.builder()
.tocId(toPrimitiveIntArray(entry.tocId())) .tocId(toPrimitiveIntArray(entry.tocId()))
.subEntries(entry.children().stream().map(this::toEntryData).toList()) .subEntries(entry.children().stream().map(this::toEntryData).toList())
.type(entry.type()) .type(entry.type())
.atomicTextBlocks(atomicTextBlocks) .atomicTextBlocks(atomicTextBlocks)
.pages(entry.node().getPages().stream().map(PageNode::getNumber).toArray(Long[]::new)) .pages(entry.node().getPages().stream().map(PageNode::getNumber).map(Integer::longValue).toArray(Long[]::new))
.numberOnPage(entry.node().getNumberOnPage()) .numberOnPage(entry.node().getNumberOnPage())
.properties(properties)
.build(); .build();
} }
private Map<String, String> buildImageProperties(ImageNode image) {
Map<String, String> properties = new HashMap<>();
properties.put("imageType", image.getImageType().toString());
properties.put("transparency", String.valueOf(image.isTransparency()));
properties.put("position", RectangleTransformations.toString(image.getPosition()));
return properties;
}
private Map<String, String> buildTableCellProperties(TableCellNode tableCell) {
Map<String, String> properties = new HashMap<>();
properties.put("row", String.valueOf(tableCell.getRow()));
properties.put("col", String.valueOf(tableCell.getCol()));
properties.put("header", String.valueOf(tableCell.isHeader()));
if (tableCell.getPages().size() > 1 || tableCell.getBBox().keySet().size() > 1) {
throw new IllegalArgumentException("TableCell can only occur on a single page!");
}
String bBoxString = RectangleTransformations.toString(tableCell.getBBox().get(tableCell.getPages().stream().findFirst().get()));
properties.put("bBox", bBoxString);
return properties;
}
private Map<String, String> buildTableProperties(TableNode table) {
Map<String, String> properties = new HashMap<>();
properties.put("numberOfRows", String.valueOf(table.getNumberOfRows()));
properties.put("numberOfCols", String.valueOf(table.getNumberOfCols()));
return properties;
}
private Long[] toAtomicTextBlockIds(TextBlock textBlock) { private Long[] toAtomicTextBlockIds(TextBlock textBlock) {
return textBlock.getAtomicTextBlocks().stream().map(AtomicTextBlock::getId).toArray(Long[]::new); return textBlock.getAtomicTextBlocks().stream().map(AtomicTextBlock::getId).toArray(Long[]::new);
@ -65,11 +116,7 @@ public class DocumentDataMapper {
private PageData toPageData(PageNode pageNode) { private PageData toPageData(PageNode pageNode) {
return PageData.builder() return PageData.builder().height(pageNode.getHeight()).width(pageNode.getWidth()).number(pageNode.getNumber()).build();
.height(pageNode.getHeight())
.width(pageNode.getWidth())
.number(pageNode.getNumber())
.build();
} }
@ -77,6 +124,7 @@ public class DocumentDataMapper {
return AtomicTextBlockData.builder() return AtomicTextBlockData.builder()
.id(atomicTextBlock.getId()) .id(atomicTextBlock.getId())
.page(atomicTextBlock.getPage().getNumber().longValue())
.searchText(atomicTextBlock.getSearchText()) .searchText(atomicTextBlock.getSearchText())
.start(atomicTextBlock.getBoundary().start()) .start(atomicTextBlock.getBoundary().start())
.end(atomicTextBlock.getBoundary().end()) .end(atomicTextBlock.getBoundary().end())

View File

@ -1,16 +1,15 @@
package com.iqser.red.service.redaction.v1.server.document.data.mapper; package com.iqser.red.service.redaction.v1.server.document.data.mapper;
import static com.iqser.red.service.redaction.v1.server.document.graph.factory.RectangleTransformations.parseRectangle2D;
import static java.lang.Math.toIntExact; import static java.lang.Math.toIntExact;
import static java.lang.String.format; import static java.lang.String.format;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.NotImplementedException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -23,15 +22,22 @@ import com.iqser.red.service.redaction.v1.server.document.data.TableOfContentsDa
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary; import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph; import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph;
import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents; import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents;
import com.iqser.red.service.redaction.v1.server.document.graph.entity.ImageNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.FooterNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.HeaderNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.HeadlineNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.NodeType;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ParagraphNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ParagraphNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode; import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SemanticNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableCellNode;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableNode;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector; import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
import com.iqser.red.service.redaction.v1.server.exception.NotFoundException; import com.iqser.red.service.redaction.v1.server.exception.NotFoundException;
import com.iqser.red.service.redaction.v1.server.redaction.model.ImageType;
@Service @Service
public class DocumentGraphMapper { public class DocumentGraphMapper {
@ -42,7 +48,7 @@ public class DocumentGraphMapper {
context.pages.addAll(documentData.getPages().stream().map(this::buildPage).toList()); context.pages.addAll(documentData.getPages().stream().map(this::buildPage).toList());
buildNodesFromTableOfContents(Collections.emptyList(), context); context.tableOfContents.setEntries(buildEntries(documentData.getTableOfContents().getEntries(), context));
DocumentGraph documentGraph = DocumentGraph.builder() DocumentGraph documentGraph = DocumentGraph.builder()
.numberOfPages(documentData.getPages().size()) .numberOfPages(documentData.getPages().size())
@ -54,67 +60,127 @@ public class DocumentGraphMapper {
} }
private void buildNodesFromTableOfContents(List<Integer> currentTocId, Context context) { private List<TableOfContents.Entry> buildEntries(List<TableOfContentsData.EntryData> entries, Context context) {
List<TableOfContentsData.EntryData> entries; List<TableOfContents.Entry> newEntries = new LinkedList<>();
if (currentTocId.size() == 0) {
entries = context.documentData().getTableOfContents().getEntries();
} else {
entries = context.documentData().getTableOfContents().get(currentTocId).subEntries();
}
for (TableOfContentsData.EntryData entryData : entries) { for (TableOfContentsData.EntryData entryData : entries) {
switch (entryData.type()) { boolean terminal = isTerminal(entryData);
case SECTION -> buildSection(entryData, currentTocId, context);
case PARAGRAPH -> buildParagraph(entryData, currentTocId, context);
default -> throw new NotImplementedException("Not yet implemented for type " + entryData.type());
}
}
}
private void buildSection(TableOfContentsData.EntryData entryData, List<Integer> currentTocId, Context context) {
SectionNode section = SectionNode.builder().entities(new HashSet<>()).tableOfContents(context.tableOfContents()).build();
context.sections().add(section);
List<PageNode> pages = Arrays.stream(entryData.pages()).map(pageNumber -> getPage(pageNumber, context)).toList(); List<PageNode> pages = Arrays.stream(entryData.pages()).map(pageNumber -> getPage(pageNumber, context)).toList();
pages.forEach(page -> page.getMainBody().add(section));
section.getPages().addAll(pages);
List<Integer> sectionId = context.tableOfContents.createNewChildEntryAndReturnId(currentTocId, NodeType.SECTION, section); SemanticNode node = switch (entryData.type()) {
section.setTocId(sectionId); case SECTION -> buildSection(context);
buildNodesFromTableOfContents(sectionId, context); case PARAGRAPH -> buildParagraph(context, terminal);
case HEADLINE -> buildHeadline(context, terminal);
case HEADER -> buildHeader(context, terminal);
case FOOTER -> buildFooter(context, terminal);
case TABLE -> buildTable(context, entryData.properties());
case TABLE_CELL -> buildTableCell(context, entryData.properties(), terminal);
case IMAGE -> buildImage(context, entryData.properties());
default -> throw new NotImplementedException("Not yet implemented for type " + entryData.type());
};
if (node.isTerminal()) {
TextBlock textBlock = toTextBlock(entryData.atomicTextBlocks(), context, node);
node.setTerminalTextBlock(textBlock);
}
List<Integer> tocId = Arrays.stream(entryData.tocId()).boxed().toList();
node.setTocId(tocId);
if (entryData.type() == NodeType.HEADER) {
pages.forEach(page -> page.setHeader((HeaderNode) node));
} else if (entryData.type() == NodeType.FOOTER) {
pages.forEach(page -> page.setFooter((FooterNode) node));
} else {
pages.forEach(page -> page.getMainBody().add(node));
}
newEntries.add(TableOfContents.Entry.builder().tocId(tocId).type(entryData.type()).children(buildEntries(entryData.subEntries(), context)).node(node).build());
}
return newEntries;
} }
private void buildParagraph(TableOfContentsData.EntryData entryData, List<Integer> currentTocId, Context context) { private HeadlineNode buildHeadline(Context context, boolean terminal) {
Set<PageNode> pages = Arrays.stream(entryData.pages()).map(pageNumber -> getPage(pageNumber, context)).collect(Collectors.toSet()); return HeadlineNode.builder().terminal(terminal).tableOfContents(context.tableOfContents()).build();
SectionNode parentSection = (SectionNode) context.tableOfContents().getEntryById(currentTocId).node();
ParagraphNode paragraph = ParagraphNode.builder().build();
TextBlock textBlock = toTextBlock(entryData.atomicTextBlocks(), context, paragraph);
paragraph.setTerminalTextBlock(textBlock);
pages.forEach(page -> page.getMainBody().add(paragraph));
for (PageNode page : pages) {
if (!parentSection.getPages().contains(page)) {
parentSection.getPages().add(page);
}
} }
List<Integer> tocId = context.tableOfContents.createNewChildEntryAndReturnId(currentTocId, NodeType.PARAGRAPH, paragraph);
paragraph.setTocId(tocId); private static boolean isTerminal(TableOfContentsData.EntryData entryData) {
return entryData.atomicTextBlocks().length > 0;
}
private ImageNode buildImage(Context context, Map<String, String> properties) {
ImageType imageType = parseImageType(properties.get("imageType"));
boolean transparency = Boolean.parseBoolean(properties.get("transparency"));
Rectangle2D position = parseRectangle2D(properties.get("position"));
return ImageNode.builder().tableOfContents(context.tableOfContents()).imageType(imageType).transparency(transparency).position(position).build();
}
private TableCellNode buildTableCell(Context context, Map<String, String> properties, boolean terminal) {
int row = Integer.parseInt(properties.get("row"));
int col = Integer.parseInt(properties.get("col"));
boolean header = Boolean.parseBoolean(properties.get("header"));
Rectangle2D bBox = parseRectangle2D(properties.get("bBox"));
return TableCellNode.builder().terminal(terminal).bBox(bBox).header(header).col(col).row(row).tableOfContents(context.tableOfContents()).build();
}
private TableNode buildTable(Context context, Map<String, String> properties) {
int numberOfRows = Integer.parseInt(properties.get("numberOfRows"));
int numberOfCols = Integer.parseInt(properties.get("numberOfCols"));
return TableNode.builder().tableOfContents(context.tableOfContents()).numberOfRows(numberOfRows).numberOfCols(numberOfCols).build();
}
private FooterNode buildFooter(Context context, boolean terminal) {
return FooterNode.builder().terminal(terminal).tableOfContents(context.tableOfContents()).build();
}
private HeaderNode buildHeader(Context context, boolean terminal) {
return HeaderNode.builder().terminal(terminal).tableOfContents(context.tableOfContents()).build();
}
private SectionNode buildSection(Context context) {
return SectionNode.builder().tableOfContents(context.tableOfContents()).build();
}
private ParagraphNode buildParagraph(Context context, boolean terminal) {
return ParagraphNode.builder().terminal(terminal).tableOfContents(context.tableOfContents()).build();
}
private static ImageType parseImageType(String imageType) {
return switch (imageType) {
case "LOGO" -> ImageType.LOGO;
case "FORMULA" -> ImageType.FORMULA;
case "SIGNATURE" -> ImageType.SIGNATURE;
case "OCR" -> ImageType.OCR;
default -> ImageType.OTHER;
};
} }
private TextBlock toTextBlock(Long[] atomicTextBlockIds, Context context, SemanticNode parent) { private TextBlock toTextBlock(Long[] atomicTextBlockIds, Context context, SemanticNode parent) {
return Arrays.stream(atomicTextBlockIds) return Arrays.stream(atomicTextBlockIds)
.map(atomicTextBlockId -> toAtomicTextBlock(context.atomicTextBlockData.get(toIntExact(atomicTextBlockId)), parent)) .map(atomicTextBlockId -> toAtomicTextBlock(context.atomicTextBlockData.get(toIntExact(atomicTextBlockId)), parent, context))
.collect(new TextBlockCollector()); .collect(new TextBlockCollector());
} }
@ -125,10 +191,11 @@ public class DocumentGraphMapper {
} }
private AtomicTextBlock toAtomicTextBlock(AtomicTextBlockData atomicTextBlockData, SemanticNode parent) { private AtomicTextBlock toAtomicTextBlock(AtomicTextBlockData atomicTextBlockData, SemanticNode parent, Context context) {
return AtomicTextBlock.builder() return AtomicTextBlock.builder()
.id(atomicTextBlockData.getId()) .id(atomicTextBlockData.getId())
.page(getPage(atomicTextBlockData.getPage(), context))
.searchText(atomicTextBlockData.getSearchText()) .searchText(atomicTextBlockData.getSearchText())
.boundary(new Boundary(atomicTextBlockData.getStart(), atomicTextBlockData.getEnd())) .boundary(new Boundary(atomicTextBlockData.getStart(), atomicTextBlockData.getEnd()))
.lineBreaks(Ints.asList(atomicTextBlockData.getLineBreaks())) .lineBreaks(Ints.asList(atomicTextBlockData.getLineBreaks()))

View File

@ -0,0 +1,2 @@
package com.iqser.red.service.redaction.v1.server.document.data.mapper;public class PropertiesMapper {
}

View File

@ -1,7 +1,10 @@
package com.iqser.red.service.redaction.v1.server.document.graph.factory; package com.iqser.red.service.redaction.v1.server.document.graph.factory;
import static java.lang.String.format;
import java.awt.geom.Area; import java.awt.geom.Area;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@ -60,6 +63,19 @@ public class RectangleTransformations {
} }
public static String toString(Rectangle2D rectangle2D) {
return format("%f,%f,%f,%f", rectangle2D.getX(), rectangle2D.getY(), rectangle2D.getWidth(), rectangle2D.getHeight());
}
public static Rectangle2D parseRectangle2D(String bBox) {
List<Float> floats = Arrays.stream(bBox.split(",")).map(Float::parseFloat).toList();
return new Rectangle2D.Float(floats.get(0), floats.get(1), floats.get(2), floats.get(3));
}
private static class Rectangle2DUnion implements Collector<Rectangle2D, Area, Rectangle2D> { private static class Rectangle2DUnion implements Collector<Rectangle2D, Area, Rectangle2D> {
@Override @Override

View File

@ -21,7 +21,6 @@ public class TextBlockFactory {
stringOffset = new AtomicInteger(); stringOffset = new AtomicInteger();
textBlockIdx = new AtomicLong(); textBlockIdx = new AtomicLong();
} }
@ -64,7 +63,7 @@ public class TextBlockFactory {
public AtomicTextBlock emptyTextBlock(SemanticNode parent, Integer numberOnPage, PageNode page) { public AtomicTextBlock emptyTextBlock(SemanticNode parent, Integer numberOnPage, PageNode page) {
return AtomicTextBlock.builder() return AtomicTextBlock.builder()
.id(textBlockIdx.get()) .id(textBlockIdx.getAndIncrement())
.boundary(new Boundary(stringOffset.get(), stringOffset.get())) .boundary(new Boundary(stringOffset.get(), stringOffset.get()))
.searchText("") .searchText("")
.lineBreaks(Collections.emptyList()) .lineBreaks(Collections.emptyList())

View File

@ -126,6 +126,12 @@ public interface SemanticNode {
} }
default void setTerminalTextBlock(TextBlock textBlock) {
throw new UnsupportedOperationException();
}
/** /**
* Each AtomicTextBlock has an index on its page, this returns the number of the first AtomicTextBlock underneath this node. * Each AtomicTextBlock has an index on its page, this returns the number of the first AtomicTextBlock underneath this node.
* If this node does not have any AtomicTexBlocks underneath it, e.g. an empty TableCell. It returns -1. * If this node does not have any AtomicTexBlocks underneath it, e.g. an empty TableCell. It returns -1.

View File

@ -48,7 +48,7 @@ public class ConcatenatedTextBlock implements TextBlock {
boundary.setStart(textBlock.getBoundary().start()); boundary.setStart(textBlock.getBoundary().start());
boundary.setEnd(textBlock.getBoundary().end()); boundary.setEnd(textBlock.getBoundary().end());
} else if (boundary.end() != textBlock.getBoundary().start()) { } else if (boundary.end() != textBlock.getBoundary().start()) {
throw new UnsupportedOperationException(format("Can only concat consecutive TextBlocks, trying to concat %s to %s", textBlock.getBoundary(), boundary)); throw new UnsupportedOperationException(format("Can only concat consecutive TextBlocks, trying to concat %s and %s", boundary, textBlock.getBoundary()));
} }
this.atomicTextBlocks.addAll(textBlock.getAtomicTextBlocks()); this.atomicTextBlocks.addAll(textBlock.getAtomicTextBlocks());
boundary.setEnd(textBlock.getBoundary().end()); boundary.setEnd(textBlock.getBoundary().end());