Compare commits

..

20 Commits
0.16.0 ... main

Author SHA1 Message Date
Kilian Schüttler
23e63db6c5 Merge branch 'RED-8670' into 'main'
RED-8670: add features to status update

See merge request fforesight/azure-ocr-service!23
2025-01-09 11:27:33 +01:00
Kilian Schüttler
635fd4abf8 RED-8670: add features to status update 2025-01-09 11:27:33 +01:00
Kilian Schuettler
98123a5938 RED-8670: add features to status update 2024-12-17 12:33:32 +01:00
Kilian Schuettler
9bbeaf2335 RED-8670: add features to status update 2024-12-17 12:32:34 +01:00
Kilian Schuettler
b6666f6953 RED-8670: add features to status update 2024-12-17 12:30:26 +01:00
Kilian Schuettler
80dfa16103 RED-8670: add features to status update 2024-12-17 12:20:19 +01:00
Kilian Schüttler
76c8e98384 Merge branch 'RED-8670' into 'main'
RED-8670: write IDP results to file

See merge request fforesight/azure-ocr-service!22
2024-12-06 11:02:03 +01:00
Kilian Schüttler
df154cfe9c RED-8670: write IDP results to file 2024-12-06 11:02:03 +01:00
Kilian Schüttler
e7b61353bf Merge branch 'serverbuild-test' into 'main'
fix ghostscript

See merge request fforesight/azure-ocr-service!21
2024-11-27 10:53:05 +01:00
Kilian Schüttler
d63562ad24 fix ghostscript apt package 2024-11-27 10:53:05 +01:00
Kilian Schuettler
14bd6cf5c8 fix tests 2024-11-26 17:06:31 +01:00
Kilian Schuettler
ebfa55d3d1 RED-8670: add tables to idp result
* apparently i've fixed some error, where the ocr-service sometimes hangs
2024-11-26 17:05:06 +01:00
Kilian Schuettler
f06e5779f3 RED-8670: add tables to idp result
* apparently i've fixed some error, where the ocr-service sometimes hangs
2024-11-26 16:59:26 +01:00
Kilian Schuettler
1d1bd321c2 RED-8670: add tables to idp result
* apparently i've fixed some error, where the ocr-service sometimes hangs
2024-11-26 16:58:03 +01:00
Kilian Schuettler
9ed9a3c37c RED-10477: update api version
* introduce file-based caches for faster recovery after retries
 * some refactoring
2024-11-26 16:57:48 +01:00
Kilian Schüttler
eac6a49100 Merge branch 'RED-10477' into 'main'
RED-10477: update api version

See merge request fforesight/azure-ocr-service!19
2024-11-20 11:44:55 +01:00
Kilian Schüttler
0b8de28823 RED-10477: update api version 2024-11-20 11:44:55 +01:00
Kilian Schuettler
8bbc33e01b enable snuggification again, but with min size 2024-11-06 16:43:04 +01:00
Kilian Schuettler
e8483a8352 disable snuggification by default 2024-11-06 15:57:31 +01:00
Kilian Schuettler
6d6a0adcd4 upgrade pdftron-logic-commons version 2024-11-05 16:45:58 +01:00
61 changed files with 1411 additions and 457 deletions

View File

@ -1,25 +0,0 @@
package com.knecon.fforesight.service.ocr.v1.api.model;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@Getter
@Builder
@AllArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class AzureAnalyzeResult {
@Builder.Default
List<KeyValuePair> keyValuePairs = new ArrayList<>();
@Builder.Default
List<TextRegion> handWrittenText = new ArrayList<>();
@Builder.Default
List<Figure> figures = new ArrayList<>();
}

View File

@ -1,10 +1,11 @@
package com.knecon.fforesight.service.ocr.v1.api.model; package com.knecon.fforesight.service.ocr.v1.api.model;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.Builder; import lombok.Builder;
@Builder @Builder
public record Figure(Optional<TextRegion> caption, Region image) { public record Figure(TextRegion caption, Region image, List<TextRegion> footnotes) {
} }

View File

@ -0,0 +1,23 @@
package com.knecon.fforesight.service.ocr.v1.api.model;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public record IdpResult(List<KeyValuePair> keyValuePairs, List<TextRegion> handWrittenText, List<Figure> figures, List<Table> tables) {
public static IdpResult initSynchronized() {
return new IdpResult(Collections.synchronizedList(new LinkedList<>()),
Collections.synchronizedList(new LinkedList<>()),
Collections.synchronizedList(new LinkedList<>()),
Collections.synchronizedList(new LinkedList<>()));
}
public static IdpResult empty() {
return new IdpResult(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
}
}

View File

@ -1,5 +1,8 @@
package com.knecon.fforesight.service.ocr.v1.api.model; package com.knecon.fforesight.service.ocr.v1.api.model;
import java.util.Collections;
import java.util.Set;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -12,9 +15,16 @@ import lombok.NoArgsConstructor;
public class OCRStatusUpdateResponse { public class OCRStatusUpdateResponse {
private String fileId; private String fileId;
private Set<AzureOcrFeature> features;
private int numberOfPagesToOCR; private int numberOfPagesToOCR;
private int numberOfOCRedPages; private int numberOfOCRedPages;
private boolean ocrFinished; private boolean ocrFinished;
private boolean ocrStarted; private boolean ocrStarted;
public Set<AzureOcrFeature> getFeatures() {
return features == null ? Collections.emptySet() : features;
}
} }

View File

@ -5,9 +5,12 @@ import java.awt.geom.Line2D;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Stream; import java.util.stream.Stream;
public record QuadPoint(Point2D a, Point2D b, Point2D c, Point2D d) { import lombok.Getter;
public final class QuadPoint {
public enum Direction { public enum Direction {
RIGHT, RIGHT,
@ -41,7 +44,38 @@ public record QuadPoint(Point2D a, Point2D b, Point2D c, Point2D d) {
* ?|_____|? * ?|_____|?
*/ */
} }
private static final double THRESHOLD_ANGLE = Math.toRadians(5); // QuadPoint is considered straight, when its angles are below this threshold
private static final double THRESHOLD_ANGLE = Math.toRadians(5); // QuadPoint is considered straight, when its angles are below this threshold.
private final Point2D a;
private final Point2D b;
private final Point2D c;
private final Point2D d;
@Getter
private final Direction direction;
// This constructor assumes, the points form a convex polygon, I will omit the assertion for performance reasons.
public QuadPoint(Point2D a, Point2D b, Point2D c, Point2D d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.direction = calculateDirection();
}
private Direction calculateDirection() {
if (isHorizontal()) {
return a.getX() < d.getX() ? Direction.RIGHT : Direction.LEFT;
}
if (isVertical()) {
return a.getY() < d.getY() ? Direction.UP : Direction.DOWN;
}
return Direction.NONE;
}
public static QuadPoint fromRectangle2D(Rectangle2D rectangle2D) { public static QuadPoint fromRectangle2D(Rectangle2D rectangle2D) {
@ -69,7 +103,9 @@ public record QuadPoint(Point2D a, Point2D b, Point2D c, Point2D d) {
public static QuadPoint fromPolygons(List<Double> polygon) { public static QuadPoint fromPolygons(List<Double> polygon) {
assert polygon.size() == 8; if (polygon.size() != 8) {
throw new AssertionError();
}
return new QuadPoint(new Point2D.Double(polygon.get(0), polygon.get(1)), return new QuadPoint(new Point2D.Double(polygon.get(0), polygon.get(1)),
new Point2D.Double(polygon.get(6), polygon.get(7)), new Point2D.Double(polygon.get(6), polygon.get(7)),
new Point2D.Double(polygon.get(4), polygon.get(5)), new Point2D.Double(polygon.get(4), polygon.get(5)),
@ -115,18 +151,6 @@ public record QuadPoint(Point2D a, Point2D b, Point2D c, Point2D d) {
} }
public Direction getDirection() {
if (isHorizontal()) {
return a.getX() < d.getX() ? Direction.RIGHT : Direction.LEFT;
}
if (isVertical()) {
return a.getY() < d.getY() ? Direction.UP : Direction.DOWN;
}
return Direction.NONE;
}
public Stream<Line2D> asLines() { public Stream<Line2D> asLines() {
return Stream.of(new Line2D.Double(a(), b()), new Line2D.Double(b(), c()), new Line2D.Double(c(), d()), new Line2D.Double(d(), a())); return Stream.of(new Line2D.Double(a(), b()), new Line2D.Double(b(), c()), new Line2D.Double(c(), d()), new Line2D.Double(d(), a()));
@ -146,6 +170,142 @@ public record QuadPoint(Point2D a, Point2D b, Point2D c, Point2D d) {
} }
public boolean contains(double x, double y) {
// split into two triangles, test if either contains the point, assumes the QuadPoint is convex and created correctly. More specifically, the points must be in the correct order.
return triangleContains(a, b, c, x, y) || triangleContains(a, c, d, x, y);
}
/*
checks if a triangle contains a point by converting the point to barycentric coordinates using cramer's rule and then checking if the linear combination is within the bounds of the triangle.
https://en.wikipedia.org/wiki/Barycentric_coordinate_system#Barycentric_coordinates_on_triangles
*/
private boolean triangleContains(Point2D a, Point2D b, Point2D c, double x, double y) {
// area of the triangle
double denominator = ((b.getY() - c.getY()) * (a.getX() - c.getX()) + (c.getX() - b.getX()) * (a.getY() - c.getY()));
double invertedDenominator = 1.0 / denominator;
double alpha = ((b.getY() - c.getY()) * (x - c.getX()) + (c.getX() - b.getX()) * (y - c.getY())) * invertedDenominator;
double beta = ((c.getY() - a.getY()) * (x - c.getX()) + (a.getX() - c.getX()) * (y - c.getY())) * invertedDenominator;
return alpha >= 0 && beta >= 0 && alpha + beta <= 1;
}
public boolean contains(Point2D p) {
return contains(p.getX(), p.getY());
}
public boolean contains(Rectangle2D r) {
double x = r.getX();
double y = r.getY();
double maxY = r.getMaxY();
double maxX = r.getMaxX();
Point2D p1 = new Point2D.Double(x, y);
Point2D p2 = new Point2D.Double(x, maxY);
Point2D p3 = new Point2D.Double(maxX, maxY);
Point2D p4 = new Point2D.Double(maxX, y);
return contains(p1) && contains(p2) && contains(p3) && contains(p4);
}
public double getCenterX() {
return (a.getX() + b.getX() + c.getX() + d.getX()) / 4;
}
public double getCenterY() {
return (a.getY() + b.getY() + c.getY() + d.getY()) / 4;
}
public Point2D getCenter() {
return new Point2D.Double(getCenterX(), getCenterY());
}
public boolean intersects(Line2D line) {
return contains(line.getP1()) || contains(line.getP2()) || asLines().anyMatch(qLine -> qLine.intersectsLine(line));
}
public Line2D getRightLine() {
return new Line2D.Double(getTopRight(), getLowerRight());
}
public Line2D getLeftLine() {
return new Line2D.Double(getTopLeft(), getLowerLeft());
}
public Line2D getBottomLine() {
return new Line2D.Double(getLowerLeft(), getLowerRight());
}
public Line2D getTopLine() {
return new Line2D.Double(getTopLeft(), getTopRight());
}
public Point2D getTopLeft() {
return switch (direction) {
case DOWN -> a;
case LEFT -> d;
case UP -> c;
default -> b;
};
}
public Point2D getTopRight() {
return switch (direction) {
case DOWN -> b;
case LEFT -> a;
case UP -> d;
default -> c;
};
}
public Point2D getLowerRight() {
return switch (direction) {
case DOWN -> c;
case LEFT -> b;
case UP -> a;
default -> d;
};
}
public Point2D getLowerLeft() {
return switch (direction) {
case DOWN -> d;
case LEFT -> c;
case UP -> b;
default -> a;
};
}
/** /**
* Determines if the given QuadPoint aligns with this QuadPoint within a given threshold. * Determines if the given QuadPoint aligns with this QuadPoint within a given threshold.
* It does os by trying every possible combination of aligning sides. It starts with the most likely combination of ab and cd. * It does os by trying every possible combination of aligning sides. It starts with the most likely combination of ab and cd.
@ -224,4 +384,37 @@ public record QuadPoint(Point2D a, Point2D b, Point2D c, Point2D d) {
return Math.atan2(deltaY, deltaX); return Math.atan2(deltaY, deltaX);
} }
public Point2D a() {return a;}
public Point2D b() {return b;}
public Point2D c() {return c;}
public Point2D d() {return d;}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
var that = (QuadPoint) obj;
return Objects.equals(this.a, that.a) && Objects.equals(this.b, that.b) && Objects.equals(this.c, that.c) && Objects.equals(this.d, that.d);
}
@Override
public int hashCode() {
return Objects.hash(a, b, c, d);
}
} }

View File

@ -5,4 +5,9 @@ import lombok.Builder;
@Builder @Builder
public record QuadPointData(float[] values) { public record QuadPointData(float[] values) {
public QuadPoint get() {
return QuadPoint.fromData(this);
}
} }

View File

@ -0,0 +1,7 @@
package com.knecon.fforesight.service.ocr.v1.api.model;
import java.util.List;
public record Table(TextRegion caption, int numberOfCols, int numberOfRows, List<TableCell> cells, List<TextRegion> footnotes, List<Region> bboxes) {
}

View File

@ -0,0 +1,5 @@
package com.knecon.fforesight.service.ocr.v1.api.model;
public record TableCell(TextRegion textRegion, int row, int col, TableCellType kind) {
}

View File

@ -0,0 +1,5 @@
package com.knecon.fforesight.service.ocr.v1.api.model;
public enum TableCellType {
ROW_HEADER, COLUMN_HEADER, CONTENT, STUB_HEAD, DESCRIPTION
}

View File

@ -10,19 +10,18 @@ configurations {
} }
dependencies { dependencies {
api(project(":azure-ocr-service-api")) implementation(project(":azure-ocr-service-api"))
api("com.iqser.red.service:persistence-service-internal-api-v1:2.224.0") implementation("net.sourceforge.tess4j:tess4j:5.8.0")
api("net.sourceforge.tess4j:tess4j:5.8.0") implementation("com.iqser.red.commons:metric-commons:2.1.0")
api("com.iqser.red.commons:metric-commons:2.1.0") implementation("com.pdftron:PDFNet:11.0.0")
api("com.iqser.red.commons:storage-commons:2.49.0") implementation("org.apache.pdfbox:pdfbox:3.0.0")
api("com.knecon.fforesight:tenant-commons:0.30.0") implementation("org.apache.commons:commons-math3:3.6.1")
api("com.pdftron:PDFNet:10.7.0") implementation("com.amazonaws:aws-java-sdk-kms:1.12.440")
api("org.apache.pdfbox:pdfbox:3.0.0") implementation("com.google.guava:guava:31.1-jre")
api("org.apache.commons:commons-math3:3.6.1") implementation("com.knecon.fforesight:viewer-doc-processor:0.193.0")
api("com.amazonaws:aws-java-sdk-kms:1.12.440") implementation("com.azure:azure-ai-documentintelligence:1.0.0")
api("com.google.guava:guava:31.1-jre")
api("com.knecon.fforesight:viewer-doc-processor:0.177.0") implementation("com.iqser.red.commons:pdftron-logic-commons:2.32.0")
api("com.azure:azure-ai-documentintelligence:1.0.0-beta.3")
api("com.iqser.red.commons:pdftron-logic-commons:2.31.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
} }

View File

@ -6,6 +6,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import com.iqser.red.pdftronlogic.commons.InvisibleElementRemovalService;
import com.iqser.red.pdftronlogic.commons.WatermarkRemovalService;
import com.knecon.fforesight.service.viewerdoc.service.PDFTronViewerDocumentService; import com.knecon.fforesight.service.viewerdoc.service.PDFTronViewerDocumentService;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
@ -22,4 +24,18 @@ public class OcrServiceProcessorConfiguration {
return new PDFTronViewerDocumentService(registry); return new PDFTronViewerDocumentService(registry);
} }
@Bean
public InvisibleElementRemovalService invisibleElementRemovalService() {
return new InvisibleElementRemovalService();
}
@Bean
public WatermarkRemovalService watermarkRemovalService() {
return new WatermarkRemovalService();
}
} }

View File

@ -11,14 +11,16 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class OcrServiceSettings { public class OcrServiceSettings {
// Limits the number of concurrent calls to the azure API. In my very rudimentary testing, azure starts throwing "too many requests" errors at around 80/s. Higher numbers greatly improve the speed. // Limits the number of concurrent calls to azure
int concurrency = 8; int concurrency = 2;
// Limits the number of pages per call. // Limits the number of pages per call.
int batchSize = 128; int batchSize = 32;
boolean debug; // writes the ocr layer visibly to the viewer doc pdf boolean debug; // writes the ocr layer visibly to the viewer doc pdf
boolean drawTablesAsLines; // writes the tables to the PDF as invisible lines. boolean drawTablesAsLines; // writes the tables to the PDF as invisible lines.
boolean snuggify = true; // Enables bold detection using ghostscript and leptonica boolean snuggify = true; // attempts to shrink the word boxes returned by azure to fit the actual word pixels snug
boolean useCaches; // skips azure api, pdf rendering and image processing, when the files are already present
boolean azureFontStyleDetection; // omits all image processing and uses azures FONT_STYLE feature (costs 0.6ct per page)
String contentFormat; // Either markdown or text. But, for whatever reason, with markdown enabled, key-values are not written by azure.... String contentFormat; // Either markdown or text. But, for whatever reason, with markdown enabled, key-values are not written by azure....
} }

View File

@ -21,6 +21,8 @@ public class NativeLibrariesInitializer {
@Value("${pdftron.license:}") @Value("${pdftron.license:}")
private String pdftronLicense; private String pdftronLicense;
@Value("${native-libs.path:}")
private String nativeLibsPath;
@SneakyThrows @SneakyThrows
@PostConstruct @PostConstruct
@ -32,8 +34,8 @@ public class NativeLibrariesInitializer {
PDFNet.setTempPath("/tmp/pdftron"); PDFNet.setTempPath("/tmp/pdftron");
PDFNet.initialize(pdftronLicense); PDFNet.initialize(pdftronLicense);
log.info("Setting jna.library.path: {}", System.getenv("VCPKG_DYNAMIC_LIB")); log.info("Setting jna.library.path: {}", nativeLibsPath);
System.setProperty("jna.library.path", System.getenv("VCPKG_DYNAMIC_LIB")); System.setProperty("jna.library.path", nativeLibsPath);
log.info("Asserting Native Libraries loaded"); log.info("Asserting Native Libraries loaded");

View File

@ -0,0 +1,102 @@
package com.knecon.fforesight.service.ocr.processor.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import com.azure.ai.documentintelligence.models.AnalyzeResult;
import com.azure.ai.documentintelligence.models.DocumentPage;
import com.azure.ai.documentintelligence.models.DocumentSpan;
import com.azure.ai.documentintelligence.models.DocumentWord;
public class DocumentSpanLookup {
List<PageSpanLookup> documentWordLookup;
public DocumentSpanLookup(AnalyzeResult analyzeResult) {
documentWordLookup = new ArrayList<>(analyzeResult.getPages().size());
int offset = 0;
for (DocumentPage page : analyzeResult.getPages()) {
if (page.getWords() == null || page.getWords().isEmpty()) {
documentWordLookup.add(new PageSpanLookup(offset, offset, null));
}
int start = page.getWords()
.get(0).getSpan().getOffset();
DocumentSpan span = page.getWords()
.get(page.getWords().size() - 1).getSpan();
int end = span.getOffset() + span.getLength();
SpanLookup<DocumentWord> pageWords = new SpanLookup<>(page.getWords()
.stream(), DocumentWord::getSpan);
documentWordLookup.add(new PageSpanLookup(start, end, pageWords));
offset = end + 1;
}
}
public List<WordOnPage> findWordsOnPages(DocumentSpan documentSpan) {
if (documentSpan == null) {
return Collections.emptyList();
}
int firstSmallerIdx = findIdxOfFirstSmallerObject(documentSpan);
PageSpanLookup firstPage = documentWordLookup.get(firstSmallerIdx);
List<WordOnPage> wordsOnPages = new ArrayList<>();
for (int pageNumber = firstSmallerIdx; pageNumber < documentWordLookup.size(); pageNumber++) {
PageSpanLookup page = documentWordLookup.get(pageNumber);
if (page.end >= documentSpan.getOffset()) {
break;
}
firstPage.wordSpanLookup.findElementsContainedInSpan(documentSpan)
.stream()
.map(documentWord -> new WordOnPage(documentWord, firstSmallerIdx))
.forEach(wordsOnPages::add);
}
return wordsOnPages;
}
private int findIdxOfFirstSmallerObject(DocumentSpan documentSpan) {
int idx = Collections.binarySearch(documentWordLookup, new PageSpanLookup(documentSpan.getOffset(), -1, null), Comparator.comparing(PageSpanLookup::start));
if (idx >= 0) {
return idx;
} else {
int insertionPoint = -(idx + 1);
if (insertionPoint == 0) {
return -1;
}
var lastSmaller = documentWordLookup.get(insertionPoint - 1);
for (int resultIdx = insertionPoint - 2; resultIdx >= 0; resultIdx--) {
if (documentWordLookup.get(resultIdx).compareTo(lastSmaller) == 0) {
return resultIdx + 1;
}
}
return 0;
}
}
public record WordOnPage(DocumentWord documentWord, int pageNumber) {
}
private record PageSpanLookup(int start, int end, SpanLookup<DocumentWord> wordSpanLookup) implements Comparable<PageSpanLookup> {
@Override
public int compareTo(PageSpanLookup o) {
return Integer.compare(start, o.start);
}
}
}

View File

@ -1,5 +1,7 @@
package com.knecon.fforesight.service.ocr.processor.model; package com.knecon.fforesight.service.ocr.processor.model;
import java.io.File;
import net.sourceforge.lept4j.Leptonica1; import net.sourceforge.lept4j.Leptonica1;
import net.sourceforge.lept4j.Pix; import net.sourceforge.lept4j.Pix;
@ -10,4 +12,10 @@ public record ImageFile(int pageNumber, String absoluteFilePath) {
return Leptonica1.pixRead(absoluteFilePath); return Leptonica1.pixRead(absoluteFilePath);
} }
public boolean exists() {
return new File(absoluteFilePath).exists();
}
} }

View File

@ -2,16 +2,25 @@ package com.knecon.fforesight.service.ocr.processor.model;
import static com.knecon.fforesight.service.ocr.processor.utils.ListSplittingUtils.formatIntervals; import static com.knecon.fforesight.service.ocr.processor.utils.ListSplittingUtils.formatIntervals;
import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.azure.ai.documentintelligence.models.AnalyzeResult;
import com.azure.core.util.BinaryData; import com.azure.core.util.BinaryData;
import com.azure.json.JsonOptions;
import com.azure.json.JsonReader;
import com.azure.json.implementation.DefaultJsonReader;
import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.GhostScriptService;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
@RequiredArgsConstructor @RequiredArgsConstructor
@ -21,23 +30,101 @@ public final class PageBatch implements Comparable<PageBatch> {
@Getter @Getter
int index; int index;
@NonNull @NonNull
List<Integer> lookup; List<Integer> batchPageToOriginPageLookup;
@NonNull @NonNull
@Getter @Getter
Path batchDoc; Path batchDoc;
@NonNull @NonNull
@Getter @Getter
Path imagePipelineDir; Path batchDir;
@SneakyThrows
public AnalyzeResult getAzureResultCache() {
try (var in = new FileInputStream(getAzureResultCacheFile()); JsonReader reader = DefaultJsonReader.fromStream(in, new JsonOptions());) {
return AnalyzeResult.fromJson(reader);
}
}
@SneakyThrows
public File getAzureResultCacheFile() {
return batchDir.resolve("analyzeResult.json").toFile();
}
public List<ImageFile> getRenderedImageFiles() {
List<ImageFile> renderedImageFiles = new ArrayList<>();
for (int i = 0; i < batchPageToOriginPageLookup.size(); i++) {
renderedImageFiles.add(getRenderedImageFile(batchPageToOriginPageLookup.get(i), i + 1));
}
return renderedImageFiles;
}
public ImageFile getRenderedImageFile(int pageNumber, int numberInBatch) {
return new ImageFile(pageNumber, getRenderedImageNameFormat().formatted(numberInBatch));
}
public ImageFile getProcessedImageFile(int pageNumber, int numberInBatch) {
return new ImageFile(pageNumber, getProcessedImageNameFormat().formatted(numberInBatch));
}
public List<ImageFile> getProcessedImageFiles() {
List<ImageFile> processedImageFiles = new ArrayList<>();
for (int i = 0; i < batchPageToOriginPageLookup.size(); i++) {
processedImageFiles.add(getProcessedImageFile(batchPageToOriginPageLookup.get(i), i + 1));
}
return processedImageFiles;
}
public String getRenderedImageNameFormat() {
return getRenderedImageDir().resolve(getImageFormat()).toFile().toString();
}
public String getProcessedImageNameFormat() {
return getProcessedImageDir().resolve(getImageFormat()).toFile().toString();
}
private String getImageFormat() {
return "output_" + index + ".%04d" + GhostScriptService.FORMAT;
}
public Path getRenderedImageDir() {
return batchDir.resolve("rendered");
}
public Path getProcessedImageDir() {
return batchDir.resolve("processed");
}
@Override @Override
public String toString() { public String toString() {
if (size() == 1) { if (size() == 1) {
return String.format("%d", lookup.get(0)); return String.format("%d", batchPageToOriginPageLookup.get(0));
} }
List<String> intervals = formatIntervals(lookup); List<String> intervals = formatIntervals(batchPageToOriginPageLookup);
if (intervals.size() > 4) { if (intervals.size() > 4) {
intervals = intervals.subList(0, 4); intervals = intervals.subList(0, 4);
intervals.add("..."); intervals.add("...");
@ -49,46 +136,46 @@ public final class PageBatch implements Comparable<PageBatch> {
public void forEach(Consumer<? super Integer> consumer) { public void forEach(Consumer<? super Integer> consumer) {
lookup.forEach(consumer); batchPageToOriginPageLookup.forEach(consumer);
} }
public List<Integer> getAllPageNumbers() { public List<Integer> getAllPageNumbers() {
return lookup; return batchPageToOriginPageLookup;
} }
public int size() { public int size() {
return lookup.size(); return batchPageToOriginPageLookup.size();
} }
public boolean isEmpty() { public boolean isEmpty() {
return lookup.isEmpty(); return batchPageToOriginPageLookup.isEmpty();
} }
public int getPageNumber(int pageNumber) { public int getPageNumber(int pageNumber) {
return lookup.get(pageNumber - 1); return batchPageToOriginPageLookup.get(pageNumber - 1);
} }
@Override @Override
public int compareTo(PageBatch o) { public int compareTo(PageBatch o) {
if (lookup.isEmpty() && o.lookup.isEmpty()) { if (batchPageToOriginPageLookup.isEmpty() && o.batchPageToOriginPageLookup.isEmpty()) {
return 0; return 0;
} else if (lookup.isEmpty()) { } else if (batchPageToOriginPageLookup.isEmpty()) {
return 1; return 1;
} else if (o.lookup.isEmpty()) { } else if (o.batchPageToOriginPageLookup.isEmpty()) {
return -1; return -1;
} }
return Integer.compare(lookup.get(0), o.lookup.get(0)); return Integer.compare(batchPageToOriginPageLookup.get(0), o.batchPageToOriginPageLookup.get(0));
} }

View File

@ -13,7 +13,7 @@ import com.pdftron.pdf.Rect;
import lombok.SneakyThrows; import lombok.SneakyThrows;
public record PageInformation(Rectangle2D mediabox, int number, int rotationDegrees, List<Rectangle2D> wordBBoxes) { public record PageInformation(Rectangle2D mediabox, Rectangle2D cropBox, int number, int rotationDegrees, List<Rectangle2D> wordBBoxes) {
@SneakyThrows @SneakyThrows
public static Map<Integer, PageInformation> fromPDFDoc(PDFDoc pdfDoc) { public static Map<Integer, PageInformation> fromPDFDoc(PDFDoc pdfDoc) {
@ -34,8 +34,9 @@ public record PageInformation(Rectangle2D mediabox, int number, int rotationDegr
@SneakyThrows @SneakyThrows
public static PageInformation fromPage(int pageNum, Page page) { public static PageInformation fromPage(int pageNum, Page page) {
try (Rect mediaBox = page.getCropBox()) { try (Rect mediaBox = page.getCropBox(); Rect cropBox = page.getCropBox()) {
return new PageInformation(new Rectangle2D.Double(mediaBox.getX1(), mediaBox.getY1(), mediaBox.getWidth(), mediaBox.getHeight()), return new PageInformation(new Rectangle2D.Double(mediaBox.getX1(), mediaBox.getY1(), mediaBox.getWidth(), mediaBox.getHeight()),
new Rectangle2D.Double(cropBox.getX1(), cropBox.getY1(), cropBox.getWidth(), cropBox.getHeight()),
pageNum, pageNum,
page.getRotation() * 90, page.getRotation() * 90,
DocumentTextExtractor.getTextBBoxes(page)); DocumentTextExtractor.getTextBBoxes(page));

View File

@ -162,7 +162,7 @@ public class Statistics {
return batchStats.values() return batchStats.values()
.stream() .stream()
.mapToLong(BatchStats::getWritingTextDuration) .mapToLong(BatchStats::getMappingResultDuration)
.toArray(); .toArray();
} }

View File

@ -1,8 +1,9 @@
package com.knecon.fforesight.service.ocr.processor.service; package com.knecon.fforesight.service.ocr.processor.service;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import org.slf4j.MDC; import org.slf4j.MDC;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -22,6 +23,7 @@ import com.pdftron.pdf.PDFDoc;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -67,6 +69,10 @@ public class AsyncOcrService {
private void beginAnalysis(BinaryData data, BatchContext batchContext, Set<AzureOcrFeature> features) throws InterruptedException { private void beginAnalysis(BinaryData data, BatchContext batchContext, Set<AzureOcrFeature> features) throws InterruptedException {
if (settings.isUseCaches() && batchContext.batch().getAzureResultCacheFile().exists()) {
handleCached(batchContext);
}
batchContext.supervisor.enterConcurrency(batchContext.batch); batchContext.supervisor.enterConcurrency(batchContext.batch);
batchContext.supervisor.logUploadStart(batchContext.batch, data.getLength()); batchContext.supervisor.logUploadStart(batchContext.batch, data.getLength());
@ -85,16 +91,39 @@ public class AsyncOcrService {
if (LongRunningOperationStatus.SUCCESSFULLY_COMPLETED == response.getStatus()) { if (LongRunningOperationStatus.SUCCESSFULLY_COMPLETED == response.getStatus()) {
return response.getFinalResult(); return response.getFinalResult();
} }
return Mono.error(new IllegalStateException("Polling completed unsuccessfully with status: " + response.getStatus())); String message = "Polling completed unsuccessfully with status: " + response.getStatus();
log.error(message);
return Mono.error(new IllegalStateException(message));
}).subscribe(finalResult -> handleSuccessful(finalResult, batchContext),// }).subscribe(finalResult -> handleSuccessful(finalResult, batchContext),//
ex -> handleError(ex, batchContext),// ex -> handleError(ex, batchContext),//
() -> handleCompleted(batchContext)); () -> handleCompleted(batchContext));
} }
@SneakyThrows
private static void handleCached(BatchContext batchContext) {
var mdcContext = MDC.getCopyOfContextMap();
Thread thread = new Thread(() -> {
MDC.setContextMap(mdcContext);
log.info("Batch {}: Using cached ocr result", batchContext.batch.getIndex());
batchContext.batchStats().finishUpload();
batchContext.batchStats().finishApiWait();
batchContext.supervisor.logPageSuccess(batchContext.batch());
try {
batchContext.layerFactory.processAnalyzeResult(batchContext.batch(), batchContext.batch().getAzureResultCache());
} catch (InterruptedException e) {
batchContext.supervisor.logPageError(batchContext.batch, e);
}
});
thread.start();
}
private static void handleCompleted(BatchContext batchContext) { private static void handleCompleted(BatchContext batchContext) {
log.info("Completed batch {} with pages {}", batchContext.batch.getIndex(), batchContext.batch); log.info("Batch {}: Completed with pages {}", batchContext.batch.getIndex(), batchContext.batch);
} }
@ -109,10 +138,8 @@ public class AsyncOcrService {
batchContext.supervisor.leaveConcurrency(batchContext.batch); batchContext.supervisor.leaveConcurrency(batchContext.batch);
try { try {
mapper.writeValue(batchContext.batch().getAzureResultCacheFile(), finalResult);
batchContext.supervisor.logPageSuccess(batchContext.batch()); batchContext.supervisor.logPageSuccess(batchContext.batch());
if (settings.isDebug()) {
mapper.writeValue(batchContext.batch().getImagePipelineDir().resolve("azure_result_%d.json" .formatted(batchContext.batch().getIndex())).toFile(), finalResult);
}
batchContext.layerFactory.processAnalyzeResult(batchContext.batch(), finalResult); batchContext.layerFactory.processAnalyzeResult(batchContext.batch(), finalResult);
} catch (Exception e) { } catch (Exception e) {
handleError(e, batchContext); handleError(e, batchContext);

View File

@ -6,15 +6,14 @@ import java.util.Set;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.events.Event;
import com.azure.ai.documentintelligence.DocumentIntelligenceAsyncClient; import com.azure.ai.documentintelligence.DocumentIntelligenceAsyncClient;
import com.azure.ai.documentintelligence.DocumentIntelligenceClientBuilder; import com.azure.ai.documentintelligence.DocumentIntelligenceClientBuilder;
import com.azure.ai.documentintelligence.models.AnalyzeDocumentRequest; import com.azure.ai.documentintelligence.models.AnalyzeDocumentOptions;
import com.azure.ai.documentintelligence.models.AnalyzeOperationDetails;
import com.azure.ai.documentintelligence.models.AnalyzeResult; import com.azure.ai.documentintelligence.models.AnalyzeResult;
import com.azure.ai.documentintelligence.models.AnalyzeResultOperation;
import com.azure.ai.documentintelligence.models.ContentFormat;
import com.azure.ai.documentintelligence.models.DocumentAnalysisFeature; import com.azure.ai.documentintelligence.models.DocumentAnalysisFeature;
import com.azure.ai.documentintelligence.models.DocumentContentFormat;
import com.azure.ai.documentintelligence.models.StringIndexType; import com.azure.ai.documentintelligence.models.StringIndexType;
import com.azure.core.credential.AzureKeyCredential; import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.util.BinaryData; import com.azure.core.util.BinaryData;
@ -45,28 +44,23 @@ public class AzureOcrResource {
@SneakyThrows @SneakyThrows
public PollerFlux<AnalyzeResultOperation, AnalyzeResult> callAzureAsync(BinaryData data, Set<AzureOcrFeature> features) { public PollerFlux<AnalyzeOperationDetails, AnalyzeResult> callAzureAsync(BinaryData data, Set<AzureOcrFeature> features) {
AnalyzeDocumentRequest analyzeRequest = new AnalyzeDocumentRequest().setBase64Source(data.toBytes()); AnalyzeDocumentOptions analyzeDocumentOptions = new AnalyzeDocumentOptions(data.toBytes());
analyzeDocumentOptions.setStringIndexType(StringIndexType.UTF16_CODE_UNIT);
return asyncClient.beginAnalyzeDocument(getModelId(features), analyzeDocumentOptions.setDocumentAnalysisFeatures(buildFeatures(features));
null, analyzeDocumentOptions.setOutputContentFormat(buildContentFormat());
null, return asyncClient.beginAnalyzeDocument(getModelId(features), analyzeDocumentOptions);
StringIndexType.UTF16CODE_UNIT,
buildFeatures(features),
null,
buildContentFormat(),
analyzeRequest);
} }
private ContentFormat buildContentFormat() { private DocumentContentFormat buildContentFormat() {
if (Objects.equal(settings.getContentFormat(), "markdown")) { if (Objects.equal(settings.getContentFormat(), "markdown")) {
return ContentFormat.MARKDOWN; return DocumentContentFormat.MARKDOWN;
} }
return ContentFormat.TEXT; return DocumentContentFormat.TEXT;
} }
@ -86,6 +80,9 @@ public class AzureOcrResource {
if (features.contains(AzureOcrFeature.IDP)) { if (features.contains(AzureOcrFeature.IDP)) {
azureFeatures.add(DocumentAnalysisFeature.KEY_VALUE_PAIRS); azureFeatures.add(DocumentAnalysisFeature.KEY_VALUE_PAIRS);
} }
if (settings.isAzureFontStyleDetection() && features.contains(AzureOcrFeature.FONT_STYLE_DETECTION)) {
azureFeatures.add(DocumentAnalysisFeature.STYLE_FONT);
}
azureFeatures.add(DocumentAnalysisFeature.BARCODES); azureFeatures.add(DocumentAnalysisFeature.BARCODES);
return azureFeatures; return azureFeatures;

View File

@ -1,7 +1,12 @@
package com.knecon.fforesight.service.ocr.processor.service; package com.knecon.fforesight.service.ocr.processor.service;
import static com.knecon.fforesight.service.ocr.processor.utils.ListSplittingUtils.formatIntervals;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -18,9 +23,10 @@ import com.pdftron.sdf.SDFDoc;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@ -29,26 +35,39 @@ public class BatchFactory {
OcrServiceSettings settings; OcrServiceSettings settings;
public static String formatBatchFilename(int number) { @SneakyThrows
public List<PageBatch> splitIntoBatches(PDFDoc pdfDoc, OcrExecutionSupervisor supervisor, Set<AzureOcrFeature> features, Path runDir) {
return "batch_%d.pdf".formatted(number); Set<Integer> pagesToProcess = findPagesToProcess(pdfDoc, features);
supervisor.logImageExtractionFinished(pdfDoc.getPageCount(), pagesToProcess.size());
List<PageBatch> batches = buildBatches(pdfDoc, supervisor, features, runDir, pagesToProcess);
if (batches.size() > 1) {
log.info("Split {} pages to process into {} batches", pagesToProcess.size(), batches.size());
}
return batches;
} }
@SneakyThrows @SneakyThrows
public List<PageBatch> splitIntoBatches(PDFDoc pdfDoc, OcrExecutionSupervisor supervisor, Set<AzureOcrFeature> features, Path fileDir) { public Set<Integer> findPagesToProcess(PDFDoc pdfDoc, Set<AzureOcrFeature> features) {
Set<Integer> pagesWithImages = ImageDetectionService.findPagesToProcess(pdfDoc, features); if (features.contains(AzureOcrFeature.ALL_PAGES)) {
supervisor.logImageExtractionFinished(pdfDoc.getPageCount(), pagesWithImages.size()); Set<Integer> pages = new HashSet<>();
for (int i = 1; i <= pdfDoc.getPageCount(); i++) {
pages.add(i);
}
return Collections.unmodifiableSet(pages);
}
return buildBatches(pdfDoc, supervisor, features, fileDir, pagesWithImages); return ImageDetectionService.findPagesWithImages(pdfDoc);
} }
public List<PageBatch> buildBatches(PDFDoc pdfDoc, public List<PageBatch> buildBatches(PDFDoc pdfDoc,
OcrExecutionSupervisor supervisor, OcrExecutionSupervisor supervisor,
Set<AzureOcrFeature> features, Set<AzureOcrFeature> features,
Path fileDir, Path runDir,
Set<Integer> pagesWithImages) throws PDFNetException { Set<Integer> pagesWithImages) throws PDFNetException {
List<PageBatch> batches = new ArrayList<>(); List<PageBatch> batches = new ArrayList<>();
@ -60,30 +79,48 @@ public class BatchFactory {
} }
numbersForCurrentBatch.add(pageNumber); numbersForCurrentBatch.add(pageNumber);
if (numbersForCurrentBatch.size() == settings.getBatchSize()) { if (numbersForCurrentBatch.size() == settings.getBatchSize()) {
batches.add(create(batches.size(), pdfDoc, numbersForCurrentBatch, fileDir)); batches.add(create(batches.size(), pdfDoc, numbersForCurrentBatch, runDir));
numbersForCurrentBatch = new ArrayList<>(); numbersForCurrentBatch = new ArrayList<>();
} }
} }
if (!numbersForCurrentBatch.isEmpty()) { if (!numbersForCurrentBatch.isEmpty()) {
batches.add(create(batches.size(), pdfDoc, numbersForCurrentBatch, fileDir)); batches.add(create(batches.size(), pdfDoc, numbersForCurrentBatch, runDir));
} }
return batches; return batches;
} }
@SneakyThrows @SneakyThrows
public static PageBatch create(int number, PDFDoc pdfDoc, List<Integer> pageNumbers, Path fileDir) { public static PageBatch create(int number, PDFDoc pdfDoc, List<Integer> pageNumbers, Path runDir) {
if (pageNumbers.isEmpty()) { if (pageNumbers.isEmpty()) {
throw new IllegalArgumentException("pageNumbers must not be empty"); throw new IllegalArgumentException("pageNumbers must not be empty");
} }
Path batchDir = formatBatchDir(number, pageNumbers, runDir);
Files.createDirectories(batchDir);
Path batchDocPath = fileDir.resolve(formatBatchFilename(number)); Path batchDocPath = batchDir.resolve("batch.pdf");
try (var batchDoc = extractBatchDocument(pdfDoc, pageNumbers)) { try (var batchDoc = extractBatchDocument(pdfDoc, pageNumbers)) {
Optimizer.optimize(batchDoc); Optimizer.optimize(batchDoc);
batchDoc.save(batchDocPath.toFile().toString(), SDFDoc.SaveMode.LINEARIZED, null); batchDoc.save(batchDocPath.toFile().toString(), SDFDoc.SaveMode.LINEARIZED, null);
} }
return new PageBatch(number, pageNumbers, batchDocPath, fileDir); PageBatch batch = new PageBatch(number, pageNumbers, batchDocPath, batchDir);
Files.createDirectories(batch.getRenderedImageDir());
Files.createDirectories(batch.getProcessedImageDir());
return batch;
}
private static Path formatBatchDir(int number, List<Integer> pageNumbers, Path runDir) {
List<String> intervals = formatIntervals(pageNumbers);
if (intervals.size() > 4) {
intervals = intervals.subList(0, 4);
intervals.add("...");
}
String batchName = String.join(", ", intervals);
return runDir.resolve("batch_%04d_%s".formatted(number, batchName));
} }

View File

@ -10,7 +10,7 @@ public class BatchStats {
private long apiWaitTimestamp = -1; private long apiWaitTimestamp = -1;
private long imageUploadTimestamp = -1; private long imageUploadTimestamp = -1;
private long writingTextTimestamp = -1; private long mappingResultTimestamp = -1;
private long batchRenderTimestamp = -1; private long batchRenderTimestamp = -1;
@ -38,9 +38,9 @@ public class BatchStats {
} }
public void finishWritingText() { public void finishMappingResult() {
writingTextTimestamp = System.currentTimeMillis(); mappingResultTimestamp = System.currentTimeMillis();
} }
@ -50,15 +50,33 @@ public class BatchStats {
} }
public boolean isApiWaitFinished() {
return apiWaitTimestamp > 0;
}
public boolean isMappingResultFinished() {
return mappingResultTimestamp > 0;
}
public boolean isBatchRenderFinished() {
return batchRenderTimestamp > 0;
}
public long getApiWaitDuration() {return this.apiWaitTimestamp - imageUploadTimestamp;} public long getApiWaitDuration() {return this.apiWaitTimestamp - imageUploadTimestamp;}
public long getImageUploadDuration() {return this.imageUploadTimestamp - batchRenderTimestamp;} public long getImageUploadDuration() {return this.imageUploadTimestamp - batchRenderTimestamp;}
public long getWritingTextDuration() {return this.writingTextTimestamp - apiWaitTimestamp;} public long getMappingResultDuration() {return this.mappingResultTimestamp - apiWaitTimestamp;}
public long getBatchRenderDuration() {return this.batchRenderTimestamp - startTimestamp;} public long getBatchRenderDuration() {return startTimestamp - this.batchRenderTimestamp;}
} }

View File

@ -1,16 +1,24 @@
package com.knecon.fforesight.service.ocr.processor.service; package com.knecon.fforesight.service.ocr.processor.service;
import java.util.Set;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.ocr.v1.api.model.AzureOcrFeature;
import com.knecon.fforesight.service.ocr.v1.api.model.DocumentRequest;
@Service @Service
public interface IOcrMessageSender { public interface IOcrMessageSender {
void sendUpdate(String fileId, int finishedImages, int totalImages); void sendUpdate(String fileId, int finishedImages, int totalImages, Set<AzureOcrFeature> features);
void sendOCRStarted(String fileId);
void sendOcrFinished(String fileId, int totalImages); void sendOCRStarted(String fileId, Set<AzureOcrFeature> features);
void sendOcrResponse(String dossierId, String fileId);
void sendOcrFinished(String fileId, int totalImages, Set<AzureOcrFeature> features);
void sendOcrResponse(DocumentRequest request);
} }

View File

@ -24,21 +24,7 @@ public class ImageDetectionService {
@SneakyThrows @SneakyThrows
public Set<Integer> findPagesToProcess(PDFDoc pdfDoc, Set<AzureOcrFeature> features) { public Set<Integer> findPagesWithImages(PDFDoc pdfDoc) {
if (features.contains(AzureOcrFeature.ALL_PAGES)) {
Set<Integer> pages = new HashSet<>();
for (int i = 1; i <= pdfDoc.getPageCount(); i++) {
pages.add(i);
}
return Collections.unmodifiableSet(pages);
}
return findPagesWithImages(pdfDoc);
}
private Set<Integer> findPagesWithImages(PDFDoc pdfDoc) throws PDFNetException {
Set<Integer> pagesWithImages = new HashSet<>(); Set<Integer> pagesWithImages = new HashSet<>();
try (ElementReader reader = new ElementReader()) { try (ElementReader reader = new ElementReader()) {

View File

@ -5,6 +5,7 @@ import static com.knecon.fforesight.service.ocr.processor.model.Statistics.human
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
@ -13,6 +14,7 @@ import java.util.Set;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.iqser.red.pdftronlogic.commons.InvisibleElementRemovalService; import com.iqser.red.pdftronlogic.commons.InvisibleElementRemovalService;
import com.iqser.red.pdftronlogic.commons.OCGWatermarkRemovalService; import com.iqser.red.pdftronlogic.commons.OCGWatermarkRemovalService;
import com.iqser.red.pdftronlogic.commons.WatermarkRemovalService; import com.iqser.red.pdftronlogic.commons.WatermarkRemovalService;
@ -38,8 +40,6 @@ import lombok.extern.slf4j.Slf4j;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class OCRService { public class OCRService {
public static final String IMAGE_PIPELINE_DIR = "image_pipeline";
public static final String AZURE_OUTPUT_DIR = "azure_output";
IOcrMessageSender ocrMessageSender; IOcrMessageSender ocrMessageSender;
WatermarkRemovalService watermarkRemovalService; WatermarkRemovalService watermarkRemovalService;
InvisibleElementRemovalService invisibleElementRemovalService; InvisibleElementRemovalService invisibleElementRemovalService;
@ -47,6 +47,7 @@ public class OCRService {
BatchFactory batchFactory; BatchFactory batchFactory;
AsyncOcrService asyncOcrService; AsyncOcrService asyncOcrService;
OcrServiceSettings settings; OcrServiceSettings settings;
ObjectMapper mapper;
/** /**
@ -59,10 +60,10 @@ public class OCRService {
* @param tmpDir working directory for all files * @param tmpDir working directory for all files
* @param documentFile the file to perform ocr on, results are written invisibly * @param documentFile the file to perform ocr on, results are written invisibly
* @param viewerDocumentFile debugging file, results are written visibly in an optional content group * @param viewerDocumentFile debugging file, results are written visibly in an optional content group
* @param analyzeResultFile result file with additional information * @param idpResultFile result file with additional information
*/ */
@Observed(name = "OCRService", contextualName = "run-ocr-on-document") @Observed(name = "OCRService", contextualName = "run-ocr-on-document")
public void runOcrOnDocument(String dossierId, String fileId, Set<AzureOcrFeature> features, Path tmpDir, File documentFile, File viewerDocumentFile, File analyzeResultFile) { public void runOcrOnDocument(String dossierId, String fileId, Set<AzureOcrFeature> features, Path tmpDir, File documentFile, File viewerDocumentFile, File idpResultFile) {
if (features.contains(AzureOcrFeature.REMOVE_WATERMARKS)) { if (features.contains(AzureOcrFeature.REMOVE_WATERMARKS)) {
removeWatermark(documentFile); removeWatermark(documentFile);
@ -70,10 +71,9 @@ public class OCRService {
removeInvisibleElements(documentFile); removeInvisibleElements(documentFile);
log.info("Starting OCR");
long ocrStart = System.currentTimeMillis(); long ocrStart = System.currentTimeMillis();
Statistics stats = runOcr(tmpDir, documentFile, viewerDocumentFile, fileId, dossierId, analyzeResultFile, features).getStatistics(); Statistics stats = runOcr(tmpDir, documentFile, viewerDocumentFile, fileId, dossierId, idpResultFile, features).getStatistics();
long ocrEnd = System.currentTimeMillis(); long ocrEnd = System.currentTimeMillis();
log.info("OCR successful, took {}", humanizeDuration(ocrEnd - ocrStart)); log.info("OCR successful, took {}", humanizeDuration(ocrEnd - ocrStart));
@ -120,28 +120,22 @@ public class OCRService {
@SneakyThrows @SneakyThrows
public OcrExecutionSupervisor runOcr(Path tmpDir, public OcrExecutionSupervisor runOcr(Path runDir,
File documentFile, File documentFile,
File viewerDocumentFile, File viewerDocumentFile,
String fileId, String fileId,
String dossierId, String dossierId,
File analyzeResultFile, File idpResultFile,
Set<AzureOcrFeature> features) { Set<AzureOcrFeature> features) {
Path tmpImageDir = tmpDir.resolve(IMAGE_PIPELINE_DIR);
Path azureOutputDir = tmpDir.resolve(AZURE_OUTPUT_DIR);
Files.createDirectories(azureOutputDir);
Files.createDirectories(tmpImageDir);
try (var in = new FileInputStream(documentFile); PDFDoc pdfDoc = new PDFDoc(in)) { try (var in = new FileInputStream(documentFile); PDFDoc pdfDoc = new PDFDoc(in)) {
OCGWatermarkRemovalService.removeWatermarks(pdfDoc); OCGWatermarkRemovalService.removeWatermarks(pdfDoc);
OcrExecutionSupervisor supervisor = new OcrExecutionSupervisor(pdfDoc.getPageCount(), ocrMessageSender, fileId, settings); OcrExecutionSupervisor supervisor = new OcrExecutionSupervisor(pdfDoc.getPageCount(), ocrMessageSender, fileId, settings, features);
supervisor.getStatistics().setStart(); supervisor.getStatistics().setStart();
List<PageBatch> batches = batchFactory.splitIntoBatches(pdfDoc, supervisor, features, tmpImageDir); List<PageBatch> batches = batchFactory.splitIntoBatches(pdfDoc, supervisor, features, runDir);
OcrResult ocrResult = asyncOcrService.awaitOcr(pdfDoc, supervisor, features, batches); OcrResult ocrResult = asyncOcrService.awaitOcr(pdfDoc, supervisor, features, batches);
@ -153,6 +147,10 @@ public class OCRService {
RotationCorrectionUtility.rotatePages(viewerDocumentFile.toPath(), viewerDocumentFile.toPath(), ocrResult.anglesPerPage()); RotationCorrectionUtility.rotatePages(viewerDocumentFile.toPath(), viewerDocumentFile.toPath(), ocrResult.anglesPerPage());
} }
if (features.contains(AzureOcrFeature.IDP)) {
saveIdpResultFile(idpResultFile, ocrResult);
}
supervisor.getStatistics().drawingPdfFinished(); supervisor.getStatistics().drawingPdfFinished();
supervisor.sendFinished(); supervisor.sendFinished();
@ -162,4 +160,12 @@ public class OCRService {
} }
private void saveIdpResultFile(File idpResultFile, OcrResult ocrResult) throws IOException {
try (var out = new FileOutputStream(idpResultFile)) {
mapper.writeValue(out, ocrResult.idpResult());
}
}
} }

View File

@ -5,6 +5,7 @@ import static com.knecon.fforesight.service.ocr.processor.model.Statistics.human
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@ -13,6 +14,7 @@ import java.util.concurrent.CountDownLatch;
import com.knecon.fforesight.service.ocr.processor.OcrServiceSettings; import com.knecon.fforesight.service.ocr.processor.OcrServiceSettings;
import com.knecon.fforesight.service.ocr.processor.model.PageBatch; import com.knecon.fforesight.service.ocr.processor.model.PageBatch;
import com.knecon.fforesight.service.ocr.processor.model.Statistics; import com.knecon.fforesight.service.ocr.processor.model.Statistics;
import com.knecon.fforesight.service.ocr.v1.api.model.AzureOcrFeature;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
@ -38,12 +40,15 @@ public class OcrExecutionSupervisor {
String fileId; String fileId;
Set<AzureOcrFeature> features;
public OcrExecutionSupervisor(int totalPageCount, IOcrMessageSender ocrMessageSender, String fileId, OcrServiceSettings settings) {
public OcrExecutionSupervisor(int totalPageCount, IOcrMessageSender ocrMessageSender, String fileId, OcrServiceSettings settings, Set<AzureOcrFeature> features) {
this.totalPageCount = totalPageCount; this.totalPageCount = totalPageCount;
this.ocrMessageSender = ocrMessageSender; this.ocrMessageSender = ocrMessageSender;
this.fileId = fileId; this.fileId = fileId;
this.features = features;
this.errorPages = Collections.synchronizedSet(new HashSet<>()); this.errorPages = Collections.synchronizedSet(new HashSet<>());
this.countDownPagesToProcess = new CountDownLatch(totalPageCount); this.countDownPagesToProcess = new CountDownLatch(totalPageCount);
this.statistics = new Statistics(); this.statistics = new Statistics();
@ -65,10 +70,10 @@ public class OcrExecutionSupervisor {
} }
public void logImageExtractionFinished(int numberOfPages, int numberOfImages) { public void logImageExtractionFinished(int numberOfPages, int numberOfPagesToProcess) {
statistics.imageExtractionFinished(); statistics.imageExtractionFinished();
log.info("Images found on {}/{} pages in {}", numberOfImages, numberOfPages, humanizeDuration(statistics.getImageExtractionDuration())); log.info("Images found on {}/{} pages in {}", numberOfPagesToProcess, numberOfPages, humanizeDuration(statistics.getImageExtractionDuration()));
} }
@ -85,7 +90,7 @@ public class OcrExecutionSupervisor {
if (!statistics.getBatchStats(pageRange).isUploadFinished()) { if (!statistics.getBatchStats(pageRange).isUploadFinished()) {
log.info("Batch {}: Pages {} is in progress", pageRange.getIndex(), pageRange); log.info("Batch {}: Pages {} is in progress", pageRange.getIndex(), pageRange);
statistics.getBatchStats(pageRange).finishUpload(); statistics.getBatchStats(pageRange).finishUpload();
ocrMessageSender.sendUpdate(fileId, processedPages(), getTotalPageCount()); ocrMessageSender.sendUpdate(fileId, processedPages(), getTotalPageCount(), features);
} else { } else {
log.debug("Batch {}: Pages {} still in progress", pageRange.getIndex(), pageRange); log.debug("Batch {}: Pages {} still in progress", pageRange.getIndex(), pageRange);
} }
@ -95,15 +100,16 @@ public class OcrExecutionSupervisor {
public void finishMappingResult(PageBatch batch) { public void finishMappingResult(PageBatch batch) {
batch.forEach(pageIndex -> countDownPagesToProcess.countDown()); batch.forEach(pageIndex -> countDownPagesToProcess.countDown());
statistics.getBatchStats(batch).finishWritingText(); statistics.getBatchStats(batch).finishMappingResult();
ocrMessageSender.sendUpdate(fileId, this.processedPages(), getTotalPageCount()); ocrMessageSender.sendUpdate(fileId, this.processedPages(), getTotalPageCount(), features);
log.info("Batch {}: Finished mapping result with pages {}", batch.getIndex(), batch);
} }
public void logPageSkipped(Integer pageIndex) { public void logPageSkipped(Integer pageIndex) {
this.countDownPagesToProcess.countDown(); this.countDownPagesToProcess.countDown();
ocrMessageSender.sendUpdate(fileId, this.processedPages(), getTotalPageCount()); ocrMessageSender.sendUpdate(fileId, this.processedPages(), getTotalPageCount(), features);
log.debug("{}/{}: No images to ocr on page {}", processedPages(), getTotalPageCount(), pageIndex); log.debug("{}/{}: No images to ocr on page {}", processedPages(), getTotalPageCount(), pageIndex);
} }
@ -113,7 +119,7 @@ public class OcrExecutionSupervisor {
this.errorPages.add(batch); this.errorPages.add(batch);
batch.forEach(pageIndex -> this.countDownPagesToProcess.countDown()); batch.forEach(pageIndex -> this.countDownPagesToProcess.countDown());
ocrMessageSender.sendUpdate(fileId, this.processedPages(), getTotalPageCount()); ocrMessageSender.sendUpdate(fileId, this.processedPages(), getTotalPageCount(), features);
log.error("{}/{}: Error occurred in batch {} with pages {}", processedPages(), getTotalPageCount(), batch.getIndex(), batch, e); log.error("{}/{}: Error occurred in batch {} with pages {}", processedPages(), getTotalPageCount(), batch.getIndex(), batch, e);
} }
@ -127,7 +133,29 @@ public class OcrExecutionSupervisor {
private int processedPages() { private int processedPages() {
return (int) (totalPageCount - countDownPagesToProcess.getCount()); if (countDownPagesToProcess.getCount() == 0) {
return totalPageCount;
}
int processedPages = 0;
for (Map.Entry<PageBatch, BatchStats> entry : statistics.getBatchStats().entrySet()) {
PageBatch pageBatch = entry.getKey();
BatchStats batchStats = entry.getValue();
float percentage = 0;
if (batchStats.isBatchRenderFinished()) {
percentage += 0.1f;
}
if (batchStats.isUploadFinished()) {
percentage += 0.3f;
}
if (batchStats.isApiWaitFinished()) {
percentage += 0.3f;
}
if (batchStats.isMappingResultFinished()) {
percentage += 0.3f;
}
processedPages += (int) (pageBatch.size() * percentage);
}
return processedPages;
} }
@ -144,7 +172,7 @@ public class OcrExecutionSupervisor {
requireNoErrors(); requireNoErrors();
log.info("{}/{}: Finished OCR on all pages", getTotalPageCount(), getTotalPageCount()); log.info("{}/{}: Finished OCR on all pages", getTotalPageCount(), getTotalPageCount());
ocrMessageSender.sendOcrFinished(fileId, getTotalPageCount()); ocrMessageSender.sendOcrFinished(fileId, getTotalPageCount(), features);
} }

View File

@ -1,4 +1,4 @@
package com.knecon.fforesight.service.ocr.processor.visualizations; package com.knecon.fforesight.service.ocr.processor.service;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D; import java.awt.geom.Line2D;
@ -18,13 +18,13 @@ import java.util.stream.Stream;
import com.azure.ai.documentintelligence.models.AnalyzeResult; import com.azure.ai.documentintelligence.models.AnalyzeResult;
import com.azure.ai.documentintelligence.models.BoundingRegion; import com.azure.ai.documentintelligence.models.BoundingRegion;
import com.azure.ai.documentintelligence.models.DocumentFontStyle;
import com.azure.ai.documentintelligence.models.DocumentPage; import com.azure.ai.documentintelligence.models.DocumentPage;
import com.azure.ai.documentintelligence.models.DocumentSpan; import com.azure.ai.documentintelligence.models.DocumentSpan;
import com.azure.ai.documentintelligence.models.DocumentStyle; import com.azure.ai.documentintelligence.models.DocumentStyle;
import com.azure.ai.documentintelligence.models.DocumentTable; import com.azure.ai.documentintelligence.models.DocumentTable;
import com.azure.ai.documentintelligence.models.DocumentTableCell; import com.azure.ai.documentintelligence.models.DocumentTableCell;
import com.azure.ai.documentintelligence.models.DocumentWord; import com.azure.ai.documentintelligence.models.DocumentWord;
import com.azure.ai.documentintelligence.models.FontWeight;
import com.google.common.base.Functions; import com.google.common.base.Functions;
import com.knecon.fforesight.service.ocr.processor.OcrServiceSettings; import com.knecon.fforesight.service.ocr.processor.OcrServiceSettings;
import com.knecon.fforesight.service.ocr.processor.model.ImageFile; import com.knecon.fforesight.service.ocr.processor.model.ImageFile;
@ -32,11 +32,12 @@ import com.knecon.fforesight.service.ocr.processor.model.PageBatch;
import com.knecon.fforesight.service.ocr.processor.model.PageInformation; import com.knecon.fforesight.service.ocr.processor.model.PageInformation;
import com.knecon.fforesight.service.ocr.processor.model.SpanLookup; import com.knecon.fforesight.service.ocr.processor.model.SpanLookup;
import com.knecon.fforesight.service.ocr.processor.model.TextPositionInImage; import com.knecon.fforesight.service.ocr.processor.model.TextPositionInImage;
import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.BBoxSnuggificationService;
import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.FontStyleDetector; import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.FontStyleDetector;
import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.ImageProcessingPipeline; import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.ImageProcessingPipeline;
import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.ImageProcessingSupervisor; import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.ImageProcessingSupervisor;
import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.BBoxSnuggificationService;
import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.StrokeWidthCalculator; import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.StrokeWidthCalculator;
import com.knecon.fforesight.service.ocr.processor.visualizations.WritableOcrResult;
import com.knecon.fforesight.service.ocr.processor.visualizations.fonts.FontMetricsProvider; import com.knecon.fforesight.service.ocr.processor.visualizations.fonts.FontMetricsProvider;
import com.knecon.fforesight.service.ocr.processor.visualizations.fonts.FontStyle; import com.knecon.fforesight.service.ocr.processor.visualizations.fonts.FontStyle;
import com.knecon.fforesight.service.ocr.processor.visualizations.fonts.Type0FontMetricsProvider; import com.knecon.fforesight.service.ocr.processor.visualizations.fonts.Type0FontMetricsProvider;
@ -56,7 +57,7 @@ import net.sourceforge.lept4j.util.LeptUtils;
@Slf4j @Slf4j
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class WritableOcrResultFactory { public class OcrResultPostProcessingPipeline {
@Getter @Getter
Map<Integer, AffineTransform> resultToPageTransforms; Map<Integer, AffineTransform> resultToPageTransforms;
@ -67,10 +68,10 @@ public class WritableOcrResultFactory {
@SneakyThrows @SneakyThrows
public WritableOcrResultFactory(Map<Integer, PageInformation> pageInformation, public OcrResultPostProcessingPipeline(Map<Integer, PageInformation> pageInformation,
ImageProcessingPipeline imageProcessingPipeline, ImageProcessingPipeline imageProcessingPipeline,
OcrServiceSettings settings, OcrServiceSettings settings,
Set<AzureOcrFeature> features) { Set<AzureOcrFeature> features) {
this.imageProcessingPipeline = imageProcessingPipeline; this.imageProcessingPipeline = imageProcessingPipeline;
this.pageInformation = pageInformation; this.pageInformation = pageInformation;
@ -80,14 +81,9 @@ public class WritableOcrResultFactory {
} }
public List<WritableOcrResult> buildOcrResultToWrite(AnalyzeResult analyzeResult, PageBatch batch) throws InterruptedException { public List<WritableOcrResult> processAnalyzeResult(AnalyzeResult analyzeResult, PageBatch batch) throws InterruptedException {
Map<Integer, Double> anglesPerPage = analyzeResult.getPages() ImageProcessingSupervisor imageSupervisor = renderImagesIfNecessary(analyzeResult, batch);
.stream()
.collect(Collectors.toMap(DocumentPage::getPageNumber, documentPage -> -documentPage.getAngle()));
RotationCorrectionUtility.rotatePages(batch.getBatchDoc(), batch.getBatchDoc(), anglesPerPage);
ImageProcessingSupervisor imageSupervisor = imageProcessingPipeline.addToPipeline(batch);
List<WritableOcrResult> writableOcrResultList = new ArrayList<>(); List<WritableOcrResult> writableOcrResultList = new ArrayList<>();
@ -110,10 +106,35 @@ public class WritableOcrResultFactory {
writableOcrResultList.add(builder.build()); writableOcrResultList.add(builder.build());
} }
log.debug("Batch {}: finished post-processing.", batch.getIndex());
return writableOcrResultList; return writableOcrResultList;
} }
private ImageProcessingSupervisor renderImagesIfNecessary(AnalyzeResult analyzeResult, PageBatch batch) {
ImageProcessingSupervisor imageSupervisor = null;
if (useRenderedImages()) {
Map<Integer, Double> anglesPerPage = analyzeResult.getPages()
.stream()
.collect(Collectors.toMap(DocumentPage::getPageNumber, documentPage -> -documentPage.getAngle()));
RotationCorrectionUtility.rotatePages(batch.getBatchDoc(), batch.getBatchDoc(), anglesPerPage);
imageSupervisor = imageProcessingPipeline.addToPipeline(batch);
}
return imageSupervisor;
}
private boolean useRenderedImages() {
if (settings.isAzureFontStyleDetection() && features.contains(AzureOcrFeature.FONT_STYLE_DETECTION)) {
return false;
}
return settings.isSnuggify() || features.contains(AzureOcrFeature.FONT_STYLE_DETECTION);
}
private List<TextPositionInImage> buildTextPositionsInImage(PageBatch pageOffset, private List<TextPositionInImage> buildTextPositionsInImage(PageBatch pageOffset,
DocumentPage resultPage, DocumentPage resultPage,
AffineTransform resultToPageTransform, AffineTransform resultToPageTransform,
@ -121,7 +142,7 @@ public class WritableOcrResultFactory {
PageInformation pageInformation, PageInformation pageInformation,
ImageProcessingSupervisor imageSupervisor) throws InterruptedException { ImageProcessingSupervisor imageSupervisor) throws InterruptedException {
if (!settings.isSnuggify() && !features.contains(AzureOcrFeature.FONT_STYLE_DETECTION)) { if (!useRenderedImages()) {
return buildText(resultPage, resultToPageTransform, lookups, pageInformation); return buildText(resultPage, resultToPageTransform, lookups, pageInformation);
} }
@ -302,16 +323,13 @@ public class WritableOcrResultFactory {
return Lookups.empty(); return Lookups.empty();
} }
SpanLookup<DocumentSpan> boldLookup = new SpanLookup<>(analyzeResult.getStyles() // Azure stopped supporting bold text detection in 1.0.0 release
.stream() SpanLookup<DocumentSpan> boldLookup = new SpanLookup<>(Stream.empty(), Function.identity());
.filter(style -> Objects.equals(style.getFontWeight(), FontWeight.BOLD))
.map(DocumentStyle::getSpans)
.flatMap(Collection::stream), Function.identity());
SpanLookup<DocumentSpan> italicLookup = new SpanLookup<>(analyzeResult.getStyles() SpanLookup<DocumentSpan> italicLookup = new SpanLookup<>(analyzeResult.getStyles()
.stream() .stream()
.filter(style -> Objects.equals(style.getFontStyle(), .filter(style -> Objects.equals(style.getFontStyle(),
com.azure.ai.documentintelligence.models.FontStyle.ITALIC)) DocumentFontStyle.ITALIC))
.map(DocumentStyle::getSpans) .map(DocumentStyle::getSpans)
.flatMap(Collection::stream), Functions.identity()); .flatMap(Collection::stream), Functions.identity());

View File

@ -7,7 +7,7 @@ import java.util.Optional;
import com.azure.ai.documentintelligence.models.DocumentPage; import com.azure.ai.documentintelligence.models.DocumentPage;
import com.azure.ai.documentintelligence.models.DocumentWord; import com.azure.ai.documentintelligence.models.DocumentWord;
import com.knecon.fforesight.service.ocr.processor.visualizations.WritableOcrResultFactory; import com.knecon.fforesight.service.ocr.processor.service.OcrResultPostProcessingPipeline;
import com.knecon.fforesight.service.ocr.processor.visualizations.utils.RotationCorrectionUtility; import com.knecon.fforesight.service.ocr.processor.visualizations.utils.RotationCorrectionUtility;
import com.knecon.fforesight.service.ocr.v1.api.model.QuadPoint; import com.knecon.fforesight.service.ocr.v1.api.model.QuadPoint;
import com.sun.jna.Pointer; import com.sun.jna.Pointer;
@ -30,6 +30,8 @@ public class BBoxSnuggificationService {
public static final int PIXEL_COUNT_THRESHOLD = 2; // minimum active pixel count per row for shrinking to stop public static final int PIXEL_COUNT_THRESHOLD = 2; // minimum active pixel count per row for shrinking to stop
private static final double AVERAGE_ANGLE_THRESHOLD = 0.2; // Skips snuggification, if the average remaining word rotation of a word, written from left-to-right is bigger than this private static final double AVERAGE_ANGLE_THRESHOLD = 0.2; // Skips snuggification, if the average remaining word rotation of a word, written from left-to-right is bigger than this
public static final int INDIVIDUAL_ANGLE_THRESHOLD = 5; // skips snuggification for word, if the remaining rotation is larger than this angle public static final int INDIVIDUAL_ANGLE_THRESHOLD = 5; // skips snuggification for word, if the remaining rotation is larger than this angle
public static final int MAX_SHRINK_PIXELS = 40; // Number of pixels that are allowed to be removed from the top or bottom of an image
private static final int MINIMUM_WORD_PIXELS = 5; // Number of pixels that are required for snuggification
private enum Operation { private enum Operation {
HORIZONTAL, HORIZONTAL,
@ -46,6 +48,11 @@ public class BBoxSnuggificationService {
return Optional.empty(); return Optional.empty();
} }
if (origin.getContent().equals("-") || origin.getContent().equals(",")) {
// very slim characters should not be snuggified, or the fontsize may be off significantly
return Optional.empty();
}
QuadPoint originTransformed = QuadPoint.fromPolygons(origin.getPolygon()).getTransformed(resultToImageTransform); QuadPoint originTransformed = QuadPoint.fromPolygons(origin.getPolygon()).getTransformed(resultToImageTransform);
double remainingAngle = Math.abs(RotationCorrectionUtility.getRemainingAngle(originTransformed.getAngle())); double remainingAngle = Math.abs(RotationCorrectionUtility.getRemainingAngle(originTransformed.getAngle()));
QuadPoint.Direction direction = originTransformed.getDirection(); QuadPoint.Direction direction = originTransformed.getDirection();
@ -56,7 +63,7 @@ public class BBoxSnuggificationService {
return Optional.empty(); return Optional.empty();
} }
Pix wordImage = WritableOcrResultFactory.extractWordImage(originTransformed, pageImage); Pix wordImage = OcrResultPostProcessingPipeline.extractWordImage(originTransformed, pageImage);
if (wordImage == null) { if (wordImage == null) {
log.debug("Unable to extract word image! wordImage: {}, pageImage {}", originTransformed.getBounds2D(), new Rectangle2D.Float(0, 0, pageImage.w, pageImage.h)); log.debug("Unable to extract word image! wordImage: {}, pageImage {}", originTransformed.getBounds2D(), new Rectangle2D.Float(0, 0, pageImage.w, pageImage.h));
@ -116,13 +123,13 @@ public class BBoxSnuggificationService {
Numa colCounts = Leptonica1.pixCountPixelsByColumn(wordImage); Numa colCounts = Leptonica1.pixCountPixelsByColumn(wordImage);
int start = 0; int start = 0;
int end = wordImage.w - PIXEL_COUNT_THRESHOLD; int end = wordImage.w - PIXEL_COUNT_THRESHOLD;
for (int i = start; i < Math.min(wordImage.w, 25); i++) { for (int i = start; i < Math.min(wordImage.w, MAX_SHRINK_PIXELS); i++) {
if (pixCountPerColumn(i, colCounts) > PIXEL_COUNT_THRESHOLD) { if (pixCountPerColumn(i, colCounts) > PIXEL_COUNT_THRESHOLD) {
start = i; start = i;
break; break;
} }
} }
for (int i = end; i > Math.max(0, wordImage.w - 25); i--) { for (int i = end; i > Math.max(0, wordImage.w - MAX_SHRINK_PIXELS); i--) {
if (pixCountPerColumn(i, colCounts) > PIXEL_COUNT_THRESHOLD) { if (pixCountPerColumn(i, colCounts) > PIXEL_COUNT_THRESHOLD) {
end = i; end = i;
break; break;
@ -131,7 +138,9 @@ public class BBoxSnuggificationService {
if (start == 0 && end == wordImage.w) { if (start == 0 && end == wordImage.w) {
return Optional.empty(); return Optional.empty();
} }
if (Math.abs(start - end) < MINIMUM_WORD_PIXELS) {
return Optional.empty();
}
return Optional.of(new Rectangle2D.Double(origin.getX() + start, origin.getY(), origin.getWidth() - start - (wordImage.w - end), origin.getHeight())); return Optional.of(new Rectangle2D.Double(origin.getX() + start, origin.getY(), origin.getWidth() - start - (wordImage.w - end), origin.getHeight()));
} }
@ -140,13 +149,13 @@ public class BBoxSnuggificationService {
int start = 0; int start = 0;
int end = wordImage.h - 1; int end = wordImage.h - 1;
for (int i = start; i < Math.min(wordImage.h, 25); i++) { for (int i = start; i < Math.min(wordImage.h, MAX_SHRINK_PIXELS); i++) {
if (pixCountPerRow(i, wordImage) > PIXEL_COUNT_THRESHOLD) { if (pixCountPerRow(i, wordImage) > PIXEL_COUNT_THRESHOLD) {
start = i; start = i;
break; break;
} }
} }
for (int i = end; i > Math.max(0, wordImage.h - 25); i--) { for (int i = end; i > Math.max(0, wordImage.h - MAX_SHRINK_PIXELS); i--) {
if (pixCountPerRow(i, wordImage) > PIXEL_COUNT_THRESHOLD) { if (pixCountPerRow(i, wordImage) > PIXEL_COUNT_THRESHOLD) {
end = i; end = i;
break; break;
@ -155,6 +164,9 @@ public class BBoxSnuggificationService {
if (start == 0 && end == wordImage.h) { if (start == 0 && end == wordImage.h) {
return Optional.empty(); return Optional.empty();
} }
if (Math.abs(start - end) < MINIMUM_WORD_PIXELS) {
return Optional.empty();
}
return Optional.of(new Rectangle2D.Double(origin.getX(), origin.getY() + start, origin.getWidth(), origin.getHeight() - start - (wordImage.h - end))); return Optional.of(new Rectangle2D.Double(origin.getX(), origin.getY() + start, origin.getWidth(), origin.getHeight() - start - (wordImage.h - end)));
} }

View File

@ -75,13 +75,14 @@ public class GhostScriptOutputHandler extends Thread {
if (line == null) { if (line == null) {
break; break;
} }
switch (type) {
if (type.equals(Type.ERROR)) { case STD_OUT -> {
log.error("{}_{}>{}", processName, type.name(), line); log.debug("Batch {}: {}_{}>{}", batchIdx, processName, type.name(), line);
} else { addProcessedImageToQueue(line);
log.debug("{}_{}>{}", processName, type.name(), line); }
addProcessedImageToQueue(line); case ERROR -> log.error("Batch {}: {}_{}>{}", batchIdx, processName, type.name(), line);
} }
} }
} }
is.close(); is.close();
@ -91,7 +92,7 @@ public class GhostScriptOutputHandler extends Thread {
if (!pagesToProcess.isEmpty()) { if (!pagesToProcess.isEmpty()) {
errorHandler.accept(String.format("Ghostscript finished for batch %d, but pages %s remain unprocessed.", batchIdx, formatPagesToProcess())); errorHandler.accept(String.format("Ghostscript finished for batch %d, but pages %s remain unprocessed.", batchIdx, formatPagesToProcess()));
} else { } else {
log.info("Batch: {} rendered successfully!", batchIdx); log.info("Batch {}: rendered successfully!", batchIdx);
} }
} }

View File

@ -5,55 +5,95 @@ import java.nio.file.Path;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.ocr.processor.OcrServiceSettings;
import com.knecon.fforesight.service.ocr.processor.model.ImageFile; import com.knecon.fforesight.service.ocr.processor.model.ImageFile;
import com.knecon.fforesight.service.ocr.processor.model.PageBatch; import com.knecon.fforesight.service.ocr.processor.model.PageBatch;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@SuppressWarnings("PMD") // can't figure out how to safely close the stdOut and stdError streams in line 72/74 @SuppressWarnings("PMD") // can't figure out how to safely close the stdOut and stdError streams in line 72/74
public class GhostScriptService { public class GhostScriptService {
static String FORMAT = ".tiff"; private OcrServiceSettings ocrServiceSettings;
public static String FORMAT = ".tiff";
static String DEVICE = "tiffgray"; static String DEVICE = "tiffgray";
static int DPI = 300; static int DPI = 300;
private Semaphore concurrencySemaphore = new Semaphore(3);
@SneakyThrows public GhostScriptService(OcrServiceSettings ocrServiceSettings) {
public void startBatchRender(PageBatch batch, ImageProcessingSupervisor supervisor, Path renderedImageDir, Consumer<ImageFile> successHandler, Consumer<String> errorHandler) {
supervisor.requireNoErrors(); this.ocrServiceSettings = ocrServiceSettings;
assertGhostscriptIsInstalled();
}
log.info("Batch {}: starting GhostScript rendering with page(s) {}", batch.getIndex(), batch);
executeProcess(batch.getIndex(), buildCmdArgs(batch, renderedImageDir, batch.getBatchDoc()), successHandler, errorHandler); private void assertGhostscriptIsInstalled() {
try {
Process p = Runtime.getRuntime().exec("gs -v");
InputStream stdOut = p.getInputStream();
InputStream errOut = p.getErrorStream();
assert p.waitFor(1, TimeUnit.SECONDS);
log.info("Ghostscript is installed.");
String out = new String(stdOut.readAllBytes());
String error = new String(errOut.readAllBytes());
for (String line : out.split("\n")) {
log.info(line);
}
if (!error.isBlank()) {
log.error(error);
}
} catch (Exception e) {
log.error("Ghostscript is not installed!");
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
} }
@SneakyThrows @SneakyThrows
private ProcessCmdsAndRenderedImageFiles buildCmdArgs(PageBatch batch, Path outputDir, Path document) { public void startBatchRender(PageBatch batch, ImageProcessingSupervisor supervisor, Consumer<ImageFile> successHandler, Consumer<String> errorHandler) {
String imagePathFormat = outputDir.resolve("output_" + batch.getIndex() + ".%04d" + FORMAT).toFile().toString(); supervisor.requireNoErrors();
Map<Integer, ImageFile> fullPageImages = new HashMap<>(); List<ImageFile> renderedImageFiles = batch.getRenderedImageFiles();
List<Integer> allPageNumbers = batch.getAllPageNumbers(); if (ocrServiceSettings.isUseCaches() && renderedImageFiles.stream()
.allMatch(ImageFile::exists)) {
for (int i = 0; i < allPageNumbers.size(); i++) { log.info("Batch {}: Using cached GhostScript rendering with page(s) {}", batch.getIndex(), batch);
Integer pageNumber = allPageNumbers.get(i); renderedImageFiles.forEach(successHandler);
fullPageImages.put(i + 1, new ImageFile(pageNumber, String.format(imagePathFormat, i + 1))); return;
} }
String[] cmdArgs = buildCmdArgs(document, imagePathFormat); concurrencySemaphore.acquire();
log.info("Batch {}: starting GhostScript rendering with page(s) {}", batch.getIndex(), batch);
executeProcess(batch, buildCmdArgs(batch, batch.getBatchDoc()), successHandler, errorHandler);
}
@SneakyThrows
private ProcessCmdsAndRenderedImageFiles buildCmdArgs(PageBatch batch, Path document) {
Map<Integer, ImageFile> fullPageImages = new HashMap<>();
List<ImageFile> renderedImageFiles = batch.getRenderedImageFiles();
for (int i = 1; i <= renderedImageFiles.size(); i++) {
ImageFile renderedImageFile = renderedImageFiles.get(i - 1);
fullPageImages.put(i, renderedImageFile);
}
String[] cmdArgs = buildCmdArgs(document, batch.getRenderedImageNameFormat());
return new ProcessCmdsAndRenderedImageFiles(cmdArgs, fullPageImages); return new ProcessCmdsAndRenderedImageFiles(cmdArgs, fullPageImages);
} }
@ -66,16 +106,32 @@ public class GhostScriptService {
@SneakyThrows @SneakyThrows
private void executeProcess(int batchIdx, ProcessCmdsAndRenderedImageFiles processInfo, Consumer<ImageFile> successHandler, Consumer<String> errorHandler) { private void executeProcess(PageBatch batch, ProcessCmdsAndRenderedImageFiles processInfo, Consumer<ImageFile> successHandler, Consumer<String> errorHandler) {
Process p = Runtime.getRuntime().exec(processInfo.cmdArgs()); Process p = Runtime.getRuntime().exec(processInfo.cmdArgs());
InputStream stdOut = p.getInputStream(); InputStream stdOut = p.getInputStream();
GhostScriptOutputHandler stdOutLogger = GhostScriptOutputHandler.stdOut(batchIdx, stdOut, processInfo.renderedPageImageFiles(), successHandler, errorHandler); GhostScriptOutputHandler stdOutLogger = GhostScriptOutputHandler.stdOut(batch.getIndex(), stdOut, processInfo.renderedPageImageFiles(), successHandler, errorHandler);
InputStream stdError = p.getErrorStream(); InputStream stdError = p.getErrorStream();
GhostScriptOutputHandler stdErrorLogger = GhostScriptOutputHandler.stdError(batchIdx, stdError, errorHandler); GhostScriptOutputHandler stdErrorLogger = GhostScriptOutputHandler.stdError(batch.getIndex(), stdError, errorHandler);
stdOutLogger.start(); stdOutLogger.start();
stdErrorLogger.start(); stdErrorLogger.start();
handleFinished(p, errorHandler, batch, successHandler);
}
private void handleFinished(Process p, Consumer<String> errorHandler, PageBatch batch, Consumer<ImageFile> successHandler) {
Thread finishedThread = new Thread(() -> {
try {
p.waitFor(2, TimeUnit.MINUTES);
} catch (InterruptedException e) {
errorHandler.accept("Batch %d: Ghostscript rendering has been terminated after 2 minutes \n %s".formatted(batch.getIndex(), e.getMessage()));
} finally {
concurrencySemaphore.release();
}
});
finishedThread.start();
} }

View File

@ -1,7 +1,5 @@
package com.knecon.fforesight.service.ocr.processor.service.imageprocessing; package com.knecon.fforesight.service.ocr.processor.service.imageprocessing;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -20,9 +18,6 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class ImageProcessingPipeline { public class ImageProcessingPipeline {
public static final String PROCESSED_DIR = "processed";
public static final String RENDERED_DIR = "rendered";
GhostScriptService ghostScriptService; GhostScriptService ghostScriptService;
ImageProcessingService imageProcessingService; ImageProcessingService imageProcessingService;
@ -30,20 +25,14 @@ public class ImageProcessingPipeline {
@SneakyThrows @SneakyThrows
public ImageProcessingSupervisor addToPipeline(PageBatch batch) { public ImageProcessingSupervisor addToPipeline(PageBatch batch) {
Path processedImageDir = batch.getImagePipelineDir().resolve(PROCESSED_DIR);
Path renderedImageDir = batch.getImagePipelineDir().resolve(RENDERED_DIR);
Files.createDirectories(renderedImageDir);
Files.createDirectories(processedImageDir);
List<Integer> pageNumbers = batch.getAllPageNumbers(); List<Integer> pageNumbers = batch.getAllPageNumbers();
ImageProcessingSupervisor supervisor = new ImageProcessingSupervisor(pageNumbers); ImageProcessingSupervisor supervisor = new ImageProcessingSupervisor(pageNumbers);
Consumer<ImageFile> renderingSuccessConsumer = imageFile -> imageProcessingService.addToProcessingQueue(imageFile, processedImageDir, supervisor); Consumer<ImageFile> renderingSuccessConsumer = imageFile -> imageProcessingService.addToProcessingQueue(imageFile, batch.getProcessedImageDir(), supervisor);
Consumer<String> renderingErrorConsumer = supervisor::markError; Consumer<String> renderingErrorConsumer = supervisor::markError;
ghostScriptService.startBatchRender(batch, supervisor, renderedImageDir, renderingSuccessConsumer, renderingErrorConsumer); ghostScriptService.startBatchRender(batch, supervisor, renderingSuccessConsumer, renderingErrorConsumer);
return supervisor; return supervisor;
} }

View File

@ -1,11 +1,13 @@
package com.knecon.fforesight.service.ocr.processor.service.imageprocessing; package com.knecon.fforesight.service.ocr.processor.service.imageprocessing;
import java.io.File;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.ocr.processor.OcrServiceSettings;
import com.knecon.fforesight.service.ocr.processor.model.ImageFile; import com.knecon.fforesight.service.ocr.processor.model.ImageFile;
import lombok.AccessLevel; import lombok.AccessLevel;
@ -23,9 +25,10 @@ import net.sourceforge.lept4j.util.LeptUtils;
public class ImageProcessingService { public class ImageProcessingService {
BlockingQueue<ProcessParams> queue = new LinkedBlockingQueue<>(); BlockingQueue<ProcessParams> queue = new LinkedBlockingQueue<>();
private final OcrServiceSettings ocrServiceSettings;
public ImageProcessingService() { public ImageProcessingService(OcrServiceSettings ocrServiceSettings) {
Thread queueConsumerThread = new Thread(() -> { Thread queueConsumerThread = new Thread(() -> {
while (true) { while (true) {
@ -44,7 +47,7 @@ public class ImageProcessingService {
} }
}); });
queueConsumerThread.start(); queueConsumerThread.start();
this.ocrServiceSettings = ocrServiceSettings;
} }
@ -58,24 +61,36 @@ public class ImageProcessingService {
private void process(ImageFile unprocessedImage, Path outputDir, ImageProcessingSupervisor supervisor) { private void process(ImageFile unprocessedImage, Path outputDir, ImageProcessingSupervisor supervisor) {
String absoluteFilePath = outputDir.resolve(Path.of(unprocessedImage.absoluteFilePath()).getFileName()).toFile().toString(); String absoluteFilePath = outputDir.resolve(Path.of(unprocessedImage.absoluteFilePath()).getFileName()).toFile().toString();
ImageFile imageFile = new ImageFile(unprocessedImage.pageNumber(), absoluteFilePath); ImageFile processedImage = new ImageFile(unprocessedImage.pageNumber(), absoluteFilePath);
if (ocrServiceSettings.isUseCaches() && processedImage.exists()) {
supervisor.markPageFinished(processedImage);
return;
}
try { try {
if (!unprocessedImage.exists()) {
log.error("ERROR, rendered image {} does not exist", unprocessedImage.absoluteFilePath());
throw new AssertionError();
}
synchronized (ImageProcessingSupervisor.class) { synchronized (ImageProcessingSupervisor.class) {
// Leptonica is not thread safe, but is being called in WritableOcrResultFactory as well // Leptonica is not thread safe, but is being called in WritableOcrResultFactory as well
Pix processedPix; Pix processedPix;
Pix pix = unprocessedImage.readPix(); Pix pix = unprocessedImage.readPix();
assert pix != null;
processedPix = processPix(pix); processedPix = processPix(pix);
Leptonica1.pixWrite(absoluteFilePath, processedPix, ILeptonica.IFF_TIFF_PACKBITS); Leptonica1.pixWrite(processedImage.absoluteFilePath(), processedPix, ILeptonica.IFF_TIFF_PACKBITS);
LeptUtils.disposePix(pix); LeptUtils.disposePix(pix);
LeptUtils.disposePix(processedPix); LeptUtils.disposePix(processedPix);
} }
} catch (Exception e) { } catch (Exception e) {
supervisor.markError(e.getMessage()); supervisor.markError("Page %d could not be processed due to: %s".formatted(unprocessedImage.pageNumber(), e.getMessage()));
} finally { } finally {
supervisor.markPageFinished(imageFile); supervisor.markPageFinished(processedImage);
log.debug("Finished page: {}", processedImage.pageNumber());
} }
} }

View File

@ -69,6 +69,7 @@ public class ImageProcessingSupervisor {
public void markError(String errorMessage) { public void markError(String errorMessage) {
log.error(errorMessage);
this.errors.add(errorMessage); this.errors.add(errorMessage);
} }

View File

@ -1,25 +1,26 @@
package com.knecon.fforesight.service.ocr.processor.utils; package com.knecon.fforesight.service.ocr.processor.utils;
import org.apache.commons.lang3.StringUtils; import java.util.Locale;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
@UtilityClass @UtilityClass
public final class OsUtils { public final class OsUtils {
private static final String SERVICE_NAME = "azure-ocr-service";
private static boolean isWindows() { private static boolean isWindows() {
return StringUtils.containsIgnoreCase(System.getProperty("os.name"), "Windows"); String osName = System.getProperty("os.name");
if (osName == null) {
return false;
}
return osName.toLowerCase(Locale.ENGLISH).contains("windows");
} }
public static String getTemporaryDirectory() { public static String getTemporaryDirectory() {
String tmpdir = System.getProperty("java.io.tmpdir"); String tmpdir = System.getProperty("java.io.tmpdir");
if (isWindows() && StringUtils.isNotBlank(tmpdir)) { if (isWindows() && !tmpdir.isBlank()) {
return tmpdir; return tmpdir;
} }
return "/tmp"; return "/tmp";

View File

@ -0,0 +1,40 @@
package com.knecon.fforesight.service.ocr.processor.utils;
import java.util.regex.Pattern;
import lombok.experimental.UtilityClass;
@UtilityClass
public class StringCleaningUtility {
public static final Pattern hyphenLineBreaks = Pattern.compile("[-~‐‒⁻−﹣゠⁓‑\\u00AD][\\r\\n]+");
public static final Pattern linebreaks = Pattern.compile("[\\r\\n]+");
public static final Pattern doubleWhitespaces = Pattern.compile("\\s{2,}");
public static String cleanString(String value) {
String noHyphenLinebreaks = removeHyphenLinebreaks(value);
String noLinebreaks = removeLinebreaks(noHyphenLinebreaks);
return removeMultipleWhitespaces(noLinebreaks);
}
private String removeHyphenLinebreaks(String value) {
return hyphenLineBreaks.matcher(value).replaceAll("");
}
private String removeMultipleWhitespaces(String value) {
return doubleWhitespaces.matcher(value).replaceAll(" ");
}
private String removeLinebreaks(String value) {
return linebreaks.matcher(value).replaceAll(" ");
}
}

View File

@ -1,14 +1,14 @@
package com.knecon.fforesight.service.ocr.processor.visualizations; package com.knecon.fforesight.service.ocr.processor.visualizations;
import com.azure.ai.documentintelligence.models.AnalyzeResult; import com.azure.ai.documentintelligence.models.AnalyzeResult;
import com.knecon.fforesight.service.ocr.v1.api.model.AzureAnalyzeResult; import com.knecon.fforesight.service.ocr.v1.api.model.IdpResult;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
@UtilityClass @UtilityClass
public class AnalyzeResultMapper { public class AnalyzeResultMapper {
public AzureAnalyzeResult map(AnalyzeResult analyzeResult) { public IdpResult map(AnalyzeResult analyzeResult) {
return null; return null;
} }

View File

@ -12,8 +12,6 @@ import com.azure.ai.documentintelligence.models.DocumentBarcode;
import com.azure.ai.documentintelligence.models.DocumentFigure; import com.azure.ai.documentintelligence.models.DocumentFigure;
import com.azure.ai.documentintelligence.models.DocumentKeyValuePair; import com.azure.ai.documentintelligence.models.DocumentKeyValuePair;
import com.azure.ai.documentintelligence.models.DocumentLine; import com.azure.ai.documentintelligence.models.DocumentLine;
import com.azure.ai.documentintelligence.models.DocumentList;
import com.azure.ai.documentintelligence.models.DocumentListItem;
import com.azure.ai.documentintelligence.models.DocumentParagraph; import com.azure.ai.documentintelligence.models.DocumentParagraph;
import com.azure.ai.documentintelligence.models.DocumentSection; import com.azure.ai.documentintelligence.models.DocumentSection;
import com.azure.ai.documentintelligence.models.DocumentTable; import com.azure.ai.documentintelligence.models.DocumentTable;
@ -23,8 +21,8 @@ import com.azure.ai.documentintelligence.models.DocumentWord;
import com.azure.ai.documentintelligence.models.ParagraphRole; import com.azure.ai.documentintelligence.models.ParagraphRole;
import com.knecon.fforesight.service.ocr.processor.model.PageBatch; import com.knecon.fforesight.service.ocr.processor.model.PageBatch;
import com.knecon.fforesight.service.ocr.processor.model.SpanLookup; import com.knecon.fforesight.service.ocr.processor.model.SpanLookup;
import com.knecon.fforesight.service.ocr.processor.visualizations.utils.Rectangle2DBBoxCollector;
import com.knecon.fforesight.service.ocr.processor.visualizations.utils.LineUtils; import com.knecon.fforesight.service.ocr.processor.visualizations.utils.LineUtils;
import com.knecon.fforesight.service.ocr.processor.visualizations.utils.Rectangle2DBBoxCollector;
import com.knecon.fforesight.service.ocr.v1.api.model.QuadPoint; import com.knecon.fforesight.service.ocr.v1.api.model.QuadPoint;
import com.knecon.fforesight.service.viewerdoc.layers.IdpLayerConfig; import com.knecon.fforesight.service.viewerdoc.layers.IdpLayerConfig;
import com.knecon.fforesight.service.viewerdoc.model.ColoredLine; import com.knecon.fforesight.service.viewerdoc.model.ColoredLine;
@ -69,14 +67,6 @@ public class IdpLayer extends IdpLayerConfig {
} }
public void addList(DocumentList list, PageBatch pageOffset) {
for (DocumentListItem item : list.getItems()) {
addBoundingRegion(item.getBoundingRegions(), lists, PARAGRAPH_COLOR, pageOffset);
}
}
public void addBarcode(int pageNumber, DocumentBarcode barcode) { public void addBarcode(int pageNumber, DocumentBarcode barcode) {
addPolygon(pageNumber, barcode.getPolygon(), barcodes, IMAGE_COLOR); addPolygon(pageNumber, barcode.getPolygon(), barcodes, IMAGE_COLOR);
@ -85,8 +75,11 @@ public class IdpLayer extends IdpLayerConfig {
public void addKeyValue(DocumentKeyValuePair keyValue, PageBatch pageOffset) { public void addKeyValue(DocumentKeyValuePair keyValue, PageBatch pageOffset) {
if (keyValue.getKey() == null || keyValue.getKey().getContent().isEmpty()) {
return;
}
addBoundingRegion(keyValue.getKey().getBoundingRegions(), keyValuePairs, KEY_COLOR, pageOffset); addBoundingRegion(keyValue.getKey().getBoundingRegions(), keyValuePairs, KEY_COLOR, pageOffset);
if (keyValue.getValue() != null) { if (keyValue.getValue() != null && !keyValue.getValue().getContent().isEmpty()) {
addBoundingRegion(keyValue.getValue().getBoundingRegions(), keyValuePairs, VALUE_COLOR, pageOffset); addBoundingRegion(keyValue.getValue().getBoundingRegions(), keyValuePairs, VALUE_COLOR, pageOffset);
if (keyValue.getKey().getBoundingRegions() if (keyValue.getKey().getBoundingRegions()

View File

@ -65,10 +65,6 @@ public class IdpLayerFactory {
analyzeResult.getTables() analyzeResult.getTables()
.forEach(documentTable -> idpLayer.addTable(documentTable, pageOffset)); .forEach(documentTable -> idpLayer.addTable(documentTable, pageOffset));
} }
if (analyzeResult.getLists() != null) {
analyzeResult.getLists()
.forEach(list -> idpLayer.addList(list, pageOffset));
}
if (analyzeResult.getKeyValuePairs() != null) { if (analyzeResult.getKeyValuePairs() != null) {
analyzeResult.getKeyValuePairs() analyzeResult.getKeyValuePairs()
.forEach(keyValue -> idpLayer.addKeyValue(keyValue, pageOffset)); .forEach(keyValue -> idpLayer.addKeyValue(keyValue, pageOffset));

View File

@ -0,0 +1,241 @@
package com.knecon.fforesight.service.ocr.processor.visualizations.layers;
import static com.knecon.fforesight.service.ocr.processor.utils.StringCleaningUtility.cleanString;
import java.awt.geom.AffineTransform;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.azure.ai.documentintelligence.models.AnalyzeResult;
import com.azure.ai.documentintelligence.models.BoundingRegion;
import com.azure.ai.documentintelligence.models.DocumentCaption;
import com.azure.ai.documentintelligence.models.DocumentFigure;
import com.azure.ai.documentintelligence.models.DocumentFootnote;
import com.azure.ai.documentintelligence.models.DocumentKeyValuePair;
import com.azure.ai.documentintelligence.models.DocumentTable;
import com.azure.ai.documentintelligence.models.DocumentTableCell;
import com.knecon.fforesight.service.ocr.processor.model.DocumentSpanLookup;
import com.knecon.fforesight.service.ocr.processor.model.PageBatch;
import com.knecon.fforesight.service.ocr.processor.model.PageInformation;
import com.knecon.fforesight.service.ocr.processor.visualizations.utils.Rectangle2DBBoxCollector;
import com.knecon.fforesight.service.ocr.processor.visualizations.utils.RotationCorrectionUtility;
import com.knecon.fforesight.service.ocr.v1.api.model.AzureOcrFeature;
import com.knecon.fforesight.service.ocr.v1.api.model.Figure;
import com.knecon.fforesight.service.ocr.v1.api.model.IdpResult;
import com.knecon.fforesight.service.ocr.v1.api.model.KeyValuePair;
import com.knecon.fforesight.service.ocr.v1.api.model.QuadPoint;
import com.knecon.fforesight.service.ocr.v1.api.model.Region;
import com.knecon.fforesight.service.ocr.v1.api.model.Table;
import com.knecon.fforesight.service.ocr.v1.api.model.TableCell;
import com.knecon.fforesight.service.ocr.v1.api.model.TableCellType;
import com.knecon.fforesight.service.ocr.v1.api.model.TextRegion;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@Getter
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class IdpResultFactory {
IdpResult idpResult;
Map<Integer, AffineTransform> resultToPageTransforms;
Map<Integer, PageInformation> pageInformation;
Map<Integer, Double> angles;
boolean rotationCorrection;
public IdpResultFactory(Map<Integer, AffineTransform> resultToPageTransforms,
Map<Integer, PageInformation> pageInformation,
Map<Integer, Double> angles,
Set<AzureOcrFeature> features) {
this.angles = angles;
this.rotationCorrection = features.contains(AzureOcrFeature.ROTATION_CORRECTION);
this.resultToPageTransforms = resultToPageTransforms;
this.pageInformation = pageInformation;
this.idpResult = IdpResult.initSynchronized();
}
public AffineTransform getResultToPageTransform(Integer pageNumber) {
AffineTransform transform;
if (rotationCorrection) {
PageInformation page = pageInformation.get(pageNumber);
transform = RotationCorrectionUtility.buildTransform(-angles.get(pageNumber), page.cropBox().getWidth(), page.cropBox().getHeight(), false);
} else {
transform = new AffineTransform();
}
transform.concatenate(resultToPageTransforms.get(pageNumber));
return transform;
}
public void addAnalyzeResult(AnalyzeResult analyzeResult, PageBatch batch) {
DocumentSpanLookup words = new DocumentSpanLookup(analyzeResult);
if (analyzeResult.getTables() != null) {
analyzeResult.getTables()
.forEach(documentTable -> addTable(documentTable, words, batch));
}
if (analyzeResult.getKeyValuePairs() != null) {
analyzeResult.getKeyValuePairs()
.forEach(documentKeyValuePair -> addKeyValuePair(documentKeyValuePair, batch));
}
if (analyzeResult.getFigures() != null) {
analyzeResult.getFigures()
.forEach(documentFigure -> addFigure(documentFigure, batch, words));
}
}
private void addFigure(DocumentFigure documentFigure, PageBatch batch, DocumentSpanLookup words) {
List<TextRegion> footNotes = new LinkedList<>();
if (documentFigure.getFootnotes() != null) {
documentFigure.getFootnotes()
.stream()
.map(footNote -> toTextRegion(footNote, batch))
.filter(Objects::nonNull)
.forEach(footNotes::add);
}
int batchPageNumber = documentFigure.getBoundingRegions()
.get(0).getPageNumber();
Region bbox = toRegionFromRegions(batch.getPageNumber(batchPageNumber), documentFigure.getBoundingRegions());
TextRegion caption = toTextRegion(documentFigure.getCaption(), batch);
idpResult.figures().add(new Figure(caption, bbox, footNotes));
}
private void addKeyValuePair(DocumentKeyValuePair documentKeyValuePair, PageBatch batch) {
TextRegion key = null;
if (documentKeyValuePair.getKey() != null && !documentKeyValuePair.getKey().getContent().isEmpty()) {
Region region = toRegionFromRegions(batch, documentKeyValuePair.getKey().getBoundingRegions());
key = new TextRegion(region, cleanString(documentKeyValuePair.getKey().getContent()));
}
TextRegion value = null;
if (documentKeyValuePair.getValue() != null && !documentKeyValuePair.getValue().getContent().isEmpty()) {
Region region = toRegionFromRegions(batch, documentKeyValuePair.getValue().getBoundingRegions());
value = new TextRegion(region, cleanString(documentKeyValuePair.getValue().getContent()));
}
idpResult.keyValuePairs().add(new KeyValuePair(key, value));
}
private void addTable(DocumentTable documentTable, DocumentSpanLookup words, PageBatch batch) {
TextRegion caption = toTextRegion(documentTable.getCaption(), batch);
List<TableCell> tableCells = documentTable.getCells()
.stream()
.map(documentTableCell -> toTableCell(documentTableCell, words, batch))
.toList();
List<TextRegion> footNotes = new LinkedList<>();
if (documentTable.getFootnotes() != null) {
documentTable.getFootnotes()
.stream()
.map(footNote -> toTextRegion(footNote, batch))
.filter(Objects::nonNull)
.forEach(footNotes::add);
}
List<Region> bbox = documentTable.getBoundingRegions()
.stream()
.map(b -> toRegion(b, batch))
.toList();
Table table = new Table(caption, documentTable.getColumnCount(), documentTable.getRowCount(), tableCells, footNotes, bbox);
idpResult.tables().add(table);
}
private TextRegion toTextRegion(DocumentFootnote footNote, PageBatch batch) {
if (footNote == null || footNote.getBoundingRegions().isEmpty()) {
return null;
}
Region region = toRegionFromRegions(batch, footNote.getBoundingRegions());
return new TextRegion(region, cleanString(footNote.getContent()));
}
private TextRegion toTextRegion(DocumentCaption caption, PageBatch batch) {
if (caption == null || caption.getBoundingRegions().isEmpty()) {
return null;
}
Region region = toRegionFromRegions(batch, caption.getBoundingRegions());
return new TextRegion(region, cleanString(caption.getContent()));
}
private TableCell toTableCell(DocumentTableCell documentTableCell, DocumentSpanLookup words, PageBatch batch) {
int batchPageNumber = documentTableCell.getBoundingRegions()
.get(0).getPageNumber();
Region region = toRegionFromRegions(batch.getPageNumber(batchPageNumber), documentTableCell.getBoundingRegions());
TableCellType kind = mapTableCellType(documentTableCell);
return new TableCell(new TextRegion(region, cleanString(documentTableCell.getContent())), documentTableCell.getRowIndex(), documentTableCell.getColumnIndex(), kind);
}
private static TableCellType mapTableCellType(DocumentTableCell documentTableCell) {
if (documentTableCell.getKind() == null) {
return TableCellType.CONTENT;
}
return switch (documentTableCell.getKind().toString()) {
case "columnHeader" -> TableCellType.COLUMN_HEADER;
case "rowHeader" -> TableCellType.ROW_HEADER;
case "description" -> TableCellType.DESCRIPTION;
case "stubHead" -> TableCellType.STUB_HEAD;
default -> TableCellType.CONTENT;
};
}
private Region toRegion(BoundingRegion boundingRegion, PageBatch batch) {
int pageNumber = batch.getPageNumber(boundingRegion.getPageNumber());
QuadPoint qp = QuadPoint.fromPolygons(boundingRegion.getPolygon()).getTransformed(getResultToPageTransform(pageNumber));
return new Region(pageNumber, qp.toData());
}
private Region toRegionFromRegions(int pageNumber, List<BoundingRegion> regions) {
if (regions.size() == 1) {
return new Region(pageNumber, QuadPoint.fromPolygons(regions.get(0).getPolygon()).getTransformed(getResultToPageTransform(pageNumber)).toData());
}
QuadPoint bbox = QuadPoint.fromRectangle2D(regions.stream()
.map(BoundingRegion::getPolygon)
.map(QuadPoint::fromPolygons)
.map(qp -> qp.getTransformed(getResultToPageTransform(pageNumber)).getBounds2D())
.collect(new Rectangle2DBBoxCollector()));
return new Region(pageNumber, bbox.toData());
}
private Region toRegionFromRegions(PageBatch batch, List<BoundingRegion> regions) {
assert !regions.isEmpty();
int batchPageNumber = regions.get(0).getPageNumber();
if (!regions.stream()
.map(BoundingRegion::getPageNumber)
.allMatch(number -> number == batchPageNumber)) {
throw new AssertionError();
}
int pageNumber = batch.getPageNumber(batchPageNumber);
return toRegionFromRegions(pageNumber, regions);
}
}

View File

@ -8,27 +8,31 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import com.azure.ai.documentintelligence.models.AnalyzeResult; import com.azure.ai.documentintelligence.models.AnalyzeResult;
import com.knecon.fforesight.service.ocr.processor.OcrServiceSettings;
import com.knecon.fforesight.service.ocr.processor.model.PageBatch; import com.knecon.fforesight.service.ocr.processor.model.PageBatch;
import com.knecon.fforesight.service.ocr.processor.model.PageInformation; import com.knecon.fforesight.service.ocr.processor.model.PageInformation;
import com.knecon.fforesight.service.ocr.processor.service.OcrExecutionSupervisor; import com.knecon.fforesight.service.ocr.processor.service.OcrExecutionSupervisor;
import com.knecon.fforesight.service.ocr.processor.OcrServiceSettings; import com.knecon.fforesight.service.ocr.processor.service.OcrResultPostProcessingPipeline;
import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.ImageProcessingPipeline; import com.knecon.fforesight.service.ocr.processor.service.imageprocessing.ImageProcessingPipeline;
import com.knecon.fforesight.service.ocr.processor.visualizations.WritableOcrResult; import com.knecon.fforesight.service.ocr.processor.visualizations.WritableOcrResult;
import com.knecon.fforesight.service.ocr.processor.visualizations.WritableOcrResultFactory; import com.knecon.fforesight.service.ocr.v1.api.model.IdpResult;
import com.knecon.fforesight.service.ocr.v1.api.model.AzureOcrFeature; import com.knecon.fforesight.service.ocr.v1.api.model.AzureOcrFeature;
import com.knecon.fforesight.service.viewerdoc.layers.LayerGroup; import com.knecon.fforesight.service.viewerdoc.layers.LayerGroup;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class LayerFactory { public class LayerFactory {
OcrExecutionSupervisor supervisor; OcrExecutionSupervisor supervisor;
WritableOcrResultFactory writableOcrResultFactory; OcrResultPostProcessingPipeline ocrResultPostProcessingPipeline;
IdpLayerFactory idpLayerFactory; IdpLayerFactory idpLayerFactory;
OcrDebugLayerFactory ocrDebugLayerFactory; OcrDebugLayerFactory ocrDebugLayerFactory;
OcrTextLayerFactory ocrTextLayerFactory; OcrTextLayerFactory ocrTextLayerFactory;
IdpResultFactory idpResultFactory;
OcrServiceSettings settings; OcrServiceSettings settings;
Set<AzureOcrFeature> features; Set<AzureOcrFeature> features;
Map<Integer, Double> angles; Map<Integer, Double> angles;
@ -40,21 +44,21 @@ public class LayerFactory {
Map<Integer, PageInformation> pageInformation, Map<Integer, PageInformation> pageInformation,
ImageProcessingPipeline imageProcessingPipeline) { ImageProcessingPipeline imageProcessingPipeline) {
this.writableOcrResultFactory = new WritableOcrResultFactory(pageInformation, imageProcessingPipeline, settings, features); this.ocrResultPostProcessingPipeline = new OcrResultPostProcessingPipeline(pageInformation, imageProcessingPipeline, settings, features);
this.idpLayerFactory = new IdpLayerFactory(writableOcrResultFactory.getResultToPageTransforms()); this.idpLayerFactory = new IdpLayerFactory(ocrResultPostProcessingPipeline.getResultToPageTransforms());
this.ocrDebugLayerFactory = new OcrDebugLayerFactory(); this.ocrDebugLayerFactory = new OcrDebugLayerFactory();
this.ocrTextLayerFactory = new OcrTextLayerFactory(); this.ocrTextLayerFactory = new OcrTextLayerFactory();
this.settings = settings; this.settings = settings;
this.features = features; this.features = features;
this.supervisor = supervisor; this.supervisor = supervisor;
this.angles = Collections.synchronizedMap(new HashMap<>()); this.angles = Collections.synchronizedMap(new HashMap<>());
this.idpResultFactory = new IdpResultFactory(ocrResultPostProcessingPipeline.getResultToPageTransforms(), pageInformation, angles, features);
} }
public void processAnalyzeResult(PageBatch batch, AnalyzeResult analyzeResult) throws InterruptedException { public void processAnalyzeResult(PageBatch batch, AnalyzeResult analyzeResult) throws InterruptedException {
List<WritableOcrResult> results = writableOcrResultFactory.buildOcrResultToWrite(analyzeResult, batch); List<WritableOcrResult> results = ocrResultPostProcessingPipeline.processAnalyzeResult(analyzeResult, batch);
results.forEach(result -> angles.put(result.getPageNumber(), result.getAngle())); results.forEach(result -> angles.put(result.getPageNumber(), result.getAngle()));
ocrTextLayerFactory.addWritableOcrResult(results); ocrTextLayerFactory.addWritableOcrResult(results);
@ -64,6 +68,7 @@ public class LayerFactory {
} }
if (features.contains(AzureOcrFeature.IDP)) { if (features.contains(AzureOcrFeature.IDP)) {
idpLayerFactory.addAnalyzeResult(analyzeResult, batch); idpLayerFactory.addAnalyzeResult(analyzeResult, batch);
idpResultFactory.addAnalyzeResult(analyzeResult, batch);
} }
this.supervisor.finishMappingResult(batch); this.supervisor.finishMappingResult(batch);
@ -82,7 +87,8 @@ public class LayerFactory {
if (features.contains(AzureOcrFeature.IDP)) { if (features.contains(AzureOcrFeature.IDP)) {
debugLayers.add(idpLayerFactory.getIdpLayer()); debugLayers.add(idpLayerFactory.getIdpLayer());
} }
return new OcrResult(List.of(ocrTextLayer), debugLayers, angles); IdpResult idpResult = features.contains(AzureOcrFeature.IDP) ? idpResultFactory.getIdpResult() : null;
return new OcrResult(List.of(ocrTextLayer), debugLayers, angles, idpResult);
} }
} }

View File

@ -3,8 +3,9 @@ package com.knecon.fforesight.service.ocr.processor.visualizations.layers;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.knecon.fforesight.service.ocr.v1.api.model.IdpResult;
import com.knecon.fforesight.service.viewerdoc.layers.LayerGroup; import com.knecon.fforesight.service.viewerdoc.layers.LayerGroup;
public record OcrResult(List<LayerGroup> regularLayers, List<LayerGroup> debugLayers, Map<Integer, Double> anglesPerPage) { public record OcrResult(List<LayerGroup> regularLayers, List<LayerGroup> debugLayers, Map<Integer, Double> anglesPerPage, IdpResult idpResult) {
} }

View File

@ -85,12 +85,14 @@ public class RotationCorrectionUtility {
List<String> commands = new LinkedList<>(); List<String> commands = new LinkedList<>();
double scale = getScalingFactor(angle, page); double scale = getScalingFactor(angle, page);
double x = page.getCropBox().getWidth() / 2;
double y = page.getCropBox().getHeight() / 2;
commands.add("q"); commands.add("q");
commands.add("/%s <<>> BDC".formatted(KNECON_ROTATION_CORRECTION.markedContentName())); commands.add("/%s <<>> BDC".formatted(KNECON_ROTATION_CORRECTION.markedContentName()));
commands.add(buildMatrixCommands(AffineTransform.getTranslateInstance(page.getPageWidth() / 2, page.getPageHeight() / 2))); commands.add(buildMatrixCommands(AffineTransform.getTranslateInstance(x, y)));
commands.add(buildMatrixCommands(AffineTransform.getRotateInstance(Math.toRadians(angle)))); commands.add(buildMatrixCommands(AffineTransform.getRotateInstance(Math.toRadians(angle))));
commands.add(buildMatrixCommands(AffineTransform.getScaleInstance(scale, scale))); commands.add(buildMatrixCommands(AffineTransform.getScaleInstance(scale, scale)));
commands.add(buildMatrixCommands(AffineTransform.getTranslateInstance(-page.getPageWidth() / 2, -page.getPageHeight() / 2))); commands.add(buildMatrixCommands(AffineTransform.getTranslateInstance(-x, -y)));
commands.add("EMC"); commands.add("EMC");
return String.join("\n", commands); return String.join("\n", commands);
} }
@ -133,22 +135,32 @@ public class RotationCorrectionUtility {
public static AffineTransform buildTransform(double angle, double originalWidth, double originalHeight) { public static AffineTransform buildTransform(double angle, double originalWidth, double originalHeight) {
return buildTransform(angle, originalWidth, originalHeight, true);
}
public static AffineTransform buildTransform(double angle, double originalWidth, double originalHeight, boolean quadrantRotation) {
int quadrants = getQuadrantRotation(angle); int quadrants = getQuadrantRotation(angle);
double h = originalHeight; double h = originalHeight;
double w = originalWidth; double w = originalWidth;
if (quadrants == 1 || quadrants == 3) { AffineTransform quadrantRotationTransform = new AffineTransform();
w = originalHeight; if (quadrantRotation) {
h = originalWidth;
}
AffineTransform quadrantRotation = switch (quadrants) { if (quadrants == 1 || quadrants == 3) {
case 1 -> new AffineTransform(0, 1, -1, 0, h, 0); w = originalHeight;
case 2 -> new AffineTransform(-1, 0, 0, -1, w, h); h = originalWidth;
case 3 -> new AffineTransform(0, -1, 1, 0, w - h, h); }
default -> new AffineTransform();
}; quadrantRotationTransform = switch (quadrants) {
case 1 -> new AffineTransform(0, 1, -1, 0, h, 0);
case 2 -> new AffineTransform(-1, 0, 0, -1, w, h);
case 3 -> new AffineTransform(0, -1, 1, 0, w - h, h);
default -> new AffineTransform();
};
}
double remainder = getRemainingAngle(angle, quadrants); double remainder = getRemainingAngle(angle, quadrants);
double scale = getScalingFactor(remainder, w, h); double scale = getScalingFactor(remainder, w, h);
@ -158,7 +170,7 @@ public class RotationCorrectionUtility {
transform.rotate(Math.toRadians(remainder)); transform.rotate(Math.toRadians(remainder));
transform.scale(scale, scale); transform.scale(scale, scale);
transform.translate(-w / 2, -h / 2); transform.translate(-w / 2, -h / 2);
transform.concatenate(quadrantRotation); transform.concatenate(quadrantRotationTransform);
return transform; return transform;
} }
@ -187,7 +199,7 @@ public class RotationCorrectionUtility {
public static double getRemainingAngle(double angle, int quadrants) { public static double getRemainingAngle(double angle, int quadrants) {
double referenceAngle = 90 * quadrants; double referenceAngle = 90 * quadrants;
return angle - referenceAngle; return (angle - referenceAngle) % 360;
} }

View File

@ -36,10 +36,11 @@ class ImageProcessingPipelineTest {
@BeforeEach @BeforeEach
public void setup() { public void setup() {
new NativeLibrariesInitializer("demo:1650351709282:7bd235e003000000004ec28a6743e1163a085e2115de2536ab6e2cfe5a").init(); new NativeLibrariesInitializer("demo:1650351709282:7bd235e003000000004ec28a6743e1163a085e2115de2536ab6e2cfe5a", "/home/kschuettler/software/leptonica/vcpkg/installed/x64-linux-dynamic/lib/").init();
ImageProcessingService imageProcessingService = new ImageProcessingService(); OcrServiceSettings settings = new OcrServiceSettings();
GhostScriptService ghostScriptService = new GhostScriptService(); ImageProcessingService imageProcessingService = new ImageProcessingService(settings);
GhostScriptService ghostScriptService = new GhostScriptService(settings);
imageProcessingPipeline = new ImageProcessingPipeline(ghostScriptService, imageProcessingService); imageProcessingPipeline = new ImageProcessingPipeline(ghostScriptService, imageProcessingService);
} }

View File

@ -1,17 +1,12 @@
package com.knecon.fforesight.service.ocr.processor.service; package com.knecon.fforesight.service.ocr.processor.service;
import static com.knecon.fforesight.service.ocr.processor.service.OCRService.IMAGE_PIPELINE_DIR;
import static com.knecon.fforesight.service.ocr.processor.service.imageprocessing.ImageProcessingPipeline.PROCESSED_DIR;
import java.awt.Color; import java.awt.Color;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D; import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -32,7 +27,6 @@ import com.knecon.fforesight.service.ocr.processor.model.ImageFile;
import com.knecon.fforesight.service.ocr.processor.model.PageInformation; import com.knecon.fforesight.service.ocr.processor.model.PageInformation;
import com.knecon.fforesight.service.ocr.processor.model.TextPositionInImage; import com.knecon.fforesight.service.ocr.processor.model.TextPositionInImage;
import com.knecon.fforesight.service.ocr.processor.visualizations.WritableOcrResult; import com.knecon.fforesight.service.ocr.processor.visualizations.WritableOcrResult;
import com.knecon.fforesight.service.ocr.processor.visualizations.WritableOcrResultFactory;
import com.knecon.fforesight.service.ocr.processor.visualizations.layers.OcrDebugLayerFactory; import com.knecon.fforesight.service.ocr.processor.visualizations.layers.OcrDebugLayerFactory;
import com.knecon.fforesight.service.ocr.processor.visualizations.utils.RotationCorrectionUtility; import com.knecon.fforesight.service.ocr.processor.visualizations.utils.RotationCorrectionUtility;
import com.knecon.fforesight.service.ocr.v1.api.model.QuadPoint; import com.knecon.fforesight.service.ocr.v1.api.model.QuadPoint;
@ -44,15 +38,8 @@ import com.pdftron.pdf.ElementBuilder;
import com.pdftron.pdf.ElementWriter; import com.pdftron.pdf.ElementWriter;
import com.pdftron.pdf.PDFDoc; import com.pdftron.pdf.PDFDoc;
import com.pdftron.pdf.Page; import com.pdftron.pdf.Page;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.PointerByReference;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.sourceforge.lept4j.Box;
import net.sourceforge.lept4j.Boxa;
import net.sourceforge.lept4j.util.LeptUtils;
@Disabled // leptonica is not available in build server @Disabled // leptonica is not available in build server
public class SnugBoxesTest { public class SnugBoxesTest {
@ -60,9 +47,8 @@ public class SnugBoxesTest {
public static final int PAGE_NUMBER = 41; public static final int PAGE_NUMBER = 41;
public static final Path ORIGIN_FILE = Path.of("/home/kschuettler/Dokumente/TestFiles/OCR/VV-331340-first100.pdf"); public static final Path ORIGIN_FILE = Path.of("/home/kschuettler/Dokumente/TestFiles/OCR/VV-331340-first100.pdf");
public static final Path TEST_FOLDER = Path.of("/tmp/OCR_TEST/").resolve(ORIGIN_FILE.getFileName()); public static final Path TEST_FOLDER = Path.of("/tmp/OCR_TEST/").resolve(ORIGIN_FILE.getFileName());
public static final Path PROCESSED_FOLDER = TEST_FOLDER.resolve(IMAGE_PIPELINE_DIR).resolve(PROCESSED_DIR); public static final Path BATCH_FOLDER = TEST_FOLDER.resolve("batch_0");
public static final Path DESTINATION_FILE = TEST_FOLDER.resolve("SnugBoxesTest.pdf"); public static final Path DESTINATION_FILE = BATCH_FOLDER.resolve("SnugBoxesTest.pdf");
public static final Path RESULT_FILE = TEST_FOLDER.resolve(IMAGE_PIPELINE_DIR).resolve("azure_result_0.json");
PDFTronViewerDocumentService viewerDocumentService = new PDFTronViewerDocumentService(null); PDFTronViewerDocumentService viewerDocumentService = new PDFTronViewerDocumentService(null);
@ -70,7 +56,7 @@ public class SnugBoxesTest {
@BeforeAll @BeforeAll
public static void setUp() { public static void setUp() {
new NativeLibrariesInitializer("demo:1650351709282:7bd235e003000000004ec28a6743e1163a085e2115de2536ab6e2cfe5a").init(); new NativeLibrariesInitializer("demo:1650351709282:7bd235e003000000004ec28a6743e1163a085e2115de2536ab6e2cfe5a", "/home/kschuettler/software/leptonica/vcpkg/installed/x64-linux-dynamic/lib/").init();
} }
@ -79,24 +65,24 @@ public class SnugBoxesTest {
public void snugBoxes() { public void snugBoxes() {
String filePath = ORIGIN_FILE.toFile().toString(); String filePath = ORIGIN_FILE.toFile().toString();
File file = PROCESSED_FOLDER.resolve("output_0.%04d.tiff".formatted(PAGE_NUMBER)).toFile(); File file = new File(filePath);
assert file.exists(); assert file.exists();
ImageFile imageFile = new ImageFile(PAGE_NUMBER, file.toString()); ImageFile imageFile = new ImageFile(PAGE_NUMBER, file.toString());
AnalyzeResult result = null; AnalyzeResult result = null;
try (var in = new FileInputStream(RESULT_FILE.toFile()); JsonReader reader = DefaultJsonReader.fromStream(in, new JsonOptions());) { try (var in = new FileInputStream(BATCH_FOLDER.resolve("analyzeResult.json").toFile()); JsonReader reader = DefaultJsonReader.fromStream(in, new JsonOptions());) {
result = AnalyzeResult.fromJson(reader); result = AnalyzeResult.fromJson(reader);
} }
var resultPage = result.getPages() var resultPage = result.getPages()
.get(PAGE_NUMBER - 1); .get(PAGE_NUMBER - 1);
WritableOcrResultFactory writableOcrResultFactory = new WritableOcrResultFactory(null, null, new OcrServiceSettings(), Set.of()); OcrResultPostProcessingPipeline ocrResultPostProcessingPipeline = new OcrResultPostProcessingPipeline(null, null, new OcrServiceSettings(), Set.of());
OcrDebugLayerFactory debugLayerFactory = new OcrDebugLayerFactory(); OcrDebugLayerFactory debugLayerFactory = new OcrDebugLayerFactory();
InvisibleElementRemovalService invisibleElementRemovalService = new InvisibleElementRemovalService(); InvisibleElementRemovalService invisibleElementRemovalService = new InvisibleElementRemovalService();
try (var in = new FileInputStream(ORIGIN_FILE.toFile()); var out = new FileOutputStream(DESTINATION_FILE.toFile())) { try (var in = new FileInputStream(ORIGIN_FILE.toFile()); var out = new FileOutputStream(DESTINATION_FILE.toFile())) {
invisibleElementRemovalService.removeInvisibleElements(in, out, false); invisibleElementRemovalService.removeInvisibleElements(in, out, false);
} }
PageInformation pageInformation = getPageInformation(PAGE_NUMBER, DESTINATION_FILE.toFile().toString()); PageInformation pageInformation = getPageInformation(PAGE_NUMBER, DESTINATION_FILE.toFile().toString());
WritableOcrResultFactory.Lookups empty = WritableOcrResultFactory.Lookups.empty(); OcrResultPostProcessingPipeline.Lookups empty = OcrResultPostProcessingPipeline.Lookups.empty();
AffineTransform pageCtm = getPageCtm(PAGE_NUMBER, filePath, resultPage.getWidth()); AffineTransform pageCtm = getPageCtm(PAGE_NUMBER, filePath, resultPage.getWidth());
// pageCtm.preConcatenate(rotationCorrection); // pageCtm.preConcatenate(rotationCorrection);
@ -117,7 +103,7 @@ public class SnugBoxesTest {
// //
List<TextPositionInImage> words = writableOcrResultFactory.buildTextWithSnugBBoxes(resultPage, imageFile, pageCtm, empty, pageInformation); List<TextPositionInImage> words = ocrResultPostProcessingPipeline.buildTextWithSnugBBoxes(resultPage, imageFile, pageCtm, empty, pageInformation);
var results = new WritableOcrResult(PAGE_NUMBER, -resultPage.getAngle(), words, Collections.emptyList()); var results = new WritableOcrResult(PAGE_NUMBER, -resultPage.getAngle(), words, Collections.emptyList());
debugLayerFactory.addAnalysisResult(List.of(results)); debugLayerFactory.addAnalysisResult(List.of(results));
@ -231,7 +217,7 @@ public class SnugBoxesTest {
@SneakyThrows @SneakyThrows
private static AffineTransform getPageCtm(int pageNumber, String file, double imageWidh) { private static AffineTransform getPageCtm(int pageNumber, String file, double imageWidh) {
return WritableOcrResultFactory.buildResultToPageTransform(getPageInformation(pageNumber, file), imageWidh); return OcrResultPostProcessingPipeline.buildResultToPageTransform(getPageInformation(pageNumber, file), imageWidh);
} }

View File

@ -31,12 +31,17 @@ dependencies {
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.1") implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.1")
implementation("org.springframework.boot:spring-boot-starter-amqp:${springBootStarterVersion}") implementation("org.springframework.boot:spring-boot-starter-amqp:${springBootStarterVersion}")
implementation("com.iqser.red.service:persistence-service-internal-api-v1:2.224.0")
implementation("com.knecon.fforesight:tenant-commons:0.31.0")
implementation("com.iqser.red.commons:storage-commons:2.50.0")
implementation("net.logstash.logback:logstash-logback-encoder:7.4") implementation("net.logstash.logback:logstash-logback-encoder:7.4")
implementation("ch.qos.logback:logback-classic") implementation("ch.qos.logback:logback-classic")
testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootStarterVersion}") testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootStarterVersion}")
testImplementation("com.iqser.red.commons:test-commons:2.1.0") testImplementation("com.iqser.red.commons:test-commons:2.1.0")
testImplementation("org.springframework.amqp:spring-rabbit-test:3.0.2") testImplementation("org.springframework.amqp:spring-rabbit-test:3.0.2")
testImplementation("com.iqser.red.commons:pdftron-logic-commons:2.32.0")
} }
tasks.named<BootBuildImage>("bootBuildImage") { tasks.named<BootBuildImage>("bootBuildImage") {

View File

@ -9,11 +9,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import com.iqser.red.pdftronlogic.commons.InvisibleElementRemovalService; import com.iqser.red.storage.commons.StorageAutoConfiguration;
import com.iqser.red.pdftronlogic.commons.WatermarkRemovalService;
import com.knecon.fforesight.service.ocr.processor.OcrServiceProcessorConfiguration; import com.knecon.fforesight.service.ocr.processor.OcrServiceProcessorConfiguration;
import com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration; import com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration;
import com.iqser.red.storage.commons.StorageAutoConfiguration;
import com.knecon.fforesight.tenantcommons.MultiTenancyAutoConfiguration; import com.knecon.fforesight.tenantcommons.MultiTenancyAutoConfiguration;
import io.micrometer.core.aop.TimedAspect; import io.micrometer.core.aop.TimedAspect;
@ -43,17 +41,4 @@ public class Application {
} }
@Bean
public InvisibleElementRemovalService invisibleElementRemovalService() {
return new InvisibleElementRemovalService();
}
@Bean
public WatermarkRemovalService watermarkRemovalService() {
return new WatermarkRemovalService();
}
} }

View File

@ -1,4 +1,4 @@
package com.knecon.fforesight.service.ocr.processor.service; package com.knecon.fforesight.service.ocr.v1.server;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -33,20 +33,23 @@ public class FileStorageService {
public void storeFiles(DocumentRequest request, File documentFile, File viewerDocumentFile, File analyzeResultFile) { public void storeFiles(DocumentRequest request, File documentFile, File viewerDocumentFile, File analyzeResultFile) {
try (var in = new FileInputStream(viewerDocumentFile)) { try (var in = new FileInputStream(viewerDocumentFile)) {
if (request.optionalViewerDocumentId().isPresent()) { if (request.optionalViewerDocumentId()
.isPresent()) {
storageService.storeObject(TenantContext.getTenantId(), request.getViewerDocId(), in); storageService.storeObject(TenantContext.getTenantId(), request.getViewerDocId(), in);
} else { } else {
storageService.storeObject(TenantContext.getTenantId(), getStorageId(request.getDossierId(), request.getFileId(), FileType.VIEWER_DOCUMENT), in); storageService.storeObject(TenantContext.getTenantId(), getStorageId(request.getDossierId(), request.getFileId(), FileType.VIEWER_DOCUMENT), in);
} }
} }
try (var in = new FileInputStream(documentFile)) { try (var in = new FileInputStream(documentFile)) {
if (request.optionalOriginDocumentId().isPresent()) { if (request.optionalOriginDocumentId()
.isPresent()) {
storageService.storeObject(TenantContext.getTenantId(), request.getOriginDocumentId(), in); storageService.storeObject(TenantContext.getTenantId(), request.getOriginDocumentId(), in);
} else { } else {
storageService.storeObject(TenantContext.getTenantId(), getStorageId(request.getDossierId(), request.getFileId(), FileType.ORIGIN), in); storageService.storeObject(TenantContext.getTenantId(), getStorageId(request.getDossierId(), request.getFileId(), FileType.ORIGIN), in);
} }
} }
if (request.optionalIdpResultId().isPresent()) { if (request.optionalIdpResultId()
.isPresent() && analyzeResultFile.exists()) {
try (var in = new FileInputStream(analyzeResultFile)) { try (var in = new FileInputStream(analyzeResultFile)) {
storageService.storeObject(TenantContext.getTenantId(), request.getIdpResultId(), in); storageService.storeObject(TenantContext.getTenantId(), request.getIdpResultId(), in);
} }
@ -59,7 +62,8 @@ public class FileStorageService {
Files.createDirectories(documentFile.getParentFile().toPath()); Files.createDirectories(documentFile.getParentFile().toPath());
String originDocumentId = request.optionalOriginDocumentId().orElse(getStorageId(request.getDossierId(), request.getFileId(), FileType.ORIGIN)); String originDocumentId = request.optionalOriginDocumentId()
.orElse(getStorageId(request.getDossierId(), request.getFileId(), FileType.ORIGIN));
storageService.downloadTo(TenantContext.getTenantId(), originDocumentId, documentFile); storageService.downloadTo(TenantContext.getTenantId(), originDocumentId, documentFile);

View File

@ -1,11 +0,0 @@
package com.knecon.fforesight.service.ocr.v1.server.configuration;
import org.springframework.context.annotation.Configuration;
import com.knecon.fforesight.tenantcommons.queue.TenantMessagingConfiguration;
@Configuration
public class TenantMessagingConfigurationImpl extends TenantMessagingConfiguration {
}

View File

@ -0,0 +1,32 @@
package com.knecon.fforesight.service.ocr.v1.server.configuration;
import static com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration.OCR_DLQ;
import static com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration.OCR_REQUEST_EXCHANGE;
import static com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration.OCR_REQUEST_QUEUE_PREFIX;
import java.util.Map;
import java.util.Set;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.knecon.fforesight.service.ocr.v1.server.queue.OcrMessageReceiver;
import com.knecon.fforesight.tenantcommons.model.TenantQueueConfiguration;
import com.knecon.fforesight.tenantcommons.model.TenantQueueProvider;
@Configuration
public class TenantQueueProviderConfig {
@Bean
protected TenantQueueProvider getTenantQueueConfigs() {
return new TenantQueueProvider(Set.of(TenantQueueConfiguration.builder()
.listenerId(OcrMessageReceiver.OCR_REQUEST_LISTENER_ID)
.exchangeName(OCR_REQUEST_EXCHANGE)
.queuePrefix(OCR_REQUEST_QUEUE_PREFIX)
.dlqName(OCR_DLQ)
.arguments(Map.of("x-max-priority", 2))
.build()));
}
}

View File

@ -1,10 +1,13 @@
package com.knecon.fforesight.service.ocr.v1.server.queue; package com.knecon.fforesight.service.ocr.v1.server.queue;
import java.util.Set;
import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.ocr.processor.service.IOcrMessageSender; import com.knecon.fforesight.service.ocr.processor.service.IOcrMessageSender;
import com.knecon.fforesight.service.ocr.v1.api.model.AzureOcrFeature;
import com.knecon.fforesight.service.ocr.v1.api.model.DocumentRequest; import com.knecon.fforesight.service.ocr.v1.api.model.DocumentRequest;
import com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration; import com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration;
import com.knecon.fforesight.tenantcommons.TenantContext; import com.knecon.fforesight.tenantcommons.TenantContext;
@ -22,24 +25,24 @@ public class NoStatusUpdateOcrMessageSender implements IOcrMessageSender {
RabbitTemplate rabbitTemplate; RabbitTemplate rabbitTemplate;
public void sendOcrFinished(String fileId, int totalImages) { public void sendOcrFinished(String fileId, int totalImages, Set<AzureOcrFeature> features) {
} }
public void sendOCRStarted(String fileId) { public void sendOCRStarted(String fileId, Set<AzureOcrFeature> features) {
} }
public void sendUpdate(String fileId, int finishedImages, int totalImages) { public void sendUpdate(String fileId, int finishedImages, int totalImages, Set<AzureOcrFeature> features) {
} }
public void sendOcrResponse(String dossierId, String fileId) { public void sendOcrResponse(DocumentRequest request) {
rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_RESPONSE_EXCHANGE, TenantContext.getTenantId(), new DocumentRequest(dossierId, fileId)); rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_RESPONSE_EXCHANGE, TenantContext.getTenantId(), request);
} }
} }

View File

@ -2,10 +2,11 @@ package com.knecon.fforesight.service.ocr.v1.server.queue;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.stream.Collectors;
import org.slf4j.MDC; import org.slf4j.MDC;
import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.AmqpRejectAndDontRequeueException;
@ -16,7 +17,8 @@ import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils; import org.springframework.util.FileSystemUtils;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.knecon.fforesight.service.ocr.processor.service.FileStorageService; import com.knecon.fforesight.service.ocr.processor.utils.OsUtils;
import com.knecon.fforesight.service.ocr.v1.server.FileStorageService;
import com.knecon.fforesight.service.ocr.processor.service.IOcrMessageSender; import com.knecon.fforesight.service.ocr.processor.service.IOcrMessageSender;
import com.knecon.fforesight.service.ocr.processor.service.OCRService; import com.knecon.fforesight.service.ocr.processor.service.OCRService;
import com.knecon.fforesight.service.ocr.v1.api.model.DocumentRequest; import com.knecon.fforesight.service.ocr.v1.api.model.DocumentRequest;
@ -34,6 +36,9 @@ import lombok.extern.slf4j.Slf4j;
public class OcrMessageReceiver { public class OcrMessageReceiver {
public static final String OCR_REQUEST_LISTENER_ID = "ocr-request-listener"; public static final String OCR_REQUEST_LISTENER_ID = "ocr-request-listener";
public static final String IDP_RESULT_FILE_NAME = "idpResult.json";
public static final String VIEWER_DOCUMENT_FILE_NAME = "viewerDocument.pdf";
public static final String DOCUMENT_FILE_NAME = "document.pdf";
FileStorageService fileStorageService; FileStorageService fileStorageService;
ObjectMapper objectMapper; ObjectMapper objectMapper;
@ -52,25 +57,25 @@ public class OcrMessageReceiver {
DocumentRequest request = objectMapper.readValue(in.getBody(), DocumentRequest.class); DocumentRequest request = objectMapper.readValue(in.getBody(), DocumentRequest.class);
String dossierId = request.getDossierId(); String dossierId = request.getDossierId();
String fileId = request.getFileId(); String fileId = request.getFileId();
Path tmpDir = Files.createTempDirectory(null); Path runDir = Path.of(OsUtils.getTemporaryDirectory()).resolve(request.getDossierId()).resolve(request.getFileId());
try { try {
MDC.put("fileId", fileId); MDC.put("fileId", fileId);
log.info("--------------------------------------------------------------------------"); log.info("--------------------------------- Starting OCR ---------------------------------");
log.info("Features: {}", request.getFeatures().stream().map(Objects::toString).collect(Collectors.joining(", ")));
ocrMessageSender.sendOCRStarted(fileId, request.getFeatures());
ocrMessageSender.sendOCRStarted(fileId); File documentFile = runDir.resolve(DOCUMENT_FILE_NAME).toFile();
File viewerDocumentFile = runDir.resolve(VIEWER_DOCUMENT_FILE_NAME).toFile();
File documentFile = tmpDir.resolve("document.pdf").toFile(); File idpResultFile = runDir.resolve(IDP_RESULT_FILE_NAME).toFile();
File viewerDocumentFile = tmpDir.resolve("viewerDocument.pdf").toFile();
File analyzeResultFile = tmpDir.resolve("azureAnalysisResult.json").toFile();
fileStorageService.downloadFiles(request, documentFile); fileStorageService.downloadFiles(request, documentFile);
ocrService.runOcrOnDocument(dossierId, fileId, request.getFeatures(), tmpDir, documentFile, viewerDocumentFile, analyzeResultFile); ocrService.runOcrOnDocument(dossierId, fileId, request.getFeatures(), runDir, documentFile, viewerDocumentFile, idpResultFile);
fileStorageService.storeFiles(request, documentFile, viewerDocumentFile, analyzeResultFile); fileStorageService.storeFiles(request, documentFile, viewerDocumentFile, idpResultFile);
ocrMessageSender.sendOcrResponse(dossierId, fileId); ocrMessageSender.sendOcrResponse(request);
} catch (Exception e) { } catch (Exception e) {
log.warn("An exception occurred in ocr file stage: {}", e.getMessage()); log.warn("An exception occurred in ocr file stage: {}", e.getMessage());
in.getMessageProperties().getHeaders().put(MessagingConfiguration.X_ERROR_INFO_HEADER, e.getMessage()); in.getMessageProperties().getHeaders().put(MessagingConfiguration.X_ERROR_INFO_HEADER, e.getMessage());
@ -78,8 +83,9 @@ public class OcrMessageReceiver {
throw new RuntimeException(e); throw new RuntimeException(e);
} finally { } finally {
log.info("Done"); log.info("Done");
log.info("--------------------------------- Done ---------------------------------");
MDC.remove("fileId"); MDC.remove("fileId");
FileSystemUtils.deleteRecursively(tmpDir); FileSystemUtils.deleteRecursively(runDir);
} }
} }

View File

@ -1,10 +1,13 @@
package com.knecon.fforesight.service.ocr.v1.server.queue; package com.knecon.fforesight.service.ocr.v1.server.queue;
import java.util.Set;
import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.knecon.fforesight.service.ocr.processor.service.IOcrMessageSender; import com.knecon.fforesight.service.ocr.processor.service.IOcrMessageSender;
import com.knecon.fforesight.service.ocr.v1.api.model.AzureOcrFeature;
import com.knecon.fforesight.service.ocr.v1.api.model.DocumentRequest; import com.knecon.fforesight.service.ocr.v1.api.model.DocumentRequest;
import com.knecon.fforesight.service.ocr.v1.api.model.OCRStatusUpdateResponse; import com.knecon.fforesight.service.ocr.v1.api.model.OCRStatusUpdateResponse;
import com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration; import com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration;
@ -25,38 +28,46 @@ public class OcrMessageSender implements IOcrMessageSender {
RabbitTemplate rabbitTemplate; RabbitTemplate rabbitTemplate;
public void sendOcrFinished(String fileId, int totalImages) { public void sendOcrFinished(String fileId, int totalImages, Set<AzureOcrFeature> features) {
rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_STATUS_UPDATE_RESPONSE_EXCHANGE, rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_STATUS_UPDATE_RESPONSE_EXCHANGE,
TenantContext.getTenantId(), TenantContext.getTenantId(),
OCRStatusUpdateResponse.builder()
OCRStatusUpdateResponse.builder().fileId(fileId).numberOfPagesToOCR(totalImages).numberOfOCRedPages(totalImages).ocrFinished(true).build()); .fileId(fileId)
.numberOfPagesToOCR(totalImages)
.numberOfOCRedPages(totalImages)
.ocrFinished(true)
.features(features)
.build());
} }
public void sendOCRStarted(String fileId) { public void sendOCRStarted(String fileId, Set<AzureOcrFeature> features) {
rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_STATUS_UPDATE_RESPONSE_EXCHANGE, rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_STATUS_UPDATE_RESPONSE_EXCHANGE,
TenantContext.getTenantId(), TenantContext.getTenantId(),
OCRStatusUpdateResponse.builder().fileId(fileId).features(features).ocrStarted(true).build());
OCRStatusUpdateResponse.builder().fileId(fileId).ocrStarted(true).build());
} }
public void sendUpdate(String fileId, int finishedImages, int totalImages) { public void sendUpdate(String fileId, int finishedImages, int totalImages, Set<AzureOcrFeature> features) {
rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_STATUS_UPDATE_RESPONSE_EXCHANGE, rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_STATUS_UPDATE_RESPONSE_EXCHANGE,
TenantContext.getTenantId(), TenantContext.getTenantId(),
OCRStatusUpdateResponse.builder()
OCRStatusUpdateResponse.builder().fileId(fileId).numberOfPagesToOCR(totalImages).numberOfOCRedPages(finishedImages).build()); .fileId(fileId)
.features(features)
.numberOfPagesToOCR(totalImages)
.numberOfOCRedPages(finishedImages)
.build());
} }
public void sendOcrResponse(String dossierId, String fileId) { public void sendOcrResponse(DocumentRequest request) {
rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_RESPONSE_EXCHANGE, TenantContext.getTenantId(), new DocumentRequest(dossierId, fileId)); rabbitTemplate.convertAndSend(MessagingConfiguration.OCR_RESPONSE_EXCHANGE, TenantContext.getTenantId(), request);
} }
} }

View File

@ -1,70 +0,0 @@
package com.knecon.fforesight.service.ocr.v1.server.queue;
import static com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration.OCR_DLQ;
import static com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration.OCR_REQUEST_EXCHANGE;
import static com.knecon.fforesight.service.ocr.v1.server.configuration.MessagingConfiguration.OCR_REQUEST_QUEUE_PREFIX;
import java.util.Map;
import java.util.Set;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import com.knecon.fforesight.tenantcommons.TenantProvider;
import com.knecon.fforesight.tenantcommons.model.TenantCreatedEvent;
import com.knecon.fforesight.tenantcommons.model.TenantQueueConfiguration;
import com.knecon.fforesight.tenantcommons.model.TenantResponse;
import com.knecon.fforesight.tenantcommons.queue.RabbitQueueFromExchangeService;
import com.knecon.fforesight.tenantcommons.queue.TenantExchangeMessageReceiver;
@Service
public class TenantExchangeMessageReceiverImpl extends TenantExchangeMessageReceiver {
public TenantExchangeMessageReceiverImpl(RabbitQueueFromExchangeService rabbitQueueService, TenantProvider tenantProvider) {
super(rabbitQueueService, tenantProvider);
}
@Override
protected Set<TenantQueueConfiguration> getTenantQueueConfigs() {
return Set.of(TenantQueueConfiguration.builder()
.listenerId(OcrMessageReceiver.OCR_REQUEST_LISTENER_ID)
.exchangeName(OCR_REQUEST_EXCHANGE)
.queuePrefix(OCR_REQUEST_QUEUE_PREFIX)
.dlqName(OCR_DLQ)
.arguments(Map.of("x-max-priority", 2))
.build());
}
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
System.out.println("application ready invoked");
super.initializeQueues();
}
@RabbitHandler
@RabbitListener(queues = "#{tenantMessagingConfigurationImpl.getTenantCreatedQueueName()}")
public void reactToTenantCreation(TenantCreatedEvent tenantCreatedEvent) {
super.reactToTenantCreation(tenantCreatedEvent);
}
@RabbitHandler
@RabbitListener(queues = "#{tenantMessagingConfigurationImpl.getTenantDeletedQueueName()}")
public void reactToTenantDeletion(TenantResponse tenantResponse) {
super.reactToTenantDeletion(tenantResponse);
}
}

View File

@ -1,5 +1,5 @@
# you can list packages # you can list packages
ghostscript=9.55.0~dfsg1-0ubuntu5.9 ghostscript=9.55.0~dfsg1-0ubuntu5.10
pkg-config pkg-config
zip zip
unzip unzip

View File

@ -63,3 +63,5 @@ azure:
ocrService: ocrService:
sendStatusUpdates: true sendStatusUpdates: true
native-libs.path: ${VCPKG_DYNAMIC_LIB}

View File

@ -0,0 +1,50 @@
package com.knecon.fforesight.service.ocr.v1.api.model;
import static org.junit.jupiter.api.Assertions.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.junit.jupiter.api.Test;
class QuadPointTest {
@Test
public void testContains() {
var a = new Point2D.Double(0, 0);
var b = new Point2D.Double(0, 1);
var c = new Point2D.Double(1, 1);
var d = new Point2D.Double(1, 0);
var q = new QuadPoint(a, b, c, d);
assertTrue(q.isHorizontal());
assertFalse(q.isVertical());
assertTrue(q.contains(a));
assertTrue(q.contains(b));
assertTrue(q.contains(c));
assertTrue(q.contains(d));
var p = new Point2D.Double(0.5, 0.5);
assertTrue(q.contains(p));
var r = new Rectangle2D.Double(0.5, 0.5, 0.1, 0.1);
assertTrue(q.contains(r));
}
@Test
public void testCenter() {
var a = new Point2D.Double(0, 0);
var b = new Point2D.Double(1, 1);
var c = new Point2D.Double(2, 1);
var d = new Point2D.Double(1, 0);
var q = new QuadPoint(a, b, c, d);
assertTrue(q.isHorizontal());
assertFalse(q.isVertical());
assertEquals(QuadPoint.Direction.RIGHT, q.getDirection());
assertEquals(new Point2D.Double(1, 0.5), q.getCenter());
}
}

View File

@ -1,6 +1,9 @@
package com.knecon.fforesight.service.ocr.v1.server; package com.knecon.fforesight.service.ocr.v1.server;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
@ -10,6 +13,7 @@ import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.MessageListenerContainer;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -57,11 +61,9 @@ public class AbstractTest {
@MockBean @MockBean
private RabbitAdmin rabbitAdmin; private RabbitAdmin rabbitAdmin;
@MockBean
private RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry;
private static String pdftronLicense; private static String pdftronLicense;
@BeforeEach @BeforeEach
public void openMocks() { public void openMocks() {
@ -107,6 +109,16 @@ public class AbstractTest {
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)}) @ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StorageAutoConfiguration.class)})
public static class TestConfiguration { public static class TestConfiguration {
@Bean
public RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry() {
var mock = mock(RabbitListenerEndpointRegistry.class);
when(mock.getListenerContainer(any())).thenReturn(mock(MessageListenerContainer.class));
return mock;
}
@Bean @Bean
@Primary @Primary
public StorageService inMemoryStorage() { public StorageService inMemoryStorage() {

View File

@ -1,6 +1,9 @@
package com.knecon.fforesight.service.ocr.v1.server; package com.knecon.fforesight.service.ocr.v1.server;
import static com.iqser.red.pdftronlogic.commons.PdfTextExtraction.extractAllTextFromDocument; import static com.iqser.red.pdftronlogic.commons.PdfTextExtraction.extractAllTextFromDocument;
import static com.knecon.fforesight.service.ocr.v1.server.queue.OcrMessageReceiver.DOCUMENT_FILE_NAME;
import static com.knecon.fforesight.service.ocr.v1.server.queue.OcrMessageReceiver.IDP_RESULT_FILE_NAME;
import static com.knecon.fforesight.service.ocr.v1.server.queue.OcrMessageReceiver.VIEWER_DOCUMENT_FILE_NAME;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -25,12 +28,12 @@ import com.knecon.fforesight.service.ocr.v1.api.model.AzureOcrFeature;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@Disabled
// in order to run, the azure.key must be set first in the application.yml and you must set the env variable VCPKG_DYNAMIC_LIB to your tesseract and leptonica installation folder // in order to run, the azure.key must be set first in the application.yml and you must set the env variable VCPKG_DYNAMIC_LIB to your tesseract and leptonica installation folder
@Disabled
@SpringBootTest() @SpringBootTest()
public class OcrServiceIntegrationTest extends AbstractTest { public class OcrServiceIntegrationTest extends AbstractTest {
public static final Set<AzureOcrFeature> FEATURES = Set.of(AzureOcrFeature.ROTATION_CORRECTION, AzureOcrFeature.FONT_STYLE_DETECTION); public static final Set<AzureOcrFeature> FEATURES = Set.of(AzureOcrFeature.ROTATION_CORRECTION, AzureOcrFeature.FONT_STYLE_DETECTION, AzureOcrFeature.IDP);
@Autowired @Autowired
private OCRService ocrService; private OCRService ocrService;
@ -55,7 +58,7 @@ public class OcrServiceIntegrationTest extends AbstractTest {
@SneakyThrows @SneakyThrows
public void testOcrWithFile() { public void testOcrWithFile() {
testOCR("/home/kschuettler/Dokumente/LayoutparsingEvaluation/RAW_FILES/Difficult Headlines/VV-284053.pdf/VV-284053.pdf.ORIGIN.pdf"); testOCR("/home/kschuettler/Dokumente/TestFiles/OCR/TestSet/VV-331340-first100.pdf");
} }
@ -108,9 +111,9 @@ public class OcrServiceIntegrationTest extends AbstractTest {
assert tmpDir.toFile().exists() || tmpDir.toFile().mkdirs(); assert tmpDir.toFile().exists() || tmpDir.toFile().mkdirs();
var documentFile = tmpDir.resolve(Path.of("document.pdf")); var documentFile = tmpDir.resolve(Path.of(DOCUMENT_FILE_NAME));
var viewerDocumentFile = tmpDir.resolve(Path.of("viewerDocument.pdf")); var viewerDocumentFile = tmpDir.resolve(Path.of(VIEWER_DOCUMENT_FILE_NAME));
var analyzeResultFile = tmpDir.resolve(Path.of("azureAnalysisResult.json")); var analyzeResultFile = tmpDir.resolve(Path.of(IDP_RESULT_FILE_NAME));
Files.copy(file.toPath(), documentFile, StandardCopyOption.REPLACE_EXISTING); Files.copy(file.toPath(), documentFile, StandardCopyOption.REPLACE_EXISTING);
Files.copy(file.toPath(), viewerDocumentFile, StandardCopyOption.REPLACE_EXISTING); Files.copy(file.toPath(), viewerDocumentFile, StandardCopyOption.REPLACE_EXISTING);

View File

@ -2,8 +2,10 @@ persistence-service.url: "http://persistence-service-v1:8080"
pdftron.license: demo:1650351709282:7bd235e003000000004ec28a6743e1163a085e2115de2536ab6e2cfe5a pdftron.license: demo:1650351709282:7bd235e003000000004ec28a6743e1163a085e2115de2536ab6e2cfe5a
azure: azure:
endpoint: https://ff-ocr-test.cognitiveservices.azure.com/ endpoint: https://ff-ocr-dev.cognitiveservices.azure.com/
key: # find key in Bitwarden under: Azure IDP Test Key key: 444fe2f83e9c48da8e588c7bd5295309 # find key in Bitwarden under: Azure IDP Test Key
native-libs:
logging.type: ${LOGGING_TYPE:CONSOLE} logging.type: ${LOGGING_TYPE:CONSOLE}
@ -19,4 +21,5 @@ management:
endpoints.web.exposure.include: prometheus, health, metrics endpoints.web.exposure.include: prometheus, health, metrics
metrics.export.prometheus.enabled: true metrics.export.prometheus.enabled: true
POD_NAME: azure-ocr-service POD_NAME: azure-ocr-service
native-libs.path: /home/kschuettler/software/leptonica/vcpkg/installed/x64-linux-dynamic/lib/

View File

@ -15,6 +15,7 @@
<exclude name="NullAssignment"/> <exclude name="NullAssignment"/>
<exclude name="AssignmentInOperand"/> <exclude name="AssignmentInOperand"/>
<exclude name="BeanMembersShouldSerialize"/> <exclude name="BeanMembersShouldSerialize"/>
<exclude name="AvoidFieldNameMatchingMethodName"/>
</rule> </rule>
</ruleset> </ruleset>

View File

@ -17,6 +17,7 @@
<exclude name="AssignmentInOperand"/> <exclude name="AssignmentInOperand"/>
<exclude name="TestClassWithoutTestCases"/> <exclude name="TestClassWithoutTestCases"/>
<exclude name="BeanMembersShouldSerialize"/> <exclude name="BeanMembersShouldSerialize"/>
<exclude name="AvoidFieldNameMatchingMethodName"/>
</rule> </rule>
</ruleset> </ruleset>