From c19c41f8122c6962fad40c03270978ed46f30559 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 12 Apr 2026 15:12:11 +0200 Subject: [PATCH] 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 --- .../service/AnnotationService.java | 20 ++++++++ .../service/AnnotationServiceTest.java | 49 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java index f52c70b0..6735ef31 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java @@ -48,6 +48,26 @@ public class AnnotationService { return annotationRepository.save(annotation); } + @Transactional + public DocumentAnnotation createOcrAnnotation(UUID documentId, CreateAnnotationDTO dto, + UUID userId, String fileHash, + List> 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 diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java index 2605cfb1..37652179 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java @@ -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> 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