From fbdedf24a411f3d7f014ce7125e736c021bfe68a Mon Sep 17 00:00:00 2001 From: k3b <1374583+k3b@users.noreply.github.com> Date: Fri, 21 Aug 2020 05:45:27 +0200 Subject: [PATCH] #169: ExifInterfaceEx Added setFactory() to allow swapping class implementation for create --- .../main/java/de/k3b/media/ExifInterface.java | 153 +++++++++++------- .../java/de/k3b/media/ExifInterfaceEx.java | 129 ++++++++++----- .../media/ExifInterfaceIntegrationTests.java | 4 +- showexif/src/main/java/de/k3b/ShowExif.java | 4 +- 4 files changed, 190 insertions(+), 100 deletions(-) diff --git a/fotolib2/src/main/java/de/k3b/media/ExifInterface.java b/fotolib2/src/main/java/de/k3b/media/ExifInterface.java index f7ee542f..a2354679 100644 --- a/fotolib2/src/main/java/de/k3b/media/ExifInterface.java +++ b/fotolib2/src/main/java/de/k3b/media/ExifInterface.java @@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; +import java.io.Closeable; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; @@ -54,12 +55,21 @@ import de.k3b.io.filefacade.IFile; /** - * This is a class for reading and writing Exif tags in a JPEG file. - * It is based on ExifInterface of android-6 version. - * + * This is a class for reading and writing Exif tags in a JPEG File through {@link java.io.File}, + * {@link java.io.InputStream}, {@link java.io.OutputStream} or {@link IFile}. + *

+ * The code is based on ExifInterface of android-6 version and can be used under j2se and under android. + *

* Improvements: - * * with dependencies to android removed - * * added microsoft exiftags: TAG_WIN_xxxxx + * * all dependencies to android removed so it can be used outside android, too. + * ** android.util.Log is replaced by org.slf4j.Logger + * * java native code nonly, no jini + * * added microsoft exiftags: TAG_WIN_xxxxx + *

+ * Since using java.io.File is heavily restricted in Android it uses de.k3b.io.filefacade.IFile + * insted (both have same methods with same signatures). + * There are two implementatins of IFile: one based on java.io.File for j2se + * and one based om android.support.v4.provider.DocumentFile for android code. */ public class ExifInterface { // public to allow error filtering @@ -449,30 +459,6 @@ public double calculate() { } } - /** - * This function decides which parser to read the image data according to the given input stream - * type and the content of the input stream. In each case, it reads the first three bytes to - * determine whether the image data format is JPEG or not. - */ - private void loadAttributes(InputStream in) { - try { - // Initialize mAttributes. - for (int i = 0; i < EXIF_TAGS.length; ++i) { - mAttributes[i] = new HashMap(); - } - getJpegAttributes(in); - } catch (IOException e) { - // Ignore exceptions in order to keep the compatibility with the old versions of - // ExifInterface. - logWarn("Invalid image.", e); - validJpgExifFormat = false; - } finally { - FileUtils.close(in, "Exifinterface loadAttributes " + in); - if (DEBUG_INTERNAL) { - logDebug(this.toString()); - } - } - } // A class for defining EXIF tag. private static class ExifTag { public final int id; @@ -800,34 +786,45 @@ private ExifTag(String name, int id, int primaryFormat, int secondaryFormat) { // Pattern to check gps timestamp private static final Pattern sGpsTimestampPattern = Pattern.compile("^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$"); + /** - * Reads Exif tags from the specified image file. + * @deprecated use {@link #loadAttributes(InputStream, IFile, String)} instead */ - protected ExifInterface(InputStream in, IFile jpgFile, String filename) throws IOException { - mExifFile = (jpgFile != null) - ? jpgFile - : (filename != null) - ? FileFacade.convert("ExifInterface()", filename) - : null; + @Deprecated + private ExifInterface(InputStream in, IFile jpgFile, String filename) throws IOException { + loadAttributes(in, jpgFile, filename); + } - if (in != null) { - loadAttributes(in); - } else if (mExifFile != null) { - loadAttributes(mExifFile.openInputStream()); - } + public ExifInterface() { } - protected ExifInterface() {} + protected void reset() { + validJpgExifFormat = true; + mExifFile = null; + for (int i = 0; i < EXIF_TAGS.length; ++i) { + mAttributes[i] = null; + } - /** false means this is no valid jpg format */ - public boolean isValidJpgExifFormat() {return validJpgExifFormat;} + mExifByteOrder = ByteOrder.BIG_ENDIAN; + mHasThumbnail = false; + mThumbnailOffset = 0; + mThumbnailLength = 0; + mThumbnailBytes = null; + } + + /** + * false means this is no valid jpg format + */ + public boolean isValidJpgExifFormat() { + return validJpgExifFormat; + } /** - * Returns the EXIF attribute of the specified tagName or {@code null} if there is no such tagName in - * the image file. - * - * @param tagName the name of the tagName. - */ + * Returns the EXIF attribute of the specified tagName or {@code null} if there is no such tagName in + * the image file. + * + * @param tagName the name of the tagName. + */ private ExifAttribute getExifAttribute(String tagName) { if (mAttributes[0] != null) { // Retrieves all tagName groups. The value from primary image tagName group has a higher priority @@ -1112,10 +1109,14 @@ public byte[] getThumbnail(InputStream in) throws IOException { mThumbnailBytes = buffer; return buffer; } finally { - FileUtils.close(in, "ExifInterface getThumbnail" + in); + closeSilently(in, "ExifInterface getThumbnail " + in); } } + private void closeSilently(Closeable in, String debugContext) { + FileUtils.close(in, debugContext); + } + @Override public String toString() { return getDebugString("\n", TAG_DATETIME, TAG_GPS_VERSION_ID); @@ -1303,11 +1304,55 @@ public byte[] getThumbnail(IFile inFile) { return null; } + /** + * Reads Exif tags from the specified image file. + */ + protected ExifInterface loadAttributes(InputStream in, IFile jpgFile, String filename) throws IOException { + reset(); + mExifFile = (jpgFile != null) + ? jpgFile + : (filename != null) + ? FileFacade.convert("ExifInterface()", filename) + : null; + + if (in != null) { + loadAttributes(in); + } else if (mExifFile != null) { + loadAttributes(mExifFile.openInputStream()); + } + return this; + } + + /** + * This function decides which parser to read the image data according to the given input stream + * type and the content of the input stream. In each case, it reads the first three bytes to + * determine whether the image data format is JPEG or not. + */ + private void loadAttributes(InputStream in) { + try { + // Initialize mAttributes. + for (int i = 0; i < EXIF_TAGS.length; ++i) { + mAttributes[i] = new HashMap(); + } + getJpegAttributes(in); + } catch (IOException e) { + // Ignore exceptions in order to keep the compatibility with the old versions of + // ExifInterface. + logWarn("Invalid image.", e); + validJpgExifFormat = false; + } finally { + closeSilently(in, "Exifinterface loadAttributes " + in); + if (DEBUG_INTERNAL) { + logDebug(this.toString()); + } + } + } + // Loads EXIF attributes from a JPEG input stream. private void getJpegAttributes(InputStream inputStream) throws IOException { // See JPEG File Interchange Format Specification page 5. if (DEBUG_INTERNAL) { - logDebug( "getJpegAttributes starting with: " + inputStream); + logDebug("getJpegAttributes starting with: " + inputStream); } DataInputStream dataInputStream = null; try { @@ -1430,7 +1475,7 @@ private void getJpegAttributes(InputStream inputStream) throws IOException { bytesRead += length; } } finally { - FileUtils.close(dataInputStream, "ExifInterface getJpegAttributes " + dataInputStream); + closeSilently(dataInputStream, "ExifInterface getJpegAttributes " + dataInputStream); } } @@ -1674,8 +1719,8 @@ public void saveJpegAttributes(InputStream inputStream, OutputStream outputStrea } } } finally { - FileUtils.close(dataOutputStream, "ExifInterface saveJpegAttributes out " + outputStream); - FileUtils.close(dataInputStream, "ExifInterface saveJpegAttributes in " + dataInputStream); + closeSilently(dataOutputStream, "ExifInterface saveJpegAttributes out " + outputStream); + closeSilently(dataInputStream, "ExifInterface saveJpegAttributes in " + dataInputStream); } } diff --git a/fotolib2/src/main/java/de/k3b/media/ExifInterfaceEx.java b/fotolib2/src/main/java/de/k3b/media/ExifInterfaceEx.java index 6764408a..3ecdae38 100644 --- a/fotolib2/src/main/java/de/k3b/media/ExifInterfaceEx.java +++ b/fotolib2/src/main/java/de/k3b/media/ExifInterfaceEx.java @@ -41,13 +41,13 @@ import de.k3b.io.filefacade.FileFacade; import de.k3b.io.filefacade.IFile; import de.k3b.media.MediaFormatter.FieldID; + /** - * Thin Wrapper around Android-s ExifInterface to read/write exif data from jpg file - * and also implements IPhotoProperties - * + * Thin Wrapper around Android-s ExifInterface to read/write exif data as {@link IPhotoProperties} + * from jpg {@link java.io.File} or {@link IFile} + *

* Created by k3b on 08.10.2016. */ - public class ExifInterfaceEx extends ExifInterface implements IPhotoProperties, IPhotoPropertyFileWriter, IPhotoPropertyFileReader { private static final Logger logger = LoggerFactory.getLogger(LOG_TAG); @@ -61,46 +61,103 @@ public class ExifInterfaceEx extends ExifInterface } - /** when xmp sidecar file was last modified or 0 */ + /** + * when xmp sidecar file was last modified or 0 + */ private long filelastModified = 0; // false for unittests because UserComment = null is not implemented for COM - Marker protected static boolean useUserComment = true; - private final String mDbg_context; - /** if not null content of xmp sidecar file */ + private static Factory factory = new Factory() { + @Override + public ExifInterfaceEx create() { + return new ExifInterfaceEx(); + } + }; + /** + * if not null content of xmp sidecar file + */ private IPhotoProperties xmpExtern = null; + private String mDbg_context = null; - public ExifInterfaceEx(IFile jpgFile, String dbg_context) throws IOException { - this(null, jpgFile, null, null, dbg_context); + /** + * Prefer using one of the {@link #create()} methods instead + */ + public ExifInterfaceEx() { + try { + loadAttributes(null, null, null, null, null); + } catch (IOException e) { + e.printStackTrace(); + } } + + public static void setFactory(Factory factory) { + ExifInterfaceEx.factory = factory; + } + + /** + * All instances of {@link ExifInterfaceEx} should be created through this factory method. + *

+ * Use {@link #setFactory(Factory)} if you want to use a derived class + * of {@link ExifInterfaceEx} globally. + */ + public static ExifInterfaceEx create() { + return factory.create(); + } + + public static ExifInterfaceEx create(IFile jpgFile, InputStream _in, IPhotoProperties xmpExtern, String dbg_context) throws IOException { + InputStream in = (_in != null) ? _in : jpgFile.openInputStream(); + final String absolutePath = (jpgFile != null) ? jpgFile.getAbsolutePath() : null; + return create().loadAttributes(in, jpgFile, absolutePath, xmpExtern, dbg_context); + } + + public static ExifInterfaceEx create(String absoluteJpgPath, InputStream in, IPhotoProperties xmpExtern, String dbg_context) throws IOException { + return create().loadAttributes(in, null, absoluteJpgPath, xmpExtern, dbg_context); + } + + @Override + protected void reset() { + super.reset(); + filelastModified = 0; + mDbg_context = null; + xmpExtern = null; + mLatitude = null; + mLongitude = null; + } + /** * Reads Exif tags from the specified source. - * @param xmpExtern if not null content of xmp sidecar file + * + * @param xmpExtern if not null content of xmp sidecar file * @param dbg_context */ - private ExifInterfaceEx(InputStream in, IFile jpgFile, String absoluteJpgPath, IPhotoProperties xmpExtern, String dbg_context) throws IOException { - super(in, jpgFile, absoluteJpgPath); + private ExifInterfaceEx loadAttributes(InputStream in, IFile jpgFile, String absoluteJpgPath, IPhotoProperties xmpExtern, String dbg_context) throws IOException { + super.loadAttributes(in, jpgFile, absoluteJpgPath); setFilelastModified(mExifFile); this.xmpExtern = xmpExtern; - this.mDbg_context = dbg_context + "->ExifInterfaceEx(" + absoluteJpgPath+ ") "; + this.mDbg_context = dbg_context + "->ExifInterfaceEx(" + absoluteJpgPath + ") "; if (LibGlobal.debugEnabledJpgMetaIo) { logger.debug(this.mDbg_context + " load: " + PhotoPropertiesFormatter.format(this, false, null, FieldID.path, FieldID.clasz)); } // Log.d(LOG_TAG, msg); - - } - - public static ExifInterfaceEx create(IFile jpgFile, InputStream _in, IPhotoProperties xmpExtern, String dbg_context) throws IOException { - InputStream in = (_in != null) ? _in : jpgFile.openInputStream(); - final String absolutePath = (jpgFile != null) ? jpgFile.getAbsolutePath() : null; - return new ExifInterfaceEx(in, jpgFile, absolutePath, xmpExtern, dbg_context); + return this; } - public static ExifInterfaceEx create(String absoluteJpgPath, InputStream in, IPhotoProperties xmpExtern, String dbg_context) throws IOException { - return new ExifInterfaceEx(in, null, absoluteJpgPath, xmpExtern, dbg_context); + @Override + public IPhotoProperties load(IFile jpgFile, IPhotoProperties childProperties, String dbg_context) { + try { + return create(jpgFile, null, childProperties, dbg_context); + } catch (IOException e) { + if (LibGlobal.debugEnabledJpgMetaIo) { + logger.info(StringUtils.appendMessage( + null, dbg_context, getClass().getSimpleName(), "load failed", + jpgFile, e.getMessage()).toString(), e); + } + return null; + } } public static int getOrientationId(IFile fullPath) { @@ -111,8 +168,6 @@ public static int getOrientationId(IFile fullPath) { return 0; } - protected ExifInterfaceEx() {super();xmpExtern=null; mDbg_context = "";} - @Override public void saveAttributes(IFile inFile, IFile outFile, boolean deleteInFileOnFinish) throws IOException { fixDateTakenIfNeccessary(inFile); @@ -403,7 +458,9 @@ public Integer getRating() { return result; } - /** not implemented in {@link ExifInterface} */ + /** + * not implemented in {@link ExifInterface} + */ @Override public IPhotoProperties setRating(Integer value) { setAttribute(TAG_WIN_RATING, (value != null) ? value.toString() : null); @@ -411,25 +468,13 @@ public IPhotoProperties setRating(Integer value) { return this; } - @Override - public IPhotoProperties load(IFile jpgFile, IPhotoProperties childProperties, String dbg_context) { - try { - return new ExifInterfaceEx(null, jpgFile, null, childProperties, dbg_context); - } catch (IOException e) { - if (LibGlobal.debugEnabledJpgMetaIo) { - logger.info(StringUtils.appendMessage( - null, dbg_context, getClass().getSimpleName(), "load failed", - jpgFile, e.getMessage()).toString(), e); - } - return null; - } + public interface Factory { + public ExifInterfaceEx create(); } - protected interface Factory { - ExifInterfaceEx create(String absoluteJpgPath, InputStream in, IPhotoProperties xmpExtern, String dbg_context) throws IOException; - } - - /** return the image orinentation as id (one of the ORIENTATION_ROTATE_XXX constants) */ + /** + * return the image orinentation as id (one of the ORIENTATION_ROTATE_XXX constants) + */ public int getOrientationId() { return getAttributeInt( ExifInterfaceEx.TAG_ORIENTATION, 0); diff --git a/fotolib2/src/test/java/de/k3b/media/ExifInterfaceIntegrationTests.java b/fotolib2/src/test/java/de/k3b/media/ExifInterfaceIntegrationTests.java index b5d7d2e3..9ae96048 100644 --- a/fotolib2/src/test/java/de/k3b/media/ExifInterfaceIntegrationTests.java +++ b/fotolib2/src/test/java/de/k3b/media/ExifInterfaceIntegrationTests.java @@ -166,7 +166,7 @@ private ExifInterface assertUpdateSameAsAfterWrite(String fileNameDest, String f final IFile sutFile = OUTDIR.create(fileNameDest); OutputStream outputStream = sutFile.openOutputStream(); - ExifInterface sutWrite = new ExifInterface(inputStream, null, sutFile.getAbsolutePath()); + ExifInterface sutWrite = new ExifInterface().loadAttributes(inputStream, null, sutFile.getAbsolutePath()); for(String key : testItems.keySet()) { sutWrite.setAttribute(key, testItems.get(key)); } @@ -180,7 +180,7 @@ private ExifInterface assertUpdateSameAsAfterWrite(String fileNameDest, String f FileUtils.close(outputStream, sutFile); - ExifInterface sutRead = new ExifInterface(null, sutFile, "junit"); + ExifInterface sutRead = new ExifInterface().loadAttributes(null, sutFile, "junit"); Assert.assertEquals(sutWriteText, sutRead.toString()); return sutRead; diff --git a/showexif/src/main/java/de/k3b/ShowExif.java b/showexif/src/main/java/de/k3b/ShowExif.java index e3bbf657..c5300a46 100644 --- a/showexif/src/main/java/de/k3b/ShowExif.java +++ b/showexif/src/main/java/de/k3b/ShowExif.java @@ -58,8 +58,8 @@ private static void show(String fileName, boolean debug) { final IFile file = FileFacade.convert(dbg_context, fileName); IPhotoProperties jpg = photoPropertyFileReader.load(file, null, dbg_context); - IPhotoProperties exif = new ExifInterfaceEx(null, null) - .load(file, photoPropertyFileReader.getXmp(), dbg_context); + IPhotoProperties exif = ExifInterfaceEx.create(file, null, photoPropertyFileReader.getXmp(), dbg_context); + // PhotoPropertiesImageReader jpg = new PhotoPropertiesImageReader().load(fileName, xmp, dbg_context); show(jpg, debug); show(exif, debug);