Skip to content

Commit

Permalink
Auto-resize images
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurentTreguier committed Oct 8, 2024
1 parent 0653445 commit 8d14efc
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 81 deletions.
10 changes: 5 additions & 5 deletions src/main/java/app/fyreplace/api/data/StoredFile.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package app.fyreplace.api.data;

import app.fyreplace.api.services.MimeTypeService;
import app.fyreplace.api.services.ImageService;
import app.fyreplace.api.services.StorageService;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
Expand All @@ -27,7 +27,7 @@ public class StoredFile extends EntityBase {
private StorageService storageService;

@Transient
private MimeTypeService mimeTypeService;
private ImageService imageService;

@Column(unique = true, nullable = false)
@Schema(required = true)
Expand All @@ -46,7 +46,7 @@ public StoredFile() {
public StoredFile(final String directory, @Nullable final byte[] data) {
initServices();
this.path = UriBuilder.fromPath(directory)
.path(UUID.randomUUID() + "." + mimeTypeService.getExtension(data))
.path(UUID.randomUUID() + "." + imageService.getExtension(data))
.build()
.getPath();
this.data = data;
Expand Down Expand Up @@ -79,9 +79,9 @@ final void postRemove() throws IOException {

private void initServices() {
try (final var storage = Arc.container().instance(StorageService.class);
final var mimeType = Arc.container().instance(MimeTypeService.class)) {
final var mimeType = Arc.container().instance(ImageService.class)) {
storageService = storage.get();
mimeTypeService = mimeType.get();
imageService = mimeType.get();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import app.fyreplace.api.data.User;
import app.fyreplace.api.exceptions.ExplainedFailure;
import app.fyreplace.api.exceptions.ForbiddenException;
import app.fyreplace.api.services.MimeTypeService;
import app.fyreplace.api.services.ImageService;
import io.quarkus.cache.CacheResult;
import io.quarkus.panache.common.Sort;
import io.quarkus.security.Authenticated;
Expand Down Expand Up @@ -44,7 +44,7 @@ public final class ChaptersEndpoint {
int postsMaxChapterCount;

@Inject
MimeTypeService mimeTypeService;
ImageService imageService;

@Context
SecurityContext context;
Expand Down Expand Up @@ -175,7 +175,7 @@ public String setChapterImage(
@PathParam("position") @PositiveOrZero final int position,
final byte[] input)
throws IOException {
mimeTypeService.validate(input);
imageService.validate(input);
final var user = User.getFromSecurityContext(context);
final var post = Post.<Post>findById(id);
Post.validateAccess(post, user, false, true);
Expand All @@ -186,7 +186,7 @@ public String setChapterImage(
final var oldImage = chapter.image;
chapter.width = image.getWidth();
chapter.height = image.getHeight();
chapter.image = new StoredFile("chapters", input);
chapter.image = new StoredFile("chapters", imageService.shrink(input));
chapter.image.persist();
chapter.persist();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package app.fyreplace.api.endpoints;

import app.fyreplace.api.services.MimeTypeService;
import app.fyreplace.api.services.ImageService;
import app.fyreplace.api.services.StorageService;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
Expand All @@ -22,7 +22,7 @@ public final class StoredFilesEndpoint {
StorageService storageService;

@Inject
MimeTypeService mimeTypeService;
ImageService imageService;

@Context
UriInfo uriInfo;
Expand All @@ -40,7 +40,7 @@ public Response getStoredFile(@PathParam("path") final String path) throws URISy
try {
final byte[] data;
data = storageService.fetch(path);
return Response.ok(data).type(mimeTypeService.getMimeType(data)).build();
return Response.ok(data).type(imageService.getMimeType(data)).build();
} catch (final IOException e) {
throw new NotFoundException();
}
Expand Down
11 changes: 6 additions & 5 deletions src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import app.fyreplace.api.exceptions.ExplainedFailure;
import app.fyreplace.api.exceptions.ForbiddenException;
import app.fyreplace.api.exceptions.GoneException;
import app.fyreplace.api.services.MimeTypeService;
import app.fyreplace.api.services.ImageService;
import io.quarkus.cache.CacheResult;
import io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport;
import io.quarkus.panache.common.Sort;
Expand All @@ -39,6 +39,7 @@
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.SecurityContext;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
Expand All @@ -55,7 +56,7 @@ public final class UsersEndpoint {
int pagingSize;

@Inject
MimeTypeService mimeTypeService;
ImageService imageService;

@Inject
UserActivationEmail userActivationEmail;
Expand Down Expand Up @@ -243,11 +244,11 @@ public String setCurrentUserBio(@NotNull @Length(max = User.BIO_MAX_LENGTH) fina
@APIResponse(responseCode = "200", description = "OK")
@APIResponse(responseCode = "413", description = "Payload Too Large")
@APIResponse(responseCode = "415", description = "Unsupported Media Type")
public String setCurrentUserAvatar(final byte[] input) {
mimeTypeService.validate(input);
public String setCurrentUserAvatar(final byte[] input) throws IOException {
imageService.validate(input);
final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE);
final var oldAvatar = user.avatar;
user.avatar = new StoredFile("avatars", input);
user.avatar = new StoredFile("avatars", imageService.shrink(input));
user.avatar.persist();
user.persist();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.fyreplace.api.exceptions;

import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.core.Response;

public class RequestEntityTooLargeException extends ClientErrorException {
public RequestEntityTooLargeException() {
super(Response.Status.REQUEST_ENTITY_TOO_LARGE);
}
}
105 changes: 105 additions & 0 deletions src/main/java/app/fyreplace/api/services/ImageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package app.fyreplace.api.services;

import app.fyreplace.api.exceptions.RequestEntityTooLargeException;
import app.fyreplace.api.exceptions.UnsupportedMediaTypeException;
import app.fyreplace.api.services.mimetype.KnownFileType;
import io.quarkus.runtime.configuration.MemorySize;
import jakarta.enterprise.context.ApplicationScoped;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.NoSuchElementException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import org.eclipse.microprofile.config.inject.ConfigProperty;

@ApplicationScoped
public final class ImageService {
@ConfigProperty(name = "app.storage.limits.max-size")
MemorySize fileMaxSize;

public String getMimeType(final byte[] data) throws IOException {
final var reader = getFirstValidReader(data);
final var format = reader.getFormatName().toUpperCase();

try {
return Arrays.stream(KnownFileType.values())
.filter(m -> m.name().equals(format))
.findFirst()
.orElseThrow()
.mime;
} catch (final NoSuchElementException ignored) {
throw new IOException();
}
}

public String getExtension(final byte[] data) {
try {
final var mime = getMimeType(data);
return Arrays.stream(KnownFileType.values())
.filter(m -> m.mime.equals(mime))
.findFirst()
.orElseThrow()
.name()
.toLowerCase();
} catch (final IOException | NoSuchElementException e) {
return "unknown";
}
}

public void validate(final byte[] data) throws UnsupportedMediaTypeException {
try {
final var mimeType = getMimeType(data);

if (Arrays.stream(KnownFileType.values()).noneMatch(m -> m.mime.equals(mimeType))) {
throw new UnsupportedMediaTypeException();
}
} catch (final IOException e) {
throw new UnsupportedMediaTypeException();
}
}

public byte[] shrink(final byte[] data) throws IOException {
final var softMaxSize = fileMaxSize.asLongValue();

if (data.length <= softMaxSize) {
return data;
}

final var scaleFactor = Math.sqrt((double) softMaxSize / data.length);
final var reader = getFirstValidReader(data);
final var inputImage = ImageIO.read(new ByteArrayInputStream(data));
final var width = inputImage.getWidth() * scaleFactor;
final var height = inputImage.getHeight() * scaleFactor;
final var scaledImage = inputImage.getScaledInstance((int) width, (int) height, BufferedImage.SCALE_SMOOTH);
final var outputImage = new BufferedImage((int) width, (int) height, inputImage.getType());
outputImage.getGraphics().drawImage(scaledImage, 0, 0, null);
final var outputStream = new ByteArrayOutputStream();
ImageIO.write(outputImage, reader.getFormatName(), outputStream);
final var outputData = outputStream.toByteArray();

if (outputData.length > softMaxSize * 1.5) {
throw new RequestEntityTooLargeException();
}

return outputData;
}

private ImageReader getFirstValidReader(final byte[] data) throws IOException {
final var input = ImageIO.createImageInputStream(new ByteArrayInputStream(data));
final var readers = ImageIO.getImageReaders(input);

while (readers.hasNext()) {
final var reader = readers.next();
final var format = reader.getFormatName().toUpperCase();

if (Arrays.stream(KnownFileType.values()).anyMatch(m -> m.name().equals(format))) {
return reader;
}
}

throw new IOException();
}
}
60 changes: 0 additions & 60 deletions src/main/java/app/fyreplace/api/services/MimeTypeService.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package app.fyreplace.api.services.storage.s3;

import app.fyreplace.api.services.MimeTypeService;
import app.fyreplace.api.services.ImageService;
import app.fyreplace.api.services.StorageService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down Expand Up @@ -33,7 +33,7 @@ public final class S3StorageService implements StorageService {
ObjectMapper objectMapper;

@Inject
MimeTypeService mimeTypeService;
ImageService imageService;

public void onStartup(@Observes final StartupEvent event) {
client.putBucketPolicy(b -> {
Expand Down Expand Up @@ -61,7 +61,7 @@ public byte[] fetch(final String path) throws IOException {

@Override
public void store(final String path, final byte[] data) throws IOException {
final var mime = mimeTypeService.getMimeType(data);
final var mime = imageService.getMimeType(data);
client.putObject(b -> b.bucket(config.bucket()).key(path).contentType(mime), RequestBody.fromBytes(data));
}

Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ quarkus:
policy: "deny"
cors: true
limits:
max-body-size: "1M"
max-body-size: "10M"
rest:
jackson:
optimization:
Expand Down Expand Up @@ -65,6 +65,8 @@ app:
s3:
bucket: ""
custom-endpoint: ""
limits:
max-size: "1M"
posts:
max-chapter-count: 10
starting-life: 4
Expand Down

0 comments on commit 8d14efc

Please sign in to comment.