RED-6369: Rules Refactor

* fixed AnnotationService with new Coordinates
* fixed bug in split boundary function
This commit is contained in:
Kilian Schuettler 2023-04-05 18:03:07 +02:00 committed by Kilian Schuettler
parent 59827eab81
commit 9d8e544d0c
8 changed files with 96 additions and 71 deletions

View File

@ -92,6 +92,11 @@ public class Boundary implements Comparable<Boundary> {
List<Boundary> splitBoundaries = new LinkedList<>();
int previousIndex = start;
for (int splitIndex : splitIndices) {
// skip split if it would produce a boundary of length 0
if (splitIndex == previousIndex) {
continue;
}
splitBoundaries.add(new Boundary(previousIndex, splitIndex));
previousIndex = splitIndex;
}

View File

@ -13,6 +13,8 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
@ -39,6 +41,12 @@ public class RectangleTransformations {
}
public static Rectangle2D bBoxUnionRectangle(List<Rectangle> rectangles) {
return rectangles.stream().map(RectangleTransformations::toRectangle2D).collect(new Rectangle2DUnion());
}
public static Rectangle2D rectangleUnion(List<Rectangle2D> rectangle2DList) {
return rectangle2DList.stream().collect(new Rectangle2DUnion());
@ -57,6 +65,34 @@ public class RectangleTransformations {
}
public static Rectangle2D toRectangle2D(Rectangle rectangle) {
return new Rectangle2D.Double(rectangle.getTopLeft().getX() - rectangle.getWidth(),
rectangle.getTopLeft().getY() - rectangle.getHeight(),
rectangle.getWidth(),
rectangle.getHeight());
}
public static Rectangle2D toRectangle2D(PDRectangle rectangle) {
return new Rectangle2D.Double(rectangle.getLowerLeftX(), rectangle.getLowerLeftY(), rectangle.getWidth(), rectangle.getHeight());
}
public static PDRectangle toPDRectangleUnion(List<Rectangle> rectangles) {
Rectangle2D rectangle2D = RectangleTransformations.bBoxUnionRectangle(rectangles);
PDRectangle annotationPosition = new PDRectangle();
annotationPosition.setLowerLeftX((float) rectangle2D.getMinX());
annotationPosition.setLowerLeftY((float) rectangle2D.getMinY());
annotationPosition.setUpperRightX((float) rectangle2D.getMaxX());
annotationPosition.setUpperRightY((float) rectangle2D.getMaxY());
return annotationPosition;
}
public static Rectangle toRectangle(Rectangle2D rectangle2D, int pageNumber) {
return new Rectangle(new Point((float) rectangle2D.getMaxX(), (float) rectangle2D.getMaxY()), (float) rectangle2D.getWidth(), (float) rectangle2D.getHeight(), pageNumber);

View File

@ -76,7 +76,7 @@ public class DocumentDataMapper {
.tocId(toPrimitiveIntArray(entry.tocId()))
.subEntries(entry.children().stream().map(this::toEntryData).toList())
.type(entry.type())
.atomicTextBlocks(atomicTextBlocks)
.atomicBlocks(atomicTextBlocks)
.pages(entry.node().getPages().stream().map(PageNode::getNumber).map(Integer::longValue).toArray(Long[]::new))
.properties(properties)
.build();

View File

@ -1,22 +1,24 @@
package com.iqser.red.service.redaction.v1.server.utils;
package com.iqser.red.service.redaction.v1.server.visualization.service;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph;
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode;
import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents;
import com.iqser.red.service.redaction.v1.server.document.graph.factory.RectangleTransformations;
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.textblock.AtomicTextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.document.graph.factory.RectangleTransformations;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@ -80,6 +82,12 @@ public class PdfDraw {
public static void drawRectangle2DList(PDDocument document, int pageNumber, List<Rectangle2D> rectCollection, Options options) {
var pdPage = document.getPage(pageNumber - 1);
drawRectangle2DList(document, rectCollection, options, pdPage);
}
private static void drawRectangle2DList(PDDocument document, List<Rectangle2D> rectCollection, Options options, PDPage pdPage) throws IOException {
var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true);
contentStream.setStrokingColor(options.getStrokeColor());

View File

@ -1,7 +1,7 @@
package com.iqser.red.service.redaction.v1.server;
import static com.iqser.red.service.redaction.v1.server.redaction.utils.SeparatorUtils.boundaryIsSurroundedBySeparators;
import static com.iqser.red.service.redaction.v1.server.utils.PdfDraw.drawRectangle2DList;
import static com.iqser.red.service.redaction.v1.server.visualization.service.PdfDraw.drawRectangle2DList;
import java.awt.Color;
import java.awt.geom.Rectangle2D;
@ -35,7 +35,7 @@ import com.iqser.red.service.redaction.v1.server.redaction.service.DictionarySer
import com.iqser.red.service.redaction.v1.server.redaction.service.RedactionLogCreatorService;
import com.iqser.red.service.redaction.v1.server.redaction.utils.SearchImplementation;
import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService;
import com.iqser.red.service.redaction.v1.server.utils.PdfDraw;
import com.iqser.red.service.redaction.v1.server.visualization.service.PdfDraw;
import lombok.SneakyThrows;

View File

@ -1,6 +1,9 @@
package com.iqser.red.service.redaction.v1.server.annotate;
import static com.iqser.red.service.redaction.v1.server.document.graph.factory.RectangleTransformations.toPDRectangleUnion;
import java.awt.Color;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
@ -9,6 +12,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.pdmodel.PDDocument;
@ -22,13 +26,17 @@ import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationText;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup;
import org.springframework.stereotype.Service;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.*;
import com.google.common.primitives.Floats;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogComment;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.CellRectangle;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionGrid;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionRectangle;
import com.iqser.red.service.redaction.v1.server.document.graph.factory.RectangleTransformations;
import com.iqser.red.service.redaction.v1.server.exception.RedactionException;
import com.iqser.red.service.redaction.v1.server.redaction.service.DictionaryService;
import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService;
@ -43,7 +51,20 @@ public class AnnotationService {
private final DictionaryService dictionaryService;
/**
* This method draws a PDF Text Markup Annotation for each RedactionLogEntry and the section grid.
* A Text Markup Annotation has two main functionalities.
* First, it highlights text with a color and second, when hovered over in a PDF Viewer it displays a popup message.
* Where the Popup message appears is defined by the Rectangle provided to the Annotation.
* Further, the highlighted text regions are identified by multiple quad points which are represented by 8 floats each, which identify the four corners.
* According to the PDF specification, these points should be provided in counterclockwise fashion.
* Experiments have shown, PDFBox requires them to be provided in Z-like fashion as such: top-left, top-right, bottom-left, bottom-right
* If multiple regions should be highlighted per annotation, the quad points need to be concatenated
* For a more detailed information look in the PDF Specification 12.5.6.10
*
* @param annotateRequest containing the SectionGrid, RedactionLog, ManualRedactions and the colors for the highlights
* @return the annotated PDFDoc
*/
public AnnotateResponse annotate(AnnotateRequest annotateRequest) {
var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(annotateRequest.getDossierId(),
@ -96,12 +117,12 @@ public class AnnotationService {
List<PDAnnotation> annotations = pdPage.getAnnotations();
for (RedactionLogEntry entry : logEntries) {
annotations.addAll(createAnnotation(entry, page, pdPage.getMediaBox(), pdPage.getCropBox()));
annotations.addAll(createAnnotation(entry, page, pdPage.getRotation(), pdPage.getCropBox()));
}
}
private List<PDAnnotation> createAnnotation(RedactionLogEntry redactionLogEntry, int page, PDRectangle mediaBox, PDRectangle cropBox) {
private List<PDAnnotation> createAnnotation(RedactionLogEntry redactionLogEntry, int page, int rotation, PDRectangle cropBox) {
List<PDAnnotation> annotations = new ArrayList<>();
@ -113,9 +134,9 @@ public class AnnotationService {
PDAnnotationTextMarkup annotation = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
annotation.constructAppearances();
PDRectangle pdRectangle = toPDRectangle(rectangles, mediaBox, cropBox);
PDRectangle pdRectangle = toPDRectangleUnion(rectangles);
annotation.setRectangle(pdRectangle);
annotation.setQuadPoints(toQuadPoints(rectangles, mediaBox, cropBox));
annotation.setQuadPoints(Floats.toArray(toQuadPoints(rectangles)));
if (!redactionLogEntry.isHint()) {
annotation.setContents(redactionLogEntry.getValue() + " " + createAnnotationContent(redactionLogEntry));
}
@ -151,72 +172,26 @@ public class AnnotationService {
}
private PDRectangle toPDRectangle(List<Rectangle> rectangles, PDRectangle mediaBox, PDRectangle cropBox) {
public static List<Double> toQuadPoints(List<Rectangle> rectangles) {
float lowerLeftX = Float.MAX_VALUE;
float upperRightX = 0;
float lowerLeftY = 0;
float upperRightY = Float.MAX_VALUE;
for (Rectangle rectangle : rectangles) {
if (rectangle.getTopLeft().getX() < lowerLeftX) {
lowerLeftX = rectangle.getTopLeft().getX();
}
if (rectangle.getTopLeft().getX() + rectangle.getWidth() > upperRightX) {
upperRightX = rectangle.getTopLeft().getX() + rectangle.getWidth();
}
if (rectangle.getTopLeft().getY() + rectangle.getHeight() > lowerLeftY) {
lowerLeftY = rectangle.getTopLeft().getY() + rectangle.getHeight();
}
if (rectangle.getTopLeft().getY() < upperRightY) {
upperRightY = rectangle.getTopLeft().getY();
}
}
var x1 = lowerLeftX + cropBox.getLowerLeftX() - mediaBox.getLowerLeftY();
var y1 = lowerLeftY + (mediaBox.getLowerLeftY() - cropBox.getLowerLeftY());
var x2 = upperRightX + cropBox.getLowerLeftX() - mediaBox.getLowerLeftY();
var y2 = upperRightY - (mediaBox.getLowerLeftY() - cropBox.getLowerLeftY());
PDRectangle annotationPosition = new PDRectangle();
annotationPosition.setLowerLeftX(x1);
annotationPosition.setLowerLeftY(y1);
annotationPosition.setUpperRightX(x2);
annotationPosition.setUpperRightY(y2);
return annotationPosition;
return rectangles.stream().map(RectangleTransformations::toRectangle2D).flatMap(AnnotationService::toQuadPoints).toList();
}
private float[] toQuadPoints(List<Rectangle> rectangles, PDRectangle mediaBox, PDRectangle cropBox) {
public static Stream<Double> toQuadPoints(Rectangle2D rectangle) {
float[] quadPoints = new float[rectangles.size() * 8];
int i = 0;
double x1 = rectangle.getMinX();
double y1 = rectangle.getMinY();
for (Rectangle rectangle : rectangles) {
float[] quadPoint = toQuadPoint(rectangle, mediaBox, cropBox);
for (int j = 0; j <= 7; j++) {
quadPoints[i + j] = quadPoint[j];
}
i += 8;
}
return quadPoints;
}
double x2 = rectangle.getMaxX();
double y2 = rectangle.getMinY();
double x3 = rectangle.getMaxX();
double y3 = rectangle.getMaxY();
private float[] toQuadPoint(Rectangle rectangle, PDRectangle mediaBox, PDRectangle cropBox) {
var x1 = rectangle.getTopLeft().getX() + cropBox.getLowerLeftX() - mediaBox.getLowerLeftX() + (cropBox.toString()
.equals(mediaBox.toString()) ? cropBox.getLowerLeftX() : 0f);
var y1 = rectangle.getTopLeft().getY() + (mediaBox.getLowerLeftY() - cropBox.getLowerLeftY()) + (cropBox.toString()
.equals(mediaBox.toString()) ? cropBox.getLowerLeftY() : 0f);
var x2 = rectangle.getTopLeft().getX() + rectangle.getWidth() + cropBox.getLowerLeftX() - mediaBox.getLowerLeftX() + (cropBox.toString()
.equals(mediaBox.toString()) ? cropBox.getLowerLeftX() : 0f);
var y2 = rectangle.getTopLeft().getY() + rectangle.getHeight() - (mediaBox.getLowerLeftY() - cropBox.getLowerLeftY()) + (cropBox.toString()
.equals(mediaBox.toString()) ? cropBox.getLowerLeftY() : 0f);
// quadPoints is array of x,y coordinates in Z-like order (top-left, top-right, bottom-left,bottom-right)
// of the area to be highlighted
return new float[]{x1, y1, x2, y2, x1, y2 - rectangle.getHeight(), x2, y1 - rectangle.getHeight()};
double x4 = rectangle.getMinX();
double y4 = rectangle.getMaxY();
return Stream.of(x4, y4, x3, y3, x1, y1, x2, y2);
}

View File

@ -60,6 +60,7 @@ class BoundaryTest {
assertEquals(List.of(new Boundary(10, 12), new Boundary(12, 40), new Boundary(40, 90), new Boundary(90, 100)), startBoundary.split(List.of(12, 40, 90)));
assertEquals(List.of(new Boundary(10, 40), new Boundary(40, 100)), startBoundary.split(List.of(40)));
assertEquals(1, startBoundary.split(Collections.emptyList()).size());
assertEquals(1, startBoundary.split(List.of(startBoundary.start())).size());
assertThrows(IndexOutOfBoundsException.class, () -> startBoundary.split(Collections.singletonList(0)));
assertThrows(IndexOutOfBoundsException.class, () -> startBoundary.split(Collections.singletonList(100)));
assertThrows(IndexOutOfBoundsException.class, () -> startBoundary.split(List.of(12, 40, 100)));

View File

@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
import com.iqser.red.service.redaction.v1.server.utils.PdfDraw;
import com.iqser.red.service.redaction.v1.server.visualization.service.PdfDraw;
import lombok.SneakyThrows;