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.

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.








Comments
Is that your new
Is that your new transportation to work? I just saw my first smart car in the US. Pretty efficient, though not super cheap considering how small it is. http://www.smartusa.com/smart-fortwo-pure.aspx
And I do realize I am off
And I do realize I am off subject here.
Yeah. The Smart in the US
Yeah. The Smart in the US seem really quite $$$ to me.
import
Where is the FileUtils class coming from?
FileUtils.copyFile(dst, file);
Thanks.
Craig
import - commons io
That's from commons-io.
You can see all the imports, and all the source for the entire project, in it's entirety, in SVN:
http://www.totsp.com/svn/repo/ImageIOTesting/trunk/ImageIOTesting/src/co....
Deps - in case it helps further
http://www.totsp.com/svn/repo/ImageIOTesting/trunk/ImageIOTesting/lib/
And the Sanselan version is not using the metadata extractor dep, I was using that in another file, playing with that too. It's great if you just need to read EXIF (and other meta data), but doesn't work to write it back.
many lose ends
1. some fields are given with encoding numbers that hide the real meaning. E.g. Color space 1 which is supposedly sRGB.
2. Did you notice you have two XRESOLUTIONS?
Didn't lose anything, but appear to have gained a few yes
I think you meant loose ends, but anyway . . .
1. The output display of the fields is primitive, yes, but that wasn't really the point of the article - and that is taken directly from the Sanselan examples.
2. I did not, good catch, I will look into that when I get a chance. Looks like Xres, Yres, and res unit, are all repeated in the output (not sure if that's a print out bug, or if they were actually doubled up in the meta data - either way valid issue).
Yes I did mean "loose ends"
Yes I did mean "loose ends" and I was referring to the package, not your code. No offense.
Human-readability is a problem with this package. I'd rather see sRGB, or resolution unit as inches (instead of "2"), and so on and so forth.
Another issue with the package is lack of document :(
And yes I do like the capability of writing the exif, just as you do.
Agreed on sanselan issues
No offense taken, sorry if things came across that way. I agree with you about the sanselan docs and readability stuff. It's not perfect by any sense, but I was able to use it and get some stuff done, which I failed to be able to do with the new JAI stuff.
I think sanselan is moving to an Apache project, so hopefully it will get more traction, more rigor. Having a sane Java library to do this stuff would be a huge plus. (And maybe JAI and Image-IO can do it, but back to the docs point, I couldn't tell how with those - I even posted a few questions on the boards there and got mostly "can't do it" back, so I moved on a found sanselan.)
not right
your usage of sanselan isn't correct.
You're writing the item's value to the document as a blunt string of ascii characters.
Reading it back with this application will parse the string and show it to be the value of the item, however when you try to read the exif data with any other application, the tag will not be displayed, or you will be warned that it is 'broken'
Sanselan the right way
I started from the demos and docs that Sanselan has - so my example is based on that.
If you have other suggestions, I would be happy to be made aware of them, and would be happy to correct the article. And, back when I wrote this, I am 90% sure I did test with several external EXIF reading capable applications and it worked fine.
I will go back and check that again to verify, and again, actual tangible suggestions are welcome.
Reading EXIF after Sanselan
I tried a few apps, EXIFViewer on the Mac can read the updated file (though it doesn't see any of the custom fields), but other things like PhotoInfo can't read it (PhotInfo can read the before, but not the after, what I am doing to the file is causing problems for it, but not for ExifViewer, ExifViewer can read the standard data both before and after, but can't read the added data).
To clarify, programmatically, this works, the program can read back the field and tell it's there, and that met the needs I had at the time. But yes, something must be amiss because some other apps seem to have some problems, sometimes (other apps being relative, I find varying mileage with EXIF reading apps in general).
When I get a chance I will try to learn more about the EXIF spec and what is going on here, and re-read the Sanselan docs, to figure out how to do it better.
ImageIO tools
Reading and writing of exif metadata can be accomplished by installing ImageIO tools, without using any library-specific code. Here's an article on how to read the metadata:
http://easyproblemsolutions.blogspot.com/2008/06/java-and-jpeg-metadata....
ImageIO writing not so easy
I tried ImageIO first too, as I think I stated in the original article. I even tried posting some questions on the ImageIO group when I felt like I wasn't getting anywhere on my own with the documentation.
In the end I did see how it could be used to READ back meta data, but I did not see a clear path to writing it. And, as I also noted in the article, I bet it can be done, I just didn't find a way - and Sanselan worked rather easily.
Thanks for the demo code; how to write the UserComment field
Thanks for sharing your example -- Sanselan's demo showed how to write a numeric EXIF field, but not how to write a String-based field like you have shown with this example. This was helpful to me since I was trying to write the UserComment field. However, I ran into a little issue when doing it -- the definition of the UserComment field in the EXIF standard says that the first eight bytes is a "code" which tells a reader how to interpret the rest of the UserComment field. It is a simple extension to write the code to add this header ... here it is:
String userCommentHeaderForASCII = "ASCII\0\0\0";
String userCommentData = userCommentHeaderForASCII + "some string value";
TiffOutputField uc = new TiffOutputField(TiffConstants.EXIF_TAG_USER_COMMENT,
TiffConstants.FIELD_TYPE_ASCII,
userCommentData.length(),
userCommentData.getBytes());
Embed thumbnail into jpeg
Thanks for your sample Code!
I have the problem that I have to embed a thumbnail into a jpeg. All the sanselan examples I've found are not dealing with embedded thumbnails. The SanselanMetadataExample dont help me, because onla the tumbnail offest (Jpg From Raw Start) and lenght (Jpg From Raw Length) are displayed.
Can anyone give me a hint if its possible to read and more important to write the tumbnail into the exif metadata of a jpeg?
Even if I have an jpeg with embeded thumbnail the JpegImageMetadata.getEXIFThumbnail() and JpegImageMetadata.getRawImageData() methods cant read the thumbnail data.
Here is the code I tried to read the thumbnail. I've no idea how to get this running and how to write a thumbnail into the EXIF data.
... Map params = new HashMap(); params.put(SanselanConstants.PARAM_KEY_READ_THUMBNAILS, true); IImageMetadata metadata = Sanselan.getMetadata(file, params); if (metadata instanceof JpegImageMetadata) { JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; ... BufferedImage bufferedThumbnail = jpegMetadata.getEXIFThumbnail(); if(bufferedThumbnail != null){ int width = bufferedThumbnail.getWidth(); int height = bufferedThumbnail.getHeight(); System.out.println("found thumbnail in image ["+width + " x " + height + "]"); } else { System.out.println("no thumbnail in image"); } TiffImageData rawImage = jpegMetadata.getRawImageData(); if(rawImage != null){ System.out.println("found rawImage in image ["+rawImage+ "]"); } else { System.out.println("no rawImage in image"); } ... } ...Thanks for your help.
Had the same problem myself.
Had the same problem myself. Turns out throw new ImageReadException("Sanselan cannot read or write JPEG images."); is the best it can do though.
getEXIFThumbnail searches the TIFF directory structure used by JPEGs for a thumbnail. Unfortunately it only searches for "tiffImageData" once it has found a folder with a JPEG thumb in it.
The SVN does contain an update which allows you to extract a byte[] of the raw JPEG thumbnail though.
Mitul from Technology Blog
You have a nice blog … I liked your blog very much. The contents are worth reading and I’ve found much valuable information from your post. My learning curve is increasing :-). Thanks for sharing such a nice post with us. Keep it up.
Yes is a nice blog... We
Yes is a nice blog...
We thank you for the code screaming-penguin..com :)
Nicolae