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 @@
ehcache
1.5.0
+
+ log4j
+ log4j
+ 1.2.14
+
diff --git a/src/org/forkalsrud/album/exif/DirectoryEntry.java b/src/org/forkalsrud/album/exif/DirectoryEntry.java
index bc9bcb2..b8e235a 100644
--- a/src/org/forkalsrud/album/exif/DirectoryEntry.java
+++ b/src/org/forkalsrud/album/exif/DirectoryEntry.java
@@ -155,6 +155,9 @@ public class DirectoryEntry extends Entry {
if (f.isDirectory()) {
if ("CVS".equals(name)) {
continue;
+ }
+ if (f.isHidden()) {
+ continue;
}
generateDirectoryProperties(props, f);
continue;
diff --git a/src/org/forkalsrud/album/web/AlbumServlet.java b/src/org/forkalsrud/album/web/AlbumServlet.java
index d714a79..7747546 100644
--- a/src/org/forkalsrud/album/web/AlbumServlet.java
+++ b/src/org/forkalsrud/album/web/AlbumServlet.java
@@ -1,21 +1,9 @@
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.io.Serializable;
-import java.util.Iterator;
+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 javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
@@ -28,7 +16,6 @@ import net.sf.ehcache.Element;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.PropertyConfigurator;
-import org.forkalsrud.album.exif.Dimension;
import org.forkalsrud.album.exif.DirectoryEntry;
import org.forkalsrud.album.exif.Entry;
import org.forkalsrud.album.exif.FileEntry;
@@ -41,6 +28,7 @@ public class AlbumServlet
String basePrefix;
Cache imageCache;
CacheManager cacheManager;
+ PictureScaler pictureScaler;
@Override
public void init()
@@ -54,6 +42,7 @@ public class AlbumServlet
LogFactory.getFactory().setAttribute("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.Log4JLogger");
cacheManager = CacheManager.create();
imageCache = cacheManager.getCache("imageCache");
+ pictureScaler = new PictureScaler();
}
@Override
@@ -103,7 +92,7 @@ public class AlbumServlet
try {
FileEntry e = (FileEntry)DirectoryEntry.getEntry(file);
- scaleImage(req, res, file, e.getThumbnail(), size);
+ procesScaledImageRequest(req, res, file, e.getThumbnail(), size);
return;
} catch (Exception e) {
throw new RuntimeException("sadness", e);
@@ -156,7 +145,7 @@ public class AlbumServlet
return reqDate > 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);
@@ -169,120 +158,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);
@@ -318,6 +229,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 {
@@ -325,12 +239,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