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 // https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-webp
runtimeOnly 'com.twelvemonkeys.imageio:imageio-webp:3.12.0' 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' compileOnly 'org.jetbrains:annotations:24.1.0'
// Custom testing // Custom testing

View File

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

View File

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

View File

@ -35,11 +35,9 @@ public class S3ImageEntity implements Image {
@Column(nullable = false) @Column(nullable = false)
private String objectName; private String objectName;
@Column(nullable = false) private Integer height;
private int height;
@Column(nullable = false) private Integer width;
private int width;
@ManyToOne(optional = false) @ManyToOne(optional = false)
@JoinColumn(name = "owner_id", nullable = false) @JoinColumn(name = "owner_id", nullable = false)
@ -123,20 +121,20 @@ public class S3ImageEntity implements Image {
} }
@Override @Override
public int getHeight() { public @Nullable Integer getHeight() {
return this.height; return this.height;
} }
public void setHeight(int height) { public void setHeight(Integer height) {
this.height = height; this.height = height;
} }
@Override @Override
public int getWidth() { public @Nullable Integer getWidth() {
return this.width; return this.width;
} }
public void setWidth(int width) { public void setWidth(Integer width) {
this.width = 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.User;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -16,6 +14,8 @@ import org.springframework.stereotype.Service;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -27,7 +27,11 @@ import java.util.regex.Pattern;
public class S3ImageService implements ImageService { public class S3ImageService implements ImageService {
private static final Pattern extensionPattern = Pattern.compile(".+\\.(.+)$"); 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 S3Manager s3Manager;
private final S3ImageRepository imageRepository; private final S3ImageRepository imageRepository;
@ -51,10 +55,10 @@ public class S3ImageService implements ImageService {
if (m.matches()) { if (m.matches()) {
final String extension = m.group(1); final String extension = m.group(1);
return switch (extension) { return switch (extension) {
case "jpg", "jpeg" -> "image/jpeg"; case "jpg", "jpeg" -> IMAGE_JPEG;
case "png" -> "image/png"; case "png" -> IMAGE_PNG;
case "svg" -> "image/svg+xml"; case "svg" -> IMAGE_SVG;
case "webp" -> "image/webp"; case "webp" -> IMAGE_WEBP;
default -> throw new IllegalArgumentException("Cannot determine mime type for extension: " + extension); default -> throw new IllegalArgumentException("Cannot determine mime type for extension: " + extension);
}; };
} else { } else {
@ -64,13 +68,13 @@ public class S3ImageService implements ImageService {
private String getExtension(String mimeType) throws ImageException { private String getExtension(String mimeType) throws ImageException {
return switch (mimeType) { return switch (mimeType) {
case "image/jpeg" -> "jpg"; case IMAGE_JPEG -> "jpg";
case "image/png" -> "png"; case IMAGE_PNG -> "png";
case "image/svg+xml" -> "svg"; case IMAGE_SVG -> "svg";
case "image/webp" -> "webp"; case IMAGE_WEBP -> "webp";
default -> throw new ImageException( default -> throw new ImageException(
ImageException.Type.UNKNOWN_MIME_TYPE, ImageException.Type.UNSUPPORTED_IMAGE_TYPE,
"Unknown mime type: " + mimeType "Unsupported mime type: " + mimeType
); );
}; };
} }
@ -101,6 +105,18 @@ public class S3ImageService implements ImageService {
return didTransfer; 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 @Override
public Image create( public Image create(
User owner, User owner,
@ -113,19 +129,29 @@ public class S3ImageService implements ImageService {
final String uuid = UUID.randomUUID().toString(); final String uuid = UUID.randomUUID().toString();
final String extension = this.getExtension(mimeType); final String extension = this.getExtension(mimeType);
final String filename = uuid + "." + extension; 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( final String objectName = this.s3Manager.store(
this.imageBucketName, filename, mimeType, inputStream, objectSize this.imageBucketName, filename, mimeType, toStore, objectSize
); );
final int height, width; final BufferedImage bufferedImage = ImageIO.read(toRead);
try (final InputStream imageContent = this.s3Manager.load(this.imageBucketName, objectName)) { if (bufferedImage == null) {
final BufferedImage bufferedImage = ImageIO.read(imageContent); throw new ImageException(
if (bufferedImage == null) { ImageException.Type.UNSUPPORTED_IMAGE_TYPE,
logger.error("ImageIO could not read image: {} ({})", userFilename, objectName); "ImageIO could not read image: " + userFilename
} );
height = bufferedImage.getHeight();
width = bufferedImage.getWidth();
} }
final int height = bufferedImage.getHeight();
final int width = bufferedImage.getWidth();
toRead.close();
toStore.close();
inputStream.close();
final S3ImageEntity draft = new S3ImageEntity(); final S3ImageEntity draft = new S3ImageEntity();
draft.setOwner((UserEntity) owner); draft.setOwner((UserEntity) owner);

View File

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