From 8862ff9735aad437be16e3f83649bff1248775ec Mon Sep 17 00:00:00 2001 From: PiCiU1221 <96005164+PiCiU1221@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:53:18 +0100 Subject: [PATCH 1/4] Add new exception --- .../zut/bookrider/exception/GlobalExceptionHandler.java | 6 ++++++ .../zut/bookrider/exception/ImageProcessingException.java | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 backend/src/main/java/edu/zut/bookrider/exception/ImageProcessingException.java diff --git a/backend/src/main/java/edu/zut/bookrider/exception/GlobalExceptionHandler.java b/backend/src/main/java/edu/zut/bookrider/exception/GlobalExceptionHandler.java index 9c5ba9f6..964ccb29 100644 --- a/backend/src/main/java/edu/zut/bookrider/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/edu/zut/bookrider/exception/GlobalExceptionHandler.java @@ -237,6 +237,12 @@ public ResponseEntity handleRentalAlreadyReturnedException( return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); } + @ExceptionHandler(ImageProcessingException.class) + public ResponseEntity handleImageProcessingException(ImageProcessingException ex) { + ApiErrorResponseDTO errorResponse = new ApiErrorResponseDTO(422, ex.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.UNPROCESSABLE_ENTITY); + } + @ExceptionHandler(IllegalStateException.class) public ResponseEntity handleIllegalStateException(IllegalStateException ex) { diff --git a/backend/src/main/java/edu/zut/bookrider/exception/ImageProcessingException.java b/backend/src/main/java/edu/zut/bookrider/exception/ImageProcessingException.java new file mode 100644 index 00000000..e2196224 --- /dev/null +++ b/backend/src/main/java/edu/zut/bookrider/exception/ImageProcessingException.java @@ -0,0 +1,7 @@ +package edu.zut.bookrider.exception; + +public class ImageProcessingException extends RuntimeException { + public ImageProcessingException(String message) { + super(message); + } +} From ed0fe6670bc2053b2c05e03745655de2383ffa6e Mon Sep 17 00:00:00 2001 From: PiCiU1221 <96005164+PiCiU1221@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:53:48 +0100 Subject: [PATCH 2/4] Fix incorrect compression --- .../bookrider/service/ImageUploadService.java | 154 ++++++++++-------- 1 file changed, 88 insertions(+), 66 deletions(-) diff --git a/backend/src/main/java/edu/zut/bookrider/service/ImageUploadService.java b/backend/src/main/java/edu/zut/bookrider/service/ImageUploadService.java index 00f9e714..be3fde76 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/ImageUploadService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/ImageUploadService.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import edu.zut.bookrider.exception.ImageProcessingException; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; @@ -16,6 +17,7 @@ import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -28,101 +30,121 @@ public class ImageUploadService { private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; - public String uploadImage(MultipartFile imageFile) throws IOException { - // Compress the image - byte[] compressedImageBytes = compressImage(imageFile.getBytes()); + private static final int MAX_DIMENSION = 1600; + private static final float JPG_QUALITY = 0.8f; - // Encode the compressed image bytes to base64 - String base64Image = Base64.getEncoder().encodeToString(compressedImageBytes); + public String uploadImage(MultipartFile imageFile) { + try { + byte[] resizedBytes = resizeAndCompress(imageFile.getBytes()); - // Prepare the request body - MultiValueMap body = new LinkedMultiValueMap<>(); + String base64Image = Base64.getEncoder().encodeToString(resizedBytes); - // key is public, no need to hide it - String apiKey = "6d207e02198a847aa98d0a2a901485a5"; + MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("key", apiKey); - body.add("action", "upload"); - body.add("source", base64Image); - body.add("format", "json"); + // key is public, no need to hide it + String apiKey = "6d207e02198a847aa98d0a2a901485a5"; - String apiUrl = "https://freeimage.host/api/1/upload"; + body.add("key", apiKey); + body.add("action", "upload"); + body.add("source", base64Image); + body.add("format", "json"); - ResponseEntity responseEntity = restTemplate.postForEntity( - apiUrl, - new HttpEntity<>(body), - String.class - ); + String apiUrl = "https://freeimage.host/api/1/upload"; + + ResponseEntity responseEntity = restTemplate.postForEntity( + apiUrl, + new HttpEntity<>(body), + String.class + ); + + if (!responseEntity.getStatusCode().is2xxSuccessful()) { + throw new RuntimeException("Failed to upload image: " + responseEntity.getStatusCode()); + } - if (responseEntity.getStatusCode().is2xxSuccessful()) { return extractImageUrlFromResponse(responseEntity.getBody()); - } else { - throw new RuntimeException("Failed to upload image: " + responseEntity.getStatusCode()); + } catch (IOException e) { + throw new ImageProcessingException("Failed to process uploaded image: " + e.getMessage()); } } private String extractImageUrlFromResponse(String responseBody) throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(responseBody); - return jsonNode.path("image").path("url").asText(); } - private byte[] compressImage(byte[] originalImageBytes) throws IOException { - // Convert the original image bytes to BufferedImage - BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(originalImageBytes)); + private BufferedImage createOptimizedImage(BufferedImage src, int targetWidth, int targetHeight) { + BufferedImage img = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g = img.createGraphics(); + + g.setColor(Color.WHITE); + g.fillRect(0, 0, targetWidth, targetHeight); - // Ensure the image is in a compatible RGB color space - BufferedImage rgbImage = new BufferedImage( - originalImage.getWidth(), - originalImage.getHeight(), - BufferedImage.TYPE_INT_RGB - ); - rgbImage.createGraphics().drawImage(originalImage, 0, 0, null); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - // Set the compression quality based on the original image size - float compressionQuality = calculateCompressionQuality(originalImageBytes.length); + g.drawImage(src, 0, 0, targetWidth, targetHeight, null); + g.dispose(); + + return img; + } + + private BufferedImage convertToRGB(BufferedImage img) { + if (img.getType() == BufferedImage.TYPE_INT_RGB) { + return img; + } + return createOptimizedImage(img, img.getWidth(), img.getHeight()); + } - // Create a ByteArrayOutputStream to hold the compressed image bytes - ByteArrayOutputStream compressedImageOutputStream = new ByteArrayOutputStream(); - // Get all available ImageWriters + private BufferedImage resizeImage(BufferedImage img) { + float scale = (float) MAX_DIMENSION / Math.max(img.getWidth(), img.getHeight()); + int newWidth = Math.round(img.getWidth() * scale); + int newHeight = Math.round(img.getHeight() * scale); + + return createOptimizedImage(img, newWidth, newHeight); + } + + private byte[] encodeJpg(BufferedImage img) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Iterator writers = ImageIO.getImageWritersByFormatName("jpg"); - if (writers.hasNext()) { - ImageWriter writer = writers.next(); - ImageWriteParam writeParam = writer.getDefaultWriteParam(); - - // Set compression quality - writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - writeParam.setCompressionQuality(compressionQuality); - - // Write the compressed image bytes to the ByteArrayOutputStream - try (ImageOutputStream ios = ImageIO.createImageOutputStream(compressedImageOutputStream)) { - writer.setOutput(ios); - writer.write(null, new IIOImage(rgbImage, null, null), writeParam); - } finally { - writer.dispose(); - } + if (!writers.hasNext()) { + throw new IllegalStateException("No JPG writers available"); } - return compressedImageOutputStream.toByteArray(); + ImageWriter writer = writers.next(); + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(JPG_QUALITY); + + try (ImageOutputStream ios = ImageIO.createImageOutputStream(out)) { + writer.setOutput(ios); + writer.write(null, new IIOImage(img, null, null), param); + } finally { + writer.dispose(); + } + + return out.toByteArray(); } - private float calculateCompressionQuality(int originalImageSize) { - // Set a threshold for image size above which we apply more compression - int sizeThreshold = 10 * 1024 * 1024; // 10 MB + private byte[] resizeAndCompress(byte[] inputBytes) throws IOException { + BufferedImage original = ImageIO.read(new ByteArrayInputStream(inputBytes)); + if (original == null) { + throw new IOException("Invalid image data"); + } - // Set the initial compression quality - float initialCompressionQuality = 0.8f; + BufferedImage processed; + int maxSide = Math.max(original.getWidth(), original.getHeight()); - // Adjust compression quality based on image size - if (originalImageSize > sizeThreshold) { - // If the original image is larger than 10MB, increase compression to reduce size - return 0.5f; + if (maxSide > MAX_DIMENSION) { + processed = resizeImage(original); } else { - // Otherwise, use the initial compression quality - return initialCompressionQuality; + processed = convertToRGB(original); } + + return encodeJpg(processed); } } From 583e60a90be03fe45b9e4eaaa73f75a6d0475449 Mon Sep 17 00:00:00 2001 From: PiCiU1221 <96005164+PiCiU1221@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:54:38 +0100 Subject: [PATCH 3/4] Remove try catch blocks --- .../edu/zut/bookrider/service/BookService.java | 15 ++------------- .../DriverApplicationRequestService.java | 14 +++++--------- .../service/DriverDocumentService.java | 2 +- .../edu/zut/bookrider/service/OrderService.java | 8 +------- .../DriverApplicationRequestServiceTest.java | 17 +---------------- .../unit/service/DriverDocumentServiceTest.java | 11 +---------- 6 files changed, 11 insertions(+), 56 deletions(-) diff --git a/backend/src/main/java/edu/zut/bookrider/service/BookService.java b/backend/src/main/java/edu/zut/bookrider/service/BookService.java index 6ca60c84..38d5d9e4 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/BookService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/BookService.java @@ -135,13 +135,7 @@ public BookResponseDto addNewBook( byte[] imageBase64 = Base64.getDecoder().decode(bookRequestDto.getImage()); MultipartFile multipartFile = new BASE64DecodedMultipartFile(imageBase64); - String coverImageUrl; - - try { - coverImageUrl = imageUploadService.uploadImage(multipartFile); - } catch (IOException e) { - throw new RuntimeException(e); - } + String coverImageUrl = imageUploadService.uploadImage(multipartFile); Book book = new Book(); book.setTitle(bookRequestDto.getTitle()); @@ -203,13 +197,8 @@ public BookResponseDto updateBook(Integer bookId, @Valid BookRequestDto bookRequ byte[] imageBase64 = Base64.getDecoder().decode(bookRequestDto.getImage()); MultipartFile multipartFile = new BASE64DecodedMultipartFile(imageBase64); - String coverImageUrl; - try { - coverImageUrl = imageUploadService.uploadImage(multipartFile); - } catch (IOException e) { - throw new RuntimeException(e); - } + String coverImageUrl = imageUploadService.uploadImage(multipartFile); book.setTitle(bookRequestDto.getTitle()); book.setReleaseYear(bookRequestDto.getReleaseYear()); diff --git a/backend/src/main/java/edu/zut/bookrider/service/DriverApplicationRequestService.java b/backend/src/main/java/edu/zut/bookrider/service/DriverApplicationRequestService.java index b95de2ab..acecfaa8 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/DriverApplicationRequestService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/DriverApplicationRequestService.java @@ -95,15 +95,11 @@ public CreateDriverApplicationResponseDTO createDriverApplication( List createdDocuments = new ArrayList<>(); for (CreateDriverDocumentDTO documentDTO : documents) { - try { - CreateDriverDocumentResponseDTO createdDocument = driverDocumentService.saveDriverDocument( - documentDTO, - savedRequest - ); - createdDocuments.add(createdDocument); - } catch (IOException e) { - throw new RuntimeException(e); - } + CreateDriverDocumentResponseDTO createdDocument = driverDocumentService.saveDriverDocument( + documentDTO, + savedRequest + ); + createdDocuments.add(createdDocument); } List administrators = userService.getAllAdministrators(); diff --git a/backend/src/main/java/edu/zut/bookrider/service/DriverDocumentService.java b/backend/src/main/java/edu/zut/bookrider/service/DriverDocumentService.java index c8a1166d..e3b8c18c 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/DriverDocumentService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/DriverDocumentService.java @@ -23,7 +23,7 @@ public class DriverDocumentService { public CreateDriverDocumentResponseDTO saveDriverDocument( @Valid CreateDriverDocumentDTO documentDto, DriverApplicationRequest applicationRequest - ) throws IOException { + ) { MultipartFile multipartFile = new BASE64DecodedMultipartFile(documentDto.getImageInBytes()); String documentUrl = imageUploadService.uploadImage(multipartFile); diff --git a/backend/src/main/java/edu/zut/bookrider/service/OrderService.java b/backend/src/main/java/edu/zut/bookrider/service/OrderService.java index 1aa07482..28a0f8fb 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/OrderService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/OrderService.java @@ -402,13 +402,7 @@ public DeliverOrderResponseDTO deliverOrder(Integer orderId, @Valid DeliverOrder byte[] imageBase64 = Base64.getDecoder().decode(requestDTO.getPhotoBase64()); MultipartFile multipartFile = new BASE64DecodedMultipartFile(imageBase64); - String deliveryPhotoUrl; - - try { - deliveryPhotoUrl = imageUploadService.uploadImage(multipartFile); - } catch (IOException e) { - throw new RuntimeException(e); - } + String deliveryPhotoUrl = imageUploadService.uploadImage(multipartFile); order.setDeliveryPhotoUrl(deliveryPhotoUrl); order.setDeliveredAt(LocalDateTime.now()); diff --git a/backend/src/test/java/edu/zut/bookrider/unit/service/DriverApplicationRequestServiceTest.java b/backend/src/test/java/edu/zut/bookrider/unit/service/DriverApplicationRequestServiceTest.java index 0c5a11ed..ce0e1bb5 100644 --- a/backend/src/test/java/edu/zut/bookrider/unit/service/DriverApplicationRequestServiceTest.java +++ b/backend/src/test/java/edu/zut/bookrider/unit/service/DriverApplicationRequestServiceTest.java @@ -89,7 +89,7 @@ void setUp() { } @Test - void whenValidInputData_thenReturnCreatedDTO() throws IOException { + void whenValidInputData_thenReturnCreatedDTO() { when(authentication.getName()).thenReturn("driver@email.com:driver"); when(userRepository.findByEmailAndRoleName("driver@email.com", "driver")).thenReturn(java.util.Optional.of(driver)); when(driverApplicationRequestRepository.existsByUserIdAndPendingOrUnderReview(driver.getId())).thenReturn(false); @@ -124,21 +124,6 @@ void whenUserAlreadyHasPendingOrUnderReviewRequest_thenThrowException() { }); } - @Test - void whenImageUploadFails_thenThrowException() throws IOException { - when(authentication.getName()).thenReturn("driver@email.com:driver"); - when(userRepository.findByEmailAndRoleName("driver@email.com", "driver")).thenReturn(java.util.Optional.of(driver)); - when(driverApplicationRequestRepository.existsByUserIdAndPendingOrUnderReview(driver.getId())).thenReturn(false); - - when(driverApplicationRequestRepository.save(any(DriverApplicationRequest.class))).thenReturn(applicationRequest); - - when(driverDocumentService.saveDriverDocument(documentDto, applicationRequest)).thenThrow(new IOException("Upload failed")); - - assertThrows(RuntimeException.class, () -> { - driverApplicationRequestService.createDriverApplication(authentication, List.of(documentDto)); - }); - } - @Test void whenGetApplicationDetailsForAdmin_thenReturnDetails() { when(driverApplicationRequestRepository.findById(applicationRequest.getId())) diff --git a/backend/src/test/java/edu/zut/bookrider/unit/service/DriverDocumentServiceTest.java b/backend/src/test/java/edu/zut/bookrider/unit/service/DriverDocumentServiceTest.java index e162e854..f4c871a0 100644 --- a/backend/src/test/java/edu/zut/bookrider/unit/service/DriverDocumentServiceTest.java +++ b/backend/src/test/java/edu/zut/bookrider/unit/service/DriverDocumentServiceTest.java @@ -51,7 +51,7 @@ void setUp() { } @Test - void whenValidInputData_thenReturnCreatedDTO() throws IOException { + void whenValidInputData_thenReturnCreatedDTO() { String documentUrl = "http://example.com/driver-license.jpg"; MultipartFile multipartFile = new MockMultipartFile("driver_license.jpg", documentDto.getImageInBytes()); when(imageUploadService.uploadImage(multipartFile)).thenReturn(documentUrl); @@ -70,13 +70,4 @@ void whenValidInputData_thenReturnCreatedDTO() throws IOException { assertEquals(documentUrl, response.getDocumentPhotoUrl()); assertEquals(LocalDate.now().plusYears(5), response.getExpiryDate()); } - - @Test - void whenInvalidImageInRequest_thenThrowException() throws IOException { - when(imageUploadService.uploadImage(any())).thenThrow(new IOException()); - - assertThrows(IOException.class, () -> { - driverDocumentService.saveDriverDocument(documentDto, applicationRequest); - }); - } } From d2363755c0871220a268cb82e865748990e09e47 Mon Sep 17 00:00:00 2001 From: PiCiU1221 <96005164+PiCiU1221@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:55:50 +0100 Subject: [PATCH 4/4] Remove unnecessary imports --- .../src/main/java/edu/zut/bookrider/service/BookService.java | 1 - .../zut/bookrider/service/DriverApplicationRequestService.java | 1 - .../java/edu/zut/bookrider/service/DriverDocumentService.java | 2 -- .../src/main/java/edu/zut/bookrider/service/OrderService.java | 1 - .../unit/service/DriverApplicationRequestServiceTest.java | 1 - .../zut/bookrider/unit/service/DriverDocumentServiceTest.java | 2 -- 6 files changed, 8 deletions(-) diff --git a/backend/src/main/java/edu/zut/bookrider/service/BookService.java b/backend/src/main/java/edu/zut/bookrider/service/BookService.java index 38d5d9e4..bcefeabe 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/BookService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/BookService.java @@ -20,7 +20,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; import java.util.ArrayList; import java.util.Base64; import java.util.List; diff --git a/backend/src/main/java/edu/zut/bookrider/service/DriverApplicationRequestService.java b/backend/src/main/java/edu/zut/bookrider/service/DriverApplicationRequestService.java index acecfaa8..f6e0bfe1 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/DriverApplicationRequestService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/DriverApplicationRequestService.java @@ -23,7 +23,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; diff --git a/backend/src/main/java/edu/zut/bookrider/service/DriverDocumentService.java b/backend/src/main/java/edu/zut/bookrider/service/DriverDocumentService.java index e3b8c18c..3f6bd02e 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/DriverDocumentService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/DriverDocumentService.java @@ -11,8 +11,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - @RequiredArgsConstructor @Service public class DriverDocumentService { diff --git a/backend/src/main/java/edu/zut/bookrider/service/OrderService.java b/backend/src/main/java/edu/zut/bookrider/service/OrderService.java index 28a0f8fb..fbcc797e 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/OrderService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/OrderService.java @@ -23,7 +23,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Base64; diff --git a/backend/src/test/java/edu/zut/bookrider/unit/service/DriverApplicationRequestServiceTest.java b/backend/src/test/java/edu/zut/bookrider/unit/service/DriverApplicationRequestServiceTest.java index ce0e1bb5..cc7b00cb 100644 --- a/backend/src/test/java/edu/zut/bookrider/unit/service/DriverApplicationRequestServiceTest.java +++ b/backend/src/test/java/edu/zut/bookrider/unit/service/DriverApplicationRequestServiceTest.java @@ -19,7 +19,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.core.Authentication; -import java.io.IOException; import java.time.LocalDate; import java.util.List; import java.util.Optional; diff --git a/backend/src/test/java/edu/zut/bookrider/unit/service/DriverDocumentServiceTest.java b/backend/src/test/java/edu/zut/bookrider/unit/service/DriverDocumentServiceTest.java index f4c871a0..1559aadb 100644 --- a/backend/src/test/java/edu/zut/bookrider/unit/service/DriverDocumentServiceTest.java +++ b/backend/src/test/java/edu/zut/bookrider/unit/service/DriverDocumentServiceTest.java @@ -16,11 +16,9 @@ import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when;