/** ** -----------------------------------------------------------------------------** ** ElphelJp4Reader.java ** ** loci.format compatible reader for Elphel JP4 files ** ** ** Copyright (C) 2019 Elphel, Inc. ** ** -----------------------------------------------------------------------------** ** ** ElphelJp4Reader.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.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.Date; import java.util.HashMap; import java.util.Hashtable; import org.apache.commons.compress.utils.IOUtils; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.ImageIOReader; import loci.formats.in.MetadataLevel; //import loci.formats.services.EXIFService; import ome.xml.meta.MetadataStore; import ome.xml.model.primitives.Timestamp; //ElphelTiffReader public class ElphelJp4Reader extends ImageIOReader{ // -- Constants -- 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 String ELPHEL_PROPERTY_PREFIX = "ELPHEL_"; public static final String CONTENT_FILENAME = "CONTENT_FILENAME"; 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"}}; /** Logger for this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(ElphelTiffReader.class); // -- Fields -- 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 byte [] image_bytes = null; 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 ElphelJp4Reader() { super("JP4", new String[] {"jp4"}); // 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 suffixNecessary = true; // false suffixSufficient = true; // false; LOGGER.info("ElphelTiffReader(), after super()"); 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) */ /* https://docs.openmicroscopy.org/bio-formats/5.7.2/developers/reader-guide.html * 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 { // same as for tiff? image_bytes = null; // buffered_data = null; 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 initFile() method, read file directly"); super.setId(id); } else { // If URL, then read to memory, if normal file - use direct access url = null; // String mime = null; // use to select jp4/tiff later? Or to check it is correct content_fileName = null; try { url = new URL(id); } catch (MalformedURLException e) { // LOGGER.warn("Bad URL1: " + id); // will try direct file, not an error } 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; } /* @see loci.formats.FormatReader#initFile(String) */ @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 JPEG 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); } } 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; 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#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { // HashMap<String,Object> dbg_loc = Location.getIdMap(); String saveCurrentId = currentId; currentId = null; LOGGER.info("close("+fileOnly+") before super"); super.close(fileOnly); // curerent_id == null only during actual close? LOGGER.info("close("+fileOnly+") after super"); currentId = saveCurrentId; // 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; } // dbg_loc = Location.getIdMap(); if (!fileOnly) { // companionFile = null; // description = null; // calibrationUnit = null; // physicalSizeZ = null; // timeIncrement = null; // xOrigin = null; // yOrigin = null; } } /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) * * openBytes(int, byte[], int, int, int, int) Returns a byte array containing * the pixel data for a specified subimage from the given file. The dimensions * of the subimage (upper left X coordinate, upper left Y coordinate, width, * and height) are specified in the final four int parameters. * This should throw a FormatException if the image number is invalid (less than * 0 or >= the number of images). The ordering of the array returned by openBytes * should correspond to the values returned by isLittleEndian and isInterleaved. * Also, the length of the byte array should be * [image_width * image height * bytes_per_pixel]. Extra bytes will generally * be truncated. It is recommended that the first line of this method be * FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h) - this ensures * that all of the parameters are valid. */ @Override public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { LOGGER.info("openBytes() - before super()"); FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h); if (image_bytes == null) { jp4Decode(no); } int width = getSizeX(); int y1 = y + h; int dest=0; for (int line = y; line < y1; line++) { System.arraycopy(image_bytes, line*width+x, buf, dest, w); dest += w; } LOGGER.info("openBytes() - after super()"); return buf; } public void jp4Decode(int no) throws FormatException, IOException { int width = getSizeX(); int height = getSizeY(); image_bytes = new byte[width*height]; byte [] ib = new byte [width*height]; byte [][] macroblock=new byte[16][16]; super.openBytes(no, ib, 0, 0, width, height); if (REORDER) { int yb,y,xb,x,offset,nb,xbyr,ybyr; //,i; for (yb=0;yb<(height>>4); yb++) for (xb=0;xb<(width>>4); xb++) { /* iterating macroblocks */ for (nb=0; nb<4;nb++) { xbyr=nb & 1; ybyr=(nb>>1) & 1; for (y=0;y<8;y++) { offset=((yb<<4)+y)*width+ (nb<<3) +((xb>=(width>>5))?(((xb<<5)-width)+(width<<3)):(xb<<5)); for (x=0;x<8;x++) { macroblock[(y<<1) | ybyr][(x<<1) | xbyr]=ib[offset+x]; } } } for (y=0;y<16;y++) { System.arraycopy(macroblock[y], 0, image_bytes, ((yb<<4)+y)*width + (xb<<4), 16); } } } else { image_bytes = ib; // temporary } LOGGER.info("jp4Decode()"); } }