http://www.developer.com/

Back to article

Create a Chronological Photo Album with Java


September 21, 2006

With the proliferation of the online picture sharing sites, such as flickr.com, snapfish.com, or kodakgallery.com, many developers are wondering how to achieve the same on their own. With the right, generally free, tools and a few clever Java algorithms, you can be hosting your own photo album in no time, without paying any subscription fees. In this article, I will teach you how to create a personal photo album that will be automatically sorted and ordered in correct chronological order, and independent from the file system names and locations. With the sample code, you will be able to drop the new folder of pictures into some directory and have them correctly show up in relation to the other picture folders you are sharing.

The back-end engine algorithms are in Java and are generic enough to have any front-end technology such as JSPs or Servlets. I developed the back-end engine; it uses stack data types and recursion. The algorithms for reading exif metadata, such as the date the picture was taken, uses a metadata extractor library developed by Drew Noakes (see references). Additionally, due to Java's shortcoming of returning the file modification time instead of the creation time with the standard File I/O APIs, I use a FileTimes utility to correctly retrieve the file creation time. This utility is free for a single person, single site, or single server. The front-end technologies are beyond the scope of this article. However, I will discuss a very simple way to have your pictures shared on an Apache Web server.

Setting Up Your Own Photo Album

To implement a photo album project, I have established a very simple directory structure. You may ultimately use something different, but if you want to use the included source with minimal changes, follow this structure:

  1. All photo album folders will reside in some main "root" directory.
  2. Every album folder must contain two sub folders, called "images" and "thumbnails" (more on this later).
  3. Album folders can be nested.

For example:

C:photos: is my main root folder

C:photosSomeAlbum: photo album folder

C:photosSomeAlbumimages

C:photosSomeAlbumthumbnails

The images folder contains actual photos in JPEG format, and the thumbnails folder contains small versions of the main pictures for front-end display purposes. This folder can be created with many free tools available online, such as Google's Picasa or Jalbum. The thumbnails are usually 160x120 pixels, for faster downloads over the wire. I will talk about thumbnails later, but for now, suffice it to say that my algorithm also will pick a random thumbnail to display next to the album information. This function would make it more interesting to the viewers, who will see a different image representing the whole album every time they refresh (or come back to) the page.

Here is a screen shot of several nested albums. Fictional folders demonstrate the required structure:

Actual folders were used for testing: note their alphabetical order on the hard drive is chronologically incorrect and different from when the pictures where taken versus when the folder was created.

Picture Metadata Standard

All the new digital cameras follow the exif or iptc standard for metadata embedded with every picture. Exif is a standard for image files created with digital cameras and other input devices. The standard is set by the Japan Electronic Industry Development Association, and formally it is called the Digital Still Camera Image File Format Standard. Exif files contain either uncompressed TIFF images or compressed JPEG images. In addition, these files can contain many comment fields of various types.

Here is a sample of the metadata that can be extracted from a jpeg file:

(Image properties shown in Windows XP)

Besides camera manufacturers, different software and hardware vendors endorse and support the standard as well. Therefore, you can be assured that the code to extract this data will work for most photos and will not break in the future.

Project Structure

For the sake of simplicity, this project uses a standard property file, and not an XML file. The two properties

image_dir_name=images
thumbnails_dir_name=thumbnails

are defined in the a.property file, which is located in the root project folder. If the project will be converted to the J2EE structure, this file should be placed somewhere under the WEB-INF/ folder.

For further expandability and maintainability, I separated the back-end into several packages. The beans package will hold a representation of the real work objects (such as Album), the sys holds the main execution and initialization classes, and the filters and util packages hold the helper classes. See the complete project tree below.

The back-end engine algorithms that encapsulate the date and sort logic are contained in several beans, several utility classes, and a few helper classes. The main method is called from a GetResourceFiles class.

The InitSystem class initiates all the properties (from the a.property file) associated with the project such as logs, names of image and thumbnail folders, and so forth.

     

Helper Objects

I will start with helper classes before moving on to the main algorithms. To filter out only specific file types to work with, I implemented classes using the standard java.io FileFilter interface, located in the filters package. The DirFilter filters out only directory file types and only those that I'm interested in.

Here is the code implementing the accept method from the java.io FileFilter interface.

public boolean accept(File file) {
      return file.isDirectory() && !excludeDir(file.getName());
   }

   private boolean excludeDir(String dirname) {
      for (int i = 0; i < exclude.length; i++) {
         if (exclude[i].equals(dirname)) {
            return true;
         }
      }

      return false;
   }

Similarly, the ImageFilesOnly filters out only image file types from the list of all files in a folder—in this case, only JPG files.

static String[] accepted = {"jpg", "JPG"};

   public boolean accept(File file) {
      if (file.isFile()) {
         String fn = file.getName();
         return (file.isFile() && (fn.endsWith(accepted[0]) ||
            fn.endsWith(accepted[1])));
      }
      return false;
   }

I apply the filters when I create the file objects representing a file system folders or files. For instance,

File dir = new File("C:\photos");
File[] pics = dir.listFiles(new ImageFilesOnly());

will create a list of JPG files located in the "photos" folder.

The ImageUtils from the util package contains APIs pertaining to the logical Image object, which represents one picture. I did not create a separate picture representation object in the beans package because all of the image properties required for the back-end engine are calculated in real time, and the image is represented simply by java.io.File. The APIs for the Image are:

  • getImageCameraManufacturer(File img)
  • getImageExifCameraModel(File img)
  • getImageExifCreationDate(File img)
  • getImageExifMetaData(File img, int meta)

As you may have guessed, all of these APIs use the metadata extractor library developed by Drew Noakes (see reference), and the names are self explanatory. One API of particular interest is the getImageExifCreationDate. It uses the FileTimes utility and has some logic in addition to the metadata extractor calls. Different manufacturers may call the field in the metadata of the file representing date the picture was taken differently, and in rare cases may not store that field at all. Therefore, I try different tags to locate the creation date of the picture. Otherwise, if image does not have this field, I get the time the file was created.

Here is the source code for this method. See the complete source for the other methods:

public static String getImageExifCreationDate(File img) {
   SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
   try {
      Metadata metadata = JpegMetadataReader.readMetadata(img);
      Directory exifDirectory =
         metadata.getDirectory(ExifDirectory.class);
      String dt = exifDirectory.getString(ExifDirectory.TAG_DATETIME);
      String dt_org =
         exifDirectory.getString(ExifDirectory.TAG_DATETIME_ORIGINAL);
      String dt_dt =
         exifDirectory.getString(ExifDirectory.TAG_DATETIME_DIGITIZED);

      if (dt_org != null && !dt_org.startsWith("0000")) {
         return dt_org;
      } else if (dt != null && !dt.startsWith("0000")) {
         return dt;
      } else if (dt_dt != null && !dt_dt.startsWith("0000")) {
         return dt_dt;
      } else {
         // not correct call . java has no API to get the file
         // create date of a file
         // return sdf.format(new Date(img.lastModified()));
         return sdf.format(new Date(FileTimes.getFileCreated(
            img.getPath())));
         }

      } catch (JpegProcessingException ex) {
         logger.error(ex, ex);
         return null;
      }
   }

The AlbumUtil class in the same util package that pertains to the Album object, which is a higher obstructed object than the single image and is represented by the folder on the file system. AlbumUtil is much more complex than the ImageUtil and has a lot of logic that constitutes a big part of the main back-end engine. For example, the getFolderDate method does a selective sampling of the file creation dates of the first, middle, and last picture in the folder using the getImageExifCreationDate method, which then returns the latest date. The getAlbumDate method uses the getFolderDate method to recursively calculate one master date for the whole Album, including subfolders and excluding folders designated by the EXCLUDE_DIR parameter.

Here is the source for the getAlbumDate method:

public static String getAlbumDate(File dir) {

   String[] dates = null;
   ArrayList aDates = new ArrayList();
   File[] innerAlbums =
      dir.listFiles(new DirFilter(new String[] {TNAILS_DIR}));
   if (innerAlbums != null && innerAlbums.length > 0) {
      String currentDirDate = getFolderDate(dir);
      if (currentDirDate != null) {    // if there are pictures in
                                       // the current dir
         aDates.add(currentDirDate);
      }
      for (int i = 0; i < innerAlbums.length; i++) {
         // recursive call to continue traversing the dir tree
         String date = getAlbumDate(innerAlbums[i]);
         if (date != null) {
            aDates.add(date);
         }
      }
      dates = (String[]) aDates.toArray(new String[aDates.size()]);
      Arrays.sort(dates, Collections.reverseOrder());
   }
   else {
      // there are no child dir - return date for this dir
      return getFolderDate(dir);
   }

   return dates[0];
}

For the complete source of the other methods, see the download section.

  • getAlbumDate(File dir)
  • getFolderDate(File dir)
  • outList(Object[] obj)
  • sortAlbums(Album[] albums)

Chronological Album Sort: the Back-End Engine

The brains of this project and the logic that will be used by the front-end application uses a Stack and recursion to analyze the root folder, create Album objects, and calculate Album attributes. In addition, I use the SortedMap object to sort the Albums by their calculated master date.

Here is the file system view of my test picture folders. Note their alphabetical order on the hard drive.



Click here for a larger image.

Here is the output from the back-end engine, which is chronologically correct, sorted by the true times the events occurred.



Click here for a larger image.

(Time: 953ms)

Because I do not read the creation date for every file in every subfolder, but instead use selective sampling, very little I/O is generated and the calculation engine is extremely fast. The selective sampling works even if pictures are not in alphabetical order. For example, over 800 Mb of pictures can be analyzed in 953 MS on a standard hard drive (7200 RMP), with RAID optimized I/O; with 10000 RPM hard drives, this time can be decreased even further.

Now, look at how the engine works.

The GetResourceFiles object (called from the main method) is passed a location of the root folder.

It then calls a getRootView method that recursively traverses the folders and pushes them on the Stack. For every folder that is pushed on the stack, a random thumbnail image is calculated every time the engine is called (see the pushAlbum method). This is the property that can be accessed on the front end to display a different picture for an album on every refresh.

In addition, if the folder has subfolders, they are all analyzed, sorted by their calculated date, and also pushed on the Stack. The output indentation you see is created by the relative position of the object in the stack. Therefore, child folders are correctly indented from the parent folders (see the indent method) But, for the Web representation, the indent method would need to be changed to account for the HTML or other output.

public void getRootView(File dir) {
   if (dir.exists() && dir.isDirectory()) {
      Album a = new Album(dir);
      pushAlbum(a);

      // one level down only
      Album[] innerAlbums = a.getInnerAlbums();

      if (innerAlbums != null && innerAlbums.length > 0) {
         // sort in the order of newest first here
         // before expecting them
         SortedMap sortedInnerAlbums =
            AlbumUtils.sortAlbums(innerAlbums);
         Collection sAlbums = sortedInnerAlbums.values();
         innerAlbums =
            (Album[]) sAlbums.toArray(new Album[sAlbums.size()]);

         sortedAlbums.putAll(sortedInnerAlbums);

         for (int i = 0; i < innerAlbums.length; i++) {
            // recursive call to continue traversing the dir tree
            getRootView(innerAlbums[i].getDir());
         }
      }
      popAlbum();
   }
}

private String indent(String tag) { StringBuffer res = new StringBuffer(); int indent = stack.size() - 1; for (int i = 0; i < indent; i++) { res.append("t"); } res.append(tag); return res.toString(); }

Here is the execution chain for the recursive logic:

  1. Call getRootView.
  2. Push this folder on the Stack
    1. Output the folder info and thumbnail picture info.
  3. Get all SubFolders and sort them by the "master" date.
    1. Call AlbumUtils.sortAlbums(innerAlbums);
    2. Call setAndGetDate per Album, which calls the calcAlbumDate method, which calls AlbumUtils.getAlbumDate.
  4. Call getRootView per subfolder—recursive call—back to Step 1.
  5. In no more subfolders—exit recursion—pop this folder from the stack.

Sharing with the World

To share your own album site, using this backend engine, you'll need an application server and a Web server. The front end would call the engine and display the output in some nicely formatted way. When you place a new folder under the root folder, visitors to the site will automatically see it on the top if its pictures were taken last. If you place a new folder as a subfolder, visitors will see it in the correct chronological order in relation to other subfolders. All of the folder names should be links pointing to the thumbnails directory, or if you are using Google's Picasa, for instance, you can pre-generate HTML Web album pages for the links.

If you only have an Apache Web server, you can write the output of the back-end engine as an HTML file, with album links pointing to the pre-generated HTML Web albums and place that file under the htdocs folder. However, the problem with this simple approach is that you lose the dynamic aspect of the site, and will need to re-generate the main HTML every time you add an Album folder. On the other hand, you do not need an Application server, and no JSPs or Servlets are involved.

Conclusion

In this article, I described how to implement a back-end engine to create your own picture sharing site. I talked about the exif metadata standard, reading file date, using selective sampling to minimize I/O, and, most importantly, how to use Stack and recursion to chronologically sort picture Albums. Now, the only thing left is to download the source code project and start sharing! As always, your comments are welcome.

Download the Source

You can download the source.zip file here.

References

About the Author

Vlad Kofman is a Senior System Architect. He has implemented enterprise-scale projects for the major Wall Street firms, defense contracts, and the U.S. government. His main interests are object-oriented programming methodologies and design patterns. You can contact him at

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date