Commit efbd10b7 authored by Martin Lowe's avatar Martin Lowe 🇨🇦
Browse files

#410 - Add image resizing, defaulting to a max dimension of 200px

parent 24f40174
...@@ -134,6 +134,13 @@ ...@@ -134,6 +134,13 @@
<version>${openhtml.version}</version> <version>${openhtml.version}</version>
</dependency> </dependency>
<!-- Image rescaling - better results than vanilla -->
<dependency>
<groupId>org.imgscalr</groupId>
<artifactId>imgscalr-lib</artifactId>
<version>4.2</version>
</dependency>
<!-- Annotation preprocessors - reduce all of the boiler plate --> <!-- Annotation preprocessors - reduce all of the boiler plate -->
<dependency> <dependency>
<groupId>com.google.auto.value</groupId> <groupId>com.google.auto.value</groupId>
...@@ -261,7 +268,7 @@ ...@@ -261,7 +268,7 @@
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version> <version>${surefire-plugin.version}</version>
<configuration> <configuration>
<skipTests>false</skipTests> <skipTests>false</skipTests>
<systemPropertyVariables> <systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home> <maven.home>${maven.home}</maven.home>
......
...@@ -73,7 +73,7 @@ public interface ImageStoreService { ...@@ -73,7 +73,7 @@ public interface ImageStoreService {
String webRoot(); String webRoot();
@WithDefault("65000") @WithDefault("1000000")
long maxSizeInBytes(); long maxSizeInBytes();
@WithDefault("") @WithDefault("")
...@@ -94,6 +94,9 @@ public interface ImageStoreService { ...@@ -94,6 +94,9 @@ public interface ImageStoreService {
@WithDefault("true") @WithDefault("true")
boolean enabled(); boolean enabled();
@WithDefault("200")
int maxDimension();
@WithDefault("0.80f") @WithDefault("0.80f")
@Max(1) @Max(1)
float factor(); float factor();
......
...@@ -52,6 +52,7 @@ import org.eclipsefoundation.persistence.service.FilterService; ...@@ -52,6 +52,7 @@ import org.eclipsefoundation.persistence.service.FilterService;
import org.eclipsefoundation.react.helper.ImageFileHelper; import org.eclipsefoundation.react.helper.ImageFileHelper;
import org.eclipsefoundation.react.namespace.ImageStoreFormat; import org.eclipsefoundation.react.namespace.ImageStoreFormat;
import org.eclipsefoundation.react.service.ImageStoreService; import org.eclipsefoundation.react.service.ImageStoreService;
import org.imgscalr.Scalr;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
...@@ -145,44 +146,24 @@ public class DefaultImageStoreService implements ImageStoreService { ...@@ -145,44 +146,24 @@ public class DefaultImageStoreService implements ImageStoreService {
Path p = imageStoreRoot.resolve(ImageFileHelper.getFileNameWithExtension(fileName, format, mimeType)); Path p = imageStoreRoot.resolve(ImageFileHelper.getFileNameWithExtension(fileName, format, mimeType));
BasicFileAttributeView attrView = Files.getFileAttributeView(p, BasicFileAttributeView.class); BasicFileAttributeView attrView = Files.getFileAttributeView(p, BasicFileAttributeView.class);
try { try {
// for images bigger than the current max that don't already exist, refuse to write them
byte[] bytes = imageBytes.get(); byte[] bytes = imageBytes.get();
if (bytes == null) { if (bytes == null) {
LOGGER.warn("Could not generate image file with name {} and format {}, no image passed", fileName, LOGGER.warn("Could not generate image file with name {} and format {}, no image passed", fileName,
format); format);
// cannot provide image, return null // cannot provide image, return null
return null; return null;
} else if (bytes.length > config.maxSizeInBytes() && (!Files.exists(p) }
|| !approximatelyMatch((long) bytes.length, attrView.readAttributes().size(), 1000))) { // compress image and then compare max size
byte[] compressedImage = compress(bytes, mimeType);
if (compressedImage.length > config.maxSizeInBytes() && (!Files.exists(p)
|| !approximatelyMatch((long) compressedImage.length, attrView.readAttributes().size(), 1000))) {
throw new BadRequestException( throw new BadRequestException(
"Passed image is larger than allowed size of '" + config.maxSizeInBytes() + "' bytes"); "Passed image is larger than allowed size of '" + config.maxSizeInBytes() + "' bytes");
} }
// if enabled, update the EclipseDB on logo update when image name is numeric // if enabled, update the EclipseDB on logo update when image name is numeric
// max size check is related to max blob size of 64kb
// TODO remove once the Drupal API references this API as this will no longer be needed then // TODO remove once the Drupal API references this API as this will no longer be needed then
if (config.persistToDb() && format.isPresent() handleDBPersist(fileName, compressedImage, mimeType, format);
&& ImageStoreFormat.ImageStoreFormats.WEB.equals(format.get()) && StringUtils.isNumeric(fileName)) {
// get a ref to the given organization information object
// get ref and update the object
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add(DefaultUrlParameterNames.ID.getName(), fileName);
List<OrganizationInformation> infoRefs = dao
.get(new RDBMSQuery<>(new RequestWrapper(), filters.get(OrganizationInformation.class), params));
// if ref doesn't exist, create one
OrganizationInformation oi;
if (infoRefs.isEmpty()) {
oi = new OrganizationInformation();
oi.setOrganizationID(Integer.valueOf(fileName));
oi.setCompanyUrl("");
} else {
oi = infoRefs.get(0);
}
oi.setLargeLogo(bytes);
oi.setSmallLogo(bytes);
oi.setLargeMime(mimeType);
oi.setSmallMime(mimeType);
dao.add(new RDBMSQuery<>(new RequestWrapper(), filters.get(OrganizationInformation.class)), Arrays.asList(oi));
}
// write will create and overwrite file by default if it exists // write will create and overwrite file by default if it exists
return getWebUrl(Files.write(p, compress(bytes, mimeType))); return getWebUrl(Files.write(p, compress(bytes, mimeType)));
...@@ -228,6 +209,49 @@ public class DefaultImageStoreService implements ImageStoreService { ...@@ -228,6 +209,49 @@ public class DefaultImageStoreService implements ImageStoreService {
}); });
} }
/**
* Handles persistence to the DB layer for images. Includes checks for size, format, and format before persisting to
* the database.
*
* @param fileName name of the file to be persisted
* @param compressedImage the compressed image bytes
* @param mimeType the mime type of the image
* @param format the format of the file
*/
private void handleDBPersist(String fileName, byte[] compressedImage, String mimeType,
Optional<ImageStoreFormat> format) {
if (config.persistToDb() && format.isPresent() && ImageStoreFormat.ImageStoreFormats.WEB.equals(format.get())
&& StringUtils.isNumeric(fileName)) {
if (compressedImage.length < 65535) {
LOGGER.warn("Cannot persist image with name {}, size of compressed image is over max size of table",
fileName);
} else {
// get a ref to the given organization information object
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add(DefaultUrlParameterNames.ID.getName(), fileName);
List<OrganizationInformation> infoRefs = dao
.get(new RDBMSQuery<>(new RequestWrapper(), filters.get(OrganizationInformation.class), params));
// if ref doesn't exist, create one
OrganizationInformation oi;
if (infoRefs.isEmpty()) {
oi = new OrganizationInformation();
oi.setOrganizationID(Integer.valueOf(fileName));
oi.setCompanyUrl("");
} else {
oi = infoRefs.get(0);
}
// as long as this org exists, update the logo
oi.setLargeLogo(compressedImage);
oi.setSmallLogo(compressedImage);
oi.setLargeMime(mimeType);
oi.setSmallMime(mimeType);
dao.add(new RDBMSQuery<>(new RequestWrapper(), filters.get(OrganizationInformation.class)),
Arrays.asList(oi));
}
}
}
/** /**
* Fuzzy match used to check byte size of files. Used just in case there is some minor changes to the data done via * Fuzzy match used to check byte size of files. Used just in case there is some minor changes to the data done via
* metadata or the like. * metadata or the like.
...@@ -270,7 +294,7 @@ public class DefaultImageStoreService implements ImageStoreService { ...@@ -270,7 +294,7 @@ public class DefaultImageStoreService implements ImageStoreService {
ImageOutputStream ios = ImageIO.createImageOutputStream(os)) { ImageOutputStream ios = ImageIO.createImageOutputStream(os)) {
// read in the image bytes for image io // read in the image bytes for image io
// Note: if this fails due to an zlib exception, the image is most likely corrupt somehow // Note: if this fails due to an zlib exception, the image is most likely corrupt somehow
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes)); BufferedImage image = rescale(ImageIO.read(new ByteArrayInputStream(imageBytes)));
// get the image writer for the current mime type // get the image writer for the current mime type
Iterator<ImageWriter> writers = ImageIO Iterator<ImageWriter> writers = ImageIO
...@@ -298,4 +322,21 @@ public class DefaultImageStoreService implements ImageStoreService { ...@@ -298,4 +322,21 @@ public class DefaultImageStoreService implements ImageStoreService {
} }
} }
} }
/**
* Using Scalr library, uses quality-focused resizing algorithms to shrink images larger than the configured maximum
* dimension.
*
* @param original the original image to resize
* @return the resized image if larger than max dimensions, or original image
*/
private BufferedImage rescale(BufferedImage original) {
// check if we need to rescale the image to save cycles
if (original.getHeight() < config.compression().maxDimension()
&& original.getWidth() < config.compression().maxDimension()) {
return original;
}
// use Scalr to maintain ratio and resize image
return Scalr.resize(original, Scalr.Method.QUALITY, config.compression().maxDimension());
}
} }
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment