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

Merge branch 'malowe/dev/410' into 'dev'

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

See merge request !453
parents 24f40174 efbd10b7
Pipeline #1559 passed with stage
in 0 seconds
......@@ -134,6 +134,13 @@
<version>${openhtml.version}</version>
</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 -->
<dependency>
<groupId>com.google.auto.value</groupId>
......@@ -261,7 +268,7 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<skipTests>false</skipTests>
<skipTests>false</skipTests>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
......
......@@ -73,7 +73,7 @@ public interface ImageStoreService {
String webRoot();
@WithDefault("65000")
@WithDefault("1000000")
long maxSizeInBytes();
@WithDefault("")
......@@ -94,6 +94,9 @@ public interface ImageStoreService {
@WithDefault("true")
boolean enabled();
@WithDefault("200")
int maxDimension();
@WithDefault("0.80f")
@Max(1)
float factor();
......
......@@ -52,6 +52,7 @@ import org.eclipsefoundation.persistence.service.FilterService;
import org.eclipsefoundation.react.helper.ImageFileHelper;
import org.eclipsefoundation.react.namespace.ImageStoreFormat;
import org.eclipsefoundation.react.service.ImageStoreService;
import org.imgscalr.Scalr;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -145,44 +146,24 @@ public class DefaultImageStoreService implements ImageStoreService {
Path p = imageStoreRoot.resolve(ImageFileHelper.getFileNameWithExtension(fileName, format, mimeType));
BasicFileAttributeView attrView = Files.getFileAttributeView(p, BasicFileAttributeView.class);
try {
// for images bigger than the current max that don't already exist, refuse to write them
byte[] bytes = imageBytes.get();
if (bytes == null) {
LOGGER.warn("Could not generate image file with name {} and format {}, no image passed", fileName,
format);
// cannot provide image, 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(
"Passed image is larger than allowed size of '" + config.maxSizeInBytes() + "' bytes");
}
// 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
if (config.persistToDb() && format.isPresent()
&& 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));
}
handleDBPersist(fileName, compressedImage, mimeType, format);
// write will create and overwrite file by default if it exists
return getWebUrl(Files.write(p, compress(bytes, mimeType)));
......@@ -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
* metadata or the like.
......@@ -270,7 +294,7 @@ public class DefaultImageStoreService implements ImageStoreService {
ImageOutputStream ios = ImageIO.createImageOutputStream(os)) {
// read in the image bytes for image io
// 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
Iterator<ImageWriter> writers = ImageIO
......@@ -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