Skip to content
Merged
66 changes: 57 additions & 9 deletions app/actions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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());

Expand All @@ -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<std::string::iterator> rend;
std::regex_token_iterator<std::string::iterator> token(format.begin(), format.end(), format_regex);
while (token != rend) {
Exiv2::Internal::enforce<std::overflow_error>(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()) {
Expand All @@ -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 << "' "
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions app/exiv2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
20 changes: 15 additions & 5 deletions exiv2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
```

<div id="adjust_time">

### **-a** *time*, **--adjust** *time*
Expand Down Expand Up @@ -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)

1 change: 1 addition & 0 deletions test/data/test_reference_files/exiv2-test.out
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
208 changes: 208 additions & 0 deletions tests/bash_tests/test_rename.py
Original file line number Diff line number Diff line change
@@ -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?f<g>h|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?f<g>h|i" $infilename""",
"$exiv2 --grep Exif.Image.ImageDescription $infilename",
"$exiv2 --verbose --rename " + renformat + " " + infilename
]
stdout = [
"",
"""Exif.Image.ImageDescription Ascii 18 a\\b/c:d*e?f<g>h|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]

Loading