diff --git a/album.launch b/album.launch index 07b754c..4424885 100644 --- a/album.launch +++ b/album.launch @@ -8,6 +8,7 @@ + diff --git a/etc/ehcache.xml b/etc/ehcache.xml index b2c8fc6..0e05280 100644 --- a/etc/ehcache.xml +++ b/etc/ehcache.xml @@ -8,7 +8,7 @@ 0 && fDate > 0 && fDate <= reqDate; } - void scaleImage(HttpServletRequest req, HttpServletResponse res, File file, Thumbnail thumbnail, String size) throws IOException { + void procesScaledImageRequest(HttpServletRequest req, HttpServletResponse res, File file, Thumbnail thumbnail, String size) throws IOException { if (notModified(req, file)) { res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); @@ -167,120 +156,42 @@ public class AlbumServlet System.out.println(file.getName() + " not modified (based on etag)"); return; } - res.setDateHeader("Last-Modified", file.lastModified()); - res.setHeader("ETag", fileEtag); - String key = file.getPath() + ":" + size; + + CachedImage cimg = null; Element element = imageCache.get(key); if (element != null) { - CachedImage cimg = (CachedImage) element.getObjectValue(); + cimg = (CachedImage) element.getObjectValue(); if (cimg.lastModified == file.lastModified()) { System.out.println("cache hit on " + key); - //res.setContentType(cimg.mimeType); - res.setContentLength(cimg.bits.length); - res.getOutputStream().write(cimg.bits); - return; } else { System.out.println(" " + key + " has changed so cache entry wil be refreshed"); imageCache.remove(key); + cimg = null; } } - - synchronized (this) { - Dimension orig = thumbnail.getSize(); - Dimension outd; - if (size.endsWith("h")) { - int height = Integer.parseInt(size.substring(0, size.length() - 1)); - outd = orig.scaleHeight(height); - } else if (size.endsWith("w")) { - int width = Integer.parseInt(size.substring(0, size.length() - 1)); - outd = orig.scaleWidth(width); - } else { - int worh = Integer.parseInt(size); - outd = orig.scaled(worh); + if (cimg == null) { + try { + cimg = pictureScaler.scalePicture(file, thumbnail, size); + imageCache.put(new Element(key, cimg)); + System.out.println(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + imageCache.getSize() + " entries"); + } catch (TimeoutException e) { + res.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + return; + } } - - /* In order to make the quality as good as possible we follow the advice from - * http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html - * and scale the image by a factor of 2 in intermediate steps, thereby getting a smoother - * end result. The first scale will get to a size which is a multiple of the result size. - * the first scaling operation will also take care of any rotation that needs to happen. - */ - Dimension intermediate = new Dimension(outd); - int targetWidth = orig.getWidth() / 2; - while (intermediate.getWidth() < targetWidth) { - intermediate = intermediate.doubled(); - } - - Iterator readers = ImageIO.getImageReadersByFormatName("jpg"); - ImageReader reader = readers.next(); - ImageInputStream iis = ImageIO.createImageInputStream(file); - reader.setInput(iis, true); - ImageReadParam param = reader.getDefaultReadParam(); - BufferedImage img = reader.read(0, param); - - // Recalculate scale after sub-sampling was applied - double scale; - AffineTransform xform; - if (thumbnail.getOrientation() == 6) { - xform = AffineTransform.getTranslateInstance(intermediate.getWidth() / 2d, intermediate.getHeight() / 2d); - xform.rotate(Math.PI / 2); - scale = (double)intermediate.getHeight() / img.getWidth(); - xform.scale(scale, scale); - xform.translate(-img.getWidth() / 2d, -img.getHeight() / 2d); - - } else { - scale = (double)intermediate.getWidth() / img.getWidth(); - xform = AffineTransform.getScaleInstance(scale, scale); - } - - BufferedImage buf2 = new BufferedImage(intermediate.getWidth(), intermediate.getHeight(), img.getType()); - Graphics2D g2 = buf2.createGraphics(); - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - - g2.transform(xform); - g2.drawImage(img, 0, 0, null); - - while (intermediate.getWidth() > outd.getWidth()) { - - BufferedImage buf3 = buf2; - intermediate = intermediate.halved(); - buf2 = new BufferedImage(intermediate.getWidth(), intermediate.getHeight(), img.getType()); - Graphics2D g3 = buf2.createGraphics(); - g3.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g3.drawImage(buf3, 0, 0, intermediate.getWidth(), intermediate.getHeight(), null); - } - - // g2.drawImage(img, operation, 0, 0); - - - Iterator writers = ImageIO.getImageWritersByFormatName("jpg"); - ImageWriter writer = writers.next(); - - ByteArrayOutputStream bits = new ByteArrayOutputStream(); - - ImageOutputStream ios = ImageIO.createImageOutputStream(bits); - writer.setOutput(ios); - writer.write(buf2); - ios.flush(); - - CachedImage cimg = new CachedImage(); - cimg.lastModified = file.lastModified(); - cimg.mimeType = "image/jpeg"; - cimg.bits = bits.toByteArray(); - imageCache.put(new Element(key, cimg)); - - System.out.println(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + imageCache.getSize() + " entries"); - res.setStatus(HttpServletResponse.SC_OK); + res.setDateHeader("Last-Modified", file.lastModified()); + res.setHeader("ETag", fileEtag); res.setContentType(cimg.mimeType); res.setContentLength(cimg.bits.length); res.getOutputStream().write(cimg.bits); - } } + + Entry resolveEntry(String pathInfo) { if (pathInfo == null || "/".equals(pathInfo)) return resolve(base); @@ -316,6 +227,9 @@ public class AlbumServlet } StringBuilder appendFile(StringBuilder buf, File file) { + if (file == null) { + return buf; + } if (base.equals(file.getAbsoluteFile())) { return buf.append("/").append(base.getName()); } else { @@ -323,12 +237,6 @@ public class AlbumServlet } } } - - static class CachedImage implements Serializable { - long lastModified; - String mimeType; - byte[] bits; - } } // eof diff --git a/src/org/forkalsrud/album/web/CachedImage.java b/src/org/forkalsrud/album/web/CachedImage.java new file mode 100644 index 0000000..54e97d2 --- /dev/null +++ b/src/org/forkalsrud/album/web/CachedImage.java @@ -0,0 +1,12 @@ +/** + * + */ +package org.forkalsrud.album.web; + +import java.io.Serializable; + +class CachedImage implements Serializable { + long lastModified; + String mimeType; + byte[] bits; +} \ No newline at end of file diff --git a/src/org/forkalsrud/album/web/PictureScaler.java b/src/org/forkalsrud/album/web/PictureScaler.java new file mode 100644 index 0000000..799b07d --- /dev/null +++ b/src/org/forkalsrud/album/web/PictureScaler.java @@ -0,0 +1,226 @@ +package org.forkalsrud.album.web; + +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; + +import org.forkalsrud.album.exif.Dimension; +import org.forkalsrud.album.exif.Thumbnail; + +public class PictureScaler { + + ExecutorService executor; + BlockingQueue queue; + HashMap outstandingRequests; + + + public PictureScaler() { + queue = new PriorityBlockingQueue(20, createPriorityComparator()); + executor = Executors.newSingleThreadExecutor(); + outstandingRequests = new HashMap(); + } + + + synchronized PictureRequest getPictureRequest(File file, Thumbnail thumbnail, String size) { + String key = file.getPath() + ":" + size; + PictureRequest req = outstandingRequests.get(key); + if (req == null) { + req = new PictureRequest(key, file, thumbnail, size); + outstandingRequests.put(key, req); + Future future = executor.submit(req); + req.setFuture(future); + } + return req; + } + + synchronized void completePictureRequest(PictureRequest req) { + + outstandingRequests.remove(req.getKey()); + } + + + class PictureRequest implements Callable { + + long priority; + String key; + File file; + Thumbnail thumbnail; + String size; + Future future; + + public PictureRequest(String key, File file, Thumbnail thumbnail, String size) { + this.key = key; + this.file = file; + this.thumbnail = thumbnail; + this.size = size; + this.priority = System.currentTimeMillis(); + } + + public String getKey() { + return key; + } + + + public CachedImage call() throws Exception { + return scalePictureReally(file, thumbnail, size); + } + + public void setFuture(Future future) { + this.future = future; + } + + public CachedImage waitForIt(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + + return future.get(/*timeout, unit*/); + } + + public void cancel() { + future.cancel(false); + } + } + + Comparator createPriorityComparator() { + + return new Comparator() { + + public int compare(PictureRequest o1, PictureRequest o2) { + return Long.signum(o1.priority - o2.priority); + } + }; + } + + + public CachedImage scalePicture(File file, Thumbnail thumbnail, String size) throws TimeoutException { + + PictureRequest req = getPictureRequest(file, thumbnail, size); + try { + return req.waitForIt(30, TimeUnit.SECONDS); + } catch (TimeoutException toe) { + req.cancel(); + toe.printStackTrace(); + throw new TimeoutException("timed out"); + } catch (InterruptedException e) { + e.printStackTrace(); + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } finally { + completePictureRequest(req); + } + } + + + /** + * @param file + * @param thumbnail + * @param size + * @throws IOException + */ + CachedImage scalePictureReally(File file, Thumbnail thumbnail, String size) throws IOException { + + Dimension orig = thumbnail.getSize(); + Dimension outd; + if (size.endsWith("h")) { + int height = Integer.parseInt(size.substring(0, size.length() - 1)); + outd = orig.scaleHeight(height); + } else if (size.endsWith("w")) { + int width = Integer.parseInt(size.substring(0, size.length() - 1)); + outd = orig.scaleWidth(width); + } else { + int worh = Integer.parseInt(size); + outd = orig.scaled(worh); + } + + /* In order to make the quality as good as possible we follow the advice from + * http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html + * and scale the image by a factor of 2 in intermediate steps, thereby getting a smoother + * end result. The first scale will get to a size which is a multiple of the result size. + * the first scaling operation will also take care of any rotation that needs to happen. + */ + Dimension intermediate = new Dimension(outd); + int targetWidth = orig.getWidth() / 2; + while (intermediate.getWidth() < targetWidth) { + intermediate = intermediate.doubled(); + } + + Iterator readers = ImageIO.getImageReadersByFormatName("jpg"); + ImageReader reader = readers.next(); + ImageInputStream iis = ImageIO.createImageInputStream(file); + reader.setInput(iis, true); + ImageReadParam param = reader.getDefaultReadParam(); + BufferedImage img = reader.read(0, param); + + // Recalculate scale after sub-sampling was applied + double scale; + AffineTransform xform; + if (thumbnail.getOrientation() == 6) { + xform = AffineTransform.getTranslateInstance(intermediate.getWidth() / 2d, intermediate.getHeight() / 2d); + xform.rotate(Math.PI / 2); + scale = (double)intermediate.getHeight() / img.getWidth(); + xform.scale(scale, scale); + xform.translate(-img.getWidth() / 2d, -img.getHeight() / 2d); + } else { + scale = (double)intermediate.getWidth() / img.getWidth(); + xform = AffineTransform.getScaleInstance(scale, scale); + } + + int imgType = img.getType(); + BufferedImage buf2 = new BufferedImage(intermediate.getWidth(), intermediate.getHeight(), imgType); + Graphics2D g2 = buf2.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + g2.transform(xform); + g2.drawImage(img, 0, 0, null); + img = null; + + while (intermediate.getWidth() > outd.getWidth()) { + + BufferedImage buf3 = buf2; + intermediate = intermediate.halved(); + buf2 = new BufferedImage(intermediate.getWidth(), intermediate.getHeight(), imgType); + g2 = buf2.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(buf3, 0, 0, intermediate.getWidth(), intermediate.getHeight(), null); + } + + Iterator writers = ImageIO.getImageWritersByFormatName("jpg"); + ImageWriter writer = writers.next(); + + ByteArrayOutputStream bits = new ByteArrayOutputStream(); + + ImageOutputStream ios = ImageIO.createImageOutputStream(bits); + writer.setOutput(ios); + writer.write(buf2); + ios.flush(); + + CachedImage cimg = new CachedImage(); + cimg.lastModified = file.lastModified(); + cimg.mimeType = "image/jpeg"; + cimg.bits = bits.toByteArray(); + return cimg; + } + +} diff --git a/webapp/WEB-INF/velocity/photo.vm b/webapp/WEB-INF/velocity/photo.vm index 710c283..a1dcc4b 100644 --- a/webapp/WEB-INF/velocity/photo.vm +++ b/webapp/WEB-INF/velocity/photo.vm @@ -57,7 +57,7 @@ #set($thpath = $mapper.map(${entry.thumbnail.getPath()})) #set($enpath = $mapper.map(${entry.getPath()}))
-
+
$!entry.caption
#else