Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename to allow all exiv2 tags as placeholders #2791

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 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 << '\n';
}
} else {
rc = renameFile(newPath, &tm);
rc = renameFile(newPath, &tm, exifData);
if (rc == -1)
return 0; // skip
}
Expand Down Expand Up @@ -1821,7 +1821,7 @@ 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);
Expand All @@ -1843,7 +1843,51 @@ 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.
int len = p.parent_path().string().length();
std::string parent_path_sep = "";
if (len > 0)
parent_path_sep = newPath.substr(0, ++len);

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) {
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 @@ -1864,8 +1908,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 @@ -1879,9 +1922,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
13 changes: 7 additions & 6 deletions exiv2.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,12 +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 |
| :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 |
| 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
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