com.techempower.helper.ImageMagickHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gemini Show documentation
Show all versions of gemini Show documentation
Gemini is a web framework created by TechEmpower.
/*******************************************************************************
* Copyright (c) 2018, TechEmpower, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name TechEmpower, Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/
package com.techempower.helper;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import org.im4java.core.*;
import org.im4java.process.*;
import com.techempower.gemini.*;
import com.techempower.io.image.*;
import com.techempower.log.*;
/**
* Provides simple utility methods for dealing with images.
*/
public final class ImageMagickHelper
extends ImageHelper
{
private final GeminiApplication app;
private final ComponentLog log;
public ImageMagickHelper(GeminiApplication application)
{
this.app = application;
this.log = this.app.getLog("IMaH");
this.log.log("ImageMagickHelper instantiated.");
}
@Override
public BufferedImage createBufferedImage(TransformParams transformParams,
ImageHelper.ImageTransform imageTransform)
{
return this.createBufferedImage((MagickParams) transformParams, (MagickTransform)imageTransform);
}
/**
* Method of creating a BufferedImage object instead of doing
* simpler IO.
* Note: when a BufferedImage is created from
* lossy file types (i.e. JPEG), it is compressed at the time
* the Raster is set (construction) and the default compression
* is set to 75 (1-100).
* If different compression is desired, then do not use this
* method or use lossless filetypes.
* @see ImageMagickHelper#transformImage(
* ImageMagickHelper.MagickParams,
* ImageMagickHelper.MagickTransform)
*
* @return The transformed BufferedImage.
*/
public BufferedImage createBufferedImage(MagickParams params,
MagickTransform magickTransform)
{
try
{
byte[] outBytes = transformImage(params, magickTransform);
if (outBytes == null)
{
return ImageIO.read(params.getDestFile());
}
return ImageIO.read(ImageIO.createImageInputStream(outBytes));
}
catch (IOException ioe)
{
this.log.log("Exception while trying to transform image.", LogLevel.ALERT, ioe);
}
return null;
}
/**
* Takes a MagickParams object and calls *Magick based on the params and
* imageTransform. IO is determined by presence of certain values in the
* MagickParams object:
*
* - If source file is present, its path is passed to *Magick (IO is
* done by *Magick, not Java; more efficient).
* - If source file is not present, byte data is passed to *Magick via
* stdio streams (less efficient than #1).
* - If destination file is present, its patch is passed to *Magick (IO
* is done by *Magick, not Java; more efficient).
* - If destination file is not present, then the byte[] is returned from
* *Magick via stdio and must be handled by code (less efficient than
* #3 iff the byte[] will eventually be output to a file without any
* alteration).
*
* Normally, this byte array is something to ultimately be written to disk;
* here is an example of how to do just that:
*
*
* byte[] thumbBytes = ImageMagickHelper.transformImage(params, transform);
* FileOutputStream fos = new FileOutputStream(outFile);
* for(int b : thumbBytes)
* {
* fos.write(b);
* }
*
* @return The byte array of the transformed image iff no destination file
* is specified.
*/
public byte[] transformImage(MagickParams params,
MagickTransform magickTransform)
{
ConvertCmd cmd;
ByteArrayInputStream bais = null;
try
{
GMOperation op = new GMOperation();
// Input is source file if present, else tries to use bytes (stdin).
if (params.getSourceFile() != null)
{
op.addImage(params.getSourceFile().getAbsolutePath());
}
else if(params.getImageFileData() != null)
{
op.addImage("-"); // input: stdin
}
// im4java omits null Integers from the parameters, so if we have a
// newWidth or newHeight of 0, just leave their respective Integers
// null.
Integer w = magickTransform.getNewWidth();
Integer h = magickTransform.getNewHeight();
// Don't do any cropping or resizing if we have no dimensions.
if (w != null || h != null)
{
if (!magickTransform.isPreserveAspect())
{
// Adjust aspect ratio as necessary to exactly reach desired
// dimensions.
op.resize(w, h, '!');
}
else if (magickTransform.isCropToFit())
{
// Resize until smallest side is smallest of w or h.
op.resize(w, h, '^');
// Try and center the cropping.
int offsetY = 0;
int offsetX = 0;
Integer oldW = magickTransform.getOldWidth();
Integer oldH = magickTransform.getOldHeight();
if(oldW != null && oldH != null)
{
// We need to mathematically figure out the new width and height that
// op.resize(w, h, '^') gives us for free.
int newWidth = w;
int newHeight = h;
if( w < h )
{
if(oldH >= oldW)
{
newWidth = (int)(((double)oldW * (double)h) / (double)oldH);
}
if(oldH <= oldW)
{
newHeight = (int)(((double)oldH * (double)w) / (double)oldW);
}
}
else if( h < w )
{
if(oldH >= oldW)
{
newHeight = (int)(((double)oldH * (double)w) / (double)oldW);
}
else if(oldH <= oldW)
{
newWidth = (int)(((double)oldW * (double)h) / (double)oldH);
}
}
if(newWidth > w)
{
offsetX = (newWidth - w) / 2;
}
if(newHeight > h)
{
offsetY = (newHeight - h) / 2;
}
}
// Now split the difference to "center" the crop
op.crop(w, h, offsetX, offsetY);
}
else
{
// Resize until largest side is largest of w or h. Could be smaller
// than desired dimensions.
op.resize(w, h);
}
}
// Flatten onto a white background image
if (magickTransform.isFlatten())
{
op.flatten();
}
// JPEG quality settings can make a big difference.
if (magickTransform.getQuality() != null)
{
op.quality(magickTransform.getQuality());
}
// Gets rid of any ICM, EXIF, IPTC, or other meta-data profiles to save
// a bit more space.
op.p_profile("*");
// Output is stdout unless we write directly to a file for higher JPEG
// quality.
String outPath = "-"; // output: stdout
if (params.getDestFile() != null)
{
outPath = params.getDestFile().getAbsolutePath();
}
// If no newFormat is specified, don't change the format.
if (StringHelper.isEmptyTrimmed(magickTransform.getNewFormat()))
{
op.addImage(outPath);
}
else
{
op.addImage(magickTransform.getNewFormat().trim() + ":" + outPath);
}
// Set up command
cmd = new ConvertCmd(params.usesGraphicsMagick());
if(params.getSourceFile() == null)
{
// Pipe the fileData to stdin, to avoid writing to a file first.
bais = new ByteArrayInputStream(params.getImageFileData());
Pipe pipeIn = new Pipe(bais, null);
cmd.setInputProvider(pipeIn);
}
ByteArrayOutputConsumer out = null;
if(params.getDestFile() == null)
{
out = new ByteArrayOutputConsumer();
cmd.setOutputConsumer(out);
}
// Run the commend.
cmd.run(op);
if (out == null)
{
return null; // We wrote the output to dest, so nothing to return.
}
else
{
// Return the resulting image.
return out.getRawBytes();
}
}
catch (IOException | IM4JavaException | InterruptedException e)
{
this.log.log("Exception while trying to transform image.", LogLevel.ALERT, e);
}
finally
{
if (bais != null)
{
try
{
bais.close();
}
catch (IOException ioe)
{
this.log.log("Exception trying to close input stream.", LogLevel.ALERT, ioe);
}
}
}
return null;
}
@Override
public byte[] transformImage(TransformParams params,
ImageHelper.ImageTransform imageTransform)
{
return transformImage((MagickParams)params, (MagickTransform)imageTransform);
}
/**
* Describes a transformation to be done to an image. Passed as a parameter to
* ImageMagickHelper.resizeImage(). This is broken out into a separate class so that
* transformations can be defined once and reused in multiple places.
*
*/
public static class MagickTransform extends ImageHelper.ImageTransform
{
private Integer oldWidth;
private Integer oldHeight;
private boolean flatten = false;
/**
* Constructor.
*
* @param newFormat The target format you'd like the image to end up in.
* E.g., "jpg", "png".
* @param newWidth Desired width. Ignored if less than 1.
* @param newHeight Desired height. Ignored if less than 1.
* @param preserveAspect If true, will not squish image. If image is not
* square, then one of the dimensions will be shorter than requested.
* @param cropToFit If true, the target dimensions will be reached by
* cropping out the part of the image that exceeds them, after resizing.
*/
/*public ImageTransform(String newFormat, int newWidth, int newHeight,
boolean preserveAspect, boolean cropToFit)
{
this(newFormat, newWidth, newHeight, 0, 0, preserveAspect, cropToFit);
}*/
public MagickTransform(String newFormat, int newWidth, int newHeight,
int oldWidth, int oldHeight, boolean preserveAspect, boolean cropToFit)
{
super(newFormat, newWidth, newHeight, preserveAspect, cropToFit);
this.oldWidth = oldWidth > 0 ? oldWidth : null;
this.oldHeight = oldHeight > 0 ? oldHeight : null;
}
/**
* @param newFormat The target format you'd like the image to end up in.
* E.g., "jpg", "png".
* @param newWidth Desired width. Ignored if less than 1.
* @param newHeight Desired height. Ignored if less than 1.
* @param preserveAspect If true, will not squish image. If image is not
* square, then one of the dimensions will be shorter than requested.
* @param cropToFit If true, the target dimensions will be reached by
* cropping out the part of the image that exceeds them, after resizing.
* @param flatten If true, the image(s) will compose on to a background
* to form one single image. Default is onto a white canvas.
*/
public MagickTransform(String newFormat, int newWidth, int newHeight,
boolean preserveAspect, boolean cropToFit, boolean flatten)
{
this(newFormat, newWidth, newHeight, 0, 0, preserveAspect, cropToFit,
flatten);
}
public MagickTransform(String newFormat, int newWidth, int newHeight,
int oldWidth, int oldHeight, boolean preserveAspect, boolean cropToFit,
boolean flatten)
{
this(newFormat, newWidth, newHeight, oldWidth, oldHeight, preserveAspect, cropToFit);
this.flatten = flatten;
}
public Integer getOldWidth()
{
return this.oldWidth;
}
public void setOldWidth(Integer oldWidth)
{
this.oldWidth = oldWidth;
}
public Integer getOldHeight()
{
return this.oldHeight;
}
public void setOldHeight(Integer oldHeight)
{
this.oldHeight = oldHeight;
}
public boolean isFlatten()
{
return this.flatten;
}
public void setFlatten(boolean flatten)
{
this.flatten = flatten;
}
}
/**
* Describes all the various parameters associated with *Magick execution.
* Houses destination file, source file, source data, and whether Graphics
* or ImageMagick should be used.
*/
public static class MagickParams extends ImageHelper.TransformParams
{
private final boolean useGraphicsMagick;
/**
* Simple constructor that sets the imageFileData and specifies
* whether GraphicsMagick or ImageMagick is to be used by ImageMagickHelper.
*/
public MagickParams(byte[] imageFileData, boolean useGraphicsMagick)
{
super(imageFileData);
this.useGraphicsMagick = useGraphicsMagick;
}
/**
* Constructor that sets the imageFileData to be piped to *Magick
* and specifies an output File destination to which *Magick will
* output directly (no Java file-writing).
*/
public MagickParams(byte[] imageFileData, File destFile,
boolean useGraphicsMagick)
{
super(imageFileData, destFile);
this.useGraphicsMagick = useGraphicsMagick;
}
/**
* Constructor that sets the input and output files for *Magick to
* use as input and output for the process.
*/
public MagickParams(File sourceFile, File destFile,
boolean useGraphicsMagick)
{
super(sourceFile, destFile);
this.useGraphicsMagick = useGraphicsMagick;
}
/**
* Constructor that sets the input file for *Magick to use. This
* will tell transformImage
to send the resulting
* bytes from *Magick back to the application to be turned into
* a byte array only.
* Note: nothing will be done with the byte[] from *Magick. This
* must be handled in code directly.
*/
public MagickParams(File sourceFile, boolean useGraphicsMagick)
{
super(sourceFile);
this.useGraphicsMagick = useGraphicsMagick;
}
public boolean usesGraphicsMagick()
{
return useGraphicsMagick;
}
}
} // End ImageMagickHelper.