Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ public ResponseEntity<ApiErrorResponseDTO> handleRentalAlreadyReturnedException(
return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT);
}

@ExceptionHandler(ImageProcessingException.class)
public ResponseEntity<ApiErrorResponseDTO> handleImageProcessingException(ImageProcessingException ex) {
ApiErrorResponseDTO errorResponse = new ApiErrorResponseDTO(422, ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.UNPROCESSABLE_ENTITY);
}


@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<ApiErrorResponseDTO> handleIllegalStateException(IllegalStateException ex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package edu.zut.bookrider.exception;

public class ImageProcessingException extends RuntimeException {
public ImageProcessingException(String message) {
super(message);
}
}
16 changes: 2 additions & 14 deletions backend/src/main/java/edu/zut/bookrider/service/BookService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -135,13 +134,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());
Expand Down Expand Up @@ -203,13 +196,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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -95,15 +94,11 @@ public CreateDriverApplicationResponseDTO createDriverApplication(
List<CreateDriverDocumentResponseDTO> 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<User> administrators = userService.getAllAdministrators();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@RequiredArgsConstructor
@Service
public class DriverDocumentService {
Expand All @@ -23,7 +21,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<String, Object> body = new LinkedMultiValueMap<>();
String base64Image = Base64.getEncoder().encodeToString(resizedBytes);

// key is public, no need to hide it
String apiKey = "6d207e02198a847aa98d0a2a901485a5";
MultiValueMap<String, Object> 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<String> responseEntity = restTemplate.postForEntity(
apiUrl,
new HttpEntity<>(body),
String.class
);
String apiUrl = "https://freeimage.host/api/1/upload";

ResponseEntity<String> 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<ImageWriter> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -402,13 +401,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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -89,7 +88,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);
Expand Down Expand Up @@ -124,21 +123,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()))
Expand Down
Loading