Reply to comment

Reading and writing JPEG metadata (EXIF) from Java with Sanselan

Recently I had a need to be able to work with EXIF image metadata in JPEG files from Java. First, some background, then, some code.

Jpg image with EXIF - bike

Sample image I used, lots of EXIF info on this shot of a 1971 SL70 that I am rebuilding.

I started by looking at JAI-imageio, which is the current home of the old Sun JAI project (to be fair, that and more), but I couldn't readily figure out how to use it to write EXIF data. The capability might be there, but I couldn't find any substantive documentation or examples (apart from JavaDocs, which are useful yes, but I needed more hand holding to start out). When I ran into this basic getting started stumbling block I broadened my search.

From there I found several third party libraries. Most notable where Metadata Extractor, and Sanselan. Both are free, open, and very useful - but Sanselan ended up being my choice because it allowed me not only to read metadata, but also to write it. (I found Metadata Extractor to be excellent in terms of reading the data, but I had a need to write tags as well, and it did not appear to have that capability.)

Sanselan, as noted, fit the bill entirely - the docs clearly showed how to read AND write metadata (and a host of other features). I didn't see many, make that *any*, tutorials on the web for writing EXIF data, apart from the Sanselan demo apps - so I started there.

What I ended up with was a simple approach to check for the existence of a particular EXIF tag, and if there remove it (which is naive, because tags can appear in multiples, but works in my case), and then write it back with a time-stamp. In all I was able to manipulate various tags in my tests, and ended up with a little utility to manipulate a single tag for my purposes (basically similar to "touching" the file, to update the ImageHistory tag, in order to determine if an image process had already handled the image, or not, in case the image was passed in multiple times - needed at the office - long story).

Enough with the loquacious, long-winded, wordy . . . here is the damn code:
package com.totsp.imageio;

// no warranty expressed, implied, blah-blah - back up any images before you try this
// imports omitted for brevity - see SVN below

public class SanselanDemo {
    
    /**
     * Read metadata from image file and display it. 
     * @param file
     */
    public void readMetaData(File file) {
        IImageMetadata metadata = null;
        try {
            metadata = Sanselan.getMetadata(file);
        } catch (ImageReadException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (metadata instanceof JpegImageMetadata) {
            JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata;
            System.out.println("\nFile: " + file.getPath());
            printTagValue(jpegMetadata,
                TiffConstants.TIFF_TAG_XRESOLUTION);
            printTagValue(jpegMetadata,
                TiffConstants.TIFF_TAG_DATE_TIME);
            printTagValue(jpegMetadata,
                TiffConstants.EXIF_TAG_DATE_TIME_ORIGINAL);
            printTagValue(jpegMetadata,
                TiffConstants.EXIF_TAG_CREATE_DATE);
            printTagValue(jpegMetadata,
                TiffConstants.EXIF_TAG_ISO);
            printTagValue(jpegMetadata,
                TiffConstants.EXIF_TAG_SHUTTER_SPEED_VALUE);
            printTagValue(jpegMetadata,
                TiffConstants.EXIF_TAG_APERTURE_VALUE);
            printTagValue(jpegMetadata,
                TiffConstants.EXIF_TAG_BRIGHTNESS_VALUE);

            // simple interface to GPS data
            TiffImageMetadata exifMetadata = jpegMetadata.getExif();
            if (exifMetadata != null) {
                try {
                    TiffImageMetadata.GPSInfo gpsInfo = exifMetadata.getGPS();
                    if (null != gpsInfo) {
                        double longitude = gpsInfo.getLongitudeAsDegreesEast();
                        double latitude = gpsInfo.getLatitudeAsDegreesNorth();
                        System.out.println("    " +
                            "GPS Description: " + gpsInfo);
                        System.out.println("    " +
                            "GPS Longitude (Degrees East): " +
                            longitude);
                        System.out.println("    " +
                            "GPS Latitude (Degrees North): " +
                            latitude);
                    }
                } catch (ImageReadException e) {
                    e.printStackTrace();
                }
            }

            System.out.println("EXIF items -");
            ArrayList items = jpegMetadata.getItems();
            for (int i = 0; i < items.size(); i++) {
                Object item = items.get(i);
                System.out.println("    " + "item: " +
                    item);
            }
            System.out.println();
        }
    }

    private static void printTagValue(
        JpegImageMetadata jpegMetadata, TagInfo tagInfo) {
        TiffField field = jpegMetadata.findEXIFValue(tagInfo);
        if (field == null) {
            System.out.println(tagInfo.name + ": " +
                "Not Found.");
        } else {
            System.out.println(tagInfo.name + ": " +
                field.getValueDescription());
        }
    }

    /**
     * Example of adding an EXIF item to metadata, in this case using ImageHistory field. 
     * (I have no idea if this is an appropriate use of ImageHistory, or not, just picked
     * a field to update that looked like it wasn't commonly mucked with.)
     * @param file
     */
    public void addImageHistoryTag(File file) {
        File dst = null;
        IImageMetadata metadata = null;
        JpegImageMetadata jpegMetadata = null;
        TiffImageMetadata exif = null;
        OutputStream os = null;
        TiffOutputSet outputSet = new TiffOutputSet();

        // establish metadata
        try {
            metadata = Sanselan.getMetadata(file);
        } catch (ImageReadException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // establish jpegMedatadata
        if (metadata != null) {
            jpegMetadata = (JpegImageMetadata) metadata;
        }

        // establish exif
        if (jpegMetadata != null) {
            exif = jpegMetadata.getExif();
        }

        // establish outputSet
        if (exif != null) {
            try {
                outputSet = exif.getOutputSet();
            } catch (ImageWriteException e) {
                e.printStackTrace();
            }
        }

        if (outputSet != null) {         
            // check if field already EXISTS - if so remove         
            TiffOutputField imageHistoryPre = outputSet
                    .findField(TiffConstants.EXIF_TAG_IMAGE_HISTORY);
            if (imageHistoryPre != null) {
                outputSet.removeField(TiffConstants.EXIF_TAG_IMAGE_HISTORY);
            }                    
            // add field 
            try {               
                String fieldData = "ImageHistory-" + System.currentTimeMillis();                
                TiffOutputField imageHistory = new TiffOutputField(
                           ExifTagConstants.EXIF_TAG_IMAGE_HISTORY, 
                           TiffFieldTypeConstants.FIELD_TYPE_ASCII, 
                           fieldData.length(), 
                           fieldData.getBytes());
                TiffOutputDirectory exifDirectory = outputSet.getOrCreateExifDirectory();
                exifDirectory.add(imageHistory);
            } catch (ImageWriteException e) {

                e.printStackTrace();
            }
        }
        
        // create stream using temp file for dst
        try {
            dst = File.createTempFile("temp-" + System.currentTimeMillis(), ".jpeg");
            os = new FileOutputStream(dst);
            os = new BufferedOutputStream(os);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // write/update EXIF metadata to output stream
        try {
            new ExifRewriter().updateExifMetadataLossless(file,
                os, outputSet);
        } catch (ImageReadException e) {
            e.printStackTrace();
        } catch (ImageWriteException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                }
            }
        }
        
        // copy temp file over original file
        try {
            FileUtils.copyFile(dst, file);
        } catch (IOException e) {
            e.printStackTrace();
        }        
    }

    public static void main(String[] args) {
        File bikeFile = new File("data/bike.jpg");
        SanselanDemo demo = new SanselanDemo();
        System.out.println("BEFORE update");
        demo.readMetaData(bikeFile);
        demo.addImageHistoryTag(bikeFile);
        System.out.println("\nAFTER update");
        demo.readMetaData(bikeFile);
    }
}

In terms of the sample code here, what this does is read the metadata from a test JPEG first, display it in the console, and then write metadata to that same file, and save over it - displaying it again at the end. If you run this it will show output that demonstrates the ImageHistory tag being updated, such as:

BEFORE update
File: data\bike.jpg
XResolution: 180
Date Time: '2008:03:23 01:36:18'
Date Time Original: '2008:03:23 01:36:18'
Create Date: '2008:03:23 01:36:18'
ISO: 80
Shutter Speed Value: 255/32 (7.969)
Aperture Value: 4
Brightness Value: Not Found.
EXIF items -
    item: Make: 'Canon'
    item: Model: 'Canon PowerShot A590 IS'
    item: Orientation: 1
    item: XResolution: 180
    item: YResolution: 180
    item: Resolution Unit: 2
    item: Modify Date: '2008:03:23 01:36:18'
    item: YCbCr Positioning: 1
    item: Exif Offset: 2384
    item: Exposure Time: 1/250 (0.004)
    item: FNumber: 4
    item: ISO: 80
    item: Exif Version: 48, 50, 50, 48
    item: Date Time Original: '2008:03:23 01:36:18'
    item: Create Date: '2008:03:23 01:36:18'
    item: Components Configuration: 1, 2, 3, 0
    item: Compressed Bits Per Pixel: 3
    item: Shutter Speed Value: 255/32 (7.969)
    item: Aperture Value: 4
    item: Exposure Compensation: 0
    item: Max Aperture Value: 88/32 (2.75)
    item: Metering Mode: 5
    item: Flash: 24
    item: Focal Length: 5800/1000 (5.8)
    item: Maker Note: 25,...)
    item: UserComment: ''
    item: Flashpix Version: 48, 49, 48, 48
    item: Color Space: 1
    item: Exif Image Width: 3264
    item: Exif Image Length: 2448
    item: Interop Offset: 3128
    item: Focal Plane XResolution: 3264000/225 (14,506.667)
    item: Focal Plane YResolution: 2448000/169 (14,485.207)
    item: Focal Plane Resolution Unit: 2
    item: Image History: 'ImageHistory-1207245408150'
    item: Sensing Method: 2
    item: File Source: 3
    item: Custom Rendered: 0
    item: Exposure Mode: 0
    item: White Balance: 0
    item: Digital Zoom Ratio: 1
    item: Scene Capture Type: 0
    item: Interop Index: 'R98'
    item: Interop Version: 48, 49, 48, 48
    item: Related Image Width: 3264
    item: Related Image Length: 2448
    item: Compression: 6
    item: XResolution: 180
    item: YResolution: 180
    item: Resolution Unit: 2
    item: Jpg From Raw Start: 5108
    item: Jpg From Raw Length: 6322

AFTER update
File: data\bike.jpg
XResolution: 180
Date Time: '2008:03:23 01:36:18'
Date Time Original: '2008:03:23 01:36:18'
Create Date: '2008:03:23 01:36:18'
ISO: 80
Shutter Speed Value: 255/32 (7.969)
Aperture Value: 4
Brightness Value: Not Found.
EXIF items -
    item: Make: 'Canon'
    item: Model: 'Canon PowerShot A590 IS'
    item: Orientation: 1
    item: XResolution: 180
    item: YResolution: 180
    item: Resolution Unit: 2
    item: Modify Date: '2008:03:23 01:36:18'
    item: YCbCr Positioning: 1
    item: Exif Offset: 2384
    item: Exposure Time: 1/250 (0.004)
    item: FNumber: 4
    item: ISO: 80
    item: Exif Version: 48, 50, 50, 48
    item: Date Time Original: '2008:03:23 01:36:18'
    item: Create Date: '2008:03:23 01:36:18'
    item: Components Configuration: 1, 2, 3, 0
    item: Compressed Bits Per Pixel: 3
    item: Shutter Speed Value: 255/32 (7.969)
    item: Aperture Value: 4
    item: Exposure Compensation: 0
    item: Max Aperture Value: 88/32 (2.75)
    item: Metering Mode: 5
    item: Flash: 24
    item: Focal Length: 5800/1000 (5.8)
    item: Maker Note: 25, ...)
    item: UserComment: ''
    item: Flashpix Version: 48, 49, 48, 48
    item: Color Space: 1
    item: Exif Image Width: 3264
    item: Exif Image Length: 2448
    item: Interop Offset: 3128
    item: Focal Plane XResolution: 3264000/225 (14,506.667)
    item: Focal Plane YResolution: 2448000/169 (14,485.207)
    item: Focal Plane Resolution Unit: 2
    item: Image History: 'ImageHistory-1207247905886'
    item: Sensing Method: 2
    item: File Source: 3
    item: Custom Rendered: 0
    item: Exposure Mode: 0
    item: White Balance: 0
    item: Digital Zoom Ratio: 1
    item: Scene Capture Type: 0
    item: Interop Index: 'R98'
    item: Interop Version: 48, 49, 48, 48
    item: Related Image Width: 3264
    item: Related Image Length: 2448
    item: Compression: 6
    item: XResolution: 180
    item: YResolution: 180
    item: Resolution Unit: 2
    item: Jpg From Raw Start: 5108
    item: Jpg From Raw Length: 6322

Notice in the after there the ImageHistory tag has been updated
item: Image History: 'ImageHistory-1207247905886'.

If you want to grab the whole project from anon SVN, you can do that too (Eclipse project, with libs, didn't mavenize or antize the example - in real life I would not check in an Eclipse file, really) : http://www.totsp.com/svn/repo/ImageIOTesting/trunk/ImageIOTesting/.

Props and thanks and so on to the Sanselan team.

Reply

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <img> <a> <em> <strong> <cite> <code> <ul> <ol> <hr> <li> <dl> <dt> <dd> <pre> <b> <h1> <h2> <h3> <blockquote>
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
6 + 3 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.