/** ** -----------------------------------------------------------------------------** ** ElphelTiffReader.java ** ** loci.format compatible reader for Elphel 8/16 bpp monochrome Tiff files with ** MakerNote ** ** ** Copyright (C) 2019 Elphel, Inc. ** ** -----------------------------------------------------------------------------** ** ** ElphelTiffReader.java 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 com.elphel.imagej.readers; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.time.LocalDateTime; import java.util.Date; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Properties; import java.util.Set; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.metadata.IIOMetadata; import javax.imageio.plugins.tiff.TIFFDirectory; import javax.imageio.plugins.tiff.TIFFField; import javax.imageio.plugins.tiff.TIFFTag; import javax.imageio.stream.ImageInputStream; import javax.imageio.plugins.tiff.ExifGPSTagSet; import org.apache.commons.compress.utils.IOUtils; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; ///import ch.qos.logback.classic.Level; ///import ch.qos.logback.classic.Logger; import com.drew.imaging.ImageMetadataReader; import com.drew.metadata.Metadata; import com.drew.metadata.Tag; import com.drew.metadata.exif.ExifIFD0Directory; import com.drew.metadata.exif.ExifSubIFDDirectory; import com.drew.metadata.exif.GpsDirectory; import loci.common.ByteArrayHandle; import loci.common.Location; import loci.common.RandomAccessInputStream; import loci.common.services.ServiceException; import loci.formats.FormatException; import loci.formats.FormatTools; import loci.formats.in.MetadataLevel; import loci.formats.in.TiffReader; import loci.formats.meta.MetadataStore; import ome.xml.model.primitives.Timestamp; /* // non-IFD tags (for internal use) public static final int LITTLE_ENDIAN = 0; public static final int BIG_TIFF = 1; public static final int REUSE = 3; <MakerNote tag="0x927c" format="LONG" count="16" seq="26" dlen="64"/> 37500 package loci.formats.tiff; IFD.java public static final int MAKER_NOTE = 37500; * IFD data { 0=false, LITTLE_ENDIAN 1=false, BIG_TIFF 0x0100 256=160, ImageWidth 0x0101 257=120, ImageLength 0x0102 258= 16, BitsPerSample 0x0106 262= 1, PhotometricInterpretation 0x0129 297= [I@3d136141, PageNumber 0x8769 34665=242, ExifTag (A pointer to the Exif IFD.) 0x010e 270=[Ljava.lang.String;@640a5e9d, ImageDescription 0x010f 271=Elphel, Make 0xc62f 50735=00:0E:64:10:8F:15, CameraSerialNumber 0x0110 272=LEPTON35_15, Model 0x0111 273=557, StripOffsets 0x0131 305=https://git.elphel.com/Elphel/elphel393, Software 0x9211 37393=122, ImageNumber 0x0112 274=1, Orientation 0x0132 306=2019:05:06 14:41:51, DateTime 0x0115 277=1, SamplesPerPixel 0x0116 278=120, RowsPerStrip 0x0117 279=38400, StripByteCounts 0x00fe 254=0 NewSubfileType } */ public class ElphelTiffReader extends TiffReader{ // BaseTiffReader { // -- Constants -- public static final String ELPHEL_PROPERTY_PREFIX = "ELPHEL_"; public static final String CONTENT_FILENAME = "CONTENT_FILENAME"; public static final int TAG_IMAGE_NUMBER = 0x9211; //com.drew.metadata.exif.ExifDirectoryBase public static final int TAG_SERIAL_NUMBER = 0xc62f; public static final int TAG_IMAGE_DESCRIPTION = 0x010e; // added 08/03/2021 public static final String MAKER_NOTE = "Makernote"; public static final String SUB_SEC_TIME_ORIGINAL = "SUBSEC_TIME_ORIGINAL"; // "Sub-Sec Time Original"; public static final String EXPOSURE_TIME = "Exposure Time"; public static final String DATE_TIME_ORIGINAL = "DATE_TIME_ORIGINAL"; // "Date/Time Original"; public static final boolean REORDER = true; // false; public static final String[][] REPLACEMENT_TAGS = // to/from! TODO: check/add for GPS? {{"SUBSEC_TIME_ORIGINAL", "Sub-Sec Time Original"}, {"DATE_TIME_ORIGINAL", "Date/Time Original"}, {"Instrument_Make", "Make"}, {"Serial_Number", "Unknown tag (0xc62f)"}, {"Instrument_Model", "Model"}}; /** Merge SubIFDs into the main IFD list. */ // protected transient boolean mergeSubIFDs = true; // false; /** Logger for this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(ElphelTiffReader.class); // public static final String[] ELPHEL_TIFF_SUFFIXES = // {"tif", "tiff"}; // , "tf2", "tf8", "btf"}; // public static final String[] COMPANION_SUFFIXES = {"xml", "txt"}; // public static final int IMAGEJ_TAG = 50839; // -- Fields -- // private String inId = null; // to close Location.mapFile private URL url = null; // save here actual URL when reading file to memory private String content_fileName = null; // from Content-disposition private boolean mapped_externally = false; // file is read/mapped externally, do not close it here private boolean file_initialized = false; // private String companionFile; // private String description; // private String calibrationUnit; // private Double physicalSizeZ; // private Double timeIncrement; // private Integer xOrigin, yOrigin; private ExifSubIFDDirectory directory; private ExifIFD0Directory directory_ifd0; private GpsDirectory directory_gps; private HashMap<String,String> REPLACEMENT_TAG_MAP = null; // per instance // -- Constructor -- /** Constructs a new Tiff reader. */ public ElphelTiffReader() { super(); // "Tagged Image File Format", ELPHEL_TIFF_SUFFIXES); // See if we can use TiffReader without its parent // mergeSubIFDs = true; // false; // TODO: See if the selection is just between 2 readers (jp4 and tiff - just Elphel cameras), // or these readers are combined with all other readers in readers.txt //https://stackoverflow.com/questions/3837801/how-to-change-root-logging-level-programmatically-for-logback // LOGGER.setLevel(Level.ERROR); // Logger root = (Logger)LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); // root.setLevel(Level.ERROR); suffixNecessary = true; // false suffixSufficient = true; // false; /// mergeSubIFDs = true; // false; LOGGER.info("ElphelTiffReader(), after supper(), mergeSubIFDs = true;"); if (REPLACEMENT_TAG_MAP == null) { REPLACEMENT_TAG_MAP = new HashMap<String,String>(); for (String [] line: REPLACEMENT_TAGS) { REPLACEMENT_TAG_MAP.put(line[1], line[0]); } } } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(String, boolean) */ @Override public boolean isThisType(String name, boolean open) { if (open) { return super.isThisType(name, open); } return checkSuffix(name, getSuffixes()); } /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ /* * Check the first few bytes of a file to determine if the file can be read * by this reader. You can assume that index 0 in the stream corresponds to * the index 0 in the file. Return true if the file can be read; false if not * (or if there is no way of checking). */ @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { final int blockLen = 4; if (!FormatTools.validStream(stream, blockLen, false)) return false; byte[] signature = new byte[blockLen]; stream.read(signature); if (signature[0] != (byte) 0xff || signature[1] != (byte) 0xd8 || signature[2] != (byte) 0xff || (signature[3] & 0xf0) == 0) { return false; } return true; } @Override public void setId(String id) throws FormatException, IOException { LOGGER.info("setId("+id+"). before super" ); file_initialized = false; mapped_externally = false; if (Location.getIdMap().containsKey(id)) { LOGGER.info("id '"+id+"' is already mapped" ); content_fileName = id; // id; // maybe set to null to handle externally? mapped_externally = true; LOGGER.info("Starting setId() method, read file directly"); super.setId(id); } else { // If URL, then read to memory, if normal file - use direct access // String cameraURL = "http://192.168.0.36:2323/img"; // for testing // just testing - ignore file name and use camera URL // id = cameraURL; url = null; // String mime = null; // use to select jp4/tiff later? Or to check it is correct content_fileName = null; boolean bad_file = false; try { File test_file = new File(id); test_file.getCanonicalPath(); } catch (IOException e) { bad_file = true; } if (bad_file) { try { url = new URL(id); } catch (MalformedURLException e) { LOGGER.warn("Bad URL2: " + id); } } if (url != null) { LOGGER.info("Starting initFile() method, read "+ id +" to memory first"); //https://www.rgagnon.com/javadetails/java-0487.html URLConnection connection = url.openConnection(); String content_disposition = connection.getHeaderField("Content-Disposition"); // raw = "attachment; filename=abc.jpg" if(content_disposition != null && content_disposition.indexOf("=") != -1) { content_fileName = content_disposition.split("=")[1]; //getting value after '=' // trim quotes content_fileName= content_fileName.substring(1, content_fileName.length()-1); } else { String mime = connection.getContentType(); int slash = mime.lastIndexOf("/"); String suffix = slash < 0 ? "" : mime.substring(slash+1); content_fileName = "unknown." + suffix; } // currentId = fileName; //??? // LOGGER.info("Mime type = "+mime); // https://stackoverflow.com/questions/2793150/how-to-use-java-net-urlconnection-to-fire-and-handle-http-requests //https://stackoverflow.com/questions/2295221/java-net-url-read-stream-to-byte InputStream is = url.openStream (); // byte[] inBytes = IOUtils.toByteArray(is); if (is != null) is.close(); LOGGER.info("Bytes read: "+ inBytes.length); Location.mapFile(content_fileName, new ByteArrayHandle(inBytes)); // HashMap<String,Object> dbg_loc = Location.getIdMap(); super.setId(content_fileName); } else { // read file normally content_fileName = id; LOGGER.info("read file directly"); super.setId(id); } } //getReader // super.setId(id); LOGGER.info("setId("+id+"). after super" ); file_initialized = true; } // @Override protected void initFileOld(java.lang.String id) throws FormatException, java.io.IOException { // Trying ServiceFactory before it is going to be initialized, so static defaultFactory will be initialized // with small set of services - only needed for Elphel LOGGER.info("Starting initFile() method"); super.initFile(id); LOGGER.info("Ending initFile() method"); } /* @see loci.formats.FormatReader#initFile(String) */ // copied from ElphelJp4Reader @Override protected void initFile(String id) throws FormatException, IOException { LOGGER.info("initFile("+id+"), currentId="+currentId+", before super" ); try { super.initFile(id); // fails class_not_found } catch (IllegalArgumentException e) { throw new FormatException(e); } LOGGER.info("initFile("+id+"), currentId="+currentId+", after super" ); // Below needs to be modified - EXIFService does not work with mapFile MetadataStore store = makeFilterMetadata(); LOGGER.info("Parsing TIFF EXIF data"); HashMap<String, String> tags = null; try { // Reimplementing ExifServiceImpl as original does not have ExifIFD0Directory try (RandomAccessInputStream jpegFile = new RandomAccessInputStream(id)) { try { Metadata metadata = ImageMetadataReader.readMetadata(jpegFile); directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); directory_ifd0 = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); directory_gps = metadata.getFirstDirectoryOfType(GpsDirectory.class); } catch (Throwable e) { throw new ServiceException("Could not read EXIF data", e); } } if (directory==null) { // trying to read ImageJ file 640x512 return; } Date date = directory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL); if (date != null) { Timestamp timestamp = new Timestamp(new DateTime(date)); store.setImageAcquisitionDate(timestamp, 0); } tags = new HashMap<String, String>(); if (directory != null) { for (Tag tag : directory.getTags()) { String tag_name = tag.getTagName(); if (REPLACEMENT_TAG_MAP.containsKey(tag_name)) { tags.put(REPLACEMENT_TAG_MAP.get(tag_name), tag.getDescription()); } else { tags.put(tag.getTagName(), tag.getDescription()); } } } if (directory_ifd0 != null) { for (Tag tag : directory_ifd0.getTags()) { String tag_name = tag.getTagName(); if (REPLACEMENT_TAG_MAP.containsKey(tag_name)) { tags.put(REPLACEMENT_TAG_MAP.get(tag_name), tag.getDescription()); } else { tags.put(tag.getTagName(), tag.getDescription()); } } } if (directory_gps != null) { for (Tag tag : directory_gps.getTags()) { String tag_name = tag.getTagName(); if (REPLACEMENT_TAG_MAP.containsKey(tag_name)) { tags.put(REPLACEMENT_TAG_MAP.get(tag_name), tag.getDescription()); } else { tags.put(tag.getTagName(), tag.getDescription()); } } } // remove "sec" from exposure if (tags.containsKey(EXPOSURE_TIME)){ tags.put(EXPOSURE_TIME, tags.get(EXPOSURE_TIME).split(" ")[0]); } for (String tagName : tags.keySet()) { addGlobalMeta(tagName, tags.get(tagName)); } //{Makernote=105455 131072 127570 300581 171508736 171508736 171508736 171508736 169869312 124780556 1118544 0 0 327779 648 1296, Sub-Sec Time Original=560439, Exposure Time=11167/500000 sec, Date/Time Original=2019:05:13 04:30:26} } catch (ServiceException e) { LOGGER.info("Could not parse EXIF data", e); } long [] maker_note = null; double exposure = Double.NaN; String date_time = null; if (tags.containsKey(MAKER_NOTE)){ String [] smn = tags.get(MAKER_NOTE).split(" "); maker_note = new long[smn.length]; for (int i = 0; i < maker_note.length; i++) { maker_note[i] = Integer.parseInt(smn[i]); } } if (tags.containsKey(EXPOSURE_TIME)){ if (tags.get(EXPOSURE_TIME).contains("/")) { String [] s = tags.get(EXPOSURE_TIME).split("/"); exposure = 1.0 * Integer.parseInt(s[0]) / Integer.parseInt(s[1].split(" ")[0]); } else { exposure = Double.parseDouble(tags.get(EXPOSURE_TIME)); } } if (tags.containsKey(DATE_TIME_ORIGINAL)){ date_time = tags.get(DATE_TIME_ORIGINAL); if (tags.containsKey(SUB_SEC_TIME_ORIGINAL)){ date_time += "."+tags.get(SUB_SEC_TIME_ORIGINAL); } } // int bytes_per_pixel = 1; int bpp = getBitsPerPixel(); int bytes_per_pixel = (bpp + 7) / 9; Hashtable<String, String> property_table = ElphelMeta.getMeta( null, maker_note, exposure, date_time, bytes_per_pixel, true ); LOGGER.info("Created elphelMeta table, size="+property_table.size()); for (String key:property_table.keySet()) { addGlobalMeta(ELPHEL_PROPERTY_PREFIX+key,property_table.get(key)); } MetadataLevel level = getMetadataOptions().getMetadataLevel(); if (level != MetadataLevel.MINIMUM) { // Integer[] tags = ifds.get(0).keySet().toArray(new Integer[0]); // LOGGER.info("initStandardMetadata() - got "+tags.length+" tags"); } addGlobalMeta(ELPHEL_PROPERTY_PREFIX+CONTENT_FILENAME,content_fileName); } /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */ @Override public String[] getSeriesUsedFiles(boolean noPixels) { return super.getSeriesUsedFiles(noPixels); // if (noPixels) { // return companionFile == null ? null : new String[] {companionFile}; // } // if (companionFile != null) return new String[] {companionFile, currentId}; // return new String[] {currentId}; } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { LOGGER.info("close("+fileOnly+") before super"); super.close(fileOnly); // curerent_id == null only during actual close? LOGGER.info("close("+fileOnly+") after super"); // if ((content_fileName != null) && file_initialized){ if (!mapped_externally && file_initialized){ // will try to unmap non-mapped file, OK Location.mapFile(content_fileName, null); file_initialized = false; } // HashMap<String,Object> dbg_loc = Location.getIdMap(); if (!fileOnly) { // companionFile = null; // description = null; // calibrationUnit = null; // physicalSizeZ = null; // timeIncrement = null; // xOrigin = null; // yOrigin = null; } } // -- Internal BaseTiffReader API methods -- /* @see BaseTiffReader#initStandardMetadata() */ /* Removed 08/03/2021 to match IP4 @Override protected void initStandardMetadata() throws FormatException, IOException { LOGGER.info("initStandardMetadata() - before super()"); super.initStandardMetadata(); String comment = ifds.get(0).getComment(); // IMAGE_DESCRIPTION LOGGER.info("initStandardMetadata() - after super()"); long[] maker_note = null; double exposure = Double.NaN; String date_time = null; IFDList exifIFDs = tiffParser.getExifIFDs(); //ifds.get(0) has 37393=297894 (ImageNumbder), but not exifIFD IFD ifd0 = ifds.get(0); if (ifd0.containsKey(TAG_IMAGE_NUMBER)) addGlobalMeta("Image_Number", ifd0.get(TAG_IMAGE_NUMBER)); if (ifd0.containsKey(TAG_SERIAL_NUMBER)) addGlobalMeta("Serial_Number", ifd0.get(TAG_SERIAL_NUMBER)); if (ifd0.containsKey(TAG_IMAGE_DESCRIPTION)) addGlobalMeta("Image_Description", ifd0.get(TAG_IMAGE_DESCRIPTION)); //TAG_SERIAL_NUMBER if (exifIFDs.size() > 0) { IFD exifIFD = exifIFDs.get(0); tiffParser.fillInIFD(exifIFD); if (exifIFD.containsKey(IFD.MAKER_NOTE)) { maker_note = (long[]) exifIFD.get(IFD.MAKER_NOTE); } if (exifIFD.containsKey(IFD.EXPOSURE_TIME)) { Object exp = exifIFD.get(IFD.EXPOSURE_TIME); if (exp instanceof TiffRational) { TiffRational texp = (TiffRational) exp; exposure = 1.0*texp.getNumerator()/texp.getDenominator(); } } if (exifIFD.containsKey(IFD.DATE_TIME_ORIGINAL)) { date_time = exifIFD.get(IFD.DATE_TIME_ORIGINAL).toString(); if (exifIFD.containsKey(IFD.SUB_SEC_TIME_ORIGINAL)) { date_time += "."+exifIFD.get(IFD.SUB_SEC_TIME_ORIGINAL).toString(); } } } int bpp = getBitsPerPixel(); int bytes_per_pixel = (bpp + 7) / 9; Hashtable<String, String> property_table = ElphelMeta.getMeta( null, maker_note, exposure, date_time, bytes_per_pixel, true ); LOGGER.info("Created elphelMeta table, size="+property_table.size()); for (String key:property_table.keySet()) { addGlobalMeta(ELPHEL_PROPERTY_PREFIX+key,property_table.get(key)); } MetadataLevel level = getMetadataOptions().getMetadataLevel(); if (level != MetadataLevel.MINIMUM) { Integer[] tags = ifds.get(0).keySet().toArray(new Integer[0]); LOGGER.info("initStandardMetadata() - got "+tags.length+" tags"); } addGlobalMeta(ELPHEL_PROPERTY_PREFIX+CONTENT_FILENAME,content_fileName); // convert MAKER_NOTE to the same text format as in com.drew.metadata if (maker_note != null) { StringBuffer sb_maker_note = new StringBuffer(); for (int i = 0; i < maker_note.length; i++) { if (i > 0) sb_maker_note.append(" "); sb_maker_note.append(maker_note[i]); } addGlobalMeta("MAKER_NOTE", sb_maker_note.toString()); } // check for ImageJ-style TIFF comment boolean ij = checkCommentImageJ(comment); // if (ij) parseCommentImageJ(comment); } */ /* @see BaseTiffReader#initMetadataStore() */ /* Removed 08/03/2021 to match IP4 @Override protected void initMetadataStore() throws FormatException { super.initMetadataStore(); MetadataStore store = makeFilterMetadata(); // if (description != null) { // store.setImageDescription(description, 0); // } populateMetadataStoreImageJ(store); } */ /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) */ @Override public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { LOGGER.info("openBytes() - before super()"); super.openBytes(no, buf, x, y, w, h); LOGGER.info("openBytes() - after super()"); return buf; } // -- Helper methods -- // Convert to Elphel-specific parameters /** * Checks the original metadata table for ImageJ-specific information * to propagate into the metadata store. */ private void populateMetadataStoreImageJ(MetadataStore store) { // TODO: Perhaps we should only populate the physical Z size if the unit is // a known, physical quantity such as "micron" rather than "pixel". // e.g.: if (calibrationUnit.equals("micron")) /* if (physicalSizeZ != null) { double zDepth = physicalSizeZ.doubleValue(); if (zDepth < 0) zDepth = -zDepth; store.setPixelsPhysicalSizeZ(new PositiveFloat(zDepth), 0); } if (timeIncrement != null) { store.setPixelsTimeIncrement(timeIncrement, 0); } */ } // private boolean checkCommentImageJ(String comment) { return comment != null && comment.startsWith("ImageJ="); } private static String getFileExtension(File file) { String fileName = file.getName(); int lastDot = fileName.lastIndexOf('.'); return fileName.substring(lastDot + 1); } public static Properties getTiffMeta(String path) throws IOException { // System.out.println("getTiffMeta(): "+path); Properties properties = new Properties(); File file = new File(path); String extension = getFileExtension(file); // System.out.println("\nProcessing " + file.getName() + ":\n"); Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(extension); ImageInputStream stream = null; while (readers.hasNext()) { ImageReader reader = readers.next(); stream = ImageIO.createImageInputStream(file); reader.setInput(stream, true); IIOMetadata metadata = reader.getImageMetadata(0); //java.lang.IllegalStateException: Input not set! TIFFDirectory ifd = TIFFDirectory.createFromMetadata(metadata); TIFFField [] tfs = ifd.getTIFFFields(); for (TIFFField tf: tfs) { parseField(tf, properties); } stream.close(); } // Set<String> pnames = properties.stringPropertyNames(); // for (String pn:pnames) { // System.out.println(pn+": "+properties.getProperty(pn)); // } return properties; } private static void parseField(TIFFField tf, Properties properties) { if (tf.hasDirectory()) { TIFFField [] stfs = tf.getDirectory().getTIFFFields(); if ("GPSInfoIFDPointer".equals(tf.getTag().getName())) { TIFFDirectory gifd = tf.getDirectory(); long [][] lat = gifd.getTIFFField(ExifGPSTagSet.TAG_GPS_LATITUDE).getAsRationals(); double dlat = 1.0*lat[0][0]/lat[0][1]+1.0/60*lat[1][0]/lat[1][1]+1.0/3600*lat[2][0]/lat[2][1]; String lat_ref = gifd.getTIFFField(ExifGPSTagSet.TAG_GPS_LATITUDE_REF).getValueAsString(0); if (!lat_ref.equals("N")) { dlat =- dlat; } long [][] lon = gifd.getTIFFField(ExifGPSTagSet.TAG_GPS_LONGITUDE).getAsRationals(); double dlon = 1.0*lon[0][0]/lon[0][1]+1.0/60*lon[1][0]/lon[1][1]+1.0/3600*lon[2][0]/lon[2][1]; String lon_ref = gifd.getTIFFField(ExifGPSTagSet.TAG_GPS_LONGITUDE_REF).getValueAsString(0); if (!lon_ref.equals("E")) { dlon =- dlon; } long [] alt = gifd.getTIFFField(ExifGPSTagSet.TAG_GPS_ALTITUDE).getAsRational(0); double dalt = 1.0 *alt[0]/alt[1]; String alt_ref = gifd.getTIFFField(ExifGPSTagSet.TAG_GPS_ALTITUDE_REF).getValueAsString(0); if (!alt_ref.equals("0")) { dalt =- dalt; } String [] date_stamp = gifd.getTIFFField(ExifGPSTagSet.TAG_GPS_DATE_STAMP).getValueAsString(0).split(":"); long [][] gts = gifd.getTIFFField(ExifGPSTagSet.TAG_GPS_TIME_STAMP).getAsRationals(); double secs = 3600.0*gts[0][0]/gts[0][1]+60.0*gts[1][0]/gts[1][1]+1.0*gts[2][0]/gts[2][1]; int isecs = (int) Math.floor(secs); int nanos = (int) Math.round((secs - isecs)*1E9); LocalDateTime dt = LocalDateTime.of( Integer.parseInt(date_stamp[0]), // year Integer.parseInt(date_stamp[1]), // month Integer.parseInt(date_stamp[2]), // day 0,0,0); dt = dt.plusSeconds(isecs); dt = dt.plusNanos(nanos); properties.setProperty ("GPSLatitude", ""+dlat); properties.setProperty ("GPSLongitude", ""+dlon); properties.setProperty ("GPSAltitude", ""+dalt); properties.setProperty ("GPSDateTime", ""+dt.toString()); } else { for (TIFFField stf: stfs) { parseField(stf, properties); } } } else { // will not parse long tags (>3) if (tf.getCount() == 1) { switch (tf.getType()) { case TIFFTag.TIFF_RATIONAL: { long [] ndn = tf.getAsRational(0); double v = 1.0*ndn[0]/ndn[1]; properties.setProperty(tf.getTag().getName(), ""+v); } break; case TIFFTag.TIFF_SRATIONAL: { int [] ndn = tf.getAsSRational(0); double v = 1.0*ndn[0]/ndn[1]; properties.setProperty(tf.getTag().getName(), ""+v); } break; case TIFFTag.TIFF_ASCII: case TIFFTag.TIFF_BYTE: case TIFFTag.TIFF_DOUBLE: case TIFFTag.TIFF_FLOAT: case TIFFTag.TIFF_LONG: case TIFFTag.TIFF_SHORT: case TIFFTag.TIFF_SBYTE: case TIFFTag.TIFF_SLONG: case TIFFTag.TIFF_SSHORT: properties.setProperty(tf.getTag().getName(), tf.getValueAsString(0)); break; default: System.out.println("not processed: "+tf.getTag().getName()); } } } } public static double [] getLLA(Properties properties) { double [] lla = new double [3]; lla[0] = Double.parseDouble(properties.getProperty("GPSLatitude")); lla[1] = Double.parseDouble(properties.getProperty("GPSLongitude")); lla[2] = Double.parseDouble(properties.getProperty("GPSAltitude")); return lla; } public static LocalDateTime getLocalDateTime(Properties properties) { LocalDateTime dt = LocalDateTime.parse(properties.getProperty("GPSDateTime")); return dt; } public static double [] getPixelSize(Properties properties) { int res_unit = Integer.parseInt(properties.getProperty("ResolutionUnit")); double unit_size = 0; switch(res_unit) { case 2: unit_size = 0.0254; break; case 3: unit_size = 0.01; break; default: System.out.println("Wrong ResolutionUnit="+res_unit); return null; } double [] xy_pixel_in_meters = new double[2]; xy_pixel_in_meters[0] = unit_size/Double.parseDouble(properties.getProperty("XResolution")); xy_pixel_in_meters[1] = unit_size/Double.parseDouble(properties.getProperty("YResolution")); return xy_pixel_in_meters; } public static int getWidth(Properties properties) { return Integer.parseInt(properties.getProperty("ImageWidth")); } public static int getHeight(Properties properties) { return Integer.parseInt(properties.getProperty("ImageLength")); } public static double [] getXYOffsetMeters(Properties properties) { int res_unit = Integer.parseInt(properties.getProperty("ResolutionUnit")); double unit_size = 0; switch(res_unit) { case 2: unit_size = 0.0254; break; case 3: unit_size = 0.01; break; default: System.out.println("Wrong ResolutionUnit="+res_unit); return null; } double [] xy_offset_meters = new double[2]; xy_offset_meters[0] = unit_size * Double.parseDouble(properties.getProperty("XPosition")); xy_offset_meters[1] = unit_size * Double.parseDouble(properties.getProperty("YPosition")); return xy_offset_meters; } public static double [] getXYOffsetPixels(Properties properties) { double [] xy_offset_pixels = new double[2]; xy_offset_pixels[0] = Double.parseDouble(properties.getProperty("XPosition")) * Double.parseDouble(properties.getProperty("XResolution")); xy_offset_pixels[1] = Double.parseDouble(properties.getProperty("YPosition")) * Double.parseDouble(properties.getProperty("YResolution")); return xy_offset_pixels; } }