Coverage Report - net.sourceforge.pebble.decorator.ThumbnailDecorator
 
Classes in this File Line Coverage Branch Coverage Complexity
ThumbnailDecorator
0%
0/119
0%
0/46
8.2
 
 1  
 /*
 2  
  * Copyright (c) 2003-2011, Simon Brown
 3  
  * All rights reserved.
 4  
  *
 5  
  * Redistribution and use in source and binary forms, with or without
 6  
  * modification, are permitted provided that the following conditions are met:
 7  
  *
 8  
  *   - Redistributions of source code must retain the above copyright
 9  
  *     notice, this list of conditions and the following disclaimer.
 10  
  *
 11  
  *   - Redistributions in binary form must reproduce the above copyright
 12  
  *     notice, this list of conditions and the following disclaimer in
 13  
  *     the documentation and/or other materials provided with the
 14  
  *     distribution.
 15  
  *
 16  
  *   - Neither the name of Pebble nor the names of its contributors may
 17  
  *     be used to endorse or promote products derived from this software
 18  
  *     without specific prior written permission.
 19  
  *
 20  
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 21  
  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 22  
  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 23  
  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 24  
  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 25  
  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 26  
  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 27  
  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 28  
  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 29  
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 30  
  * POSSIBILITY OF SUCH DAMAGE.
 31  
  */
 32  
 
 33  
 package net.sourceforge.pebble.decorator;
 34  
 
 35  
 import java.awt.RenderingHints;
 36  
 import java.awt.geom.AffineTransform;
 37  
 import java.awt.image.AffineTransformOp;
 38  
 import java.awt.image.BufferedImage;
 39  
 import java.io.File;
 40  
 import java.io.IOException;
 41  
 import java.util.HashMap;
 42  
 import java.util.Iterator;
 43  
 import java.util.Map;
 44  
 import net.sourceforge.pebble.domain.BlogEntry;
 45  
 import net.sourceforge.pebble.domain.StaticPage;
 46  
 import net.sourceforge.pebble.api.decorator.ContentDecoratorContext;
 47  
 import java.util.regex.Matcher;
 48  
 import java.util.regex.Pattern;
 49  
 import javax.imageio.IIOImage;
 50  
 import javax.imageio.ImageIO;
 51  
 import javax.imageio.ImageWriteParam;
 52  
 import javax.imageio.ImageWriter;
 53  
 import javax.imageio.stream.FileImageOutputStream;
 54  
 import net.sourceforge.pebble.PluginProperties;
 55  
 import net.sourceforge.pebble.domain.Blog;
 56  
 
 57  
 /**
 58  
  * Converts <thumbnail> tags into image thumbnails.
 59  
  * @author Alan Burlison
 60  
  */
 61  0
 public class ThumbnailDecorator extends ContentDecoratorSupport {
 62  
 
 63  
     /**
 64  
      * Decorates the specified blog entry.
 65  
      *
 66  
      * @param context   the context in which the decoration is running
 67  
      * @param blogEntry the blog entry to be decorated
 68  
      */
 69  
     @Override
 70  
     public void decorate(ContentDecoratorContext context, BlogEntry blogEntry) {
 71  0
         Blog blog = blogEntry.getBlog();
 72  0
         blogEntry.setBody(replaceTags(blogEntry.getBody(), blog));
 73  0
         blogEntry.setExcerpt(replaceTags(blogEntry.getExcerpt(), blog));
 74  
 
 75  0
     }
 76  
 
 77  
     /**
 78  
      * Decorates the specified static page.
 79  
      *
 80  
      * @param context    the context in which the decoration is running
 81  
      * @param staticPage the static page to be decorated
 82  
      */
 83  
     @Override
 84  
     public void decorate(ContentDecoratorContext context,
 85  
       StaticPage staticPage) {
 86  0
         staticPage.setBody(replaceTags(staticPage.getContent(),
 87  
           staticPage.getBlog()));
 88  0
     }
 89  
 
 90  
     /**
 91  
      * Find all the thumbnail tags and replace them with the equivalent HTML.
 92  
      * @param content the page content
 93  
      * @param blog the current blog.
 94  
      * @return the decorated content.
 95  
      */
 96  
     private String replaceTags(String content, Blog blog) {
 97  
 
 98  
         // Check there is something to do.
 99  0
         if (content == null || content.length() == 0) {
 100  0
             return "";
 101  
         }
 102  
 
 103  
         // Establish default sizes.
 104  0
         int defaultThumbSize = 200;
 105  0
         int defaultPopupSize = 640;
 106  0
         PluginProperties props = blog.getPluginProperties();
 107  0
         if (props.hasProperty(thumbSizeProp)) {
 108  
             try {
 109  0
                 defaultThumbSize =
 110  
                   Integer.parseInt(props.getProperty(thumbSizeProp));
 111  0
             } catch (NumberFormatException nfe) {
 112  
                 // Ignore.
 113  0
             }
 114  
         }
 115  0
         if (props.hasProperty(popupSizeProp)) {
 116  
             try {
 117  0
                 defaultPopupSize =
 118  
                   Integer.parseInt(props.getProperty(popupSizeProp));
 119  0
             } catch (NumberFormatException nfe) {
 120  
                 // Ignore.
 121  0
             }
 122  
         }
 123  
 
 124  
         // Find all the thumbnail tags.
 125  0
         Matcher tagM = tagRE.matcher(content);
 126  0
         StringBuffer sb = new StringBuffer(content.length());
 127  0
         while (tagM.find()) {
 128  0
             StringBuilder repl = new StringBuilder();
 129  0
             Matcher attrM = attrRE.matcher(tagM.group(1));
 130  0
             String src = null;
 131  0
             String alt = "";
 132  0
             int thumbSize = defaultThumbSize;
 133  0
             int popupSize = defaultPopupSize;
 134  
 
 135  
             // Parse the attributes of the tag.
 136  0
             while (attrM.find()) {
 137  0
                 String an = attrM.group(1);
 138  0
                 String av = attrM.group(2);
 139  0
                 if (an.equalsIgnoreCase("src")) {
 140  0
                     src = av;
 141  0
                 } else if (an.equalsIgnoreCase("alt")) {
 142  0
                     alt = av;
 143  0
                 } else if (an.equalsIgnoreCase("thumbSize")) {
 144  
                     try {
 145  0
                         thumbSize = Integer.parseInt(av);
 146  0
                     } catch (NumberFormatException e) {
 147  0
                         tagM.appendReplacement(sb, String.format(
 148  
                           "<!-- ERROR: invalid thumbnail thumbSize \"%s\" -->",
 149  
                           av));
 150  0
                     }
 151  0
                 } else if (an.equalsIgnoreCase("popupSize")) {
 152  
                     try {
 153  0
                         popupSize = Integer.parseInt(av);
 154  0
                     } catch (NumberFormatException e) {
 155  0
                         tagM.appendReplacement(sb, String.format(
 156  
                           "<!-- ERROR: invalid thumbnail popupSize \"%s\" -->",
 157  
                           av));
 158  0
                     }
 159  
                 }
 160  0
             }
 161  
 
 162  
             // Replace the tag with the equivalent HTML.
 163  0
             if (src == null) {
 164  0
                 tagM.appendReplacement(sb, String.format(
 165  
                     "<!-- ERROR: missing thumbnail src -->"));
 166  
             } else {
 167  0
                 tagM.appendReplacement(sb,
 168  
                   renderTag(new File(src), thumbSize, popupSize, alt, blog));
 169  
             }
 170  0
         }
 171  0
         tagM.appendTail(sb);
 172  0
         return sb.toString();
 173  
     }
 174  
 
 175  
     /**
 176  
      * Render a thumbnail tag into the equivalent HTML, also creating and
 177  
      * caching the thumbnail as appropriate.
 178  
      * @param src image source.
 179  
      * @param thumbSize required thumbmail size, maximum dimension.
 180  
      * @param popupSize required popup size, maximum dimension.
 181  
      * @param alt alternate text for link.
 182  
      * @param blog the current blog.
 183  
      * @return the equivalent HTML.
 184  
      */
 185  
     private String renderTag(File src, int thumbSize, int popupSize,
 186  
       String alt, Blog blog) {
 187  
 
 188  
         // Check the image exists.
 189  0
         File img = new File(blog.getRoot(), src.getPath());
 190  0
         if (! img.isFile()) {
 191  0
             return String.format(
 192  
               "<!-- ERROR: invalid thumbnail src \"%s\" -->", src);
 193  
         }
 194  
 
 195  
         // Check the thumbnail directory exists.
 196  0
         File thumbDir = new File(img.getParent(), "thumbnails");
 197  0
         if (!thumbDir.isDirectory()) {
 198  0
             if (!thumbDir.mkdir()) {
 199  0
                 return String.format(
 200  
                   "<!-- ERROR: can't create thumbnail directory -->",
 201  
                   thumbDir.toString());
 202  
             }
 203  
         }
 204  
 
 205  
         // Check that an up-to-date thumbnail exists.
 206  0
         File thumb = new File(thumbDir, img.getName());
 207  0
         if (!thumb.isFile() || thumb.lastModified() < img.lastModified()) {
 208  0
             if (! createThumbnail(img, thumb, thumbSize)) {
 209  0
                 return String.format(
 210  
                   "<!-- ERROR: can't create thumbnail \"%s\" -->", thumb);
 211  
             }
 212  
         }
 213  
 
 214  
         // Build the replacement HTML & return it.
 215  0
         File tsrc = new File(new File(src.getParent(), "thumbnails"),
 216  
           src.getName());
 217  0
         return String.format("<a href=\"%1$s\" onclick=\"window.open(" +
 218  
           "'%1$s','popup','width=%4$d,height=%4$d,toolbar=no,directories=no," +
 219  
           "location=no,menubar=no,status=no'); return false\" " +
 220  
           "class=\"thumbnailLink\"><img src=\"%2$s\" alt=\"%3$s\" " +
 221  
           "class=\"thumbnailImage\"/></a>",
 222  
           src, tsrc, alt, popupSize);
 223  
     }
 224  
 
 225  
     /**
 226  
      * Create a new thumbnail from an image.
 227  
      * @param imgFile the image to thumbnail.
 228  
      * @param thumbFile the thumbnail to create.
 229  
      * @param thumbSize the maximum dimension of the thumbnail.
 230  
      * @return true if the thumbnail was created successfully.
 231  
      */
 232  
     public boolean createThumbnail(File imgFile, File thumbFile,
 233  
       int thumbSize) {
 234  
         // Work out the image file suffix.
 235  0
         String suffix = imgFile.getName();
 236  0
         int dot = suffix.lastIndexOf('.');
 237  0
         if (dot < 1) {
 238  0
             return false;
 239  
         }
 240  0
         suffix = suffix.substring(dot + 1);
 241  
 
 242  
         try {
 243  
             // Read in the image.
 244  0
             BufferedImage img = ImageIO.read(imgFile);
 245  0
             if (img == null) {
 246  0
                 return false;
 247  
             }
 248  
 
 249  
             // Calcuate the scaling.
 250  0
             int ih = img.getHeight();
 251  0
             int iw = img.getWidth();
 252  
             int thumbH, thumbW;
 253  
             float scale;
 254  0
             if (iw > ih) {
 255  0
                 scale = (float) thumbSize / (float) iw;
 256  0
                 thumbW = thumbSize;
 257  0
                 thumbH = Math.round(ih * scale);
 258  
             } else {
 259  0
                 scale = (float) thumbSize / (float) ih;
 260  0
                 thumbH = thumbSize;
 261  0
                 thumbW = Math.round(iw * scale);
 262  
             }
 263  
 
 264  
             // Scale the image.
 265  0
             BufferedImage thumb =
 266  
               new BufferedImage(thumbW, thumbH, img.getType());
 267  0
             AffineTransform at =
 268  
               AffineTransform.getScaleInstance(scale, scale);
 269  0
             AffineTransformOp ato = new AffineTransformOp(at, renderHints);
 270  0
             ato.filter(img, thumb);
 271  
 
 272  
             // Save the image.
 273  0
             Iterator<ImageWriter> iwi =
 274  
               ImageIO.getImageWritersBySuffix(suffix);
 275  0
             if (! iwi.hasNext()) {
 276  0
                 return false;
 277  
             }
 278  0
             ImageWriter thumbWriter = iwi.next();
 279  0
             ImageWriteParam iwp = thumbWriter.getDefaultWriteParam();
 280  0
             if (iwp.canWriteCompressed()) {
 281  0
                 String ct[] = iwp.getCompressionTypes();
 282  0
                 if (ct != null) {
 283  0
                     iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
 284  0
                     iwp.setCompressionType(ct[0]);
 285  0
                     iwp.setCompressionQuality(0.8F);
 286  
                 }
 287  
             }
 288  0
             FileImageOutputStream thumbOut =
 289  
               new FileImageOutputStream(thumbFile);
 290  0
             thumbWriter.setOutput(thumbOut);
 291  0
             thumbWriter.write(null, new IIOImage(thumb, null, null), iwp);
 292  0
             thumbWriter.dispose();
 293  0
             thumbOut.close();
 294  
             
 295  0
         } catch (IOException ex) {
 296  0
             return false;
 297  0
         }
 298  0
         return true;
 299  
     }
 300  
     
 301  
     /** Default thumbnail size property name. */
 302  
     private static final String thumbSizeProp = "Thumbnail.thumbnailSize";
 303  
     /** Default popup window size property name. */
 304  
     private static final String popupSizeProp = "Thumbnail.popupSize";
 305  
     /** RE for matching thumbnail tags. */
 306  0
     private static Pattern tagRE = Pattern.compile(
 307  
       "<thumbnail\\s+(.+?)\\s*/>",
 308  
       Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
 309  
     /** RE for matching tag attributes. */
 310  0
     private static Pattern attrRE = Pattern.compile(
 311  
       "([\\w_-]+)\\s*=\\s*\"([^\"]+)\"",
 312  
       Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
 313  
     /** Image transform hints. */
 314  
     private static final RenderingHints renderHints;
 315  
 
 316  
     // Initialise rendering hints.
 317  
     static {
 318  0
         Map<RenderingHints.Key, Object> hintMap =
 319  
           new HashMap<RenderingHints.Key, Object>();
 320  0
         hintMap.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
 321  
           RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
 322  0
         hintMap.put(RenderingHints.KEY_ANTIALIASING,
 323  
           RenderingHints.VALUE_ANTIALIAS_ON);
 324  0
         hintMap.put(RenderingHints.KEY_COLOR_RENDERING,
 325  
           RenderingHints.VALUE_COLOR_RENDER_QUALITY);
 326  0
         hintMap.put(RenderingHints.KEY_DITHERING,
 327  
           RenderingHints.VALUE_DITHER_DISABLE);
 328  0
         hintMap.put(RenderingHints.KEY_INTERPOLATION,
 329  
           RenderingHints.VALUE_INTERPOLATION_BILINEAR);
 330  0
         hintMap.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
 331  
           RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
 332  0
         hintMap.put(RenderingHints.KEY_COLOR_RENDERING,
 333  
           RenderingHints.VALUE_COLOR_RENDER_QUALITY);
 334  0
         renderHints = new RenderingHints(hintMap);
 335  0
     }
 336  
 }