Compare commits
6 Commits
feat/issue
...
ce1d118882
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce1d118882 | ||
|
|
5cdf52be67 | ||
|
|
01dbf6de86 | ||
|
|
edd7e8f5b0 | ||
|
|
f41729885b | ||
|
|
b12ff9c808 |
@@ -50,6 +50,13 @@ public class Document {
|
|||||||
@Column(name = "thumbnail_generated_at")
|
@Column(name = "thumbnail_generated_at")
|
||||||
private LocalDateTime thumbnailGeneratedAt;
|
private LocalDateTime thumbnailGeneratedAt;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "thumbnail_aspect", length = 16)
|
||||||
|
private ThumbnailAspect thumbnailAspect;
|
||||||
|
|
||||||
|
@Column(name = "page_count")
|
||||||
|
private Integer pageCount;
|
||||||
|
|
||||||
// Originaler Dateiname beim Upload (z.B. "Brief_Oma_1940.pdf")
|
// Originaler Dateiname beim Upload (z.B. "Brief_Oma_1940.pdf")
|
||||||
@Column(name = "original_filename", nullable = false)
|
@Column(name = "original_filename", nullable = false)
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package org.raddatz.familienarchiv.model;
|
||||||
|
|
||||||
|
public enum ThumbnailAspect {
|
||||||
|
PORTRAIT,
|
||||||
|
LANDSCAPE
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
|||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
|
import org.raddatz.familienarchiv.model.ThumbnailAspect;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -45,6 +46,9 @@ public class ThumbnailService {
|
|||||||
private static final int THUMBNAIL_WIDTH = 240;
|
private static final int THUMBNAIL_WIDTH = 240;
|
||||||
private static final float JPEG_QUALITY = 0.85f;
|
private static final float JPEG_QUALITY = 0.85f;
|
||||||
private static final int PDF_RENDER_DPI = 100;
|
private static final int PDF_RENDER_DPI = 100;
|
||||||
|
// Anything below this w/h ratio stays PORTRAIT — near-square A4 scans should
|
||||||
|
// render in the portrait tile rather than flipping to landscape at 1.01.
|
||||||
|
private static final float LANDSCAPE_THRESHOLD = 1.1f;
|
||||||
private static final String PDF_CONTENT_TYPE = "application/pdf";
|
private static final String PDF_CONTENT_TYPE = "application/pdf";
|
||||||
private static final Set<String> IMAGE_CONTENT_TYPES =
|
private static final Set<String> IMAGE_CONTENT_TYPES =
|
||||||
Set.of("image/jpeg", "image/png", "image/tiff");
|
Set.of("image/jpeg", "image/png", "image/tiff");
|
||||||
@@ -82,27 +86,41 @@ public class ThumbnailService {
|
|||||||
return Outcome.SKIPPED;
|
return Outcome.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferedImage source = readSourceImage(doc, contentType);
|
SourcePreview preview = readSourcePreview(doc, contentType);
|
||||||
if (source == null) return Outcome.FAILED;
|
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;
|
if (jpeg == null) return Outcome.FAILED;
|
||||||
|
|
||||||
String thumbnailKey = thumbnailKeyFor(doc.getId());
|
String thumbnailKey = thumbnailKeyFor(doc.getId());
|
||||||
if (!uploadToStorage(thumbnailKey, jpeg, doc.getId())) return Outcome.FAILED;
|
if (!uploadToStorage(thumbnailKey, jpeg, doc.getId())) return Outcome.FAILED;
|
||||||
|
|
||||||
return persistThumbnailMetadata(doc, thumbnailKey);
|
ThumbnailAspect aspect = aspectOf(preview.image());
|
||||||
|
return persistThumbnailMetadata(doc, thumbnailKey, aspect, preview.pageCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ThumbnailAspect aspectOf(BufferedImage source) {
|
||||||
|
float ratio = (float) source.getWidth() / source.getHeight();
|
||||||
|
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) {
|
private static String thumbnailKeyFor(UUID documentId) {
|
||||||
return THUMBNAIL_KEY_PREFIX + documentId + THUMBNAIL_KEY_SUFFIX;
|
return THUMBNAIL_KEY_PREFIX + documentId + THUMBNAIL_KEY_SUFFIX;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BufferedImage readSourceImage(Document doc, String contentType) {
|
private SourcePreview readSourcePreview(Document doc, String contentType) {
|
||||||
try {
|
try {
|
||||||
return PDF_CONTENT_TYPE.equals(contentType)
|
return PDF_CONTENT_TYPE.equals(contentType)
|
||||||
? renderPdfFirstPage(doc.getFilePath())
|
? renderPdfFirstPage(doc.getFilePath())
|
||||||
: readImage(doc.getFilePath());
|
: new SourcePreview(readImage(doc.getFilePath()), 1);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Thumbnail source read failed for doc={} reason={}",
|
log.warn("Thumbnail source read failed for doc={} reason={}",
|
||||||
doc.getId(), e.getMessage());
|
doc.getId(), e.getMessage());
|
||||||
@@ -138,10 +156,13 @@ public class ThumbnailService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Outcome persistThumbnailMetadata(Document doc, String thumbnailKey) {
|
private Outcome persistThumbnailMetadata(Document doc, String thumbnailKey,
|
||||||
|
ThumbnailAspect aspect, int pageCount) {
|
||||||
try {
|
try {
|
||||||
doc.setThumbnailKey(thumbnailKey);
|
doc.setThumbnailKey(thumbnailKey);
|
||||||
doc.setThumbnailGeneratedAt(LocalDateTime.now());
|
doc.setThumbnailGeneratedAt(LocalDateTime.now());
|
||||||
|
doc.setThumbnailAspect(aspect);
|
||||||
|
doc.setPageCount(pageCount);
|
||||||
documentRepository.save(doc);
|
documentRepository.save(doc);
|
||||||
return Outcome.SUCCESS;
|
return Outcome.SUCCESS;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -160,11 +181,12 @@ public class ThumbnailService {
|
|||||||
return PDF_CONTENT_TYPE.equals(contentType) || IMAGE_CONTENT_TYPES.contains(contentType);
|
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);
|
try (InputStream in = fileService.downloadFileStream(s3Key);
|
||||||
PDDocument pdf = Loader.loadPDF(new RandomAccessReadBuffer(in))) {
|
PDDocument pdf = Loader.loadPDF(new RandomAccessReadBuffer(in))) {
|
||||||
PDFRenderer renderer = new PDFRenderer(pdf);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Adds two nullable metadata columns populated by ThumbnailService when it
|
||||||
|
-- generates the JPEG preview: thumbnail_aspect (PORTRAIT | LANDSCAPE, from the
|
||||||
|
-- source image w/h ratio with threshold 1.1) and page_count (from PDDocument
|
||||||
|
-- for PDFs, 1 for image uploads). Both are null until the existing admin
|
||||||
|
-- backfill endpoint (/api/admin/generate-thumbnails) reruns the service.
|
||||||
|
ALTER TABLE documents
|
||||||
|
ADD COLUMN thumbnail_aspect VARCHAR(16),
|
||||||
|
ADD COLUMN page_count INTEGER;
|
||||||
@@ -8,6 +8,7 @@ import org.raddatz.familienarchiv.model.DocumentAnnotation;
|
|||||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||||
import org.raddatz.familienarchiv.model.Person;
|
import org.raddatz.familienarchiv.model.Person;
|
||||||
import org.raddatz.familienarchiv.model.Tag;
|
import org.raddatz.familienarchiv.model.Tag;
|
||||||
|
import org.raddatz.familienarchiv.model.ThumbnailAspect;
|
||||||
import org.raddatz.familienarchiv.model.TranscriptionBlock;
|
import org.raddatz.familienarchiv.model.TranscriptionBlock;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||||
@@ -65,6 +66,40 @@ class DocumentRepositoryTest {
|
|||||||
assertThat(found.get().getStatus()).isEqualTo(DocumentStatus.PLACEHOLDER);
|
assertThat(found.get().getStatus()).isEqualTo(DocumentStatus.PLACEHOLDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── thumbnailAspect + pageCount round-trip ───────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void save_persistsThumbnailAspectAndPageCount() {
|
||||||
|
Document document = Document.builder()
|
||||||
|
.title("Mit Aspekt")
|
||||||
|
.originalFilename("aspect.pdf")
|
||||||
|
.status(DocumentStatus.UPLOADED)
|
||||||
|
.thumbnailAspect(ThumbnailAspect.LANDSCAPE)
|
||||||
|
.pageCount(7)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Document saved = documentRepository.save(document);
|
||||||
|
Document found = documentRepository.findById(saved.getId()).orElseThrow();
|
||||||
|
|
||||||
|
assertThat(found.getThumbnailAspect()).isEqualTo(ThumbnailAspect.LANDSCAPE);
|
||||||
|
assertThat(found.getPageCount()).isEqualTo(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void save_thumbnailAspectAndPageCount_defaultToNull() {
|
||||||
|
Document document = Document.builder()
|
||||||
|
.title("Ohne Aspekt")
|
||||||
|
.originalFilename("no_aspect.pdf")
|
||||||
|
.status(DocumentStatus.PLACEHOLDER)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Document saved = documentRepository.save(document);
|
||||||
|
Document found = documentRepository.findById(saved.getId()).orElseThrow();
|
||||||
|
|
||||||
|
assertThat(found.getThumbnailAspect()).isNull();
|
||||||
|
assertThat(found.getPageCount()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
// ─── findByStatus ─────────────────────────────────────────────────────────
|
// ─── findByStatus ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -302,6 +302,51 @@ class MigrationIntegrationTest {
|
|||||||
).isInstanceOf(DataIntegrityViolationException.class);
|
).isInstanceOf(DataIntegrityViolationException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── V53: add thumbnail_aspect + page_count columns to documents ─────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void v53_thumbnailAspectColumn_existsAndIsNullable() {
|
||||||
|
UUID docId = createDocument();
|
||||||
|
|
||||||
|
// Column must exist and accept NULL (freshly-created doc has no thumbnail yet)
|
||||||
|
String aspect = jdbc.queryForObject(
|
||||||
|
"SELECT thumbnail_aspect FROM documents WHERE id = ?", String.class, docId);
|
||||||
|
assertThat(aspect).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void v53_pageCountColumn_existsAndIsNullable() {
|
||||||
|
UUID docId = createDocument();
|
||||||
|
|
||||||
|
Integer pageCount = jdbc.queryForObject(
|
||||||
|
"SELECT page_count FROM documents WHERE id = ?", Integer.class, docId);
|
||||||
|
assertThat(pageCount).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void v53_thumbnailAspectColumn_acceptsPortraitAndLandscape() {
|
||||||
|
UUID docId = createDocument();
|
||||||
|
|
||||||
|
int portraitRows = jdbc.update(
|
||||||
|
"UPDATE documents SET thumbnail_aspect = 'PORTRAIT' WHERE id = ?", docId);
|
||||||
|
assertThat(portraitRows).isEqualTo(1);
|
||||||
|
|
||||||
|
int landscapeRows = jdbc.update(
|
||||||
|
"UPDATE documents SET thumbnail_aspect = 'LANDSCAPE' WHERE id = ?", docId);
|
||||||
|
assertThat(landscapeRows).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void v53_pageCountColumn_storesInteger() {
|
||||||
|
UUID docId = createDocument();
|
||||||
|
|
||||||
|
jdbc.update("UPDATE documents SET page_count = 4 WHERE id = ?", docId);
|
||||||
|
|
||||||
|
Integer stored = jdbc.queryForObject(
|
||||||
|
"SELECT page_count FROM documents WHERE id = ?", Integer.class, docId);
|
||||||
|
assertThat(stored).isEqualTo(4);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── V51: backfill annotation_id on block comments and notifications ─────
|
// ─── V51: backfill annotation_id on block comments and notifications ─────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||||
|
import org.raddatz.familienarchiv.model.ThumbnailAspect;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
import software.amazon.awssdk.core.sync.RequestBody;
|
import software.amazon.awssdk.core.sync.RequestBody;
|
||||||
@@ -167,6 +168,116 @@ class ThumbnailServiceTest {
|
|||||||
verify(documentRepository, never()).save(any());
|
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.
|
||||||
|
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.
|
||||||
|
// Without the corrupt-image guard this would later NPE inside the aspect /
|
||||||
|
// dimension computation in scaleToWidth.
|
||||||
|
Document doc = makeDoc("image/jpeg", "documents/corrupt.jpg");
|
||||||
|
byte[] truncated = new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0};
|
||||||
|
when(fileService.downloadFileStream(anyString()))
|
||||||
|
.thenReturn(new ByteArrayInputStream(truncated));
|
||||||
|
|
||||||
|
ThumbnailService.Outcome outcome = thumbnailService.generate(doc);
|
||||||
|
|
||||||
|
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
||||||
|
verifyNoInteractions(s3Client);
|
||||||
|
verify(documentRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generate_returnsFailed_whenPdfBytesAreCorrupt() throws IOException {
|
||||||
|
// "PDF" header but no body — PDFBox throws IOException while loading.
|
||||||
|
Document doc = makeDoc("application/pdf", "documents/corrupt.pdf");
|
||||||
|
byte[] fakePdf = "%PDF-1.4\n".getBytes();
|
||||||
|
when(fileService.downloadFileStream(anyString()))
|
||||||
|
.thenReturn(new ByteArrayInputStream(fakePdf));
|
||||||
|
|
||||||
|
ThumbnailService.Outcome outcome = thumbnailService.generate(doc);
|
||||||
|
|
||||||
|
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
||||||
|
verifyNoInteractions(s3Client);
|
||||||
|
verify(documentRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void generate_returnsFailed_whenPersistThrows_butUploadSucceeded() throws IOException {
|
void generate_returnsFailed_whenPersistThrows_butUploadSucceeded() throws IOException {
|
||||||
// Covers the "orphan thumbnail" edge case: S3 upload succeeded but the
|
// Covers the "orphan thumbnail" edge case: S3 upload succeeded but the
|
||||||
@@ -202,15 +313,21 @@ class ThumbnailServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] createSamplePdf() throws IOException {
|
private static byte[] createSamplePdf() throws IOException {
|
||||||
|
return createSamplePdf(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] createSamplePdf(int pageCount) throws IOException {
|
||||||
try (PDDocument doc = new PDDocument()) {
|
try (PDDocument doc = new PDDocument()) {
|
||||||
PDPage page = new PDPage(PDRectangle.A4);
|
for (int i = 0; i < pageCount; i++) {
|
||||||
doc.addPage(page);
|
PDPage page = new PDPage(PDRectangle.A4);
|
||||||
try (PDPageContentStream content = new PDPageContentStream(doc, page)) {
|
doc.addPage(page);
|
||||||
content.beginText();
|
try (PDPageContentStream content = new PDPageContentStream(doc, page)) {
|
||||||
content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 24);
|
content.beginText();
|
||||||
content.newLineAtOffset(100, 700);
|
content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 24);
|
||||||
content.showText("Lieber Hans,");
|
content.newLineAtOffset(100, 700);
|
||||||
content.endText();
|
content.showText("Lieber Hans,");
|
||||||
|
content.endText();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
doc.save(bos);
|
doc.save(bos);
|
||||||
|
|||||||
@@ -1386,6 +1386,10 @@ export interface components {
|
|||||||
thumbnailKey?: string;
|
thumbnailKey?: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
thumbnailGeneratedAt?: string;
|
thumbnailGeneratedAt?: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
thumbnailAspect?: "PORTRAIT" | "LANDSCAPE";
|
||||||
|
/** Format: int32 */
|
||||||
|
pageCount?: number;
|
||||||
originalFilename: string;
|
originalFilename: string;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
status: "PLACEHOLDER" | "UPLOADED" | "TRANSCRIBED" | "REVIEWED" | "ARCHIVED";
|
status: "PLACEHOLDER" | "UPLOADED" | "TRANSCRIBED" | "REVIEWED" | "ARCHIVED";
|
||||||
|
|||||||
Reference in New Issue
Block a user