/src/be/pw/jexif/JExifTool.java
https://bitbucket.org/P_W999/j-exiftool · Java · 576 lines · 361 code · 34 blank · 181 comment · 34 complexity · 0658d0bdce2b85189766a4cdfc02f343 MD5 · raw file
- /*******************************************************************************
- * Copyright 2012 P_W999
- *
- * 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.
- ******************************************************************************/
- package be.pw.jexif;
-
- import java.io.File;
- import java.io.FileNotFoundException;
- import java.io.FileWriter;
- import java.io.IOException;
- import java.nio.charset.Charset;
- import java.security.SecureRandom;
- import java.text.MessageFormat;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Map.Entry;
-
- import org.apache.commons.exec.CommandLine;
- import org.apache.commons.exec.DefaultExecuteResultHandler;
- import org.apache.commons.exec.DefaultExecutor;
- import org.apache.commons.exec.ExecuteWatchdog;
- import org.apache.commons.exec.Executor;
- import org.apache.commons.exec.ShutdownHookProcessDestroyer;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-
- import be.pw.jexif.enums.DateTag;
- import be.pw.jexif.enums.Errors;
- import be.pw.jexif.enums.tag.ExifGPS;
- import be.pw.jexif.enums.tag.ExifIFD;
- import be.pw.jexif.enums.tag.IFD0;
- import be.pw.jexif.enums.tag.Tag;
- import be.pw.jexif.exception.ExifError;
- import be.pw.jexif.exception.JExifException;
- import be.pw.jexif.internal.action.IAction;
- import be.pw.jexif.internal.action.impl.ActionFactory;
- import be.pw.jexif.internal.constants.ExecutionConstant;
- import be.pw.jexif.internal.result.ResultHandler;
- import be.pw.jexif.internal.thread.FlushablePumpStreamHandler;
- import be.pw.jexif.internal.thread.JExifOutputStream;
- import be.pw.jexif.internal.thread.event.DebugHandler;
- import be.pw.jexif.internal.thread.event.EventHandler;
- import be.pw.jexif.internal.util.Cal10nUtil;
- import be.pw.jexif.internal.util.ExiftoolPathUtil;
- import be.pw.jexif.internal.util.GPSUtil;
- import be.pw.jexif.internal.util.TagUtil;
-
- import com.google.common.annotations.Beta;
- import com.google.common.base.Preconditions;
- import com.google.common.eventbus.EventBus;
-
- /**
- * The JExifTool class acts as the bridge between ExifTool and your java code.<br />
- * This class is responsible for starting and stopping the ExifTool process. <br />
- * <br />
- * In order to use J-ExifTool you must download ExifTool from <a href="http://www.sno.phy.queensu.ca/~phil/exiftool/">http://www.sno.phy.queensu.ca/~phil/exiftool/</a> <br />
- * The System Property {@link ExecutionConstant#EXIFTOOLPATH} should point to the executable.<br />
- * <br />
- * If you want to read or write Exif tags, you first need to make an instance of this class and then use the {@link #getInfo(File)) method to create a {@link JExifInfo} object. <br />
- * <b>JExifTool is not thread-safe</b>
- * <p>
- * The following System Properties are used by J-ExifTool and may or must be set for proper operation:
- * <ul>
- * <li>{@link ExecutionConstant#EXIFTOOLPATH}: the path to the ExifTool executable</li>
- * <li>{@link ExecutionConstant#EXIFTOOLDEADLOCK}: timeout</li>
- * <li>{@link ExecutionConstant#EXIFTOOLBYPASSVALIDATION}: whether to bypass validations when writing tags (set to true to bypass, use at own risk)</li>
- * </ul>
- *
- * @author phillip
- */
- @Beta
- public class JExifTool {
- /**
- * The argsFile is a file on the disk where the command line arguments are stored for ExifTool in -stay_open mode. The path to this file will be passed as the -@ argument.
- */
- private final File argsFile = new File(System.getProperty("args.path", "args") + (new SecureRandom(Long.toString(System.currentTimeMillis()).getBytes(Charset.forName("UTF-8"))).nextInt()));
- /**
- * The writer for {@link be.pw.jexif.JExifTool#argsFile}.
- */
- private FileWriter argsWriter = null;
- /**
- * The path to the ExifTool executable. If the system property was not defined, the application will back to the current directory and expects the executable to be called "exiftool.exe".
- * <p>
- * See for more info {@link ExiftoolPathUtil#getPath()}
- */
- private final File exifToolPath = ExiftoolPathUtil.getPath();
- /**
- * Timeout in milliseconds before the application decides that the for-looping which is waiting for the ExifTool output has deadlocked.
- * <p>
- * This might happen if all of a sudden ExifTool crashes or is not up and running.
- *
- * @see be.pw.jexif.internal.constants.ExecutionConstant#EXIFTOOLDEADLOCK
- */
- private final int deadLock = Integer.getInteger(ExecutionConstant.EXIFTOOLDEADLOCK, 4000);
- /**
- * The guava EventBuss will be used to handle the output- and error-streams from the ExifTool process.
- */
- private final EventBus bus = new EventBus();
-
- /**
- * The logger for this class.
- */
- private static final Logger LOG = LoggerFactory.getLogger(JExifTool.class);
-
- /**
- * Apache Commons result handler used for the async process handling.
- */
- private DefaultExecuteResultHandler resultHandler = null;
-
- /**
- * The executor responsible for launching the exif tool binary.
- */
- private Executor executor = null;
-
- /**
- * Default constructor. <br />
- * This automatically starts the ExifTool process and registers the standard Tag-set that comes with this library.
- *
- * @throws JExifException if failed to start ExifTool
- */
- @Beta
- public JExifTool() throws JExifException {
- TagUtil.register(IFD0.class);
- TagUtil.register(ExifIFD.class);
- TagUtil.register(ExifGPS.class);
- argsFile.deleteOnExit();
- this.start();
- }
-
- /**
- * Creates a new instance of a JExifInfo object. This object can be used to read and write Exif tags to the specified file.
- *
- * @param file the image file from which tags shall be read from or written to.
- * @return a JExifInfo object.
- * @throws IOException if the file does not exist or if it's folder
- */
- @Beta
- public JExifInfo getInfo(final File file) throws IOException {
- Preconditions.checkNotNull(file);
- if (!file.exists() || file.isDirectory()) {
- throw new FileNotFoundException(Cal10nUtil.get(Errors.IO_FILE_NOT_VALID, file.getName()));
- }
- return new JExifInfo(this, file);
- }
-
- /**
- * Starts the ExifTool process using Apache Commons Exec This method is automatically called when a new instance of JExifTool is created.
- *
- * @throws JExifException if failed to start thread.
- */
- private void start() throws JExifException {
- LOG.info("Starting ExifTool");
- LOG.trace("Using exifToolPath in: " + exifToolPath.getAbsolutePath());
- LOG.trace("Argsfile is stored in: " + argsFile.getAbsolutePath());
- if (!exifToolPath.exists() || !exifToolPath.isFile()) {
- LOG.error("The provided path to ExifTool is not valid: " + exifToolPath.getAbsolutePath());
- LOG.error("To configure the path to your exiftool installation, please set the following system property: " + ExecutionConstant.EXIFTOOLPATH);
- LOG.error("or configure the environment variable: " + ExecutionConstant.EXIFTOOLENV);
- throw new JExifException(Cal10nUtil.get(Errors.EXIFTOOL_INVALID_PATH));
- }
- try {
- argsWriter = new FileWriter(argsFile);
- argsWriter.write("");
- argsWriter.flush();
-
- String claFormat;
- String cla;
- if (System.getProperty("os.name").toLowerCase().contains("windows")) {
- claFormat = ExecutionConstant.WINDOWS_CLA;
- if (System.getProperty(ExecutionConstant.EXIFTOOLCLIENCODING) == null) {
- System.setProperty(ExecutionConstant.EXIFTOOLCLIENCODING, "Cp850");
- }
- } else {
- if (System.getProperty("os.name").toLowerCase().contains("mac")) {
- claFormat = ExecutionConstant.MAC_CLA;
- if (System.getProperty(ExecutionConstant.EXIFTOOLCLIENCODING) == null) {
- System.setProperty(ExecutionConstant.EXIFTOOLCLIENCODING, "UTF-8");
- }
- } else {
- claFormat = ExecutionConstant.LINUX_CLA;
- if (System.getProperty(ExecutionConstant.EXIFTOOLCLIENCODING) == null) {
- System.setProperty(ExecutionConstant.EXIFTOOLCLIENCODING, "UTF-8");
- }
- }
- }
- cla = MessageFormat.format(claFormat, exifToolPath.getCanonicalPath(), argsFile.getCanonicalPath());
- LOG.trace("Starting ExifTool with command line arguments {}", cla);
-
- DebugHandler debug = new DebugHandler();
- bus.register(debug);
-
- CommandLine cl = CommandLine.parse(cla); // TODO: make better use of CommandLine
- resultHandler = new DefaultExecuteResultHandler();
- executor = new DefaultExecutor();
- executor.setWatchdog(new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT));
- executor.setStreamHandler(new FlushablePumpStreamHandler(new JExifOutputStream(bus, false), new JExifOutputStream(bus, true)));
- executor.setProcessDestroyer(new ShutdownHookProcessDestroyer());
- executor.execute(cl, resultHandler);
- LOG.info("ExifTool was started");
- Thread.sleep(250); // wait a bit to ensure process has started
- } catch (IOException e) {
- LOG.debug("Failed to start ExifTool", e);
- throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
- } catch (InterruptedException e) {
- LOG.debug("Failed to start ExifTool", e);
- throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
- }
- }
-
- /**
- * Starts the ExifTool process if needed.
- *
- * @throws JExifException if failed to start ExifTool.
- */
- private void startIfNecessary() throws JExifException {
- if (resultHandler != null) {
- if (resultHandler.hasResult() || executor.getWatchdog().killedProcess()) {
- LOG.info("Restarting ExifTool");
- start();
- }
- } else {
- LOG.trace("ResultHandler was null");
- start();
- }
- }
-
- /**
- * Stops the ExifTool-thread. You <b>ALWAYS</b> have to call this method when you don't need J-ExifTool anymore, otherwise the ExifTool process will continue running in the background.
- *
- * @throws JExifException if it failed to stop (there is a Thread.sleep which may throw an InterruptedException).
- */
- @Beta
- public void stop() throws JExifException {
- try {
- LOG.info("Stopping ExifTool process");
- argsWriter.append(ExecutionConstant.STAY_OPEN).append("\r\n");
- argsWriter.append("false\n");
- argsWriter.flush();
- executor.setWatchdog(new ExecuteWatchdog(1500));
- while (!resultHandler.hasResult()) {
- // wait for ExifTool to stop
- }
- LOG.info("ExifTool stopped");
- } catch (IOException e) {
- LOG.debug("Failed to stop ExifTool", e);
- throw new JExifException(Cal10nUtil.get(Errors.IO_CLOSING), e);
- }
- }
-
- /**
- * This method will read out the specified tag from the given file. Default to using the non-exact format.
- *
- * @param file the file from which to read.
- * @param tag the tag to read out.
- * @return the tag value as String or null if nothing found.
- * @throws JExifException when writing the argsfile went wrong or when the Tread.sleep caused problems.
- * @throws ExifError if there was a problem in ExifTool
- */
- String readTagInfo(final File file, final Tag tag) throws JExifException, ExifError {
- return readTagInfo(file, tag, false);
- }
-
- /**
- * This method will read out the specified tag from the given file. Default to using the non-exact format.
- *
- * @param file the file from which to read.
- * @param tag the tag to read out.
- * @param exact whether the exact value should be returned (true) or the human readable format (false)
- * @return the tag value as String or null if nothing found.
- * @throws JExifException when writing the argsfile went wrong or when the Tread.sleep caused problems (or the execution took to long).
- * @throws ExifError if there was a problem in ExifTool
- */
- String readTagInfo(final File file, final Tag tag, final boolean exact) throws JExifException, ExifError {
- LOG.trace("Starting readTagInfo");
- IAction action;
- EventHandler handler = new EventHandler();
- try {
- if (exact) {
- action = ActionFactory.createExactReadAction(file, tag);
- } else {
- action = ActionFactory.createReadAction(file, tag);
- }
- executeAction(handler, action);
- } catch (IOException e) {
- throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
- } catch (InterruptedException e) {
- throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
- }
- List<String> results = handler.getResultList();
- List<String> errors = handler.getErrorList();
- ResultHandler.run(action, results, errors);
- return action.getResult().get(tag);
- }
-
- /**
- * This method will write a value to a given Tag.
- *
- * @param file the file to which the value should be written.
- * @param tag the tag that should be written.
- * @param value the value to write (if null or empty string, the Tag will be cleared)
- * @throws JExifException when writing the argsfile went wrong or when the Tread.sleep caused problems (or the execution took to long).
- * @throws ExifError if there was a problem in ExifTool
- */
- void writeTagInfo(final File file, final Tag tag, final String value) throws ExifError, JExifException {
- LOG.trace("Starting readTagInfo");
- IAction action;
- EventHandler handler = new EventHandler();
- try {
- Map<Tag, String> valuesToWrite = new HashMap<>(1);
- valuesToWrite.put(tag, value);
- action = ActionFactory.createTagWriteAction(file, valuesToWrite);
- executeAction(handler, action);
- } catch (IOException e) {
- throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
- } catch (InterruptedException e) {
- throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
- }
- List<String> results = handler.getResultList();
- List<String> errors = handler.getErrorList();
- ResultHandler.run(action, results, errors);
- }
-
- /**
- * This method will write a value to a given Tag.
- *
- * @param file the file to which the value should be written.
- * @param valuesToWrite the value to write
- * @throws JExifException when writing the argsfile went wrong or when the Tread.sleep caused problems (or the execution took to long).
- * @throws ExifError if there was a problem in ExifTool
- */
- void writeGPSTagInfo(final File file, final Map<Tag, String> valuesToWrite) throws ExifError, JExifException {
- LOG.trace("Starting readTagInfo");
- IAction action;
- EventHandler handler = new EventHandler();
- try {
- GPSUtil.validateGPSValues(valuesToWrite);
- GPSUtil.formatGPSValues(valuesToWrite);
- action = ActionFactory.createTagWriteAction(file, valuesToWrite);
- handler = new EventHandler();
- executeAction(handler, action);
- } catch (IOException e) {
- throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
- } catch (InterruptedException e) {
- throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
- }
- List<String> results = handler.getResultList();
- List<String> errors = handler.getErrorList();
- ResultHandler.run(action, results, errors);
- }
-
- /**
- * Returns all the tags, even the ones not known in J-ExifTool.
- *
- * @param file the file from which to read.
- * @param exact whether the exact value should be returned or not.
- * @return the map with tag-name - tag-value.
- * @throws JExifException when writing the argsfile went wrong or when the Tread.sleep caused problems (or the execution took to long).
- * @throws ExifError if there was a problem in ExifTool
- */
- @SuppressWarnings("unchecked")
- Map<String, String> getAllTagInfo(final File file, final boolean exact) throws ExifError, JExifException {
- return (Map<String, String>) getAllTagInfo(file, exact, false);
- }
-
- /**
- * Returns all the tags known to J-ExifTool.
- *
- * @param file the file from which to read.
- * @param exact whether the exact value should be returned or not.
- * @return the map with tag-name - tag-value.
- * @throws JExifException when writing the argsfile went wrong or when the Tread.sleep caused problems (or the execution took to long).
- * @throws ExifError if there was a problem in ExifTool
- */
- @SuppressWarnings("unchecked")
- Map<Tag, String> getAllSupportedTagInfo(final File file, final boolean exact) throws ExifError, JExifException {
- return (Map<Tag, String>) getAllTagInfo(file, exact, true);
- }
-
- void timeShift(final File file, final String shift, final DateTag tag) throws JExifException, ExifError {
- LOG.trace("Starting timeShift");
- IAction action;
- EventHandler handler = new EventHandler();
- try {
- action = ActionFactory.createDateTimeShiftAction(file, shift, tag);
- executeAction(handler, action);
- } catch (IOException e) {
- throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
- } catch (InterruptedException e) {
- throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
- }
- List<String> results = handler.getResultList();
- List<String> errors = handler.getErrorList();
- ResultHandler.run(action, results, errors);
- }
-
- void copyFrom(final File to, final File from, final Tag... tags) throws JExifException, ExifError {
- LOG.trace("Starting copyFrom");
- IAction action;
- EventHandler handler = new EventHandler();
- try {
- action = ActionFactory.createCopyFromAction(from, to, tags);
- executeAction(handler, action);
- } catch (IOException e) {
- throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
- } catch (InterruptedException e) {
- throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
- }
- List<String> results = handler.getResultList();
- List<String> errors = handler.getErrorList();
- ResultHandler.run(action, results, errors);
- }
-
- /**
- * Extracts a thumnail image to a file.
- *
- * @param file the file
- * @param format the file name format for the thumnbail image (see {@link be.pw.jexif.internal.action.IThumbnailAction}
- * @throws JExifException
- * @throws ExifError
- */
- void extractThumbnail(final File file, final String format) throws JExifException, ExifError {
- LOG.trace("Starting extractThumbnail");
- IAction action;
- EventHandler handler = new EventHandler();
- try {
- action = ActionFactory.createThumnailAction(file, format);
- executeAction(handler, action);
- } catch (IOException e) {
- throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
- } catch (InterruptedException e) {
- throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
- }
- List<String> results = handler.getResultList();
- List<String> errors = handler.getErrorList();
- ResultHandler.run(action, results, errors);
- }
-
- /**
- * Delets all known exiftags from a jpeg file.
- *
- * @param file the file to clear
- * @throws JExifException if something goest wrong internally
- * @throws ExifError if something goes wrong in exiftool
- */
- void deleteAllExifTags(final File file) throws JExifException, ExifError {
- LOG.trace("Starting delete all exif tags");
- IAction action;
- EventHandler handler = new EventHandler();
- try {
- action = ActionFactory.createDeleteAllAction(file);
- executeAction(handler, action);
- } catch (IOException e) {
- throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
- } catch (InterruptedException e) {
- throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
- }
- List<String> results = handler.getResultList();
- List<String> errors = handler.getErrorList();
- ResultHandler.run(action, results, errors);
- }
-
- /**
- * Returns tags from a file.
- *
- * @param file the file from which to read the tags
- * @param exact whether the exact (true) or human readable format (false) should be used
- * @param onlySupported if only supported tags should be extracted (returns Tag objects as key)
- * @return a map with as key a String (onlySupported=false) or a Tag (onlySupported=true)
- * @throws ExifError if something goes wrong in exiftool
- * @throws JExifException if something goes wrong internally
- */
- private Map<? extends Object, String> getAllTagInfo(final File file, final boolean exact, final boolean onlySupported) throws ExifError, JExifException {
- LOG.trace("Starting readTagInfo");
- IAction action;
- EventHandler handler = new EventHandler();
- try {
- if (exact) {
- action = ActionFactory.createTagReadExactAllAction(file);
- } else {
- action = ActionFactory.createTagReadAllAction(file);
- }
- executeAction(handler, action);
- } catch (IOException e) {
- throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
- } catch (InterruptedException e) {
- throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
- }
- List<String> results = handler.getResultList();
- List<String> errors = handler.getErrorList();
- ResultHandler.run(action, results, errors);
- if (onlySupported) {
- return action.getResult();
- } else {
- Map<String, String> tags = action.getUnsupportedTags();
- for (Entry<Tag, String> e : action.getResult().entrySet()) {
- tags.put(e.getKey().getName(), e.getValue());
- }
- return tags;
- }
- }
-
- /**
- * This method will write the exiftool execution arguments to the argument file and wait until the handler has finished aquiring the results.
- *
- * @param handler the handler which will be registered to the bus in order to process the exiftool output
- * @param action the action to execute
- * @throws InterruptedException in case the thread can not be suspended while waiting for output
- * @throws JExifException if something goes wrong internally
- * @throws IOException if there was a problem writing the arguments to the args file
- */
- private void executeAction(final EventHandler handler, final IAction action) throws InterruptedException, JExifException, IOException {
- try {
- startIfNecessary();
- int i = 0;
- bus.register(handler);
-
- try {
- File exiftoolTemp = new File(action.getFile().getCanonicalPath() + "_exiftool_tmp");
- if (exiftoolTemp.exists()) {
- LOG.debug("Exiftool temporary file found. Deleting it");
- exiftoolTemp.delete();
- }
- } catch (Exception e) {
- LOG.warn("An error occured trying to delete exiftool temp file", e);
- LOG.warn("J-Exiftool will continue it's attempt to execute the action");
- }
-
- String[] arguments = action.buildArguments();
- argsWriter.append(ExecutionConstant.ECHO).append("\r\n");
- argsWriter.append(ExecutionConstant.START).append(" ").append(action.getId()).append("\r\n");
-
- for (String argument : arguments) {
- argsWriter.append(argument).append("\r\n");
- }
- argsWriter.append(ExecutionConstant.EXECUTE).append("\r\n");
-
- argsWriter.append(ExecutionConstant.ECHO).append("\r\n");
- argsWriter.append(ExecutionConstant.STOP).append(" ").append(action.getId()).append("\r\n");
- argsWriter.append(ExecutionConstant.EXECUTE).append("\r\n");
-
- argsWriter.append(ExecutionConstant.ECHO).append("\r\n");
- argsWriter.append(ExecutionConstant.FLUSH).append("\r\n");
- argsWriter.append(ExecutionConstant.EXECUTE).append("\r\n");
- argsWriter.flush();
- while (!handler.isFinished() && i <= deadLock) {
- Thread.sleep(50);
- i += 50;
- }
- if (!handler.isFinished()) {
- if (executor instanceof FlushablePumpStreamHandler) {
- ((FlushablePumpStreamHandler) executor.getStreamHandler()).flush();
- }
- Thread.sleep(100);
- }
- if (!handler.isFinished()) {
- LOG.error(Cal10nUtil.get(Errors.DEADLOCK, i));
- throw new JExifException(Cal10nUtil.get(Errors.DEADLOCK, i));
- }
- } finally {
- bus.unregister(handler);
- }
- }
-
- }