Merge branch 'master' of ssh://odb/home/gitroot/album

This commit is contained in:
Erik Forkalsrud 2009-01-24 17:36:41 -08:00
commit 2dee5b85cb
8 changed files with 274 additions and 119 deletions

View file

@ -8,6 +8,7 @@
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="RunJettyRunWebAppClassPathProvider"/> <stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="RunJettyRunWebAppClassPathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="runjettyrun.Bootstrap"/> <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="runjettyrun.Bootstrap"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="album"/> <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="album"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx1024m"/>
<stringAttribute key="run_jetty_run.CONTEXT_ATTR" value="/"/> <stringAttribute key="run_jetty_run.CONTEXT_ATTR" value="/"/>
<stringAttribute key="run_jetty_run.PORT_ATTR" value="5080"/> <stringAttribute key="run_jetty_run.PORT_ATTR" value="5080"/>
<stringAttribute key="run_jetty_run.WEBAPPDIR_ATTR" value="webapp"/> <stringAttribute key="run_jetty_run.WEBAPPDIR_ATTR" value="webapp"/>

View file

@ -8,7 +8,7 @@
<cache <cache
name="imageCache" name="imageCache"
maxElementsInMemory="120" maxElementsInMemory="1"
maxElementsOnDisk="5000" maxElementsOnDisk="5000"
eternal="true" eternal="true"
overflowToDisk="true" overflowToDisk="true"

View file

@ -113,6 +113,11 @@
<artifactId>ehcache</artifactId> <artifactId>ehcache</artifactId>
<version>1.5.0</version> <version>1.5.0</version>
</dependency> </dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
</dependency>
</dependencies> </dependencies>
<repositories> <repositories>
<repository> <repository>

View file

@ -155,6 +155,9 @@ public class DirectoryEntry extends Entry {
if (f.isDirectory()) { if (f.isDirectory()) {
if ("CVS".equals(name)) { if ("CVS".equals(name)) {
continue; continue;
}
if (f.isHidden()) {
continue;
} }
generateDirectoryProperties(props, f); generateDirectoryProperties(props, f);
continue; continue;

View file

@ -1,21 +1,9 @@
package org.forkalsrud.album.web; 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.File;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.util.concurrent.TimeoutException;
import java.util.Iterator;
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.RequestDispatcher;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
@ -28,7 +16,6 @@ import net.sf.ehcache.Element;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.log4j.PropertyConfigurator; import org.apache.log4j.PropertyConfigurator;
import org.forkalsrud.album.exif.Dimension;
import org.forkalsrud.album.exif.DirectoryEntry; import org.forkalsrud.album.exif.DirectoryEntry;
import org.forkalsrud.album.exif.Entry; import org.forkalsrud.album.exif.Entry;
import org.forkalsrud.album.exif.FileEntry; import org.forkalsrud.album.exif.FileEntry;
@ -41,6 +28,7 @@ public class AlbumServlet
String basePrefix; String basePrefix;
Cache imageCache; Cache imageCache;
CacheManager cacheManager; CacheManager cacheManager;
PictureScaler pictureScaler;
@Override @Override
public void init() public void init()
@ -54,6 +42,7 @@ public class AlbumServlet
LogFactory.getFactory().setAttribute("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.Log4JLogger"); LogFactory.getFactory().setAttribute("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.Log4JLogger");
cacheManager = CacheManager.create(); cacheManager = CacheManager.create();
imageCache = cacheManager.getCache("imageCache"); imageCache = cacheManager.getCache("imageCache");
pictureScaler = new PictureScaler();
} }
@Override @Override
@ -103,7 +92,7 @@ public class AlbumServlet
try { try {
FileEntry e = (FileEntry)DirectoryEntry.getEntry(file); FileEntry e = (FileEntry)DirectoryEntry.getEntry(file);
scaleImage(req, res, file, e.getThumbnail(), size); procesScaledImageRequest(req, res, file, e.getThumbnail(), size);
return; return;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("sadness", e); throw new RuntimeException("sadness", e);
@ -156,7 +145,7 @@ public class AlbumServlet
return reqDate > 0 && fDate > 0 && fDate <= reqDate; 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)) { if (notModified(req, file)) {
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
@ -169,118 +158,40 @@ public class AlbumServlet
System.out.println(file.getName() + " not modified (based on etag)"); System.out.println(file.getName() + " not modified (based on etag)");
return; return;
} }
res.setDateHeader("Last-Modified", file.lastModified());
res.setHeader("ETag", fileEtag);
String key = file.getPath() + ":" + size; String key = file.getPath() + ":" + size;
CachedImage cimg = null;
Element element = imageCache.get(key); Element element = imageCache.get(key);
if (element != null) { if (element != null) {
CachedImage cimg = (CachedImage) element.getObjectValue(); cimg = (CachedImage) element.getObjectValue();
if (cimg.lastModified == file.lastModified()) { if (cimg.lastModified == file.lastModified()) {
System.out.println("cache hit on " + key); System.out.println("cache hit on " + key);
//res.setContentType(cimg.mimeType);
res.setContentLength(cimg.bits.length);
res.getOutputStream().write(cimg.bits);
return;
} else { } else {
System.out.println(" " + key + " has changed so cache entry wil be refreshed"); System.out.println(" " + key + " has changed so cache entry wil be refreshed");
imageCache.remove(key); imageCache.remove(key);
cimg = null;
} }
} }
if (cimg == null) {
synchronized (this) { try {
Dimension orig = thumbnail.getSize(); cimg = pictureScaler.scalePicture(file, thumbnail, size);
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<ImageReader> 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<ImageWriter> 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)); imageCache.put(new Element(key, cimg));
System.out.println(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + imageCache.getSize() + " entries"); 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;
}
}
res.setStatus(HttpServletResponse.SC_OK); res.setStatus(HttpServletResponse.SC_OK);
res.setDateHeader("Last-Modified", file.lastModified());
res.setHeader("ETag", fileEtag);
res.setContentType(cimg.mimeType); res.setContentType(cimg.mimeType);
res.setContentLength(cimg.bits.length); res.setContentLength(cimg.bits.length);
res.getOutputStream().write(cimg.bits); res.getOutputStream().write(cimg.bits);
} }
}
Entry resolveEntry(String pathInfo) { Entry resolveEntry(String pathInfo) {
@ -318,6 +229,9 @@ public class AlbumServlet
} }
StringBuilder appendFile(StringBuilder buf, File file) { StringBuilder appendFile(StringBuilder buf, File file) {
if (file == null) {
return buf;
}
if (base.equals(file.getAbsoluteFile())) { if (base.equals(file.getAbsoluteFile())) {
return buf.append("/").append(base.getName()); return buf.append("/").append(base.getName());
} else { } else {
@ -325,12 +239,6 @@ public class AlbumServlet
} }
} }
} }
static class CachedImage implements Serializable {
long lastModified;
String mimeType;
byte[] bits;
}
} }
// eof // eof

View file

@ -0,0 +1,12 @@
/**
*
*/
package org.forkalsrud.album.web;
import java.io.Serializable;
class CachedImage implements Serializable {
long lastModified;
String mimeType;
byte[] bits;
}

View file

@ -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<String, PictureRequest> outstandingRequests;
public PictureScaler() {
queue = new PriorityBlockingQueue<PictureRequest>(20, createPriorityComparator());
executor = Executors.newSingleThreadExecutor();
outstandingRequests = new HashMap<String, PictureRequest>();
}
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<CachedImage> future = executor.submit(req);
req.setFuture(future);
}
return req;
}
synchronized void completePictureRequest(PictureRequest req) {
outstandingRequests.remove(req.getKey());
}
class PictureRequest implements Callable<CachedImage> {
long priority;
String key;
File file;
Thumbnail thumbnail;
String size;
Future<CachedImage> 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<CachedImage> 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<PictureRequest> createPriorityComparator() {
return new Comparator<PictureRequest>() {
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<ImageReader> 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<ImageWriter> 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;
}
}

View file

@ -57,7 +57,7 @@
#set($thpath = $mapper.map(${entry.thumbnail.getPath()})) #set($thpath = $mapper.map(${entry.thumbnail.getPath()}))
#set($enpath = $mapper.map(${entry.getPath()})) #set($enpath = $mapper.map(${entry.getPath()}))
<div class="photo"> <div class="photo">
<a href="${base}${enpath}"><img src="${base}${thpath}?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/> <img src="${base}${thpath}?size=$thmb" border="0" width="$dim.width" height="$dim.height"/><br/>
<span class="caption">$!entry.caption</span> <span class="caption">$!entry.caption</span>
</div> </div>
#else #else