slash.navigation.photo.PhotoFormat.java Source code

Java tutorial

Introduction

Here is the source code for slash.navigation.photo.PhotoFormat.java

Source

/*
This file is part of RouteConverter.
    
RouteConverter is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
    
RouteConverter is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with RouteConverter; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
    
Copyright (C) 2007 Christian Pesch. All Rights Reserved.
*/

package slash.navigation.photo;

import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.ImageWriteException;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.common.ImageMetadata;
import org.apache.commons.imaging.common.RationalNumber;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
import org.apache.commons.imaging.formats.tiff.TiffDirectory;
import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
import org.apache.commons.imaging.formats.tiff.TiffImageMetadata.Directory;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
import slash.common.type.CompactCalendar;
import slash.navigation.base.ParserContext;
import slash.navigation.base.RouteCharacteristics;
import slash.navigation.base.SimpleFormat;
import slash.navigation.base.Wgs84Position;
import slash.navigation.base.Wgs84Route;
import slash.navigation.common.NavigationPosition;

import java.awt.*;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.logging.Logger;

import static java.io.File.createTempFile;
import static java.lang.Math.abs;
import static java.util.Calendar.DAY_OF_MONTH;
import static java.util.Calendar.HOUR;
import static java.util.Calendar.HOUR_OF_DAY;
import static java.util.Calendar.MINUTE;
import static java.util.Calendar.MONTH;
import static java.util.Calendar.SECOND;
import static java.util.Calendar.YEAR;
import static java.util.Collections.singletonList;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_DATE_TIME_DIGITIZED;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_EXIF_IMAGE_LENGTH;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_EXIF_IMAGE_WIDTH;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_EXPOSURE_TIME;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_FLASH;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_FNUMBER;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_FOCAL_LENGTH;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_ISO;
import static org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants.EXIF_TAG_USER_COMMENT;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_ALTITUDE;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_ALTITUDE_REF;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_DATE_STAMP;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_DOP;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_LATITUDE;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_LONGITUDE;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_MEASURE_MODE;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_MEASURE_MODE_VALUE_2_DIMENSIONAL_MEASUREMENT;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_MEASURE_MODE_VALUE_3_DIMENSIONAL_MEASUREMENT;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_SATELLITES;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_SPEED;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_SPEED_REF;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_SPEED_REF_VALUE_KMPH;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_SPEED_REF_VALUE_KNOTS;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_SPEED_REF_VALUE_MPH;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_TIME_STAMP;
import static org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants.GPS_TAG_GPS_VERSION_ID;
import static org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants.DIRECTORY_TYPE_EXIF;
import static org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants.DIRECTORY_TYPE_GPS;
import static org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants.DIRECTORY_TYPE_ROOT;
import static org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants.TIFF_TAG_DATE_TIME;
import static org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants.TIFF_TAG_MAKE;
import static org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants.TIFF_TAG_MODEL;
import static slash.common.helpers.ExceptionHelper.getLocalizedMessage;
import static slash.common.io.Directories.getTemporaryDirectory;
import static slash.common.io.InputOutput.copyAndClose;
import static slash.common.io.Transfer.parseInteger;
import static slash.common.io.Transfer.trim;
import static slash.common.type.CompactCalendar.fromCalendar;
import static slash.common.type.CompactCalendar.parseDate;
import static slash.common.type.ISO8601.formatDate;
import static slash.navigation.base.RouteCharacteristics.Waypoints;
import static slash.navigation.base.WaypointType.Photo;
import static slash.navigation.common.UnitConversion.nauticMilesToKiloMeter;
import static slash.navigation.common.UnitConversion.statuteMilesToKiloMeter;
import static slash.navigation.photo.TagState.NotTaggable;
import static slash.navigation.photo.TagState.Tagged;

/**
 * Reads a route with a position from Photo (.jpg) files with embedded EXIF and GPS metadata.
 *
 * @author Christian Pesch
 */
public class PhotoFormat extends SimpleFormat<Wgs84Route> {
    private static final Logger log = Logger.getLogger(PhotoFormat.class.getName());
    private static final String DATE_FORMAT = "yyyy:MM:dd";
    private static final String DATE_TIME_FORMAT = "yyyy:MM:dd HH:mm:ss";
    private static final DecimalFormat XX_FORMAT = new DecimalFormat("00");
    private static final DecimalFormat XXXX_FORMAT = new DecimalFormat("0000");
    private static final int READ_BUFFER_SIZE = 64 * 1024;

    public String getName() {
        return "Photo (" + getExtension() + ")";
    }

    public String getExtension() {
        return ".jpg";
    }

    public int getMaximumPositionCount() {
        return 1;
    }

    public boolean isSupportsMultipleRoutes() {
        return false;
    }

    public boolean isWritingRouteCharacteristics() {
        return false;
    }

    @SuppressWarnings("unchecked")
    public <P extends NavigationPosition> Wgs84Route createRoute(RouteCharacteristics characteristics, String name,
            List<P> positions) {
        return new Wgs84Route(this, characteristics, (List<Wgs84Position>) positions);
    }

    public void read(BufferedReader reader, String encoding, ParserContext<Wgs84Route> context) throws IOException {
        // this format parses the InputStream directly but wants to derive from SimpleFormat to use Wgs84Route
        throw new UnsupportedOperationException();
    }

    public void write(Wgs84Route route, PrintWriter writer, int startIndex, int endIndex) {
        // this format parses the InputStream directly but wants to derive from SimpleFormat to use Wgs84Route
        throw new UnsupportedOperationException();
    }

    public void read(InputStream source, ParserContext<Wgs84Route> context) throws Exception {
        BufferedInputStream bufferedSource = new BufferedInputStream(source, READ_BUFFER_SIZE);
        bufferedSource.mark(READ_BUFFER_SIZE);

        Dimension size = Imaging.getImageSize(bufferedSource, null);
        if (size == null)
            return;

        PhotoPosition position = new PhotoPosition(NotTaggable, context.getStartDate(), "No EXIF data", null);

        bufferedSource.reset();
        ImageMetadata metadata = Imaging.getMetadata(bufferedSource, null);
        TiffImageMetadata tiffImageMetadata = extractTiffImageMetadata(metadata);
        if (tiffImageMetadata != null) {
            @SuppressWarnings("unchecked")
            List<Directory> directories = (List<Directory>) tiffImageMetadata.getDirectories();
            for (Directory directory : directories)
                log.info("Reading EXIF directory " + directory);

            extendPosition(position, tiffImageMetadata, context.getStartDate());
        }

        bufferedSource.reset();
        File image = context.getFile();
        if (image == null)
            image = extractToTempFile(bufferedSource);
        position.setOrigin(image);
        position.setWaypointType(Photo);
        context.appendRoute(new Wgs84Route(this, Waypoints, new ArrayList<Wgs84Position>(singletonList(position))));
    }

    private TiffImageMetadata extractTiffImageMetadata(ImageMetadata metadata) {
        TiffImageMetadata result = null;
        if (metadata instanceof JpegImageMetadata)
            result = ((JpegImageMetadata) metadata).getExif();
        else if (metadata instanceof TiffImageMetadata)
            result = (TiffImageMetadata) metadata;
        return result;
    }

    private File extractToTempFile(InputStream inputStream) throws IOException {
        File temp = createTempFile("photo", ".jpg", getTemporaryDirectory());
        temp.deleteOnExit();
        copyAndClose(inputStream, new FileOutputStream(temp));
        return temp;
    }

    private String parseRoot(TiffImageMetadata metadata, TagInfo tag) throws ImageReadException {
        TiffDirectory rootDirectory = metadata.findDirectory(DIRECTORY_TYPE_ROOT);
        return rootDirectory != null ? trim((String) rootDirectory.getFieldValue(tag)) : null;
    }

    private String parseMake(TiffImageMetadata metadata) throws ImageReadException {
        return parseRoot(metadata, TIFF_TAG_MAKE);
    }

    private String parseModel(TiffImageMetadata metadata) throws ImageReadException {
        return parseRoot(metadata, TIFF_TAG_MODEL);
    }

    private CompactCalendar parseDateTime(TiffImageMetadata metadata) throws ImageReadException {
        return parseDate(parseRoot(metadata, TIFF_TAG_DATE_TIME), DATE_TIME_FORMAT);
    }

    private String parseExif(TiffImageMetadata metadata, TagInfo tag) throws ImageReadException {
        TiffDirectory exifDirectory = metadata.findDirectory(DIRECTORY_TYPE_EXIF);
        return exifDirectory != null ? trim((String) exifDirectory.getFieldValue(tag)) : null;
    }

    private Integer parseExifInteger(TiffImageMetadata metadata, TagInfo tag) throws ImageReadException {
        TiffDirectory exifDirectory = metadata.findDirectory(DIRECTORY_TYPE_EXIF);
        if (exifDirectory == null)
            return null;
        Object fieldValue = exifDirectory.getFieldValue(tag);
        return fieldValue != null ? new Integer(fieldValue.toString()) : null;
    }

    private RationalNumber parseExifRationalNumber(TiffImageMetadata metadata, TagInfo tag)
            throws ImageReadException {
        TiffDirectory exifDirectory = metadata.findDirectory(DIRECTORY_TYPE_EXIF);
        if (exifDirectory == null)
            return null;
        return (RationalNumber) exifDirectory.getFieldValue(tag);
    }

    private String parseUserComment(TiffImageMetadata metadata) throws ImageReadException {
        TiffDirectory exifDirectory = metadata.findDirectory(DIRECTORY_TYPE_EXIF);
        if (exifDirectory != null) {
            String userComment = parseExif(metadata, EXIF_TAG_USER_COMMENT);
            if (userComment != null)
                return userComment;
        }

        TiffDirectory rootDirectory = metadata.findDirectory(DIRECTORY_TYPE_ROOT);
        if (rootDirectory != null) {
            String make = parseMake(metadata);
            String model = parseModel(metadata);
            CompactCalendar dateTime = parseDateTime(metadata);
            return trim((make != null ? make : "") + " " + (model != null ? model : "") + " Photo"
                    + (dateTime != null ? " from " + formatDate(dateTime) : ""));
        }
        return "GPS";
    }

    private CompactCalendar parseExifTime(TiffImageMetadata metadata, CompactCalendar startDate)
            throws ImageReadException {
        String dateString = null;
        TiffDirectory exifDirectory = metadata.findDirectory(DIRECTORY_TYPE_EXIF);
        if (exifDirectory != null) {
            dateString = parseExif(metadata, EXIF_TAG_DATE_TIME_ORIGINAL);
            if (dateString == null)
                dateString = parseExif(metadata, EXIF_TAG_DATE_TIME_DIGITIZED);
        }
        TiffDirectory rootDirectory = metadata.findDirectory(DIRECTORY_TYPE_ROOT);
        if (rootDirectory != null && dateString == null)
            dateString = (String) rootDirectory.getFieldValue(TIFF_TAG_DATE_TIME);
        return dateString != null ? parseDate(dateString, DATE_TIME_FORMAT) : startDate;
    }

    private RationalNumber getFieldValue(TiffDirectory directory, TagInfoRational tagInfoRational) {
        try {
            return directory.getFieldValue(tagInfoRational);
        } catch (ImageReadException e) {
            log.fine("Could not parse " + tagInfoRational + ": " + getLocalizedMessage(e));
            return null;
        }
    }

    private Double parseAltitude(TiffDirectory directory) throws ImageReadException {
        RationalNumber altitudeNumber = getFieldValue(directory, GPS_TAG_GPS_ALTITUDE);
        if (altitudeNumber == null)
            return null;

        double altitude = altitudeNumber.doubleValue();
        Byte altitudeRef = directory.getFieldValue(GPS_TAG_GPS_ALTITUDE_REF);
        return altitudeRef == 1 ? -altitude : altitude;
    }

    private Double parseSpeed(TiffDirectory directory) throws ImageReadException {
        RationalNumber speedNumber = getFieldValue(directory, GPS_TAG_GPS_SPEED);
        if (speedNumber == null)
            return null;

        double speed = speedNumber.doubleValue();
        String speedRef = (String) directory.getFieldValue(GPS_TAG_GPS_SPEED_REF);
        if (speedRef != null) {
            if (GPS_TAG_GPS_SPEED_REF_VALUE_MPH.equals(speedRef))
                speed = statuteMilesToKiloMeter(speed);
            else if (GPS_TAG_GPS_SPEED_REF_VALUE_KNOTS.equals(speedRef))
                speed = nauticMilesToKiloMeter(speed);
        }
        return speed;
    }

    private CompactCalendar parseGPSTime(TiffDirectory directory, CompactCalendar startDate)
            throws ImageReadException {
        String dateString = (String) directory.getFieldValue(GPS_TAG_GPS_DATE_STAMP);
        CompactCalendar date = dateString != null ? parseDate(dateString, DATE_FORMAT) : startDate;
        RationalNumber[] timeStamp = (RationalNumber[]) directory.getFieldValue(GPS_TAG_GPS_TIME_STAMP);
        if (timeStamp != null) {
            Calendar calendar = date.getCalendar();
            calendar.set(HOUR, timeStamp[0].intValue());
            calendar.set(MINUTE, timeStamp[1].intValue());
            calendar.set(SECOND, timeStamp[2].intValue());
            date = fromCalendar(calendar);
        }
        return date;
    }

    private Double parseDirection(TiffDirectory directory) throws ImageReadException {
        RationalNumber direction = getFieldValue(directory, GPS_TAG_GPS_IMG_DIRECTION);
        return direction != null ? direction.doubleValue() : null;
    }

    private Integer parseSatellites(TiffDirectory directory) throws ImageReadException {
        String satellites = (String) directory.getFieldValue(GPS_TAG_GPS_SATELLITES);
        return satellites != null ? parseInteger(satellites) : null;
    }

    private boolean is2DimensionalMeasurement(TiffDirectory directory) throws ImageReadException {
        String measurementMode = (String) directory.getFieldValue(GPS_TAG_GPS_MEASURE_MODE);
        return measurementMode != null && measurementMode
                .equals(Integer.toString(GPS_TAG_GPS_MEASURE_MODE_VALUE_2_DIMENSIONAL_MEASUREMENT));
    }

    private Double parseDOP(TiffDirectory directory) throws ImageReadException {
        RationalNumber dop = getFieldValue(directory, GPS_TAG_GPS_DOP);
        return dop != null ? dop.doubleValue() : null;
    }

    private void extendPosition(PhotoPosition position, TiffImageMetadata metadata, CompactCalendar startDate)
            throws ImageReadException {
        TiffImageMetadata.GPSInfo gpsInfo = metadata.getGPS();
        if (gpsInfo != null) {
            position.setLongitude(gpsInfo.getLongitudeAsDegreesEast());
            position.setLatitude(gpsInfo.getLatitudeAsDegreesNorth());
            position.setTagState(Tagged);
        }

        CompactCalendar time = parseExifTime(metadata, startDate);
        TiffDirectory gpsDirectory = metadata.findDirectory(DIRECTORY_TYPE_GPS);
        if (gpsDirectory != null) {
            position.setElevation(parseAltitude(gpsDirectory));
            position.setSpeed(parseSpeed(gpsDirectory));
            time = parseGPSTime(gpsDirectory, time);

            position.setHeading(parseDirection(gpsDirectory));
            position.setSatellites(parseSatellites(gpsDirectory));
            Double dop = parseDOP(gpsDirectory);
            if (is2DimensionalMeasurement(gpsDirectory))
                position.setHdop(dop);
            else
                position.setPdop(dop);
        }
        position.setTime(time);

        position.setDescription(parseUserComment(metadata));
        position.setMake(parseMake(metadata));
        position.setModel(parseModel(metadata));
        position.setWidth(parseExifInteger(metadata, EXIF_TAG_EXIF_IMAGE_WIDTH));
        position.setHeight(parseExifInteger(metadata, EXIF_TAG_EXIF_IMAGE_LENGTH));
        position.setfNumber(parseExifRationalNumber(metadata, EXIF_TAG_FNUMBER));
        position.setExposure(parseExifRationalNumber(metadata, EXIF_TAG_EXPOSURE_TIME));
        position.setFlash(parseExifInteger(metadata, EXIF_TAG_FLASH));
        position.setFocal(parseExifRationalNumber(metadata, EXIF_TAG_FOCAL_LENGTH));
        position.setPhotographicSensitivity(parseExifInteger(metadata, EXIF_TAG_ISO));
    }

    public void write(Wgs84Route route, OutputStream target, int startIndex, int endIndex) throws IOException {
        List<Wgs84Position> positions = route.getPositions();
        for (int i = startIndex; i < endIndex; i++) {
            PhotoPosition position = (PhotoPosition) positions.get(i);
            File source = position.getOrigin(File.class);
            if (source != null)
                write(position, source, target);
        }
    }

    public void write(PhotoPosition position, File source, OutputStream target) throws IOException {
        try {
            ImageMetadata metadata = Imaging.getMetadata(source);
            TiffImageMetadata tiffImageMetadata = extractTiffImageMetadata(metadata);

            TiffOutputSet outputSet = null;
            if (tiffImageMetadata != null)
                outputSet = tiffImageMetadata.getOutputSet();
            if (outputSet == null)
                outputSet = new TiffOutputSet();

            TiffOutputDirectory rootDirectory = outputSet.getOrCreateRootDirectory();
            rootDirectory.removeField(TIFF_TAG_MAKE);
            rootDirectory.removeField(TIFF_TAG_MODEL);

            rootDirectory.add(TIFF_TAG_MAKE, position.getMake());
            rootDirectory.add(TIFF_TAG_MODEL, position.getModel());

            TiffOutputDirectory exifDirectory = outputSet.getOrCreateExifDirectory();
            exifDirectory.removeField(EXIF_TAG_USER_COMMENT);
            exifDirectory.removeField(EXIF_TAG_EXIF_IMAGE_WIDTH);
            exifDirectory.removeField(EXIF_TAG_EXIF_IMAGE_LENGTH);
            exifDirectory.removeField(EXIF_TAG_FNUMBER);
            exifDirectory.removeField(EXIF_TAG_EXPOSURE_TIME);
            exifDirectory.removeField(EXIF_TAG_FLASH);
            exifDirectory.removeField(EXIF_TAG_FOCAL_LENGTH);
            exifDirectory.removeField(EXIF_TAG_ISO);

            exifDirectory.add(EXIF_TAG_USER_COMMENT, position.getDescription());
            if (position.getWidth() != null)
                exifDirectory.add(EXIF_TAG_EXIF_IMAGE_WIDTH, position.getWidth().shortValue());
            if (position.getHeight() != null)
                exifDirectory.add(EXIF_TAG_EXIF_IMAGE_LENGTH, position.getHeight().shortValue());
            exifDirectory.add(EXIF_TAG_FNUMBER, position.getfNumber());
            exifDirectory.add(EXIF_TAG_EXPOSURE_TIME, position.getExposure());
            if (position.getFlash() != null)
                exifDirectory.add(EXIF_TAG_FLASH, position.getFlash().shortValue());
            exifDirectory.add(EXIF_TAG_FOCAL_LENGTH, position.getFocal());
            if (position.getPhotographicSensitivity() != null)
                exifDirectory.add(EXIF_TAG_ISO, position.getPhotographicSensitivity().shortValue());

            TiffOutputDirectory gpsDirectory = outputSet.getOrCreateGPSDirectory();
            gpsDirectory.removeField(GPS_TAG_GPS_VERSION_ID);
            gpsDirectory.add(GPS_TAG_GPS_VERSION_ID, (byte) 2, (byte) 3, (byte) 0, (byte) 0);

            gpsDirectory.removeField(GPS_TAG_GPS_LONGITUDE);
            gpsDirectory.removeField(GPS_TAG_GPS_LONGITUDE_REF);
            gpsDirectory.removeField(GPS_TAG_GPS_LATITUDE);
            gpsDirectory.removeField(GPS_TAG_GPS_LATITUDE_REF);
            gpsDirectory.removeField(GPS_TAG_GPS_ALTITUDE);
            gpsDirectory.removeField(GPS_TAG_GPS_ALTITUDE_REF);
            gpsDirectory.removeField(GPS_TAG_GPS_SPEED);
            gpsDirectory.removeField(GPS_TAG_GPS_SPEED_REF);
            gpsDirectory.removeField(GPS_TAG_GPS_DATE_STAMP);
            gpsDirectory.removeField(GPS_TAG_GPS_TIME_STAMP);
            gpsDirectory.removeField(GPS_TAG_GPS_IMG_DIRECTION);
            gpsDirectory.removeField(GPS_TAG_GPS_SATELLITES);
            gpsDirectory.removeField(GPS_TAG_GPS_MEASURE_MODE);
            gpsDirectory.removeField(GPS_TAG_GPS_DOP);

            if (position.getLongitude() != null && position.getLatitude() != null)
                outputSet.setGPSInDegrees(position.getLongitude(), position.getLatitude());

            if (position.getElevation() != null) {
                gpsDirectory.add(GPS_TAG_GPS_ALTITUDE, RationalNumber.valueOf(abs(position.getElevation())));
                gpsDirectory.add(GPS_TAG_GPS_ALTITUDE_REF, (byte) (position.getElevation() > 0 ? 0 : 1));
            }

            if (position.getSpeed() != null) {
                gpsDirectory.add(GPS_TAG_GPS_SPEED, RationalNumber.valueOf(position.getSpeed()));
                gpsDirectory.add(GPS_TAG_GPS_SPEED_REF, GPS_TAG_GPS_SPEED_REF_VALUE_KMPH);
            }

            if (position.getTime() != null) {
                Calendar calendar = position.getTime().getCalendar();

                gpsDirectory.add(GPS_TAG_GPS_TIME_STAMP, RationalNumber.valueOf(calendar.get(HOUR_OF_DAY)),
                        RationalNumber.valueOf(calendar.get(MINUTE)), RationalNumber.valueOf(calendar.get(SECOND)));
                String dateStamp = XXXX_FORMAT.format(calendar.get(YEAR)) + ":"
                        + XX_FORMAT.format(calendar.get(MONTH) + 1) + ":"
                        + XX_FORMAT.format(calendar.get(DAY_OF_MONTH));
                gpsDirectory.add(GPS_TAG_GPS_DATE_STAMP, dateStamp);
            }

            if (position.getHeading() != null)
                gpsDirectory.add(GPS_TAG_GPS_IMG_DIRECTION, RationalNumber.valueOf(position.getHeading()));

            if (position.getSatellites() != null)
                gpsDirectory.add(GPS_TAG_GPS_SATELLITES, position.getSatellites().toString());

            if (position.getPdop() != null) {
                gpsDirectory.add(GPS_TAG_GPS_MEASURE_MODE,
                        Integer.toString(GPS_TAG_GPS_MEASURE_MODE_VALUE_3_DIMENSIONAL_MEASUREMENT));
                gpsDirectory.add(GPS_TAG_GPS_DOP, RationalNumber.valueOf(position.getPdop()));
            } else if (position.getHdop() != null) {
                gpsDirectory.add(GPS_TAG_GPS_MEASURE_MODE,
                        Integer.toString(GPS_TAG_GPS_MEASURE_MODE_VALUE_2_DIMENSIONAL_MEASUREMENT));
                gpsDirectory.add(GPS_TAG_GPS_DOP, RationalNumber.valueOf(position.getHdop()));
            }

            new ExifRewriter().updateExifMetadataLossless(source, target, outputSet);
        } catch (ImageReadException | ImageWriteException e) {
            throw new IOException(e);
        }
    }
}