diff --git a/src/actions.cpp b/src/actions.cpp index 758dc72c..e2cd7af1 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -59,6 +59,7 @@ EXIV2_RCSID("@(#) $Id$"); #include #include #include +#include #include // for stat() #include // for stat() #ifdef HAVE_UNISTD_H @@ -131,6 +132,7 @@ namespace Action { registerTask(erase, Task::AutoPtr(new Erase)); registerTask(extract, Task::AutoPtr(new Extract)); registerTask(insert, Task::AutoPtr(new Insert)); + registerTask(modify, Task::AutoPtr(new Modify)); } // TaskFactory c'tor Task::AutoPtr TaskFactory::create(TaskType type) @@ -917,6 +919,129 @@ namespace Action { return new Insert(*this); } + int Modify::run(const std::string& path) + try { + if (!Util::fileExists(path, true)) { + std::cerr << path + << ": Failed to open the file\n"; + return -1; + } + + // Read both exif and iptc metadata (ignore return code) + exifData_.read(path); + iptcData_.read(path); + + // loop through command table and apply each command + ModifyCmds& modifyCmds = Params::instance().modifyCmds_; + ModifyCmds::const_iterator i = modifyCmds.begin(); + ModifyCmds::const_iterator end = modifyCmds.end(); + for (; i != end; ++i) { + switch (i->cmdId_) { + case add: + addMetadatum(*i); + break; + case set: + setMetadatum(*i); + break; + case del: + delMetadatum(*i); + break; + default: + // Todo: complain + break; + } + } + + // Save both exif and iptc metadata + int rc = exifData_.write(path); + if (rc) { + std::cerr << Exiv2::ExifData::strError(rc, path) << "\n"; + } + rc = iptcData_.write(path); + if (rc) { + std::cerr << Exiv2::IptcData::strError(rc, path) << "\n"; + } + return rc; + } + catch(const Exiv2::Error& e) + { + std::cerr << "Exif exception in modify action for file " << path + << ":\n" << e << "\n"; + return 1; + } // Modify::run + + void Modify::addMetadatum(const ModifyCmd& modifyCmd) + { + if (Params::instance().verbose_) { + std::cout << "Add " << modifyCmd.key_ << " \"" + << modifyCmd.value_ << "\" (" + << Exiv2::TypeInfo::typeName(modifyCmd.typeId_) + << ")" << std::endl; + } + Exiv2::Value::AutoPtr value = Exiv2::Value::create(modifyCmd.typeId_); + value->read(modifyCmd.value_); + if (modifyCmd.metadataId_ == exif) { + exifData_.add(Exiv2::ExifKey(modifyCmd.key_), value.get()); + } + if (modifyCmd.metadataId_ == iptc) { + iptcData_.add(Exiv2::IptcKey(modifyCmd.key_), value.get()); + } + } + + void Modify::setMetadatum(const ModifyCmd& modifyCmd) + { + if (Params::instance().verbose_) { + std::cout << "Set " << modifyCmd.key_ << " \"" + << modifyCmd.value_ << "\" (" + << Exiv2::TypeInfo::typeName(modifyCmd.typeId_) + << ")" << std::endl; + } + Exiv2::Metadatum* metadatum = 0; + if (modifyCmd.metadataId_ == exif) { + metadatum = &exifData_[modifyCmd.key_]; + } + if (modifyCmd.metadataId_ == iptc) { + metadatum = &iptcData_[modifyCmd.key_]; + } + assert(metadatum); + Exiv2::Value::AutoPtr value = metadatum->getValue(); + // If a type was explicitly requested, use it; else + // use the current type of the metadatum, if any; + // or the default type + if (modifyCmd.explicitType_ || value.get() == 0) { + value = Exiv2::Value::create(modifyCmd.typeId_); + } + value->read(modifyCmd.value_); + metadatum->setValue(value.get()); + } + + void Modify::delMetadatum(const ModifyCmd& modifyCmd) + { + if (Params::instance().verbose_) { + std::cout << "Del " << modifyCmd.key_ << std::endl; + } + if (modifyCmd.metadataId_ == exif) { + Exiv2::ExifData::iterator pos = + exifData_.findKey(Exiv2::ExifKey(modifyCmd.key_)); + if (pos != exifData_.end()) exifData_.erase(pos); + } + if (modifyCmd.metadataId_ == iptc) { + Exiv2::IptcData::iterator pos = + iptcData_.findKey(Exiv2::IptcKey(modifyCmd.key_)); + if (pos != iptcData_.end()) iptcData_.erase(pos); + } + } + + Modify::AutoPtr Modify::clone() const + { + return AutoPtr(clone_()); + } + + Modify* Modify::clone_() const + { + return new Modify(*this); + } + int Adjust::run(const std::string& path) try { adjustment_ = Params::instance().adjustment_; diff --git a/src/actions.hpp b/src/actions.hpp index 5a83faff..ad4e279e 100644 --- a/src/actions.hpp +++ b/src/actions.hpp @@ -37,6 +37,10 @@ #include #include +#include "exiv2.hpp" +#include "exif.hpp" +#include "iptc.hpp" + // ***************************************************************************** // class declarations @@ -53,7 +57,7 @@ namespace Exiv2 { namespace Action { //! Enumerates all tasks - enum TaskType { none, adjust, print, rename, erase, extract, insert }; + enum TaskType { none, adjust, print, rename, erase, extract, insert, modify }; // ***************************************************************************** // class definitions @@ -289,6 +293,31 @@ namespace Action { }; // class Insert + /*! + @brief %Modify the Exif data according to the commands in the + modification table. + */ + class Modify : public Task { + public: + virtual ~Modify() {} + virtual int run(const std::string& path); + typedef std::auto_ptr AutoPtr; + AutoPtr clone() const; + + private: + virtual Modify* clone_() const; + + //! Add a metadatum according to \em modifyCmd + void addMetadatum(const ModifyCmd& modifyCmd); + //! Set a metadatum according to \em modifyCmd + void setMetadatum(const ModifyCmd& modifyCmd); + //! Delete a metadatum according to \em modifyCmd + void delMetadatum(const ModifyCmd& modifyCmd); + + Exiv2::ExifData exifData_; //!< Exif metadata + Exiv2::IptcData iptcData_; //!< Iptc metadata + }; // class Modify + } // namespace Action #endif // #ifndef ACTIONS_HPP_ diff --git a/src/cmd.txt b/src/cmd.txt new file mode 100644 index 00000000..c964d5b0 --- /dev/null +++ b/src/cmd.txt @@ -0,0 +1,30 @@ +# Sample Exiv2 command file +# Run exiv2 -m cmd.txt file ... +# to apply the commands to each file. +# +# Command file format +# Empty lines and lines starting with # are ignored +# Each remaining line is a command. The format for command lines is +# [[] ] +# cmd = set|add|del +# set will set the value of an existing tag of the given key or add a tag +# add will add a tag (unless the key is a non-repeatable Iptc key) +# del will delete a tag +# key = Exiv2 Exif or Iptc key +# type = Byte|Ascii|Short|Long|Rational|Undefined|SShort|SLong|SRational for Exif +# String|Date|Time|Short|Undefined for Iptc +# A default type is used if none is explicitely given. The default for Exif +# keys is Ascii, that for Iptc keys is determined based on the key itself. +# value +# The remaining text on the line is the value. It can optionally be enclosed in +# double quotes ("value") + +add Iptc.Application2.Credit String "mee too! (1)" +add Iptc.Application2.Credit mee too! (2) +del Iptc.Application2.Headline + +add Exif.Image.WhitePoint Short 32 12 4 5 6 + + set Exif.Image.DateTime Ascii "Zwanzig nach fuenf" + set Exif.Image.Artist Ascii nobody + set Exif.Image.Artist "Vincent van Gogh" diff --git a/src/exiv2.cpp b/src/exiv2.cpp index 9313c537..c140db50 100644 --- a/src/exiv2.cpp +++ b/src/exiv2.cpp @@ -46,6 +46,7 @@ EXIV2_RCSID("@(#) $Id$"); #include #include +#include #include #include #include @@ -54,6 +55,17 @@ EXIV2_RCSID("@(#) $Id$"); // local declarations namespace { + //! List of all command itentifiers and corresponding strings + static const CmdIdAndString cmdIdAndString[] = { + add, "add", + set, "set", + del, "del", + invalidCmdId, "invalidCmd" // End of list marker + }; + + // Return a command Id for a command string + CmdId commandId(const std::string& cmdString); + // Evaluate [-]HH[:MM[:SS]], returns true and sets time to the value // in seconds if successful, else returns false. bool parseTime(const std::string& ts, long& time); @@ -66,6 +78,25 @@ namespace { */ int parseCommonTargets(const std::string& optarg, const std::string& action); + + /*! + @brief Parse metadata modification commands from a file + @param modifyCmds Reference to a structure to store the parsed commands + @param filename Name of the command file + */ + bool parseCommands(ModifyCmds& modifyCmds, + const std::string& filename); + + /*! + @brief Parse one line of the command file + @param modifyCmd Reference to a command structure to store the parsed + command + @param line Input line + @param num Line number (used for error output) + */ + bool parseLine(ModifyCmd& modifyCmd, + const std::string& line, int num); + } // ***************************************************************************** @@ -123,7 +154,7 @@ Params& Params::instance() void Params::version(std::ostream& os) const { os << PACKAGE_STRING << ", " - << "Copyright (C) 2004 Andreas Huggel.\n\n" + << "Copyright (C) 2004, 2005 Andreas Huggel.\n\n" << "This is free software; see the source for copying conditions. " << "There is NO \nwarranty; not even for MERCHANTABILITY or FITNESS FOR " << "A PARTICULAR PURPOSE.\n"; @@ -148,6 +179,8 @@ void Params::help(std::ostream& os) const << " ex | extract Extract metadata to *.exv and thumbnail image files.\n" << " mv | rename Rename files according to the Exif create timestamp.\n" << " The filename format can be set with -r format.\n" + << " mo | modify Apply commands to modify (add, set, delete) the Exif\n" + << " and Iptc metadata of image files. Requires option -m\n" << "\nOptions:\n" << " -h Display this help and exit.\n" << " -V Show the program version and exit.\n" @@ -175,7 +208,9 @@ void Params::help(std::ostream& os) const << " are the same as those for the -d option.\n" << " -r fmt Filename format for the `rename' action. The format string\n" << " follows strftime(3). Default filename format is " - << format_ << ".\n\n"; + << format_ << ".\n" + << " -m file Command file for the modify action. The format for the commands\n" + << " set|add|del [[] ].\n\n"; } // Params::help int Params::option(int opt, const std::string& optarg, int optopt) @@ -323,6 +358,23 @@ int Params::option(int opt, const std::string& optarg, int optopt) break; } break; + case 'm': + switch (action_) { + case Action::none: + action_ = Action::modify; + cmdFile_ = optarg; // parse the file later + break; + case Action::modify: + std::cerr << progname() + << ": Ignoring surplus option -m " << optarg << "\n"; + break; + default: + std::cerr << progname() + << ": Option -m is not compatible with a previous option\n"; + rc = 1; + break; + } + break; case ':': std::cerr << progname() << ": Option -" << static_cast(optopt) << " requires an argument\n"; @@ -404,6 +456,15 @@ int Params::nonoption(const std::string& argv) action = true; action_ = Action::rename; } + if (argv == "mo" || argv == "modify") { + if (action_ != Action::none && action_ != Action::modify) { + std::cerr << progname() << ": Action modify is not " + << "compatible with the given options\n"; + rc = 1; + } + action = true; + action_ = Action::modify; + } if (action_ == Action::none) { // if everything else fails, assume print as the default action action_ = Action::print; @@ -430,10 +491,22 @@ int Params::getopt(int argc, char* const argv[]) << ": Adjust action requires option -a time\n"; rc = 1; } + if (action_ == Action::modify && cmdFile_.empty()) { + std::cerr << progname() + << ": Modify action requires option -m file\n"; + rc = 1; + } if (0 == files_.size()) { std::cerr << progname() << ": At least one file is required\n"; rc = 1; } + if (rc == 0 && action_ == Action::modify) { + if (!parseCommands(modifyCmds_, cmdFile_)) { + std::cerr << progname() << ": Error parsing -m option argument `" + << cmdFile_ << "'\n"; + rc = 1; + } + } return rc; } // Params::getopt @@ -505,4 +578,147 @@ namespace { return rc ? rc : target; } // parseCommonTargets + bool parseCommands(ModifyCmds& modifyCmds, + const std::string& filename) + { + try { + std::ifstream file(filename.c_str()); + if (!file) { + std::cerr << filename + << ": Failed to open command file for reading\n"; + return false; + } + int num = 0; + std::string line; + while (std::getline(file, line)) { + ModifyCmd modifyCmd; + if (parseLine(modifyCmd, line, ++num)) { + modifyCmds.push_back(modifyCmd); + } + } + return true; + } + catch (const Exiv2::Error& error) { + std::cerr << filename << ", " << error << "\n"; + return false; + } + } // parseCommands + + bool parseLine(ModifyCmd& modifyCmd, const std::string& line, int num) + { + const std::string delim = " \t"; + + // Skip empty lines and comments + std::string::size_type cmdStart = line.find_first_not_of(delim); + if (cmdStart == std::string::npos || line[cmdStart] == '#') return false; + + // Get command and key + std::string::size_type cmdEnd = line.find_first_of(delim, cmdStart+1); + std::string::size_type keyStart = line.find_first_not_of(delim, cmdEnd+1); + std::string::size_type keyEnd = line.find_first_of(delim, keyStart+1); + if ( cmdStart == std::string::npos + || cmdEnd == std::string::npos + || keyStart == std::string::npos) { + throw Exiv2::Error("line " + Exiv2::toString(num) + + ": Invalid command line"); + } + + std::string cmd(line.substr(cmdStart, cmdEnd-cmdStart)); + CmdId cmdId = commandId(cmd); + if (cmdId == invalidCmdId) { + throw Exiv2::Error("line " + Exiv2::toString(num) + + ": Invalid command `" + cmd + "'"); + } + + Exiv2::TypeId defaultType = Exiv2::invalidTypeId; + std::string key(line.substr(keyStart, keyEnd-keyStart)); + MetadataId metadataId = invalidMetadataId; + try { + Exiv2::IptcKey iptcKey(key); + metadataId = iptc; + defaultType = Exiv2::IptcDataSets::dataSetType(iptcKey.tag(), + iptcKey.record()); + } + catch (const Exiv2::Error&) {} + if (metadataId == invalidMetadataId) { + try { + Exiv2::ExifKey exifKey(key); + metadataId = exif; + defaultType = Exiv2::asciiString; + } + catch (const Exiv2::Error&) {} + } + if (metadataId == invalidMetadataId) { + throw Exiv2::Error("line " + Exiv2::toString(num) + + ": Invalid key `" + key + "'"); + } + + std::string value; + Exiv2::TypeId type = Exiv2::invalidTypeId; + bool explicitType = true; + if (cmdId != del) { + // Get type and value + std::string::size_type typeStart + = line.find_first_not_of(delim, keyEnd+1); + std::string::size_type typeEnd + = line.find_first_of(delim, typeStart+1); + std::string::size_type valStart = typeStart; + std::string::size_type valEnd = line.find_last_not_of(delim); + + if ( keyEnd == std::string::npos + || typeStart == std::string::npos + || typeEnd == std::string::npos + || valStart == std::string::npos) { + throw Exiv2::Error("line " + Exiv2::toString(num) + + ": Invalid command line"); + } + + std::string typeStr(line.substr(typeStart, typeEnd-typeStart)); + type = Exiv2::TypeInfo::typeId(typeStr); + if (type != Exiv2::invalidTypeId) { + valStart = line.find_first_not_of(delim, typeEnd+1); + if (valStart == std::string::npos) { + throw Exiv2::Error("line " + Exiv2::toString(num) + + ": Invalid command line"); + } + } + else { + type = defaultType; + explicitType = false; + } + if (type == Exiv2::invalidTypeId) { + throw Exiv2::Error("line " + Exiv2::toString(num) + + ": Invalid type"); + } + + value = line.substr(valStart, valEnd+1-valStart); + std::string::size_type last = value.length()-1; + if ( (value[0] == '"' || value[last] == '"') + && value[0] != value[last]) { + throw Exiv2::Error("line " + Exiv2::toString(num) + + ": Unbalanced quotes"); + } + if (value[0] == '"') { + value = value.substr(1, value.length()-2); + } + } + + modifyCmd.cmdId_ = cmdId; + modifyCmd.key_ = key; + modifyCmd.metadataId_ = metadataId; + modifyCmd.typeId_ = type; + modifyCmd.explicitType_ = explicitType; + modifyCmd.value_ = value; + + return true; + } // parseLine + + CmdId commandId(const std::string& cmdString) + { + int i = 0; + for (; cmdIdAndString[i].cmdId_ != invalidCmdId + && cmdIdAndString[i].cmdString_ != cmdString; ++i) {} + return cmdIdAndString[i].cmdId_; + } + } diff --git a/src/exiv2.hpp b/src/exiv2.hpp index 539c27bd..7c323ec1 100644 --- a/src/exiv2.hpp +++ b/src/exiv2.hpp @@ -32,6 +32,7 @@ // ***************************************************************************** // included header files #include "utils.hpp" +#include "types.hpp" // + standard includes #include @@ -40,6 +41,33 @@ // ***************************************************************************** // class definitions + +//! Command identifiers +enum CmdId { invalidCmdId, add, set, del }; +//! Metadata identifiers +enum MetadataId { invalidMetadataId, iptc, exif }; +//! Structure for one parsed modification command +struct ModifyCmd { + //! C'tor + ModifyCmd() : + cmdId_(invalidCmdId), metadataId_(invalidMetadataId), + typeId_(Exiv2::invalidTypeId), explicitType_(false) {} + CmdId cmdId_; //!< Command identifier + std::string key_; //!< Exiv2 key string + MetadataId metadataId_; //!< Metadata identifier + Exiv2::TypeId typeId_; //!< Exiv2 type identifier + //! Flag to indicate if the type was explicitely specified (true) + bool explicitType_; + std::string value_; //!< Data +}; +//! Container for modification commands +typedef std::vector ModifyCmds; +//! Structure to link command identifiers to strings +struct CmdIdAndString { + CmdId cmdId_; //!< Commands identifier + std::string cmdString_; //!< Command string +}; + /*! @brief Implements the command line handling for the program. @@ -103,6 +131,8 @@ public: long adjustment_; //!< Adjustment in seconds. std::string format_; //!< Filename format (-r option arg). + std::string cmdFile_; //!< Name of the modification command file + ModifyCmds modifyCmds_; //!< Parsed modification commands //! Container to store filenames. typedef std::vector Files; @@ -114,7 +144,7 @@ private: @brief Default constructor. Note that optstring_ is initialized here. The c'tor is private to force instantiation through instance(). */ - Params() : optstring_(":hVvfa:r:p:d:e:i:"), + Params() : optstring_(":hVvfa:r:p:d:e:i:m:"), help_(false), version_(false), verbose_(false), diff --git a/src/types.cpp b/src/types.cpp index 253158a1..503c40fe 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -58,14 +58,16 @@ namespace Exiv2 { TypeInfoTable(unsignedShort, "Short", 2), TypeInfoTable(unsignedLong, "Long", 4), TypeInfoTable(unsignedRational, "Rational", 8), - TypeInfoTable(invalid6, "Invalid (6)", 1), + TypeInfoTable(invalid6, "Invalid(6)", 1), TypeInfoTable(undefined, "Undefined", 1), TypeInfoTable(signedShort, "SShort", 2), TypeInfoTable(signedLong, "SLong", 4), TypeInfoTable(signedRational, "SRational", 8), TypeInfoTable(string, "String", 1), TypeInfoTable(date, "Date", 8), - TypeInfoTable(time, "Time", 11) + TypeInfoTable(time, "Time", 11), + // End of list marker + TypeInfoTable(lastTypeId, "(Unknown)", 0) }; const char* TypeInfo::typeName(TypeId typeId) @@ -73,6 +75,15 @@ namespace Exiv2 { return typeInfoTable_[ typeId < lastTypeId ? typeId : 0 ].name_; } + TypeId TypeInfo::typeId(const std::string& typeName) + { + int i = 0; + for (; typeInfoTable_[i].typeId_ != lastTypeId + && typeInfoTable_[i].name_ != typeName; ++i) {} + return typeInfoTable_[i].typeId_ == lastTypeId ? + invalidTypeId : typeInfoTable_[i].typeId_; + } + long TypeInfo::typeSize(TypeId typeId) { return typeInfoTable_[ typeId < lastTypeId ? typeId : 0 ].size_; diff --git a/src/types.hpp b/src/types.hpp index 6d5fe5f5..13b086c9 100644 --- a/src/types.hpp +++ b/src/types.hpp @@ -46,6 +46,7 @@ #include #include #include +#include #ifdef HAVE_STDINT_H # include #endif @@ -113,6 +114,8 @@ namespace Exiv2 { public: //! Return the name of the type static const char* typeName(TypeId typeId); + //! Return the type id for a type name + static TypeId typeId(const std::string& typeName); //! Return the size in bytes of one element of this type static long typeSize(TypeId typeId);