Rev 936 | Rev 1074 | 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.FileOutputStream;
import java.io.OutputStream;
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.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 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 Logger logger;
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 File dirTemplate;
protected ThumbnailPosition[] dirThumbnailPositions;
protected int[] dirTemplateSize;
protected long dirTemplateTimestamp;
public Thumbnailer()
{
this.logger = Logger.getLogger(this.getClass());
}
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()) {
logger.warn("not supported format of " + origin.getCanonicalPath());
res[0] = 0;
res[1] = 0;
}
else{
res[0] = ii.getWidth();
res[1] = ii.getHeight();
}
}
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);
}
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], null);
buildCache(children[i]);
}
else {
writeSmall(children[i], null);
writeMedium(children[i], null);
}
}
}
protected CachedFile getCached(Map cache, File imageFile)
throws IOException
{
CachedFile cached = (CachedFile)cache.get(imageFile.getCanonicalPath());
if(cached == null || !cached.getFile().exists()) {
logger.debug("not found in cache");
return null;
}
if(cached.getOriginTimestamp() != imageFile.lastModified()) {
cached.getFile().delete();
cache.remove(imageFile.getCanonicalPath());
logger.debug("timestamps dont match");
return null;
}
return cached;
}
protected boolean writeCached(Map cache, File imageFile, OutputStream out)
throws IOException
{
CachedFile cached = getCached(cache, imageFile);
if(cached == null) return false;
if(logger.isDebugEnabled())
logger.debug("write cached " + imageFile.getCanonicalPath());
if(out != null) {
FileInputStream in = null;
try {
in = new FileInputStream(cached.getFile());
FileUtils.copyStreams(in, out);
}
finally {
if(in != null) in.close();
}
}
return true;
}
protected void 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 = new File(dir,
imageFile.getName() + suffix + "." + format);
dir.mkdirs();
ImageIO.write(thumbnail, format, cacheFile);
cache.put(imageFile.getCanonicalPath(),
new CachedFile(cacheFile, imageFile,
cacheFile.lastModified(), imageFile.lastModified(), width, height));
}
public void startup()
throws IOException
{
logger.info("startup");
loadCaches(cacheDir);
logger.info("started");
}
protected void loadCaches(File dir)
throws IOException
{
if(logger.isDebugEnabled())
logger.debug("load caches in " + dir.getCanonicalPath());
File[] children = dir.listFiles();
String dirEnd = DIR_SUFFIX + "." + format;
String smallEnd = SMALL_SUFFIX + "." + format;
String mediumEnd = MEDIUM_SUFFIX + "." + format;
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())
loadCaches(children[i]);
else {
File origin;
Map cache;
if(children[i].getName().endsWith(smallEnd)) {
origin = getOriginFile(children[i], SMALL_SUFFIX);
cache = smallCache;
if(logger.isDebugEnabled())
logger.debug("load cached small " + children[i].getCanonicalPath()
+ " for " + origin.getCanonicalPath());
}
else if(children[i].getName().endsWith(mediumEnd)) {
origin = getOriginFile(children[i], MEDIUM_SUFFIX);
cache = mediumCache;
if(logger.isDebugEnabled())
logger.debug("load cached medium " + children[i].getCanonicalPath()
+ " for " + origin.getCanonicalPath());
}
else if(children[i].getName().endsWith(dirEnd)) {
origin = getOriginFile(children[i], DIR_SUFFIX);
cache = dirCache;
if(logger.isDebugEnabled())
logger.debug("load cached dir " + children[i].getCanonicalPath()
+ " for " + origin.getCanonicalPath());
}
else {
if(logger.isInfoEnabled())
logger.warn(
"unknown type of cached " + children[i].getCanonicalPath());
continue;
}
long originTimestamp = origin.lastModified();
long cachedTimestamp = children[i].lastModified();
int[] sizes = getOriginSize(children[i]);
if(origin.exists() && cachedTimestamp >= originTimestamp) {
cache.put(origin.getCanonicalPath(),
new CachedFile(children[i], origin, cachedTimestamp,
originTimestamp, sizes[0], sizes[1]));
logger.debug("added");
}
else {
children[i].delete();
if(logger.isDebugEnabled())
logger.debug("deleted: " + origin.exists()
+ " " + cachedTimestamp + " " + originTimestamp);
}
}
}
}
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()));
}
public void writeSmall(File imageFile, OutputStream out)
throws IOException
{
if(logger.isInfoEnabled())
logger.info("write small " + imageFile.getCanonicalPath());
if(writeCached(smallCache, imageFile, out)) return;
BufferedImage small = createThumbnail(imageFile, smallWidth, smallHeight);
if(small != null) {
int sizes[] = calcSizes(
small.getWidth(null), small.getHeight(null), smallWidth, smallHeight);
cacheThumbnail(
smallCache, imageFile, small, SMALL_SUFFIX, sizes[0], sizes[1]);
if(out != null) ImageIO.write(small, format, out);
}
}
public void writeMedium(File imageFile, OutputStream out)
throws IOException
{
if(logger.isInfoEnabled())
logger.info("write medium " + imageFile.getCanonicalPath());
if(writeCached(mediumCache, imageFile, out)) return;
BufferedImage medium
= createThumbnail(imageFile, mediumWidth, mediumHeight);
if(medium != null) {
int sizes[] = calcSizes(medium.getWidth(null), medium.getHeight(null),
mediumWidth, mediumHeight);
cacheThumbnail(
mediumCache, imageFile, medium, MEDIUM_SUFFIX, sizes[0], sizes[1]);
if(out != null) ImageIO.write(medium, format, out);
}
}
public void writeDir(File dir, OutputStream out)
throws IOException
{
if(logger.isInfoEnabled())
logger.info("write dir " + dir.getCanonicalPath());
if(writeCached(dirCache, dir, out)) return;
BufferedImage thumbnail = createDirThumbnail(dir);
if(thumbnail != null) {
if(dirTemplateSize == null
|| dirTemplateTimestamp != dirTemplate.lastModified())
{
dirTemplateSize = getOriginSize(dirTemplate);
dirTemplateTimestamp = dirTemplate.lastModified();
}
cacheThumbnail(dirCache, dir, thumbnail, DIR_SUFFIX,
dirTemplateSize[0], dirTemplateSize[1]);
if(out != null) ImageIO.write(thumbnail, format, out);
}
}
synchronized protected BufferedImage createThumbnail(File imageFile,
int width, int height)
throws IOException
{
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]);
}
}
synchronized protected BufferedImage createDirThumbnail(File dir)
throws IOException
{
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 = loadImage(firstFiles[i].getCanonicalPath());
if(image == null) { // not supported format
logger.warn("unsupported format for origin or operation interrupted");
return null;
}
else {
BufferedImage thumbnail;
int[] sizes;
sizes = calcSizes(image.getWidth(null), image.getHeight(null),
dirThumbnailPositions[i].getWidth(),
dirThumbnailPositions[i].getHeight());
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);
}
}
return dirThumbnail;
}
protected Image loadImage(String fileName)
{
// 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;
}
}
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; i++) {
if(children[i].isDirectory())
; //pos = getFirstFiles(children[i], count, files, pos);
else {
files[pos++] = children[i];
}
if(pos >= count) break;
}
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;
}
}