Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23e63db6c5 | ||
|
|
635fd4abf8 | ||
|
|
98123a5938 | ||
|
|
9bbeaf2335 | ||
|
|
b6666f6953 | ||
|
|
80dfa16103 | ||
|
|
76c8e98384 | ||
|
|
df154cfe9c | ||
|
|
e7b61353bf | ||
|
|
d63562ad24 | ||
|
|
14bd6cf5c8 | ||
|
|
ebfa55d3d1 | ||
|
|
f06e5779f3 | ||
|
|
1d1bd321c2 | ||
|
|
9ed9a3c37c | ||
|
|
eac6a49100 | ||
|
|
0b8de28823 |
@ -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<>();
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.knecon.fforesight.service.ocr.v1.api.model;
|
||||||
|
|
||||||
|
public record TableCell(TextRegion textRegion, int row, int col, TableCellType kind) {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.knecon.fforesight.service.ocr.v1.api.model;
|
||||||
|
|
||||||
|
public enum TableCellType {
|
||||||
|
ROW_HEADER, COLUMN_HEADER, CONTENT, STUB_HEAD, DESCRIPTION
|
||||||
|
}
|
||||||
@ -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.32.0")
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
|
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
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....
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -162,7 +162,7 @@ public class Statistics {
|
|||||||
|
|
||||||
return batchStats.values()
|
return batchStats.values()
|
||||||
.stream()
|
.stream()
|
||||||
.mapToLong(BatchStats::getWritingTextDuration)
|
.mapToLong(BatchStats::getMappingResultDuration)
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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;}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()) {
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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());
|
||||||
|
|
||||||
@ -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;
|
||||||
@ -31,7 +31,7 @@ public class BBoxSnuggificationService {
|
|||||||
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
|
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;
|
private static final int MINIMUM_WORD_PIXELS = 5; // Number of pixels that are required for snuggification
|
||||||
|
|
||||||
private enum Operation {
|
private enum Operation {
|
||||||
HORIZONTAL,
|
HORIZONTAL,
|
||||||
@ -48,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();
|
||||||
@ -58,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));
|
||||||
@ -133,7 +138,7 @@ 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) {
|
if (Math.abs(start - end) < MINIMUM_WORD_PIXELS) {
|
||||||
return Optional.empty();
|
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()));
|
||||||
@ -159,7 +164,7 @@ 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) {
|
if (Math.abs(start - end) < MINIMUM_WORD_PIXELS) {
|
||||||
return Optional.empty();
|
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)));
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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") {
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
@ -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 {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -63,3 +63,5 @@ azure:
|
|||||||
|
|
||||||
ocrService:
|
ocrService:
|
||||||
sendStatusUpdates: true
|
sendStatusUpdates: true
|
||||||
|
|
||||||
|
native-libs.path: ${VCPKG_DYNAMIC_LIB}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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() {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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/
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user