La intención de este articulo es mostrar como generar imágenes en miniatura (thumbnails) con Java y Spring.
La generación de miniatura esta basada en el algoritmo creado por Phil Reeve .
Jim McCabe también contribuyo a mejorar el algoritmo para no tener que evaluar la imagen original de forma que las miniaturas son ordenadas y generadas desde el tamaño mayor al menor utilizando el ultimo tamaño generado y de esa forma evaluando imágenes mas pequeñas en cada paso.
El codigo incluye un formulario de subida de archivos para demostrar la generación de las miniaturas. EL servicio ThumbnailGeneratorEngine puede ser usado fuera de ese contexto en cualquier otro servicio en tu configuración de Spring.
Este articulo incluye el patrón Motor / Procesador, el cual se describe aqui.
El motor ThumbnailGeneratorEngine se puede configurar con procesadores basado en los tipos de contenido (content-type) para acomodar diferentes tipos de archivo o streams.
En el ejemplo se muestra como el procesador de imágenes se utiliza para archivo de imágenes y se incluye una implementacion vacía para un procesador que actúa sobre tipos de contenido PDF, pero con este mismo patrón puedes configurar procesadores para otros tipos de contenido como word, excel, urls, etc…
El código de este articulo y el de muchos otros esta disponible para descargar con svn en :
svn checkout http://raulrajatutorials.googlecode.com/svn/trunk/ raulrajatutorials-read-only
Y ahora al grano…
Primero un vídeo del código en acción y luego el código
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.org/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 that handles each type --> <property name="thumbnailGenerators"> <map> <entry key="image/jpeg" value-ref="imageThumbnailGenerator" /> <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 types --> <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.impl.ImageThumbnailGeneratorImpl" autowire="autodetect" /> <!-- a thumbnail generator that generates thumbnails from pdf files not implemented for simplicity --> <bean id="pdfThumbnailGenerator" class="com.raulraja.util.thumbnails.impl.PDFThumbnailGeneratorImpl" autowire="autodetect" /> <!-- upload code below for demo purposes --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.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.SimpleUrlHandlerMapping"> <property name="mappings"> <value> /upload.form=fileUploader </value> </property> <property name="alwaysUseFullPath" value="true" /> </bean> <bean id="fileUploader" class="com.raulraja.util.upload.FileUploadService" autowire="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 (solo para demostración)
<?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</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener> <servlet> <servlet-name>springDispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-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 (solo para demostración)
<%@ 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 (solo para demostración)
<!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
(Este es el interfaz y punto de entrada que otros servicios utilizan para invocar la generación de miniaturas)
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 example image/jpeg */ void generateThumbnails(String fileNamePrefix, InputStream inputStream, String contentType); }
ThumbnailGeneratorImpl.java
(Esta es la implementacion del interfaz a la cual la configuracion de Spring hace referencia)
package com.raulraja.util.thumbnails.impl; import com.raulraja.util.thumbnails.ThumbnailGenerator; import com.raulraja.util.thumbnails.ThumbnailGeneratorEngine; import org.apache.log4j.Logger; import java.io.File; import java.io.InputStream; import java.util.List; import java.util.Map; /** * Default impl for the Thumbnail generator engine */ public class ThumbnailGeneratorEngineImpl implements ThumbnailGeneratorEngine { private final static Logger log = Logger.getLogger(ThumbnailGeneratorEngineImpl.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 engine mapped to a content type */ public void setThumbnailGenerators(Map<String, ThumbnailGenerator> thumbnailGenerators) { this.thumbnailGenerators = thumbnailGenerators; } private List<Integer> supportedSizes; /** * @param supportedSizes The suported sizes for the batch of generated thumbs */ public void setSupportedSizes(List<Integer> supportedSizes) { this.supportedSizes = supportedSizes; } private ThumbnailGenerator defaultThumbnailGenerator; /** * @param defaultThumbnailGenerator the default thumbnail generator to be used for unregistered mime types */ public void setDefaultThumbnailGenerator(ThumbnailGenerator defaultThumbnailGenerator) { 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 example image/jpeg */ public void generateThumbnails(String fileNamePrefix, InputStream inputStream, String contentType) { ThumbnailGenerator thumbnailGenerator = thumbnailGenerators.get(contentType); thumbnailGenerator = thumbnailGenerator != null ? thumbnailGenerator : defaultThumbnailGenerator; if (thumbnailGenerator != null) { Object hint = null; for (int dimension : supportedSizes) { File fileOut = new File(thumbnailsLocation, fileNamePrefix + "_" + dimension + generatedExtension); try { hint = thumbnailGenerator.createThumbnail(inputStream, fileOut, dimension, hint); log.debug("Generated thumbnail for: " + inputStream + " in " + fileOut + " for type " + contentType); } catch (Exception e) { log.error("Error generating thumbnail for: " + 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
(Este es el interfaz que los procesadores implementan)
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 largest 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 source. To accomplish this, * the generator can return a "hint" object that it can use in subsequent iterations, 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 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 thumbnail generation calls * for this same source data. Return null if you don't use hints, otherwise return some * object which allows you to communicate extra information to the next 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
(Una clase abstracta con metodos para guardar los archivos en formato jpg)
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; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.io.IOException; /** * Class providing convenience method for abstract thumbnail generators */ public class AbstractThumbnailGenerator { private final static Logger log = Logger.getLogger(AbstractThumbnailGenerator.class); /** * Save an image as a JPEG file on disk. * * @param image The raw image to save. * @param fileOut 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 stream */ public boolean saveImageAsJPEG(BufferedImage image, File fileOut) throws IOException { 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
(Una de las implementaciones del generador de miniaturas que es configurada para ser utilizada con diferentes tipos de contenido basados en imágenes)
package com.raulraja.util.thumbnails.impl; import com.raulraja.util.thumbnails.ThumbnailGenerator; import org.apache.log4j.Logger; import javax.imageio.ImageIO; import javax.swing.*; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.InputStream; /** * Generate thumbnails for images. */ public class ImageThumbnailGeneratorImpl extends AbstractThumbnailGenerator implements ThumbnailGenerator { private final static Logger log = Logger.getLogger(ImageThumbnailGeneratorImpl.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 iterate downwards in scale) Image imageIn; if (hint instanceof Image) { imageIn = (Image) hint; log.info("createThumbnail(" + fileOut + ") reusing prior result image..."); } 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, largestDimension); // 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 jpeg */ public BufferedImage createThumbnailImage(Image imageIn, File fileOut, int largestDimension) 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 biggest dimension int nImageWidth = imageIn.getWidth(null); int nImageHeight = imageIn.getHeight(null); int nImageLargestDim = Math.max(nImageWidth, nImageHeight); double scale = (double) largestDimension / (double) nImageLargestDim; int 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 scale // 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 previously currentStepSize = previousIntermediateSize - largestDimension; } 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 " + scaledH); imageOut = new BufferedImage((int) scaledW, (int) scaledH, BufferedImage.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, RenderingHints.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 white 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 is very noticable. imageOut = new BufferedImage(imageIn.getWidth(null), imageIn.getHeight(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
(Una implementacion vacía que se utilizaría para generar miniaturas de PDF)
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 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 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 thumbnail generation calls * for this same source data. Return null if you don't use hints, otherwise return some * object which allows you to communicate extra information to the 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 not implemented in this tutorial..."); } }
FileUploadService.java
(Un servicio que demuestra como llamar al motor)
package com.raulraja.util.upload; import com.raulraja.util.thumbnails.ThumbnailGeneratorEngine; import org.apache.log4j.Logger; import org.springframework.validation.BindException; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.ByteArrayMultipartFileEditor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.SimpleFormController; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.InputStream; /** * File upload service to demonstrate files uploads and handing over the files to the thumbnail generation system */ public class FileUploadService extends SimpleFormController { private final static Logger log = Logger.getLogger(FileUploadService.class); private ThumbnailGeneratorEngine thumbnailGeneratorEngine; public void setThumbnailGeneratorEngine(ThumbnailGeneratorEngine thumbnailGeneratorEngine) { this.thumbnailGeneratorEngine = thumbnailGeneratorEngine; } protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse 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, ServletRequestDataBinder 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 ByteArrayMultipartFileEditor()); // now Spring knows how to handle multipart object and convert them } }
FileUpload.java
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; } }

[...] no real functionality I have used this same pattern to create services that generate thumbnails, process images, extract text from files based on content types, [...]
Very Good !!