diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/Boundary.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/Boundary.java index 8f028036..d6124147 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/Boundary.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/Boundary.java @@ -92,6 +92,11 @@ public class Boundary implements Comparable { List 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; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/RectangleTransformations.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/RectangleTransformations.java index 745a7c4b..aeb94dc4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/RectangleTransformations.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/graph/factory/RectangleTransformations.java @@ -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 rectangles) { + + return rectangles.stream().map(RectangleTransformations::toRectangle2D).collect(new Rectangle2DUnion()); + } + + public static Rectangle2D rectangleUnion(List 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 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); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/mapper/DocumentDataMapper.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/mapper/DocumentDataMapper.java index eecf7183..e2a102c8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/mapper/DocumentDataMapper.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/document/mapper/DocumentDataMapper.java @@ -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(); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/utils/PdfDraw.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/visualization/service/PdfDraw.java similarity index 93% rename from redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/utils/PdfDraw.java rename to redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/visualization/service/PdfDraw.java index 95fbc5ac..ce8723a4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/utils/PdfDraw.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/visualization/service/PdfDraw.java @@ -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 rectCollection, Options options) { var pdPage = document.getPage(pageNumber - 1); + drawRectangle2DList(document, rectCollection, options, pdPage); + } + + + private static void drawRectangle2DList(PDDocument document, List rectCollection, Options options, PDPage pdPage) throws IOException { + var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true); contentStream.setStrokingColor(options.getStrokeColor()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphIntegrationTest.java index 844a5746..e8a471a1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumentGraphIntegrationTest.java @@ -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; diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 9738ef90..607050f1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -365,7 +365,7 @@ public class RedactionIntegrationTest { @Test public void titleExtraction() throws IOException { - AnalyzeRequest request = prepareStorage("files/new/crafted document.pdf"); + AnalyzeRequest request = prepareStorage("files/new/RotateTestFile.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java index b659eb5d..02101654 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java @@ -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 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 createAnnotation(RedactionLogEntry redactionLogEntry, int page, PDRectangle mediaBox, PDRectangle cropBox) { + private List createAnnotation(RedactionLogEntry redactionLogEntry, int page, int rotation, PDRectangle cropBox) { List 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 rectangles, PDRectangle mediaBox, PDRectangle cropBox) { + public static List toQuadPoints(List 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 rectangles, PDRectangle mediaBox, PDRectangle cropBox) { + public static Stream 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); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/BoundaryTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/BoundaryTest.java index 65a575da..a02a67f4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/BoundaryTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/BoundaryTest.java @@ -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))); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphVisualizationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphVisualizationTest.java index 3b99cbc2..a2215a71 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphVisualizationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentGraphVisualizationTest.java @@ -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;