Created separate thread for image scaling operations.
This commit is contained in:
parent
f78d06bc7b
commit
7f046cfdaa
6 changed files with 266 additions and 119 deletions
|
|
@ -8,6 +8,7 @@
|
|||
<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.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.PORT_ATTR" value="5080"/>
|
||||
<stringAttribute key="run_jetty_run.WEBAPPDIR_ATTR" value="webapp"/>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
<cache
|
||||
name="imageCache"
|
||||
maxElementsInMemory="120"
|
||||
maxElementsInMemory="1"
|
||||
maxElementsOnDisk="5000"
|
||||
eternal="true"
|
||||
overflowToDisk="true"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -101,7 +90,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);
|
||||
|
|
@ -154,7 +143,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);
|
||||
|
|
@ -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<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));
|
||||
|
||||
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
|
||||
|
|
|
|||
12
src/org/forkalsrud/album/web/CachedImage.java
Normal file
12
src/org/forkalsrud/album/web/CachedImage.java
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.forkalsrud.album.web;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
class CachedImage implements Serializable {
|
||||
long lastModified;
|
||||
String mimeType;
|
||||
byte[] bits;
|
||||
}
|
||||
226
src/org/forkalsrud/album/web/PictureScaler.java
Normal file
226
src/org/forkalsrud/album/web/PictureScaler.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
#set($thpath = $mapper.map(${entry.thumbnail.getPath()}))
|
||||
#set($enpath = $mapper.map(${entry.getPath()}))
|
||||
<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>
|
||||
</div>
|
||||
#else
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue