rawTherapee/rtgui/thumbnail.cc
Niklas Haas 2101b846c3
Implement file sorting in thumbnail view (#6449)
* Use mtime as fallback timestamp for files without EXIF data

As suggested in #6449, with date-based sorting it can be useful to have
at least *some* sort of time-relevant information for EXIF-less files,
to prevent them from falling back to getting sorted alphabetically all
the time.

This commit simply defaults the file timestamp to the file's mtime as
returned by g_stat. For annoying reasons, it doesn't suffice to merely
forward the timestamp to the FileData structs - we also need to keep
track of it inside FilesData to cover the case of a file with 0 frames
in it.

* Add DateTime to Thumbnail

Putting it here facilitate easier sorting without having to re-construct
the DateTime on every comparison.

To simplify things moving forwards, use the Glib::DateTime struct right
away. This struct also contains timezone information, but we don't
currently care about timezone - so just use the local timezone as the
best approximation. (Nothing currently depends on getting the timezone
right, anyway)

In addition to the above, this commit also changes the logic to allow
generating datetime strings even for files with missing EXIF (which
makes sense as a result of the previous commit allowing the use of mtime
instead).

* Implement file sorting in thumbnail view

For simplicity, I decided to only implement the attributes that I could
verily easily reach from the existing metadata exported by Thumbnail.
Ideally, I would also like to be able to sort by "last modified" but I'm
not sure of the best way to reach this from this place in the code.

It's worth pointing out that, with the current implementation, the list
will not dynamically re-sort itself until you re-select the sorting
method - even if you make changes to the files that would otherwise
affect the sorting (e.g. changing the rank while sorting by rank). One
might even call this a feature, not a bug, since it prevents thumbnails
from moving around while you're trying to re-label them. You can always
re-select "sort by ..." from the context menu to force a re-sort.

Fixes #3317

Co-authored-by: Thanatomanic <6567747+Thanatomanic@users.noreply.github.com>
2023-01-02 21:27:12 +01:00

1202 lines
35 KiB
C++

/*
* This file is part of RawTherapee.
*
*
* RawTherapee is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* RawTherapee is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with RawTherapee. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef WIN32
#include <windows.h>
#endif
#include "cachemanager.h"
#include "multilangmgr.h"
#include "thumbnail.h"
#include <sstream>
#include <iomanip>
#include <cstdio>
#include <cstdlib>
#include "../rtengine/colortemp.h"
#include "../rtengine/imagedata.h"
#include "../rtengine/procparams.h"
#include "../rtengine/rtthumbnail.h"
#include <glib/gstdio.h>
#include <glibmm/timezone.h>
#include "../rtengine/dynamicprofile.h"
#include "../rtengine/profilestore.h"
#include "../rtengine/settings.h"
#include "../rtexif/rtexif.h"
#include "guiutils.h"
#include "batchqueue.h"
#include "extprog.h"
#include "md5helper.h"
#include "pathutils.h"
#include "paramsedited.h"
#include "procparamchangers.h"
using namespace rtengine::procparams;
Thumbnail::Thumbnail(CacheManager* cm, const Glib::ustring& fname, CacheImageData* cf) :
fname(fname),
cfs(*cf),
cachemgr(cm),
ref(1),
enqueueNumber(0),
tpp(nullptr),
pparams(new ProcParams),
pparamsValid(false),
imageLoading(false),
lastImg(nullptr),
lastW(0),
lastH(0),
lastScale(0),
initial_(false)
{
loadProcParams ();
// should be safe to use the unprotected version of loadThumbnail, since we are in the constructor
_loadThumbnail ();
generateExifDateTimeStrings ();
if (cfs.rankOld >= 0) {
// rank and inTrash were found in cache (old style), move them over to pparams
// 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
// TODO? should we call notifylisterners_procParamsChanged here?
setRank(cfs.rankOld);
setStage(cfs.inTrashOld);
}
delete tpp;
tpp = nullptr;
}
Thumbnail::Thumbnail(CacheManager* cm, const Glib::ustring& fname, const std::string& md5) :
fname(fname),
cachemgr(cm),
ref(1),
enqueueNumber(0),
tpp(nullptr),
pparams(new ProcParams),
pparamsValid(false),
imageLoading(false),
lastImg(nullptr),
lastW(0),
lastH(0),
lastScale(0.0),
initial_(true)
{
cfs.md5 = md5;
loadProcParams ();
_generateThumbnailImage ();
cfs.recentlySaved = false;
initial_ = false;
delete tpp;
tpp = nullptr;
}
void Thumbnail::_generateThumbnailImage ()
{
// delete everything loaded into memory
delete tpp;
tpp = nullptr;
delete [] lastImg;
lastImg = nullptr;
tw = options.maxThumbnailWidth;
th = options.maxThumbnailHeight;
imgRatio = -1.;
// generate thumbnail image
const std::string ext = getExtension(fname).lowercase();
if (ext.empty()) {
return;
}
cfs.supported = false;
cfs.exifValid = false;
cfs.timeValid = false;
if (ext == "jpg" || ext == "jpeg") {
infoFromImage (fname);
tpp = rtengine::Thumbnail::loadFromImage (fname, tw, th, -1, pparams->wb.equal);
if (tpp) {
cfs.format = FT_Jpeg;
}
} else if (ext == "png") {
tpp = rtengine::Thumbnail::loadFromImage (fname, tw, th, -1, pparams->wb.equal);
if (tpp) {
cfs.format = FT_Png;
}
} else if (ext == "tif" || ext == "tiff") {
infoFromImage (fname);
tpp = rtengine::Thumbnail::loadFromImage (fname, tw, th, -1, pparams->wb.equal);
if (tpp) {
cfs.format = FT_Tiff;
}
} else {
// RAW works like this:
// 1. if we are here it's because we aren't in the cache so load the JPG
// image out of the RAW. Mark as "quick".
// 2. if we don't find that then just grab the real image.
bool quick = false;
rtengine::RawMetaDataLocation ri;
rtengine::eSensorType sensorType = rtengine::ST_NONE;
if ( initial_ && options.internalThumbIfUntouched) {
quick = true;
tpp = rtengine::Thumbnail::loadQuickFromRaw (fname, ri, sensorType, tw, th, 1, TRUE);
}
if ( tpp == nullptr ) {
quick = false;
tpp = rtengine::Thumbnail::loadFromRaw (fname, ri, sensorType, tw, th, 1, pparams->wb.equal, TRUE);
}
cfs.sensortype = sensorType;
if (tpp) {
cfs.format = FT_Raw;
cfs.thumbImgType = quick ? CacheImageData::QUICK_THUMBNAIL : CacheImageData::FULL_THUMBNAIL;
infoFromImage (fname, std::unique_ptr<rtengine::RawMetaDataLocation>(new rtengine::RawMetaDataLocation(ri)));
}
}
if (tpp) {
tpp->getAutoWBMultipliers(cfs.redAWBMul, cfs.greenAWBMul, cfs.blueAWBMul);
_saveThumbnail ();
cfs.supported = true;
cfs.save (getCacheFileName ("data", ".txt"));
generateExifDateTimeStrings ();
}
}
bool Thumbnail::isSupported () const
{
return cfs.supported;
}
const ProcParams& Thumbnail::getProcParams ()
{
MyMutex::MyLock lock(mutex);
return getProcParamsU();
}
// Unprotected version of getProcParams, when
const ProcParams& Thumbnail::getProcParamsU ()
{
if (pparamsValid) {
return *pparams;
} else {
*pparams = *(ProfileStore::getInstance()->getDefaultProcParams (getType() == FT_Raw));
if (pparams->wb.method == "Camera") {
double ct;
getCamWB (ct, pparams->wb.green);
pparams->wb.temperature = ct;
} else if (pparams->wb.method == "autold") {
double ct;
getAutoWB (ct, pparams->wb.green, pparams->wb.equal, pparams->wb.tempBias);
pparams->wb.temperature = ct;
}
}
return *pparams; // there is no valid pp to return, but we have to return something
}
/** @brief Create default params on demand and returns a new updatable object
*
* The loaded profile may be partial, but it return a complete ProcParams (i.e. without ParamsEdited)
*
* @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.
*
* @return Return a pointer to a ProcPamas structure to be updated if returnParams is true and if everything went fine, NULL otherwise.
*/
rtengine::procparams::ProcParams* Thumbnail::createProcParamsForUpdate(bool returnParams, bool force, bool flaggingMode)
{
// try to load the last saved parameters from the cache or from the paramfile file
ProcParams* ldprof = nullptr;
Glib::ustring defProf = getType() == FT_Raw ? options.defProfRaw : options.defProfImg;
const CacheImageData* cfs = getCacheImageData();
Glib::ustring defaultPparamsPath = options.findProfilePath(defProf);
const bool create = (!hasProcParams() || force);
const bool run_cpb = !options.CPBPath.empty() && !defaultPparamsPath.empty() && cfs && cfs->exifValid && create;
const Glib::ustring outFName =
(options.paramsLoadLocation == PLL_Input && options.saveParamsFile) ?
fname + paramFileExtension :
getCacheFileName("profiles", paramFileExtension);
if (!run_cpb) {
if (defProf == DEFPROFILE_DYNAMIC && create && cfs && cfs->exifValid) {
rtengine::FramesMetaData* imageMetaData;
if (getType() == FT_Raw) {
// Should we ask all frame's MetaData ?
imageMetaData = rtengine::FramesMetaData::fromFile (fname, std::unique_ptr<rtengine::RawMetaDataLocation>(new rtengine::RawMetaDataLocation(rtengine::Thumbnail::loadMetaDataFromRaw(fname))), true);
} else {
// Should we ask all frame's MetaData ?
imageMetaData = rtengine::FramesMetaData::fromFile (fname, nullptr, true);
}
PartialProfile *pp = ProfileStore::getInstance()->loadDynamicProfile(imageMetaData, fname);
delete imageMetaData;
int err = pp->pparams->save(outFName);
pp->deleteInstance();
delete pp;
if (!err) {
loadProcParams();
}
} else if (create && defProf != DEFPROFILE_DYNAMIC) {
const PartialProfile *p = ProfileStore::getInstance()->getProfile(defProf);
if (p && !p->pparams->save(outFName)) {
loadProcParams();
}
}
} else {
// First generate the communication file, with general values and EXIF metadata
rtengine::FramesMetaData* imageMetaData;
if (getType() == FT_Raw) {
// Should we ask all frame's MetaData ?
imageMetaData = rtengine::FramesMetaData::fromFile (fname, std::unique_ptr<rtengine::RawMetaDataLocation>(new rtengine::RawMetaDataLocation(rtengine::Thumbnail::loadMetaDataFromRaw(fname))), true);
} else {
// Should we ask all frame's MetaData ?
imageMetaData = rtengine::FramesMetaData::fromFile (fname, nullptr, true);
}
static int index = 0; // Will act as unique identifier during the session
Glib::ustring tmpFileName( Glib::build_filename(options.cacheBaseDir, Glib::ustring::compose("CPB_temp_%1.txt", index++)) );
const rtexif::TagDirectory* exifDir = nullptr;
if (imageMetaData && (exifDir = imageMetaData->getRootExifData())) {
exifDir->CPBDump(tmpFileName, fname, outFName,
defaultPparamsPath == DEFPROFILE_INTERNAL ? DEFPROFILE_INTERNAL : Glib::build_filename(defaultPparamsPath, Glib::path_get_basename(defProf) + paramFileExtension),
cfs,
flaggingMode);
}
delete imageMetaData;
// For the filename etc. do NOT use streams, since they are not UTF8 safe
Glib::ustring cmdLine = options.CPBPath + Glib::ustring(" \"") + tmpFileName + Glib::ustring("\"");
if (rtengine::settings->verbose) {
printf("Custom profile builder's command line: %s\n", Glib::ustring(cmdLine).c_str());
}
bool success = ExtProgStore::spawnCommandSync (cmdLine);
// Now they SHOULD be there (and potentially "partial"), so try to load them and store it as a full procparam
if (success) {
loadProcParams();
}
g_remove (tmpFileName.c_str ());
}
if (returnParams && hasProcParams()) {
ldprof = new ProcParams ();
*ldprof = getProcParams ();
}
return ldprof;
}
void Thumbnail::notifylisterners_procParamsChanged(int whoChangedIt)
{
for (size_t i = 0; i < listeners.size(); i++) {
listeners[i]->procParamsChanged (this, whoChangedIt);
}
}
/*
* Load the procparams from the cache or from the sidecar file (priority set in
* the Preferences).
*
* The result is a complete ProcParams with default values merged with the values
* from the loaded ProcParams (sidecar or cache file).
*/
void Thumbnail::loadProcParams ()
{
MyMutex::MyLock lock(mutex);
pparamsValid = false;
pparams->setDefaults();
if (options.paramsLoadLocation == PLL_Input) {
// try to load it from params file next to the image file
const int ppres = pparams->load(fname + paramFileExtension);
pparamsValid = !ppres && pparams->ppVersion >= 220;
// if no success, try to load the cached version of the procparams
if (!pparamsValid) {
pparamsValid = !pparams->load(getCacheFileName("profiles", paramFileExtension));
}
} else {
// try to load it from cache
pparamsValid = !pparams->load(getCacheFileName("profiles", paramFileExtension));
// if no success, try to load it from params file next to the image file
if (!pparamsValid) {
const int ppres = pparams->load(fname + paramFileExtension);
pparamsValid = !ppres && pparams->ppVersion >= 220;
}
}
}
void Thumbnail::clearProcParams (int whoClearedIt)
{
/* Clarification on current "clear profile" functionality:
a. if rank/colorlabel/inTrash are NOT set,
the "clear profile" will delete the pp3 file (as before).
b. if any of the rank/colorlabel/inTrash ARE set,
the "clear profile" will lead to execution of ProcParams::setDefaults
(the CPB is NOT called) to set the params values and will preserve
rank/colorlabel/inTrash in the param file. */
{
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;
//TODO: run though customprofilebuilder?
// probably not as this is the only option to set param values to default
// reset the params to defaults
pparams->setDefaults();
// and restore rank and inTrash
setRank(rank);
pparamsValid = cfs.rating != rank;
setColorLabel(colorlabel);
setStage(inTrash);
// params could get validated by rank/inTrash values restored above
if (pparamsValid) {
updateCache();
} else {
// remove param file from cache
Glib::ustring fname_ = getCacheFileName ("profiles", paramFileExtension);
g_remove (fname_.c_str ());
// remove param file located next to the file
fname_ = fname + paramFileExtension;
g_remove (fname_.c_str ());
fname_ = removeExtension(fname) + paramFileExtension;
g_remove (fname_.c_str ());
if (cfs.format == FT_Raw && options.internalThumbIfUntouched && cfs.thumbImgType != CacheImageData::QUICK_THUMBNAIL) {
// regenerate thumbnail, ie load the quick thumb again. For the rare formats not supporting quick thumbs this will
// be a bit slow as a new full thumbnail will be generated unnecessarily, but currently there is no way to pre-check
// if the format supports quick thumbs.
initial_ = true;
_generateThumbnailImage();
initial_ = false;
}
}
} // end of mutex lock
for (size_t i = 0; i < listeners.size(); i++) {
listeners[i]->procParamsChanged (this, whoClearedIt);
}
}
bool Thumbnail::hasProcParams () const
{
return pparamsValid;
}
void Thumbnail::setProcParams (const ProcParams& pp, ParamsEdited* pe, int whoChangedIt, bool updateCacheNow, bool resetToDefault)
{
const bool needsReprocessing =
resetToDefault
|| pparams->toneCurve != pp.toneCurve
|| pparams->locallab != pp.locallab
|| pparams->labCurve != pp.labCurve
|| pparams->localContrast != pp.localContrast
|| pparams->rgbCurves != pp.rgbCurves
|| pparams->colorToning != pp.colorToning
|| pparams->vibrance != pp.vibrance
|| pparams->wb != pp.wb
|| pparams->colorappearance != pp.colorappearance
|| pparams->epd != pp.epd
|| pparams->fattal != pp.fattal
|| pparams->sh != pp.sh
|| pparams->crop != pp.crop
|| pparams->coarse != pp.coarse
|| pparams->commonTrans != pp.commonTrans
|| pparams->rotate != pp.rotate
|| pparams->distortion != pp.distortion
|| pparams->lensProf != pp.lensProf
|| pparams->perspective != pp.perspective
|| pparams->gradient != pp.gradient
|| pparams->pcvignette != pp.pcvignette
|| pparams->cacorrection != pp.cacorrection
|| pparams->vignetting != pp.vignetting
|| pparams->chmixer != pp.chmixer
|| pparams->blackwhite != pp.blackwhite
|| pparams->icm != pp.icm
|| pparams->hsvequalizer != pp.hsvequalizer
|| pparams->filmSimulation != pp.filmSimulation
|| pparams->softlight != pp.softlight
|| pparams->dehaze != pp.dehaze
|| pparams->filmNegative != pp.filmNegative
|| whoChangedIt == FILEBROWSER
|| whoChangedIt == BATCHEDITOR;
{
MyMutex::MyLock lock(mutex);
if (*pparams != pp) {
cfs.recentlySaved = false;
} else if (pparamsValid && !updateCacheNow) {
// nothing to do
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 {
*pparams = pp;
}
pparamsValid = true;
setRank(rank);
setColorLabel(colorlabel);
setStage(inTrash);
if (updateCacheNow) {
updateCache();
}
} // end of mutex lock
if (needsReprocessing) {
for (size_t i = 0; i < listeners.size(); i++) {
listeners[i]->procParamsChanged (this, whoChangedIt);
}
}
}
bool Thumbnail::isRecentlySaved () const
{
return cfs.recentlySaved;
}
void Thumbnail::imageDeveloped ()
{
cfs.recentlySaved = true;
cfs.save (getCacheFileName ("data", ".txt"));
if (options.saveParamsCache) {
pparams->save (getCacheFileName ("profiles", paramFileExtension));
}
}
void Thumbnail::imageEnqueued ()
{
enqueueNumber++;
}
void Thumbnail::imageRemovedFromQueue ()
{
enqueueNumber--;
}
bool Thumbnail::isEnqueued () const
{
return enqueueNumber > 0;
}
bool Thumbnail::isPixelShift () const
{
return cfs.isPixelShift;
}
bool Thumbnail::isHDR () const
{
return cfs.isHDR;
}
void Thumbnail::increaseRef ()
{
MyMutex::MyLock lock(mutex);
++ref;
}
void Thumbnail::decreaseRef ()
{
{
MyMutex::MyLock lock(mutex);
if ( ref == 0 ) {
return;
}
if ( --ref != 0 ) {
return;
}
}
cachemgr->closeThumbnail (this);
}
void Thumbnail::getThumbnailSize(int &w, int &h, const rtengine::procparams::ProcParams *pparams)
{
MyMutex::MyLock lock(mutex);
int tw_ = tw;
int th_ = th;
float imgRatio_ = imgRatio;
if (pparams) {
int ppCoarse = pparams->coarse.rotate;
if (ppCoarse >= 180) {
ppCoarse -= 180;
}
int thisCoarse = this->pparams->coarse.rotate;
if (thisCoarse >= 180) {
thisCoarse -= 180;
}
if (thisCoarse != ppCoarse) {
// different orientation -> swapping width & height
std::swap(th_, tw_);
if (imgRatio_ >= 0.0001f) {
imgRatio_ = 1.f / imgRatio_;
}
}
}
if (imgRatio_ > 0.) {
w = imgRatio_ * static_cast<float>(h);
} else {
w = tw_ * h / th_;
}
if (w > options.maxThumbnailWidth) {
const float s = static_cast<float>(options.maxThumbnailWidth) / w;
w = options.maxThumbnailWidth;
h = std::max<int>(h * s, 1);
}
}
void Thumbnail::getFinalSize (const rtengine::procparams::ProcParams& pparams, int& w, int& h)
{
MyMutex::MyLock lock(mutex);
// WARNING: When downscaled, the ratio have loosed a lot of precision, so we can't get back the exact initial dimensions
double fw = lastW * lastScale;
double fh = lastH * lastScale;
if (pparams.coarse.rotate == 90 || pparams.coarse.rotate == 270) {
fh = lastW * lastScale;
fw = lastH * lastScale;
}
if (!pparams.resize.enabled) {
w = fw;
h = fh;
} else {
w = (int)(fw + 0.5);
h = (int)(fh + 0.5);
}
}
void Thumbnail::getOriginalSize (int& w, int& h) const
{
w = tw;
h = th;
}
rtengine::IImage8* Thumbnail::processThumbImage (const rtengine::procparams::ProcParams& pparams, int h, double& scale)
{
MyMutex::MyLock lock(mutex);
if ( tpp == nullptr ) {
_loadThumbnail();
if ( tpp == nullptr ) {
return nullptr;
}
}
rtengine::IImage8* image = nullptr;
if ( cfs.thumbImgType == CacheImageData::QUICK_THUMBNAIL ) {
// RAW internal thumbnail, no profile yet: just do some rotation etc.
image = tpp->quickProcessImage (pparams, h, rtengine::TI_Nearest);
} else {
// Full thumbnail: apply profile
// image = tpp->processImage (pparams, h, rtengine::TI_Bilinear, cfs.getCamera(), cfs.focalLen, cfs.focalLen35mm, cfs.focusDist, cfs.shutter, cfs.fnumber, cfs.iso, cfs.expcomp, scale );
image = tpp->processImage (pparams, static_cast<rtengine::eSensorType>(cfs.sensortype), h, rtengine::TI_Bilinear, &cfs, scale );
}
tpp->getDimensions(lastW, lastH, lastScale);
delete tpp;
tpp = nullptr;
return image;
}
rtengine::IImage8* Thumbnail::upgradeThumbImage (const rtengine::procparams::ProcParams& pparams, int h, double& scale)
{
MyMutex::MyLock lock(mutex);
if ( cfs.thumbImgType != CacheImageData::QUICK_THUMBNAIL ) {
return nullptr;
}
_generateThumbnailImage();
if ( tpp == nullptr ) {
return nullptr;
}
// rtengine::IImage8* image = tpp->processImage (pparams, h, rtengine::TI_Bilinear, cfs.getCamera(), cfs.focalLen, cfs.focalLen35mm, cfs.focusDist, cfs.shutter, cfs.fnumber, cfs.iso, cfs.expcomp, scale );
rtengine::IImage8* image = tpp->processImage (pparams, static_cast<rtengine::eSensorType>(cfs.sensortype), h, rtengine::TI_Bilinear, &cfs, scale );
tpp->getDimensions(lastW, lastH, lastScale);
delete tpp;
tpp = nullptr;
return image;
}
void Thumbnail::generateExifDateTimeStrings ()
{
if (cfs.timeValid) {
std::string dateFormat = options.dateFormat;
std::ostringstream ostr;
bool spec = false;
for (size_t i = 0; i < dateFormat.size(); i++)
if (spec && dateFormat[i] == 'y') {
ostr << cfs.year;
spec = false;
} else if (spec && dateFormat[i] == 'm') {
ostr << (int)cfs.month;
spec = false;
} else if (spec && dateFormat[i] == 'd') {
ostr << (int)cfs.day;
spec = false;
} else if (dateFormat[i] == '%') {
spec = true;
} else {
ostr << (char)dateFormat[i];
spec = false;
}
ostr << " " << (int)cfs.hour;
ostr << ":" << std::setw(2) << std::setfill('0') << (int)cfs.min;
ostr << ":" << std::setw(2) << std::setfill('0') << (int)cfs.sec;
dateTimeString = ostr.str ();
dateTime = Glib::DateTime::create_local(cfs.year, cfs.month, cfs.day,
cfs.hour, cfs.min, cfs.sec);
}
if (!dateTime.gobj() || !cfs.timeValid) {
dateTimeString = "";
dateTime = Glib::DateTime::create_now_utc(0);
}
if (!cfs.exifValid) {
exifString = "";
return;
}
exifString = Glib::ustring::compose ("f/%1 %2s %3%4 %5mm", Glib::ustring(rtengine::FramesData::apertureToString(cfs.fnumber)), Glib::ustring(rtengine::FramesData::shutterToString(cfs.shutter)), M("QINFO_ISO"), cfs.iso, Glib::ustring::format(std::setw(3), std::fixed, std::setprecision(2), cfs.focalLen));
if (options.fbShowExpComp && cfs.expcomp != "0.00" && !cfs.expcomp.empty()) { // don't show exposure compensation if it is 0.00EV;old cache files do not have ExpComp, so value will not be displayed.
exifString = Glib::ustring::compose ("%1 %2EV", exifString, cfs.expcomp); // append exposure compensation to exifString
}
}
const Glib::ustring& Thumbnail::getExifString () const
{
return exifString;
}
const Glib::ustring& Thumbnail::getDateTimeString () const
{
return dateTimeString;
}
const Glib::DateTime& Thumbnail::getDateTime () const
{
return dateTime;
}
void Thumbnail::getAutoWB (double& temp, double& green, double equal, double tempBias)
{
if (cfs.redAWBMul != -1.0) {
rtengine::ColorTemp ct(cfs.redAWBMul, cfs.greenAWBMul, cfs.blueAWBMul, equal);
temp = ct.getTemp();
green = ct.getGreen();
} else {
temp = green = -1.0;
}
}
ThFileType Thumbnail::getType () const
{
return (ThFileType) cfs.format;
}
int Thumbnail::infoFromImage (const Glib::ustring& fname, std::unique_ptr<rtengine::RawMetaDataLocation> rml)
{
rtengine::FramesMetaData* idata = rtengine::FramesMetaData::fromFile (fname, std::move(rml));
if (!idata) {
return 0;
}
int deg = 0;
cfs.timeValid = false;
cfs.exifValid = false;
if (idata->getDateTimeAsTS() > 0) {
cfs.year = 1900 + idata->getDateTime().tm_year;
cfs.month = idata->getDateTime().tm_mon + 1;
cfs.day = idata->getDateTime().tm_mday;
cfs.hour = idata->getDateTime().tm_hour;
cfs.min = idata->getDateTime().tm_min;
cfs.sec = idata->getDateTime().tm_sec;
cfs.timeValid = true;
}
if (idata->hasExif()) {
cfs.shutter = idata->getShutterSpeed ();
cfs.fnumber = idata->getFNumber ();
cfs.focalLen = idata->getFocalLen ();
cfs.focalLen35mm = idata->getFocalLen35mm ();
cfs.focusDist = idata->getFocusDist ();
cfs.iso = idata->getISOSpeed ();
cfs.expcomp = idata->expcompToString (idata->getExpComp(), false); // do not mask Zero expcomp
cfs.isHDR = idata->getHDR ();
cfs.isPixelShift = idata->getPixelShift ();
cfs.frameCount = idata->getFrameCount ();
cfs.sampleFormat = idata->getSampleFormat ();
cfs.lens = idata->getLens();
cfs.camMake = idata->getMake();
cfs.camModel = idata->getModel();
cfs.rating = idata->getRating();
cfs.exifValid = true;
if (idata->getOrientation() == "Rotate 90 CW") {
deg = 90;
} else if (idata->getOrientation() == "Rotate 180") {
deg = 180;
} else if (idata->getOrientation() == "Rotate 270 CW") {
deg = 270;
}
} else {
cfs.lens = "Unknown";
cfs.camMake = "Unknown";
cfs.camModel = "Unknown";
}
// get image filetype
std::string::size_type idx;
idx = fname.rfind('.');
if(idx != std::string::npos) {
cfs.filetype = fname.substr(idx + 1);
} else {
cfs.filetype = "";
}
delete idata;
return deg;
}
/*
* Read all thumbnail's data from the cache; build and save them if doesn't exist - NON PROTECTED
* This includes:
* - image's bitmap (*.rtti)
* - auto exposure's histogram (full thumbnail only)
* - embedded profile (full thumbnail only)
* - LiveThumbData section of the data file
*/
void Thumbnail::_loadThumbnail(bool firstTrial)
{
tw = -1;
th = options.maxThumbnailHeight;
delete tpp;
tpp = new rtengine::Thumbnail ();
tpp->isRaw = (cfs.format == (int) FT_Raw);
// load supplementary data
bool succ = tpp->readData (getCacheFileName ("data", ".txt"));
if (succ) {
tpp->getAutoWBMultipliers(cfs.redAWBMul, cfs.greenAWBMul, cfs.blueAWBMul);
}
// thumbnail image
succ = succ && tpp->readImage (getCacheFileName ("images", ""));
if (!succ && firstTrial) {
_generateThumbnailImage ();
if (cfs.supported) {
_loadThumbnail (false);
}
if (tpp == nullptr) {
return;
}
} else if (!succ) {
delete tpp;
tpp = nullptr;
return;
}
if ( cfs.thumbImgType == CacheImageData::FULL_THUMBNAIL ) {
// load embedded profile
tpp->readEmbProfile (getCacheFileName ("embprofiles", ".icc"));
tpp->init ();
}
if (!initial_) {
tw = tpp->getImageWidth (getProcParamsU(), th, imgRatio); // this might return 0 if image was just building
}
}
/*
* Save thumbnail's data to the cache - NON PROTECTED
* This includes:
* - image's bitmap (*.rtti)
* - auto exposure's histogram (full thumbnail only)
* - embedded profile (full thumbnail only)
* - LiveThumbData section of the data file
*/
void Thumbnail::_saveThumbnail ()
{
if (!tpp) {
return;
}
g_remove (getCacheFileName ("images", ".rtti").c_str ());
// save thumbnail image
tpp->writeImage (getCacheFileName ("images", ""));
// save embedded profile
tpp->writeEmbProfile (getCacheFileName ("embprofiles", ".icc"));
// save supplementary data
tpp->writeData (getCacheFileName ("data", ".txt"));
}
/*
* Save thumbnail's data to the cache - MUTEX PROTECTED
* This includes:
* - image's bitmap (*.rtti)
* - auto exposure's histogram (full thumbnail only)
* - embedded profile (full thumbnail only)
* - LiveThumbData section of the data file
*/
void Thumbnail::saveThumbnail ()
{
MyMutex::MyLock lock(mutex);
_saveThumbnail();
}
/*
* Update the cached files
* - updatePParams==true (default) : write the procparams file (sidecar or cache, depending on the options)
* - updateCacheImageData==true (default) : write the CacheImageData values in the cache folder,
* i.e. some General, DateTime, ExifInfo, File info and ExtraRawInfo,
*/
void Thumbnail::updateCache (bool updatePParams, bool updateCacheImageData)
{
if (updatePParams && pparamsValid) {
pparams->save (
options.saveParamsFile ? fname + paramFileExtension : "",
options.saveParamsCache ? getCacheFileName ("profiles", paramFileExtension) : "",
true
);
}
if (updateCacheImageData) {
cfs.save (getCacheFileName ("data", ".txt"));
}
}
Thumbnail::~Thumbnail ()
{
mutex.lock();
delete [] lastImg;
delete tpp;
mutex.unlock();
}
Glib::ustring Thumbnail::getCacheFileName (const Glib::ustring& subdir, const Glib::ustring& fext) const
{
return cachemgr->getCacheFileName (subdir, fname, fext, cfs.md5);
}
void Thumbnail::setFileName (const Glib::ustring &fn)
{
fname = fn;
cfs.md5 = ::getMD5 (fname);
}
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;
}
}
void Thumbnail::setRank (int rank)
{
pparams->rank = rank;
pparamsValid = true;
}
int Thumbnail::getColorLabel () const
{
return pparams->colorlabel;
}
void Thumbnail::setColorLabel (int colorlabel)
{
if (pparams->colorlabel != colorlabel) {
pparams->colorlabel = colorlabel;
pparamsValid = true;
}
}
int Thumbnail::getStage () const
{
return pparams->inTrash;
}
void Thumbnail::setStage (bool stage)
{
if (pparams->inTrash != stage) {
pparams->inTrash = stage;
pparamsValid = true;
}
}
void Thumbnail::addThumbnailListener (ThumbnailListener* tnl)
{
increaseRef();
listeners.push_back (tnl);
}
void Thumbnail::removeThumbnailListener (ThumbnailListener* tnl)
{
std::vector<ThumbnailListener*>::iterator f = std::find (listeners.begin(), listeners.end(), tnl);
if (f != listeners.end()) {
listeners.erase (f);
decreaseRef();
}
}
// Calculates the standard filename for the automatically named batch result
// and opens it in OS default viewer
// destination: 1=Batch conf. file; 2=batch out dir; 3=RAW dir
// Return: Success?
bool Thumbnail::openDefaultViewer(int destination)
{
#ifdef WIN32
Glib::ustring openFName;
if (destination == 1) {
openFName = Glib::ustring::compose ("%1.%2", BatchQueue::calcAutoFileNameBase(fname), options.saveFormatBatch.format);
if (Glib::file_test (openFName, Glib::FILE_TEST_EXISTS)) {
wchar_t *wfilename = (wchar_t*)g_utf8_to_utf16 (openFName.c_str(), -1, NULL, NULL, NULL);
ShellExecuteW(NULL, L"open", wfilename, NULL, NULL, SW_SHOWMAXIMIZED );
g_free(wfilename);
} else {
printf("%s not found\n", openFName.data());
return false;
}
} else {
openFName = destination == 3 ? fname
: Glib::ustring::compose ("%1.%2", BatchQueue::calcAutoFileNameBase(fname), options.saveFormatBatch.format);
printf("Opening %s\n", openFName.c_str());
if (Glib::file_test (openFName, Glib::FILE_TEST_EXISTS)) {
// Output file exists, so open explorer and select output file
wchar_t* org = (wchar_t*)g_utf8_to_utf16 (Glib::ustring::compose("/select,\"%1\"", openFName).c_str(), -1, NULL, NULL, NULL);
wchar_t* par = new wchar_t[wcslen(org) + 1];
wcscpy(par, org);
// In this case the / disturbs
wchar_t* p = par + 1; // skip the first backslash
while (*p != 0) {
if (*p == L'/') {
*p = L'\\';
}
p++;
}
ShellExecuteW(NULL, L"open", L"explorer.exe", par, NULL, SW_SHOWNORMAL );
delete[] par;
g_free(org);
} else if (Glib::file_test (Glib::path_get_dirname(openFName), Glib::FILE_TEST_EXISTS)) {
// Out file does not exist, but directory
wchar_t *wfilename = (wchar_t*)g_utf8_to_utf16 (Glib::path_get_dirname(openFName).c_str(), -1, NULL, NULL, NULL);
ShellExecuteW(NULL, L"explore", wfilename, NULL, NULL, SW_SHOWNORMAL );
g_free(wfilename);
} else {
printf("File and dir not found\n");
return false;
}
}
return true;
#else
// TODO: Add more OSes here
printf("Automatic opening not supported on this OS\n");
return false;
#endif
}
bool Thumbnail::imageLoad(bool loading)
{
MyMutex::MyLock lock(mutex);
bool previous = imageLoading;
if( loading && !previous ) {
imageLoading = true;
return true;
} else if( !loading ) {
imageLoading = false;
}
return false;
}
void Thumbnail::getCamWB(double& temp, double& green) const
{
if (tpp) {
tpp->getCamWB (temp, green);
} else {
temp = green = -1.0;
}
}
void Thumbnail::getSpotWB(int x, int y, int rect, double& temp, double& green)
{
if (tpp) {
tpp->getSpotWB (getProcParams(), x, y, rect, temp, green);
} else {
temp = green = -1.0;
}
}
void Thumbnail::applyAutoExp (rtengine::procparams::ProcParams& pparams)
{
if (tpp) {
tpp->applyAutoExp (pparams);
}
}
const CacheImageData* Thumbnail::getCacheImageData() const
{
return &cfs;
}
std::string Thumbnail::getMD5() const
{
return cfs.md5;
}
bool Thumbnail::isQuick() const
{
return cfs.thumbImgType == CacheImageData::QUICK_THUMBNAIL;
}
bool Thumbnail::isPParamsValid() const
{
return pparamsValid;
}