Skip to content

Commit

Permalink
resize uploaded images (#1209)
Browse files Browse the repository at this point in the history
  • Loading branch information
cbellone committed Apr 7, 2023
1 parent e0ade80 commit daea776
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@
@Log4j2
public class FileUploadApiController {

private static final int IMAGE_THUMB_MAX_WIDTH_PX = 500;
private static final int IMAGE_THUMB_MAX_HEIGHT_PX = 500;

private final FileUploadManager fileUploadManager;

@Autowired
Expand All @@ -50,36 +47,12 @@ public FileUploadApiController(FileUploadManager fileUploadManager) {
}

@PostMapping("/file/upload")
public ResponseEntity<String> uploadFile(@RequestParam(required = false, value = "resizeImage", defaultValue = "false") Boolean resizeImage,
@RequestBody UploadBase64FileModification upload) {

public ResponseEntity<String> uploadFile(@RequestBody UploadBase64FileModification upload) {
try {
final var mimeType = MimeTypeUtils.parseMimeType(upload.getType());
if (Boolean.TRUE.equals(resizeImage)) {
upload = resize(upload, mimeType);
}
return ResponseEntity.ok(fileUploadManager.insertFile(upload));
} catch (Exception e) {
log.error("error while uploading image", e);
return ResponseEntity.badRequest().build();
}
}

private UploadBase64FileModification resize(UploadBase64FileModification upload, MimeType mimeType) throws IOException {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(upload.getFile()));
//resize only if the image is bigger than 500px on one of the side
if (image.getWidth() > IMAGE_THUMB_MAX_WIDTH_PX || image.getHeight() > IMAGE_THUMB_MAX_HEIGHT_PX) {
UploadBase64FileModification resized = new UploadBase64FileModification();
BufferedImage thumbImg = Scalr.resize(image, Scalr.Method.QUALITY, Scalr.Mode.AUTOMATIC, IMAGE_THUMB_MAX_WIDTH_PX, IMAGE_THUMB_MAX_HEIGHT_PX, Scalr.OP_ANTIALIAS);
try (final var baos = new ByteArrayOutputStream()) {
ImageIO.write(thumbImg, mimeType.getSubtype(), baos);
resized.setFile(baos.toByteArray());
}
resized.setAttributes(upload.getAttributes());
resized.setName(upload.getName());
resized.setType(upload.getType());
return resized;
}
return upload;
}
}
2 changes: 1 addition & 1 deletion src/main/java/alfio/manager/FileDownloadManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public DownloadedFile downloadFile(String url) {
if(callSuccessful(response)) {
String[] parts = Pattern.compile("/").split(url);
String name = parts[parts.length - 1];
if(Objects.nonNull(response.body()) && response.body().length <= FileUploadManager.MAXIMUM_ALLOWED_SIZE) {
if(Objects.nonNull(response.body())) {
return new DownloadedFile(
response.body(),
name,
Expand Down
68 changes: 57 additions & 11 deletions src/main/java/alfio/manager/FileUploadManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.RemovalListener;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.imgscalr.Scalr;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.time.Duration;
import java.util.*;

Expand All @@ -42,22 +47,33 @@
@RequiredArgsConstructor
public class FileUploadManager {

static final int IMAGE_THUMB_MAX_WIDTH_PX = 300;
static final int IMAGE_THUMB_MAX_HEIGHT_PX = 200;
/**
* Maximum allowed file size is 200kb
*/
static final int MAXIMUM_ALLOWED_SIZE = 1024 * 200;
private static final int MAXIMUM_ALLOWED_SIZE = 1024 * 200;
private static final MimeType IMAGE_TYPE = MimeType.valueOf("image/*");
private final FileUploadRepository repository;
private final Cache<String, File> cache = Caffeine.newBuilder()
.maximumSize(20)
.expireAfterWrite(Duration.ofMinutes(20))
.removalListener((String key, File value, RemovalCause cause) -> {
if(value != null) {
boolean result = value.delete();
log.trace("deleted {}: {}", key, result);
}
})
.removalListener(removalListener())
.build();

private static RemovalListener<String, File> removalListener() {
return (String key, File value, RemovalCause cause) -> {
if (value != null) {
try {
Files.delete(value.toPath());
log.trace("deleted {}", key);
} catch(Exception ex) {
log.trace("Error while deleting file", ex);
}
}
};
}

public Optional<FileBlobMetadata> findMetadata(String id) {
return repository.findById(id);
}
Expand All @@ -79,12 +95,13 @@ public void outputFile(String id, OutputStream out) {
}
}


public String insertFile(UploadBase64FileModification file) {
Validate.exclusiveBetween(1, MAXIMUM_ALLOWED_SIZE, file.getFile().length);
String digest = DigestUtils.sha256Hex(file.getFile());
final var mimeType = MimeTypeUtils.parseMimeType(file.getType());
var upload = resizeIfNeeded(file, mimeType);
Validate.exclusiveBetween(1, MAXIMUM_ALLOWED_SIZE, upload.getFile().length);
String digest = DigestUtils.sha256Hex(upload.getFile());
if (Integer.valueOf(0).equals(repository.isPresent(digest))) {
repository.upload(file, digest, getAttributes(file));
repository.upload(upload, digest, getAttributes(upload));
}
return digest;
}
Expand All @@ -94,6 +111,35 @@ public void cleanupUnreferencedBlobFiles(Date date) {
log.debug("removed {} unused file_blob", deleted);
}

/**
* @author <a href="https://github.com/emassip">Etienne M.</a>
*/
private UploadBase64FileModification resizeIfNeeded(UploadBase64FileModification upload, MimeType mimeType) {
if (!mimeType.isCompatibleWith(IMAGE_TYPE)) {
// not an image, nothing to do here.
return upload;
}
try {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(upload.getFile()));
// resize only if the image is bigger than target size on either side
if (image.getWidth() > IMAGE_THUMB_MAX_WIDTH_PX || image.getHeight() > IMAGE_THUMB_MAX_HEIGHT_PX) {
UploadBase64FileModification resized = new UploadBase64FileModification();
BufferedImage thumbImg = Scalr.resize(image, Scalr.Method.QUALITY, Scalr.Mode.AUTOMATIC, IMAGE_THUMB_MAX_WIDTH_PX, IMAGE_THUMB_MAX_HEIGHT_PX, Scalr.OP_ANTIALIAS);
try (final var baos = new ByteArrayOutputStream()) {
ImageIO.write(thumbImg, mimeType.getSubtype(), baos);
resized.setFile(baos.toByteArray());
}
resized.setAttributes(upload.getAttributes());
resized.setName(upload.getName());
resized.setType(upload.getType());
return resized;
}
return upload;
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
}

private Map<String, String> getAttributes(UploadBase64FileModification file) {
if(!StringUtils.startsWith(file.getType(), "image/")) {
return Collections.emptyMap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ <h3>Logo</h3>
<div class="form-group">
<label for="imageFile">Image</label>
<div id="imageFile" class="drop-file-zone wMarginBottom well" data-accept="image/*" data-ngf-pattern="'image/*'" data-ng-model="droppedFile" data-ngf-drop data-ngf-select data-ngf-multiple="false" data-ngf-allow-dir="false" data-ngf-drag-over-class="'drop-file-zone-hover'">
Drop image here or click to upload (Maximum size : 200KB)
Drop image here or click to upload (Maximum size : 1MB)
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ <h3>Image</h3>
data-accept="image/*" data-ngf-pattern="'image/*'"
data-ng-model="$ctrl.droppedFile"
data-ngf-drop data-ngf-select data-ngf-multiple="false" data-ngf-allow-dir="false" data-ngf-drag-over-class="'drop-file-zone-hover'">
Drop image here or click to upload (Maximum size : 200KB)
Drop image here or click to upload (Maximum size : 1MB)
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/main/webapp/resources/js/admin/service/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -997,8 +997,8 @@
deferred.reject('Your image was not uploaded correctly.Please upload the image again');
} else if (!((files[0].type === 'image/png') || (files[0].type === 'image/jpeg') || (files[0].type === 'image/gif') || (files[0].type === 'image/svg+xml'))) {
deferred.reject('Only PNG, JPG, GIF or SVG image files are accepted');
} else if (files[0].size > (1024 * 200)) {
deferred.reject('Image size exceeds the allowable limit 200KB');
} else if (files[0].size > (1024 * 1024)) {
deferred.reject('Image is too big');
} else {
reader.readAsDataURL(files[0]);
}
Expand Down
33 changes: 28 additions & 5 deletions src/test/java/alfio/manager/FileUploadManagerIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@
import org.apache.commons.lang3.time.DateUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
Expand All @@ -50,7 +51,7 @@ class FileUploadManagerIntegrationTest extends BaseIntegrationTest {
private static final byte[] FILE = {1,2,3,4};

@Test
public void testInsert() {
void testInsert() {
UploadBase64FileModification toInsert = new UploadBase64FileModification();
toInsert.setFile(FILE);
toInsert.setName("myfile.txt");
Expand All @@ -76,7 +77,7 @@ public void testInsert() {


@Test
public void testInsertImage() {
void testInsertImage() {
UploadBase64FileModification toInsert = new UploadBase64FileModification();
toInsert.setFile(ONE_PIXEL_BLACK_GIF);
toInsert.setName("image.gif");
Expand All @@ -95,7 +96,29 @@ public void testInsertImage() {
}

@Test
public void testFindMetadataNotPresent() {
void testFindMetadataNotPresent() {
assertFalse(fileUploadManager.findMetadata("unknownid").isPresent());
}

@Test
void testInsertResizedImage() throws IOException {
// Image credit: NASA, ESA, CSA, and STScI
try (var in = getClass().getResourceAsStream("/images/main_image_star-forming_region_carina_reduced.jpg")) {
UploadBase64FileModification toInsert = new UploadBase64FileModification();
toInsert.setFile(Objects.requireNonNull(in).readAllBytes());
toInsert.setName("image.jpg");
toInsert.setType("image/jpeg");
String id = fileUploadManager.insertFile(toInsert);

Optional<FileBlobMetadata> metadata = fileUploadManager.findMetadata(id);

assertTrue(metadata.isPresent());

assertEquals(String.valueOf(FileUploadManager.IMAGE_THUMB_MAX_WIDTH_PX), metadata.get().getAttributes().get("width"));
assertEquals("174", metadata.get().getAttributes().get("height"));

fileUploadManager.cleanupUnreferencedBlobFiles(DateUtils.addDays(new Date(), 1));
assertFalse(fileUploadManager.findMetadata(id).isPresent());
}
}
}
1 change: 1 addition & 0 deletions src/test/resources/images/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Image credit: NASA, ESA, CSA, and STScI
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit daea776

Please sign in to comment.