/* * Copyright (C) 2014 Alec Dhuse * * This program 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 3 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package co.foldingmap.imaging; /* * Copyright 2013 Alec Dhuse * Copyright 2002-2012 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; /** * Adapted from Metadata-Extractor by Drew Noakes. This class will read the * exif headers from a jpeg file and return a GeoTag class with the location * and time the photo was taken. * * @author */ public class JpegGeoTagReader { public static final byte[] BYTES_PER_FORMAT = {1, 1, 1, 2, 4, 8}; public static final int GPS_INFO_OFFSET = 34853; public static final int TIFF_HEADER_OFFSET = 6; public static final byte SEGMENTS_END = (byte) 218; public static final byte GPS_LATITUDE_REF = 1; public static final byte GPS_LATITUDE = 2; public static final byte GPS_LONGITUDE_REF = 3; public static final byte GPS_LONGITUDE = 4; public static final byte GPS_ALTITUDE = 6; public static final byte GPS_TIME = 7; public static final byte GPS_DATE = 29; private ArrayList<byte[]> segmentDataList; private boolean isBigEndianByteOrder; private byte[] byteBuffer; public JpegGeoTagReader() { } /** * Determine the offset at which a given tag starts. * * @param dirStartOffset the offset at which the IFD starts * @param entryNumber the zero-based entry number */ private static int calculateTagOffset(int startOffset, int entryNumber) { // add 2 bytes for the tag count // each entry is 12 bytes, so we skip 12 * the number seen so far return startOffset + 2 + (12 * entryNumber); } private GeoTag extract() throws Exception { GeoTag geo = new GeoTag(); // this should be either "MM" or "II" String byteOrderIdentifier = getString(TIFF_HEADER_OFFSET, 2); if (byteOrderIdentifier.equals("MM")) { //big-endian byte order isBigEndianByteOrder = true; } else if (byteOrderIdentifier.equals("II")) { //little-endian byte order isBigEndianByteOrder = false; } else { return null; } int firstDirectoryOffset = getInt32(4 + TIFF_HEADER_OFFSET) + TIFF_HEADER_OFFSET; processDirectory(geo, firstDirectoryOffset); return geo; } /** * Process one of the nested Tiff IFD directories. * <p/> * Header * 2 bytes: number of tags * <p/> * Then for each tag * 2 bytes: tag type * 2 bytes: format code * 4 bytes: component count */ private void processDirectory(GeoTag geo, int directoryOffset) throws Exception { // First two bytes in the IFD are the number of tags in this directory int dirTagCount = getUnsignedInt16(directoryOffset); // Handle each tag in this directory for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++) { int tagOffset = calculateTagOffset(directoryOffset, tagNumber); int tagType = getUnsignedInt16(tagOffset); int formatCode = getUnsignedInt16(tagOffset + 2); int componentCount = getInt32(tagOffset + 4); int byteCount = componentCount * BYTES_PER_FORMAT[formatCode]; int tagValueOffset; if (byteCount > 4) { // If it's bigger than 4 bytes, the dir entry contains an offset. final int offsetVal = getInt32(tagOffset + 8); tagValueOffset = TIFF_HEADER_OFFSET + offsetVal; } else { // 4 bytes or less and value is in the dir entry itself tagValueOffset = tagOffset + 8; } switch (tagType) { case GPS_INFO_OFFSET: { int subdirOffset = TIFF_HEADER_OFFSET + getInt32(tagValueOffset); processDirectory(geo, subdirOffset); continue; } default: { processGeoInfo(geo, tagType, tagValueOffset, componentCount); break; } } } // at the end of each IFD is an optional link to the next IFD int finalTagOffset = calculateTagOffset(directoryOffset, dirTagCount); int nextDirectoryOffset = getInt32(finalTagOffset); if (nextDirectoryOffset != 0) { nextDirectoryOffset += TIFF_HEADER_OFFSET; } } private void processGeoInfo(GeoTag geo, int tagType, int tagValueOffset, int componentCount) throws Exception { switch (tagType) { case GPS_ALTITUDE: geo.setAltitude(getUnsignedInt32(tagValueOffset) / getUnsignedInt32(tagValueOffset + 4)); break; case GPS_TIME: double[] times = new double[componentCount]; for (int i = 0; i < componentCount; i++) times[i] = getUnsignedInt32(tagValueOffset + (8 * i)) / getUnsignedInt32(tagValueOffset + 4 + (8 * i)); geo.setTime(times[0], times[1], times[2]); break; case GPS_LONGITUDE_REF: geo.setLongitudeReference(getNullTerminatedString(tagValueOffset, componentCount)); break; case GPS_LONGITUDE: double[] longitudes = new double[componentCount]; for (int i = 0; i < componentCount; i++) longitudes[i] = getUnsignedInt32(tagValueOffset + (8 * i)) / getUnsignedInt32(tagValueOffset + 4 + (8 * i)); Double lon = GeoTag.degreesToDecimal(longitudes[0], longitudes[1], longitudes[2]); geo.setLongitude(lon); break; case GPS_LATITUDE_REF: geo.setLatitudeReference(getNullTerminatedString(tagValueOffset, componentCount)); break; case GPS_LATITUDE: double[] latitudes = new double[componentCount]; for (int i = 0; i < componentCount; i++) latitudes[i] = getUnsignedInt32(tagValueOffset + (8 * i)) / getUnsignedInt32(tagValueOffset + 4 + (8 * i)); Double lat = GeoTag.degreesToDecimal(latitudes[0], latitudes[1], latitudes[2]); geo.setLatitude(lat); break; case GPS_DATE: String date = getNullTerminatedString(tagValueOffset, componentCount); geo.setDate(date); break; } } public GeoTag readMetadata(File file) throws Exception { GeoTag geoLocation = new GeoTag(); BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); //init segmentDataList = new ArrayList<byte[]>(); isBigEndianByteOrder = true; readSegments(bis); bis.close(); // Loop through all segments, looking for the EXIF segment. for (byte[] segment : segmentDataList) { if ("EXIF".equalsIgnoreCase(new String(segment, 0, 4))) { this.byteBuffer = segment; geoLocation = extract(); } } return geoLocation; } private void readSegments(BufferedInputStream inputStream) throws IOException { try { boolean hasValidHeader; int offset = 0; byte[] headerBytes = new byte[2]; if (inputStream.read(headerBytes, 0, 2) != 2) { hasValidHeader = false; } else { hasValidHeader = (headerBytes[0] & 0xFF) == 0xFF && (headerBytes[1] & 0xFF) == 0xD8; } if (!hasValidHeader) throw new IOException("This file is not a jpeg file."); offset += 2; do { byte segmentIdentifier = (byte) (inputStream.read() & 0xFF); offset++; // next byte is <segment-marker> byte thisSegmentMarker = (byte) (inputStream.read() & 0xFF); offset++; // next 2-bytes are <segment-size>: [high-byte] [low-byte] byte[] segmentLengthBytes = new byte[2]; if (inputStream.read(segmentLengthBytes, 0, 2) != 2) throw new IOException("Malformed Jpeg data."); offset += 2; int segmentLength = ((segmentLengthBytes[0] << 8) & 0xFF00) | (segmentLengthBytes[1] & 0xFF); // segment length includes size bytes, so subtract two segmentLength -= 2; byte[] segmentBytes = new byte[segmentLength]; if (inputStream.read(segmentBytes, 0, segmentLength) != segmentLength) throw new IOException("Jpeg data ended unexpectedly."); offset += segmentLength; if (thisSegmentMarker == JpegGeoTagReader.SEGMENTS_END) { break; } else { segmentDataList.add(segmentBytes); } } while (true); inputStream.close(); } catch (IOException ioe) { throw new IOException("Exception processing Jpeg file: " + ioe.getMessage(), ioe); } } private int getUnsignedInt16(int index) throws Exception { if (isBigEndianByteOrder) { // Motorola - MSB first return (byteBuffer[index ] << 8 & 0xFF00) | (byteBuffer[index + 1] & 0xFF); } else { // Intel ordering - LSB first return (byteBuffer[index + 1] << 8 & 0xFF00) | (byteBuffer[index ] & 0xFF); } } private long getUnsignedInt32(int index) throws Exception { if (isBigEndianByteOrder) { // Motorola - MSB first (big endian) return (((long) byteBuffer[index ]) << 24 & 0xFF000000L) | (((long) byteBuffer[index + 1]) << 16 & 0xFF0000L) | (((long) byteBuffer[index + 2]) << 8 & 0xFF00L) | (((long) byteBuffer[index + 3]) & 0xFFL); } else { // Intel ordering - LSB first (little endian) return (((long) byteBuffer[index + 3]) << 24 & 0xFF000000L) | (((long) byteBuffer[index + 2]) << 16 & 0xFF0000L) | (((long) byteBuffer[index + 1]) << 8 & 0xFF00L) | (((long) byteBuffer[index ]) & 0xFFL); } } private int getInt32(int index) throws Exception { if (isBigEndianByteOrder) { // Motorola - MSB first (big endian) return (byteBuffer[index ] << 24 & 0xFF000000) | (byteBuffer[index + 1] << 16 & 0xFF0000) | (byteBuffer[index + 2] << 8 & 0xFF00) | (byteBuffer[index + 3] & 0xFF); } else { // Intel ordering - LSB first (little endian) return (byteBuffer[index + 3] << 24 & 0xFF000000) | (byteBuffer[index + 2] << 16 & 0xFF0000) | (byteBuffer[index + 1] << 8 & 0xFF00) | (byteBuffer[index ] & 0xFF); } } private String getString(int index, int length) throws Exception { byte[] bytes = new byte[length]; System.arraycopy(byteBuffer, index, bytes, 0, length); return new String(bytes); } private String getNullTerminatedString(int index, int maxLengthBytes) throws Exception { // Check for null terminators int length = 0; while ((index + length) < byteBuffer.length && byteBuffer[index + length] != '\0' && length < maxLengthBytes) length++; byte[] bytes = new byte[length]; System.arraycopy(byteBuffer, index, bytes, 0, length); return new String(bytes); } }