diff --git a/app/actions.cpp b/app/actions.cpp index 0088f7937c..da5e126d92 100644 --- a/app/actions.cpp +++ b/app/actions.cpp @@ -107,7 +107,7 @@ int metacopy(const std::string& source, const std::string& tgt, Exiv2::ImageType the file to. @return 0 if successful, -1 if the file was skipped, 1 on error. */ -int renameFile(std::string& path, const tm* tm); +int renameFile(std::string& path, const tm* tm, Exiv2::ExifData& exifData); /*! @brief Make a file path from the current file path, destination @@ -652,7 +652,7 @@ int Rename::run(const std::string& path) { std::cout << _("Updating timestamp to") << " " << v << std::endl; } } else { - rc = renameFile(newPath, &tm); + rc = renameFile(newPath, &tm, exifData); if (rc == -1) return 0; // skip } @@ -1821,12 +1821,18 @@ void replace(std::string& text, const std::string& searchText, const std::string } } -int renameFile(std::string& newPath, const tm* tm) { +int renameFile(std::string& newPath, const tm* tm, Exiv2::ExifData& exifData) { auto p = fs::path(newPath); std::string path = newPath; auto oldFsPath = fs::path(path); std::string format = Params::instance().format_; + std::string filename = p.stem().string(); + std::string basesuffix = ""; + const size_t pos = filename.find('.'); + if (pos != std::string::npos) + basesuffix = filename.substr(pos); replace(format, ":basename:", p.stem().string()); + replace(format, ":basesuffix:", basesuffix); replace(format, ":dirname:", p.parent_path().filename().string()); replace(format, ":parentname:", p.parent_path().parent_path().filename().string()); @@ -1837,7 +1843,52 @@ int renameFile(std::string& newPath, const tm* tm) { return 1; } - newPath = (p.parent_path() / (basename + p.extension().string())).string(); + // get parent path with separator + // for concatenation of new file name, concatenation operator of std::filesystem::path is not used: + // On MSYS2 UCRT64 the path separator to be used in terminal is slash, but as concatenation operator + // a back slash will be added. Rename works but with verbose a path with different operators will be shown. + const size_t len = p.parent_path().string().length(); + std::string parent_path_sep = ""; + if (len > 0) + parent_path_sep = newPath.substr(0, len + 1); + + newPath = parent_path_sep + std::string(basename) + p.extension().string(); + + // rename using exiv2 tags + // is done after calling setting date/time: the value retrieved from tag might include something like %Y, which then + // should not be replaced by year + std::regex format_regex(":{1}?(Exif\\..*?):{1}?"); +#if defined(_WIN32) + std::string illegalChars = "\\/:*?\"<>|"; +#else + std::string illegalChars = "/:"; +#endif + std::regex_token_iterator rend; + std::regex_token_iterator token(format.begin(), format.end(), format_regex); + while (token != rend) { + Exiv2::Internal::enforce(token->str().length() >= 2, "token too short"); + std::string tag = token->str().substr(1, token->str().length() - 2); + const auto key = exifData.findKey(Exiv2::ExifKey(tag)); + std::string val = ""; + if (key != exifData.end()) { + val = key->print(&exifData); + if (val.length() == 0) { + std::cerr << path << ": " << _("Warning: ") << tag << _(" is empty.") << std::endl; + } else { + // replace characters invalid in file name + for (std::string::iterator it = val.begin(); it < val.end(); ++it) { + bool found = illegalChars.find(*it) != std::string::npos; + if (found) { + *it = '_'; + } + } + } + } else { + std::cerr << path << ": " << _("Warning: ") << tag << _(" is not included.") << std::endl; + } + replace(newPath, *token++, val); + } + p = fs::path(newPath); if (p.parent_path() == oldFsPath.parent_path() && p.filename() == oldFsPath.filename()) { @@ -1858,8 +1909,7 @@ int renameFile(std::string& newPath, const tm* tm) { go = false; break; case Params::renamePolicy: - newPath = (p.parent_path() / (std::string(basename) + "_" + Exiv2::toString(seq++) + p.extension().string())) - .string(); + newPath = parent_path_sep + std::string(basename) + "_" + Exiv2::toString(seq++) + p.extension().string(); break; case Params::askPolicy: std::cout << Params::instance().progname() << ": " << _("File") << " `" << newPath << "' " @@ -1873,9 +1923,7 @@ int renameFile(std::string& newPath, const tm* tm) { case 'r': case 'R': fileExistsPolicy = Params::renamePolicy; - newPath = - (p.parent_path() / (std::string(basename) + "_" + Exiv2::toString(seq++) + p.extension().string())) - .string(); + newPath = parent_path_sep + std::string(basename) + "_" + Exiv2::toString(seq++) + p.extension().string(); break; default: // skip return -1; diff --git a/app/exiv2.cpp b/app/exiv2.cpp index ca8eafa92e..d37bf69cfb 100644 --- a/app/exiv2.cpp +++ b/app/exiv2.cpp @@ -335,6 +335,7 @@ void Params::help(std::ostream& os) const { << _(" -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") + << _(" :basesuffix: - suffix in original filename, starts with first dot and ends before 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") diff --git a/exiv2.md b/exiv2.md index 99bd373756..1be3a10f84 100644 --- a/exiv2.md +++ b/exiv2.md @@ -455,11 +455,13 @@ environment variable). The *fmt* string follows the definitions in date and time. In addition, the following special character sequences are also provided: -| Variable | Description | -|:------ |:---- | -| :basename: | Original filename without extension | -| :dirname: | Name of the directory holding the original file | -| :parentname: | Name of parent directory | +| Variable | Description | +|:------ |:---- | +| :basename: | Original filename without extension | +| :basesuffix: | Suffix in original filename, starts with first dot and ends before extension, e.g. PANO, MP, NIGHT added by Google Camera app | +| :dirname: | Name of the directory holding the original file | +| :parentname: | Name of parent directory | +| :*ExifTagName*: | Placeholder will be replaced by translated value of tag, characters not allowed in file name are replaced by underscore | The default *fmt* is %Y%m%d_%H%M%S @@ -491,6 +493,13 @@ exiv2.exe: File `./Stonehenge_16_Jul_2015.jpg' exists. [O]verwrite, [r]ename or Renaming file to ./Stonehenge_16_Jul_2015_1.jpg ``` +If the filename contains a suffix, which shall be included in new filename: +``` +$ exiv2 --verbose --rename '%d_%b_%Y:basesuffix:' Stonehenge.PANO.jpg +File 1/1: Stonehenge.PANO.jpg +Renaming file to '16_Jul_2015.PANO'.jpg +``` +
### **-a** *time*, **--adjust** *time* @@ -1861,3 +1870,4 @@ Exiv2 github contributors: https://github.com/Exiv2/exiv2/graphs/contributors | Exiv2 GitHub contributors | https://github.com/Exiv2/exiv2/graphs/contributors | [TOC](#TOC) + diff --git a/test/data/test_reference_files/exiv2-test.out b/test/data/test_reference_files/exiv2-test.out index b89a78907f..a35ef9b37b 100644 --- a/test/data/test_reference_files/exiv2-test.out +++ b/test/data/test_reference_files/exiv2-test.out @@ -140,6 +140,7 @@ Options: -r fmt Filename format for the 'rename' action. The format string follows strftime(3). The following keywords are also supported: :basename: - original filename without extension + :basesuffix: - suffix in original filename, starts with first dot and ends before extension :dirname: - name of the directory holding the original file :parentname: - name of parent directory Default 'fmt' is %Y%m%d_%H%M%S diff --git a/tests/bash_tests/test_rename.py b/tests/bash_tests/test_rename.py new file mode 100644 index 0000000000..8f7e16e07d --- /dev/null +++ b/tests/bash_tests/test_rename.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +import sys +from system_tests import CaseMeta, CopyFiles, CopyTmpFiles, DeleteFiles, path + +########################################################### +# rename with different formats +########################################################### + +infile ="_DSC8437.exv" +outfile = "_DSC8437_02_Sep_2018.exv" +renformat = ":basename:_%d_%b_%Y" + +@CopyTmpFiles("$data_path/" + infile) +@DeleteFiles("$tmp_path/" + outfile) +class Rename_dbY(metaclass=CaseMeta): + infilename = path("$tmp_path/" + infile) + outfilename = path("$tmp_path/" + outfile) + commands = [ + "$exiv2 --verbose --rename " + renformat + " " + infilename + ] + stdout = [ + """File 1/1: $infilename +Renaming file to $outfilename +""" + ] + stderr = [""] * len(commands) + retval = [0] * len(commands) + +########################################################### + +infile ="_DSC8437.exv" +outfile = "_DSC8437_2018-09-02-19-40.exv" +renformat = ":basename:_%Y-%m-%d-%H-%M" + +@CopyTmpFiles("$data_path/" + infile) +@DeleteFiles("$tmp_path/" + outfile) +class Rename_YmdHM(metaclass=CaseMeta): + infilename = path("$tmp_path/" + infile) + outfilename = path("$tmp_path/" + outfile) + commands = [ + "$exiv2 --verbose --rename " + renformat + " " + infilename + ] + stdout = [ + """File 1/1: $infilename +Renaming file to $outfilename +""" + ] + stderr = [""] * len(commands) + retval = [0] * len(commands) + +########################################################### + +infile ="_DSC8437.exv" +outfile = "_DSC8437_NIKON D850_46.0 mm.exv" +renformat = ":basename:_:Exif.Image.Model:_:Exif.Photo.FocalLengthIn35mmFilm:" + +@CopyTmpFiles("$data_path/" + infile) +@DeleteFiles("$tmp_path/" + outfile) +class Rename_ExifTags(metaclass=CaseMeta): + infilename = path("$tmp_path/" + infile) + outfilename = path("$tmp_path/" + outfile) + commands = [ + "$exiv2 --verbose --rename " + renformat + " " + infilename + ] + stdout = [ + """File 1/1: $infilename +Renaming file to $outfilename +""" + ] + stderr = [""] * len(commands) + retval = [0] * len(commands) + +########################################################### + +infile ="_DSC8437.exv" +if sys.platform == 'win32': + outfile = "_DSC8437_a_b_c_d_e_f_g_h_i.exv" +else: + outfile = "_DSC8437_a\\b_c_d*e?fh|i.exv" + +renformat = ":basename:_:Exif.Image.ImageDescription:" + +@CopyTmpFiles("$data_path/" + infile) +@DeleteFiles("$tmp_path/" + outfile) +class Rename_ExifTagsInvalidChar(metaclass=CaseMeta): + infilename = path("$tmp_path/" + infile) + outfilename = path("$tmp_path/" + outfile) + commands = [ + """$exiv2 -M"set Exif.Image.ImageDescription Ascii a\\b/c:d*e?fh|i" $infilename""", + "$exiv2 --grep Exif.Image.ImageDescription $infilename", + "$exiv2 --verbose --rename " + renformat + " " + infilename + ] + stdout = [ + "", + """Exif.Image.ImageDescription Ascii 18 a\\b/c:d*e?fh|i +""", + """File 1/1: $infilename +Renaming file to $outfilename +""" + ] + stderr = [""] * len(commands) + retval = [0] * len(commands) + +########################################################### +# rename with keeping suffix +########################################################### + +basename ="_DSC8437" +outfile = "02_Sep_2018.PANO.exv" +renformat = "%d_%b_%Y:basesuffix:" + +@CopyTmpFiles("$data_path/_DSC8437.exv") +@DeleteFiles("$tmp_path/" + outfile) +class Rename_basesuffix(metaclass=CaseMeta): + infilename1 = path("$tmp_path/" + basename + ".exv") + infilename2 = path("$tmp_path/" + basename + ".PANO.exv") + outfilename = path("$tmp_path/" + outfile) + commands = [ + # first command to prepare a file name with suffix + "$exiv2 --verbose --rename :basename:.PANO " + infilename1, + "$exiv2 --verbose --rename " + renformat + " " + infilename2 + ] + stdout = [ + """File 1/1: $infilename1 +Renaming file to $infilename2 +""", + """File 1/1: $infilename2 +Renaming file to $outfilename +""" + ] + stderr = [""] * len(commands) + retval = [0] * len(commands) + +########################################################### +# rename error: tag is not included +########################################################### + +infile ="_DSC8437.exv" +outfile = "_DSC8437_.exv" +renformat = ":basename:_:Exif.Image.ImageDescription:" + +@CopyTmpFiles("$data_path/" + infile) +@DeleteFiles("$tmp_path/" + outfile) +class Rename_TagNotIncluded(metaclass=CaseMeta): + infilename = path("$tmp_path/" + infile) + outfilename = path("$tmp_path/" + outfile) + commands = [ + "$exiv2 --verbose --rename " + renformat + " " + infilename + ] + stdout = [ + """File 1/1: $infilename +Renaming file to $outfilename +""" + ] + stderr = ["""$infilename: Warning: Exif.Image.ImageDescription is not included. +"""] + retval = [0] * len(commands) + +########################################################### +# rename error: invalid tag name +########################################################### + +infile ="_DSC8437.exv" +renformat = ":basename:_:Exif.Image.ImageDescript:" + +@CopyTmpFiles("$data_path/" + infile) +class Rename_InvalidTagName(metaclass=CaseMeta): + infilename = path("$tmp_path/" + infile) + commands = [ + "$exiv2 --verbose --rename " + renformat + " " + infilename + ] + stdout = [ + """File 1/1: $infilename +""" + ] + stderr = ["""Exiv2 exception in rename action for file $infilename: +Invalid tag name or ifdId `ImageDescript', ifdId 1 +"""] + retval = [1] * len(commands) + +########################################################### +# rename error: file contains no Exif data +########################################################### + +infile ="_DSC8437.exv" +outfile = "_DSC8437_.exv" +renformat = ":basename:_:Exif.Image.ImageDescription:" + +@CopyTmpFiles("$data_path/" + infile) +#@DeleteFiles("$tmp_path/" + outfile) +class Rename_NoExifData(metaclass=CaseMeta): + infilename = path("$tmp_path/" + infile) + outfilename = path("$tmp_path/" + outfile) + commands = [ + "$exiv2 --delete a " + infilename, + "$exiv2 --verbose --rename " + renformat + " " + infilename + ] + stdout = [ + "", + """File 1/1: $infilename +""" + ] + stderr = ["", + """$infilename: No Exif data found in the file +"""] + retval = [0, 253] +