Java Spring - Thumbnail Generating
Java Spring - Thumbnail Generating
The purpose of this tutorial is to demonstrate how to generate thumbnails in Jav a using the Spring framework. The thumbnail generation is based on the algorithm by Phil Reeve . It has also been further improved by Jim McCabe to base the thumbnail generation in previously generated thumbnail so that the image is not continuously evaluat ed. The code included contains also a file upload form that demonstrates uploading f iles with Spring MVC although you can use the ThumbnailGeneratorEngine in any of your service beans. This tutorial also uses the Engine / Processor pattern as explained here. The ThumbnailGeneratorEngine can be configured with processors that know how to process certain content types and input streams. The code only demonstrates gene rating thumbnails from images but other libraries can be used to implement proce ssors that generate thumbnails from pdf, word, excel, urls, etc The pattern allows to add as many processors as necessary by plugin them into th e spring configuration and associating them to content types in the engine confi guration. This tutorial is available for download with svn: svn checkout http://raulrajatutorials.googlecode.com/svn/trunk/ raulrajatutorial s-read-only Comments in the code itself should be self-explanatory if you already have some java + spring experience. And now to the point. First a video of the actual code at work and then the code applicationContext.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.or g/schema/beans/spring-beans-2.0.xsd" default-autowire="byName"> <!-- thumbnail generator engine --> <bean id="thumbnailGeneratorEngine" class="com.raulraja.util.thumbnails.impl .ThumbnailGeneratorEngineImpl"> <!-- the file extension for the thumbnail files --> <property name="generatedExtension"> <value>.jpg</value> </property> <!-- the different sizes we want to generate, adjusting the aspect ratio based on the biggest dimension --> <property name="supportedSizes"> <list> <value>900</value>
<value>768</value> <value>375</value> <value>128</value> <value>64</value> <value>48</value> <value>22</value> </list> </property> <!-- mappings from the different content types to the right generator th at handles each type --> <property name="thumbnailGenerators"> <map> <entry key="image/jpeg" value-ref="imageThumbnai lGenerator" /> <entry key="image/jpg" value-ref="imageThumbnailGenerator" /> <entry key="image/pjpeg" value-ref="imageThumbnailGenerator" /> <entry key="image/gif" value-ref="imageThumbnailGenerator" /> <entry key="image/png" value-ref="imageThumbnailGenerator" /> <entry key="image/tiff" value-ref="imageThumbnailGenerator" /> <entry key="image/bmp" value-ref="imageThumbnailGenerator" /> <entry key="application/pdf" value-ref="pdfThumbnailGenerator" / > </map> </property> <!-- A default thumbnail generator to be used for unregistered mime type s --> <property name="defaultThumbnailGenerator" ref="imageThumbnailGenerator" /> <!-- location for the generated thumbnails --> <property name="thumbnailsLocation" value="/Users/raul/Pictures/test/"/> </bean> <!-- a thumbnail generator that generates thumbnails from images --> <bean id="imageThumbnailGenerator" class="com.raulraja.util.thumbnails.i mpl.ImageThumbnailGeneratorImpl" autowire="autodetect" /> <!-- a thumbnail generator that generates thumbnails from pdf files not impl emented for simplicity --> <bean id="pdfThumbnailGenerator" class="com.raulraja.util.thumbnails.imp l.PDFThumbnailGeneratorImpl" autowire="autodetect" /> <!-- upload code below for demo purposes --> <bean id="multipartResolver" class="org.springframework.web.multipart.common s.CommonsMultipartResolver"> <!-- one of the properties available; the maximum file size in bytes --> <property name="maxUploadSize" value="50000000"/> </bean> <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleU rlHandlerMapping"> <property name="mappings"> <value> /upload.form=fileUploader </value> </property> <property name="alwaysUseFullPath" value="true" />
</bean> <bean id="fileUploader" class="com.raulraja.util.upload.FileUploadService" a utowire="autodetect" scope="session"> <property name="commandClass" value="com.raulraja.util.upload.FileUpload "/> <property name="formView" value="uploadFile.jsp"/> <property name="successView" value="success.jsp" /> </bean> </beans> web.xml (for demo purposes) <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <description>method-profiling-with-spring</description> <display-name>method-profiling-with-spring</display-name> <!-- Spring config --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/conf/applicationContext.xml </param-value> </context-param> <context-param> <param-name>log4jConfigLocation</param-name> <param-value>/WEB-INF/conf/log4j.properties</param-value> </context-param> <!-- Spring Listeners --> <listener> <listener-class>org.springframework.web.util.Log4jConfigListener</listen er-class> </listener> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</l istener-class> </listener> <listener> <listener-class>org.springframework.web.context.request.RequestContextLi stener</listener-class> </listener> <servlet> <servlet-name>springDispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servle t-class> <load-on-startup>1</load-on-startup> </servlet> <!-- maps the dispatcher to *.form -->
<servlet-mapping> <servlet-name>springDispatcher</servlet-name> <url-pattern>*.form</url-pattern> </servlet-mapping> </web-app> uploadFile.jsp (for demo purposes) <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <html> <head> <title></title> </head> <body> <form action="upload.form" enctype="multipart/form-data" method="post"> <input type="file" name="file" id="file" /> <input type="submit" value="Upload"/> </form> </body> </html> success.jsp (for demo purposes) <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title></title> </head> <body> sucess!!! </body> </html> ThumbnailGeneratorEngine.java (This is the interface and entry point for services to ask for thumbnails to be generated) package com.raulraja.util.thumbnails; import java.io.InputStream; /** * An engine in charge of generating thumbnails for files */ public interface ThumbnailGeneratorEngine { /** * @param fileNamePrefix the prefix for the generated thumbnails * @param inputStream the stream to generate thumbnails for * @param contentType the content type of this input stream for exampl e image/jpeg */ void generateThumbnails(String fileNamePrefix, InputStream inputStream, String contentType); } ThumbnailGeneratorImpl.java (The implementation for the thumbnail generator engine that is configured in the
spring configuration) package com.raulraja.util.thumbnails.impl; import com.raulraja.util.thumbnails.ThumbnailGenerator; import com.raulraja.util.thumbnails.ThumbnailGeneratorEngine; import org.apache.log4j.Logger; import import import import java.io.File; java.io.InputStream; java.util.List; java.util.Map;
/** * Default impl for the Thumbnail generator engine */ public class ThumbnailGeneratorEngineImpl implements ThumbnailGeneratorEngine { private final static Logger log = Logger.getLogger(ThumbnailGeneratorEng ineImpl.class); private String generatedExtension; /** * @param generatedExtension The extension for the generated thumbnails */ public void setGeneratedExtension(String generatedExtension) { this.generatedExtension = generatedExtension; } public String getGeneratedExtension() { return generatedExtension; } private Map<String, ThumbnailGenerator> thumbnailGenerators; /** * @param thumbnailGenerators The thumbnail generators known by this eng ine mapped to a content type */ public void setThumbnailGenerators(Map<String, ThumbnailGenerator> thumb nailGenerators) { this.thumbnailGenerators = thumbnailGenerators; } private List<Integer> supportedSizes; /** * @param supportedSizes The suported sizes for the batch of generated t humbs */ public void setSupportedSizes(List<Integer> supportedSizes) { this.supportedSizes = supportedSizes; } private ThumbnailGenerator defaultThumbnailGenerator; /** * @param defaultThumbnailGenerator the default thumbnail generator to b e used for unregistered mime types
*/ public void setDefaultThumbnailGenerator(ThumbnailGenerator defaultThumb nailGenerator) { this.defaultThumbnailGenerator = defaultThumbnailGenerator; } private String thumbnailsLocation; /** * @param thumbnailsLocation location for the generated thumbnails */ public void setThumbnailsLocation(String thumbnailsLocation) { this.thumbnailsLocation = thumbnailsLocation; } /** * @param fileNamePrefix the prefix for the generated thumbnails * @param inputStream the stream to generate thumbnails for * @param contentType the content type of this input stream for exampl e image/jpeg */ public void generateThumbnails(String fileNamePrefix, InputStream inputS tream, String contentType) { ThumbnailGenerator thumbnailGenerator = thumbnailGenerators.get( contentType); thumbnailGenerator = thumbnailGenerator != null ? thumbnailGener ator : defaultThumbnailGenerator; if (thumbnailGenerator != null) { Object hint = null; for (int dimension : supportedSizes) { File fileOut = new File(thumbnailsLocation, file NamePrefix + "_" + dimension + generatedExtension); try { hint = thumbnailGenerator.createThumbnai l(inputStream, fileOut, dimension, hint); log.debug("Generated thumbnail for: " + inputStream + " in " + fileOut + " for type " + contentType); } catch (Exception e) { log.error("Error generating thumbnail fo r: " + inputStream + " in " + fileOut + " for type " + contentType, e); } } } else { log.warn("Thumbnail generator not found for content type : " + contentType + " and no default generator was provided"); } } } ThumbnailGenerator.java (This is the interface the different generators implement) package com.raulraja.util.thumbnails; import java.io.File; import java.io.InputStream; import java.io.IOException;
/** * Interface implemented by all thumbnail generators. * * <p>The thumbnail generation process is always performed in order with the lar gest thumbnails first. * Therefore it may be very handy for the generator to create smaller thumbnails using the results from * the prior iteration instead of always using the full-sized image as the sourc e. To accomplish this, * the generator can return a "hint" object that it can use in subsequent iterat ions, containing whatever * helpers it might want (such as the prior image already loaded in memory, etc) . */ public interface ThumbnailGenerator {
/** * Create the thumbnail. The thumbnail should always save as a JPEG file. * * @param inputStream The source data. * @param fileOut The output file. * @param largestDimension The max width and height. The generator should size the thumbnail so * that the width and height both stay within this limit. * @param hint Optional hint that was returned from the prior t humbnail generation * on this same file, null if none was returned or if this is the first * thumbnail in this context. * * @return an optional hint object that will be passed to subsequent thumbna il generation calls * for this same source data. Return null if you don't use hints, o therwise return some * object which allows you to communicate extra information to the n ext round, such as * the scaled image already loaded. * @throws java.io.IOException if something goes wrong handling the io */ public Object createThumbnail(InputStream inputStream, File fileOut, int largestDimension, Object hint) throws IOException; } AbstractThumbnailGenerator.java (An abstract class with a utility method to save images as jpeg for all thumbnai l generators to extend) package com.raulraja.util.thumbnails.impl; import com.sun.image.codec.jpeg.JPEGCodec; import com.sun.image.codec.jpeg.JPEGImageEncoder; import org.apache.log4j.Logger; import java.awt.image.BufferedImage;
/** * Class providing convenience method for abstract thumbnail generators */ public class AbstractThumbnailGenerator { private final static Logger log = Logger.getLogger(AbstractThumbnailGene rator.class); * * * * * * m */ public boolean saveImageAsJPEG(BufferedImage image, File fileOut) throws IOE xception { OutputStream streamOut = null; boolean bSuccess = false; try { streamOut = new FileOutputStream(fileOut); JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(streamOut); encoder.encode(image); bSuccess = true; } catch (Throwable t) { log.warn("Files.saveImageAsJPEG(" + fileOut + "): " + t, t); } finally { if (streamOut != null) { streamOut.close(); } } return bSuccess; } } ImageThumbnailGeneratorImpl.java (A concrete implementation a thumbnail generator that is handling multiple image content types) package com.raulraja.util.thumbnails.impl; import com.raulraja.util.thumbnails.ThumbnailGenerator; import org.apache.log4j.Logger; import javax.imageio.ImageIO; /** Save an image as a JPEG file on disk. @param image @param fileOut The raw image to save. The location where you want to save the file.
@return true if successful, false if unsuccessful. * @throws java.io.IOException if something goes wrong closing the strea
/** * Generate thumbnails for images. */ public class ImageThumbnailGeneratorImpl extends AbstractThumbnailGenerator impl ements ThumbnailGenerator { private final static Logger log = Logger.getLogger(ImageThumbnailGeneratorIm pl.class); public Object createThumbnail(InputStream inputStream, File fileOut, int largestDimension, Object hint) throws IOException { // What's the base image that we are starting with? If there's a hint, that's the scaled image // from the last time around, use that... (since we know we always itera te downwards in scale) Image imageIn; if (hint instanceof Image) { imageIn = (Image) hint; log.info("createThumbnail(" + fileOut + ") reusing prior result imag e..."); } else { log.info("createThumbnail(" + fileOut + ") reading image from stream " + inputStream); imageIn = ImageIO.read(inputStream); } if (imageIn == null) { log.warn("Could not read image file: " + inputStream); return hint; } BufferedImage imageOut = createThumbnailImage(imageIn, fileOut, largestD imension); // Return this image now as the hint for the next scaling iteration if (imageOut != null) hint = imageOut; return hint; } /** * Create a thumbnail image and save it to disk. * * This algorithm is based on: * http://www.philreeve.com/java_high_quality_thumbnails.php * * @param imageIn The image you want to scale.
* @param fileOut The output file. * @param largestDimension The largest dimension, so that neither the width nor height * will exceed this value. * * @return the image that was created, null if imageIn or fileOut is null. * @throws java.io.IOException if something goes wrong when saving as jp eg */ public BufferedImage createThumbnailImage(Image imageIn, File fileOut, int l argestDimension) throws IOException { if ((imageIn == null) || (fileOut == null)) return null; //it seems to not return the right size until the methods get called for the first time imageIn.getWidth(null); imageIn.getHeight(null); // Find int int int double int biggest dimension nImageWidth = imageIn.getWidth(null); nImageHeight = imageIn.getHeight(null); nImageLargestDim = Math.max(nImageWidth, nImageHeight); scale = (double) largestDimension / (double) nImageLargestDim; sizeDifference = nImageLargestDim - largestDimension;
//create an image buffer to draw to BufferedImage imageOut = new BufferedImage(100, 100, BufferedImage.TYPE_ INT_RGB); // 8-bit RGB Graphics2D g2d; AffineTransform tx; // Use a few steps if the sizes are drastically different, and only scal e // if the desired size is smaller than the original. int numSteps = 0; if (scale < 1.0d) { // Make sure we have at least 1 step numSteps = Math.max(1, (sizeDifference / 100)); } if (numSteps > 0) { int stepSize = sizeDifference / numSteps; int stepWeight = stepSize / 2; int heavierStepSize = stepSize + stepWeight; int lighterStepSize = stepSize - stepWeight; int currentStepSize, centerStep; double scaledW = imageIn.getWidth(null); double scaledH = imageIn.getHeight(null); if ((numSteps % 2) == 1) //if there's an odd number of steps centerStep = (int) Math.ceil((double) numSteps / 2d); //find the center step else centerStep = -1; //set it to -1 so it's ignored later Integer intermediateSize; Integer previousIntermediateSize = nImageLargestDim; for (Integer i = 0; i < numSteps; i++) {
if (i + 1 != centerStep) { //if this isn't the center step if (i == numSteps - 1) { //if this is the last step //fix the stepsize to account for decimal place errors p reviously currentStepSize = previousIntermediateSize - largestDime nsion; } else { if (numSteps - i > numSteps / 2) //if we're in the first half of the reductions currentStepSize = heavierStepSize; else currentStepSize = lighterStepSize; } } else { //center step, use natural step size currentStepSize = stepSize; } intermediateSize = previousIntermediateSize - currentStepSize; scale = intermediateSize / (double) previousIntermediateSize; scaledW = Math.max((int)(scaledW * scale), 1); scaledH = Math.max((int)(scaledH * scale), 1); log.info("step " + i + ": scaling to " + scaledW + " x " + scale dH); imageOut = new BufferedImage((int) scaledW, (int) scaledH, Buffe redImage.TYPE_INT_RGB); // 8 bit RGB g2d = imageOut.createGraphics(); g2d.setBackground(Color.WHITE); g2d.clearRect(0, 0, imageOut.getWidth(), imageOut .getHeight()); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHint s.VALUE_RENDER_QUALITY); tx = new AffineTransform(); tx.scale(scale, scale); g2d.drawImage(imageIn, tx, null); g2d.dispose(); imageIn = new ImageIcon(imageOut).getImage(); previousIntermediateSize = intermediateSize; } } else { // This enforces a rule that we always have an 8-bit image with whit e background for the thumbnail. Plus, for large // images, this makes subsequent downscaling really fast because we are working on a large 8-bit image // instead of a large 12 or 24 bit image, so the downstream effect i s very noticable. imageOut = new BufferedImage(imageIn.getWidth(null), imageIn.getHeig ht(null), BufferedImage.TYPE_INT_RGB); g2d = imageOut.createGraphics(); g2d.setBackground(Color.WHITE); g2d.clearRect(0, 0, imageOut.getWidth(), imageOut.getHeight()); tx = new AffineTransform(); tx.setToIdentity(); //use identity matrix so image is copied exactly g2d.drawImage(imageIn, tx, null);
g2d.dispose(); } // JPEG-encode the image and write to file. saveImageAsJPEG(imageOut, fileOut); return imageOut; } } PDFThumbnailGeneratorImpl.java (An empty stub impl for a PDF based thumbnail generator) package com.raulraja.util.thumbnails.impl; import com.raulraja.util.thumbnails.ThumbnailGenerator; import java.io.InputStream; import java.io.File; import java.io.IOException; /** * Empty impl for generating thumbnails for pdfs */ public class PDFThumbnailGeneratorImpl implements ThumbnailGenerator { /** * Create the thumbnail. The thumbnail should always save as a JPEG fil e. * * @param inputStream The source data. * @param fileOut The output file. * @param largestDimension The max width and height. The generator shou ld size the thumbnail so * that the width and height both stay within th is limit. * @param hint Optional hint that was returned from th e prior thumbnail generation * on this same file, null if none was returned or if this is the first * thumbnail in this context. * @return an optional hint object that will be passed to subsequent thu mbnail generation calls * for this same source data. Return null if you don't use hint s, otherwise return some * object which allows you to communicate extra information to t he next round, such as * the scaled image already loaded. */ public Object createThumbnail(InputStream inputStream, File fileOut, int largestDimension, Object hint) throws IOException { throw new UnsupportedOperationException("thumbnails for pdf's no t implemented in this tutorial..."); } } FileUploadService.java (The service that demonstrates how the ThumbnailGeneratorEngine can be injected as interface and used to handle streams and content types for thumbnails to be g
enerated without actually knowing the content type or stream type. This is here for demo purposes but you can inject the engine into any other service not based on file uploads) package com.raulraja.util.upload; import import import import import import import import import import import import com.raulraja.util.thumbnails.ThumbnailGeneratorEngine; org.apache.log4j.Logger; org.springframework.validation.BindException; org.springframework.web.bind.ServletRequestDataBinder; org.springframework.web.multipart.MultipartFile; org.springframework.web.multipart.support.ByteArrayMultipartFileEditor; org.springframework.web.servlet.ModelAndView; org.springframework.web.servlet.mvc.SimpleFormController; javax.servlet.ServletException; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; java.io.InputStream;
/** * File upload service to demonstrate files uploads and handing over the files t o the thumbnail generation system */ public class FileUploadService extends SimpleFormController { private final static Logger log = Logger.getLogger(FileUploadService.cla ss); private ThumbnailGeneratorEngine thumbnailGeneratorEngine; public void setThumbnailGeneratorEngine(ThumbnailGeneratorEngine thumbna ilGeneratorEngine) { this.thumbnailGeneratorEngine = thumbnailGeneratorEngine; } protected ModelAndView onSubmit(HttpServletRequest request, HttpServletR esponse response, Object command, BindException errors) throws Exception { FileUpload fileUpload = (FileUpload) command; MultipartFile file = fileUpload.getFile(); InputStream stream = file.getInputStream(); try { //generate thumbnails thumbnailGeneratorEngine.generateThumbnails(file.getName (), stream, file.getContentType()); } catch (Exception e) { log.error(e); } finally { stream.close(); } return super.onSubmit(request, response, command, errors); } protected void initBinder(HttpServletRequest request, ServletRequestData Binder binder) throws ServletException { // to actually be able to convert Multipart instance to byte[] // we have to register a custom editor binder.registerCustomEditor(byte[].class, new ByteArrayMultipart FileEditor());
// now Spring knows how to handle multipart object and convert t hem } } FileUpload.java (A simple bean that encapsulates the File reference at the time of uploading a f ile) package com.raulraja.util.upload; import org.springframework.web.multipart.MultipartFile; /** * A file upload object */ public class FileUpload { private MultipartFile file; public MultipartFile getFile() { return file; } public void setFile(MultipartFile file) { this.file = file; } }