ch.entwine.weblounge.preview.imagemagick.ImageMagickPreviewGenerator.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.preview.imagemagick.ImageMagickPreviewGenerator.java

Source

/*
 *  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);
        }

    }

}