Merge pull request #6988 from sgotti/add_optional_thumbnail_rank_color_load_save_from_to_xmp_sidecar

Add optional image rank/color load/save from/to xmp sidecar
This commit is contained in:
Lawrence37 2024-05-11 21:45:00 -07:00 committed by GitHub
commit bfc94310bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 259 additions and 78 deletions

View File

@ -2090,6 +2090,7 @@ PREFERENCES_THUMBNAIL_INSPECTOR_JPEG;Embedded JPEG preview
PREFERENCES_THUMBNAIL_INSPECTOR_MODE;Image to show
PREFERENCES_THUMBNAIL_INSPECTOR_RAW;Neutral raw rendering
PREFERENCES_THUMBNAIL_INSPECTOR_RAW_IF_NO_JPEG_FULLSIZE;Embedded JPEG if fullsize, neutral raw otherwise
PREFERENCES_THUMBNAIL_RANK_COLOR_MODE;Load/Save thumbnail rank and color from/to XMP sidecars
PREFERENCES_TOOLPANEL_AVAILABLETOOLS;Available Tools
PREFERENCES_TOOLPANEL_CLONE_FAVORITES;Keep favorite tools in original locations
PREFERENCES_TOOLPANEL_CLONE_FAVORITES_TOOLTIP;If set, favorite tools will appear in both the favorites tab and their original tabs.\n\nNote: Enabling this option may result in a slight delay when switching tabs.

View File

@ -106,19 +106,6 @@ void clear_metadata_key(Data &data, const Key &key)
}
}
template <typename Iterator, typename Integer = std::size_t>
auto to_long(const Iterator &iter, Integer n = Integer{0}) -> decltype(
#if EXIV2_TEST_VERSION(0,28,0)
iter->toInt64()
) {
return iter->toInt64(n);
#else
iter->toLong()
) {
return iter->toLong(n);
#endif
}
} // namespace

View File

@ -97,4 +97,18 @@ private:
static std::unique_ptr<ImageCache> cache_;
};
template <typename Iterator, typename Integer = std::size_t>
auto to_long(const Iterator &iter, Integer n = Integer{0}) -> decltype(
#if EXIV2_TEST_VERSION(0,28,0)
iter->toInt64()
) {
return iter->toInt64(n);
#else
iter->toLong()
) {
return iter->toLong(n);
#endif
}
} // namespace rtengine

View File

@ -547,13 +547,13 @@ void FileBrowser::rightClicked ()
untrash->set_sensitive (false);
for (size_t i = 0; i < selected.size(); i++)
if ((static_cast<FileBrowserEntry*>(selected[i]))->thumbnail->getStage()) {
if ((static_cast<FileBrowserEntry*>(selected[i]))->thumbnail->getTrashed()) {
untrash->set_sensitive (true);
break;
}
for (size_t i = 0; i < selected.size(); i++)
if (!(static_cast<FileBrowserEntry*>(selected[i]))->thumbnail->getStage()) {
if (!(static_cast<FileBrowserEntry*>(selected[i]))->thumbnail->getTrashed()) {
trash->set_sensitive (true);
break;
}
@ -649,7 +649,7 @@ void FileBrowser::addEntry_ (FileBrowserEntry* entry)
entry->addButtonSet(new FileThumbnailButtonSet(entry));
entry->getThumbButtonSet()->setRank(entry->thumbnail->getRank());
entry->getThumbButtonSet()->setColorLabel(entry->thumbnail->getColorLabel());
entry->getThumbButtonSet()->setInTrash(entry->thumbnail->getStage());
entry->getThumbButtonSet()->setInTrash(entry->thumbnail->getTrashed());
entry->getThumbButtonSet()->setButtonListener(this);
entry->resize(getThumbnailHeight());
entry->filtered = !checkFilter(entry);
@ -1023,12 +1023,12 @@ void FileBrowser::menuItemActivated (Gtk::MenuItem* m)
const auto thumbnail = mselected[i]->thumbnail;
const auto rank = thumbnail->getRank();
const auto colorLabel = thumbnail->getColorLabel();
const auto stage = thumbnail->getStage();
const auto stage = thumbnail->getTrashed();
thumbnail->createProcParamsForUpdate (false, true);
thumbnail->setRank(rank);
thumbnail->setColorLabel(colorLabel);
thumbnail->setStage(stage);
thumbnail->setTrashed(stage);
// Empty run to update the thumb
rtengine::procparams::ProcParams params = thumbnail->getProcParams ();
@ -1558,8 +1558,8 @@ bool FileBrowser::checkFilter (ThumbBrowserEntryBase* entryb) const // true ->
((entry->thumbnail->isRecentlySaved() && filter.showRecentlySaved[0]) && !filter.showRecentlySaved[1]) ||
((!entry->thumbnail->isRecentlySaved() && filter.showRecentlySaved[1]) && !filter.showRecentlySaved[0]) ||
(entry->thumbnail->getStage() && !filter.showTrash) ||
(!entry->thumbnail->getStage() && !filter.showNotTrash)) {
(entry->thumbnail->getTrashed() && !filter.showTrash) ||
(!entry->thumbnail->getTrashed() && !filter.showNotTrash)) {
return false;
}
@ -1625,11 +1625,11 @@ void FileBrowser::toTrashRequested (std::vector<FileBrowserEntry*> tbe)
// no need to notify listeners as item goes to trash, likely to be deleted
if (tbe[i]->thumbnail->getStage()) {
if (tbe[i]->thumbnail->getTrashed()) {
continue;
}
tbe[i]->thumbnail->setStage (true);
tbe[i]->thumbnail->setTrashed (true);
if (tbe[i]->getThumbButtonSet()) {
tbe[i]->getThumbButtonSet()->setRank (tbe[i]->thumbnail->getRank());
@ -1649,11 +1649,11 @@ void FileBrowser::fromTrashRequested (std::vector<FileBrowserEntry*> tbe)
for (size_t i = 0; i < tbe.size(); i++) {
// if thumbnail was marked inTrash=true then param file must be there, no need to run customprofilebuilder
if (!tbe[i]->thumbnail->getStage()) {
if (!tbe[i]->thumbnail->getTrashed()) {
continue;
}
tbe[i]->thumbnail->setStage (false);
tbe[i]->thumbnail->setTrashed (false);
if (tbe[i]->getThumbButtonSet()) {
tbe[i]->getThumbButtonSet()->setRank (tbe[i]->thumbnail->getRank());
@ -1784,7 +1784,7 @@ void FileBrowser::buttonPressed (LWButton* button, int actionCode, void* actionD
FileBrowserEntry* entry = static_cast<FileBrowserEntry*>(actionData);
tbe.push_back (entry);
if (!entry->thumbnail->getStage()) {
if (!entry->thumbnail->getTrashed()) {
toTrashRequested (tbe);
} else {
fromTrashRequested (tbe);

View File

@ -1910,7 +1910,7 @@ void FileCatalog::emptyTrash ()
std::vector<FileBrowserEntry*> toDel;
for (const auto entry : t) {
if ((static_cast<FileBrowserEntry*>(entry))->thumbnail->getStage() == 1) {
if ((static_cast<FileBrowserEntry*>(entry))->thumbnail->getTrashed()) {
toDel.push_back(static_cast<FileBrowserEntry*>(entry));
}
}
@ -1926,7 +1926,7 @@ bool FileCatalog::trashIsEmpty ()
const auto& t = fileBrowser->getEntries();
for (const auto entry : t) {
if ((static_cast<FileBrowserEntry*>(entry))->thumbnail->getStage() == 1) {
if ((static_cast<FileBrowserEntry*>(entry))->thumbnail->getTrashed()) {
return false;
}
}

View File

@ -695,6 +695,7 @@ void Options::setDefaults()
lastICCProfCreatorDir = "";
gimpPluginShowInfoDialog = true;
maxRecentFolders = 15;
thumbnailRankColorMode = Options::ThumbnailPropertyMode::PROCPARAMS;
sortMethod = SORT_BY_NAME;
sortDescending = false;
rtSettings.lensfunDbDirectory = ""; // set also in main.cc and main-cli.cc
@ -1367,6 +1368,15 @@ void Options::readFromFile(Glib::ustring fname)
if (keyFile.has_key("File Browser", "BrowseRecursiveFollowLinks")) {
browseRecursiveFollowLinks = keyFile.get_boolean("File Browser", "BrowseRecursiveFollowLinks");
}
if (keyFile.has_key("File Browser", "ThumbnailRankColorMode")) {
std::string val = keyFile.get_string("File Browser", "ThumbnailRankColorMode");
if (val == "xmp") {
thumbnailRankColorMode = ThumbnailPropertyMode::XMP;
} else {
thumbnailRankColorMode = ThumbnailPropertyMode::PROCPARAMS;
}
}
}
if (keyFile.has_group("Clipping Indication")) {
@ -2467,6 +2477,14 @@ void Options::saveToFile(Glib::ustring fname)
keyFile.set_string_list("File Browser", "RecentFolders", temp);
}
switch (thumbnailRankColorMode) {
case ThumbnailPropertyMode::XMP:
keyFile.set_string("File Browser", "ThumbnailRankColorMode", "xmp");
break;
default: // ThumbnailPropertyMode::PROCPARAMS
keyFile.set_string("File Browser", "ThumbnailRankColorMode", "procparams");
break;
}
keyFile.set_integer("File Browser", "SortMethod", sortMethod);
keyFile.set_boolean("File Browser", "SortDescending", sortDescending);
keyFile.set_boolean("File Browser", "BrowseRecursive", browseRecursive);

View File

@ -492,6 +492,12 @@ public:
size_t maxRecentFolders; // max. number of recent folders stored in options file
std::vector<Glib::ustring> recentFolders; // List containing all recent folders
enum class ThumbnailPropertyMode {
PROCPARAMS, // store rank and color in procparams sidecars
XMP // store rank and color xmp sidecar
};
ThumbnailPropertyMode thumbnailRankColorMode;
enum SortMethod {
SORT_BY_NAME,
SORT_BY_DATE,

View File

@ -1470,6 +1470,9 @@ Gtk::Widget* Preferences::getFileBrowserPanel()
vbro->pack_start(*sameThumbSize, Gtk::PACK_SHRINK, 0);
vbro->pack_start(*ckbInternalThumbIfUntouched, Gtk::PACK_SHRINK, 0);
thumbnailRankColorMode = Gtk::manage(new Gtk::CheckButton(M("PREFERENCES_THUMBNAIL_RANK_COLOR_MODE")));
vbro->pack_start(*thumbnailRankColorMode, Gtk::PACK_SHRINK, 0);
Gtk::Box* hbrecent = Gtk::manage(new Gtk::Box());
Gtk::Label* labrecent = Gtk::manage (new Gtk::Label (M("PREFERENCES_MAXRECENTFOLDERS") + ":", Gtk::ALIGN_START));
maxRecentFolders = Gtk::manage(new Gtk::SpinButton());
@ -2029,6 +2032,8 @@ void Preferences::storePreferences()
moptions.rtSettings.metadata_xmp_sync = rtengine::Settings::MetadataXmpSync(metadataSyncCombo->get_active_row_number());
moptions.rtSettings.xmp_sidecar_style = rtengine::Settings::XmpSidecarStyle(xmpSidecarCombo->get_active_row_number());
moptions.thumbnailRankColorMode = thumbnailRankColorMode->get_active() ? Options::ThumbnailPropertyMode::XMP : Options::ThumbnailPropertyMode::PROCPARAMS;
}
void Preferences::fillPreferences()
@ -2287,6 +2292,8 @@ void Preferences::fillPreferences()
metadataSyncCombo->set_active(int(moptions.rtSettings.metadata_xmp_sync));
xmpSidecarCombo->set_active(int(moptions.rtSettings.xmp_sidecar_style));
thumbnailRankColorMode->set_active(moptions.thumbnailRankColorMode == Options::ThumbnailPropertyMode::XMP);
}
/*

View File

@ -224,6 +224,8 @@ class Preferences final :
Gtk::CheckButton* ckbInternalThumbIfUntouched;
Gtk::CheckButton *thumbnailRankColorMode;
Gtk::Entry* txtCustProfBuilderPath;
Gtk::ComboBoxText* custProfBuilderLabelType;

View File

@ -105,6 +105,46 @@ bool CPBDump(
return true;
}
struct ColorMapper {
std::map<int, std::string> indexLabelMap;
std::map<std::string, int> labelIndexMap;
ColorMapper(std::map<int, std::string> colors) {
for (const auto& color: colors) {
indexLabelMap.insert({color.first, color.second});
labelIndexMap.insert({color.second, color.first});
}
}
int index(const std::string &label) const
{
auto it = labelIndexMap.find(label);
if (it != labelIndexMap.end()) {
return it->second;
}
return 0;
}
std::string label(int index) const
{
auto it = indexLabelMap.find(index);
if (it != indexLabelMap.end()) {
return it->second;
}
return "";
}
};
const std::map<int, std::string> defaultColors = {
{1, "Red"},
{2, "Yellow"},
{3, "Green"},
{4, "Blue"},
{5, "Purple"}
};
auto defaultColorMapper = ColorMapper(defaultColors);
} // namespace
using namespace rtengine::procparams;
@ -133,7 +173,7 @@ Thumbnail::Thumbnail(CacheManager* cm, const Glib::ustring& fname, CacheImageDat
generateExifDateTimeStrings ();
if (cfs.rankOld >= 0) {
// rank and inTrash were found in cache (old style), move them over to pparams
// rank and inTrash were found in cache (old style), move them over to pparams or xmp sidecar
// try to load the last saved parameters from the cache or from the paramfile file
createProcParamsForUpdate(false, false); // this can execute customprofilebuilder to generate param file
@ -141,9 +181,11 @@ Thumbnail::Thumbnail(CacheManager* cm, const Glib::ustring& fname, CacheImageDat
// TODO? should we call notifylisterners_procParamsChanged here?
setRank(cfs.rankOld);
setStage(cfs.inTrashOld);
setTrashed(cfs.inTrashOld);
}
loadProperties();
delete tpp;
tpp = nullptr;
}
@ -173,6 +215,8 @@ Thumbnail::Thumbnail(CacheManager* cm, const Glib::ustring& fname, const std::st
initial_ = false;
loadProperties();
delete tpp;
tpp = nullptr;
}
@ -306,7 +350,7 @@ const ProcParams& Thumbnail::getProcParamsU ()
* @param returnParams Ask to return a pointer to a ProcParams object if true
* @param force True if the profile has to be re-generated even if it already exists
* @param flaggingMode True if the ProcParams will be created because the file browser is being flagging an image
* (rang, to trash, color labels). This parameter is passed to the CPB.
* (rank, to trash, color labels). This parameter is passed to the CPB.
*
* @return Return a pointer to a ProcPamas structure to be updated if returnParams is true and if everything went fine, NULL otherwise.
*/
@ -441,12 +485,6 @@ void Thumbnail::clearProcParams (int whoClearedIt)
{
MyMutex::MyLock lock(mutex);
// preserve rank, colorlabel and inTrash across clear
int rank = getRank();
int colorlabel = getColorLabel();
int inTrash = getStage();
cfs.recentlySaved = false;
pparamsValid = false;
@ -456,13 +494,10 @@ void Thumbnail::clearProcParams (int whoClearedIt)
// reset the params to defaults
pparams->setDefaults();
// and restore rank and inTrash
setRank(rank);
pparamsValid = cfs.rating != rank;
setColorLabel(colorlabel);
setStage(inTrash);
// preserve rank, colorlabel and inTrash across clear
updateProcParamsProperties();
// params could get validated by rank/inTrash values restored above
// params could get validated by updateProcParamsProperties
if (pparamsValid) {
updateCache();
} else {
@ -560,11 +595,6 @@ void Thumbnail::setProcParams (const ProcParams& pp, ParamsEdited* pe, int whoCh
return;
}
// do not update rank, colorlabel and inTrash
const int rank = getRank();
const int colorlabel = getColorLabel();
const int inTrash = getStage();
if (pe) {
pe->combine(*pparams, pp, true);
} else {
@ -573,9 +603,8 @@ void Thumbnail::setProcParams (const ProcParams& pp, ParamsEdited* pe, int whoCh
pparamsValid = true;
setRank(rank);
setColorLabel(colorlabel);
setStage(inTrash);
// do not update rank, colorlabel and inTrash
updateProcParamsProperties();
if (updateCacheNow) {
updateCache();
@ -1045,6 +1074,7 @@ void Thumbnail::saveThumbnail ()
*/
void Thumbnail::updateCache (bool updatePParams, bool updateCacheImageData)
{
updateProcParamsProperties();
if (updatePParams && pparamsValid) {
pparams->save (
@ -1061,6 +1091,8 @@ void Thumbnail::updateCache (bool updatePParams, bool updateCacheImageData)
if (updatePParams && pparamsValid) {
saveMetadata();
}
saveXMPSidecarProperties();
}
Thumbnail::~Thumbnail ()
@ -1084,48 +1116,34 @@ void Thumbnail::setFileName (const Glib::ustring &fn)
cfs.md5 = ::getMD5 (fname);
}
int Thumbnail::getRank () const
int Thumbnail::getRank() const
{
// prefer the user-set rank over the embedded Rating
// pparams->rank == -1 means that there is no saved rank yet, so we should
// next look for the embedded Rating metadata.
if (pparams->rank != -1) {
return pparams->rank;
} else {
return cfs.rating;
}
return properties.rank;
}
void Thumbnail::setRank (int rank)
void Thumbnail::setRank(int rank)
{
pparams->rank = rank;
pparamsValid = true;
properties.rank = rank;
}
int Thumbnail::getColorLabel () const
int Thumbnail::getColorLabel() const
{
return pparams->colorlabel;
return properties.color;
}
void Thumbnail::setColorLabel (int colorlabel)
void Thumbnail::setColorLabel(int colorlabel)
{
if (pparams->colorlabel != colorlabel) {
pparams->colorlabel = colorlabel;
pparamsValid = true;
}
properties.color = colorlabel;
}
int Thumbnail::getStage () const
bool Thumbnail::getTrashed() const
{
return pparams->inTrash;
return properties.trashed;
}
void Thumbnail::setStage (bool stage)
void Thumbnail::setTrashed(bool trashed)
{
if (pparams->inTrash != stage) {
pparams->inTrash = stage;
pparamsValid = true;
}
properties.trashed = trashed;
}
void Thumbnail::addThumbnailListener (ThumbnailListener* tnl)
@ -1239,6 +1257,107 @@ void Thumbnail::getCamWB(double& temp, double& green, rtengine::StandardObserver
}
}
void Thumbnail::loadProperties()
{
properties = Properties();
// get initial rank from cache or image metadata
if (cfs.exifValid) {
properties.rank.value = rtengine::LIM(cfs.getRating(), 0, 5);
} else {
const std::unique_ptr<const rtengine::FramesMetaData> md(rtengine::FramesMetaData::fromFile(fname));
if (md && md->hasExif()) {
properties.rank.value = rtengine::LIM(md->getRating(), 0, 5);
}
}
// update rank and color from procparams or xmp sidecar
// load trash from procparams
if (pparamsValid) {
if (options.thumbnailRankColorMode == Options::ThumbnailPropertyMode::PROCPARAMS) {
if (pparams->rank >= 0) {
properties.rank.value = pparams->rank;
}
}
properties.trashed.value = pparams->inTrash;
properties.color.value = pparams->colorlabel;
}
if (options.thumbnailRankColorMode == Options::ThumbnailPropertyMode::XMP) {
try {
auto xmp = rtengine::Exiv2Metadata::getXmpSidecar(fname);
auto pos = xmp.findKey(Exiv2::XmpKey("Xmp.xmp.Rating"));
if (pos != xmp.end()) {
int r = rtengine::to_long(pos);
properties.rank.value = rtengine::LIM(r, 0, 5);
}
pos = xmp.findKey(Exiv2::XmpKey("Xmp.xmp.Label"));
if (pos != xmp.end()) {
properties.color.value = defaultColorMapper.index(pos->toString());
}
} catch (std::exception &exc) {
std::cerr << "ERROR loading thumbnail properties data from "
<< rtengine::Exiv2Metadata::xmpSidecarPath(fname)
<< ": " << exc.what() << std::endl;
}
}
}
void Thumbnail::updateProcParamsProperties()
{
if (!properties.edited()) {
return;
}
if (properties.trashed.edited && properties.trashed != pparams->inTrash) {
pparams->inTrash = properties.trashed;
pparamsValid = true;
}
// save procparams rank and color also when options.thumbnailRankColorMode == Options::ThumbnailPropertyMode::XMP
// so they'll be kept in sync
if (properties.rank.edited && properties.rank != pparams->rank) {
pparams->rank = properties.rank;
pparamsValid = true;
}
if (properties.color.edited && properties.color != pparams->colorlabel) {
pparams->colorlabel = properties.color;
pparamsValid = true;
}
}
void Thumbnail::saveXMPSidecarProperties()
{
if (!properties.edited()) {
return;
}
if (options.thumbnailRankColorMode != Options::ThumbnailPropertyMode::XMP) {
return;
}
auto fn = rtengine::Exiv2Metadata::xmpSidecarPath(fname);
try {
auto xmp = rtengine::Exiv2Metadata::getXmpSidecar(fname);
if (properties.rank.edited) {
xmp["Xmp.xmp.Rating"] = std::to_string(properties.rank);
}
if (properties.color.edited) {
xmp["Xmp.xmp.Label"] = defaultColorMapper.label(properties.color);
}
rtengine::Exiv2Metadata meta;
meta.xmpData() = std::move(xmp);
meta.saveToXmp(fn);
} catch (std::exception &exc) {
std::cerr << "ERROR saving thumbnail properties data to " << fn
<< ": " << exc.what() << std::endl;
}
}
void Thumbnail::saveMetadata()
{
if (options.rtSettings.metadata_xmp_sync != rtengine::Settings::MetadataXmpSync::READ_WRITE) {

View File

@ -78,6 +78,30 @@ class Thumbnail
bool initial_;
// Properties holds values and edited states for rank, color and trashed
struct Properties {
template <class T> struct Property {
T value;
bool edited;
Property(T v): value(v), edited(false) {}
Property& operator=(T v)
{
value = v;
edited = true;
return *this;
}
operator T() const { return value; }
};
Property<int> rank;
Property<int> color;
Property<bool> trashed;
explicit Properties(int r=0, int c=0, bool t=false):
rank(r), color(c), trashed(t) {}
bool edited() const { return rank.edited || color.edited || trashed.edited; }
};
Properties properties;
// vector of listeners
std::vector<ThumbnailListener*> listeners;
@ -90,6 +114,9 @@ class Thumbnail
Glib::ustring getCacheFileName (const Glib::ustring& subdir, const Glib::ustring& fext) const;
void saveMetadata();
void loadProperties();
void updateProcParamsProperties();
void saveXMPSidecarProperties();
public:
Thumbnail (CacheManager* cm, const Glib::ustring& fname, CacheImageData* cf);
@ -155,8 +182,8 @@ public:
int getColorLabel () const;
void setColorLabel (int colorlabel);
int getStage () const;
void setStage (bool stage);
bool getTrashed () const;
void setTrashed (bool trashed);
void addThumbnailListener (ThumbnailListener* tnl);
void removeThumbnailListener (ThumbnailListener* tnl);