Rev 1272 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
package ak.photoalbum.images;
import java.util.Map;
import java.util.HashMap;
import java.util.Comparator;
import java.util.Arrays;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.awt.image.PixelGrabber;
import javax.imageio.ImageIO;
import org.apache.log4j.Logger;
import marcoschmidt.image.ImageInfo;
import ak.photoalbum.util.TimestampRecipient;
import ak.photoalbum.util.FileUtils;
import ak.photoalbum.util.FileNameComparator;
public class Thumbnailer
{
protected static final String DEFAULT_FORMAT = "jpg";
protected static final String SMALL_SUFFIX = ".small";
protected static final String MEDIUM_SUFFIX = ".medium";
protected static final String DIR_SUFFIX = ".dir";
protected static final String DIR_PART_SUFFIX = ".dir_part";
protected static final int DEFAULT_SMALL_WIDTH = 120;
protected static final int DEFAULT_SMALL_HEIGHT = 120;
protected static final int DEFAULT_MEDIUM_WIDTH = 800;
protected static final int DEFAULT_MEDIUM_HEIGHT = 800;
protected static final int CACHED_NOT_FOUND = 0;
protected static final int CACHED_NOT_MODIFIED = 1;
protected static final int CACHED_WRITTEN = 2;
private static final Logger logger = Logger.getLogger(Thumbnailer.class);
protected ImageResizer resizer;
protected int smallWidth = DEFAULT_SMALL_WIDTH;
protected int smallHeight = DEFAULT_SMALL_HEIGHT;
protected int mediumWidth = DEFAULT_MEDIUM_WIDTH;
protected int mediumHeight = DEFAULT_MEDIUM_HEIGHT;
protected File cacheDir;
protected String format = DEFAULT_FORMAT;
protected Map smallCache = new HashMap();
protected Map mediumCache = new HashMap();
protected Map dirCache = new HashMap();
protected File imagesRoot;
protected FileFilter imagesFilter = null;
protected Comparator fileNameComparator = new FileNameComparator(true);
protected Comparator fileNameComparatorRev = new FileNameComparator(false);
protected boolean nativeMode = true; // FIXME config
protected File dirTemplate;
protected ThumbnailPosition[] dirThumbnailPositions;
protected int[] dirTemplateSize;
protected long dirTemplateTimestamp;
public Thumbnailer()
{
}
public String getMime()
{
return FileUtils.getMime(format);
}
public int[] getDirSize(File origin)
throws IOException
{
if(dirTemplateSize == null
|| dirTemplateTimestamp != dirTemplate.lastModified())
{
dirTemplateSize = getOriginSize(dirTemplate);
dirTemplateTimestamp = dirTemplate.lastModified();
}
return dirTemplateSize;
}
public int[] getSmallSize(File origin)
throws IOException
{
CachedFile cached = getCached(smallCache, origin);
if(cached == null) {
int[] originSize = getOriginSize(origin);
return calcSizes(originSize[0], originSize[1], smallWidth, smallHeight);
}
else {
int[] originSize = new int[2];
originSize[0] = cached.getWidth();
originSize[1] = cached.getHeight();
return originSize;
}
}
public int[] getMediumSize(File origin)
throws IOException
{
CachedFile cached = getCached(mediumCache, origin);
if(cached == null) {
int[] originSize = getOriginSize(origin);
return calcSizes(originSize[0], originSize[1], mediumWidth, mediumHeight);
}
else {
int[] originSize = new int[2];
originSize[0] = cached.getWidth();
originSize[1] = cached.getHeight();
return originSize;
}
}
protected int[] getOriginSize(File origin)
throws IOException
{
if(logger.isDebugEnabled())
logger.debug("get size of " + origin.getCanonicalPath());
ImageInfo ii = new ImageInfo();
FileInputStream in = null;
int[] res = new int[2];
try {
in = new FileInputStream(origin);
ii.setInput(in);
if(ii.check()) {
res[0] = ii.getWidth();
res[1] = ii.getHeight();
}
else{
logger.warn("not supported format of " + origin.getCanonicalPath());
res[0] = 0;
res[1] = 0;
}
}
finally {
if(in != null) in.close();
}
return res;
}
public void rebuildCache()
throws IOException
{
logger.info("rebuild cache");
deleteCache();
buildCache();
}
public void deleteCache()
throws IOException
{
logger.info("delete cache");
deleteCache(cacheDir);
}
public void buildCache()
throws IOException
{
logger.info("build cache");
buildCache(imagesRoot);
}
public void reloadCache()
throws IOException
{
logger.info("reload cache");
smallCache.clear();
mediumCache.clear();
dirCache.clear();
loadCaches(cacheDir);
}
protected void deleteCache(File dir)
throws IOException
{
File[] children = dir.listFiles();
if(children == null) return; // the dir does not exists
Arrays.sort(children, fileNameComparator);
for(int i = 0; i < children.length; i++) {
if(children[i].isDirectory())
deleteCache(children[i]);
else
children[i].delete();
}
dir.delete();
}
protected void buildCache(File dir)
throws IOException
{
File[] children;
if(imagesFilter == null)
children = dir.listFiles();
else
children = dir.listFiles(imagesFilter);
if(children == null) return; // the dir does not exists
Arrays.sort(children, fileNameComparator);
for(int i = 0; i < children.length; i++) {
if(children[i].isDirectory()) {
writeDir(children[i], -1, null, null);
buildCache(children[i]);
}
else {
writeSmall(children[i], -1, null, null);
writeMedium(children[i], -1, null, null);
}
}
}
protected CachedFile getCached(Map cache, File imageFile)
throws IOException
{
String key = imageFile.getCanonicalPath();
CachedFile cached = (CachedFile)cache.get(key);
if(cached == null || !cached.getFile().exists()) {
cache.remove(key);
logger.debug("not found in cache");
return null;
}
if(cached.getOriginTimestamp() != imageFile.lastModified()) {
cached.getFile().delete();
cache.remove(key);
logger.debug("timestamps dont match");
return null;
}
return cached;
}
protected int writeCached(Map cache, File imageFile, long ifModifiedSince,
OutputStream out, TimestampRecipient timestampRecipient)
throws IOException
{
CachedFile cached = getCached(cache, imageFile);
if(cached == null) return CACHED_NOT_FOUND;
if(ifModifiedSince >= 0 && ifModifiedSince <= cached.getTimestamp())
return CACHED_NOT_MODIFIED; // cached image found and not modified
// since given timestamp - do nothing
if(logger.isDebugEnabled())
logger.debug("write cached " + imageFile.getCanonicalPath());
if(timestampRecipient != null) {
timestampRecipient.setTimestamp(cached.getTimestamp());
}
if(out != null) {
FileInputStream in = null;
try {
in = new FileInputStream(cached.getFile());
FileUtils.copyStreams(in, out);
}
finally {
if(in != null) in.close();
}
}
return CACHED_WRITTEN;
}
protected File getCacheFile(File dir, File imageFile, String suffix)
{
return new File(dir, imageFile.getName() + suffix + "." + format);
}
// @return last modified timestamp pf the cached thumbnail
protected long cacheThumbnail(Map cache, File imageFile,
BufferedImage thumbnail, String suffix, int width, int height)
throws IOException
{
logger.debug("cache thumbnail " + suffix + " "
+ imageFile.getCanonicalPath());
File dir = getCacheFileDir(imageFile);
File cacheFile = getCacheFile(dir, imageFile, suffix);
dir.mkdirs();
ImageIO.write(thumbnail, format, cacheFile);
cache.put(imageFile.getCanonicalPath(),
new CachedFile(cacheFile, imageFile,
cacheFile.lastModified(), imageFile.lastModified(), width, height));
return cacheFile.lastModified();
}
public void startup()
throws IOException
{
logger.info("startup");
loadCaches(cacheDir);
logger.info("started");
}
protected void loadFileCache(File file)
throws IOException
{
/* FIXME use this optimization?
String dirEnd = DIR_SUFFIX + "." + format;
String smallEnd = SMALL_SUFFIX + "." + format;
String mediumEnd = MEDIUM_SUFFIX + "." + format;
*/
File origin = null;
Map cache = null;
if(file.getName().endsWith(SMALL_SUFFIX + "." + format)) {
origin = getOriginFile(file, SMALL_SUFFIX);
cache = smallCache;
if(logger.isDebugEnabled())
logger.debug("load cached small " + file.getCanonicalPath()
+ " for " + origin.getCanonicalPath());
}
else if(file.getName().endsWith(MEDIUM_SUFFIX + "." + format)) {
origin = getOriginFile(file, MEDIUM_SUFFIX);
cache = mediumCache;
if(logger.isDebugEnabled())
logger.debug("load cached medium " + file.getCanonicalPath()
+ " for " + origin.getCanonicalPath());
}
else if(file.getName().endsWith(DIR_SUFFIX + "." + format)) {
origin = getOriginFile(file, DIR_SUFFIX);
cache = dirCache;
if(logger.isDebugEnabled())
logger.debug("load cached dir " + file.getCanonicalPath()
+ " for " + origin.getCanonicalPath());
}
else if(file.getName().endsWith(DIR_PART_SUFFIX + "." + format)) {
if(logger.isDebugEnabled())
logger.debug("skip cached dir part " + file.getCanonicalPath());
}
else {
if(logger.isInfoEnabled())
logger.warn(
"unknown type of cached " + file.getCanonicalPath());
}
if(origin == null || cache == null) {
file.delete();
if(logger.isDebugEnabled())
logger.debug("deleted (unknown origin)");
}
else {
loadCacheInfo(file, origin, cache);
}
}
protected void loadCacheInfo(File file, File origin, Map cache)
throws IOException
{
long originTimestamp = origin.lastModified();
long cachedTimestamp = file.lastModified();
int[] sizes = getOriginSize(file);
if(origin.exists() && cachedTimestamp >= originTimestamp) {
cache.put(origin.getCanonicalPath(),
new CachedFile(file, origin, cachedTimestamp,
originTimestamp, sizes[0], sizes[1]));
logger.debug("added");
}
else {
file.delete();
if(logger.isDebugEnabled())
logger.debug("deleted: " + origin.exists()
+ " " + cachedTimestamp + " " + originTimestamp);
}
}
protected void loadCaches(File dir)
throws IOException
{
if(logger.isDebugEnabled())
logger.debug("load caches in " + dir.getCanonicalPath());
File[] children = dir.listFiles();
if(children == null) return; // the dir does not exists
for(int i = 0; i < children.length; i++) {
if(children[i].isDirectory())
loadCaches(children[i]);
else
loadFileCache(children[i]);
}
}
protected File getOriginFile(File cached, String suffix)
throws IOException
{
String fileEnd = suffix + "." + format;
String fileName = cached.getName();
String cachedPath = cached.getParentFile().getCanonicalPath();
String cacheRoot = cacheDir.getCanonicalPath();
fileName = fileName.substring(0, fileName.length() - fileEnd.length());
if(!cacheRoot.equals(cachedPath))
if(!cacheRoot.endsWith(File.separator)) cacheRoot += File.separator;
return new File(imagesRoot, cachedPath.substring(cacheRoot.length())
+ File.separator + fileName);
}
protected File getCacheFileDir(File imageFile)
throws IOException
{
String imagePath = imageFile.getParentFile().getCanonicalPath();
String rootPath = imagesRoot.getCanonicalPath();
if(imagePath.equals(rootPath)) return cacheDir;
if(!rootPath.endsWith(File.separator)) rootPath += File.separator;
if(!imagePath.startsWith(rootPath))
throw new RuntimeException("Image " + imageFile.getCanonicalPath()
+ " is not under images root " + imagesRoot.getCanonicalPath());
return new File(cacheDir, imagePath.substring(rootPath.length()));
}
protected boolean writeThumbnail(Map cache, String suffix, int width, int height,
File imageFile, long ifModifiedSince, OutputStream out, TimestampRecipient timestampRecipient)
throws IOException
{
int cachedResult = writeCached(cache, imageFile, ifModifiedSince, out, timestampRecipient);
if(cachedResult != CACHED_NOT_FOUND) return (cachedResult == CACHED_WRITTEN);
if(nativeMode) {
File dir = getCacheFileDir(imageFile);
File cacheFile = getCacheFile(dir, imageFile, suffix);
createThumbnailNative(dir, cacheFile, imageFile, width, height);
loadCacheInfo(cacheFile, imageFile, cache);
writeCached(cache, imageFile, -1, out, timestampRecipient);
}
else {
BufferedImage thumbnail = createThumbnail(imageFile, width, height);
if(thumbnail != null) {
// a thumbnail returned - save it into the cache dir
int sizes[] = calcSizes(
thumbnail.getWidth(null), thumbnail.getHeight(null), width, height);
long lastModified = cacheThumbnail(
cache, imageFile, thumbnail, suffix, sizes[0], sizes[1]);
if(timestampRecipient != null) {
timestampRecipient.setTimestamp(lastModified);
}
if(out != null) {
ImageIO.write(thumbnail, format, out);
}
}
}
return true; // image written
}
public boolean writeSmall(File imageFile, long ifModifiedSince,
OutputStream out, TimestampRecipient timestampRecipient)
throws IOException
{
if(logger.isInfoEnabled())
logger.info("write small " + imageFile.getCanonicalPath());
return writeThumbnail(smallCache, SMALL_SUFFIX, smallWidth, smallHeight,
imageFile, ifModifiedSince, out, timestampRecipient);
}
public boolean writeMedium(File imageFile, long ifModifiedSince,
OutputStream out, TimestampRecipient timestampRecipient)
throws IOException
{
if(logger.isInfoEnabled())
logger.info("write medium " + imageFile.getCanonicalPath());
return writeThumbnail(mediumCache, MEDIUM_SUFFIX, mediumWidth, mediumHeight,
imageFile, ifModifiedSince, out, timestampRecipient);
}
public boolean writeDir(File dir, long ifModifiedSince,
OutputStream out, TimestampRecipient timestampRecipient)
throws IOException
{
if(logger.isInfoEnabled())
logger.info("write dir " + dir.getCanonicalPath());
int cachedResult = writeCached(dirCache, dir, ifModifiedSince, out, timestampRecipient);
if(cachedResult != CACHED_NOT_FOUND) return (cachedResult == CACHED_WRITTEN);
BufferedImage thumbnail = createDirThumbnail(dir);
if(thumbnail != null) {
if(dirTemplateSize == null
|| dirTemplateTimestamp != dirTemplate.lastModified())
{
dirTemplateSize = getOriginSize(dirTemplate);
dirTemplateTimestamp = dirTemplate.lastModified();
}
long lastModified = cacheThumbnail(dirCache, dir, thumbnail, DIR_SUFFIX,
dirTemplateSize[0], dirTemplateSize[1]);
if(timestampRecipient != null) {
timestampRecipient.setTimestamp(lastModified);
}
if(out != null) {
ImageIO.write(thumbnail, format, out);
}
}
return true; // image written
}
protected BufferedImage createThumbnail(File imageFile,
int width, int height)
throws IOException
{
// FIXME make several instances parallel if we have sevelar processors
synchronized(this) {
if(logger.isDebugEnabled())
logger.debug("create thumbnail " + imageFile.getCanonicalPath());
Image image = loadImage(imageFile.getCanonicalPath());
// = ImageIO.read(imageFile);
int[] sizes;
if(image == null) { // not supported format
logger.warn("unsupported format for origin or operation interrupted");
return null;
}
else {
sizes = calcSizes(image.getWidth(null), image.getHeight(null),
width, height);
logger.debug("resize to " + sizes[0] + "x" + sizes[1]);
return resizer.resize(image, sizes[0], sizes[1]);
}
}
}
protected BufferedImage createThumbnailNative(File dir, File cacheFile,
File imageFile, int width, int height)
throws IOException
{
// FIXME make several instances parallel if we have sevelar processors
synchronized(this) {
if(logger.isDebugEnabled())
logger.debug("create thumbnail2 " + imageFile.getCanonicalPath() + " to "
+ cacheFile.getCanonicalPath());
dir.mkdirs();
// FIXME: 1) make util path (and params?) configurable
Process process = Runtime.getRuntime().exec(new String[] {
"/usr/local/bin/convert",
"-size", width + "x" + height,
"-thumbnail", width + "x" + height,
imageFile.getCanonicalPath(),
cacheFile.getCanonicalPath()
});
// FIXME make it finner
BufferedReader in = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line;
while((line = in.readLine()) != null) {
logger.info("EXEC: " + line);
}
try {
int res = process.waitFor();
if(logger.isDebugEnabled())
logger.debug("process exited with result " + res);
}
catch(InterruptedException ex) {
logger.debug("process interrupted");
}
return null;
}
}
protected BufferedImage createDirThumbnail(File dir)
throws IOException
{
// FIXME make several instances parallel if we have sevelar processors
synchronized(this) {
long timeStart = System.currentTimeMillis();
if(logger.isDebugEnabled())
logger.debug("create dir thumbnail " + dir.getCanonicalPath());
Image template = loadImage(dirTemplate.getCanonicalPath());
BufferedImage dirThumbnail;
Graphics graphics;
int count;
File[] firstFiles;
if(template == null) { // not supported format
logger.warn("unsupported format for template or operation interrupted");
return null;
}
dirThumbnail = createBufferedImage(template);
graphics = dirThumbnail.getGraphics();
count = dirThumbnailPositions.length;
firstFiles = new File[count];
count = getFirstFiles(dir, count, firstFiles, 0);
for(int i = 0; i < count; i++) {
Image image;
BufferedImage thumbnail = null;
int[] sizes;
if(nativeMode) {
File cacheFileDir = getCacheFileDir(firstFiles[i]);
File cacheFile = getCacheFile(cacheFileDir, firstFiles[i], DIR_PART_SUFFIX);
createThumbnailNative(cacheFileDir, cacheFile, firstFiles[i],
dirThumbnailPositions[i].getWidth(), dirThumbnailPositions[i].getHeight());
image = loadImage(cacheFile.getCanonicalPath());
thumbnail = createBufferedImage(image);
}
else {
image = loadImage(firstFiles[i].getCanonicalPath());
}
if(image == null) { // not supported format
logger.warn("unsupported format for origin or operation interrupted");
return null;
}
else {
sizes = calcSizes(image.getWidth(null), image.getHeight(null),
dirThumbnailPositions[i].getWidth(),
dirThumbnailPositions[i].getHeight());
}
if(!nativeMode) {
thumbnail = resizer.resize(image, sizes[0], sizes[1]);
}
graphics.drawImage(thumbnail,
getXPosition(dirThumbnailPositions[i].getX(),
dirThumbnailPositions[i].getWidth(), sizes[0],
dirThumbnailPositions[i].getHorAlign()),
getYPosition(dirThumbnailPositions[i].getY(),
dirThumbnailPositions[i].getHeight(), sizes[1],
dirThumbnailPositions[i].getVertAlign()),
null);
}
if(logger.isDebugEnabled()) {
logger.debug("dir thumbnail created in "
+ (System.currentTimeMillis() - timeStart) + " ms");
}
return dirThumbnail;
}
}
protected Image loadImage(String fileName)
{
long timeStart = System.currentTimeMillis();
// FIXME: probably toolbox reads an image not by every request but
// caches it
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(fileName);
toolkit.prepareImage(image, -1, -1, null);
while(true) {
int status = toolkit.checkImage(image, -1, -1, null);
if((status & ImageObserver.ALLBITS) != 0) break;
if((status & ImageObserver.ERROR) != 0) return null;
try {
Thread.sleep(100);
}
catch(Exception ex) {
return null;
}
}
if(logger.isDebugEnabled()) {
logger.debug("image " + fileName + " loaded in "
+ (System.currentTimeMillis() - timeStart) + " ms");
}
return image;
}
protected int[] calcSizes(int width, int height, int maxWidth, int maxHeight)
{
int[] result = new int[2];
double xRate;
double yRate;
if(width == 0 || height == 0) {
result[0] = 0;
result[1] = 0;
return result;
}
xRate = (double)maxWidth / (double)width;
yRate = (double)maxHeight / (double)height;
if(xRate >= 1.0 || yRate >= 1.0) {
result[0] = width;
result[1] = height;
}
else if(xRate > yRate) {
result[0] = maxHeight * width / height;
result[1] = maxHeight;
}
else {
result[0] = maxWidth;
result[1] = maxWidth * height / width;
}
return result;
}
protected int getXPosition(int left, int maxWidth, int width, int align)
{
if(align == ThumbnailPosition.ALIGN_HOR_LEFT)
return left;
else if(align == ThumbnailPosition.ALIGN_HOR_RIGHT)
return left + (maxWidth - width);
else if(align == ThumbnailPosition.ALIGN_HOR_CENTER)
return left + (maxWidth - width) / 2;
else
throw new RuntimeException("Unknown align type: " + align);
}
protected int getYPosition(int top, int maxHeight, int height, int align)
{
if(align == ThumbnailPosition.ALIGN_VERT_TOP)
return top;
else if(align == ThumbnailPosition.ALIGN_VERT_BOTTOM)
return top + (maxHeight - height);
else if(align == ThumbnailPosition.ALIGN_VERT_CENTER)
return top + (maxHeight - height) / 2;
else
throw new RuntimeException("Unknown align type: " + align);
}
protected int getFirstFiles(File dir, int count, File[] files, int pos)
{
File[] children;
if(imagesFilter == null)
children = dir.listFiles();
else
children = dir.listFiles(imagesFilter);
if(children == null) return 0; // the dir does not exists
Arrays.sort(children, fileNameComparatorRev);
for(int i = 0; i < children.length && pos < count; i++) {
if(children[i].isDirectory()) {
//pos = getFirstFiles(children[i], count, files, pos);
}
else {
files[pos++] = children[i];
}
}
return pos;
}
protected BufferedImage createBufferedImage(Image image)
{
if(image == null) return null;
int width = image.getWidth(null);
int height = image.getHeight(null);
if(width < 1 || height < 1) return null;
// get pixels
int[] pixels = new int[width * height];
PixelGrabber pg = new PixelGrabber(image,
0, 0, width, height, pixels, 0, width);
try {
pg.grabPixels();
}
catch(InterruptedException e) {
return null;
}
if((pg.getStatus() & ImageObserver.ABORT) != 0) return null;
// create buffered image
BufferedImage buffered = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++)
buffered.setRGB(x, y, pixels[y * width + x]);
}
return buffered;
}
public ImageResizer getResizer()
{
return resizer;
}
public void setResizer(ImageResizer resizer)
{
this.resizer = resizer;
}
public String getFormat()
{
return format;
}
public void setFormat(String format)
{
this.format = format;
}
public File getCacheDir()
{
return cacheDir;
}
public void setCacheDir(File dir)
{
this.cacheDir = dir;
}
public File getImagesRoot()
{
return imagesRoot;
}
public void setImagesRoot(File dir)
{
this.imagesRoot = dir;
}
public int getSmallWidth()
{
return smallWidth;
}
public void setSmallWidth(int width)
{
this.smallWidth = width;
}
public int getSmallHeight()
{
return smallHeight;
}
public void setSmallHeight(int height)
{
this.smallHeight = height;
}
public int getMediumWidth()
{
return mediumWidth;
}
public void setMediumWidth(int width)
{
this.mediumWidth = width;
}
public int getMediumHeight()
{
return mediumHeight;
}
public void setMediumHeight(int height)
{
this.mediumHeight = height;
}
public FileFilter getImagesFilter()
{
return imagesFilter;
}
public void setImagesFilter(FileFilter filter)
{
this.imagesFilter = filter;
}
public File getDirTemplate()
{
return dirTemplate;
}
public void setDirTemplate(File dirTemplate)
{
this.dirTemplate = dirTemplate;
}
public ThumbnailPosition[] getDirThumbnailPositions()
{
return dirThumbnailPositions;
}
public void setDirThumbnailPositions(
ThumbnailPosition[] dirThumbnailPositions)
{
this.dirThumbnailPositions = dirThumbnailPositions;
}
}