Reading and writing JPEG metadata (EXIF) from Java with Sanselan
Submitted by charlie.collins on Thu, 04/03/2008 - 13:17
Tagged:
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:
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
And I do realize I am off
Yeah. The Smart in the US
import
import - commons io
Deps - in case it helps further
many lose ends
Didn't lose anything, but appear to have gained a few yes
Yes I did mean "loose ends"
Agreed on sanselan issues
not right
Sanselan the right way
Reading EXIF after Sanselan
ImageIO tools
ImageIO writing not so easy
Thanks for the demo code; how to write the UserComment field
Embed thumbnail into jpeg
... 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.
Mitul from Technology Blog
Yes is a nice blog... We
orientation
public static File fixOrientation( File imageFile ) throws ImageReadException, IOException, ImageWriteException{ JpegImageMetadata metaData = (JpegImageMetadata) Sanselan.getMetadata( imageFile ); File dest = new File( "fixed/" imageFile.getName() ); // this will be the raw image data (without metadata), // correctly rotated byte rawImageData[] = null; // Get the orientation TiffField field = metaData.findEXIFValue( TiffConstants.EXIF_TAG_ORIENTATION ); int orientation = field == null? 1:field.getIntValue(); // Only need to do work if orientation is different from 1 and 2 (correct orientation or simply flipped) if( field != 1 && field ){ BufferedImage image = ImageIO.read( imageFile ); BufferedImage target = null; Graphics2D graphics = null; switch( orientation ){ // rotate cw (was ccw, eventually flipped, but we ignore that!) case 6: case 5: target = new BufferedImage( image.getHeight(), image.getWidth(), image.getType() ); target.createGraphics(); graphics.rotate( Math.PI/2 ); graphics.translate( 0, -image.getHeight() ); break; case 7: case 8: target = new BufferedImage( image.getHeight(), image.getWidth(), image.getType() ); target.createGraphics(); graphics.rotate( -Math.PI/2 ); graphics.translate( -image.getWidth(), 0 ); break; // you shoud handle the other cases as well here! (orienation 3, 4, maybe you wanna take care of the flipping as well) } ByteArrayOutputStream out = new ByteArrayOutputStream(); graphics.drawImage( image, 0, 0, null ); ImageIO.write( target, "jpg", out ); rawImageData = out.toByteArray(); target.dipose(); image.dispose(); } else{ // Quickly read in the image... FileInputStream fis = new FileInputStream( imageFile ); rawImageData = new byte[ (int) imageFile.length() ]; fis.read( rawImageData ); fis.close(); } TiffImageMetadata exif = metaData.getExif(); OutputStream os = new FileOutputStream( dest ); TiffOutputSet outputSet = exif.getOutputSet(); outputSet.removeField( TiffConstants.EXIF_TAG_ORIENTATION ); outputSet.removeField( TiffConstants.TIFF_TAG_ORIENTATION ); // do your other metadata manipulation things here, // add gps data and whatnot. go nuts! new ExifRewriter().updateExifMetadataLossy( rawImageData, os, outputSet ); return dest; }I like your blog,and also
Thanks