Subversion Repositories general

Rev

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;
  }
}