Make height/width nullable; add support for reading svgs; better error handling.

This commit is contained in:
Jesse Brault 2025-12-13 17:14:40 -06:00
parent 0a619c5d41
commit 3166f1dd5d
6 changed files with 69 additions and 39 deletions

View File

@ -69,6 +69,12 @@ dependencies {
// https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-webp
runtimeOnly 'com.twelvemonkeys.imageio:imageio-webp:3.12.0'
// https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-batik
runtimeOnly 'com.twelvemonkeys.imageio:imageio-batik:3.12.0'
// https://mvnrepository.com/artifact/org.apache.xmlgraphics/batik-all
runtimeOnly 'org.apache.xmlgraphics:batik-all:1.19'
compileOnly 'org.jetbrains:annotations:24.1.0'
// Custom testing

View File

@ -16,7 +16,7 @@ public interface Image {
@Nullable String getCaption();
User getOwner();
boolean isPublic();
int getHeight();
int getWidth();
@Nullable Integer getHeight();
@Nullable Integer getWidth();
Set<User> getViewers();
}

View File

@ -6,7 +6,7 @@ public class ImageException extends Exception {
INVALID_ID,
INVALID_USERNAME_OR_FILENAME,
IMAGE_NOT_FOUND,
UNKNOWN_MIME_TYPE
UNSUPPORTED_IMAGE_TYPE,
}
private final Type type;

View File

@ -35,11 +35,9 @@ public class S3ImageEntity implements Image {
@Column(nullable = false)
private String objectName;
@Column(nullable = false)
private int height;
private Integer height;
@Column(nullable = false)
private int width;
private Integer width;
@ManyToOne(optional = false)
@JoinColumn(name = "owner_id", nullable = false)
@ -123,20 +121,20 @@ public class S3ImageEntity implements Image {
}
@Override
public int getHeight() {
public @Nullable Integer getHeight() {
return this.height;
}
public void setHeight(int height) {
public void setHeight(Integer height) {
this.height = height;
}
@Override
public int getWidth() {
public @Nullable Integer getWidth() {
return this.width;
}
public void setWidth(int width) {
public void setWidth(Integer width) {
this.width = width;
}

View File

@ -7,8 +7,6 @@ import app.mealsmadeeasy.api.s3.S3Manager;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
@ -16,6 +14,8 @@ import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
@ -27,7 +27,11 @@ import java.util.regex.Pattern;
public class S3ImageService implements ImageService {
private static final Pattern extensionPattern = Pattern.compile(".+\\.(.+)$");
private static final Logger logger = LoggerFactory.getLogger(S3ImageService.class);
private static final String IMAGE_JPEG = "image/jpeg";
private static final String IMAGE_PNG = "image/png";
private static final String IMAGE_SVG = "image/svg+xml";
private static final String IMAGE_WEBP = "image/webp";
private final S3Manager s3Manager;
private final S3ImageRepository imageRepository;
@ -51,10 +55,10 @@ public class S3ImageService implements ImageService {
if (m.matches()) {
final String extension = m.group(1);
return switch (extension) {
case "jpg", "jpeg" -> "image/jpeg";
case "png" -> "image/png";
case "svg" -> "image/svg+xml";
case "webp" -> "image/webp";
case "jpg", "jpeg" -> IMAGE_JPEG;
case "png" -> IMAGE_PNG;
case "svg" -> IMAGE_SVG;
case "webp" -> IMAGE_WEBP;
default -> throw new IllegalArgumentException("Cannot determine mime type for extension: " + extension);
};
} else {
@ -64,13 +68,13 @@ public class S3ImageService implements ImageService {
private String getExtension(String mimeType) throws ImageException {
return switch (mimeType) {
case "image/jpeg" -> "jpg";
case "image/png" -> "png";
case "image/svg+xml" -> "svg";
case "image/webp" -> "webp";
case IMAGE_JPEG -> "jpg";
case IMAGE_PNG -> "png";
case IMAGE_SVG -> "svg";
case IMAGE_WEBP -> "webp";
default -> throw new ImageException(
ImageException.Type.UNKNOWN_MIME_TYPE,
"Unknown mime type: " + mimeType
ImageException.Type.UNSUPPORTED_IMAGE_TYPE,
"Unsupported mime type: " + mimeType
);
};
}
@ -101,6 +105,18 @@ public class S3ImageService implements ImageService {
return didTransfer;
}
/**
* @apiNote Consumes and closes the {@link java.io.InputStream}.
*
* @param owner the User owner
* @param userFilename the name of the uploaded file from the user
* @param inputStream the image content
* @param objectSize the size of the image, in bytes
* @param createSpec the metadata for the image
* @return an {@link app.mealsmadeeasy.api.image.Image} representing the stored image
* @throws IOException if there are any errors related to IO while storing the image
* @throws ImageException if there are any errors processing the image
*/
@Override
public Image create(
User owner,
@ -113,19 +129,29 @@ public class S3ImageService implements ImageService {
final String uuid = UUID.randomUUID().toString();
final String extension = this.getExtension(mimeType);
final String filename = uuid + "." + extension;
final var baos = new ByteArrayOutputStream();
inputStream.transferTo(baos);
final InputStream toStore = new ByteArrayInputStream(baos.toByteArray());
final InputStream toRead = new ByteArrayInputStream(baos.toByteArray());
final String objectName = this.s3Manager.store(
this.imageBucketName, filename, mimeType, inputStream, objectSize
this.imageBucketName, filename, mimeType, toStore, objectSize
);
final int height, width;
try (final InputStream imageContent = this.s3Manager.load(this.imageBucketName, objectName)) {
final BufferedImage bufferedImage = ImageIO.read(imageContent);
if (bufferedImage == null) {
logger.error("ImageIO could not read image: {} ({})", userFilename, objectName);
}
height = bufferedImage.getHeight();
width = bufferedImage.getWidth();
final BufferedImage bufferedImage = ImageIO.read(toRead);
if (bufferedImage == null) {
throw new ImageException(
ImageException.Type.UNSUPPORTED_IMAGE_TYPE,
"ImageIO could not read image: " + userFilename
);
}
final int height = bufferedImage.getHeight();
final int width = bufferedImage.getWidth();
toRead.close();
toStore.close();
inputStream.close();
final S3ImageEntity draft = new S3ImageEntity();
draft.setOwner((UserEntity) owner);

View File

@ -41,8 +41,8 @@ public class ImageView {
private @Nullable String caption;
private UserInfoView owner;
private boolean isPublic;
private int height;
private int width;
private @Nullable Integer height;
private @Nullable Integer width;
private @Nullable Set<UserInfoView> viewers;
public String getUrl() {
@ -117,19 +117,19 @@ public class ImageView {
this.isPublic = isPublic;
}
public int getHeight() {
public @Nullable Integer getHeight() {
return this.height;
}
public void setHeight(int height) {
public void setHeight(Integer height) {
this.height = height;
}
public int getWidth() {
public @Nullable Integer getWidth() {
return this.width;
}
public void setWidth(int width) {
public void setWidth(Integer width) {
this.width = width;
}