feat(thumbnails): persist thumbnailAspect from source image dimensions

Computes aspect at generate-time from the loaded BufferedImage: w/h
above 1.1 → LANDSCAPE, otherwise PORTRAIT. The threshold keeps
near-square A4 scans in the portrait tile (ratio ≈ 1.0) rather than
flipping to landscape on a rounding error. Also hardens the pipeline
with an explicit dimension guard so width=0 / height=0 edge cases fail
cleanly instead of dividing by zero when the aspect is computed.

Refs #305

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-23 14:21:17 +02:00
committed by marcel
parent 7fc517b787
commit b48533be26
2 changed files with 71 additions and 3 deletions

View File

@@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.model.DocumentStatus;
import org.raddatz.familienarchiv.model.ThumbnailAspect;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.springframework.test.util.ReflectionTestUtils;
import software.amazon.awssdk.core.sync.RequestBody;
@@ -167,6 +168,59 @@ class ThumbnailServiceTest {
verify(documentRepository, never()).save(any());
}
@Test
void generate_persistsPortraitAspect_forTypicalPortraitSourceImage() throws IOException {
// 600x800 → ratio w/h = 0.75 → below 1.1 threshold → PORTRAIT.
Document doc = makeDoc("image/png", "documents/portrait.png");
when(fileService.downloadFileStream(anyString()))
.thenReturn(new ByteArrayInputStream(createSamplePng(600, 800)));
ThumbnailService.Outcome outcome = thumbnailService.generate(doc);
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.SUCCESS);
assertThat(doc.getThumbnailAspect()).isEqualTo(ThumbnailAspect.PORTRAIT);
}
@Test
void generate_persistsLandscapeAspect_whenWidthIsWellAboveHeight() throws IOException {
// 800x400 → ratio 2.0 → clearly above 1.1 → LANDSCAPE.
Document doc = makeDoc("image/jpeg", "documents/wide.jpg");
when(fileService.downloadFileStream(anyString()))
.thenReturn(new ByteArrayInputStream(createSampleJpeg(800, 400)));
ThumbnailService.Outcome outcome = thumbnailService.generate(doc);
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.SUCCESS);
assertThat(doc.getThumbnailAspect()).isEqualTo(ThumbnailAspect.LANDSCAPE);
}
@Test
void generate_persistsPortraitAspect_whenSquareImage_belowLandscapeThreshold() throws IOException {
// 500x500 → ratio 1.0 → below 1.1 threshold → PORTRAIT (A4 scans often
// come in at near-square and we want them to live in the portrait tile).
Document doc = makeDoc("image/png", "documents/square.png");
when(fileService.downloadFileStream(anyString()))
.thenReturn(new ByteArrayInputStream(createSamplePng(500, 500)));
ThumbnailService.Outcome outcome = thumbnailService.generate(doc);
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.SUCCESS);
assertThat(doc.getThumbnailAspect()).isEqualTo(ThumbnailAspect.PORTRAIT);
}
@Test
void generate_persistsPortraitAspect_justUnderLandscapeThreshold() throws IOException {
// 1099x1000 → ratio 1.099 → just under 1.1 threshold → PORTRAIT.
Document doc = makeDoc("image/png", "documents/near_threshold.png");
when(fileService.downloadFileStream(anyString()))
.thenReturn(new ByteArrayInputStream(createSamplePng(1099, 1000)));
ThumbnailService.Outcome outcome = thumbnailService.generate(doc);
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.SUCCESS);
assertThat(doc.getThumbnailAspect()).isEqualTo(ThumbnailAspect.PORTRAIT);
}
@Test
void generate_returnsFailed_whenImageBytesAreCorrupt() throws IOException {
// Truncated JPEG header — ImageIO returns null rather than throwing.