package de.eisfeldj.augendiagnosefx.util.imagefile;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import com.adobe.xmp.XMPException;
import de.eisfeldj.augendiagnosefx.util.Logger;
import de.eisfeldj.augendiagnosefx.util.PreferenceUtil;
import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.ImageWriteException;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.common.IImageMetadata;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
import org.apache.commons.imaging.formats.jpeg.xmp.JpegXmpRewriter;
import org.apache.commons.imaging.formats.tiff.TiffField;
import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.MicrosoftTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType;
import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
import org.apache.commons.imaging.util.IoUtils;
/**
* Helper clase to retrieve and save metadata in a JPEG file.
*/
public final class JpegMetadataUtil {
/**
* Hide default constructor.
*/
private JpegMetadataUtil() {
throw new UnsupportedOperationException();
}
/**
* Log all Exif data of the file.
*
* @param imageFile
* the image file.
* @throws ImageReadException
* thrown if the metadata cannot be read.
* @throws IOException
* thrown in case of other errors while reading metadata.
*/
public static void printAllExifData(final File imageFile) throws ImageReadException, IOException {
final IImageMetadata metadata = Imaging.getMetadata(imageFile);
TiffImageMetadata tiffImageMetadata = null;
if (metadata instanceof JpegImageMetadata) {
tiffImageMetadata = ((JpegImageMetadata) metadata).getExif();
}
else if (metadata instanceof TiffImageMetadata) {
tiffImageMetadata = (TiffImageMetadata) metadata;
}
else {
return;
}
@SuppressWarnings("unchecked")
List<TiffImageMetadata.Item> items = (List<TiffImageMetadata.Item>) tiffImageMetadata.getItems();
for (TiffImageMetadata.Item item : items) {
Logger.info(item.getTiffField().toString());
}
}
/**
* Retrieve the orientation of a file from the EXIF data.
*
* @param imageFile
* the image file.
* @return the orientation value.
*/
protected static int getExifOrientation(final File imageFile) {
try {
final IImageMetadata metadata = Imaging.getMetadata(imageFile);
TiffImageMetadata tiffImageMetadata = null;
if (metadata instanceof JpegImageMetadata) {
tiffImageMetadata = ((JpegImageMetadata) metadata).getExif();
}
else if (metadata instanceof TiffImageMetadata) {
tiffImageMetadata = (TiffImageMetadata) metadata;
}
else {
return TiffTagConstants.ORIENTATION_VALUE_HORIZONTAL_NORMAL;
}
TiffField field = tiffImageMetadata.findField(TiffTagConstants.TIFF_TAG_ORIENTATION);
if (field != null) {
return field.getIntValue();
}
else {
TagInfo tagInfo = new TagInfoShort("Orientation", 274, 1, TiffDirectoryType.TIFF_DIRECTORY_IFD0); // MAGIC_NUMBER
field = tiffImageMetadata.findField(tagInfo);
if (field != null) {
return field.getIntValue();
}
else {
return TiffTagConstants.ORIENTATION_VALUE_HORIZONTAL_NORMAL;
}
}
}
catch (Exception e) {
return TiffTagConstants.ORIENTATION_VALUE_HORIZONTAL_NORMAL;
}
}
/**
* Retrieve the orientation angle of a file from the EXIF data.
*
* @param imageFile
* the image file.
* @return the orientation angle.
*/
public static int getExifOrientationAngle(final File imageFile) {
final int exifValue = getExifOrientation(imageFile);
switch (exifValue) {
case TiffTagConstants.ORIENTATION_VALUE_HORIZONTAL_NORMAL:
return 0;
case TiffTagConstants.ORIENTATION_VALUE_ROTATE_90_CW:
return 90; // MAGIC_NUMBER
case TiffTagConstants.ORIENTATION_VALUE_ROTATE_180:
return 180; // MAGIC_NUMBER
case TiffTagConstants.ORIENTATION_VALUE_ROTATE_270_CW:
return 270; // MAGIC_NUMBER
default:
return 0;
}
}
/**
* Log all XML data of the file.
*
* @param imageFile
* the file.
* @throws ImageReadException
* thrown if the metadata cannot be read.
* @throws IOException
* thrown in case of other errors while reading metadata.
* @throws XMPException
* thrown in case of issues with XML handling.
*/
public static void printAllXmpData(final File imageFile) throws ImageReadException, IOException, XMPException {
final String xmpString = Imaging.getXmpXml(imageFile);
Logger.info(new XmpHandler(xmpString).getXmpString());
}
/**
* Validate that the file is a JPEG file.
*
* @param jpegImageFileName
* the file to be validated.
* @throws IOException
* thrown if the file is no jpg.
* @throws ImageReadException
* thrown if the metadata cannot be read.
*/
protected static void checkJpeg(final String jpegImageFileName) throws IOException, ImageReadException {
File file = new File(jpegImageFileName);
String mimeType = Imaging.getImageInfo(file).getMimeType();
if (!"image/jpeg".equals(mimeType)) {
throw new IOException("Bad MIME type " + mimeType + " - can handle metadata only for image/jpeg.");
}
}
/**
* Retrieve the relevant metadata of an image file.
*
* @param jpegImageFileName
* the file for which metadata should be retrieved.
* @return the metadata of the file.
* @throws ImageReadException
* thrown if the metadata cannot be read.
* @throws IOException
* thrown in case of other errors while reading metadata.
*/
public static JpegMetadata getMetadata(final String jpegImageFileName) throws ImageReadException, IOException {
checkJpeg(jpegImageFileName);
JpegMetadata result = new JpegMetadata();
final File imageFile = new File(jpegImageFileName);
// Retrieve XMP data
String xmpString = Imaging.getXmpXml(imageFile);
XmpHandler parser = new XmpHandler(xmpString);
// Standard fields are pre-filled with custom data
result.setTitle(parser.getJeItem(XmpHandler.ITEM_TITLE));
result.setDescription(parser.getJeItem(XmpHandler.ITEM_DESCRIPTION));
result.setSubject(parser.getJeItem(XmpHandler.ITEM_SUBJECT));
result.setComment(parser.getJeItem(XmpHandler.ITEM_COMMENT));
result.setPerson(parser.getJeItem(XmpHandler.ITEM_PERSON));
result.setXCenter(parser.getJeItem(XmpHandler.ITEM_X_CENTER));
result.setYCenter(parser.getJeItem(XmpHandler.ITEM_Y_CENTER));
result.setOverlayScaleFactor(parser.getJeItem(XmpHandler.ITEM_OVERLAY_SCALE_FACTOR));
result.setXPosition(parser.getJeItem(XmpHandler.ITEM_X_POSITION));
result.setYPosition(parser.getJeItem(XmpHandler.ITEM_Y_POSITION));
result.setZoomFactor(parser.getJeItem(XmpHandler.ITEM_ZOOM_FACTOR));
result.setOrganizeDate(parser.getJeDate(XmpHandler.ITEM_ORGANIZE_DATE));
result.setRightLeft(parser.getJeItem(XmpHandler.ITEM_RIGHT_LEFT));
result.setBrightness(parser.getJeItem(XmpHandler.ITEM_BRIGHTNESS));
result.setContrast(parser.getJeItem(XmpHandler.ITEM_CONTRAST));
result.setSaturation(parser.getJeItem(XmpHandler.ITEM_SATURATION));
result.setColorTemperature(parser.getJeItem(XmpHandler.ITEM_COLOR_TEMPERATURE));
result.setOverlayColor(parser.getJeItem(XmpHandler.ITEM_OVERLAY_COLOR));
result.setPupilSize(parser.getJeItem(XmpHandler.ITEM_PUPIL_SIZE));
result.setPupilXOffset(parser.getJeItem(XmpHandler.ITEM_PUPIL_X_OFFSET));
result.setPupilYOffset(parser.getJeItem(XmpHandler.ITEM_PUPIL_Y_OFFSET));
result.setFlags(parser.getJeInt(XmpHandler.ITEM_FLAGS));
// For standard fields, use custom data only if there is no other data.
if (result.getDescription() == null) {
result.setDescription(parser.getDcDescription());
}
if (result.getSubject() == null) {
result.setSubject(parser.getDcSubject());
}
if (result.getPerson() == null) {
result.setPerson(parser.getMicrosoftPerson());
}
if (result.getTitle() == null) {
result.setTitle(parser.getDcTitle());
}
if (result.getComment() == null) {
result.setComment(parser.getUserComment());
}
// Retrieve EXIF data
try {
final IImageMetadata metadata = Imaging.getMetadata(imageFile);
TiffImageMetadata tiffImageMetadata = null;
if (metadata instanceof JpegImageMetadata) {
tiffImageMetadata = ((JpegImageMetadata) metadata).getExif();
}
else if (metadata instanceof TiffImageMetadata) {
tiffImageMetadata = (TiffImageMetadata) metadata;
}
else {
return result;
}
// EXIF data have precedence only if saving EXIF is allowed
TiffField title = tiffImageMetadata.findField(TiffTagConstants.TIFF_TAG_IMAGE_DESCRIPTION);
if (title != null && (changeExifAllowed() || result.getTitle() == null)) {
result.setTitle(title.getStringValue().trim());
}
String exifComment = null;
TiffField comment = tiffImageMetadata.findField(ExifTagConstants.EXIF_TAG_USER_COMMENT);
if (comment != null && comment.getStringValue().trim().length() > 0) {
exifComment = comment.getStringValue().trim();
}
TiffField comment2 = tiffImageMetadata.findField(MicrosoftTagConstants.EXIF_TAG_XPCOMMENT);
if (comment2 != null && comment2.getStringValue().trim().length() > 0) {
// XPComment takes precedence if existing
exifComment = comment2.getStringValue().trim();
}
if (exifComment != null && (changeExifAllowed() || result.getComment() == null)) {
result.setComment(exifComment);
}
TiffField subject = tiffImageMetadata.findField(MicrosoftTagConstants.EXIF_TAG_XPSUBJECT);
if (subject != null && (changeExifAllowed() || result.getSubject() == null)) {
result.setSubject(subject.getStringValue().trim());
}
}
catch (Exception e) {
Logger.warning("Error when retrieving Exif data: " + e.toString());
}
// If fields are still null, try to get them from custom XMP
if (result.getDescription() == null) {
result.setDescription(parser.getJeItem(XmpHandler.ITEM_DESCRIPTION));
}
if (result.getSubject() == null) {
result.setSubject(parser.getJeItem(XmpHandler.ITEM_SUBJECT));
}
if (result.getPerson() == null) {
result.setPerson(parser.getJeItem(XmpHandler.ITEM_PERSON));
}
if (result.getTitle() == null) {
result.setTitle(parser.getJeItem(XmpHandler.ITEM_TITLE));
}
if (result.getComment() == null) {
result.setComment(parser.getJeItem(XmpHandler.ITEM_COMMENT));
}
return result;
}
/**
* Change metadata of the image (EXIF and XMP as far as applicable).
*
* @param jpegImageFileName
* the file for which metadata should be changed.
* @param metadata
* the new metadata.
* @throws ImageReadException
* thrown if the metadata cannot be read.
* @throws ImageWriteException
* thrown if the metadata cannot be written.
* @throws IOException
* thrown in case of other errors while reading metadata.
* @throws XMPException
* thrown in case of issues with XML handling.
*/
public static void changeMetadata(final String jpegImageFileName, final JpegMetadata metadata) throws IOException,
ImageReadException, ImageWriteException, XMPException {
if (changeJpegAllowed()) {
checkJpeg(jpegImageFileName);
changeXmpMetadata(jpegImageFileName, metadata);
if (changeExifAllowed()) {
changeExifMetadata(jpegImageFileName, metadata);
}
}
}
/**
* Change the EXIF metadata.
*
* @param jpegImageFileName
* the file for which metadata should be changed.
* @param metadata
* the new metadata
* @throws ImageReadException
* thrown if the metadata cannot be read.
* @throws ImageWriteException
* thrown if the metadata cannot be written.
* @throws IOException
* thrown in case of other errors while reading metadata.
*/
@SuppressWarnings("resource")
private static void changeExifMetadata(final String jpegImageFileName, final JpegMetadata metadata)
throws IOException, ImageReadException, ImageWriteException {
File jpegImageFile = new File(jpegImageFileName);
String tempFileName = jpegImageFileName + ".temp";
File tempFile = new File(tempFileName);
verifyTempFile(tempFile);
OutputStream os = null;
try {
TiffOutputSet outputSet = null;
// note that metadata might be null if no metadata is found.
final IImageMetadata imageMetadata = Imaging.getMetadata(jpegImageFile);
final JpegImageMetadata jpegMetadata = (JpegImageMetadata) imageMetadata;
if (jpegMetadata != null) {
// note that exif might be null if no Exif metadata is found.
final TiffImageMetadata exif = jpegMetadata.getExif();
if (exif != null) {
outputSet = exif.getOutputSet();
}
}
if (outputSet == null) {
outputSet = new TiffOutputSet();
}
final TiffOutputDirectory rootDirectory = outputSet.getOrCreateRootDirectory();
final TiffOutputDirectory exifDirectory = outputSet.getOrCreateExifDirectory();
if (metadata.getTitle() != null) {
rootDirectory.removeField(MicrosoftTagConstants.EXIF_TAG_XPTITLE);
rootDirectory.add(MicrosoftTagConstants.EXIF_TAG_XPTITLE, metadata.getTitle());
rootDirectory.removeField(TiffTagConstants.TIFF_TAG_IMAGE_DESCRIPTION);
rootDirectory.add(TiffTagConstants.TIFF_TAG_IMAGE_DESCRIPTION, metadata.getTitle());
}
if (metadata.getComment() != null) {
rootDirectory.removeField(MicrosoftTagConstants.EXIF_TAG_XPCOMMENT);
rootDirectory.add(MicrosoftTagConstants.EXIF_TAG_XPCOMMENT, metadata.getComment());
exifDirectory.removeField(ExifTagConstants.EXIF_TAG_USER_COMMENT);
exifDirectory.add(ExifTagConstants.EXIF_TAG_USER_COMMENT, metadata.getComment());
}
if (metadata.getSubject() != null) {
rootDirectory.removeField(MicrosoftTagConstants.EXIF_TAG_XPSUBJECT);
rootDirectory.add(MicrosoftTagConstants.EXIF_TAG_XPSUBJECT, metadata.getSubject());
}
try {
os = new FileOutputStream(tempFile);
os = new BufferedOutputStream(os);
new ExifRewriter().updateExifMetadataLossless(jpegImageFile, os, outputSet);
}
catch (Exception e) {
Logger.warning("Error storing EXIF data lossless - try lossy approach");
os = new FileOutputStream(tempFile);
os = new BufferedOutputStream(os);
new ExifRewriter().updateExifMetadataLossy(jpegImageFile, os, outputSet);
}
IoUtils.closeQuietly(true, os);
if (!FileUtil.moveFile(tempFile, jpegImageFile)) {
throw new IOException("Failed to rename file " + tempFileName + " to " + jpegImageFileName);
}
}
finally {
IoUtils.closeQuietly(false, os);
}
}
/**
* Change the XMP metadata.
*
* @param jpegImageFileName
* the file for which metadata should be changed.
* @param metadata
* the new metadata.
*
* @throws ImageReadException
* thrown if the metadata cannot be read.
* @throws ImageWriteException
* thrown if the metadata cannot be written.
* @throws IOException
* thrown in case of other errors while reading metadata.
* @throws XMPException
* thrown in case of issues with XML handling.
*/
@SuppressWarnings("resource")
private static void changeXmpMetadata(final String jpegImageFileName, final JpegMetadata metadata)
throws IOException, ImageReadException, ImageWriteException, XMPException {
File jpegImageFile = new File(jpegImageFileName);
String tempFileName = jpegImageFileName + ".temp";
File tempFile = new File(tempFileName);
verifyTempFile(tempFile);
OutputStream os = null;
try {
final String xmpString = Imaging.getXmpXml(jpegImageFile);
XmpHandler parser = new XmpHandler(xmpString);
if (changeExifAllowed()) {
// Change standard fields only if EXIF allowed
parser.setDcTitle(metadata.getTitle());
parser.setDcDescription(metadata.getDescription());
parser.setDcSubject(metadata.getSubject());
parser.setUserComment(metadata.getComment());
parser.setMicrosoftPerson(metadata.getPerson());
}
parser.setJeItem(XmpHandler.ITEM_TITLE, metadata.getTitle());
parser.setJeItem(XmpHandler.ITEM_DESCRIPTION, metadata.getDescription());
parser.setJeItem(XmpHandler.ITEM_SUBJECT, metadata.getSubject());
parser.setJeItem(XmpHandler.ITEM_COMMENT, metadata.getComment());
parser.setJeItem(XmpHandler.ITEM_PERSON, metadata.getPerson());
parser.setJeItem(XmpHandler.ITEM_X_CENTER, metadata.getXCenterString());
parser.setJeItem(XmpHandler.ITEM_Y_CENTER, metadata.getYCenterString());
parser.setJeItem(XmpHandler.ITEM_OVERLAY_SCALE_FACTOR, metadata.getOverlayScaleFactorString());
parser.setJeItem(XmpHandler.ITEM_X_POSITION, metadata.getXPositionString());
parser.setJeItem(XmpHandler.ITEM_Y_POSITION, metadata.getYPositionString());
parser.setJeItem(XmpHandler.ITEM_ZOOM_FACTOR, metadata.getZoomFactorString());
parser.setJeDate(XmpHandler.ITEM_ORGANIZE_DATE, metadata.getOrganizeDate());
parser.setJeItem(XmpHandler.ITEM_RIGHT_LEFT, metadata.getRightLeftString());
parser.setJeItem(XmpHandler.ITEM_BRIGHTNESS, metadata.getBrightnessString());
parser.setJeItem(XmpHandler.ITEM_CONTRAST, metadata.getContrastString());
parser.setJeItem(XmpHandler.ITEM_SATURATION, metadata.getSaturationString());
parser.setJeItem(XmpHandler.ITEM_COLOR_TEMPERATURE, metadata.getColorTemperatureString());
parser.setJeItem(XmpHandler.ITEM_OVERLAY_COLOR, metadata.getOverlayColorString());
parser.setJeItem(XmpHandler.ITEM_PUPIL_SIZE, metadata.getPupilSizeString());
parser.setJeItem(XmpHandler.ITEM_PUPIL_X_OFFSET, metadata.getPupilXOffsetString());
parser.setJeItem(XmpHandler.ITEM_PUPIL_Y_OFFSET, metadata.getPupilYOffsetString());
parser.setJeInt(XmpHandler.ITEM_FLAGS, metadata.getFlags());
os = new FileOutputStream(tempFile);
os = new BufferedOutputStream(os);
new JpegXmpRewriter().updateXmpXml(jpegImageFile, os, parser.getXmpString());
IoUtils.closeQuietly(true, os);
if (!FileUtil.moveFile(tempFile, jpegImageFile)) {
throw new IOException("Failed to rename file " + tempFileName + " to " + jpegImageFileName);
}
}
finally {
IoUtils.closeQuietly(false, os);
}
}
/**
* Verify if the temporary file already exists. If yes, delete it.
*
* @param tempFile
* the temporary file.
*/
private static void verifyTempFile(final File tempFile) {
if (tempFile.exists()) {
Logger.warning("tempFile " + tempFile.getName() + " already exists - deleting it");
boolean success = tempFile.delete();
if (!success) {
Logger.warning("Failed to delete file" + tempFile.getName());
}
}
}
/**
* Check if the settings allow a change of the JPEG.
*
* @return true if it is allowed to change image files.
*/
public static boolean changeJpegAllowed() {
int storeOption = PreferenceUtil.getPreferenceInt(PreferenceUtil.KEY_STORE_OPTION);
return storeOption > 0;
}
/**
* Check if the settings allow a change of the EXIF data.
*
* @return true if it is allowed to change EXIF data.
*/
private static boolean changeExifAllowed() {
int storeOption = PreferenceUtil.getPreferenceInt(PreferenceUtil.KEY_STORE_OPTION);
return storeOption == 2;
}
}