Subversion Repositories general

Compare Revisions

Ignore whitespace Rev 961 → Rev 962

/PhotoAlbum/trunk/src/marcoschmidt/image/ImageInfo.java
0,0 → 1,1241
/*
* ImageInfo.java
*
* Version 1.3
*
* A Java class to determine image width, height and color depth for
* a number of image file formats.
*
* Written by Marco Schmidt <marcoschmidt@users.sourceforge.net>
*
* Contributed to the Public Domain.
*
* Last modification 2002-06-17
*/
package marcoschmidt.image;
 
import java.io.DataInput;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.Vector;
 
/**
* Get file format, image resolution, number of bits per pixel and optionally
* number of images, comments and physical resolution from
* JPEG, GIF, BMP, PCX, PNG, IFF, RAS, PBM, PGM, PPM, PSD and SWF files
* (or input streams).
* <p>
* Use the class like this:
* <pre>
* ImageInfo ii = new ImageInfo();
* ii.setInput(in); // in can be InputStream or RandomAccessFile
* ii.setDetermineImageNumber(true); // default is false
* ii.setCollectComments(true); // default is false
* if (!ii.check()) {
* System.err.println("Not a supported image file format.");
* return;
* }
* System.out.println(ii.getFormatName() + ", " + ii.getMimeType() +
* ", " + ii.getWidth() + " x " + ii.getHeight() + " pixels, " +
* ii.getBitsPerPixel() + " bits per pixel, " + ii.getNumberOfImages() +
* " image(s), " + ii.getNumberOfComments() + " comment(s).");
* </pre>
* You can also use this class as a command line program.
* Call it with a number of image file names as parameters:
* <pre>
* java ImageInfo *.jpg *.png *.gif
* </pre>
* or call it without parameters and pipe data to it:
* <pre>
* cat image.jpg | java ImageInfo
* </pre>
* <p>
* Known limitations:
* <ul>
* <li>When the determination of the number of images is turned off, GIF bits
* per pixel are only read from the global header.
* For some GIFs, local palettes change this to a typically larger
* value. To be certain to get the correct color depth, call
* setDetermineImageNumber(true) before calling check().
* The complete scan over the GIF file will take additional time.</li>
* <li>Transparency information is not included in the bits per pixel count.
* Actually, it was my decision not to include those bits, so it's a feature! ;-)</li>
* </ul>
* <p>
* Requirements:
* <ul>
* <li>Java 1.1 or higher</li>
* </ul>
* <p>
* The latest version can be found at <a href="http://www.geocities.com/marcoschmidt.geo/image-info.html">http://www.geocities.com/marcoschmidt.geo/image-info.html</a>.
* <p>
* Written by <a href="mailto:marcoschmidt@users.sourceforge.net">Marco Schmidt</a>.
* <p>
* This class is contributed to the Public Domain.
* Use it at your own risk.
* <p>
* Last modification 2002-06-17.
* <p>
* History:
* <ul>
* <li><strong>2001-08-24</strong> Initial version.</li>
* <li><strong>2001-10-13</strong> Added support for the file formats BMP and PCX.</li>
* <li><strong>2001-10-16</strong> Fixed bug in read(int[], int, int) that returned
* <li><strong>2002-01-22</strong> Added support for file formats Amiga IFF and Sun Raster (RAS).</li>
* <li><strong>2002-01-24</strong> Added support for file formats Portable Bitmap / Graymap / Pixmap (PBM, PGM, PPM) and Adobe Photoshop (PSD).
* Added new method getMimeType() to return the MIME type associated with a particular file format.</li>
* <li><strong>2002-03-15</strong> Added support to recognize number of images in file. Only works with GIF.
* Use {@link #setDetermineImageNumber} with <code>true</code> as argument to identify animated GIFs
* ({@link #getNumberOfImages()} will return a value larger than <code>1</code>).</li>
* <li><strong>2002-04-10</strong> Fixed a bug in the feature 'determine number of images in animated GIF' introduced with version 1.1.
* Thanks to Marcelo P. Lima for sending in the bug report.
* Released as 1.1.1.</li>
* <li><strong>2002-04-18</strong> Added {@link #setCollectComments(boolean)}.
* That new method lets the user specify whether textual comments are to be
* stored in an internal list when encountered in an input image file / stream.
* Added two methods to return the physical width and height of the image in dpi:
* {@link #getPhysicalWidthDpi()} and {@link #getPhysicalHeightDpi()}.
* If the physical resolution could not be retrieved, these methods return <code>-1</code>.
* </li>
* <li><strong>2002-04-23</strong> Added support for the new properties physical resolution and
* comments for some formats. Released as 1.2.</li>
* <li><strong>2002-06-17</strong> Added support for SWF, sent in by Michael Aird.
* Changed checkJpeg() so that other APP markers than APP0 will not lead to a failure anymore.
* Released as 1.3.</li>
* </ul>
*/
public class ImageInfo {
/**
* Return value of {@link #getFormat()} for JPEG streams.
* ImageInfo can extract physical resolution and comments
* from JPEGs (only from APP0 headers).
* Only one image can be stored in a file.
*/
public static final int FORMAT_JPEG = 0;
 
/**
* Return value of {@link #getFormat()} for GIF streams.
* ImageInfo can extract comments from GIFs and count the number
* of images (GIFs with more than one image are animations).
* If you know of a place where GIFs store the physical resolution
* of an image, please
* <a href="http://www.geocities.com/marcoschmidt.geo/contact.html">send me a mail</a>!
*/
public static final int FORMAT_GIF = 1;
 
/**
* Return value of {@link #getFormat()} for PNG streams.
* PNG only supports one image per file.
* Both physical resolution and comments can be stored with PNG,
* but ImageInfo is currently not able to extract those.
*/
public static final int FORMAT_PNG = 2;
 
/**
* Return value of {@link #getFormat()} for BMP streams.
* BMP only supports one image per file.
* BMP does not allow for comments.
* The physical resolution can be stored.
* <em>The specification that I have says that the values must be
* interpreted as dots per meter. However, given that I only
* encounter typical dpi values like 72 or 300, I currently
* consider those values dpi. Maybe someone can shed some light
* on this, please send me a mail in that case.</em>
*/
public static final int FORMAT_BMP = 3;
 
/**
* Return value of {@link #getFormat()} for PCX streams.
* PCX does not allow for comments or more than one image per file.
* However, the physical resolution can be stored.
*/
public static final int FORMAT_PCX = 4;
 
/**
* Return value of {@link #getFormat()} for IFF streams.
*/
public static final int FORMAT_IFF = 5;
 
/**
* Return value of {@link #getFormat()} for RAS streams.
* Sun Raster allows for one image per file only and is not able to
* store physical resolution or comments.
*/
public static final int FORMAT_RAS = 6;
 
/** Return value of {@link #getFormat()} for PBM streams. */
public static final int FORMAT_PBM = 7;
 
/** Return value of {@link #getFormat()} for PGM streams. */
public static final int FORMAT_PGM = 8;
 
/** Return value of {@link #getFormat()} for PPM streams. */
public static final int FORMAT_PPM = 9;
 
/** Return value of {@link #getFormat()} for PSD streams. */
public static final int FORMAT_PSD = 10;
 
/** Return value of {@link #getFormat()} for SWF (Shockwave) streams. */
public static final int FORMAT_SWF = 11;
 
/**
* The names of all supported file formats.
* The FORMAT_xyz int constants can be used as index values for
* this array.
*/
private static final String[] FORMAT_NAMES =
{"JPEG", "GIF", "PNG", "BMP", "PCX",
"IFF", "RAS", "PBM", "PGM", "PPM",
"PSD", "SWF"};
 
/**
* The names of the MIME types for all supported file formats.
* The FORMAT_xyz int constants can be used as index values for
* this array.
*/
private static final String[] MIME_TYPE_STRINGS =
{"image/jpeg", "image/gif", "image/png", "image/bmp", "image/pcx",
"image/iff", "image/ras", "image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap",
"image/psd", "application/x-shockwave-flash"};
 
private int width;
private int height;
private int bitsPerPixel;
private int format;
private InputStream in;
private DataInput din;
private boolean collectComments = true;
private Vector comments;
private boolean determineNumberOfImages;
private int numberOfImages;
private int physicalHeightDpi;
private int physicalWidthDpi;
private int bitBuf;
private int bitPos;
 
private void addComment(String s) {
if (comments == null) {
comments = new Vector();
}
comments.addElement(s);
}
 
/**
* Call this method after you have provided an input stream or file
* using {@link #setInput(InputStream)} or {@link #setInput(DataInput)}.
* If true is returned, the file format was known and you information
* about its content can be retrieved using the various getXyz methods.
* @return if information could be retrieved from input
*/
public boolean check() {
format = -1;
width = -1;
height = -1;
bitsPerPixel = -1;
numberOfImages = 1;
physicalHeightDpi = -1;
physicalWidthDpi = -1;
comments = null;
try {
int b1 = read() & 0xff;
int b2 = read() & 0xff;
if (b1 == 0x47 && b2 == 0x49) {
return checkGif();
}
else
if (b1 == 0x89 && b2 == 0x50) {
return checkPng();
}
else
if (b1 == 0xff && b2 == 0xd8) {
return checkJpeg();
}
else
if (b1 == 0x42 && b2 == 0x4d) {
return checkBmp();
}
else
if (b1 == 0x0a && b2 < 0x06) {
return checkPcx();
}
else
if (b1 == 0x46 && b2 == 0x4f) {
return checkIff();
}
else
if (b1 == 0x59 && b2 == 0xa6) {
return checkRas();
}
else
if (b1 == 0x50 && b2 >= 0x31 && b2 <= 0x36) {
return checkPnm(b2 - '0');
}
else
if (b1 == 0x38 && b2 == 0x42) {
return checkPsd();
}
else
if (b1 == 0x46 && b2 == 0x57) {
return checkSwf();
}
else {
return false;
}
} catch (IOException ioe) {
return false;
}
}
 
private boolean checkBmp() throws IOException {
byte[] a = new byte[44];
if (read(a) != a.length) {
return false;
}
width = getIntLittleEndian(a, 16);
height = getIntLittleEndian(a, 20);
if (width < 1 || height < 1) {
return false;
}
bitsPerPixel = getShortLittleEndian(a, 26);
if (bitsPerPixel != 1 && bitsPerPixel != 4 &&
bitsPerPixel != 8 && bitsPerPixel != 16 &&
bitsPerPixel != 24 && bitsPerPixel != 32) {
return false;
}
int x = getIntLittleEndian(a, 36);
if (x > 0) {
setPhysicalWidthDpi(x);
}
int y = getIntLittleEndian(a, 40);
if (y > 0) {
setPhysicalHeightDpi(y);
}
format = FORMAT_BMP;
return true;
}
 
private boolean checkGif() throws IOException {
final byte[] GIF_MAGIC_87A = {0x46, 0x38, 0x37, 0x61};
final byte[] GIF_MAGIC_89A = {0x46, 0x38, 0x39, 0x61};
byte[] a = new byte[11]; // 4 from the GIF signature + 7 from the global header
if (read(a) != 11) {
return false;
}
if ((!equals(a, 0, GIF_MAGIC_89A, 0, 4)) &&
(!equals(a, 0, GIF_MAGIC_87A, 0, 4))) {
return false;
}
format = FORMAT_GIF;
width = getShortLittleEndian(a, 4);
height = getShortLittleEndian(a, 6);
int flags = a[8] & 0xff;
bitsPerPixel = ((flags >> 4) & 0x07) + 1;
if (!determineNumberOfImages) {
return true;
}
// skip global color palette
if ((flags & 0x80) != 0) {
int tableSize = (1 << ((flags & 7) + 1)) * 3;
skip(tableSize);
}
numberOfImages = 0;
int blockType;
do
{
blockType = read();
switch(blockType)
{
case(0x2c): // image separator
{
if (read(a, 0, 9) != 9) {
return false;
}
flags = a[8] & 0xff;
int localBitsPerPixel = (flags & 0x07) + 1;
if (localBitsPerPixel > bitsPerPixel) {
bitsPerPixel = localBitsPerPixel;
}
if ((flags & 0x80) != 0) {
skip((1 << localBitsPerPixel) * 3);
}
skip(1); // initial code length
int n;
do
{
n = read();
if (n > 0) {
skip(n);
}
else
if (n == -1) {
return false;
}
}
while (n > 0);
numberOfImages++;
break;
}
case(0x21): // extension
{
int extensionType = read();
if (collectComments && extensionType == 0xfe) {
StringBuffer sb = new StringBuffer();
int n;
do
{
n = read();
if (n == -1) {
return false;
}
if (n > 0) {
for (int i = 0; i < n; i++) {
int ch = read();
if (ch == -1) {
return false;
}
sb.append((char)ch);
}
}
}
while (n > 0);
} else {
int n;
do
{
n = read();
if (n > 0) {
skip(n);
}
else
if (n == -1) {
return false;
}
}
while (n > 0);
}
break;
}
case(0x3b): // end of file
{
break;
}
default:
{
return false;
}
}
}
while (blockType != 0x3b);
return true;
}
 
private boolean checkIff() throws IOException {
byte[] a = new byte[10];
// read remaining 2 bytes of file id, 4 bytes file size
// and 4 bytes IFF subformat
if (read(a, 0, 10) != 10) {
return false;
}
final byte[] IFF_RM = {0x52, 0x4d};
if (!equals(a, 0, IFF_RM, 0, 2)) {
return false;
}
int type = getIntBigEndian(a, 6);
if (type != 0x494c424d && // type must be ILBM...
type != 0x50424d20) { // ...or PBM
return false;
}
// loop chunks to find BMHD chunk
do {
if (read(a, 0, 8) != 8) {
return false;
}
int chunkId = getIntBigEndian(a, 0);
int size = getIntBigEndian(a, 4);
if ((size & 1) == 1) {
size++;
}
if (chunkId == 0x424d4844) { // BMHD chunk
if (read(a, 0, 9) != 9) {
return false;
}
format = FORMAT_IFF;
width = getShortBigEndian(a, 0);
height = getShortBigEndian(a, 2);
bitsPerPixel = a[8] & 0xff;
return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel < 33);
} else {
skip(size);
}
} while (true);
}
 
private boolean checkJpeg() throws IOException {
byte[] data = new byte[12];
while (true) {
if (read(data, 0, 4) != 4) {
return false;
}
int marker = getShortBigEndian(data, 0);
int size = getShortBigEndian(data, 2);
if ((marker & 0xff00) != 0xff00) {
return false; // not a valid marker
}
if (marker == 0xffe0) { // APPx
if (size < 14) {
return false; // APPx header must be larger than 14 bytes
}
if (read(data, 0, 12) != 12) {
return false;
}
final byte[] APP0_ID = {0x4a, 0x46, 0x49, 0x46, 0x00};
if (equals(APP0_ID, 0, data, 0, 5)) {
if (data[7] == 1) {
setPhysicalWidthDpi(getShortBigEndian(data, 8));
setPhysicalHeightDpi(getShortBigEndian(data, 10));
}
else
if (data[7] == 2) {
int x = getShortBigEndian(data, 8);
int y = getShortBigEndian(data, 10);
setPhysicalWidthDpi((int)(x * 2.54f));
setPhysicalHeightDpi((int)(y * 2.54f));
}
}
skip(size - 14);
}
else
if (collectComments && size > 2 && marker == 0xfffe) { // comment
size -= 2;
byte[] chars = new byte[size];
if (read(chars, 0, size) != size) {
return false;
}
String comment = new String(chars, "iso-8859-1");
comment = comment.trim();
System.out.println(comment);
addComment(comment);
}
else
if (marker >= 0xffc0 && marker <= 0xffcf && marker != 0xffc4 && marker != 0xffc8) {
if (read(data, 0, 6) != 6) {
return false;
}
format = FORMAT_JPEG;
bitsPerPixel = (data[0] & 0xff) * (data[5] & 0xff);
width = getShortBigEndian(data, 3);
height = getShortBigEndian(data, 1);
return true;
} else {
skip(size - 2);
}
}
}
 
private boolean checkPcx() throws IOException {
byte[] a = new byte[64];
if (read(a) != a.length) {
return false;
}
if (a[0] != 1) { // encoding, 1=RLE is only valid value
return false;
}
// width / height
int x1 = getShortLittleEndian(a, 2);
int y1 = getShortLittleEndian(a, 4);
int x2 = getShortLittleEndian(a, 6);
int y2 = getShortLittleEndian(a, 8);
if (x1 < 0 || x2 < x1 || y1 < 0 || y2 < y1) {
return false;
}
width = x2 - x1 + 1;
height = y2 - y1 + 1;
// color depth
int bits = a[1];
int planes = a[63];
if (planes == 1 &&
(bits == 1 || bits == 2 || bits == 4 || bits == 8)) {
// paletted
bitsPerPixel = bits;
} else
if (planes == 3 && bits == 8) {
// RGB truecolor
bitsPerPixel = 24;
} else {
return false;
}
setPhysicalWidthDpi(getShortLittleEndian(a, 10));
setPhysicalHeightDpi(getShortLittleEndian(a, 10));
format = FORMAT_PCX;
return true;
}
 
private boolean checkPng() throws IOException {
final byte[] PNG_MAGIC = {0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
byte[] a = new byte[24];
if (read(a) != 24) {
return false;
}
if (!equals(a, 0, PNG_MAGIC, 0, 6)) {
return false;
}
format = FORMAT_PNG;
width = getIntBigEndian(a, 14);
height = getIntBigEndian(a, 18);
bitsPerPixel = a[22] & 0xff;
int colorType = a[23] & 0xff;
if (colorType == 2 || colorType == 6) {
bitsPerPixel *= 3;
}
return true;
}
 
private boolean checkPnm(int id) throws IOException {
if (id < 1 || id > 6) {
return false;
}
final int[] PNM_FORMATS = {FORMAT_PBM, FORMAT_PGM, FORMAT_PPM};
format = PNM_FORMATS[(id - 1) % 3];
boolean hasPixelResolution = false;
String s;
while (true)
{
s = readLine();
if (s != null) {
s = s.trim();
}
if (s == null || s.length() < 1) {
continue;
}
if (s.charAt(0) == '#') { // comment
if (collectComments && s.length() > 1) {
addComment(s.substring(1));
}
continue;
}
if (!hasPixelResolution) { // split "343 966" into width=343, height=966
int spaceIndex = s.indexOf(' ');
if (spaceIndex == -1) {
return false;
}
String widthString = s.substring(0, spaceIndex);
spaceIndex = s.lastIndexOf(' ');
if (spaceIndex == -1) {
return false;
}
String heightString = s.substring(spaceIndex + 1);
try {
width = Integer.parseInt(widthString);
height = Integer.parseInt(heightString);
} catch (NumberFormatException nfe) {
return false;
}
if (width < 1 || height < 1) {
return false;
}
if (format == FORMAT_PBM) {
bitsPerPixel = 1;
return true;
}
hasPixelResolution = true;
}
else
{
int maxSample;
try {
maxSample = Integer.parseInt(s);
} catch (NumberFormatException nfe) {
return false;
}
if (maxSample < 0) {
return false;
}
for (int i = 0; i < 25; i++) {
if (maxSample < (1 << (i + 1))) {
bitsPerPixel = i + 1;
if (format == FORMAT_PPM) {
bitsPerPixel *= 3;
}
return true;
}
}
return false;
}
}
}
 
private boolean checkPsd() throws IOException {
byte[] a = new byte[24];
if (read(a) != a.length) {
return false;
}
final byte[] PSD_MAGIC = {0x50, 0x53};
if (!equals(a, 0, PSD_MAGIC, 0, 2)) {
return false;
}
format = FORMAT_PSD;
width = getIntBigEndian(a, 16);
height = getIntBigEndian(a, 12);
int channels = getShortBigEndian(a, 10);
int depth = getShortBigEndian(a, 20);
bitsPerPixel = channels * depth;
return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 64);
}
 
private boolean checkRas() throws IOException {
byte[] a = new byte[14];
if (read(a) != a.length) {
return false;
}
final byte[] RAS_MAGIC = {0x6a, (byte)0x95};
if (!equals(a, 0, RAS_MAGIC, 0, 2)) {
return false;
}
format = FORMAT_RAS;
width = getIntBigEndian(a, 2);
height = getIntBigEndian(a, 6);
bitsPerPixel = getIntBigEndian(a, 10);
return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 24);
}
 
// Written by Michael Aird.
private boolean checkSwf() throws IOException {
//get rid of the last byte of the signature, the byte of the version and 4 bytes of the size
byte[] a = new byte[6];
if (read(a) != a.length) {
return false;
}
format = FORMAT_SWF;
int bitSize = (int)readUBits( 5 );
int minX = (int)readSBits( bitSize );
int maxX = (int)readSBits( bitSize );
int minY = (int)readSBits( bitSize );
int maxY = (int)readSBits( bitSize );
width = maxX/20; //cause we're in twips
height = maxY/20; //cause we're in twips
setPhysicalWidthDpi(72);
setPhysicalHeightDpi(72);
return (width > 0 && height > 0);
}
 
/**
* Run over String list, return false iff at least one of the arguments
* equals <code>-c</code>.
*/
private static boolean determineVerbosity(String[] args) {
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
if ("-c".equals(args[i])) {
return false;
}
}
}
return true;
}
 
private boolean equals(byte[] a1, int offs1, byte[] a2, int offs2, int num) {
while (num-- > 0) {
if (a1[offs1++] != a2[offs2++]) {
return false;
}
}
return true;
}
 
/**
* If {@link #check()} was successful, returns the image's number of bits per pixel.
* Does not include transparency information like the alpha channel.
* @return number of bits per image pixel
*/
public int getBitsPerPixel() {
return bitsPerPixel;
}
 
/**
* Returns the index'th comment retrieved from the image.
* @throws IllegalArgumentException if index is smaller than 0 or larger than or equal
* to the number of comments retrieved
* @see #getNumberOfComments
*/
public String getComment(int index) {
if (comments == null || index < 0 || index >= comments.size()) {
throw new IllegalArgumentException("Not a valid comment index: " + index);
}
return (String)comments.elementAt(index);
}
 
/**
* If {@link #check()} was successful, returns the image format as one
* of the FORMAT_xyz constants from this class.
* Use {@link #getFormatName()} to get a textual description of the file format.
* @return file format as a FORMAT_xyz constant
*/
public int getFormat() {
return format;
}
 
/**
* If {@link #check()} was successful, returns the image format's name.
* Use {@link #getFormat()} to get a unique number.
* @return file format name
*/
public String getFormatName() {
if (format >= 0 && format < FORMAT_NAMES.length) {
return FORMAT_NAMES[format];
} else {
return "?";
}
}
 
/**
* If {@link #check()} was successful, returns one the image's vertical
* resolution in pixels.
* @return image height in pixels
*/
public int getHeight() {
return height;
}
 
private int getIntBigEndian(byte[] a, int offs) {
return
(a[offs] & 0xff) << 24 |
(a[offs + 1] & 0xff) << 16 |
(a[offs + 2] & 0xff) << 8 |
a[offs + 3] & 0xff;
}
 
private int getIntLittleEndian(byte[] a, int offs) {
return
(a[offs + 3] & 0xff) << 24 |
(a[offs + 2] & 0xff) << 16 |
(a[offs + 1] & 0xff) << 8 |
a[offs] & 0xff;
}
 
/**
* If {@link #check()} was successful, returns a String with the
* MIME type of the format.
* @return MIME type, e.g. <code>image/jpeg</code>
*/
public String getMimeType() {
if (format >= 0 && format < MIME_TYPE_STRINGS.length) {
return MIME_TYPE_STRINGS[format];
} else {
return null;
}
}
 
/**
* If {@link #check()} was successful and {@link #setCollectComments(boolean)} was called with
* <code>true</code> as argument, returns the number of comments retrieved
* from the input image stream / file.
* Any number &gt;= 0 and smaller than this number of comments is then a
* valid argument for the {@link #getComment(int)} method.
* @return number of comments retrieved from input image
*/
public int getNumberOfComments()
{
if (comments == null) {
return 0;
} else {
return comments.size();
}
}
 
/**
* Returns the number of images in the examined file.
* Assumes that <code>setDetermineImageNumber(true);</code> was called before
* a successful call to {@link #check()}.
* This value can currently be only different from <code>1</code> for GIF images.
* @return number of images in file
*/
public int getNumberOfImages()
{
return numberOfImages;
}
 
/**
* Returns the physical height of this image in dots per inch (dpi).
* Assumes that {@link #check()} was successful.
* Returns <code>-1</code> on failure.
* @return physical height (in dpi)
* @see #getPhysicalWidthDpi()
* @see #getPhysicalHeightInch()
*/
public int getPhysicalHeightDpi() {
return physicalHeightDpi;
}
 
/**
* If {@link #check()} was successful, returns the physical width of this image in dpi (dots per inch)
* or -1 if no value could be found.
* @return physical height (in dpi)
* @see #getPhysicalHeightDpi()
* @see #getPhysicalWidthDpi()
* @see #getPhysicalWidthInch()
*/
public float getPhysicalHeightInch() {
int h = getHeight();
int ph = getPhysicalHeightDpi();
if (h > 0 && ph > 0) {
return ((float)h) / ((float)ph);
} else {
return -1.0f;
}
}
 
/**
* If {@link #check()} was successful, returns the physical width of this image in dpi (dots per inch)
* or -1 if no value could be found.
* @return physical width (in dpi)
* @see #getPhysicalHeightDpi()
* @see #getPhysicalWidthInch()
* @see #getPhysicalHeightInch()
*/
public int getPhysicalWidthDpi() {
return physicalWidthDpi;
}
 
/**
* Returns the physical width of an image in inches, or
* <code>-1.0f</code> if width information is not available.
* Assumes that {@link #check} has been called successfully.
* @return physical width in inches or <code>-1.0f</code> on failure
* @see #getPhysicalWidthDpi
* @see #getPhysicalHeightInch
*/
public float getPhysicalWidthInch() {
int w = getWidth();
int pw = getPhysicalWidthDpi();
if (w > 0 && pw > 0) {
return ((float)w) / ((float)pw);
} else {
return -1.0f;
}
}
 
private int getShortBigEndian(byte[] a, int offs) {
return
(a[offs] & 0xff) << 8 |
(a[offs + 1] & 0xff);
}
 
private int getShortLittleEndian(byte[] a, int offs) {
return (a[offs] & 0xff) | (a[offs + 1] & 0xff) << 8;
}
 
/**
* If {@link #check()} was successful, returns one the image's horizontal
* resolution in pixels.
* @return image width in pixels
*/
public int getWidth() {
return width;
}
 
/**
* To use this class as a command line application, give it either
* some file names as parameters (information on them will be
* printed to standard output, one line per file) or call
* it with no parameters. It will then check data given to it
* via standard input.
* @param args the program arguments which must be file names
*/
public static void main(String[] args) {
ImageInfo imageInfo = new ImageInfo();
boolean verbose = determineVerbosity(args);
if (args.length == 0) {
run(null, System.in, imageInfo, verbose);
} else {
int index = 0;
while (index < args.length) {
FileInputStream instream = null;
try {
String filename = args[index++];
System.out.print(filename + ";");
instream = new FileInputStream(filename);
run(filename, instream, imageInfo, verbose);
instream.close();
} catch (Exception e) {
System.out.println(e);
try {
instream.close();
} catch (Exception ee) {
}
}
}
}
}
 
private static void print(String sourceName, ImageInfo ii, boolean verbose) {
if (verbose) {
printVerbose(sourceName, ii);
} else {
printCompact(sourceName, ii);
}
}
 
private static void printCompact(String sourceName, ImageInfo imageInfo) {
System.out.println(
imageInfo.getFormatName() + ";" +
imageInfo.getMimeType() + ";" +
imageInfo.getWidth() + ";" +
imageInfo.getHeight() + ";" +
imageInfo.getBitsPerPixel() + ";" +
imageInfo.getNumberOfImages() + ";" +
imageInfo.getPhysicalWidthDpi() + ";" +
imageInfo.getPhysicalHeightDpi() + ";" +
imageInfo.getPhysicalWidthInch() + ";" +
imageInfo.getPhysicalHeightInch()
);
}
 
private static void printLine(int indentLevels, String text, float value, float minValidValue) {
if (value < minValidValue) {
return;
}
printLine(indentLevels, text, Float.toString(value));
}
 
private static void printLine(int indentLevels, String text, int value, int minValidValue) {
if (value >= minValidValue) {
printLine(indentLevels, text, Integer.toString(value));
}
}
 
private static void printLine(int indentLevels, String text, String value) {
if (value == null || value.length() == 0) {
return;
}
while (indentLevels-- > 0) {
System.out.print("\t");
}
if (text != null && text.length() > 0) {
System.out.print(text);
System.out.print(" ");
}
System.out.println(value);
}
 
private static void printVerbose(String sourceName, ImageInfo ii) {
printLine(0, null, sourceName);
printLine(1, "File format: ", ii.getFormatName());
printLine(1, "MIME type: ", ii.getMimeType());
printLine(1, "Width (pixels): ", ii.getWidth(), 1);
printLine(1, "Height (pixels): ", ii.getHeight(), 1);
printLine(1, "Bits per pixel: ", ii.getBitsPerPixel(), 1);
printLine(1, "Number of images: ", ii.getNumberOfImages(), 1);
printLine(1, "Physical width (dpi): ", ii.getPhysicalWidthDpi(), 1);
printLine(1, "Physical height (dpi): ", ii.getPhysicalHeightDpi(), 1);
printLine(1, "Physical width (inches): ", ii.getPhysicalWidthInch(), 1.0f);
printLine(1, "Physical height (inches): ", ii.getPhysicalHeightInch(), 1.0f);
int numComments = ii.getNumberOfComments();
printLine(1, "Number of textual comments: ", numComments, 1);
if (numComments > 0) {
for (int i = 0; i < numComments; i++) {
printLine(2, null, ii.getComment(i));
}
}
}
 
private int read() throws IOException {
if (in != null) {
return in.read();
} else {
return din.readByte();
}
}
 
private int read(byte[] a) throws IOException {
if (in != null) {
return in.read(a);
} else {
din.readFully(a);
return a.length;
}
}
 
private int read(byte[] a, int offset, int num) throws IOException {
if (in != null) {
return in.read(a, offset, num);
} else {
din.readFully(a, offset, num);
return num;
}
}
 
private String readLine() throws IOException {
return readLine(new StringBuffer());
}
 
private String readLine(StringBuffer sb) throws IOException {
boolean finished;
do {
int value = read();
finished = (value == -1 || value == 10);
if (!finished) {
sb.append((char)value);
}
} while (!finished);
return sb.toString();
}
 
/**
* Read an unsigned value from the given number of bits
*/
public long readUBits( int numBits ) throws IOException
{
if (numBits == 0) {
return 0;
}
int bitsLeft = numBits;
long result = 0;
if (bitPos == 0) { //no value in the buffer - read a byte
if (in != null) {
bitBuf = in.read();
} else {
bitBuf = din.readByte();
}
bitPos = 8;
}
 
while( true )
{
int shift = bitsLeft - bitPos;
if( shift > 0 )
{
// Consume the entire buffer
result |= bitBuf << shift;
bitsLeft -= bitPos;
 
// Get the next byte from the input stream
if (in != null) {
bitBuf = in.read();
} else {
bitBuf = din.readByte();
}
bitPos = 8;
}
else
{
// Consume a portion of the buffer
result |= bitBuf >> -shift;
bitPos -= bitsLeft;
bitBuf &= 0xff >> (8 - bitPos); // mask off the consumed bits
 
return result;
}
}
}
 
/**
* Read a signed value from the given number of bits
*/
private int readSBits( int numBits ) throws IOException
{
// Get the number as an unsigned value.
long uBits = readUBits( numBits );
 
// Is the number negative?
if( ( uBits & (1L << (numBits - 1))) != 0 )
{
// Yes. Extend the sign.
uBits |= -1L << numBits;
}
 
return (int)uBits;
}
 
/**
* Reset the bit buffer
*/
public void synchBits()
{
bitBuf = 0;
bitPos = 0;
}
 
private String readLine(int firstChar) throws IOException {
StringBuffer result = new StringBuffer();
result.append((char)firstChar);
return readLine(result);
}
 
private static void run(String sourceName, InputStream in, ImageInfo imageInfo, boolean verbose) {
imageInfo.setInput(in);
imageInfo.setDetermineImageNumber(true);
imageInfo.setCollectComments(verbose);
if (imageInfo.check()) {
print(sourceName, imageInfo, verbose);
}
}
 
/**
* Specify whether textual comments are supposed to be extracted from input.
* Default is <code>false</code>.
* If enabled, comments will be added to an internal list.
* @param newValue if <code>true</code>, this class will read comments
* @see #getNumberOfComments
* @see #getComment
*/
public void setCollectComments(boolean newValue)
{
collectComments = newValue;
}
 
/**
* Specify whether the number of images in a file is to be
* determined - default is <code>false</code>.
* This is a special option because some file formats require running over
* the entire file to find out the number of images, a rather time-consuming
* task.
* Not all file formats support more than one image.
* If this method is called with <code>true</code> as argument,
* the actual number of images can be queried via
* {@link #getNumberOfImages()} after a successful call to
* {@link #check()}.
* @param newValue will the number of images be determined?
* @see #getNumberOfImages
*/
public void setDetermineImageNumber(boolean newValue)
{
determineNumberOfImages = newValue;
}
 
/**
* Set the input stream to the argument stream (or file).
* Note that {@link java.io.RandomAccessFile} implements
* {@link java.io.DataInput}.
* @param dataInput the input stream to read from
*/
public void setInput(DataInput dataInput) {
din = dataInput;
in = null;
}
 
/**
* Set the input stream to the argument stream (or file).
* @param inputStream the input stream to read from
*/
public void setInput(InputStream inputStream) {
in = inputStream;
din = null;
}
 
private void setPhysicalHeightDpi(int newValue) {
physicalWidthDpi = newValue;
}
 
private void setPhysicalWidthDpi(int newValue) {
physicalHeightDpi = newValue;
}
 
private void skip(int num) throws IOException {
if (in != null) {
in.skip(num);
} else {
din.skipBytes(num);
}
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/util/FileUtils.java
0,0 → 1,79
package ak.photoalbum.util;
 
import java.util.Map;
import java.util.HashMap;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
 
public class FileUtils
{
protected static final int COPY_BUFFER_SIZE = 4096;
protected static final Map MIME_MAP = new HashMap();
 
static {
MIME_MAP.put("jpg", "image/jpeg");
MIME_MAP.put("jpeg", "image/jpeg");
MIME_MAP.put("png", "image/png");
}
 
public static String getMime(String ext)
{
if(ext == null)
return null;
else
return (String)MIME_MAP.get(ext.toLowerCase());
}
 
public static void copyStreams(InputStream in, OutputStream out)
throws IOException
{
byte[] buf = new byte[COPY_BUFFER_SIZE];
int len;
 
while((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
}
 
public static String extractFileName(String fullName)
{
if(fullName == null) return null;
 
int pointPos = fullName.lastIndexOf('.');
 
if(pointPos <= 0)
return fullName;
else
return fullName.substring(0, pointPos);
}
 
public static String extractFileExt(String fullName)
{
if(fullName == null) return null;
 
int pointPos = fullName.lastIndexOf('.');
 
if(pointPos <= 0)
return "";
else
return fullName.substring(pointPos + 1);
}
 
public static String replaceFileSeparator(String path, String replaceWith)
{
String sep = File.separator;
StringBuffer buf = new StringBuffer();
int pos = -1;
int oldPos = -1;
 
while((pos = path.indexOf(sep, pos + 1)) >= 0) {
buf.append(path.substring(oldPos + 1, pos)).append(replaceWith);
oldPos = pos;
}
buf.append(path.substring(oldPos + 1));
 
return buf.toString();
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/util/FileNameComparator.java
0,0 → 1,31
package ak.photoalbum.util;
 
import java.util.Comparator;
import java.io.File;
 
public class FileNameComparator
implements Comparator
{
private boolean dirFirst;
 
public FileNameComparator(boolean dirFirst)
{
this.dirFirst = dirFirst;
}
 
public int compare(Object o1, Object o2)
throws ClassCastException
{
return compare((File)o1, (File)o2);
}
 
public int compare(File f1, File f2)
{
boolean d1 = f1.isDirectory();
boolean d2 = f2.isDirectory();
 
if(d1 && !d2) return (dirFirst ? -1 : 1);
else if(!d1 && d2) return (dirFirst ? 1 : -1);
else return f1.getName().compareToIgnoreCase(f2.getName());
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/util/ImagesFilter.java
0,0 → 1,37
package ak.photoalbum.util;
 
import java.io.File;
import java.io.FileFilter;
import java.util.Set;
import java.util.HashSet;
import java.util.StringTokenizer;
 
public class ImagesFilter
implements FileFilter
{
Set extentions = new HashSet();
 
public ImagesFilter(String imagesMask)
{
StringTokenizer tokenizer = new StringTokenizer(imagesMask, ";");
 
while(tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
 
if(token.startsWith("*.")) token = token.substring(2);
 
extentions.add(token.toLowerCase());
}
}
 
public boolean accept(File pathname)
{
if(pathname.isDirectory()) {
return !pathname.getName().startsWith("."); // skip hidden dirs
}
else{
return extentions.contains(FileUtils.extractFileExt(
pathname.getName().toLowerCase()));
}
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/IndexAction.java
0,0 → 1,45
package ak.photoalbum.webapp;
 
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.log4j.Logger;
import ak.photoalbum.util.FileUtils;
 
public final class IndexAction
extends BaseAction
{
private static final Logger logger = Logger.getLogger(IndexAction.class);
 
public ActionForward executeAction(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception
{
PathForm theForm = (PathForm)form;
String dir = theForm.getPath();
IndexEntry entry = new IndexEntry();
IndexEntry top = new IndexEntry();
IndexEntry prev = new IndexEntry();
IndexEntry current = new IndexEntry();
IndexEntry next = new IndexEntry();
List index;
 
if(dir == null) dir = ""; // the images root
 
logger.info("get index for " + dir);
Logic.getLogic().getEntry(dir, entry, top, prev, current, next);
index = Logic.getLogic().listDirectory(dir);
 
request.setAttribute("dir", FileUtils.replaceFileSeparator(dir, " - "));
request.setAttribute("index", index);
request.setAttribute("top", top);
request.setAttribute("prevEntry", prev);
request.setAttribute("current", current);
request.setAttribute("nextEntry", next);
 
return mapping.findForward("success");
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/BaseAction.java
0,0 → 1,51
package ak.photoalbum.webapp;
 
import java.util.List;
import java.io.FileNotFoundException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.log4j.Logger;
import ak.photoalbum.util.FileUtils;
 
public abstract class BaseAction
extends Action
{
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception
{
Logger logger = Logger.getLogger(getClass());
 
try {
return executeAction(mapping, form, request, response);
}
catch(LogicSecurityException ex) {
logger.warn("forbidden: " + ex.getMessage());
logger.debug("forbidden", ex);
response.sendError(HttpServletResponse.SC_FORBIDDEN);
 
return null;
}
catch(FileNotFoundException ex) {
logger.warn("not found: " + ex.getMessage());
logger.debug("not found", ex);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
 
return null;
}
catch(Exception ex) {
logger.error("exception", ex);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
 
return null;
}
}
 
public abstract ActionForward executeAction(ActionMapping mapping,
ActionForm form, HttpServletRequest request, HttpServletResponse response)
throws Exception;
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/InitServlet.java
0,0 → 1,50
package ak.photoalbum.webapp;
 
import javax.servlet.ServletException;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServlet;
 
public class InitServlet
extends HttpServlet
{
public void init()
throws ServletException
{
ServletConfig config = getServletConfig();
 
String imagesRoot;
String imagesMask;
String cacheDir;
String thumbnailFormat;
Integer smallWidth = null;
Integer smallHeight = null;
Integer mediumWidth = null;
Integer mediumHeight = null;
Integer columns = null;
String dirTemplate;
String dirThumbnailPositions;
 
imagesRoot = config.getInitParameter("images root");
imagesMask = config.getInitParameter("images mask");
cacheDir = config.getInitParameter("cache dir");
thumbnailFormat = config.getInitParameter("thumbnail format");
 
if(config.getInitParameter("small width") != null)
smallWidth = new Integer(config.getInitParameter("small width"));
if(config.getInitParameter("small height") != null)
smallHeight = new Integer(config.getInitParameter("small height"));
if(config.getInitParameter("medium width") != null)
mediumWidth = new Integer(config.getInitParameter("medium width"));
if(config.getInitParameter("medium heught") != null)
mediumHeight = new Integer(config.getInitParameter("medium heught"));
if(config.getInitParameter("columns") != null)
columns = new Integer(config.getInitParameter("columns"));
 
dirTemplate = config.getInitParameter("dir template");
dirThumbnailPositions = config.getInitParameter("dir thumbnails positions");
 
Logic.getLogic().init(imagesRoot, imagesMask, cacheDir, thumbnailFormat,
smallWidth, smallHeight, mediumWidth, mediumHeight, columns,
dirTemplate, dirThumbnailPositions);
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/IndexEntry.java
0,0 → 1,94
package ak.photoalbum.webapp;
 
import java.io.File;
 
public class IndexEntry
{
private File file;
private String path;
private String title;
private boolean isDir;
private int width;
private int height;
 
public IndexEntry()
{
}
 
public IndexEntry(File file, String path, String title, boolean isDir,
int width, int height)
{
this.file = file;
this.path = path;
this.title = title;
this.isDir = isDir;
this.width = width;
this.height = height;
}
 
public File getFile()
{
return file;
}
 
public void setFile(File file)
{
this.file = file;
}
 
public String getPath()
{
return path;
}
 
public void setPath(String path)
{
this.path = path;
}
 
public String getTitle()
{
return title;
}
 
public void setTitle(String title)
{
this.title = title;
}
 
public boolean getIsDir()
{
return isDir;
}
 
public void setIsDir(boolean isDir)
{
this.isDir = isDir;
}
 
public int getWidth()
{
return width;
}
 
public void setWidth(int width)
{
this.width = width;
}
 
public int getHeight()
{
return height;
}
 
public void setHeight(int height)
{
this.height = height;
}
 
public String toString()
{
return super.toString() + ": " + file + " - " + path + " - " + title
+ " " + width + "x" + height;
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/PageAction.java
0,0 → 1,41
package ak.photoalbum.webapp;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.log4j.Logger;
 
public final class PageAction
extends BaseAction
{
private static final Logger logger = Logger.getLogger(IndexAction.class);
 
public ActionForward executeAction(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception
{
PathForm theForm = (PathForm)form;
String page = theForm.getPath();
IndexEntry entry = new IndexEntry();
IndexEntry index = new IndexEntry();
IndexEntry prev = new IndexEntry();
IndexEntry current = new IndexEntry();
IndexEntry next = new IndexEntry();
 
if(page == null) page = ""; // the images root
 
logger.info("get page " + page);
Logic.getLogic().getEntry(page, entry, index, prev, current, next);
 
request.setAttribute("page", page);
request.setAttribute("entry", entry);
request.setAttribute("index", index);
request.setAttribute("prevEntry", prev);
request.setAttribute("current", current);
request.setAttribute("nextEntry", next);
 
return mapping.findForward("success");
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/LogicException.java
0,0 → 1,10
package ak.photoalbum.webapp;
 
public class LogicException
extends Exception
{
public LogicException(String message)
{
super(message);
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/Logic.java
0,0 → 1,342
package ak.photoalbum.webapp;
 
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.StringTokenizer;
import java.io.File;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.io.FileNotFoundException;
import java.net.URLEncoder;
import org.apache.log4j.Logger;
import ak.photoalbum.images.Thumbnailer;
import ak.photoalbum.images.ThumbnailPosition;
import ak.photoalbum.util.FileUtils;
import ak.photoalbum.util.FileNameComparator;
import ak.photoalbum.util.ImagesFilter;
 
public class Logic
{
protected static final int DEFAULT_COLUMNS = 4;
protected static final String URL_ENCODING = "UTF-8";
 
protected Logger logger;
protected Thumbnailer thumbnailer;
protected File imagesRoot;
protected int columns = DEFAULT_COLUMNS;
protected ImagesFilter imagesFilter;
protected Comparator fileNameComparator = new FileNameComparator(true);
 
protected Logic()
{
this.logger = Logger.getLogger(this.getClass());
}
 
public void init(String imagesRoot, String imagesMask,
String cacheDir, String thumbnailFormat,
Integer smallWidth, Integer smallHeight,
Integer mediumWidth, Integer mediumHeight, Integer columns,
String dirTemplate, String dirThumbnailPositions)
{
this.imagesRoot = new File(imagesRoot);
this.imagesFilter = new ImagesFilter(imagesMask);
if(columns != null) this.columns = columns.intValue();
 
this.thumbnailer = new Thumbnailer();
this.thumbnailer.setImagesRoot(this.imagesRoot);
this.thumbnailer.setCacheDir(new File(cacheDir));
 
if(thumbnailFormat != null)
this.thumbnailer.setFormat(thumbnailFormat);
if(smallWidth != null)
this.thumbnailer.setSmallWidth(smallWidth.intValue());
if(smallHeight != null)
this.thumbnailer.setSmallHeight(smallHeight.intValue());
if(mediumWidth != null)
this.thumbnailer.setMediumWidth(mediumWidth.intValue());
if(mediumHeight != null)
this.thumbnailer.setMediumHeight(mediumHeight.intValue());
 
thumbnailer.setResizer(new ak.photoalbum.images.jiu.JiuResizer());
thumbnailer.setImagesFilter(this.imagesFilter);
thumbnailer.setDirTemplate(new File(dirTemplate));
thumbnailer.setDirThumbnailPositions(
parseThumbnailPositions(dirThumbnailPositions));
 
try {
thumbnailer.startup();
}
catch(Exception ex) {
logger.error("init thumbnailer", ex);
}
 
logger.info("started");
}
 
public void buildCache()
throws IOException
{
thumbnailer.buildCache();
}
 
public void getEntry(String path, IndexEntry page,
IndexEntry index, IndexEntry prev, IndexEntry current, IndexEntry next)
throws IOException, LogicException
{
File file = new File(imagesRoot, path);
 
securePath(imagesRoot, file);
 
if(!file.exists())
throw new FileNotFoundException(
"[" + file.getCanonicalPath() + "] not found");
 
File dir = file.getParentFile();
File[] children = dir.listFiles(imagesFilter);
int pos;
 
Arrays.sort(children, fileNameComparator);
pos = Arrays.binarySearch(children, file, fileNameComparator);
 
if(pos < 0)
throw new FileNotFoundException("[" + file.getCanonicalPath()
+ "] not found in [" + dir.getCanonicalPath() + "]");
 
setEntryInfo(page, file, false);
setEntryInfo(current, file, true);
setEntryInfo(index, dir, true);
if(pos > 0) setEntryInfo(prev, children[pos-1], true);
if(pos < children.length-1) setEntryInfo(next, children[pos+1], true);
}
 
protected void setEntryInfo(IndexEntry entry, File file, boolean small)
throws IOException
{
String title = file.getName();
int[] size;
String path = getPath(file);
 
if(file.isDirectory()) {
size = thumbnailer.getDirSize(file);
}
else {
title = FileUtils.extractFileName(title);
 
if(small)
size = thumbnailer.getSmallSize(file);
else
size = thumbnailer.getMediumSize(file);
}
 
entry.setFile(file);
entry.setPath(path == null ? null : URLEncoder.encode(path, URL_ENCODING));
entry.setTitle(title);
entry.setIsDir(file.isDirectory());
entry.setWidth(size[0]);
entry.setHeight(size[1]);
}
 
public String getThumbnailMime()
{
return thumbnailer.getMime();
}
 
public String getOriginMime(String path)
throws IOException, LogicException
{
File file = new File(imagesRoot, path);
 
if(!file.exists()) return null;
securePath(imagesRoot, file);
 
return FileUtils.getMime(FileUtils.extractFileExt(path));
}
 
public void writeDir(String path, OutputStream out)
throws IOException, LogicException
{
File file = new File(imagesRoot, path);
 
securePath(imagesRoot, file);
thumbnailer.writeDir(file, out);
}
 
public void writeSmall(String path, OutputStream out)
throws IOException, LogicException
{
File file = new File(imagesRoot, path);
 
securePath(imagesRoot, file);
thumbnailer.writeSmall(file, out);
}
 
public void writeMedium(String path, OutputStream out)
throws IOException, LogicException
{
File file = new File(imagesRoot, path);
 
securePath(imagesRoot, file);
thumbnailer.writeMedium(file, out);
}
 
public void writeOrigin(String path, OutputStream out)
throws IOException, LogicException
{
FileInputStream in = null;
File file = new File(imagesRoot, path);
 
securePath(imagesRoot, file);
 
try {
in = new FileInputStream(file);
FileUtils.copyStreams(in, out);
}
finally {
if(in != null) in.close();
}
}
 
public List listDirectory(String dirName)
throws UnsupportedEncodingException, IOException, LogicException
{
File dir = new File(imagesRoot, dirName);
 
securePath(imagesRoot, dir);
if(!dir.exists()) return null;
 
File[] children = dir.listFiles(imagesFilter);
List rows = new ArrayList();
int pos = 0;
 
Arrays.sort(children, fileNameComparator);
 
while(pos < children.length) {
List row = new ArrayList();
int rowPos = 0;
 
rows.add(row);
 
while(rowPos < columns && pos < children.length) {
String path = getPath(children[pos]);
String title = children[pos].getName();
int[] size;
 
if(children[pos].isDirectory()) {
size = thumbnailer.getDirSize(children[pos]);
}
else {
size = thumbnailer.getSmallSize(children[pos]);
title = FileUtils.extractFileName(title);
}
 
row.add(new IndexEntry(children[pos],
URLEncoder.encode(path, URL_ENCODING),
title, children[pos].isDirectory(), size[0], size[1]));
rowPos++;
pos++;
}
 
while(rowPos < columns) {
row.add(null);
rowPos++;
}
}
 
return rows;
}
 
protected String getPath(File file)
throws IOException
{
String path = file.getCanonicalPath();
String rootPath = imagesRoot.getCanonicalPath();
 
if(path.equals(rootPath)) return "";
if(!rootPath.endsWith(File.separator)) rootPath += File.separator;
 
if(!path.startsWith(rootPath))
return null;
 
return path.substring(rootPath.length());
}
 
protected ThumbnailPosition[] parseThumbnailPositions(String str)
{
List list = new ArrayList();
StringTokenizer tokenizer = new StringTokenizer(str, ";");
 
while(tokenizer.hasMoreTokens()) {
StringTokenizer tokenParser
= new StringTokenizer(tokenizer.nextToken(), ",");
int x = Integer.parseInt(tokenParser.nextToken().trim());
int y = Integer.parseInt(tokenParser.nextToken().trim());
int width = Integer.parseInt(tokenParser.nextToken().trim());
int height = Integer.parseInt(tokenParser.nextToken().trim());
String horAlignStr = tokenParser.nextToken().trim().toLowerCase();
String vertAlignStr = tokenParser.nextToken().trim().toLowerCase();
int horAlign;
int vertAlign;
 
if("l".equals(horAlignStr))
horAlign = ThumbnailPosition.ALIGN_HOR_LEFT;
else if("r".equals(horAlignStr))
horAlign = ThumbnailPosition.ALIGN_HOR_RIGHT;
else if("c".equals(horAlignStr))
horAlign = ThumbnailPosition.ALIGN_HOR_CENTER;
else
throw new RuntimeException(
"Cannot parse " + horAlignStr + " as horizontal alignment");
 
if("t".equals(vertAlignStr))
vertAlign = ThumbnailPosition.ALIGN_VERT_TOP;
else if("b".equals(vertAlignStr))
vertAlign = ThumbnailPosition.ALIGN_VERT_BOTTOM;
else if("c".equals(vertAlignStr))
vertAlign = ThumbnailPosition.ALIGN_VERT_CENTER;
else
throw new RuntimeException(
"Cannot parse " + vertAlignStr + " as vertical alignment");
 
list.add(new ThumbnailPosition(x, y, width, height, horAlign, vertAlign));
}
 
ThumbnailPosition[] res = new ThumbnailPosition[list.size()];
 
for(int i = 0; i < res.length; i++)
res[i] = (ThumbnailPosition)list.get(i);
 
return res;
}
 
protected static final Logic instance = new Logic();
 
public static Logic getLogic()
{
return instance;
}
 
/**
* checks if given file is really under the parent directory
*/
protected void securePath(File parentDir, File file)
throws IOException, LogicException
{
if(parentDir == null || file == null) return;
 
File partFile = file.getCanonicalFile();
 
parentDir = parentDir.getCanonicalFile();
while(partFile != null) {
if(partFile.equals(parentDir)) return;
partFile = partFile.getParentFile();
}
 
throw new LogicSecurityException(
"[" + file.getCanonicalPath() + "] is outside of directory ["
+ parentDir.getCanonicalPath() + "]");
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/ImageAction.java
0,0 → 1,45
package ak.photoalbum.webapp;
 
import java.io.FileNotFoundException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.log4j.Logger;
 
public final class ImageAction
extends BaseAction
{
private static final Logger logger = Logger.getLogger(ImageAction.class);
 
public ActionForward executeAction(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception
{
PathForm theForm = (PathForm)form;
String path = theForm.getPath();
Logic logic = Logic.getLogic();
 
logger.info("get image " + mapping.getParameter() + " for " + path);
 
if("dir".equals(mapping.getParameter())) {
response.setContentType(logic.getThumbnailMime());
logic.writeDir(path, response.getOutputStream());
}
else if("small".equals(mapping.getParameter())) {
response.setContentType(logic.getThumbnailMime());
logic.writeSmall(path, response.getOutputStream());
}
else if("medium".equals(mapping.getParameter())) {
response.setContentType(logic.getThumbnailMime());
logic.writeMedium(path, response.getOutputStream());
}
else if("origin".equals(mapping.getParameter())) {
response.setContentType(logic.getOriginMime(path));
logic.writeOrigin(path, response.getOutputStream());
}
 
return null;
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/PathForm.java
0,0 → 1,31
package ak.photoalbum.webapp;
 
import javax.servlet.http.HttpServletRequest;
import org.apache.struts.action.ActionError;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionMapping;
 
public class PathForm
extends ActionForm
{
protected String path;
 
public String getPath()
{
return path;
}
 
public void setPath(String path)
{
this.path = path;
}
 
public ActionErrors validate(ActionMapping mapping,
HttpServletRequest request)
{
ActionErrors errors = new ActionErrors();
 
return errors;
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/BuildCacheAction.java
0,0 → 1,25
package ak.photoalbum.webapp;
 
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.log4j.Logger;
 
public final class BuildCacheAction
extends Action
{
private static final Logger logger = Logger.getLogger(IndexAction.class);
 
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception
{
Logic.getLogic().buildCache();
 
return mapping.findForward("success");
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/webapp/LogicSecurityException.java
0,0 → 1,10
package ak.photoalbum.webapp;
 
public class LogicSecurityException
extends LogicException
{
public LogicSecurityException(String message)
{
super(message);
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/images/ImageResizer.java
0,0 → 1,9
package ak.photoalbum.images;
 
import java.awt.Image;
import java.awt.image.BufferedImage;
 
public interface ImageResizer
{
public BufferedImage resize(Image origin, int newWidth, int newHeight);
}
/PhotoAlbum/trunk/src/ak/photoalbum/images/CachedFile.java
0,0 → 1,99
package ak.photoalbum.images;
 
import java.io.File;
 
class CachedFile
{
private File file;
private File originFile;
private long timestamp;
private long originTimestamp;
private int width;
private int height;
 
public CachedFile()
{
}
 
public CachedFile(File file, File originFile, long timestamp,
long originTimestamp, int width, int height)
{
this.file = file;
this.originFile = originFile;
this.timestamp = timestamp;
this.originTimestamp = originTimestamp;
this.width = width;
this.height = height;
}
 
public File getFile()
{
return file;
}
 
public void setFile(File file)
{
this.file = file;
}
 
public File getOriginFile()
{
return originFile;
}
 
public void setOriginFile(File file)
{
this.originFile = file;
}
 
public long getTimestamp()
{
return timestamp;
}
 
public void setTimestamp(long timestamp)
{
this.timestamp = timestamp;
}
 
public long getOriginTimestamp()
{
return originTimestamp;
}
 
public void setOriginTimestamp(long timestamp)
{
this.originTimestamp = timestamp;
}
 
public int getWidth()
{
return width;
}
 
public void setWidth(int width)
{
this.width = width;
}
 
public int getHeight()
{
return height;
}
 
public void setHeight(int height)
{
this.height = height;
}
 
public String toString()
{
return super.toString() + ":"
+ " file=" + file
+ " originFile=" + originFile
+ " timestamp=" + timestamp
+ " originTimestamp=" + originTimestamp
+ " width=" + width
+ " height=" + height;
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/images/jiu/JiuResizer.java
0,0 → 1,205
package ak.photoalbum.images.jiu;
 
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.PixelGrabber;
import java.awt.image.ImageObserver;
 
import org.apache.log4j.Logger;
 
import ak.photoalbum.images.ImageResizer;
 
import net.sourceforge.jiu.data.BilevelImage;
import net.sourceforge.jiu.data.Gray8Image;
import net.sourceforge.jiu.data.MemoryRGB24Image;
import net.sourceforge.jiu.data.Palette;
import net.sourceforge.jiu.data.Paletted8Image;
import net.sourceforge.jiu.data.PixelImage;
import net.sourceforge.jiu.data.RGB24Image;
import net.sourceforge.jiu.data.RGBIndex;
import net.sourceforge.jiu.geometry.Resample;
 
public class JiuResizer
implements ImageResizer
{
protected static final int DEFAULT_ALPHA = 0xff000000;
protected static final int FILTER_TYPE = Resample.FILTER_TYPE_LANCZOS3;
/*
Possible type small, ms medium, ms quality
-----------------------------------------------------------
FILTER_TYPE_BOX 2.4 3.5 very bad
FILTER_TYPE_TRIANGLE 2.9 5.2
FILTER_TYPE_B_SPLINE 3.6 7.2
FILTER_TYPE_BELL 3.5 6.1
FILTER_TYPE_HERMITE 2.9 5.3
FILTER_TYPE_LANCZOS3 4.7 9.7 the best
FILTER_TYPE_MITCHELL 3.6 7.1
*/
 
protected Logger logger;
 
public JiuResizer()
{
this.logger = Logger.getLogger(this.getClass());
}
 
public BufferedImage resize(Image origin, int newWidth, int newHeight)
{
try {
RGB24Image image = convertImageToRGB24Image(origin);
Resample resample;
 
if(image == null) return null;
if(image.getWidth() == newWidth && image.getHeight() == newHeight)
return convertToAwtImage(image, DEFAULT_ALPHA);
 
resample = new Resample();
resample.setInputImage(image);
resample.setSize(newWidth, newHeight);
resample.setFilter(FILTER_TYPE);
resample.process();
 
return convertToAwtImage(resample.getOutputImage(), DEFAULT_ALPHA);
}
catch(Exception ex) {
ex.printStackTrace();
 
throw new RuntimeException(ex.getMessage());
}
}
 
protected static BufferedImage convertToAwtImage(PixelImage image, int alpha)
{
if (image == null)
{
return null;
}
if (image instanceof RGB24Image)
{
return convertToAwtImage((RGB24Image)image, alpha);
}
else
if (image instanceof Gray8Image)
{
return null; //convertToAwtImage((Gray8Image)image, alpha);
}
else
if (image instanceof Paletted8Image)
{
return null; //convertToAwtImage((Paletted8Image)image, alpha);
}
else
if (image instanceof BilevelImage)
{
return null; //convertToAwtImage((BilevelImage)image, alpha);
}
else
{
return null;
}
}
 
protected static BufferedImage convertToAwtImage(RGB24Image image, int alpha)
{
if (image == null)
{
return null;
}
 
int width = image.getWidth();
int height = image.getHeight();
if (width < 1 || height < 1)
{
return null;
}
 
int[] pixels = new int[width];
byte[] red = new byte[width];
byte[] green = new byte[width];
byte[] blue = new byte[width];
 
BufferedImage newImage = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
 
for (int y = 0; y < height; y++)
{
image.getByteSamples(RGBIndex.INDEX_RED, 0, y, width, 1, red, 0);
image.getByteSamples(RGBIndex.INDEX_GREEN, 0, y, width, 1, green, 0);
image.getByteSamples(RGBIndex.INDEX_BLUE, 0, y, width, 1, blue, 0);
convertFromRGB24(red, 0, green, 0, blue, 0, alpha, pixels, 0, width);
 
for(int x = 0; x < width; x++)
newImage.setRGB(x, y, pixels[x]);
}
 
return newImage;
}
 
protected static void convertFromRGB24(
byte[] srcRed, int srcRedOffset,
byte[] srcGreen, int srcGreenOffset,
byte[] srcBlue, int srcBlueOffset,
int alpha,
int[] dest, int destOffset,
int num)
{
while (num-- > 0)
{
dest[destOffset++] =
alpha |
(srcBlue[srcBlueOffset++] & 0xff) |
((srcGreen[srcGreenOffset++] & 0xff) << 8) |
((srcRed[srcRedOffset++] & 0xff) << 16);
}
}
 
/**
* Creates an {@link RGB24Image} from the argument AWT image instance.
* @param image AWT image object to be converted to a {@link RGB24Image}
* @return a {@link RGB24Image} object holding the
* image data from the argument image
*/
protected static RGB24Image convertImageToRGB24Image(Image image)
{
if (image == null)
{
return null;
}
int width = image.getWidth(null);
int height = image.getHeight(null);
if (width < 1 || height < 1)
{
return null;
}
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)
{
//System.err.println("image fetch aborted or errored");
return null;
}
RGB24Image result = new MemoryRGB24Image(width, height);
int offset = 0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int pixel = pixels[offset++] & 0xffffff;
result.putSample(RGBIndex.INDEX_RED, x, y, pixel >> 16);
result.putSample(RGBIndex.INDEX_GREEN, x, y, (pixel >> 8) & 0xff);
result.putSample(RGBIndex.INDEX_BLUE, x, y, pixel & 0xff);
}
}
return result;
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/images/ThumbnailPosition.java
0,0 → 1,89
package ak.photoalbum.images;
 
public class ThumbnailPosition
{
public static final int ALIGN_HOR_LEFT = 1;
public static final int ALIGN_HOR_RIGHT = 2;
public static final int ALIGN_HOR_CENTER = 3;
public static final int ALIGN_VERT_TOP = 1;
public static final int ALIGN_VERT_BOTTOM = 2;
public static final int ALIGN_VERT_CENTER = 3;
 
private int x;
private int y;
private int width;
private int height;
private int horAlign;
private int vertAlign;
 
public ThumbnailPosition(int x, int y, int width, int height,
int horAlign, int vertAlign)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.horAlign = horAlign;
this.vertAlign = vertAlign;
}
 
public int getX()
{
return x;
}
 
public void setX(int x)
{
this.x = x;
}
 
public int getY()
{
return y;
}
 
public void setY(int y)
{
this.y = y;
}
 
public int getWidth()
{
return width;
}
 
public void setWidth(int width)
{
this.width = width;
}
 
public int getHeight()
{
return height;
}
 
public void setHeight(int height)
{
this.height = height;
}
 
public int getHorAlign()
{
return horAlign;
}
 
public void setHorAlign(int horAlign)
{
this.horAlign = horAlign;
}
 
public int getVertAlign()
{
return vertAlign;
}
 
public void setVertAlign(int vertAlign)
{
this.vertAlign = vertAlign;
}
}
/PhotoAlbum/trunk/src/ak/photoalbum/images/Thumbnailer.java
0,0 → 1,799
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;
}
}