// SPDX-License-Identifier: GPL-2.0-or-later #include // include local header files which are not part of libexiv2 #include "actions.hpp" #include "app_utils.hpp" #include "exiv2app.hpp" #include "convert.hpp" #include "getopt.hpp" #include "i18n.h" // NLS support. #include "xmp_exiv2.hpp" #include #include #include #include #include #include #include #include #if defined(_WIN32) #include #include #include #else #include #include #include #endif // ***************************************************************************** // local declarations namespace { constexpr auto emptyYodAdjust_ = std::array{ Params::YodAdjust{false, "-Y", 0}, Params::YodAdjust{false, "-O", 0}, Params::YodAdjust{false, "-D", 0}, }; //! List of all command identifiers and corresponding strings constexpr auto cmdIdAndString = std::array{ CmdIdAndString{CmdId::add, "add"}, CmdIdAndString{CmdId::set, "set"}, CmdIdAndString{CmdId::del, "del"}, CmdIdAndString{CmdId::reg, "reg"}, CmdIdAndString{CmdId::invalid, "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, int64_t& time); /*! @brief Parse the oparg string into a bitmap of common targets. @param optArg Option arguments @param action Action being processed @return A bitmap of common targets or -1 in case of a parse error */ int64_t parseCommonTargets(const std::string& optArg, const std::string& action); /*! @brief Parse numbers separated by commas into container @param previewNumbers Container for the numbers @param optArg Option arguments @param j Starting index into optArg @return Number of characters processed */ int parsePreviewNumbers(Params::PreviewNumbers& previewNumbers, const std::string& optArg, int j); /*! @brief Parse metadata modification commands from multiple files @param modifyCmds Reference to a structure to store the parsed commands @param cmdFiles Container with the file names */ bool parseCmdFiles(ModifyCmds& modifyCmds, const Params::CmdFiles& cmdFiles); /*! @brief Parse metadata modification commands from a container of commands @param modifyCmds Reference to a structure to store the parsed commands @param cmdLines Container with the commands */ bool parseCmdLines(ModifyCmds& modifyCmds, const Params::CmdLines& cmdLines); /*! @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); /*! @brief Parses a string containing backslash-escapes @param input Input string, assumed to be UTF-8 */ std::string parseEscapes(const std::string& input); } // namespace // ***************************************************************************** // Main int main(int argc, char* const argv[]) { setlocale(LC_CTYPE, ".utf8"); Exiv2::XmpParser::initialize(); ::atexit(Exiv2::XmpParser::terminate); #ifdef EXV_ENABLE_BMFF Exiv2::enableBMFF(); #endif #ifdef EXV_ENABLE_NLS setlocale(LC_ALL, ""); const std::string localeDir = EXV_LOCALEDIR[0] == '/' ? EXV_LOCALEDIR : (Exiv2::getProcessPath() + EXV_SEPARATOR_STR + EXV_LOCALEDIR); bindtextdomain(EXV_PACKAGE_NAME, localeDir.c_str()); textdomain(EXV_PACKAGE_NAME); #endif // Handle command line arguments Params& params = Params::instance(); if (params.getopt(argc, argv)) { params.usage(); return 1; } if (params.help_) { params.help(); return 0; } if (params.version_) { Params::version(params.verbose_); return 0; } int returnCode = EXIT_SUCCESS; try { // Create the required action class auto task = Action::TaskFactory::instance().create(static_cast(params.action_)); // Process all files auto filesCount = params.files_.size(); if (params.action_ & Action::extract && params.target_ & Params::ctStdInOut && filesCount > 1) { std::cerr << params.progname() << ": " << _("Only one file is allowed when extracting to stdout") << std::endl; returnCode = EXIT_FAILURE; } else { int w = filesCount > 9 ? filesCount > 99 ? 3 : 2 : 1; int n = 1; for (const auto& file : params.files_) { // If extracting to stdout then ignore verbose if (params.verbose_ && !(params.action_ & Action::extract && params.target_ & Params::ctStdInOut)) { std::cout << _("File") << " " << std::setw(w) << std::right << n++ << "/" << filesCount << ": " << file << std::endl; } task->setBinary(params.binary_); int ret = task->run(file); if (returnCode == EXIT_SUCCESS) returnCode = ret; } Action::TaskFactory::instance().cleanup(); Exiv2::XmpParser::terminate(); } } catch (const std::exception& exc) { std::cerr << "Uncaught exception: " << exc.what() << std::endl; returnCode = EXIT_FAILURE; } // Return a positive one byte code for better consistency across platforms return static_cast(returnCode) % 256; } // main // ***************************************************************************** // class Params Params::Params() : optstring_(":hVvqfbuktTFa:Y:O:D:r:p:P:d:e:i:c:m:M:l:S:g:K:n:Q:"), target_(ctExif | ctIptc | ctComment | ctXmp), format_("%Y%m%d_%H%M%S") { yodAdjust_[yodYear] = emptyYodAdjust_[yodYear]; yodAdjust_[yodMonth] = emptyYodAdjust_[yodMonth]; yodAdjust_[yodDay] = emptyYodAdjust_[yodDay]; } Params& Params::instance() { static Params instance_; return instance_; } void Params::version(bool verbose, std::ostream& os) { os << EXV_PACKAGE_STRING << std::endl; if (Params::instance().greps_.empty() && !verbose) { os << "\n" << _("This program is free software; you can redistribute it and/or\n" "modify it under the terms of the GNU General Public License\n" "as published by the Free Software Foundation; either version 2\n" "of the License, or (at your option) any later version.\n") << "\n" << _("This program is distributed in the hope that it will be useful,\n" "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" "GNU General Public License for more details.\n") << "\n" << _("You should have received a copy of the GNU General Public\n" "License along with this program; if not, write to the Free\n" "Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n" "Boston, MA 02110-1301 USA\n"); } if (verbose) Exiv2::dumpLibraryInfo(os, Params::instance().greps_); } void Params::usage(std::ostream& os) const { os << _("Usage:") << " " << progname() << " " << _("[ option [ arg ] ]+ [ action ] file ...\n\n") << _("Image metadata manipulation tool.\n"); } void Params::help(std::ostream& os) const { usage(os); os << _("\nWhere file is one or more files, optionally containing a URL\n" "(http, https, ftp, sftp, data or file) or wildcard\n") << _("\nActions:\n") << _(" pr | print Print image metadata (default is a summary). This is the default\n" " action\n") << _(" ad | adjust Adjust Exif timestamps by the given time. Requires\n" " at least one of -a, -Y, -O or -D\n") << _(" rm | delete Deletes image metadata, use -d to choose type to delete\n" " (default is all)\n") << _(" in | insert Insert metadata from .exv, .xmp, thumbnail or .icc file.\n" " Use option -S to change the suffix of the input files and\n" " -l to change the location\n") << _(" ex | extract Extract metadata to .exv, .xmp, preview image, thumbnail,\n" " or ICC profile. Use option -S to change the suffix of the input\n" " files and -l to change the location\n") << _(" mv | rename Rename files and/or set file timestamps according to the\n" " Exif timestamps. The filename format can be set with\n" " -r format, timestamp options are controlled with -t and -T\n") << _(" mo | modify Apply commands to modify the Exif, IPTC and XMP metadata.\n" " Requires option -m or -M\n") << _(" fi | fixiso Copy ISO setting from Canon and Nikon makernotes, to the\n" " standard Exif tag\n") << _(" fc | fixcom Convert the Unicode Exif user comment to UCS-2. The current\n" " character encoding can be specified with the -n option\n") << _("\nOptions:\n") << _(" -h Display this help and exit\n") << _(" -V Show the program version and exit\n") << _(" -v Be verbose during the program run\n") << _(" -q Silence warnings and error messages (quiet)\n") << _(" -Q lvl Set log-level to d(ebug), i(nfo), w(arning), e(rror) or m(ute)\n") << _(" -b Obsolete, reserved for use with the test suit\n") << _(" -u Show unknown tags (e.g., Exif.SonyMisc3c.0x022b)\n") << _(" -g str Only output where 'str' matches in output text (grep)\n" " Append /i to 'str' for case insensitive\n") << _(" -K key Only output where 'key' exactly matches tag's key\n") << _(" -n enc Character set to decode Exif Unicode user comments\n") << _(" -k Preserve file timestamps when updating files (keep)\n") << _(" -t Set the file timestamp from Exif metadata when renaming (overrides -k)\n") << _(" -T Only set the file timestamp from Exif metadata ('rename' action)\n") << _(" -f Do not prompt before overwriting existing files (force)\n") << _(" -F Do not prompt before renaming files (Force)\n") << _(" -a time Time adjustment in the format [+|-]HH[:MM[:SS]]. For 'adjust' action\n") << _(" -Y yrs Year adjustment with the 'adjust' action\n") << _(" -O mon Month adjustment with the 'adjust' action\n") << _(" -D day Day adjustment with the 'adjust' action\n") << _(" -p mode Print mode for the 'print' action. Possible modes are:\n") << _(" s : A summary of the Exif metadata (the default)\n") << _(" a : Exif, IPTC and XMP tags (shortcut for -Pkyct)\n") << _(" e : Exif tags (shortcut for -PEkycv)\n") << _(" t : Interpreted (translated) Exif tags (-PEkyct)\n") << _(" v : Plain (untranslated) Exif tags values (-PExgnycv)\n") << _(" h : Hex dump of the Exif tags (-PExgnycsh)\n") << _(" i : IPTC tags (-PIkyct)\n") << _(" x : XMP tags (-PXkyct)\n") << _(" c : JPEG comment\n") << _(" p : List available image preview, sorted by size\n") << _(" C : Print ICC profile\n") << _(" R : Recursive print structure of image (debug build only)\n") << _(" S : Print structure of image (limited file types)\n") << _(" X : Extract \"raw\" XMP\n") << _(" -P flgs Print flags for fine control of tag lists ('print' action):\n") << _(" E : Exif tags\n") << _(" I : IPTC tags\n") << _(" X : XMP tags\n") << _(" x : Tag number for Exif or IPTC tags (in hexadecimal)\n") << _(" g : Group name (e.g. Exif.Photo.UserComment, Photo)\n") << _(" k : Key (e.g. Exif.Photo.UserComment)\n") << _(" l : Tag label (e.g. Exif.Photo.UserComment, 'User comment')\n") << _(" d : Tag description\n") << _(" n : Tag name (e.g. Exif.Photo.UserComment, UserComment)\n") << _(" y : Type\n") << _(" y : Type\n") << _(" c : Number of components (count)\n") << _(" s : Size in bytes of vanilla value (may include NULL)\n") << _(" v : Plain data value of untranslated (vanilla)\n") << _(" V : Plain data value, data type and the word 'set'\n") << _(" t : Interpreted (translated) human readable values\n") << _(" h : Hex dump of the data\n") << _(" -d tgt1 Delete target(s) for the 'delete' action. Possible targets are:\n") << _(" a : All supported metadata (the default)\n") << _(" e : Exif tags\n") << _(" t : Exif thumbnail only\n") << _(" i : IPTC tags\n") << _(" x : XMP tags\n") << _(" c : JPEG comment\n") << _(" C : ICC Profile\n") << _(" c : All IPTC data (any broken multiple IPTC blocks)\n") << _(" - : Input from stdin\n") << _(" -i tgt2 Insert target(s) for the 'insert' action. Possible targets are\n") << _(" a : All supported metadata (the default)\n") << _(" e : Exif tags\n") << _(" t : Exif thumbnail only (JPEGs only from -thumb.jpg)\n") << _(" i : IPTC tags\n") << _(" x : XMP tags\n") << _(" c : JPEG comment\n") << _(" C : ICC Profile, from .icc\n") << _(" X : XMP sidecar from file .xmp\n") << _(" XX: \"raw\" metadata from .exv. XMP default, optional Exif and IPTC\n") << _(" - : Input from stdin\n") << _(" -e tgt3 Extract target(s) for the 'extract' action. Possible targets\n") << _(" a : All supported metadata (the default)\n") << _(" e : Exif tags\n") << _(" t : Exif thumbnail only (to -thumb.jpg)\n") << _(" i : IPTC tags\n") << _(" x : XMP tags\n") << _(" c : JPEG comment\n") << _(" pN: Extract N'th preview image to -preview.\n") << _(" C : ICC Profile, to .icc\n") << _(" X : XMP sidecar to .xmp\n") << _(" XX: \"raw\" metadata to .exv. XMP default, optional Exif and IPTC\n") << _(" - : Output to stdin\n") << _(" -r fmt Filename format for the 'rename' action. The format string\n") << _(" follows strftime(3). The following keywords are also supported:\n") << _(" :basename: - original filename without extension\n") << _(" :dirname: - name of the directory holding the original file\n") << _(" :parentname: - name of parent directory\n") << _(" Default 'fmt' is %Y%m%d_%H%M%S\n") << _(" -c txt JPEG comment string to set in the image.\n") << _(" -m cmdf Applies commands in 'cmdf' file, for the modify action (see -M for format).\n") << _(" -M cmd Command line for the modify action. The format is:\n") << _(" ( (set | add) [[] ] |\n") << _(" del [] |\n") << _(" reg prefix namespace )\n") << _(" -l dir Location (directory) for files to be inserted from or extracted to.\n") << _(" -S suf Use suffix 'suf' for source files for insert action.\n") << _("\nExamples:\n") << _(" exiv2 -pe image.dng *.jp2\n" " Print all Exif tags in image.dng and all .jp2 files\n") << _(" exiv2 -g date/i https://clanmills.com/Stonehenge.jpg\n" " Print all tags in file, where key contains 'date' (case insensitive)\n") << _(" exiv2 -M\"set Xmp.dc.subject XmpBag Sky\" image.tiff\n" " Set (or add if missing) value to tag in file\n\n"); } // Params::help int Params::option(int opt, const std::string& optArg, int optOpt) { int rc = 0; switch (opt) { case 'h': help_ = true; break; case 'V': version_ = true; break; case 'v': verbose_ = true; break; case 'q': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::mute); break; case 'Q': rc = setLogLevel(optArg); break; case 'k': preserve_ = true; break; case 'b': binary_ = true; break; case 'u': unknown_ = false; break; case 'f': force_ = true; fileExistsPolicy_ = overwritePolicy; break; case 'F': force_ = true; fileExistsPolicy_ = renamePolicy; break; case 'g': rc = evalGrep(optArg); break; case 'K': rc = evalKey(optArg); printMode_ = pmList; break; case 'n': charset_ = optArg; break; case 'r': rc = evalRename(opt, optArg); break; case 't': rc = evalRename(opt, optArg); break; case 'T': rc = evalRename(opt, optArg); break; case 'a': rc = evalAdjust(optArg); break; case 'Y': rc = evalYodAdjust(yodYear, optArg); break; case 'O': rc = evalYodAdjust(yodMonth, optArg); break; case 'D': rc = evalYodAdjust(yodDay, optArg); break; case 'p': rc = evalPrint(optArg); break; case 'P': rc = evalPrintFlags(optArg); break; case 'd': rc = evalDelete(optArg); break; case 'e': rc = evalExtract(optArg); break; case 'C': rc = evalExtract(optArg); break; case 'i': rc = evalInsert(optArg); break; case 'c': rc = evalModify(opt, optArg); break; case 'm': rc = evalModify(opt, optArg); break; case 'M': rc = evalModify(opt, optArg); break; case 'l': directory_ = optArg; break; case 'S': suffix_ = optArg; break; case ':': std::cerr << progname() << ": " << _("Option") << " -" << static_cast(optOpt) << " " << _("requires an argument\n"); rc = 1; break; case '?': std::cerr << progname() << ": " << _("Unrecognized option") << " -" << static_cast(optOpt) << "\n"; rc = 1; break; default: std::cerr << progname() << ": " << _("getopt returned unexpected character code") << " " << std::hex << opt << "\n"; rc = 1; break; } return rc; } // Params::option int Params::setLogLevel(const std::string& optArg) { int rc = 0; const char logLevel = tolower(optArg[0]); switch (logLevel) { case 'd': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::debug); break; case 'i': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::info); break; case 'w': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::warn); break; case 'e': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::error); break; case 'm': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::mute); break; default: std::cerr << progname() << ": " << _("Option") << " -Q: " << _("Invalid argument") << " \"" << optArg << "\"\n"; rc = 1; break; } return rc; } // Params::setLogLevel int Params::evalGrep(const std::string& optArg) { // check that string ends in "/i" bool bIgnoreCase = optArg.size() > 2 && optArg.back() == 'i' && optArg[optArg.size() - 2] == '/'; auto pattern = bIgnoreCase ? optArg.substr(0, optArg.size() - 2) : optArg; try { // use POSIX syntax, optimize for faster matching, treat all sub expressions as unnamed auto flags = std::regex::basic | std::regex::optimize | std::regex::nosubs; flags = bIgnoreCase ? flags | std::regex::icase : flags; // try and emplace regex into vector // might throw if invalid pattern greps_.emplace_back(pattern, flags); } catch (std::regex_error const&) { // there was an error compiling the regexp std::cerr << progname() << ": " << _("Option") << " -g: " << _("Invalid regexp") << " \"" << optArg << "\n"; return 1; } return 0; } // Params::evalGrep int Params::evalKey(const std::string& optArg) { int result = 0; keys_.push_back(optArg); return result; } // Params::evalKey int Params::evalRename(int opt, const std::string& optArg) { int rc = 0; switch (action_) { case Action::none: action_ = Action::rename; switch (opt) { case 'r': format_ = optArg; formatSet_ = true; break; case 't': timestamp_ = true; break; case 'T': timestampOnly_ = true; break; } break; case Action::rename: if (opt == 'r' && (formatSet_ || timestampOnly_)) { std::cerr << progname() << ": " << _("Ignoring surplus option") << " -r \"" << optArg << "\"\n"; } else { format_ = optArg; formatSet_ = true; } break; default: std::cerr << progname() << ": " << _("Option") << " -" << static_cast(opt) << " " << _("is not compatible with a previous option\n"); rc = 1; break; } return rc; } // Params::evalRename int Params::evalAdjust(const std::string& optArg) { int rc = 0; switch (action_) { case Action::none: case Action::adjust: if (adjust_) { std::cerr << progname() << ": " << _("Ignoring surplus option -a") << " " << optArg << "\n"; break; } action_ = Action::adjust; adjust_ = parseTime(optArg, adjustment_); if (!adjust_) { std::cerr << progname() << ": " << _("Error parsing -a option argument") << " `" << optArg << "'\n"; rc = 1; } break; default: std::cerr << progname() << ": " << _("Option -a is not compatible with a previous option\n"); rc = 1; break; } return rc; } // Params::evalAdjust int Params::evalYodAdjust(const Yod& yod, const std::string& optArg) { int rc = 0; switch (action_) { case Action::none: // fall-through case Action::adjust: if (yodAdjust_[yod].flag_) { std::cerr << progname() << ": " << _("Ignoring surplus option") << " " << yodAdjust_[yod].option_ << " " << optArg << "\n"; break; } action_ = Action::adjust; yodAdjust_[yod].flag_ = true; if (!Util::strtol(optArg.c_str(), yodAdjust_[yod].adjustment_)) { std::cerr << progname() << ": " << _("Error parsing") << " " << yodAdjust_[yod].option_ << " " << _("option argument") << " `" << optArg << "'\n"; rc = 1; } break; default: std::cerr << progname() << ": " << _("Option") << " " << yodAdjust_[yod].option_ << " " << _("is not compatible with a previous option\n"); rc = 1; break; } return rc; } // Params::evalYodAdjust int Params::evalPrint(const std::string& optArg) { int rc = 0; switch (action_) { case Action::none: switch (optArg[0]) { case 's': action_ = Action::print; printMode_ = pmSummary; break; case 'a': rc = evalPrintFlags("kyct"); break; case 'e': rc = evalPrintFlags("Ekycv"); break; case 't': rc = evalPrintFlags("Ekyct"); break; case 'v': rc = evalPrintFlags("Exgnycv"); break; case 'h': rc = evalPrintFlags("Exgnycsh"); break; case 'i': rc = evalPrintFlags("Ikyct"); break; case 'x': rc = evalPrintFlags("Xkyct"); break; case 'c': action_ = Action::print; printMode_ = pmComment; break; case 'p': action_ = Action::print; printMode_ = pmPreview; break; case 'C': action_ = Action::print; printMode_ = pmIccProfile; break; case 'R': #ifdef NDEBUG std::cerr << progname() << ": " << _("Action not available in Release mode") << ": '" << optArg << "'\n"; rc = 1; #else action_ = Action::print; printMode_ = pmRecursive; #endif break; case 'S': action_ = Action::print; printMode_ = pmStructure; break; case 'X': action_ = Action::print; printMode_ = pmXMP; break; default: std::cerr << progname() << ": " << _("Unrecognized print mode") << " `" << optArg << "'\n"; rc = 1; break; } break; case Action::print: std::cerr << progname() << ": " << _("Ignoring surplus option -p") << optArg << "\n"; break; default: std::cerr << progname() << ": " << _("Option -p is not compatible with a previous option\n"); rc = 1; break; } return rc; } // Params::evalPrint int Params::evalPrintFlags(const std::string& optArg) { int rc = 0; switch (action_) { case Action::none: action_ = Action::print; printMode_ = pmList; for (auto&& i : optArg) { switch (i) { case 'E': printTags_ |= MetadataId::exif; break; case 'I': printTags_ |= MetadataId::iptc; break; case 'X': printTags_ |= MetadataId::xmp; break; case 'x': printItems_ |= prTag; break; case 'g': printItems_ |= prGroup; break; case 'k': printItems_ |= prKey; break; case 'l': printItems_ |= prLabel; break; case 'n': printItems_ |= prName; break; case 'y': printItems_ |= prType; break; case 'c': printItems_ |= prCount; break; case 's': printItems_ |= prSize; break; case 'v': printItems_ |= prValue; break; case 't': printItems_ |= prTrans; break; case 'h': printItems_ |= prHex; break; case 'V': printItems_ |= prSet | prKey | prType | prValue; break; case 'd': printItems_ |= prDesc; break; default: std::cerr << progname() << ": " << _("Unrecognized print item") << " `" << i << "'\n"; rc = 1; break; } } break; case Action::print: std::cerr << progname() << ": " << _("Ignoring surplus option -P") << optArg << "\n"; break; default: std::cerr << progname() << ": " << _("Option -P is not compatible with a previous option\n"); rc = 1; break; } return rc; } // Params::evalPrintFlags int Params::evalDelete(const std::string& optArg) { switch (action_) { case Action::none: action_ = Action::erase; target_ = static_cast(0); // fallthrough case Action::erase: { const auto rc = parseCommonTargets(optArg, "erase"); if (rc > 0) { target_ |= static_cast(rc); return 0; } return 1; } default: std::cerr << progname() << ": " << _("Option -d is not compatible with a previous option\n"); return 1; } } // Params::evalDelete int Params::evalExtract(const std::string& optArg) { switch (action_) { case Action::none: case Action::modify: action_ = Action::extract; target_ = static_cast(0); // fallthrough case Action::extract: { const auto rc = parseCommonTargets(optArg, "extract"); if (rc > 0) { target_ |= static_cast(rc); return 0; } return 1; } default: std::cerr << progname() << ": " << _("Option -e is not compatible with a previous option\n"); return 1; } } // Params::evalExtract int Params::evalInsert(const std::string& optArg) { switch (action_) { case Action::none: case Action::modify: action_ = Action::insert; target_ = static_cast(0); // fallthrough case Action::insert: { const auto rc = parseCommonTargets(optArg, "insert"); if (rc > 0) { target_ |= static_cast(rc); return 0; } return 1; } default: std::cerr << progname() << ": " << _("Option -i is not compatible with a previous option\n"); return 1; } } // Params::evalInsert int Params::evalModify(int opt, const std::string& optArg) { switch (action_) { case Action::none: action_ = Action::modify; // fallthrough case Action::modify: case Action::extract: case Action::insert: if (opt == 'c') jpegComment_ = parseEscapes(optArg); if (opt == 'm') cmdFiles_.push_back(optArg); // parse the files later if (opt == 'M') cmdLines_.push_back(optArg); // parse the commands later return 0; default: std::cerr << progname() << ": " << _("Option") << " -" << static_cast(opt) << " " << _("is not compatible with a previous option\n"); return 1; } } // Params::evalModify int Params::nonoption(const std::string& argv) { int rc = 0; bool action = false; if (first_) { // The first non-option argument must be the action first_ = false; if (argv == "ad" || argv == "adjust") { if (action_ != Action::none && action_ != Action::adjust) { std::cerr << progname() << ": " << _("Action adjust is not compatible with the given options\n"); rc = 1; } action = true; action_ = Action::adjust; } if (argv == "pr" || argv == "print") { if (action_ != Action::none && action_ != Action::print) { std::cerr << progname() << ": " << _("Action print is not compatible with the given options\n"); rc = 1; } action = true; action_ = Action::print; } if (argv == "rm" || argv == "delete") { if (action_ != Action::none && action_ != Action::erase) { std::cerr << progname() << ": " << _("Action delete is not compatible with the given options\n"); rc = 1; } action = true; action_ = Action::erase; } if (argv == "ex" || argv == "extract") { if (action_ != Action::none && action_ != Action::extract && action_ != Action::modify) { std::cerr << progname() << ": " << _("Action extract is not compatible with the given options\n"); rc = 1; } action = true; action_ = Action::extract; } if (argv == "in" || argv == "insert") { if (action_ != Action::none && action_ != Action::insert && action_ != Action::modify) { std::cerr << progname() << ": " << _("Action insert is not compatible with the given options\n"); rc = 1; } action = true; action_ = Action::insert; } if (argv == "mv" || argv == "rename") { if (action_ != Action::none && action_ != Action::rename) { std::cerr << progname() << ": " << _("Action rename is not compatible with the given options\n"); rc = 1; } 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 (argv == "fi" || argv == "fixiso") { if (action_ != Action::none && action_ != Action::fixiso) { std::cerr << progname() << ": " << _("Action fixiso is not compatible with the given options\n"); rc = 1; } action = true; action_ = Action::fixiso; } if (argv == "fc" || argv == "fixcom" || argv == "fixcomment") { if (action_ != Action::none && action_ != Action::fixcom) { std::cerr << progname() << ": " << _("Action fixcom is not compatible with the given options\n"); rc = 1; } action = true; action_ = Action::fixcom; } if (action_ == Action::none) { // if everything else fails, assume print as the default action action_ = Action::print; } } if (!action) { files_.push_back(argv); } return rc; } // Params::nonoption static size_t readFileToBuf(FILE* f, Exiv2::DataBuf& buf) { const int buff_size = 4 * 1028; std::vector bytes(buff_size); size_t nBytes = 0; bool more{true}; std::array buff; while (more) { auto n = fread(buff.data(), 1, buff_size, f); more = n > 0; if (more) { bytes.resize(nBytes + n); std::copy_n(buff.begin(), n, bytes.begin() + nBytes); nBytes += n; } } if (nBytes) { buf.alloc(nBytes); std::copy(bytes.begin(), bytes.end(), buf.begin()); } return nBytes; } void Params::getStdin(Exiv2::DataBuf& buf) { // copy stdin to stdinBuf if (stdinBuf.empty()) { #if defined(_WIN32) DWORD fdwMode; _setmode(fileno(stdin), O_BINARY); Sleep(300); if (!GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &fdwMode)) { // failed: stdin has bytes! #else // http://stackoverflow.com/questions/34479795/make-c-not-wait-for-user-input/34479916#34479916 fd_set readfds; FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); struct timeval timeout = {1, 0}; // yes: set timeout seconds,microseconds // if we have something in the pipe, read it if (select(1, &readfds, nullptr, nullptr, &timeout)) { #endif #ifdef DEBUG std::cerr << "stdin has data" << std::endl; #endif readFileToBuf(stdin, stdinBuf); } #ifdef DEBUG // this is only used to simulate reading from stdin when debugging // to simulate exiv2 -pX foo.jpg | exiv2 -iXX- bar.jpg // exiv2 -pX foo.jpg > ~/temp/stdin ; exiv2 -iXX- bar.jpg if (stdinBuf.empty()) { const char* path = "/Users/rmills/temp/stdin"; FILE* f = fopen(path, "rb"); if (f) { readFileToBuf(f, stdinBuf); fclose(f); std::cerr << "read stdin from " << path << std::endl; } } #endif #ifdef DEBUG std::cerr << "getStdin stdinBuf.size_ = " << stdinBuf.size() << std::endl; #endif } // copy stdinBuf to buf if (!stdinBuf.empty()) { buf.alloc(stdinBuf.size()); std::copy(stdinBuf.begin(), stdinBuf.end(), buf.begin()); } #ifdef DEBUG std::cerr << "getStdin stdinBuf.size_ = " << stdinBuf.size() << std::endl; #endif } // Params::getStdin() int Params::getopt(int argc, char* const Argv[]) { std::vector argv(argc + 1); argv[argc] = nullptr; const std::unordered_map longs{ {"--adjust", "-a"}, {"--binary", "-b"}, {"--comment", "-c"}, {"--delete", "-d"}, {"--days", "-D"}, {"--extract", "-e"}, {"--force", "-f"}, {"--Force", "-F"}, {"--grep", "-g"}, {"--help", "-h"}, {"--insert", "-i"}, {"--keep", "-k"}, {"--key", "-K"}, {"--location", "-l"}, {"--modify", "-m"}, {"--Modify", "-M"}, {"--encode", "-n"}, {"--months", "-O"}, {"--print", "-p"}, {"--Print", "-P"}, {"--quiet", "-q"}, {"--log", "-Q"}, {"--rename", "-r"}, {"--suffix", "-S"}, {"--timestamp", "-t"}, {"--Timestamp", "-T"}, {"--unknown", "-u"}, {"--verbose", "-v"}, {"--Version", "-V"}, {"--version", "-V"}, {"--years", "-Y"}, }; for (int i = 0; i < argc; i++) { std::string arg(Argv[i]); if (longs.find(arg) != longs.end()) { argv[i] = ::strdup(longs.at(arg).c_str()); } else { argv[i] = ::strdup(Argv[i]); } } int rc = Util::Getopt::getopt(argc, argv.data(), optstring_); // Further consistency checks if (help_ || version_) { goto cleanup; } if (action_ == Action::none) { // This shouldn't happen since print is taken as default action std::cerr << progname() << ": " << _("An action must be specified\n"); rc = 1; } if (action_ == Action::adjust && !adjust_ && !yodAdjust_[yodYear].flag_ && !yodAdjust_[yodMonth].flag_ && !yodAdjust_[yodDay].flag_) { std::cerr << progname() << ": " << _("Adjust action requires at least one -a, -Y, -O or -D option\n"); rc = 1; } if (action_ == Action::modify && cmdFiles_.empty() && cmdLines_.empty() && jpegComment_.empty()) { std::cerr << progname() << ": " << _("Modify action requires at least one -c, -m or -M option\n"); rc = 1; } if (files_.empty()) { std::cerr << progname() << ": " << _("At least one file is required\n"); rc = 1; } if (rc == 0 && !cmdFiles_.empty()) { // Parse command files if (!parseCmdFiles(modifyCmds_, cmdFiles_)) { std::cerr << progname() << ": " << _("Error parsing -m option arguments\n"); rc = 1; } } if (rc == 0 && !cmdLines_.empty()) { // Parse command lines if (!parseCmdLines(modifyCmds_, cmdLines_)) { std::cerr << progname() << ": " << _("Error parsing -M option arguments\n"); rc = 1; } } if (rc == 0 && (!cmdFiles_.empty() || !cmdLines_.empty())) { // We'll set them again, after reading the file Exiv2::XmpProperties::unregisterNs(); } if (!directory_.empty() && !(action_ == Action::insert || action_ == Action::extract)) { std::cerr << progname() << ": " << _("-l option can only be used with extract or insert actions\n"); rc = 1; } if (!suffix_.empty() && !(action_ == Action::insert)) { std::cerr << progname() << ": " << _("-S option can only be used with insert action\n"); rc = 1; } if (timestamp_ && !(action_ == Action::rename)) { std::cerr << progname() << ": " << _("-t option can only be used with rename action\n"); rc = 1; } if (timestampOnly_ && !(action_ == Action::rename)) { std::cerr << progname() << ": " << _("-T option can only be used with rename action\n"); rc = 1; } cleanup: // cleanup the argument vector for (int i = 0; i < argc; i++) ::free(argv[i]); return rc; } // ***************************************************************************** // local implementations namespace { bool parseTime(const std::string& ts, int64_t& time) { std::string hstr, mstr, sstr; auto cts = new char[ts.length() + 1]; strcpy(cts, ts.c_str()); auto tmp = ::strtok(cts, ":"); if (tmp) hstr = tmp; tmp = ::strtok(nullptr, ":"); if (tmp) mstr = tmp; tmp = ::strtok(nullptr, ":"); if (tmp) sstr = tmp; delete[] cts; int sign = 1; int64_t hh(0), mm(0), ss(0); // [-]HH part if (!Util::strtol(hstr.c_str(), hh)) return false; if (hh < 0) { sign = -1; hh *= -1; } // check for the -0 special case if (hh == 0 && hstr.find('-') != std::string::npos) sign = -1; // MM part, if there is one if (!mstr.empty()) { if (!Util::strtol(mstr.c_str(), mm)) return false; if (mm > 59) return false; if (mm < 0) return false; } // SS part, if there is one if (!sstr.empty()) { if (!Util::strtol(sstr.c_str(), ss)) return false; if (ss > 59) return false; if (ss < 0) return false; } time = sign * (hh * 3600 + mm * 60 + ss); return true; } // parseTime void printUnrecognizedArgument(const char argc, const std::string& action) { std::cerr << Params::instance().progname() << ": " << _("Unrecognized ") << action << " " << _("target") << " `" << argc << "'\n"; } int64_t parseCommonTargets(const std::string& optArg, const std::string& action) { int64_t rc = 0; auto target = static_cast(0); Params::CommonTarget all = Params::ctExif | Params::ctIptc | Params::ctComment | Params::ctXmp; Params::CommonTarget extra = Params::ctXmpSidecar | Params::ctExif | Params::ctIptc | Params::ctXmp; for (size_t i = 0; rc == 0 && i < optArg.size(); ++i) { switch (optArg[i]) { case 'e': target |= Params::ctExif; break; case 'i': target |= Params::ctIptc; break; case 'x': target |= Params::ctXmp; break; case 'c': target |= Params::ctComment; break; case 't': target |= Params::ctThumb; break; case 'C': target |= Params::ctIccProfile; break; case 'I': target |= Params::ctIptcRaw; break; case '-': target |= Params::ctStdInOut; break; case 'a': target |= all; break; case 'X': target |= extra; // -eX if (i > 0) { // -eXX or -iXX target |= Params::ctXmpRaw; target = static_cast(target & ~extra); // turn off those bits } break; case 'p': { if (action == "extract") { i += static_cast( parsePreviewNumbers(Params::instance().previewNumbers_, optArg, static_cast(i) + 1)); target |= Params::ctPreview; break; } printUnrecognizedArgument(optArg[i], action); rc = -1; break; } default: printUnrecognizedArgument(optArg[i], action); rc = -1; break; } } return rc ? rc : static_cast(target); } int parsePreviewNumbers(Params::PreviewNumbers& previewNumbers, const std::string& optArg, int j) { size_t k = j; for (size_t i = j; i < optArg.size(); ++i) { std::ostringstream os; for (k = i; k < optArg.size() && isdigit(optArg[k]); ++k) { os << optArg[k]; } if (k > i) { bool ok = false; auto num = Exiv2::stringTo(os.str(), ok); if (ok && num >= 0) { previewNumbers.insert(num); } else { std::cerr << Params::instance().progname() << ": " << _("Invalid preview number") << ": " << num << "\n"; } i = k; } if (!(k < optArg.size() && optArg[i] == ',')) break; } auto ret = static_cast(k - j); if (ret == 0) { previewNumbers.insert(0); } #ifdef DEBUG std::cout << "\nThe set now contains: "; for (auto&& number : previewNumbers) { std::cout << number << ", "; } std::cout << std::endl; #endif return static_cast(k - j); } // parsePreviewNumbers bool parseCmdFiles(ModifyCmds& modifyCmds, const Params::CmdFiles& cmdFiles) { for (auto&& filename : cmdFiles) { try { std::ifstream file(filename.c_str()); bool bStdin = filename == "-"; if (!file && !bStdin) { std::cerr << filename << ": " << _("Failed to open command file for reading\n"); return false; } int num = 0; std::string line; while (bStdin ? std::getline(std::cin, line) : std::getline(file, line)) { ModifyCmd modifyCmd; if (parseLine(modifyCmd, line, ++num)) { modifyCmds.push_back(modifyCmd); } } } catch (const Exiv2::Error& error) { std::cerr << filename << ", " << _("line") << " " << error << "\n"; return false; } } return true; } // parseCmdFile bool parseCmdLines(ModifyCmds& modifyCmds, const Params::CmdLines& cmdLines) { try { int num = 0; for (auto&& line : cmdLines) { ModifyCmd modifyCmd; if (parseLine(modifyCmd, line, ++num)) { modifyCmds.push_back(modifyCmd); } } return true; } catch (const Exiv2::Error& error) { std::cerr << _("-M option") << " " << error << "\n"; return false; } } // parseCmdLines #ifdef _WIN32 static std::string formatArg(const char* arg) { std::string result = ""; char b = ' '; char e = '\\'; std::string E = std::string("\\"); char q = '\''; std::string Q = std::string("'"); bool qt = false; char* a = (char*)arg; while (*a) { if (*a == b || *a == e || *a == q) qt = true; if (*a == q) result += E; if (*a == e) result += E; result += std::string(a, 1); a++; } if (qt) result = Q + result + Q; return result; } #endif 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 (cmdEnd == std::string::npos || keyStart == std::string::npos) { std::string cmdLine; #ifdef _WIN32 for (int i = 1; i < __argc; i++) { cmdLine += std::string(" ") + formatArg(__argv[i]); } #endif throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, Exiv2::toString(num) + ": " + _("Invalid command line:") + cmdLine); } std::string cmd(line.substr(cmdStart, cmdEnd - cmdStart)); CmdId cmdId = commandId(cmd); if (cmdId == CmdId::invalid) { throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, Exiv2::toString(num) + ": " + _("Invalid command") + " `" + cmd + "'"); } Exiv2::TypeId defaultType = Exiv2::invalidTypeId; std::string key(line.substr(keyStart, keyEnd - keyStart)); MetadataId metadataId = MetadataId::invalid; if (cmdId != CmdId::reg) { try { Exiv2::IptcKey iptcKey(key); metadataId = MetadataId::iptc; defaultType = Exiv2::IptcDataSets::dataSetType(iptcKey.tag(), iptcKey.record()); } catch (const Exiv2::Error&) { } if (metadataId == MetadataId::invalid) { try { Exiv2::ExifKey exifKey(key); metadataId = MetadataId::exif; defaultType = exifKey.defaultTypeId(); } catch (const Exiv2::Error&) { } } if (metadataId == MetadataId::invalid) { try { Exiv2::XmpKey xmpKey(key); metadataId = MetadataId::xmp; defaultType = Exiv2::XmpProperties::propertyType(xmpKey); } catch (const Exiv2::Error&) { } } if (metadataId == MetadataId::invalid) { throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, Exiv2::toString(num) + ": " + _("Invalid key") + " `" + key + "'"); } } std::string value; Exiv2::TypeId type = defaultType; bool explicitType = false; if (cmdId != CmdId::del) { // Get type and value std::string::size_type typeStart = std::string::npos; if (keyEnd != std::string::npos) typeStart = line.find_first_not_of(delim, keyEnd + 1); std::string::size_type typeEnd = std::string::npos; if (typeStart != std::string::npos) typeEnd = line.find_first_of(delim, typeStart + 1); std::string::size_type valStart = typeStart; std::string::size_type valEnd = std::string::npos; if (valStart != std::string::npos) valEnd = line.find_last_not_of(delim); if (cmdId == CmdId::reg && (keyEnd == std::string::npos || valStart == std::string::npos)) { throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, Exiv2::toString(num) + ": " + _("Invalid command line") + " "); } if (cmdId != CmdId::reg && typeStart != std::string::npos && typeEnd != std::string::npos) { std::string typeStr(line.substr(typeStart, typeEnd - typeStart)); Exiv2::TypeId tmpType = Exiv2::TypeInfo::typeId(typeStr); if (tmpType != Exiv2::invalidTypeId) { valStart = line.find_first_not_of(delim, typeEnd + 1); if (valStart == std::string::npos) { throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, Exiv2::toString(num) + ": " + _("Invalid command line") + " "); } type = tmpType; explicitType = true; } } if (valStart != std::string::npos) { value = parseEscapes(line.substr(valStart, valEnd + 1 - valStart)); std::string::size_type last = value.length() - 1; if ((value.at(0) == '"' && value.at(last) == '"') || (value.at(0) == '\'' && value.at(last) == '\'')) { 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; if (cmdId == CmdId::reg) { if (value.empty()) { throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, Exiv2::toString(num) + ": " + _("Empty value for key") + +" `" + key + "'"); } // Registration needs to be done immediately as the new namespaces are // looked up during parsing of subsequent lines (to validate XMP keys). Exiv2::XmpProperties::registerNs(modifyCmd.value_, modifyCmd.key_); } return true; } // parseLine CmdId commandId(const std::string& cmdString) { auto it = std::find_if(cmdIdAndString.begin(), cmdIdAndString.end(), [&](auto cs) { return cs.second == cmdString; }); return it != cmdIdAndString.end() ? it->first : CmdId::invalid; } std::string parseEscapes(const std::string& input) { std::string result; for (size_t i = 0; i < input.length(); ++i) { char ch = input[i]; if (ch != '\\') { result.push_back(ch); continue; } size_t escapeStart = i; if (input.length() - 1 <= i) { result.push_back(ch); continue; } ++i; ch = input[i]; switch (ch) { case '\\': // Escaping of backslash result.push_back('\\'); break; case 'r': // Escaping of carriage return result.push_back('\r'); break; case 'n': // Escaping of newline result.push_back('\n'); break; case 't': // Escaping of tab result.push_back('\t'); break; case 'u': // Escaping of unicode if (input.length() >= 4 && input.length() - 4 > i) { uint32_t acc = 0; for (int j = 0; j < 4; ++j) { ++i; acc <<= 4; if (input[i] >= '0' && input[i] <= '9') { acc |= input[i] - '0'; } else if (input[i] >= 'a' && input[i] <= 'f') { acc |= input[i] - 'a' + 10; } else if (input[i] >= 'A' && input[i] <= 'F') { acc |= input[i] - 'A' + 10; } else { acc = 0xFFFFFFFF; break; } } if (acc == 0xFFFFFFFF) { result.push_back('\\'); i = escapeStart; break; } std::string ucs2toUtf8; ucs2toUtf8.push_back(static_cast((acc & 0xff00U) >> 8)); ucs2toUtf8.push_back(static_cast(acc & 0x00ffU)); if (Exiv2::convertStringCharset(ucs2toUtf8, "UCS-2BE", "UTF-8")) { result.append(ucs2toUtf8); } } else { result.push_back('\\'); result.push_back(ch); } break; default: result.push_back('\\'); result.push_back(ch); } } return result; } } // namespace