diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml
index 5f447fe8a..5d36fc444 100644
--- a/.github/workflows/appimage.yml
+++ b/.github/workflows/appimage.yml
@@ -15,7 +15,7 @@ on:
workflow_dispatch:
env:
- publish_pre_dev_labels: '["Beep6581:wbrefinement"]'
+ publish_pre_dev_labels: '[]'
jobs:
build:
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index 2e1d3bc69..875dce60e 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -15,7 +15,7 @@ on:
workflow_dispatch:
env:
- publish_pre_dev_labels: '["Beep6581:wbrefinement"]'
+ publish_pre_dev_labels: '[]'
jobs:
build:
diff --git a/rtdata/languages/default b/rtdata/languages/default
index 75ccea391..d6db3f607 100644
--- a/rtdata/languages/default
+++ b/rtdata/languages/default
@@ -1946,6 +1946,7 @@ PREFERENCES_LENSFUNDBDIR;Lensfun database directory
PREFERENCES_LENSFUNDBDIR_TOOLTIP;Directory containing the Lensfun database. Leave empty to use the default directories.
PREFERENCES_LENSPROFILESDIR;Lens profiles directory
PREFERENCES_LENSPROFILESDIR_TOOLTIP;Directory containing Adobe Lens Correction Profiles (LCPs)
+PREFERENCES_MAX_ZOOM_TITLE;Maximum zoom
PREFERENCES_MAXRECENTFOLDERS;Maximum number of recent folders
PREFERENCES_MENUGROUPEXTPROGS;Group 'Open with'
PREFERENCES_MENUGROUPFILEOPERATIONS;Group 'File operations'
@@ -2108,7 +2109,26 @@ QUEUE_DESTPREVIEW_TOOLTIP;Destination path for the first selected image appears
QUEUE_FORMAT_TITLE;File Format
QUEUE_LOCATION_FOLDER;Save to folder
QUEUE_LOCATION_TEMPLATE;Use template
-QUEUE_LOCATION_TEMPLATE_TOOLTIP;Specify the output location based on the source photo's location, rank, trash status or position in the queue.\n\n%dN, %d-N, %pN, %p-N, %PN and %P-N (N = 1..9) will be replaced by elements of the image file's directory path (not including the file name):\n%dN = Nth directory from the end of the path\n%d-N = Nth directory from the start of the path\n%pN = all directories up to the Nth from the end of the path\n%p-N = the first N directories in the path\n%PN = the last N directories in the path\n%P-N = all directories from the Nth to the end of the path\n%f = base filename (no extension)\nFor Windows paths, %d-1 is the drive letter and colon, and %d-2 is the base directory on that drive.\n\nUsing the following pathname as an example:\n/home/tom/photos/2010-10-31/photo1.raw\nthe meaning of the formatting strings follows:\n%d4 = %d-1 = home\n%d3 = %d-2 = tom\n%d2 = %d-3 = photos\n%d1 = %d-4 = 2010-10-31\n%p1 = %p-4 = /home/tom/photos/2010-10-31/\n%p2 = %p-3 = /home/tom/photos/\n%p3 = %p-2 = /home/tom/\n%p4 = %p-1 = /home/\n%P1 = %P-4 = 2010-10-31/\n%P2 = %P-3 = photos/2010-10-31/\n%P3 = %P-2 = tom/photos/2010-10-31/\n%P4 = %P-1 = /home/tom/photos/2010-10-31/\n%f = photo1\n\n%r will be replaced by the photo's rank. If the photo is unranked, '0' is used. If the photo is in the trash, 'x' is used.\n\n%s1, ..., %s9 will be replaced by the photo's initial position in the queue at the time the queue is started. The number specifies the padding, e.g. %s3 results in '001'.\n\nIf you want to save the output image alongside the source image, write:\n%p1/%f\n\nIf you want to save the output image in a folder named 'converted' located in the source photo's folder, write:\n%p1/converted/%f\n\nIf you want to save the output image in\n'/home/tom/photos/converted/2010-10-31', write:\n%p-3/converted/%P-4/%f
+QUEUE_LOCATION_TEMPLATE_TOOLTIP;Specify the output location based on characteristics such as the source photo's location, rank, trash status or position in the queue.\n\nThe output template field value can include specifiers beginning with %, which are replaced by those characteristics in the actual destination path.\n\nPress the ? button for full instructions.
+QUEUE_LOCATION_TEMPLATE_HELP_BUTTON_TOOLTIP;Show or hide a help panel with instructions for creating location templates
+QUEUE_LOCATION_TEMPLATE_HELP_TITLE;Creating an output template
+QUEUE_LOCATION_TEMPLATE_HELP_INTRO;The output template field allows you to to dynamically customize the destination folder and filename. When you include certain specifiers, which begin with %, they are replaced by the program when each file is being saved.\n\nThe sections below describe each type of specifier.
+QUEUE_LOCATION_TEMPLATE_HELP_PATHS_TITLE;Directories and partial paths
+QUEUE_LOCATION_TEMPLATE_HELP_PATHS_INTRO;The %dN, %d-N, %pN, %p-N, %PN and %P-N (N = 1..9) specifiers will be replaced by elements of the image file's directory path.\nThe format specifiers operate as follows:\n %dN = Nth directory from the end of the path\n %d-N = Nth directory from the start of the path\n %pN = all directories up to the Nth from the end of the path\n %p-N = the first N directories in the path\n %PN = the last N directories in the path\n %P-N = all directories from the Nth to the end of the path\n %f = base filename (no extension)
+QUEUE_LOCATION_TEMPLATE_HELP_PATHS_INTRO_WINDOWS;For Windows paths, %d-1 is the drive letter and colon, and %d-2 is the base directory on that drive.
+QUEUE_LOCATION_TEMPLATE_HELP_PATHS_BODY_1;Using this pathname as an example:
+QUEUE_LOCATION_TEMPLATE_HELP_PATHS_EXAMPLE_LINUX;/home/tom/photos/2010-10-31/photo1.raw
+QUEUE_LOCATION_TEMPLATE_HELP_PATHS_EXAMPLE_WINDOWS;D:\tom\photos\2010-10-31\photo1.raw
+QUEUE_LOCATION_TEMPLATE_HELP_PATHS_BODY_2;The meanings of the formatting strings are:
+QUEUE_LOCATION_TEMPLATE_HELP_RESULT_MISMATCH;ERROR: 2nd result is different:
+QUEUE_LOCATION_TEMPLATE_HELP_RANK_TITLE;Rank
+QUEUE_LOCATION_TEMPLATE_HELP_RANK_BODY;%r will be replaced by the photo's rank. If the photo is unranked, '0' is used. If the photo is in the trash, 'x' is used.
+QUEUE_LOCATION_TEMPLATE_HELP_SEQUENCE_TITLE;Position/sequence in queue
+QUEUE_LOCATION_TEMPLATE_HELP_SEQUENCE_BODY;%s1, ..., %s9 will be replaced by the photo's initial position in the queue at the time the queue is started. The number specifies the padding, e.g. %s3 results in '001'.
+QUEUE_LOCATION_TEMPLATE_HELP_TIMESTAMP_TITLE;Date and time
+QUEUE_LOCATION_TEMPLATE_HELP_TIMESTAMP_BODY;Three different date/time values may be used in templates:\n %tE"%Y-%m-%d" = when export started\n %tF"%Y-%m-%d" = when file was last saved\n %tP"%Y-%m-%d" = when photo was taken\nThe quoted string defines the format of the resulting date and/or time. The format string %tF"%Y-%m-%d" is just one example. The string can use all conversion specifiers defined for the g_date_time_format function (see https://docs.gtk.org/glib/method.DateTime.format.html).\n\nExample format strings:
+QUEUE_LOCATION_TEMPLATE_HELP_EXAMPLES_TITLE;Common examples
+QUEUE_LOCATION_TEMPLATE_HELP_EXAMPLES_BODY;If you want to save the output image alongside the source image, write:\n%p1/%f\n\nIf you want to save the output image in a folder named 'converted' located in the source photo's folder, write:\n%p1/converted/%f\n\nIf you want to save the output image in\n'/home/tom/photos/converted/2010-10-31', write:\n%p-3/converted/%P-4/%f
QUEUE_LOCATION_TITLE;Output Location
QUEUE_STARTSTOP_TOOLTIP;Start or stop processing the images in the queue.\n\nShortcut: Ctrl+s
SAMPLEFORMAT_0;Unknown data format
diff --git a/rtengine/camconst.json b/rtengine/camconst.json
index fe3dc1c55..f834acdbd 100644
--- a/rtengine/camconst.json
+++ b/rtengine/camconst.json
@@ -3097,7 +3097,17 @@ Camera constants:
{ // Quality C,
"make_model": "Sony ILCE-7RM4",
"dcraw_matrix": [ 7662, -2686, -660, -5240, 12965, 2530, -796, 1508, 6167 ],
- "raw_crop": [ 0, 0, -32, 0 ] // full raw frame 9600x6376 - 32 rightmost columns are garbage. Using -32 instead of 9568 to support also 16-shot pixelshift files
+ "raw_crop": [ 0, 0, -32, 0 ], // full raw frame 9600x6376 - 32 rightmost columns are garbage. Using -32 instead of 9568 to support also 16-shot pixelshift files
+ "pdaf_pattern": [ 0,12,18,36,42,60,66,72,78,96,108,120,126,138,156,168,180,186,192,198,210,222,228,240,246,252,270,276,282,288,306,312,318,330,336,348,360,366,372,378,390,396,408,420 ], // Assume the pattern is the same as the ILCE-7CR.
+ "pdaf_offset": 1
+ },
+
+ { // Quality B
+ "make_model": [ "Sony ILCE-7CR", "Sony ILCE-7RM5" ], // 7RM5 is assumed to have the same sensor as the 7CR.
+ "dcraw_matrix": [ 8200, -2976, -719, -4296, 12053, 2532, -429, 1282, 5774 ], // DNG v15.2 for ILCE-7CR and ILCE-7RM5.
+ "raw_crop": [ 0, 0, -32, 0 ], // A few repeated pixels on the right edge.
+ "pdaf_pattern": [ 0,12,18,36,42,60,66,72,78,96,108,120,126,138,156,168,180,186,192,198,210,222,228,240,246,252,270,276,282,288,306,312,318,330,336,348,360,366,372,378,390,396,408,420 ], // From issue #6938. Slightly different every repetition, maybe the real pattern is 3 or more multiples of 420 pixels. This is a composite.
+ "pdaf_offset": 1
},
{ // Quality B, assumed correct for 9M2 as well
diff --git a/rtengine/iplocallab.cc b/rtengine/iplocallab.cc
index 3d02c9df6..4e9072f7e 100644
--- a/rtengine/iplocallab.cc
+++ b/rtengine/iplocallab.cc
@@ -17813,7 +17813,7 @@ void ImProcFunctions::Lab_Local(
}
if (locchCurve && CHcurve && lp.qualcurvemet != 0) {//C=f(H) curve
const float rhue = xatan2f(bufcolcalcb, bufcolcalca);
- const float valparam = 2.f * locchCurve[500.f * static_cast(Color::huelab_to_huehsv2(rhue))] - 0.5f; //get valp=f(H)
+ const float valparam = locchCurve[500.f * static_cast(Color::huelab_to_huehsv2(rhue))] - 0.5f; //get valp=f(H)
float chromaChfactor = 1.0f + valparam;
bufcolcalca *= chromaChfactor;//apply C=f(H)
bufcolcalcb *= chromaChfactor;
diff --git a/rtengine/rtlensfun.cc b/rtengine/rtlensfun.cc
index 58f3d12a3..fc5bb0017 100644
--- a/rtengine/rtlensfun.cc
+++ b/rtengine/rtlensfun.cc
@@ -435,11 +435,21 @@ std::vector LFDatabase::getLenses() const
}
-LFCamera LFDatabase::findCamera(const Glib::ustring &make, const Glib::ustring &model) const
+LFCamera LFDatabase::findCamera(const Glib::ustring &make, const Glib::ustring &model, bool autoMatch) const
{
LFCamera ret;
if (data_ && !make.empty()) {
MyMutex::MyLock lock(lfDBMutex);
+ if (!autoMatch) {
+ // Try to find exact match by name.
+ for (auto camera_list = data_->GetCameras(); camera_list[0]; camera_list++) {
+ const auto camera = camera_list[0];
+ if (make == camera->Maker && model == camera->Model) {
+ ret.data_ = camera;
+ return ret;
+ }
+ }
+ }
auto found = data_->FindCamerasExt(make.c_str(), model.c_str());
if (found) {
ret.data_ = found[0];
@@ -455,6 +465,16 @@ LFLens LFDatabase::findLens(const LFCamera &camera, const Glib::ustring &name) c
LFLens ret;
if (data_ && !name.empty()) {
MyMutex::MyLock lock(lfDBMutex);
+ if (!camera.data_) {
+ // Only the lens name provided. Try to find exact match by name.
+ LFLens candidate;
+ for (auto lens_list = data_->GetLenses(); lens_list[0]; lens_list++) {
+ candidate.data_ = lens_list[0];
+ if (name == candidate.getLens()) {
+ return candidate;
+ }
+ }
+ }
auto found = data_->FindLenses(camera.data_, nullptr, name.c_str());
for (size_t pos = 0; !found && pos < name.size(); ) {
// try to split the maker from the model of the lens -- we have to
@@ -541,7 +561,7 @@ std::unique_ptr LFDatabase::findModifier(
return nullptr;
}
- const LFCamera c = findCamera(make, model);
+ const LFCamera c = findCamera(make, model, lensProf.lfAutoMatch());
const LFLens l = findLens(
lensProf.lfAutoMatch()
? c
diff --git a/rtengine/rtlensfun.h b/rtengine/rtlensfun.h
index 51212c9b9..bcce77f34 100644
--- a/rtengine/rtlensfun.h
+++ b/rtengine/rtlensfun.h
@@ -120,7 +120,7 @@ public:
std::vector getCameras() const;
std::vector getLenses() const;
- LFCamera findCamera(const Glib::ustring &make, const Glib::ustring &model) const;
+ LFCamera findCamera(const Glib::ustring &make, const Glib::ustring &model, bool autoMatch) const;
LFLens findLens(const LFCamera &camera, const Glib::ustring &name) const;
std::unique_ptr findModifier(
diff --git a/rtgui/batchqueue.cc b/rtgui/batchqueue.cc
index 2811e3445..c275ee675 100644
--- a/rtgui/batchqueue.cc
+++ b/rtgui/batchqueue.cc
@@ -20,6 +20,7 @@
#include
#include
#include
+#include "../rtengine/imagedata.h"
#include "../rtengine/rt_math.h"
#include "../rtengine/procparams.h"
@@ -94,6 +95,21 @@ namespace // local helper functions
}
}
}
+
+ // Look in templateText at index ix for quoted string containing a time format string, and
+ // use that string to format dateTime. Append the formatted time to path.
+ void appendFormattedTime(Glib::ustring& path, unsigned int& ix, const Glib::ustring& templateText, const Glib::DateTime& dateTime)
+ {
+ constexpr gunichar quoteMark('"');
+ if ((ix + 1) < templateText.size() && templateText[ix] == quoteMark) {
+ const auto endPos = templateText.find_first_of(quoteMark, ++ix);
+ if (endPos != Glib::ustring::npos) {
+ Glib::ustring formatString(templateText, ix, endPos-ix);
+ path += dateTime.format(formatString);
+ ix = endPos;
+ }
+ }
+ }
}
BatchQueue::BatchQueue (FileCatalog* aFileCatalog) : processing(nullptr), fileCatalog(aFileCatalog), sequence(0), listener(nullptr)
@@ -1001,6 +1017,43 @@ Glib::ustring BatchQueue::calcAutoFileNameBase (const Glib::ustring& origFileNam
seqstr << sequence;
path += seqstr.str ();
+ } else if (options.savePathTemplate[ix] == 't') {
+ // Insert formatted date/time value. Character after 't' defines time source
+ if (++ix < options.savePathTemplate.size()) {
+ Glib::DateTime dateTime;
+ switch(options.savePathTemplate[ix++])
+ {
+ case 'E': // (approximate) time when export started
+ {
+ dateTime = Glib::DateTime::create_now_local();
+ break;
+ }
+ case 'F': // time when file was last saved
+ {
+ Glib::RefPtr file = Gio::File::create_for_path(origFileName);
+ if (file) {
+ Glib::RefPtr info = file->query_info(G_FILE_ATTRIBUTE_TIME_MODIFIED);
+ if (info) {
+ dateTime = info->get_modification_date_time();
+ }
+ }
+ break;
+ }
+ case 'P': // time when picture was taken
+ {
+ const auto timestamp = FramesData(origFileName).getDateTimeAsTS();
+ dateTime = Glib::DateTime::create_now_local(timestamp);
+ break;
+ }
+ default:
+ {
+ break;
+ }
+ }
+ if (dateTime) {
+ appendFormattedTime(path, ix, options.savePathTemplate, dateTime);
+ }
+ }
}
}
diff --git a/rtgui/batchqueuepanel.cc b/rtgui/batchqueuepanel.cc
index e35149326..bf6cc0772 100644
--- a/rtgui/batchqueuepanel.cc
+++ b/rtgui/batchqueuepanel.cc
@@ -73,7 +73,10 @@ BatchQueuePanel::BatchQueuePanel (FileCatalog* aFileCatalog) : parent(nullptr)
hb2->pack_start (*useTemplate, Gtk::PACK_SHRINK, 4);
outdirTemplate = Gtk::manage (new Gtk::Entry ());
hb2->pack_start (*outdirTemplate);
- odvb->pack_start (*hb2, Gtk::PACK_SHRINK, 4);
+ templateHelpButton = Gtk::manage (new Gtk::ToggleButton("?"));
+ templateHelpButton->set_tooltip_markup (M ("QUEUE_LOCATION_TEMPLATE_HELP_BUTTON_TOOLTIP"));
+ hb2->pack_start (*templateHelpButton, Gtk::PACK_SHRINK, 0);
+ odvb->pack_start (*hb2, Gtk::PACK_SHRINK, 0);
outdirTemplate->set_tooltip_markup (M("QUEUE_LOCATION_TEMPLATE_TOOLTIP"));
useTemplate->set_tooltip_markup (M("QUEUE_LOCATION_TEMPLATE_TOOLTIP"));
Gtk::Box* hb3 = Gtk::manage (new Gtk::Box ());
@@ -136,6 +139,7 @@ BatchQueuePanel::BatchQueuePanel (FileCatalog* aFileCatalog) : parent(nullptr)
outdirTemplate->signal_changed().connect (sigc::mem_fun(*this, &BatchQueuePanel::saveOptions));
useTemplate->signal_toggled().connect (sigc::mem_fun(*this, &BatchQueuePanel::saveOptions));
useFolder->signal_toggled().connect (sigc::mem_fun(*this, &BatchQueuePanel::saveOptions));
+ templateHelpButton->signal_toggled().connect (sigc::mem_fun(*this, &BatchQueuePanel::templateHelpButtonToggled));
saveFormatPanel->setListener (this);
// setup button bar
@@ -147,8 +151,19 @@ BatchQueuePanel::BatchQueuePanel (FileCatalog* aFileCatalog) : parent(nullptr)
topBox->pack_start (*fdir, Gtk::PACK_EXPAND_WIDGET, 4);
topBox->pack_start (*fformat, Gtk::PACK_EXPAND_WIDGET, 4);
+ middleSplitPane = Gtk::manage (new Gtk::Paned(Gtk::ORIENTATION_HORIZONTAL));
+ templateHelpTextView = Gtk::manage (new Gtk::TextView());
+ templateHelpTextView->set_editable(false);
+ templateHelpTextView->set_wrap_mode(Gtk::WRAP_WORD);
+ scrolledTemplateHelpWindow = Gtk::manage(new Gtk::ScrolledWindow());
+ scrolledTemplateHelpWindow->add(*templateHelpTextView);
+ middleSplitPane->pack1 (*scrolledTemplateHelpWindow);
+ middleSplitPane->pack2 (*batchQueue);
+ scrolledTemplateHelpWindow->set_visible(false); // initially hidden, templateHelpButton shows it
+ scrolledTemplateHelpWindow->set_no_show_all(true);
+
// add middle browser area
- pack_start (*batchQueue);
+ pack_start (*middleSplitPane);
// lower box with thumbnail zoom
bottomBox = Gtk::manage (new Gtk::Box ());
@@ -322,6 +337,122 @@ void BatchQueuePanel::setGuiFromBatchState(bool queueRunning, int qsize)
updateTab(qsize);
}
+void BatchQueuePanel::templateHelpButtonToggled()
+{
+ bool visible = templateHelpButton->get_active();
+ auto buffer = templateHelpTextView->get_buffer();
+ if (buffer->get_text().empty()) {
+ // Populate the help text the first time it's shown
+ populateTemplateHelpBuffer(buffer);
+ const auto fullWidth = middleSplitPane->get_width();
+ middleSplitPane->set_position(fullWidth / 2);
+ }
+ scrolledTemplateHelpWindow->set_visible(visible);
+ templateHelpTextView->set_visible(visible);
+}
+
+void BatchQueuePanel::populateTemplateHelpBuffer(Glib::RefPtr buffer)
+{
+ auto pos = buffer->begin();
+ const auto insertTopicHeading = [&pos, buffer](const Glib::ustring& text) {
+ pos = buffer->insert_markup(pos, Glib::ustring::format("\n\n", text, "\n"));
+ };
+ const auto insertTopicBody = [&pos, buffer](const Glib::ustring& text) {
+ pos = buffer->insert_markup(pos, Glib::ustring::format("\n", text, "\n"));
+ };
+ const auto mainTitle = M("QUEUE_LOCATION_TEMPLATE_HELP_TITLE");
+ pos = buffer->insert_markup(pos, Glib::ustring::format("", mainTitle, "\n"));
+ pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_INTRO"));
+
+ insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_EXAMPLES_TITLE"));
+ pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_EXAMPLES_BODY"));
+
+ insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_TITLE"));
+ pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_INTRO"));
+ pos = buffer->insert(pos, "\n");
+#ifdef _WIN32
+ pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_INTRO_WINDOWS"));
+ pos = buffer->insert(pos, "\n");
+#endif
+ pos = buffer->insert(pos, "\n");
+ pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_BODY_1"));
+#ifdef _WIN32
+ const auto exampleFilePath = M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_EXAMPLE_WINDOWS");
+#else
+ const auto exampleFilePath = M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_EXAMPLE_LINUX");
+#endif
+ pos = buffer->insert_markup(pos, Glib::ustring::format("\n ", exampleFilePath, "\n"));
+ pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_BODY_2"));
+ // Examples are generated from exampleFilePath using the actual template processing function
+ const Options savedOptions = options; // to be restored after generating example results
+ options.saveUsePathTemplate = true;
+ // Since this code only ever runs once (the first time the help text is presented), no attempt is
+ // made to be efficient. Use a brute-force method to discover the number of elements in exampleFilePath.
+ int pathElementCount = 0;
+ for (int n=9; n>=0; n--) {
+ options.savePathTemplate = Glib::ustring::format("%d", n);
+ const auto result = BatchQueue::calcAutoFileNameBase(exampleFilePath);
+ if (!result.empty()) {
+ // The 'd' specifier returns an empty string if N exceeds the number of path elements, so
+ // the largest N that does not return an empty string is the number of elements in exampleFilePath.
+ pathElementCount = n;
+ break;
+ }
+ }
+ // Function inserts examples for a particular specifier, with every valid N value for the
+ // number of elements in the path.
+ const auto insertPathExamples = [&buffer, &pos, pathElementCount, exampleFilePath](char letter, int offset1, int mult1, int offset2, int mult2)
+ {
+ for (int n=0; ninsert_markup(pos, Glib::ustring::format("\n ", path1, " = ", path2, " = ", result1, ""));
+ if (result1 != result2) {
+ // If this error appears, it indicates a coding error in either BatchQueue::calcAutoFileNameBase
+ // or BatchQueuePanel::populateTemplateHelpBuffer.
+ pos = buffer->insert_markup(pos, Glib::ustring::format(" ", M("QUEUE_LOCATION_TEMPLATE_HELP_RESULT_MISMATCH"), " ", result2));
+ }
+ }
+ };
+ // Example outputs in comments below are for a 4-element path.
+ insertPathExamples('d', pathElementCount, -1, -1, -1); // %d4 = %d-1 = home
+ insertPathExamples('p', 1, 1, -pathElementCount, 1); // %p1 = %p-4 = /home/tom/photos/2010-10-31/
+ insertPathExamples('P', 1, 1, -pathElementCount, 1); // %P1 = %P-4 = 2010-10-31/
+ {
+ const Glib::ustring fspecifier("%f");
+ options.savePathTemplate = fspecifier;
+ const auto result = BatchQueue::calcAutoFileNameBase(exampleFilePath);
+ pos = buffer->insert_markup(pos, Glib::ustring::format("\n ", fspecifier, " = ", result, ""));
+ }
+
+ insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_RANK_TITLE"));
+ pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_RANK_BODY"));
+
+ insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_SEQUENCE_TITLE"));
+ pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_SEQUENCE_BODY"));
+
+ insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_TIMESTAMP_TITLE"));
+ pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_TIMESTAMP_BODY"));
+ const Glib::ustring dateTimeFormatExamples[] = {
+ "%Y-%m-%d",
+ "%Y%m%d_%H%M%S",
+ "%y/%b/%-d/"
+ };
+ const auto timezone = Glib::DateTime::create_now_local().get_timezone();
+ const auto timeForExamples = Glib::DateTime::create_from_iso8601("2001-02-03T04:05:06.123456", timezone);
+ for (auto && fmt : dateTimeFormatExamples) {
+ const auto result = timeForExamples.format(fmt);
+ pos = buffer->insert_markup(pos, Glib::ustring::format("\n %tE\"", fmt, "\" = ", result, ""));
+ }
+
+ pos = buffer->insert(pos, "\n");
+ options = savedOptions; // Do not add any lines in this function below here
+}
+
void BatchQueuePanel::addBatchQueueJobs(const std::vector& entries, bool head)
{
batchQueue->addEntries(entries, head);
diff --git a/rtgui/batchqueuepanel.h b/rtgui/batchqueuepanel.h
index d73f8b893..417d8a4cb 100644
--- a/rtgui/batchqueuepanel.h
+++ b/rtgui/batchqueuepanel.h
@@ -52,8 +52,12 @@ class BatchQueuePanel : public Gtk::Box,
RTWindow* parent;
BatchQueue* batchQueue;
+ Gtk::TextView* templateHelpTextView;
+ Gtk::ScrolledWindow* scrolledTemplateHelpWindow;
+ Gtk::ToggleButton* templateHelpButton;
Gtk::Box* bottomBox;
Gtk::Box* topBox;
+ Gtk::Paned* middleSplitPane;
std::atomic queueShouldRun;
@@ -80,6 +84,8 @@ private:
void stopBatchProc ();
void startOrStopBatchProc();
void setGuiFromBatchState(bool queueRunning, int qsize);
+ void templateHelpButtonToggled();
+ void populateTemplateHelpBuffer(Glib::RefPtr buffer);
void pathFolderChanged ();
void pathFolderButtonPressed ();
diff --git a/rtgui/cropwindow.cc b/rtgui/cropwindow.cc
index ef2b7a52b..1649034f8 100644
--- a/rtgui/cropwindow.cc
+++ b/rtgui/cropwindow.cc
@@ -43,6 +43,32 @@
using namespace rtengine;
+namespace {
+ inline double zoomLimitToFraction(Options::MaxZoom z) {
+ switch (z) {
+ case Options::MaxZoom::PERCENTS_100:
+ return 1.;
+ case Options::MaxZoom::PERCENTS_200:
+ return 2.;
+ case Options::MaxZoom::PERCENTS_300:
+ return 3.;
+ case Options::MaxZoom::PERCENTS_400:
+ return 4.;
+ case Options::MaxZoom::PERCENTS_500:
+ return 5.;
+ case Options::MaxZoom::PERCENTS_600:
+ return 6.;
+ case Options::MaxZoom::PERCENTS_700:
+ return 7.;
+ case Options::MaxZoom::PERCENTS_800:
+ return 8.;
+ case Options::MaxZoom::PERCENTS_1600:
+ default:
+ return 16.;
+ }
+ }
+}
+
bool CropWindow::initialized = false;
Glib::ustring CropWindow::zoomOuttt;
@@ -2284,13 +2310,18 @@ void CropWindow::updateHoveredPicker (rtengine::Coord *imgPos)
}
void CropWindow::changeZoom (int zoom, bool notify, int centerx, int centery, bool needsRedraw)
{
-
if (zoom < 0) {
zoom = 0;
} else if (zoom > int(zoomSteps.size())-1) {
zoom = int(zoomSteps.size())-1;
}
+ // Limit zoom according to user preferences
+ double zoomLimit = zoomLimitToFraction(options.maxZoomLimit);
+ while(zoomSteps[zoom].zoom > zoomLimit && zoom != 0) {
+ --zoom;
+ }
+
cropZoom = zoom;
cropLabel = zoomSteps[cropZoom].label;
diff --git a/rtgui/lensprofile.cc b/rtgui/lensprofile.cc
index 784c49f15..58ea1df1d 100644
--- a/rtgui/lensprofile.cc
+++ b/rtgui/lensprofile.cc
@@ -242,7 +242,7 @@ void LensProfilePanel::read(const rtengine::procparams::ProcParams* pp, const Pa
if (pp->lensProf.lfAutoMatch()) {
if (metadata) {
- c = db->findCamera(metadata->getMake(), metadata->getModel());
+ c = db->findCamera(metadata->getMake(), metadata->getModel(), true);
setLensfunCamera(c.getMake(), c.getModel());
}
} else if (pp->lensProf.lfManual()) {
@@ -521,7 +521,7 @@ void LensProfilePanel::onCorrModeChanged(const Gtk::RadioButton* rbChanged)
setLensfunLens("");
} else if (metadata) {
const LFDatabase* const db = LFDatabase::getInstance();
- const LFCamera c = db->findCamera(metadata->getMake(), metadata->getModel());
+ const LFCamera c = db->findCamera(metadata->getMake(), metadata->getModel(), true);
const LFLens l = db->findLens(c, metadata->getLens());
setLensfunCamera(c.getMake(), c.getModel());
setLensfunLens(l.getLens());
@@ -801,7 +801,7 @@ void LensProfilePanel::updateLensfunWarning()
return;
}
- const LFCamera c = db->findCamera((*itc)[lf->lensfunModelCam.make], (*itc)[lf->lensfunModelCam.model]);
+ const LFCamera c = db->findCamera((*itc)[lf->lensfunModelCam.make], (*itc)[lf->lensfunModelCam.model], false);
const auto itl = lensfunLenses->get_active();
if (!itl) {
diff --git a/rtgui/options.cc b/rtgui/options.cc
index 61d67f9b6..2da791cd8 100644
--- a/rtgui/options.cc
+++ b/rtgui/options.cc
@@ -368,6 +368,7 @@ void Options::setDefaults()
fbShowDateTime = true;
fbShowBasicExif = true;
fbShowExpComp = false;
+ maxZoomLimit = MaxZoom::PERCENTS_1600;
#ifdef _WIN32
// use windows setting for visibility of hidden files/folders
SHELLFLAGSTATE sft = { 0 };
@@ -574,8 +575,8 @@ void Options::setDefaults()
rtSettings.darkFramesPath = "";
rtSettings.flatFieldsPath = "";
- rtSettings.cameraProfilesPath = "";
- rtSettings.lensProfilesPath = "";
+ rtSettings.cameraProfilesPath = "";
+ rtSettings.lensProfilesPath = "";
#ifdef _WIN32
const gchar* sysRoot = g_getenv("SystemRoot"); // Returns e.g. "c:\Windows"
@@ -1729,6 +1730,9 @@ void Options::readFromFile(Glib::ustring fname)
if (keyFile.has_key("GUI", "ZoomOnScroll")) {
zoomOnScroll = keyFile.get_boolean("GUI", "ZoomOnScroll");
}
+ if (keyFile.has_key("GUI", "MaxZoom")) {
+ maxZoomLimit = static_cast(keyFile.get_integer("GUI", "MaxZoom"));
+ }
}
if (keyFile.has_group("Crop Settings")) {
@@ -2582,6 +2586,7 @@ void Options::saveToFile(Glib::ustring fname)
keyFile.set_integer("GUI", "Complexity", complexity);
keyFile.set_boolean("GUI", "InspectorWindow", inspectorWindow);
keyFile.set_boolean("GUI", "ZoomOnScroll", zoomOnScroll);
+ keyFile.set_integer("GUI", "MaxZoom", static_cast(maxZoomLimit));
//Glib::ArrayHandle crvopen = crvOpen;
//keyFile.set_integer_list ("GUI", "CurvePanelsExpanded", crvopen);
diff --git a/rtgui/options.h b/rtgui/options.h
index 7c6f1d9fb..72d2b2462 100644
--- a/rtgui/options.h
+++ b/rtgui/options.h
@@ -368,6 +368,22 @@ public:
CropGuidesMode cropGuides;
bool cropAutoFit;
+ // Other options
+
+ // Maximum zoom
+ enum class MaxZoom: int {
+ PERCENTS_100 = 0,
+ PERCENTS_200,
+ PERCENTS_300,
+ PERCENTS_400,
+ PERCENTS_500,
+ PERCENTS_600,
+ PERCENTS_700,
+ PERCENTS_800,
+ PERCENTS_1600,
+ };
+ MaxZoom maxZoomLimit;
+
// Performance options
Glib::ustring clutsDir;
int rgbDenoiseThreadLimit; // maximum number of threads for the denoising tool ; 0 = use the maximum available
diff --git a/rtgui/preferences.cc b/rtgui/preferences.cc
index 6ae52d3bb..90ae9ec41 100644
--- a/rtgui/preferences.cc
+++ b/rtgui/preferences.cc
@@ -698,7 +698,7 @@ Gtk::Widget* Preferences::getImageProcessingPanel ()
dirgrid->attach_next_to(*cameraProfilesDirLabel, *clutsDirLabel, Gtk::POS_BOTTOM, 1, 1);
dirgrid->attach_next_to(*cameraProfilesDir, *cameraProfilesDirLabel, Gtk::POS_RIGHT, 1, 1);
- //Lens Profiles Dir
+ //Lens Profiles Dir
Gtk::Label *lensProfilesDirLabel = Gtk::manage(new Gtk::Label(M("PREFERENCES_LENSPROFILESDIR") + ":"));
lensProfilesDirLabel->set_tooltip_text(M("PREFERENCES_LENSPROFILESDIR_TOOLTIP"));
setExpandAlignProperties(lensProfilesDirLabel, false, false, Gtk::ALIGN_START, Gtk::ALIGN_CENTER);
@@ -745,6 +745,29 @@ Gtk::Widget* Preferences::getImageProcessingPanel ()
cropFrame->add(*cropGrid);
vbImageProcessing->pack_start(*cropFrame, Gtk::PACK_SHRINK, 4);
+ // Other: max zoom
+ {
+ Gtk::Frame *frame = Gtk::manage(new Gtk::Frame(M("GENERAL_OTHER")));
+ frame->set_label_align (0.025, 0.5);
+ Gtk::Grid *grid = Gtk::manage(new Gtk::Grid());
+
+ Gtk::Label *label = Gtk::manage(new Gtk::Label(M("PREFERENCES_MAX_ZOOM_TITLE") + ": ", Gtk::ALIGN_START));
+ label->set_line_wrap(true);
+ grid->attach(*label, 0, 0);
+
+ maxZoomCombo = Gtk::manage(new Gtk::ComboBoxText());
+
+ // Labels order matches to Options::MaxZoom enum
+ for (int i = 1; i <= 8; ++i) {
+ maxZoomCombo->append(Glib::ustring::compose("%100%%", i));
+ }
+ maxZoomCombo->append("1600%");
+
+ grid->attach(*maxZoomCombo, 1, 0, 1, 1);
+ frame->add(*grid);
+ vbImageProcessing->pack_start(*frame, Gtk::PACK_SHRINK, 4);
+ }
+
swImageProcessing->add(*vbImageProcessing);
return swImageProcessing;
@@ -1995,6 +2018,7 @@ void Preferences::storePreferences()
moptions.cropGuides = Options::CropGuidesMode(cropGuidesCombo->get_active_row_number());
moptions.cropAutoFit = cropAutoFitCB->get_active();
+ moptions.maxZoomLimit = Options::MaxZoom(maxZoomCombo->get_active_row_number());
toolLocationPreference->updateOptions();
@@ -2229,6 +2253,7 @@ void Preferences::fillPreferences()
cropGuidesCombo->set_active(moptions.cropGuides);
cropAutoFitCB->set_active(moptions.cropAutoFit);
+ maxZoomCombo->set_active(static_cast(options.maxZoomLimit));
addc.block(false);
setc.block(false);
diff --git a/rtgui/preferences.h b/rtgui/preferences.h
index 219844d55..bc8dc0d22 100644
--- a/rtgui/preferences.h
+++ b/rtgui/preferences.h
@@ -249,6 +249,8 @@ class Preferences final :
Gtk::ComboBoxText *cropGuidesCombo;
Gtk::CheckButton *cropAutoFitCB;
+ Gtk::ComboBoxText *maxZoomCombo;
+
Gtk::ComboBoxText *metadataSyncCombo;
Gtk::ComboBoxText *xmpSidecarCombo;
diff --git a/tools/osx/macosx_bundle.sh b/tools/osx/macosx_bundle.sh
index 4a6509403..89b10c5af 100644
--- a/tools/osx/macosx_bundle.sh
+++ b/tools/osx/macosx_bundle.sh
@@ -155,8 +155,9 @@ fi
# In: OSX_CONTINUOUS:BOOL=ON
# Out: ON
-OSX_CONTINUOUS="$(cmake .. -L -N | grep OSX_CONTINUOUS)"; NIGHTLY="${OSX_CONTINUOUS#*=}" && CONTINUOUS="${OSX_CONTINUOUS#*=}"
+OSX_CONTINUOUS="$(cmake .. -L -N | grep OSX_CONTINUOUS)"; CONTINUOUS="${OSX_CONTINUOUS#*=}"
if [[ -n $CONTINUOUS ]]; then
+ NIGHTLY="${OSX_CONTINUOUS#*=}"
echo "Continuous/generically-named zip is ON."
fi
@@ -313,13 +314,17 @@ done
install_name_tool -delete_rpath RawTherapee.app/Contents/Frameworks "${EXECUTABLE}"-cli 2>/dev/null
install_name_tool -add_rpath /Applications/"${LIB}" "${EXECUTABLE}"-cli 2>/dev/null
+# Link to libomp instead of libgomp
+sudo install_name_tool -change /Applications/RawTherapee.app/Contents/Frameworks/libgomp.1.dylib /Applications/RawTherapee.app/Contents/Frameworks/libomp.dylib RawTherapee.app/Contents/Frameworks/libfftw3f_omp.3.dylib
+rm RawTherapee.app/Contents/Frameworks/libgomp.1.dylib
+
# Merge the app with the other architecture to create the Universal app.
if [[ -n $UNIVERSAL_URL ]]; then
msg "Getting Universal countercomponent."
curl -L ${UNIVERSAL_URL} -o univ.zip
msg "Extracting app."
unzip univ.zip -d univapp
- hdiutil attach -mountpoint ./RawTherapeeuniv univapp/*dmg
+ hdiutil attach -mountpoint ./RawTherapeeuniv univapp/*folder/*dmg
if [[ $arch = "arm64" ]]; then
cp -R RawTherapee.app RawTherapee-arm64.app
minimum_arm64_version=$(f=$(cat RawTherapee-arm64.app/Contents/Resources/AboutThisBuild.txt | grep mmacosx-version); echo "${f#*min=}" | cut -d ' ' -f1)
@@ -336,6 +341,7 @@ if [[ -n $UNIVERSAL_URL ]]; then
cat RawTherapee-arm64.app/Contents/Resources/AboutThisBuild.txt >> RawTherapee.app/Contents/Resources/AboutThisBuild.txt
fi
cmake -DPROJECT_SOURCE_DATA_DIR=${PROJECT_SOURCE_DATA_DIR} -DCONTENTS=${CONTENTS} -Dversion=${PROJECT_FULL_VERSION} -DshortVersion=${PROJECT_VERSION} -Dminimum_arm64_version=${minimum_arm64_version} -Dminimum_x86_64_version=${minimum_x86_64_version} -Darch=${arch} -P ${PROJECT_SOURCE_DATA_DIR}/info-plist.cmake
+ plutil -convert xml1 ${APP}/Contents/Info.plist
hdiutil unmount ./RawTherapeeuniv
rm -r univapp
# Create the fat main RawTherapee binary and move it into the new bundle
@@ -360,9 +366,26 @@ fi
if [[ -n $CODESIGNID ]]; then
msg "Codesigning Application."
iconv -f UTF-8 -t ASCII "${PROJECT_SOURCE_DATA_DIR}"/rt.entitlements > "${CMAKE_BUILD_TYPE}"/rt.entitlements
-# mv "${EXECUTABLE}"-cli "${LIB}"
- codesign --force --deep --timestamp --strict -v -s "${CODESIGNID}" -i com.rawtherapee.RawTherapee-cli "${APP}"/Contents/MacOS/rawtherapee-cli
- codesign --force --deep --timestamp --strict -v -s "${CODESIGNID}" -i com.rawtherapee.RawTherapee -o runtime --entitlements "${CMAKE_BUILD_TYPE}"/rt.entitlements "${APP}"
+ plutil -convert xml1 "${CMAKE_BUILD_TYPE}"/rt.entitlements
+ for frame in ${APP}/Contents/Frameworks/* ; do
+ echo $frame
+ codesign --preserve-metadata=identifier --digest-algorithm=sha1,sha256 --force --timestamp --strict -v -s "${CODESIGNID}" -i com.rawtherapee.RawTherapee -o runtime --entitlements "${CMAKE_BUILD_TYPE}"/rt.entitlements $frame
+ done
+ for resource in ${APP}/Contents/Resources/* ; do
+ echo $resource
+ if [ ! -d $resource ]; then
+ codesign --preserve-metadata=identifier --digest-algorithm=sha1,sha256 --force --timestamp --strict -v -s "${CODESIGNID}" -i com.rawtherapee.RawTherapee -o runtime --entitlements "${CMAKE_BUILD_TYPE}"/rt.entitlements $resource
+ else
+ for subresource in ${APP}/Contents/Resources/$(basename $resource)/* ; do
+ if [ ! -d $subresource ]; then
+ codesign --preserve-metadata=identifier --digest-algorithm=sha1,sha256 --force --timestamp --strict -v -s "${CODESIGNID}" -i com.rawtherapee.RawTherapee -o runtime --entitlements "${CMAKE_BUILD_TYPE}"/rt.entitlements $subresource
+ fi
+ done
+ fi
+ done
+ codesign --preserve-metadata=identifier --digest-algorithm=sha1,sha256 --force --timestamp --strict -v -s "${CODESIGNID}" -i com.rawtherapee.RawTherapee -o runtime --entitlements "${CMAKE_BUILD_TYPE}"/rt.entitlements "${APP}"/Contents/MacOS/rawtherapee-cli
+ codesign --preserve-metadata=identifier --digest-algorithm=sha1,sha256 --force --timestamp --strict -v -s "${CODESIGNID}" -i com.rawtherapee.RawTherapee -o runtime --entitlements "${CMAKE_BUILD_TYPE}"/rt.entitlements "${APP}"/Contents/MacOS/rawtherapee
+ codesign --preserve-metadata=identifier --digest-algorithm=sha1,sha256 --force --timestamp --strict -v -s "${CODESIGNID}" -i com.rawtherapee.RawTherapee -o runtime --entitlements "${CMAKE_BUILD_TYPE}"/rt.entitlements "${APP}"
spctl -a -vvvv "${APP}"
fi
@@ -436,7 +459,7 @@ function CreateDmg {
# Sign disk image
if [[ -n $CODESIGNID ]]; then
msg "Signing disk image"
- codesign --deep --force -v -s "${CODESIGNID}" --timestamp "${dmg_name}.dmg"
+ codesign --digest-algorithm=sha1,sha256 --force -v -s "${CODESIGNID}" --timestamp "${dmg_name}.dmg"
fi
# Notarize the dmg