Added download

This commit is contained in:
verboomp
2026-02-03 09:51:03 +01:00
parent f9ca668b39
commit 5f1d2d8610
25 changed files with 874 additions and 145 deletions

View File

@@ -163,6 +163,11 @@ public class Picture extends AbstractDateEntity {
return this;
}
public Builder evaluation(Integer evaluation) {
instance.setEvaluation(evaluation);
return this;
}
public Builder customer(Customer customer) {
instance.setCustomer(customer);
return this;

View File

@@ -11,11 +11,13 @@ import org.apache.commons.logging.LogFactory;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
import marketing.heyday.hartmann.fotodocumentation.core.utils.PdfUtils;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerListValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerValue;
@@ -35,6 +37,9 @@ import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerValue;
public class CustomerPictureService extends AbstractService {
private static final Log LOG = LogFactory.getLog(CustomerPictureService.class);
@Inject
private PdfUtils pdfUtils;
public boolean addCustomerPicture(CustomerPictureValue customerPictureValue) {
Optional<Customer> customerOpt = queryService.callNamedQuerySingleResult(Customer.FIND_BY_NUMBER, new Param(Customer.PARAM_NUMBER, customerPictureValue.customerNumber()));
Customer customer = customerOpt.orElseGet(() -> new Customer.Builder().customerNumber(customerPictureValue.customerNumber()).name(customerPictureValue.pharmacyName())
@@ -134,4 +139,20 @@ public class CustomerPictureService extends AbstractService {
return CustomerValue.builder(customer, baseUrl);
}
public byte[] getExport(Long id, Long pictureId) {
Customer customer = entityManager.find(Customer.class, id);
if (customer == null) {
return null;
}
List<Picture> pictures = customer.getPictures().stream().sorted((x, y) -> x.getPictureDate().compareTo(y.getPictureDate())).toList();
if (pictureId != null) {
Optional<Picture> pictureOpt = customer.getPictures().stream().filter(p -> p.getPictureId().equals(pictureId)).findFirst();
pictures = pictureOpt.map(p -> Arrays.asList(p)).orElse(pictures);
}
return pdfUtils.createPdf(customer, pictures);
}
}

View File

@@ -0,0 +1,300 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.List;
import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 2 Feb 2026
*/
public class PdfUtils {
private static final Log LOG = LogFactory.getLog(PdfUtils.class);
private static final String FONT_PANTON_REGULAR = "fonts/Panton-Regular.ttf";
private static final String FONT_PANTON_BOLD = "fonts/Panton-Bold.ttf";
private static final Color COLOR_CUSTOMER_NAME = new Color(0x00, 0x45, 0xFF);
private static final Color COLOR_DATE = new Color(0x00, 0x16, 0x89);
private static final Color COLOR_TEXT_GRAY = new Color(0x2F, 0x2F, 0x2F);
private static final Color COLOR_GREEN = new Color(76, 175, 80);
private static final Color COLOR_YELLOW = new Color(255, 193, 7);
private static final Color COLOR_RED = new Color(244, 67, 54);
private static final Color COLOR_HIGHLIGHT = new Color(41, 98, 175);
private static final float PAGE_MARGIN = 40f;
private static final float CIRCLE_RADIUS = 8f;
private static final float HIGHLIGHT_RADIUS = 12f;
private static final float CIRCLE_SPACING = 30f;
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.GERMAN);
public byte[] createPdf(Customer customer, List<Picture> pictures) {
try (PDDocument document = new PDDocument()) {
PDFont fontBold = loadFont(document, FONT_PANTON_BOLD, Standard14Fonts.FontName.HELVETICA_BOLD);
PDFont fontRegular = loadFont(document, FONT_PANTON_REGULAR, Standard14Fonts.FontName.HELVETICA);
boolean firstPage = true;
for (Picture picture : pictures) {
PDPage page = new PDPage(new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()));
document.addPage(page);
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
float contentWidth = pageWidth - 2 * PAGE_MARGIN;
float halfWidth = contentWidth / 2f;
try (PDPageContentStream cs = new PDPageContentStream(document, page)) {
float yPosition = pageHeight - 50f;
// Customer name on the first page
if (firstPage) {
cs.setFont(fontBold, 50);
cs.setNonStrokingColor(COLOR_CUSTOMER_NAME);
cs.beginText();
cs.newLineAtOffset(PAGE_MARGIN, yPosition);
cs.showText(nullSafe(customer.getName()));
cs.endText();
yPosition -= 60f;
firstPage = false;
}
// Left side: image (50% of content width)
float imageX = PAGE_MARGIN;
float imageY = yPosition;
float imageMaxWidth = halfWidth - 10f;
float imageMaxHeight = pageHeight - 2 * PAGE_MARGIN - 40f;
if (picture.getImage() != null) {
try {
byte[] imageBytes = Base64.getDecoder().decode(picture.getImage());
PDImageXObject pdImage = PDImageXObject.createFromByteArray(document, imageBytes, "picture");
float scale = Math.min(imageMaxWidth / pdImage.getWidth(), imageMaxHeight / pdImage.getHeight());
float drawWidth = pdImage.getWidth() * scale;
float drawHeight = pdImage.getHeight() * scale;
cs.drawImage(pdImage, imageX, imageY - drawHeight, drawWidth, drawHeight);
} catch (Exception e) {
LOG.error("Failed to embed image in PDF", e);
}
}
// Right side: metadata (top-aligned with image)
float rightX = PAGE_MARGIN + halfWidth + 10f;
float rightY = imageY - 32f;
// Date (no label, bold, size 44)
String dateStr = picture.getPictureDate() != null ? DATE_FORMAT.format(picture.getPictureDate()) + " UHR": "";
cs.setFont(fontBold, 32);
cs.setNonStrokingColor(COLOR_DATE);
cs.beginText();
cs.newLineAtOffset(rightX, rightY);
cs.showText(dateStr);
cs.endText();
rightY -= 54f;
// Customer number
float kundenNummerY = rightY;
rightY = drawLabel(cs, fontBold, "KUNDENNUMMER", rightX, rightY);
rightY = drawValue(cs, fontRegular, nullSafe(customer.getCustomerNumber()), rightX, rightY);
rightY -= 10f;
// Evaluation card with circles
float circlesX = rightX + 140f;
drawEvaluationCard(cs, fontBold, circlesX, kundenNummerY, picture.getEvaluation());
// ZIP
rightY = drawLabel(cs, fontBold, "PLZ", rightX, rightY);
rightY = drawValue(cs, fontRegular, nullSafe(customer.getZip()), rightX, rightY);
rightY -= 10f;
// City
rightY = drawLabel(cs, fontBold, "ORT", rightX, rightY);
rightY = drawValue(cs, fontRegular, nullSafe(customer.getCity()), rightX, rightY);
rightY -= 10f;
// Comment
rightY = drawLabel(cs, fontBold, "KOMMENTAR", rightX, rightY);
drawWrappedText(cs, fontRegular, nullSafe(picture.getComment()), rightX, rightY, halfWidth - 20f);
}
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
document.save(output);
return output.toByteArray();
} catch (IOException e) {
LOG.error("Failed to create PDF", e);
return new byte[0];
}
}
private PDFont loadFont(PDDocument document, String resourcePath, Standard14Fonts.FontName fallback) {
try (InputStream fontStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath)) {
if (fontStream != null) {
return PDType0Font.load(document, fontStream);
}
} catch (IOException e) {
LOG.warn("Failed to load font " + resourcePath + ", using fallback", e);
}
LOG.info("Font " + resourcePath + " not found, using fallback " + fallback.getName());
return new PDType1Font(fallback);
}
private float drawLabel(PDPageContentStream cs, PDFont font, String label, float x, float y) throws IOException {
cs.setFont(font, 10);
cs.setNonStrokingColor(COLOR_CUSTOMER_NAME);
cs.beginText();
cs.newLineAtOffset(x, y);
cs.showText(label);
cs.endText();
return y - 14f;
}
private float drawValue(PDPageContentStream cs, PDFont font, String value, float x, float y) throws IOException {
cs.setFont(font, 10);
cs.setNonStrokingColor(COLOR_TEXT_GRAY);
cs.beginText();
cs.newLineAtOffset(x, y);
cs.showText(value);
cs.endText();
return y - 14f;
}
private void drawWrappedText(PDPageContentStream cs, PDFont font, String text, float x, float y, float maxWidth) throws IOException {
if (text == null || text.isEmpty()) {
return;
}
cs.setFont(font, 10);
cs.setNonStrokingColor(COLOR_TEXT_GRAY);
String[] words = text.split("\\s+");
StringBuilder line = new StringBuilder();
float currentY = y;
for (String word : words) {
String testLine = line.isEmpty() ? word : line + " " + word;
float textWidth = font.getStringWidth(testLine) / 1000f * 10f;
if (textWidth > maxWidth && !line.isEmpty()) {
cs.beginText();
cs.newLineAtOffset(x, currentY);
cs.showText(line.toString());
cs.endText();
currentY -= 14f;
line = new StringBuilder(word);
} else {
line = new StringBuilder(testLine);
}
}
if (!line.isEmpty()) {
cs.beginText();
cs.newLineAtOffset(x, currentY);
cs.showText(line.toString());
cs.endText();
}
}
private void drawEvaluationCard(PDPageContentStream cs, PDFont fontBold, float x, float y, Integer evaluation) throws IOException {
int eval = evaluation != null ? evaluation : 0;
Color[] colors = { COLOR_GREEN, COLOR_YELLOW, COLOR_RED };
float cardPadding = 10f;
float cardWidth = 2 * CIRCLE_SPACING + 2 * HIGHLIGHT_RADIUS + 2 * cardPadding;
float labelHeight = 14f;
float cardHeight = labelHeight + 2 * HIGHLIGHT_RADIUS + 2 * cardPadding + 4f;
float cardX = x - HIGHLIGHT_RADIUS - cardPadding;
float cardY = y - cardHeight + cardPadding;
// Draw card background (rounded rectangle)
cs.setStrokingColor(new Color(0xDD, 0xDD, 0xDD));
cs.setNonStrokingColor(new Color(0xF8, 0xF8, 0xF8));
cs.setLineWidth(1f);
drawRoundedRect(cs, cardX, cardY, cardWidth, cardHeight, 6f);
cs.fillAndStroke();
// Draw "BEWERTUNG" label above circles
float labelX = x;
float labelY = y - labelHeight;
cs.setFont(fontBold, 9);
cs.setNonStrokingColor(COLOR_CUSTOMER_NAME);
cs.beginText();
cs.newLineAtOffset(labelX, labelY);
cs.showText("BEWERTUNG");
cs.endText();
// Draw circles below the label
float circleY = labelY - cardPadding - HIGHLIGHT_RADIUS - 2f;
for (int i = 0; i < 3; i++) {
float cx = x + i * CIRCLE_SPACING;
// Highlight circle if this matches the evaluation (1=green, 2=yellow, 3=red)
if (eval == i + 1) {
cs.setStrokingColor(COLOR_HIGHLIGHT);
cs.setLineWidth(2f);
drawCircle(cs, cx, circleY, HIGHLIGHT_RADIUS);
cs.stroke();
}
// Filled color circle
cs.setNonStrokingColor(colors[i]);
drawCircle(cs, cx, circleY, CIRCLE_RADIUS);
cs.fill();
}
}
private void drawRoundedRect(PDPageContentStream cs, float x, float y, float w, float h, float r) throws IOException {
cs.moveTo(x + r, y);
cs.lineTo(x + w - r, y);
cs.curveTo(x + w, y, x + w, y, x + w, y + r);
cs.lineTo(x + w, y + h - r);
cs.curveTo(x + w, y + h, x + w, y + h, x + w - r, y + h);
cs.lineTo(x + r, y + h);
cs.curveTo(x, y + h, x, y + h, x, y + h - r);
cs.lineTo(x, y + r);
cs.curveTo(x, y, x, y, x + r, y);
cs.closePath();
}
private void drawCircle(PDPageContentStream cs, float cx, float cy, float r) throws IOException {
float k = 0.5523f; // Bezier approximation for circle
cs.moveTo(cx - r, cy);
cs.curveTo(cx - r, cy + r * k, cx - r * k, cy + r, cx, cy + r);
cs.curveTo(cx + r * k, cy + r, cx + r, cy + r * k, cx + r, cy);
cs.curveTo(cx + r, cy - r * k, cx + r * k, cy - r, cx, cy - r);
cs.curveTo(cx - r * k, cy - r, cx - r, cy - r * k, cx - r, cy);
cs.closePath();
}
private String nullSafe(String value) {
return value != null ? value : "";
}
}

View File

@@ -2,6 +2,8 @@ package marketing.heyday.hartmann.fotodocumentation.rest;
import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
import java.io.OutputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.resteasy.annotations.GZIP;
@@ -16,7 +18,9 @@ import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.core.Response.Status;
import marketing.heyday.hartmann.fotodocumentation.core.service.CustomerPictureService;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerListValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerValue;
@@ -34,13 +38,13 @@ import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerValue;
@Path("customer")
public class CustomerResource {
private static final Log LOG = LogFactory.getLog(CustomerResource.class);
@Context
private UriInfo uriInfo;
@EJB
private CustomerPictureService customerPictureService;
@GZIP
@GET
@Path("")
@@ -49,10 +53,10 @@ public class CustomerResource {
@ApiResponse(responseCode = "200", description = "Successfully retrieved customer list", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = CustomerListValue.class))))
public Response doGetCustomerList(@QueryParam("query") String query, @QueryParam("startsWith") String startsWith) {
LOG.debug("Query customers for query " + query + " startsWith: " + startsWith);
var retVal = customerPictureService.getAll(query, startsWith);
var retVal = customerPictureService.getAll(query, startsWith);
return Response.ok().entity(retVal).build();
}
@GZIP
@GET
@Path("{id}")
@@ -62,8 +66,29 @@ public class CustomerResource {
public Response doGetDetailCustomer(@PathParam("id") Long id) {
LOG.debug("Get Customer details for id " + id);
String baseUrl = uriInfo.getBaseUri().toString();
var retVal = customerPictureService.get(id, baseUrl);
var retVal = customerPictureService.get(id, baseUrl);
return Response.ok().entity(retVal).build();
}
@GZIP
@GET
@Path("export/{id}")
@Produces("application/pdf")
@Operation(summary = "Get Export")
@ApiResponse(responseCode = "200", description = "Successfully retrieved export")
public Response doExport(@PathParam("id") Long id, @QueryParam("picture") Long pictureId) {
LOG.debug("Create export for customer " + id + " with optional param " + pictureId);
byte[] pdf = customerPictureService.getExport(id, pictureId);
if (pdf == null) {
return Response.status(Status.NOT_FOUND).build();
}
StreamingOutput streamingOutput = (OutputStream output) -> {
LOG.debug("Start writing content to OutputStream available bytes");
output.write(pdf);
};
return Response.status(Status.OK).entity(streamingOutput).build();
}
}

View File

@@ -1,7 +1,5 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
import java.io.OutputStream;
import org.apache.commons.logging.Log;
@@ -13,7 +11,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.CacheControl;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;

View File

@@ -0,0 +1,293 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.imageio.ImageIO;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 2 Feb 2026
*/
class PdfUtilsTest {
private static final Log LOG = LogFactory.getLog(PdfUtilsTest.class);
private PdfUtils pdfUtils;
private Customer customer;
@BeforeEach
void setUp() {
pdfUtils = new PdfUtils();
customer = new Customer.Builder()
.name("Apotheke Musterstadt")
.customerNumber("KD-12345")
.zip("50667")
.city("Köln")
.build();
}
@Test
void createPdf_singlePicture_returnsValidPdf() throws IOException {
Picture picture = createPicture(new Date(), "Schaufenster Dekoration", 1);
List<Picture> pictures = List.of(picture);
byte[] pdfBytes = pdfUtils.createPdf(customer, pictures);
assertNotNull(pdfBytes);
assertTrue(pdfBytes.length > 0);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
writeToFile(pdfBytes, "createPdf_singlePicture_returnsValidPdf.pdf");
}
@Test
void createPdf_multiplePictures_createsOnPagePerPicture() throws IOException {
List<Picture> pictures = List.of(
createPicture(new Date(), "Bild 1", 1),
createPicture(new Date(), longComment, 2),
createPicture(new Date(), "Bild 3", 3));
byte[] pdfBytes = pdfUtils.createPdf(customer, pictures);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(3, document.getNumberOfPages());
}
writeToFile(pdfBytes, "createPdf_multiplePictures_createsOnPagePerPicture.pdf");
}
@Test
void createPdf_containsCustomerName() throws IOException {
Picture picture = createPicture(new Date(), "Test", 1);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("Apotheke Musterstadt"));
}
@Test
void createPdf_containsCustomerNumber() throws IOException {
Picture picture = createPicture(new Date(), "Test", 1);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("KD-12345"));
}
@Test
void createPdf_containsLabels() throws IOException {
Picture picture = createPicture(new Date(), "Test Kommentar", 2);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("KUNDENNUMMER"));
assertTrue(text.contains("PLZ"));
assertTrue(text.contains("ORT"));
assertTrue(text.contains("KOMMENTAR"));
}
@Test
void createPdf_containsZipAndCity() throws IOException {
Picture picture = createPicture(new Date(), "Test", 1);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("50667"));
assertTrue(text.contains("Köln"));
}
@Test
void createPdf_containsComment() throws IOException {
Picture picture = createPicture(new Date(), "Wichtiger Kommentar zum Bild", 1);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("Wichtiger Kommentar zum Bild"));
}
@Test
void createPdf_emptyPictureList_returnsValidEmptyPdf() throws IOException {
byte[] pdfBytes = pdfUtils.createPdf(customer, Collections.emptyList());
assertNotNull(pdfBytes);
assertTrue(pdfBytes.length > 0);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(0, document.getNumberOfPages());
}
}
@Test
void createPdf_nullImage_doesNotThrow() throws IOException {
Picture picture = new Picture.Builder()
.pictureDate(new Date())
.comment("Ohne Bild")
.customer(customer)
.build();
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
@Test
void createPdf_nullComment_doesNotThrow() throws IOException {
Picture picture = new Picture.Builder()
.pictureDate(new Date())
.image(createTestImageBase64())
.customer(customer)
.build();
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
@Test
void createPdf_nullDate_doesNotThrow() throws IOException {
Picture picture = new Picture.Builder()
.comment("Kein Datum")
.image(createTestImageBase64())
.customer(customer)
.build();
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
@Test
void createPdf_nullEvaluation_doesNotThrow() throws IOException {
Picture picture = new Picture.Builder()
.pictureDate(new Date())
.comment("Test")
.image(createTestImageBase64())
.customer(customer)
.build();
// evaluation defaults to 0 in Builder
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
assertTrue(pdfBytes.length > 0);
}
@Test
void createPdf_allEvaluationValues_produceValidPdf() throws IOException {
for (int eval = 1; eval <= 3; eval++) {
Picture picture = createPicture(new Date(), "Eval " + eval, eval);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
}
@Test
void createPdf_nullFieldsOnCustomer_doesNotThrow() throws IOException {
Customer emptyCustomer = new Customer.Builder()
.name("Test")
.customerNumber("000")
.build();
Picture picture = createPicture(new Date(), "Test", 1);
byte[] pdfBytes = pdfUtils.createPdf(emptyCustomer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
private Picture createPicture(Date date, String comment, int evaluation) {
return new Picture.Builder()
.pictureDate(date)
.comment(comment)
.evaluation(evaluation)
.image(createTestImageBase64())
.customer(customer)
.build();
}
private String createTestImageBase64() {
try {
BufferedImage image = new BufferedImage(100, 80, BufferedImage.TYPE_INT_RGB);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String extractText(byte[] pdfBytes) throws IOException {
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
PDFTextStripper stripper = new PDFTextStripper();
return stripper.getText(document);
}
}
public void writeToFile(final byte[] content, final String fileName) {
File file = new File("target/test/output/");
file.mkdirs();
try (FileOutputStream out = new FileOutputStream(new File(file, fileName))) {
IOUtils.write(content, out);
} catch (Exception e) {
LOG.error("Error saveing pdf file", e);
}
}
String longComment = "This is a sample text used for unit testing purposes. It contains multiple sentences with different structures, punctuation marks, and line breaks. The goal is to simulate realistic content that a program might process during normal execution. Developers often need such text to verify that parsing, searching, filtering, or transformation logic behaves as expected.\n"
+ "\n"
+ "The text includes numbers like 12345, special characters such as @, #, and %, and mixed casing to ensure case-insensitive comparisons can be tested properly. It also contains repeated keywords like skillmatrix and SkillMatrix to validate string matching and normalization features.\n"
+ "\n"
+ "Additionally, this paragraph spans several lines to test newline handling and formatting behavior. Unit tests may check whether the system correctly reads files, counts words, trims whitespace, or handles empty lines without errors.\n"
+ "\n"
+ "Overall, this content is intentionally generic but sufficiently detailed to serve as stable input data for automated tests.";
}