MME-10 Attempt to include heic support; just moving to extracting metadata from images instead.

This commit is contained in:
Jesse Brault 2026-02-19 14:26:25 -06:00
parent 0d80c92850
commit 38f80aa8cd
4 changed files with 75 additions and 13 deletions

View File

@ -102,15 +102,14 @@ dependencies {
compileOnly 'org.jetbrains:annotations:26.0.2-1' compileOnly 'org.jetbrains:annotations:26.0.2-1'
// 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 // https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-batik
runtimeOnly 'com.twelvemonkeys.imageio:imageio-batik:3.12.0' runtimeOnly 'com.twelvemonkeys.imageio:imageio-batik:3.12.0'
// https://mvnrepository.com/artifact/org.apache.xmlgraphics/batik-all // https://mvnrepository.com/artifact/org.apache.xmlgraphics/batik-all
runtimeOnly 'org.apache.xmlgraphics:batik-all:1.19' runtimeOnly 'org.apache.xmlgraphics:batik-all:1.19'
implementation 'com.drewnoakes:metadata-extractor:2.19.0'
// Custom testing // Custom testing
testImplementation 'org.testcontainers:testcontainers:1.21.4' testImplementation 'org.testcontainers:testcontainers:1.21.4'
testImplementation 'org.testcontainers:junit-jupiter:1.21.4' testImplementation 'org.testcontainers:junit-jupiter:1.21.4'

Binary file not shown.

View File

@ -7,6 +7,14 @@ import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.util.MimeTypeService; import app.mealsmadeeasy.api.util.MimeTypeService;
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException; import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
import app.mealsmadeeasy.api.util.NoSuchEntityWithUsernameAndFilenameException; import app.mealsmadeeasy.api.util.NoSuchEntityWithUsernameAndFilenameException;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.jpeg.JpegDirectory;
import com.drew.metadata.png.PngDirectory;
import com.drew.metadata.webp.WebpDirectory;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@ -103,6 +111,33 @@ public class S3ImageService implements ImageService {
return didTransfer; return didTransfer;
} }
private record HeightWidth(int height, int width) {}
private <D extends Directory> HeightWidth getHeightAndWidthFromMetadata(
InputStream inputStream,
Class<D> directoryClass,
int heightTag,
int widthTag,
String debugName
) throws IOException {
try {
final Metadata metadata = ImageMetadataReader.readMetadata(inputStream);
final D directory = metadata.getFirstDirectoryOfType(directoryClass);
if (directory == null) {
throw new RuntimeException("Unable to get " + directoryClass.getSimpleName() + " for " + debugName);
}
if (!directory.containsTag(heightTag)) {
throw new RuntimeException("Unable to find height tag for " + debugName);
}
if (!directory.containsTag(widthTag)) {
throw new RuntimeException("Unable to find width tag for " + debugName);
}
return new HeightWidth(directory.getInt(heightTag), directory.getInt(widthTag));
} catch (ImageProcessingException | MetadataException e) {
throw new RuntimeException(e);
}
}
/** /**
* @apiNote Consumes and closes the {@link java.io.InputStream}. * @apiNote Consumes and closes the {@link java.io.InputStream}.
* *
@ -137,15 +172,43 @@ public class S3ImageService implements ImageService {
this.imageBucketName, filename, mimeType, toStore, objectSize this.imageBucketName, filename, mimeType, toStore, objectSize
); );
final BufferedImage bufferedImage = ImageIO.read(toRead); final HeightWidth hw;
if (bufferedImage == null) { switch (mimeType) {
throw new ImageException( case "image/jpeg" -> {
ImageException.Type.UNSUPPORTED_IMAGE_TYPE, hw = this.getHeightAndWidthFromMetadata(
"ImageIO could not read image: " + userFilename toRead,
); JpegDirectory.class,
JpegDirectory.TAG_IMAGE_HEIGHT,
JpegDirectory.TAG_IMAGE_WIDTH,
userFilename
);
}
case "image/png" -> {
hw = this.getHeightAndWidthFromMetadata(
toRead,
PngDirectory.class,
PngDirectory.TAG_IMAGE_HEIGHT,
PngDirectory.TAG_IMAGE_WIDTH,
userFilename
);
}
case "image/webp" -> {
hw = this.getHeightAndWidthFromMetadata(
toRead,
WebpDirectory.class,
WebpDirectory.TAG_IMAGE_HEIGHT,
WebpDirectory.TAG_IMAGE_WIDTH,
userFilename
);
}
case "image/svg+xml" -> {
final BufferedImage bufferedImage = ImageIO.read(toRead);
hw = new HeightWidth(bufferedImage.getHeight(), bufferedImage.getWidth());
}
default -> throw new RuntimeException("Unsupported mime type: " + mimeType);
} }
final int height = bufferedImage.getHeight(); final int height = hw.height();
final int width = bufferedImage.getWidth(); final int width = hw.width();
toRead.close(); toRead.close();
toStore.close(); toStore.close();

View File

@ -19,7 +19,7 @@ public class MimeTypeService {
final Matcher m = extensionPattern.matcher(userFilename); final Matcher m = extensionPattern.matcher(userFilename);
if (m.matches()) { if (m.matches()) {
final String extension = m.group(1); final String extension = m.group(1);
return switch (extension) { return switch (extension.toLowerCase()) {
case "jpg", "jpeg" -> IMAGE_JPEG; case "jpg", "jpeg" -> IMAGE_JPEG;
case "png" -> IMAGE_PNG; case "png" -> IMAGE_PNG;
case "svg" -> IMAGE_SVG; case "svg" -> IMAGE_SVG;
@ -32,7 +32,7 @@ public class MimeTypeService {
} }
public String getExtension(String mimeType) { public String getExtension(String mimeType) {
return switch (mimeType) { return switch (mimeType.toLowerCase()) {
case IMAGE_JPEG -> "jpg"; case IMAGE_JPEG -> "jpg";
case IMAGE_PNG -> "png"; case IMAGE_PNG -> "png";
case IMAGE_SVG -> "svg"; case IMAGE_SVG -> "svg";