diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/ThumbnailService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/ThumbnailService.java index 8fbf8d92..620a36c8 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/ThumbnailService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/ThumbnailService.java @@ -86,20 +86,21 @@ public class ThumbnailService { return Outcome.SKIPPED; } - BufferedImage source = readSourceImage(doc, contentType); - if (source == null || source.getWidth() <= 0 || source.getHeight() <= 0) { + SourcePreview preview = readSourcePreview(doc, contentType); + if (preview == null + || preview.image().getWidth() <= 0 || preview.image().getHeight() <= 0) { log.warn("Thumbnail source has invalid dimensions for doc={}", doc.getId()); return Outcome.FAILED; } - byte[] jpeg = encodeThumbnail(source, doc.getId()); + byte[] jpeg = encodeThumbnail(preview.image(), doc.getId()); if (jpeg == null) return Outcome.FAILED; String thumbnailKey = thumbnailKeyFor(doc.getId()); if (!uploadToStorage(thumbnailKey, jpeg, doc.getId())) return Outcome.FAILED; - ThumbnailAspect aspect = aspectOf(source); - return persistThumbnailMetadata(doc, thumbnailKey, aspect); + ThumbnailAspect aspect = aspectOf(preview.image()); + return persistThumbnailMetadata(doc, thumbnailKey, aspect, preview.pageCount()); } private static ThumbnailAspect aspectOf(BufferedImage source) { @@ -107,15 +108,19 @@ public class ThumbnailService { return ratio > LANDSCAPE_THRESHOLD ? ThumbnailAspect.LANDSCAPE : ThumbnailAspect.PORTRAIT; } + // First-page image + total page count for the source file. Page count is always + // 1 for image uploads; for PDFs it comes straight from PDDocument. + private record SourcePreview(BufferedImage image, int pageCount) {} + private static String thumbnailKeyFor(UUID documentId) { return THUMBNAIL_KEY_PREFIX + documentId + THUMBNAIL_KEY_SUFFIX; } - private BufferedImage readSourceImage(Document doc, String contentType) { + private SourcePreview readSourcePreview(Document doc, String contentType) { try { return PDF_CONTENT_TYPE.equals(contentType) ? renderPdfFirstPage(doc.getFilePath()) - : readImage(doc.getFilePath()); + : new SourcePreview(readImage(doc.getFilePath()), 1); } catch (Exception e) { log.warn("Thumbnail source read failed for doc={} reason={}", doc.getId(), e.getMessage()); @@ -151,11 +156,13 @@ public class ThumbnailService { } } - private Outcome persistThumbnailMetadata(Document doc, String thumbnailKey, ThumbnailAspect aspect) { + private Outcome persistThumbnailMetadata(Document doc, String thumbnailKey, + ThumbnailAspect aspect, int pageCount) { try { doc.setThumbnailKey(thumbnailKey); doc.setThumbnailGeneratedAt(LocalDateTime.now()); doc.setThumbnailAspect(aspect); + doc.setPageCount(pageCount); documentRepository.save(doc); return Outcome.SUCCESS; } catch (Exception e) { @@ -174,11 +181,12 @@ public class ThumbnailService { return PDF_CONTENT_TYPE.equals(contentType) || IMAGE_CONTENT_TYPES.contains(contentType); } - private BufferedImage renderPdfFirstPage(String s3Key) throws IOException { + private SourcePreview renderPdfFirstPage(String s3Key) throws IOException { try (InputStream in = fileService.downloadFileStream(s3Key); PDDocument pdf = Loader.loadPDF(new RandomAccessReadBuffer(in))) { PDFRenderer renderer = new PDFRenderer(pdf); - return renderer.renderImageWithDPI(0, PDF_RENDER_DPI, ImageType.RGB); + BufferedImage image = renderer.renderImageWithDPI(0, PDF_RENDER_DPI, ImageType.RGB); + return new SourcePreview(image, pdf.getNumberOfPages()); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/ThumbnailServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/ThumbnailServiceTest.java index df1f62b0..ad423401 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/ThumbnailServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/ThumbnailServiceTest.java @@ -168,6 +168,31 @@ class ThumbnailServiceTest { verify(documentRepository, never()).save(any()); } + @Test + void generate_persistsPageCount_ofOne_forSingleImageUpload() throws IOException { + // Image uploads are always a single page from the user's perspective. + Document doc = makeDoc("image/png", "documents/scan.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.getPageCount()).isEqualTo(1); + } + + @Test + void generate_persistsPageCount_fromPdfDocument() throws IOException { + Document doc = makeDoc("application/pdf", "documents/multi.pdf"); + when(fileService.downloadFileStream(anyString())) + .thenReturn(new ByteArrayInputStream(createSamplePdf(3))); + + ThumbnailService.Outcome outcome = thumbnailService.generate(doc); + + assertThat(outcome).isEqualTo(ThumbnailService.Outcome.SUCCESS); + assertThat(doc.getPageCount()).isEqualTo(3); + } + @Test void generate_persistsPortraitAspect_forTypicalPortraitSourceImage() throws IOException { // 600x800 → ratio w/h = 0.75 → below 1.1 threshold → PORTRAIT. @@ -288,15 +313,21 @@ class ThumbnailServiceTest { } private static byte[] createSamplePdf() throws IOException { + return createSamplePdf(1); + } + + private static byte[] createSamplePdf(int pageCount) throws IOException { try (PDDocument doc = new PDDocument()) { - PDPage page = new PDPage(PDRectangle.A4); - doc.addPage(page); - try (PDPageContentStream content = new PDPageContentStream(doc, page)) { - content.beginText(); - content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 24); - content.newLineAtOffset(100, 700); - content.showText("Lieber Hans,"); - content.endText(); + for (int i = 0; i < pageCount; i++) { + PDPage page = new PDPage(PDRectangle.A4); + doc.addPage(page); + try (PDPageContentStream content = new PDPageContentStream(doc, page)) { + content.beginText(); + content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 24); + content.newLineAtOffset(100, 700); + content.showText("Lieber Hans,"); + content.endText(); + } } ByteArrayOutputStream bos = new ByteArrayOutputStream(); doc.save(bos);