/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.preview.imagemagick;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceContent;
import ch.entwine.weblounge.common.content.image.ImagePreviewGenerator;
import ch.entwine.weblounge.common.content.image.ImageResource;
import ch.entwine.weblounge.common.content.image.ImageStyle;
import ch.entwine.weblounge.common.impl.content.image.ImageStyleUtils;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.ImageScalingMode;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.im4java.core.ConvertCmd;
import org.im4java.core.IMOperation;
import org.im4java.core.Info;
import org.im4java.process.OutputConsumer;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class used for dealing with images and image styles.
*/
public final class ImageMagickPreviewGenerator implements ImagePreviewGenerator {
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(ImageMagickPreviewGenerator.class);
/** List of supported formats (cached) */
private final Map<String, Boolean> supportedFormats = new HashMap<String, Boolean>();
/** Flag to indicate whether format detection is supported */
private boolean formatDecetionSupported = true;
/** The image magic temp directory */
private File imageMagickDir = null;
/**
* Called by the {@link ImageMagickActivator} on service activation.
*
* @param ctx
* the component context
*/
void activate(ComponentContext ctx) {
try {
prepareDirectory();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
/**
* Called by the {@link ImageMagickActivator} on service inactivation.
*/
void deactivate() {
FileUtils.deleteQuietly(imageMagickDir);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.PreviewGenerator#supports(ch.entwine.weblounge.common.content.Resource)
*/
public boolean supports(Resource<?> resource) {
return (resource instanceof ImageResource);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.PreviewGenerator#supports(java.lang.String)
*/
public boolean supports(String format) {
if (format == null)
throw new IllegalArgumentException("Format cannot be null");
if (!formatDecetionSupported)
return true;
// Check for verified support
if (supportedFormats.containsKey(format))
return supportedFormats.get(format);
// Reach out to ImageMagick
ConvertCmd imageMagick = new ConvertCmd();
IMOperation op = new IMOperation();
op.identify().list("format");
try {
final Pattern p = Pattern.compile("[\\s]+" + format.toUpperCase() + "[\\s]+rw");
final Boolean[] supported = new Boolean[1];
imageMagick.setOutputConsumer(new OutputConsumer() {
public void consumeOutput(InputStream is) throws IOException {
String output = IOUtils.toString(is);
Matcher m = p.matcher(output);
supported[0] = new Boolean(m.find());
}
});
imageMagick.run(op);
// Cache the result
supportedFormats.put(format, supported[0]);
return supported[0];
} catch (Throwable t) {
logger.warn("Error looking up formats supported by ImageMagick: {}", t.getMessage());
formatDecetionSupported = false;
logger.info("ImageMagick format lookup failed, assuming support for all formats");
return true;
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.PreviewGenerator#createPreview(ch.entwine.weblounge.common.content.Resource,
* ch.entwine.weblounge.common.site.Environment,
* ch.entwine.weblounge.common.language.Language,
* ch.entwine.weblounge.common.content.image.ImageStyle, String,
* java.io.InputStream, java.io.OutputStream)
*/
public void createPreview(Resource<?> resource, Environment environment,
Language language, ImageStyle style, String format, InputStream is,
OutputStream os) throws IOException {
if (format == null) {
if (resource == null)
throw new IllegalArgumentException("Resource cannot be null");
if (resource.getContent(language) == null) {
logger.warn("Skipping creation of preview for {} in language '{}': no content", resource, language.getIdentifier());
return;
}
String mimetype = resource.getContent(language).getMimetype();
logger.debug("Image preview of {} is generated using the resource's mimetype '{}'", resource.getIdentifier(), mimetype);
format = mimetype.substring(mimetype.indexOf("/") + 1);
}
style(is, os, format, style);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.image.ImagePreviewGenerator#createPreview(java.io.File,
* ch.entwine.weblounge.common.site.Environment,
* ch.entwine.weblounge.common.language.Language,
* ch.entwine.weblounge.common.content.image.ImageStyle,
* java.lang.String, java.io.InputStream, java.io.OutputStream)
*/
public void createPreview(File imageFile, Environment environment,
Language language, ImageStyle style, String format, InputStream is,
OutputStream os) throws IOException {
if (format == null) {
if (imageFile == null)
throw new IllegalArgumentException("Image file cannot be null");
format = FilenameUtils.getExtension(imageFile.getName());
logger.debug("Image preview of {} is generated as '{}'", imageFile.getAbsolutePath(), format);
}
style(is, os, format, style);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.PreviewGenerator#getContentType(ch.entwine.weblounge.common.content.Resource,
* ch.entwine.weblounge.common.language.Language,
* ch.entwine.weblounge.common.content.image.ImageStyle)
*/
public String getContentType(Resource<?> resource, Language language,
ImageStyle style) {
String mimetype = resource.getContent(language).getMimetype();
return mimetype;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.PreviewGenerator#getSuffix(ch.entwine.weblounge.common.content.Resource,
* ch.entwine.weblounge.common.language.Language,
* ch.entwine.weblounge.common.content.image.ImageStyle)
*/
public String getSuffix(Resource<?> resource, Language language,
ImageStyle style) {
// Load the resource
ResourceContent content = resource.getContent(language);
if (content == null) {
content = resource.getOriginalContent();
if (content == null) {
logger.warn("Trying to get filename suffix for {}, which has no content", resource);
return null;
}
}
// Get the file name
String filename = content.getFilename();
if (StringUtils.isBlank(filename)) {
logger.warn("Trying to get filename suffix for {}, which has no filename", resource);
return null;
}
// Add the file identifier name
if (StringUtils.isNotBlank(style.getIdentifier())) {
filename += "-" + style.getIdentifier();
}
return FilenameUtils.getExtension(filename);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.PreviewGenerator#getPriority()
*/
public int getPriority() {
return 100;
}
/**
* Resizes the given image to what is defined by the image style and writes
* the result to the output stream.
*
* @param is
* the input stream
* @param os
* the output stream
* @param format
* the image format
* @param style
* the style
* @throws IllegalArgumentException
* if the image is in an unsupported format
* @throws IllegalArgumentException
* if the input stream is empty
* @throws IOException
* if reading from or writing to the stream fails
* @throws OutOfMemoryError
* if the image is too large to be processed in memory
*/
@SuppressWarnings("cast")
private void style(InputStream is, OutputStream os, String format,
ImageStyle style) throws IllegalArgumentException, IOException,
OutOfMemoryError {
// Does the input stream contain any data?
if (is.available() == 0)
throw new IllegalArgumentException("Empty input stream was passed to image styling");
// Do we need to do any work at all?
if (style == null || ImageScalingMode.None.equals(style.getScalingMode())) {
logger.trace("No scaling needed, performing a noop stream copy");
IOUtils.copy(is, os);
return;
}
String uuid = UUID.randomUUID().toString();
File originalFile = new File(imageMagickDir, "image-" + uuid + "." + format);
File scaledFile = new File(imageMagickDir, "image-scaled-" + uuid + "." + format);
File croppedFile = new File(imageMagickDir, "image-cropped-" + uuid + "." + format);
try {
File finalFile = null;
FileOutputStream fos = new FileOutputStream(originalFile);
IOUtils.copy(is, fos);
IOUtils.closeQuietly(fos);
IOUtils.closeQuietly(is);
// Load the image from the temporary file
Info imageInfo = new Info(originalFile.getAbsolutePath(), true);
// Get the original image size
int imageWidth = imageInfo.getImageWidth();
int imageHeight = imageInfo.getImageHeight();
// Prepare for processing
ConvertCmd imageMagick = new ConvertCmd();
// Resizing
float scale = ImageStyleUtils.getScale(imageWidth, imageHeight, style);
int scaledWidth = Math.round(scale * imageWidth);
int scaledHeight = Math.round(scale * imageHeight);
int cropX = 0;
int cropY = 0;
// If either one of scaledWidth or scaledHeight is < 1.0, then
// the scale needs to be adapted to scale to 1.0 exactly and accomplish
// the rest by cropping.
if (scaledWidth < 1.0f) {
scale = 1.0f / imageWidth;
scaledWidth = 1;
cropY = imageHeight - scaledHeight;
scaledHeight = Math.round(imageHeight * scale);
} else if (scaledHeight < 1.0f) {
scale = 1.0f / imageHeight;
scaledHeight = 1;
cropX = imageWidth - scaledWidth;
scaledWidth = Math.round(imageWidth * scale);
}
// Do the scaling
IMOperation scaleOp = new IMOperation();
scaleOp.addImage(originalFile.getAbsolutePath());
scaleOp.resize((int) scaledWidth, (int) scaledHeight);
scaleOp.addImage(scaledFile.getAbsolutePath());
imageMagick.run(scaleOp);
finalFile = scaledFile;
// Cropping
cropX = (int) Math.max(cropX, Math.ceil(ImageStyleUtils.getCropX(scaledWidth, scaledHeight, style)));
cropY = (int) Math.max(cropY, Math.ceil(ImageStyleUtils.getCropY(scaledWidth, scaledHeight, style)));
if ((cropX > 0 && Math.floor(cropX / 2.0f) > 0) || (cropY > 0 && Math.floor(cropY / 2.0f) > 0)) {
int croppedLeft = (int) (cropX > 0 ? ((float) Math.floor(cropX / 2.0f)) : 0.0f);
int croppedTop = (int) (cropY > 0 ? ((float) Math.floor(cropY / 2.0f)) : 0.0f);
int croppedWidth = (int) (scaledWidth - Math.max(cropX, 0.0f));
int croppedHeight = (int) (scaledHeight - Math.max(cropY, 0.0f));
// Do the cropping
IMOperation cropOperation = new IMOperation();
cropOperation.addImage(scaledFile.getAbsolutePath());
cropOperation.crop(croppedWidth, croppedHeight, croppedLeft, croppedTop);
cropOperation.p_repage(); // Reset the page canvas and position to match
// the actual cropped image
cropOperation.addImage(croppedFile.getAbsolutePath());
imageMagick.run(cropOperation);
finalFile = croppedFile;
}
// Write resized/cropped image encoded as JPEG to the output stream
FileInputStream fis = null;
try {
fis = new FileInputStream(finalFile);
IOUtils.copy(fis, os);
} finally {
IOUtils.closeQuietly(fis);
}
} catch (Throwable t) {
throw new IllegalArgumentException(t.getMessage());
} finally {
FileUtils.deleteQuietly(originalFile);
FileUtils.deleteQuietly(scaledFile);
FileUtils.deleteQuietly(croppedFile);
}
}
/**
* Makes sure that a temp directory exists.
*
* @throws IOException
* if the directory cannot be created
*/
private void prepareDirectory() throws IOException {
imageMagickDir = new File(FileUtils.getTempDirectory(), "imagemagick");
if (!imageMagickDir.isDirectory() && !imageMagickDir.mkdirs()) {
logger.error("Unable to create temp directory for ImageMagick at {}", imageMagickDir);
throw new IOException("Unable to create temp directory for ImageMagick at " + imageMagickDir);
}
}
}