feat(annotations): add createOcrAnnotation that skips overlap check

OCR creates many adjacent text line annotations that would fail the
existing overlap check. createOcrAnnotation() accepts an optional
polygon and bypasses overlap detection entirely.

Refs #227

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-12 15:12:11 +02:00
parent 878a90a86d
commit c19c41f812
2 changed files with 69 additions and 0 deletions

View File

@@ -48,6 +48,26 @@ public class AnnotationService {
return annotationRepository.save(annotation);
}
@Transactional
public DocumentAnnotation createOcrAnnotation(UUID documentId, CreateAnnotationDTO dto,
UUID userId, String fileHash,
List<List<Double>> polygon) {
DocumentAnnotation annotation = DocumentAnnotation.builder()
.documentId(documentId)
.pageNumber(dto.getPageNumber())
.x(dto.getX())
.y(dto.getY())
.width(dto.getWidth())
.height(dto.getHeight())
.color(dto.getColor())
.fileHash(fileHash)
.createdBy(userId)
.polygon(polygon)
.build();
return annotationRepository.save(annotation);
}
@Transactional
public void deleteAnnotation(UUID documentId, UUID annotationId, UUID userId) {
DocumentAnnotation annotation = annotationRepository

View File

@@ -260,6 +260,55 @@ class AnnotationServiceTest {
verify(annotationRepository).save(any());
}
// ─── createOcrAnnotation ──────────────────────────────────────────────────
@Test
void createOcrAnnotation_skipsOverlapCheck_andSavesWithPolygon() {
UUID docId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
List<List<Double>> polygon = List.of(
List.of(0.1, 0.1), List.of(0.9, 0.11),
List.of(0.89, 0.14), List.of(0.11, 0.13));
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
DocumentAnnotation result = annotationService.createOcrAnnotation(
docId, dto, userId, "filehash", polygon);
assertThat(result.getPolygon()).isEqualTo(polygon);
assertThat(result.getDocumentId()).isEqualTo(docId);
verify(annotationRepository).save(any());
verify(annotationRepository, never()).findByDocumentIdAndPageNumber(any(), any(int.class));
}
@Test
void createOcrAnnotation_savesWithNullPolygon_whenPolygonNotProvided() {
UUID docId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
DocumentAnnotation result = annotationService.createOcrAnnotation(
docId, dto, userId, "filehash", null);
assertThat(result.getPolygon()).isNull();
verify(annotationRepository).save(any());
}
@Test
void createOcrAnnotation_doesNotCheckOverlap_evenWhenOverlappingAnnotationExists() {
UUID docId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.3, 0.3, "#00C7B1");
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
annotationService.createOcrAnnotation(docId, dto, userId, "hash", null);
verify(annotationRepository, never()).findByDocumentIdAndPageNumber(any(), any(int.class));
}
// ─── overlaps — partial overlap cases ────────────────────────────────────
@Test
void createAnnotation_noConflict_whenAnnotationIsAbove() {
// x ranges overlap, y ranges don't — existing is ABOVE the new annotation