From ba906af841a47438512ff3f67202b0760b3a6b0e Mon Sep 17 00:00:00 2001 From: Simone Gotti Date: Fri, 25 Mar 2022 15:47:40 +0100 Subject: [PATCH 01/17] dcraw: increase linear table parsing to 65536 values The dcraw linear_table method limits the max values to 4096. But 16 bit per channel linear DNGs can provide a LinearizationTable with 65536 entries. This patch changes the dcraw linear_table method to accept 65536 entries. --- rtengine/dcraw.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rtengine/dcraw.cc b/rtengine/dcraw.cc index 39e527598..95df2d21b 100644 --- a/rtengine/dcraw.cc +++ b/rtengine/dcraw.cc @@ -6292,11 +6292,11 @@ void CLASS parse_mos (int offset) void CLASS linear_table (unsigned len) { int i; - if (len > 0x1000) len = 0x1000; + if (len > 0x10000) len = 0x10000; read_shorts (curve, len); - for (i=len; i < 0x1000; i++) + for (i=len; i < 0x10000; i++) curve[i] = curve[i-1]; - maximum = curve[0xfff]; + maximum = curve[0xffff]; } void CLASS parse_kodak_ifd (int base) From 9332333a12c781ec777ba5a825563dfeb5e1951a Mon Sep 17 00:00:00 2001 From: Lawrence37 <45837045+Lawrence37@users.noreply.github.com> Date: Fri, 16 Dec 2022 23:01:23 -0800 Subject: [PATCH 02/17] Speed up compilation of rtengine/procparams.cc --- rtengine/CMakeLists.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rtengine/CMakeLists.txt b/rtengine/CMakeLists.txt index 0fab84b55..c657d6f9d 100644 --- a/rtengine/CMakeLists.txt +++ b/rtengine/CMakeLists.txt @@ -176,6 +176,18 @@ if(LENSFUN_HAS_LOAD_DIRECTORY) set_source_files_properties(rtlensfun.cc PROPERTIES COMPILE_DEFINITIONS RT_LENSFUN_HAS_LOAD_DIRECTORY) endif() +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "12.0") + # procparams.cc takes a long time to compile with optimizations starting + # with GCC 12.1 due to PTA (see issue #6548) + get_source_file_property(PROCPARAMS_COMPILE_OPTIONS procparams.cc COMPILE_OPTIONS) + if(PROCPARAMS_COMPILE_OPTIONS STREQUAL "NOTFOUND") + set(PROCPARAMS_COMPILE_OPTIONS "") + else() + set(PROCPARAMS_COMPILE_OPTIONS "${PROCPARAMS_COMPILE_OPTIONS};") + endif() + set(PROCPARAMS_COMPILE_OPTIONS "${PROCPARAMS_COMPILE_OPTIONS}-fno-tree-pta") + set_source_files_properties(procparams.cc PROPERTIES COMPILE_OPTIONS ${PROCPARAMS_COMPILE_OPTIONS}) +endif() if(WITH_BENCHMARK) add_definitions(-DBENCHMARK) From f64ddf0d8eb677ca5a13319c147a6028c43399e4 Mon Sep 17 00:00:00 2001 From: Beep6581 Date: Sun, 18 Dec 2022 01:03:59 +0100 Subject: [PATCH 03/17] Updated logo image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae9efd9c8..a30a77212 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![RawTherapee logo](https://www.rawtherapee.com/images/logos/rawtherapee_logo_discuss.png) +![RawTherapee logo](https://raw.githubusercontent.com/Beep6581/RawTherapee/dev/rtdata/images/rt-logo-text-white.svg) RawTherapee is a powerful, cross-platform raw photo processing program, released as [libre software](https://en.wikipedia.org/wiki/Free_software) under the [GNU General Public License Version 3](https://opensource.org/licenses/gpl-3.0.html). It is written mostly in C++ using a [GTK+](https://www.gtk.org) front-end. It uses a patched version of [dcraw](https://www.dechifro.org/dcraw/) for reading raw files, with an in-house solution which adds the highest quality support for certain camera models unsupported by dcraw and enhances the accuracy of certain raw files already supported by dcraw. It is notable for the advanced control it gives the user over the demosaicing and development process. From bbe4558dff509604949e1bac58152ff0836b84f3 Mon Sep 17 00:00:00 2001 From: Morgan Hardwood Date: Sun, 18 Dec 2022 03:55:29 +0100 Subject: [PATCH 04/17] Improvement to dcraw linear_table #6448 Merged on behalf of heckflosse https://github.com/Beep6581/RawTherapee/pull/6448#issuecomment-1081779513 --- rtengine/dcraw.cc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/rtengine/dcraw.cc b/rtengine/dcraw.cc index 6ca0e4331..4ab1694b8 100644 --- a/rtengine/dcraw.cc +++ b/rtengine/dcraw.cc @@ -6319,12 +6319,13 @@ void CLASS parse_mos (int offset) void CLASS linear_table (unsigned len) { - int i; - if (len > 0x10000) len = 0x10000; - read_shorts (curve, len); - for (i=len; i < 0x10000; i++) - curve[i] = curve[i-1]; - maximum = curve[0xffff]; + const unsigned maxLen = std::min(0x10000ull, 1ull << tiff_bps); + len = std::min(len, maxLen); + read_shorts(curve, len); + maximum = curve[len - 1]; + for (std::size_t i = len; i < maxLen; ++i) { + curve[i] = maximum; + } } void CLASS parse_kodak_ifd (int base) From 38a8aa81602d536f6277f0f62f786c0d761db723 Mon Sep 17 00:00:00 2001 From: Beep6581 Date: Sun, 18 Dec 2022 09:41:06 +0100 Subject: [PATCH 05/17] Added external screenshot to README.md (#6648) See #6642 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a30a77212..c43bfc2f4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![RawTherapee logo](https://raw.githubusercontent.com/Beep6581/RawTherapee/dev/rtdata/images/rt-logo-text-white.svg) +![RawTherapee screenshot](http://rawtherapee.com/images/carousel/100_rt59_provence_local_maskxxx.jpg) + RawTherapee is a powerful, cross-platform raw photo processing program, released as [libre software](https://en.wikipedia.org/wiki/Free_software) under the [GNU General Public License Version 3](https://opensource.org/licenses/gpl-3.0.html). It is written mostly in C++ using a [GTK+](https://www.gtk.org) front-end. It uses a patched version of [dcraw](https://www.dechifro.org/dcraw/) for reading raw files, with an in-house solution which adds the highest quality support for certain camera models unsupported by dcraw and enhances the accuracy of certain raw files already supported by dcraw. It is notable for the advanced control it gives the user over the demosaicing and development process. ## Target Audience From 9019c6dccd0bb942e568e3dd4e98764e5788e9fb Mon Sep 17 00:00:00 2001 From: Lawrence37 <45837045+Lawrence37@users.noreply.github.com> Date: Mon, 19 Dec 2022 13:28:20 -0800 Subject: [PATCH 06/17] Update libtiff DLL version for Windows workflow --- .github/workflows/windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 296037812..f6f83f567 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -159,7 +159,7 @@ jobs: "libstdc++-6.dll" \ "libsystre-0.dll" \ "libthai-0.dll" \ - "libtiff-5.dll" \ + "libtiff-6.dll" \ "libtre-5.dll" \ "libwebp-7.dll" \ "libwinpthread-1.dll" \ From 2c9f5a735df5c9257d0627e531731060b4611215 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Fri, 30 Dec 2022 23:51:22 -0800 Subject: [PATCH 07/17] Add raw_crop and masked_areas for Canon EOS R7 and R10 (#6608) Signed-off-by: Alex Forencich --- rtengine/camconst.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rtengine/camconst.json b/rtengine/camconst.json index 189d8f3f4..b7151f2c8 100644 --- a/rtengine/camconst.json +++ b/rtengine/camconst.json @@ -1231,12 +1231,16 @@ Camera constants: { // Quality C "make_model": "Canon EOS R7", - "dcraw_matrix" : [10424, -3138, -1300, -4221, 11938, 2584, -547, 1658, 6183] + "dcraw_matrix" : [10424, -3138, -1300, -4221, 11938, 2584, -547, 1658, 6183], + "raw_crop": [ 144, 72, 6984, 4660 ], + "masked_areas" : [ 70, 20, 4724, 138 ] }, { // Quality C "make_model": "Canon EOS R10", - "dcraw_matrix" : [9269, -2012, -1107, -3990, 11762, 2527, -569, 2093, 4913] + "dcraw_matrix" : [9269, -2012, -1107, -3990, 11762, 2527, -569, 2093, 4913], + "raw_crop": [ 144, 40, 6048, 4020 ], + "masked_areas" : [ 38, 20, 4052, 138 ] }, { // Quality C, CHDK DNGs, raw frame correction From 22831866cd72c20a1d2f6952d76883b39e9ed4f7 Mon Sep 17 00:00:00 2001 From: Thanatomanic <6567747+Thanatomanic@users.noreply.github.com> Date: Sat, 31 Dec 2022 10:16:38 +0100 Subject: [PATCH 08/17] Change RT logo to black version in README.md so text is visible --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c43bfc2f4..21f219a83 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![RawTherapee logo](https://raw.githubusercontent.com/Beep6581/RawTherapee/dev/rtdata/images/rt-logo-text-white.svg) +![RawTherapee logo](https://raw.githubusercontent.com/Beep6581/RawTherapee/dev/rtdata/images/rt-logo-text-black.svg) ![RawTherapee screenshot](http://rawtherapee.com/images/carousel/100_rt59_provence_local_maskxxx.jpg) From 401727fba9ab391ed8f4992e042e33860e2e272e Mon Sep 17 00:00:00 2001 From: Nicolas Turlais Date: Sat, 31 Dec 2022 10:51:30 +0100 Subject: [PATCH 09/17] Add filter for Paths to dynamic profiles (#6284) Work by @nicolas-t * Path filter in dynamic profile panel * Pass filename as a function argument * Removed unused include * Clearer translation --- rtdata/languages/default | 1 + rtengine/dynamicprofile.cc | 5 ++++- rtengine/dynamicprofile.h | 3 ++- rtengine/profilestore.cc | 4 ++-- rtengine/profilestore.h | 2 +- rtgui/dynamicprofilepanel.cc | 25 +++++++++++++++++++++++++ rtgui/dynamicprofilepanel.h | 6 ++++++ rtgui/main-cli.cc | 4 ++-- rtgui/thumbnail.cc | 2 +- 9 files changed, 44 insertions(+), 8 deletions(-) diff --git a/rtdata/languages/default b/rtdata/languages/default index 775b098f4..e5e053655 100644 --- a/rtdata/languages/default +++ b/rtdata/languages/default @@ -61,6 +61,7 @@ EXIFFILTER_IMAGETYPE;Image type EXIFFILTER_ISO;ISO EXIFFILTER_LENS;Lens EXIFFILTER_METADATAFILTER;Enable metadata filters +EXIFFILTER_PATH;File path EXIFFILTER_SHUTTER;Shutter EXIFPANEL_ADDEDIT;Add/Edit EXIFPANEL_ADDEDITHINT;Add new tag or edit tag. diff --git a/rtengine/dynamicprofile.cc b/rtengine/dynamicprofile.cc index 28516a1ee..6dbd2d0f0 100644 --- a/rtengine/dynamicprofile.cc +++ b/rtengine/dynamicprofile.cc @@ -77,7 +77,7 @@ bool DynamicProfileRule::operator< (const DynamicProfileRule &other) const } -bool DynamicProfileRule::matches (const rtengine::FramesMetaData *im) const +bool DynamicProfileRule::matches (const rtengine::FramesMetaData *im, const Glib::ustring& filename) const { return (iso (im->getISOSpeed()) && fnumber (im->getFNumber()) @@ -86,6 +86,7 @@ bool DynamicProfileRule::matches (const rtengine::FramesMetaData *im) const && expcomp (im->getExpComp()) && camera (im->getCamera()) && lens (im->getLens()) + && path (filename) && imagetype(im->getImageType(0))); } @@ -214,6 +215,7 @@ bool DynamicProfileRules::loadRules() get_double_range (rule.expcomp, kf, group, "expcomp"); get_optional (rule.camera, kf, group, "camera"); get_optional (rule.lens, kf, group, "lens"); + get_optional (rule.path, kf, group, "path"); get_optional (rule.imagetype, kf, group, "imagetype"); try { @@ -247,6 +249,7 @@ bool DynamicProfileRules::storeRules() set_double_range (kf, group, "expcomp", rule.expcomp); set_optional (kf, group, "camera", rule.camera); set_optional (kf, group, "lens", rule.lens); + set_optional (kf, group, "path", rule.path); set_optional (kf, group, "imagetype", rule.imagetype); kf.set_string (group, "profilepath", rule.profilepath); } diff --git a/rtengine/dynamicprofile.h b/rtengine/dynamicprofile.h index d91b91aee..654db3a8e 100644 --- a/rtengine/dynamicprofile.h +++ b/rtengine/dynamicprofile.h @@ -51,7 +51,7 @@ public: }; DynamicProfileRule(); - bool matches (const rtengine::FramesMetaData *im) const; + bool matches (const rtengine::FramesMetaData *im, const Glib::ustring& filename) const; bool operator< (const DynamicProfileRule &other) const; int serial_number; @@ -62,6 +62,7 @@ public: Range expcomp; Optional camera; Optional lens; + Optional path; Optional imagetype; Glib::ustring profilepath; }; diff --git a/rtengine/profilestore.cc b/rtengine/profilestore.cc index 7d937e736..f83ddd385 100644 --- a/rtengine/profilestore.cc +++ b/rtengine/profilestore.cc @@ -508,7 +508,7 @@ void ProfileStore::dumpFolderList() printf ("\n"); } -PartialProfile *ProfileStore::loadDynamicProfile (const FramesMetaData *im) +PartialProfile *ProfileStore::loadDynamicProfile (const FramesMetaData *im, const Glib::ustring& filename) { if (storeState == STORESTATE_NOTINITIALIZED) { parseProfilesOnce(); @@ -521,7 +521,7 @@ PartialProfile *ProfileStore::loadDynamicProfile (const FramesMetaData *im) } for (auto rule : dynamicRules) { - if (rule.matches (im)) { + if (rule.matches (im, filename)) { if (settings->verbose) { printf ("found matching profile %s\n", rule.profilepath.c_str()); } diff --git a/rtengine/profilestore.h b/rtengine/profilestore.h index 460facb72..e8e48c17f 100644 --- a/rtengine/profilestore.h +++ b/rtengine/profilestore.h @@ -209,7 +209,7 @@ public: void addListener (ProfileStoreListener *listener); void removeListener (ProfileStoreListener *listener); - rtengine::procparams::PartialProfile* loadDynamicProfile (const rtengine::FramesMetaData *im); + rtengine::procparams::PartialProfile* loadDynamicProfile (const rtengine::FramesMetaData *im, const Glib::ustring& filename); void dumpFolderList(); }; diff --git a/rtgui/dynamicprofilepanel.cc b/rtgui/dynamicprofilepanel.cc index 865603b3a..feb6aea70 100644 --- a/rtgui/dynamicprofilepanel.cc +++ b/rtgui/dynamicprofilepanel.cc @@ -42,6 +42,7 @@ DynamicProfilePanel::EditDialog::EditDialog (const Glib::ustring &title, Gtk::Wi add_optional (M ("EXIFFILTER_CAMERA"), has_camera_, camera_); add_optional (M ("EXIFFILTER_LENS"), has_lens_, lens_); + add_optional (M ("EXIFFILTER_PATH"), has_path_, path_); imagetype_ = Gtk::manage (new MyComboBoxText()); imagetype_->append(Glib::ustring("(") + M("DYNPROFILEEDITOR_IMGTYPE_ANY") + ")"); @@ -93,6 +94,9 @@ void DynamicProfilePanel::EditDialog::set_rule ( has_lens_->set_active (rule.lens.enabled); lens_->set_text (rule.lens.value); + has_path_->set_active (rule.path.enabled); + path_->set_text (rule.path.value); + if (!rule.imagetype.enabled) { imagetype_->set_active(0); } else if (rule.imagetype.value == "STD") { @@ -136,6 +140,9 @@ DynamicProfileRule DynamicProfilePanel::EditDialog::get_rule() ret.lens.enabled = has_lens_->get_active(); ret.lens.value = lens_->get_text(); + ret.path.enabled = has_path_->get_active(); + ret.path.value = path_->get_text(); + ret.imagetype.enabled = imagetype_->get_active_row_number() > 0; switch (imagetype_->get_active_row_number()) { case 1: @@ -296,6 +303,16 @@ DynamicProfilePanel::DynamicProfilePanel(): *this, &DynamicProfilePanel::render_lens)); } + cell = Gtk::manage (new Gtk::CellRendererText()); + cols_count = treeview_.append_column (M ("EXIFFILTER_PATH"), *cell); + col = treeview_.get_column (cols_count - 1); + + if (col) { + col->set_cell_data_func ( + *cell, sigc::mem_fun ( + *this, &DynamicProfilePanel::render_path)); + } + cell = Gtk::manage (new Gtk::CellRendererText()); cols_count = treeview_.append_column (M ("EXIFFILTER_IMAGETYPE"), *cell); col = treeview_.get_column (cols_count - 1); @@ -375,6 +392,7 @@ void DynamicProfilePanel::update_rule (Gtk::TreeModel::Row row, row[columns_.expcomp] = rule.expcomp; row[columns_.camera] = rule.camera; row[columns_.lens] = rule.lens; + row[columns_.path] = rule.path; row[columns_.imagetype] = rule.imagetype; row[columns_.profilepath] = rule.profilepath; } @@ -398,6 +416,7 @@ DynamicProfileRule DynamicProfilePanel::to_rule (Gtk::TreeModel::Row row, ret.expcomp = row[columns_.expcomp]; ret.camera = row[columns_.camera]; ret.lens = row[columns_.lens]; + ret.path = row[columns_.path]; ret.profilepath = row[columns_.profilepath]; ret.imagetype = row[columns_.imagetype]; return ret; @@ -510,6 +529,12 @@ void DynamicProfilePanel::render_lens ( RENDER_OPTIONAL_ (lens); } +void DynamicProfilePanel::render_path ( + Gtk::CellRenderer *cell, const Gtk::TreeModel::iterator &iter) +{ + RENDER_OPTIONAL_ (path); +} + void DynamicProfilePanel::render_imagetype ( Gtk::CellRenderer *cell, const Gtk::TreeModel::iterator &iter) { diff --git a/rtgui/dynamicprofilepanel.h b/rtgui/dynamicprofilepanel.h index 972ca1c4a..03b9e7c62 100644 --- a/rtgui/dynamicprofilepanel.h +++ b/rtgui/dynamicprofilepanel.h @@ -55,6 +55,7 @@ private: add (expcomp); add (camera); add (lens); + add (path); add (profilepath); add (imagetype); } @@ -66,6 +67,7 @@ private: Gtk::TreeModelColumn> expcomp; Gtk::TreeModelColumn camera; Gtk::TreeModelColumn lens; + Gtk::TreeModelColumn path; Gtk::TreeModelColumn imagetype; Gtk::TreeModelColumn profilepath; }; @@ -78,6 +80,7 @@ private: void render_expcomp (Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter); void render_camera (Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter); void render_lens (Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter); + void render_path (Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter); void render_imagetype (Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter); void render_profilepath (Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter); @@ -114,6 +117,9 @@ private: Gtk::CheckButton *has_lens_; Gtk::Entry *lens_; + Gtk::CheckButton *has_path_; + Gtk::Entry *path_; + MyComboBoxText *imagetype_; ProfileStoreComboBox *profilepath_; diff --git a/rtgui/main-cli.cc b/rtgui/main-cli.cc index 8375ffe8b..cebced274 100644 --- a/rtgui/main-cli.cc +++ b/rtgui/main-cli.cc @@ -743,7 +743,7 @@ int processLineParams ( int argc, char **argv ) if (options.defProfRaw == DEFPROFILE_DYNAMIC) { rawParams->deleteInstance(); delete rawParams; - rawParams = ProfileStore::getInstance()->loadDynamicProfile (ii->getMetaData()); + rawParams = ProfileStore::getInstance()->loadDynamicProfile (ii->getMetaData(), inputFile); } std::cout << " Merging default raw processing profile." << std::endl; @@ -752,7 +752,7 @@ int processLineParams ( int argc, char **argv ) if (options.defProfImg == DEFPROFILE_DYNAMIC) { imgParams->deleteInstance(); delete imgParams; - imgParams = ProfileStore::getInstance()->loadDynamicProfile (ii->getMetaData()); + imgParams = ProfileStore::getInstance()->loadDynamicProfile (ii->getMetaData(), inputFile); } std::cout << " Merging default non-raw processing profile." << std::endl; diff --git a/rtgui/thumbnail.cc b/rtgui/thumbnail.cc index cc8e9ad81..2baf03247 100644 --- a/rtgui/thumbnail.cc +++ b/rtgui/thumbnail.cc @@ -266,7 +266,7 @@ rtengine::procparams::ProcParams* Thumbnail::createProcParamsForUpdate(bool retu // Should we ask all frame's MetaData ? imageMetaData = rtengine::FramesMetaData::fromFile (fname, nullptr, true); } - PartialProfile *pp = ProfileStore::getInstance()->loadDynamicProfile(imageMetaData); + PartialProfile *pp = ProfileStore::getInstance()->loadDynamicProfile(imageMetaData, fname); delete imageMetaData; int err = pp->pparams->save(outFName); pp->deleteInstance(); From d74524f2c57c4a4084352281ec7e6b3ba3f7e1ac Mon Sep 17 00:00:00 2001 From: Lawrence37 <45837045+Lawrence37@users.noreply.github.com> Date: Sun, 1 Jan 2023 01:50:11 -0800 Subject: [PATCH 11/17] Camconst support for multiple crops (#6473) Adapted from ART Co-authored-by: Alberto Griggio * camconst: support for multiple image sizes in raw_crop and masked_areas * Clean up code after porting raw crop changes * fixed raw crop for Canon R6 reduced-resolution raws * Add Canon EOS R5 1.6 crop raw crop & masked areas --- rtengine/camconst.cc | 195 ++++++++++++++++++++++++++++++----------- rtengine/camconst.h | 27 +++--- rtengine/camconst.json | 42 +++++++-- rtengine/rawimage.cc | 25 ++++-- 4 files changed, 213 insertions(+), 76 deletions(-) diff --git a/rtengine/camconst.cc b/rtengine/camconst.cc index aab2a252c..64fc4d4ba 100644 --- a/rtengine/camconst.cc +++ b/rtengine/camconst.cc @@ -28,8 +28,6 @@ namespace rtengine CameraConst::CameraConst() : pdafOffset(0) { memset(dcraw_matrix, 0, sizeof(dcraw_matrix)); - memset(raw_crop, 0, sizeof(raw_crop)); - memset(raw_mask, 0, sizeof(raw_mask)); white_max = 0; globalGreenEquilibration = -1; } @@ -192,6 +190,68 @@ CameraConst* CameraConst::parseEntry(const void *cJSON_, const char *make_model) std::unique_ptr cc(new CameraConst); cc->make_model = make_model; + const auto get_raw_crop = + [](int w, int h, const cJSON *ji, CameraConst *cc) -> bool + { + std::array rc; + + if (ji->type != cJSON_Array) { + //fprintf(stderr, "\"raw_crop\" must be an array\n"); + return false; + } + + int i; + + for (i = 0, ji = ji->child; i < 4 && ji != nullptr; i++, ji = ji->next) { + if (ji->type != cJSON_Number) { + //fprintf(stderr, "\"raw_crop\" array must contain numbers\n"); + return false; + } + + //cc->raw_crop[i] = ji->valueint; + rc[i] = ji->valueint; + } + + if (i != 4 || ji != nullptr) { + //fprintf(stderr, "\"raw_crop\" must contain 4 numbers\n"); + return false; + } + + cc->raw_crop[std::make_pair(w, h)] = rc; + return true; + }; + + const auto get_masked_areas = + [](int w, int h, const cJSON *ji, CameraConst *cc) -> bool + { + std::array, 2> rm; + + if (ji->type != cJSON_Array) { + //fprintf(stderr, "\"masked_areas\" must be an array\n"); + return false; + } + + int i; + + for (i = 0, ji = ji->child; i < 2 * 4 && ji != nullptr; i++, ji = ji->next) { + if (ji->type != cJSON_Number) { + //fprintf(stderr, "\"masked_areas\" array must contain numbers\n"); + return false; + } + + //cc->raw_mask[i / 4][i % 4] = ji->valueint; + rm[i / 4][i % 4] = ji->valueint; + } + + if (i % 4 != 0) { + //fprintf(stderr, "\"masked_areas\" array length must be divisable by 4\n"); + return false; + } + + cc->raw_mask[std::make_pair(w, h)] = rm; + return true; + }; + const cJSON *ji = cJSON_GetObjectItem(js, "dcraw_matrix"); if (ji) { @@ -216,24 +276,32 @@ CameraConst* CameraConst::parseEntry(const void *cJSON_, const char *make_model) if (ji) { if (ji->type != cJSON_Array) { - fprintf(stderr, "\"raw_crop\" must be an array\n"); + fprintf(stderr, "invalid entry for raw_crop.\n"); return nullptr; - } - - int i; - - for (i = 0, ji = ji->child; i < 4 && ji; i++, ji = ji->next) { - if (ji->type != cJSON_Number) { - fprintf(stderr, "\"raw_crop\" array must contain numbers\n"); - return nullptr; + } else if (!get_raw_crop(0, 0, ji, cc.get())) { + cJSON *je; + cJSON_ArrayForEach(je, ji) { + if (!cJSON_IsObject(je)) { + fprintf(stderr, "invalid entry for raw_crop.\n"); + return nullptr; + } else { + auto js = cJSON_GetObjectItem(je, "frame"); + if (!js || js->type != cJSON_Array || + cJSON_GetArraySize(js) != 2 || + !cJSON_IsNumber(cJSON_GetArrayItem(js, 0)) || + !cJSON_IsNumber(cJSON_GetArrayItem(js, 1))) { + fprintf(stderr, "invalid entry for raw_crop.\n"); + return nullptr; + } + int w = cJSON_GetArrayItem(js, 0)->valueint; + int h = cJSON_GetArrayItem(js, 1)->valueint; + js = cJSON_GetObjectItem(je, "crop"); + if (!js || !get_raw_crop(w, h, js, cc.get())) { + fprintf(stderr, "invalid entry for raw_crop.\n"); + return nullptr; + } + } } - - cc->raw_crop[i] = ji->valueint; - } - - if (i != 4 || ji) { - fprintf(stderr, "\"raw_crop\" must contain 4 numbers\n"); - return nullptr; } } @@ -241,24 +309,32 @@ CameraConst* CameraConst::parseEntry(const void *cJSON_, const char *make_model) if (ji) { if (ji->type != cJSON_Array) { - fprintf(stderr, "\"masked_areas\" must be an array\n"); + fprintf(stderr, "invalid entry for masked_areas.\n"); return nullptr; - } - - int i; - - for (i = 0, ji = ji->child; i < 2 * 4 && ji; i++, ji = ji->next) { - if (ji->type != cJSON_Number) { - fprintf(stderr, "\"masked_areas\" array must contain numbers\n"); - return nullptr; + } else if (!get_masked_areas(0, 0, ji, cc.get())) { + cJSON *je; + cJSON_ArrayForEach(je, ji) { + if (!cJSON_IsObject(je)) { + fprintf(stderr, "invalid entry for masked_areas.\n"); + return nullptr; + } else { + auto js = cJSON_GetObjectItem(je, "frame"); + if (!js || js->type != cJSON_Array || + cJSON_GetArraySize(js) != 2 || + !cJSON_IsNumber(cJSON_GetArrayItem(js, 0)) || + !cJSON_IsNumber(cJSON_GetArrayItem(js, 1))) { + fprintf(stderr, "invalid entry for masked_areas.\n"); + return nullptr; + } + int w = cJSON_GetArrayItem(js, 0)->valueint; + int h = cJSON_GetArrayItem(js, 1)->valueint; + js = cJSON_GetObjectItem(je, "areas"); + if (!js || !get_masked_areas(w, h, js, cc.get())) { + fprintf(stderr, "invalid entry for masked_areas.\n"); + return nullptr; + } + } } - - cc->raw_mask[i / 4][i % 4] = ji->valueint; - } - - if (i % 4 != 0) { - fprintf(stderr, "\"masked_areas\" array length must be divisible by 4\n"); - return nullptr; } } @@ -399,29 +475,41 @@ void CameraConst::update_pdafOffset(int other) pdafOffset = other; } -bool CameraConst::has_rawCrop() const + +bool CameraConst::has_rawCrop(int raw_width, int raw_height) const { - return raw_crop[0] != 0 || raw_crop[1] != 0 || raw_crop[2] != 0 || raw_crop[3] != 0; + return raw_crop.find(std::make_pair(raw_width, raw_height)) != raw_crop.end() || raw_crop.find(std::make_pair(0, 0)) != raw_crop.end(); } -void CameraConst::get_rawCrop(int& left_margin, int& top_margin, int& width, int& height) const + +void CameraConst::get_rawCrop(int raw_width, int raw_height, int &left_margin, int &top_margin, int &width, int &height) const { - left_margin = raw_crop[0]; - top_margin = raw_crop[1]; - width = raw_crop[2]; - height = raw_crop[3]; + auto it = raw_crop.find(std::make_pair(raw_width, raw_height)); + if (it == raw_crop.end()) { + it = raw_crop.find(std::make_pair(0, 0)); + } + if (it != raw_crop.end()) { + left_margin = it->second[0]; + top_margin = it->second[1]; + width = it->second[2]; + height = it->second[3]; + } else { + left_margin = top_margin = width = height = 0; + } } -bool CameraConst::has_rawMask(int idx) const + +bool CameraConst::has_rawMask(int raw_width, int raw_height, int idx) const { if (idx < 0 || idx > 1) { return false; } - return (raw_mask[idx][0] | raw_mask[idx][1] | raw_mask[idx][2] | raw_mask[idx][3]) != 0; + return raw_mask.find(std::make_pair(raw_width, raw_height)) != raw_mask.end() || raw_mask.find(std::make_pair(0, 0)) != raw_mask.end(); } -void CameraConst::get_rawMask(int idx, int& top, int& left, int& bottom, int& right) const + +void CameraConst::get_rawMask(int raw_width, int raw_height, int idx, int &top, int &left, int &bottom, int &right) const { top = left = bottom = right = 0; @@ -429,10 +517,17 @@ void CameraConst::get_rawMask(int idx, int& top, int& left, int& bottom, int& ri return; } - top = raw_mask[idx][0]; - left = raw_mask[idx][1]; - bottom = raw_mask[idx][2]; - right = raw_mask[idx][3]; + auto it = raw_mask.find(std::make_pair(raw_width, raw_height)); + if (it == raw_mask.end()) { + it = raw_mask.find(std::make_pair(0, 0)); + } + + if (it != raw_mask.end()) { + top = it->second[idx][0]; + left = it->second[idx][1]; + bottom = it->second[idx][2]; + right = it->second[idx][3]; + } } void CameraConst::update_Levels(const CameraConst *other) @@ -464,9 +559,7 @@ void CameraConst::update_Crop(CameraConst *other) return; } - if (other->has_rawCrop()) { - other->get_rawCrop(raw_crop[0], raw_crop[1], raw_crop[2], raw_crop[3]); - } + raw_crop.insert(other->raw_crop.begin(), other->raw_crop.end()); } bool CameraConst::get_Levels(camera_const_levels & lvl, int bw, int iso, float fnumber) const diff --git a/rtengine/camconst.h b/rtengine/camconst.h index aa0702439..273bdd7a1 100644 --- a/rtengine/camconst.h +++ b/rtengine/camconst.h @@ -1,9 +1,11 @@ -/* +/* -*- C++ -*- + * * This file is part of RawTherapee. */ #pragma once #include +#include #include #include @@ -17,17 +19,17 @@ class ustring; namespace rtengine { -struct camera_const_levels { - int levels[4]; -}; - class CameraConst final { private: + struct camera_const_levels { + int levels[4]; + }; + std::string make_model; short dcraw_matrix[12]; - int raw_crop[4]; - int raw_mask[2][4]; + std::map, std::array> raw_crop; + std::map, std::array, 2>> raw_mask; int white_max; std::map mLevels[2]; std::map mApertureScaling; @@ -47,10 +49,10 @@ public: const short *get_dcrawMatrix(void) const; const std::vector& get_pdafPattern() const; int get_pdafOffset() const {return pdafOffset;}; - bool has_rawCrop(void) const; - void get_rawCrop(int& left_margin, int& top_margin, int& width, int& height) const; - bool has_rawMask(int idx) const; - void get_rawMask(int idx, int& top, int& left, int& bottom, int& right) const; + bool has_rawCrop(int raw_width, int raw_height) const; + void get_rawCrop(int raw_width, int raw_height, int& left_margin, int& top_margin, int& width, int& height) const; + bool has_rawMask(int raw_width, int raw_height, int idx) const; + void get_rawMask(int raw_width, int raw_height, int idx, int& top, int& left, int& bottom, int& right) const; int get_BlackLevel(int idx, int iso_speed) const; int get_WhiteLevel(int idx, int iso_speed, float fnumber) const; bool has_globalGreenEquilibration() const; @@ -77,4 +79,5 @@ public: const CameraConst *get(const char make[], const char model[]) const; }; -} +} // namespace rtengine + diff --git a/rtengine/camconst.json b/rtengine/camconst.json index b7151f2c8..be7b3b800 100644 --- a/rtengine/camconst.json +++ b/rtengine/camconst.json @@ -70,6 +70,14 @@ Examples: // cropped so the "negative number" way is not totally safe. "raw_crop": [ 10, 20, 4000, 3000 ], + // multi-aspect support (added 2020-12-03) + // "frame" defines the full dimensions the crop applies to + // (with [0, 0] being the fallback crop if none of the other applies) + "raw_crop" : [ + { "frame" : [4100, 3050], "crop": [10, 20, 4050, 3020] }, + { "frame" : [0, 0], "crop": [10, 20, 4000, 3000] } + ] + // Almost same as MaskedAreas DNG tag, used for black level measuring. Here up to two areas can be defined // by tetrads of numbers: "masked_areas": [ 51, 2, 3804, 156, 51, 5794, 3804, 5792 ], @@ -84,6 +92,14 @@ Examples: // instead, to take care of possible light leaks from the light sensing area to the optically black (masked) // area or sensor imperfections at the outer borders. + // multi-aspect support (added 2020-12-03) + // "frame" defines the full dimensions the masked areas apply to + // (with [0, 0] being the fallback crop if none of the other applies) + "masked_areas" : [ + { "frame" : [4100, 3050], "areas": [10, 20, 4050, 3020] }, + { "frame" : [0, 0], "areas": [10, 20, 4000, 3000] } + ] + // list of indices of the rows with on-sensor PDAF pixels, for cameras that have such features. The indices here form a pattern that is repeated for the whole height of the sensor. The values are relative to the "pdaf_offset" value (see below) "pdaf_pattern" : [ 0,12,36,54,72,90,114,126,144,162,180,204,216,240,252,270,294,306,324,342,366,384,396,414,432,450,474,492,504,522,540,564,576,594,606,630 ], // index of the first row of the PDAF pattern in the sensor (0 is the topmost row). Allowed to be negative for convenience (this means that the first repetition of the pattern doesn't start from the first row) @@ -1216,16 +1232,28 @@ Camera constants: { // Quality C "make_model": "Canon EOS R5", "dcraw_matrix" : [9766, -2953, -1254, -4276, 12116, 2433, -437, 1336, 5131], - "raw_crop" : [ 128, 96, 8224, 5490 ], - "masked_areas" : [ 94, 20, 5578, 122 ], + "raw_crop" : [ + { "frame" : [ 8352, 5586 ], "crop" : [ 128, 96, 8224, 5490 ] }, + { "frame" : [ 5248, 3510 ], "crop" : [ 128, 96, 5120, 3382 ] } + ], + "masked_areas" : [ + { "frame" : [ 8352, 5586 ], "areas": [ 94, 20, 5578, 122 ] }, + { "frame" : [ 5248, 3510 ], "areas": [ 94, 20, 3510, 122 ] } + ], "ranges" : { "white" : 16382 } }, { // Quality C "make_model": "Canon EOS R6", "dcraw_matrix" : [8293, -1611, -1132, -4759, 12710, 2275, -1013, 2415, 5508], - "raw_crop": [ 72, 38, 5496, 3670 ], - "masked_areas" : [ 40, 10, 5534, 70 ], + "raw_crop": [ + { "frame": [5568, 3708], "crop" : [ 72, 38, 5496, 3670 ] }, + { "frame": [3584, 2386], "crop" : [ 156, 108, 3404, 2270 ] } + ], + "masked_areas" : [ + { "frame": [5568, 3708], "areas": [ 40, 10, 5534, 70 ] }, + { "frame": [3584, 2386], "areas": [ 40, 10, 2374, 110 ] } + ], "ranges" : { "white" : 16382 } }, @@ -1381,7 +1409,11 @@ Camera constants: { // Quality C "make_model": [ "FUJIFILM GFX 100", "FUJIFILM GFX100S" ], "dcraw_matrix" : [ 16212, -8423, -1583, -4336, 12583, 1937, -195, 726, 6199 ], // taken from ART - "raw_crop": [ 0, 2, 11664, 8734 ] + "raw_crop": [ + // multi-aspect crop to account for 16-shot pixel shift images + { "frame" : [11808, 8754], "crop" : [ 0, 2, 11664, 8734 ] }, + { "frame" : [23616, 17508], "crop" : [ 0, 4, 23328, 17468 ] } + ] }, { // Quality B diff --git a/rtengine/rawimage.cc b/rtengine/rawimage.cc index 2354f343a..8478d56ab 100644 --- a/rtengine/rawimage.cc +++ b/rtengine/rawimage.cc @@ -548,11 +548,18 @@ int RawImage::loadRaw(bool loadData, unsigned int imageNum, bool closeFile, Prog CameraConstantsStore* ccs = CameraConstantsStore::getInstance(); const CameraConst *cc = ccs->get(make, model); + bool raw_crop_cc = false; + int orig_raw_width = width; + int orig_raw_height = height; if (raw_image) { - if (cc && cc->has_rawCrop()) { + orig_raw_width = raw_width; + orig_raw_height = raw_height; + + if (cc && cc->has_rawCrop(raw_width, raw_height)) { + raw_crop_cc = true; int lm, tm, w, h; - cc->get_rawCrop(lm, tm, w, h); + cc->get_rawCrop(raw_width, raw_height, lm, tm, w, h); if (isXtrans()) { shiftXtransMatrix(6 - ((top_margin - tm) % 6), 6 - ((left_margin - lm) % 6)); @@ -584,9 +591,9 @@ int RawImage::loadRaw(bool loadData, unsigned int imageNum, bool closeFile, Prog } } - if (cc && cc->has_rawMask(0)) { - for (int i = 0; i < 8 && cc->has_rawMask(i); i++) { - cc->get_rawMask(i, mask[i][0], mask[i][1], mask[i][2], mask[i][3]); + if (cc && cc->has_rawMask(orig_raw_width, orig_raw_height, 0)) { + for (int i = 0; i < 2 && cc->has_rawMask(orig_raw_width, orig_raw_height, i); i++) { + cc->get_rawMask(orig_raw_width, orig_raw_height, i, mask[i][0], mask[i][1], mask[i][2], mask[i][3]); } } @@ -594,9 +601,10 @@ int RawImage::loadRaw(bool loadData, unsigned int imageNum, bool closeFile, Prog free(raw_image); raw_image = nullptr; } else { - if (get_maker() == "Sigma" && cc && cc->has_rawCrop()) { // foveon images + if (get_maker() == "Sigma" && cc && cc->has_rawCrop(width, height)) { // foveon images + raw_crop_cc = true; int lm, tm, w, h; - cc->get_rawCrop(lm, tm, w, h); + cc->get_rawCrop(width, height, lm, tm, w, h); left_margin = lm; top_margin = tm; @@ -692,11 +700,12 @@ int RawImage::loadRaw(bool loadData, unsigned int imageNum, bool closeFile, Prog printf("no constants in camconst.json exists for \"%s %s\" (relying only on dcraw defaults)\n", make, model); } + printf("raw dimensions: %d x %d\n", orig_raw_width, orig_raw_height); printf("black levels: R:%d G1:%d B:%d G2:%d (%s)\n", get_cblack(0), get_cblack(1), get_cblack(2), get_cblack(3), black_from_cc ? "provided by camconst.json" : "provided by dcraw"); printf("white levels: R:%d G1:%d B:%d G2:%d (%s)\n", get_white(0), get_white(1), get_white(2), get_white(3), white_from_cc ? "provided by camconst.json" : "provided by dcraw"); - printf("raw crop: %d %d %d %d (provided by %s)\n", left_margin, top_margin, iwidth, iheight, (cc && cc->has_rawCrop()) ? "camconst.json" : "dcraw"); + printf("raw crop: %d %d %d %d (provided by %s)\n", left_margin, top_margin, iwidth, iheight, raw_crop_cc ? "camconst.json" : "dcraw"); printf("color matrix provided by %s\n", (cc && cc->has_dcrawMatrix()) ? "camconst.json" : "dcraw"); } } From a9b8ece33583727ecccef9d3d1be374ce79cb391 Mon Sep 17 00:00:00 2001 From: Thanatomanic <6567747+Thanatomanic@users.noreply.github.com> Date: Sun, 1 Jan 2023 11:01:48 +0100 Subject: [PATCH 12/17] Add raw crop for EOS R3 Fully fixes #6420 --- rtengine/camconst.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rtengine/camconst.json b/rtengine/camconst.json index be7b3b800..69b272d23 100644 --- a/rtengine/camconst.json +++ b/rtengine/camconst.json @@ -1226,7 +1226,8 @@ Camera constants: { // Quality C "make_model": "Canon EOS R3", - "dcraw_matrix" : [9423,-2839,-1195,-4532,12377,2415,-483,1374,5276] + "dcraw_matrix" : [ 9423, -2839, -1195, -4532, 12377, 2415, -483, 1374, 5276 ], + "raw_crop": [ 160, 120, 6024, 4024 ] }, { // Quality C From 21a7c97ede0953e1c87e7c99d14cfc92d506b80d Mon Sep 17 00:00:00 2001 From: Thanatomanic <6567747+Thanatomanic@users.noreply.github.com> Date: Sun, 1 Jan 2023 11:43:54 +0100 Subject: [PATCH 13/17] Multiple crop support for ILCE-7S Fixes #3960 --- rtengine/camconst.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rtengine/camconst.json b/rtengine/camconst.json index 69b272d23..ebe195c60 100644 --- a/rtengine/camconst.json +++ b/rtengine/camconst.json @@ -3025,7 +3025,10 @@ Camera constants: { // Quality B, correction for frame width "make_model": [ "Sony ILCE-7S", "Sony ILCE-7SM2" ], "dcraw_matrix": [ 5838,-1430,-246,-3497,11477,2297,-748,1885,5778 ], // DNG_v9.2 D65 - "raw_crop": [ 0, 0, 4254, 2848 ], + "raw_crop" : [ + { "frame" : [ 2816, 1872 ], "crop" : [ 0, 0, 2792, 1872 ] }, + { "frame" : [ 4254, 2848 ], "crop" : [ 0, 0, 4254, 2848 ] } + ], "ranges": { "black": 512, "white": 16300 } }, From 3423a7ac55451ac5504f982787d1a39043a64d4f Mon Sep 17 00:00:00 2001 From: Thanatomanic <6567747+Thanatomanic@users.noreply.github.com> Date: Mon, 2 Jan 2023 21:24:15 +0100 Subject: [PATCH 14/17] Support for GX680 digital back including DCP (#6655) * Initial support for GX680 digital back With help from LibRaw for decoding support. Estimates for color calibration are very rough... * Small modification to black level, add DCP --- rtdata/dcpprofiles/FUJIFILM DBP for GX680.dcp | Bin 0 -> 65374 bytes rtengine/camconst.json | 6 +++ rtengine/dcraw.cc | 37 ++++++++++++++++++ rtengine/dcraw.h | 1 + 4 files changed, 44 insertions(+) create mode 100644 rtdata/dcpprofiles/FUJIFILM DBP for GX680.dcp diff --git a/rtdata/dcpprofiles/FUJIFILM DBP for GX680.dcp b/rtdata/dcpprofiles/FUJIFILM DBP for GX680.dcp new file mode 100644 index 0000000000000000000000000000000000000000..dcfc174c70ab74330c92ea3c193c86f98b29ae7a GIT binary patch literal 65374 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

M~b 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

cP)W8`;kHZ7UJH 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 Dvh#e+ literal 0 HcmV?d00001 diff --git a/rtengine/camconst.json b/rtengine/camconst.json index ebe195c60..7a143e850 100644 --- a/rtengine/camconst.json +++ b/rtengine/camconst.json @@ -1586,6 +1586,12 @@ Camera constants: "raw_crop": [ 0, 5, 7752, 5184 ] }, + { // Quality C + "make_model": "FUJIFILM DBP for GX680", + "dcraw_matrix": [ 12741, -4916, -1420, -8510, 16791, 1715, -1767, 2302, 7771 ], // same as S2Pro as per LibRaw + "ranges": { "white": 4096, "black": 132 } + }, + { // Quality C, Leica C-Lux names can differ? "make_model" : [ "LEICA C-LUX", "LEICA CAM-DC25" ], "dcraw_matrix" : [7790, -2736, -755, -3452, 11870, 1769, -628, 1647, 4898] diff --git a/rtengine/dcraw.cc b/rtengine/dcraw.cc index 4ab1694b8..bdd92c7da 100644 --- a/rtengine/dcraw.cc +++ b/rtengine/dcraw.cc @@ -2464,6 +2464,30 @@ void CLASS unpacked_load_raw() } } +// RT - from LibRaw +void CLASS unpacked_load_raw_FujiDBP() +/* +for Fuji DBP for GX680, aka DX-2000 + DBP_tile_width = 688; + DBP_tile_height = 3856; + DBP_n_tiles = 8; +*/ +{ + int scan_line, tile_n; + int nTiles = 8; + tile_width = raw_width / nTiles; + ushort *tile; + tile = (ushort *) calloc(raw_height, tile_width * 2); + for (tile_n = 0; tile_n < nTiles; tile_n++) { + read_shorts(tile, tile_width * raw_height); + for (scan_line = 0; scan_line < raw_height; scan_line++) { + memcpy(&raw_image[scan_line * raw_width + tile_n * tile_width], + &tile[scan_line * tile_width], tile_width * 2); + } + } + free(tile); + fseek(ifp, -2, SEEK_CUR); // avoid EOF error +} // RT void CLASS sony_arq_load_raw() @@ -10067,6 +10091,9 @@ canon_a5: } else if (!strcmp(model, "X-Pro3") || !strcmp(model, "X-T3") || !strcmp(model, "X-T30") || !strcmp(model, "X-T4") || !strcmp(model, "X100V") || !strcmp(model, "X-S10")) { width = raw_width = 6384; height = raw_height = 4182; + } else if (!strcmp(model, "DBP for GX680")) { // Special case for #4204 + width = raw_width = 5504; + height = raw_height = 3856; } top_margin = (raw_height - height) >> 2 << 1; left_margin = (raw_width - width ) >> 2 << 1; @@ -10074,6 +10101,16 @@ canon_a5: if (width == 4032 || width == 4952 || width == 6032 || width == 8280) left_margin = 0; if (width == 3328 && (width -= 66)) left_margin = 34; if (width == 4936) left_margin = 4; + if (width == 5504) { // #4204, taken from LibRaw + left_margin = 32; + top_margin = 8; + width = raw_width - 2*left_margin; + height = raw_height - 2*top_margin; + load_raw = &CLASS unpacked_load_raw_FujiDBP; + filters = 0x16161616; + load_flags = 0; + flip = 6; + } if (!strcmp(model,"HS50EXR") || !strcmp(model,"F900EXR")) { width += 2; diff --git a/rtengine/dcraw.h b/rtengine/dcraw.h index 849012cb7..f78bd8aa6 100644 --- a/rtengine/dcraw.h +++ b/rtengine/dcraw.h @@ -432,6 +432,7 @@ void parse_hasselblad_gain(); void hasselblad_load_raw(); void leaf_hdr_load_raw(); void unpacked_load_raw(); +void unpacked_load_raw_FujiDBP(); void sinar_4shot_load_raw(); void imacon_full_load_raw(); void packed_load_raw(); From 2101b846c386035f8d1ca5f2d68fe9fc3227d43d Mon Sep 17 00:00:00 2001 From: Niklas Haas Date: Mon, 2 Jan 2023 21:27:12 +0100 Subject: [PATCH 15/17] 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> --- rtdata/languages/default | 8 +++ rtengine/imagedata.cc | 39 +++++++++++---- rtengine/imagedata.h | 4 +- rtgui/batchqueueentry.cc | 2 +- rtgui/filebrowser.cc | 89 +++++++++++++++++++++++++-------- rtgui/filebrowser.h | 5 ++ rtgui/filebrowserentry.cc | 4 +- rtgui/options.cc | 17 +++++++ rtgui/options.h | 11 +++++ rtgui/thumbbrowserbase.cc | 44 ++++++++++++++++- rtgui/thumbbrowserbase.h | 3 +- rtgui/thumbbrowserentrybase.cc | 5 +- rtgui/thumbbrowserentrybase.h | 32 ++++++++++-- rtgui/thumbnail.cc | 90 ++++++++++++++++++++-------------- rtgui/thumbnail.h | 3 ++ 15 files changed, 275 insertions(+), 81 deletions(-) diff --git a/rtdata/languages/default b/rtdata/languages/default index e5e053655..a33b2a9cd 100644 --- a/rtdata/languages/default +++ b/rtdata/languages/default @@ -166,6 +166,7 @@ FILEBROWSER_POPUPREMOVE;Delete permanently FILEBROWSER_POPUPREMOVEINCLPROC;Delete permanently, including queue-processed version FILEBROWSER_POPUPRENAME;Rename FILEBROWSER_POPUPSELECTALL;Select all +FILEBROWSER_POPUPSORTBY;Sort Files FILEBROWSER_POPUPTRASH;Move to trash FILEBROWSER_POPUPUNRANK;Unrank FILEBROWSER_POPUPUNTRASH;Remove from trash @@ -2060,6 +2061,13 @@ SAVEDLG_WARNFILENAME;File will be named SHCSELECTOR_TOOLTIP;Click right mouse button to reset the position of those 3 sliders. SOFTPROOF_GAMUTCHECK_TOOLTIP;Highlight pixels with out-of-gamut colors with respect to:\n- the printer profile, if one is set and soft-proofing is enabled,\n- the output profile, if a printer profile is not set and soft-proofing is enabled,\n- the monitor profile, if soft-proofing is disabled. SOFTPROOF_TOOLTIP;Soft-proofing simulates the appearance of the image:\n- when printed, if a printer profile is set in Preferences > Color Management,\n- when viewed on a display that uses the current output profile, if a printer profile is not set. +SORT_ASCENDING;Ascending +SORT_BY_NAME;By Name +SORT_BY_DATE;By Date +SORT_BY_EXIF;By EXIF +SORT_BY_RANK;By Rank +SORT_BY_LABEL;By Color Label +SORT_DESCENDING;Descending TC_PRIM_BLUX;Bx TC_PRIM_BLUY;By TC_PRIM_GREX;Gx diff --git a/rtengine/imagedata.cc b/rtengine/imagedata.cc index 3c10e7dc0..fb2fcaf3a 100644 --- a/rtengine/imagedata.cc +++ b/rtengine/imagedata.cc @@ -19,6 +19,7 @@ #include #include +#include #include @@ -57,7 +58,8 @@ template T getFromFrame( const std::vector>& frames, std::size_t frame, - const std::function& function + const std::function& function, + T defval = {} ) { if (frame < frames.size()) { @@ -66,7 +68,7 @@ T getFromFrame( if (!frames.empty()) { return function(*frames[0]); } - return {}; + return defval; } const std::string& validateUft8(const std::string& str, const std::string& on_error = "???") @@ -85,11 +87,21 @@ FramesMetaData* FramesMetaData::fromFile(const Glib::ustring& fname, std::unique return new FramesData(fname, std::move(rml), firstFrameOnly); } -FrameData::FrameData(rtexif::TagDirectory* frameRootDir_, rtexif::TagDirectory* rootDir, rtexif::TagDirectory* firstRootDir) : +static struct tm timeFromTS(const time_t ts) +{ +#if !defined(WIN32) + struct tm tm; + return *gmtime_r(&ts, &tm); +#else + return *gmtime(&ts); +#endif +} + +FrameData::FrameData(rtexif::TagDirectory* frameRootDir_, rtexif::TagDirectory* rootDir, rtexif::TagDirectory* firstRootDir, time_t ts) : frameRootDir(frameRootDir_), iptc(nullptr), - time{}, - timeStamp{}, + time(timeFromTS(ts)), + timeStamp(ts), iso_speed(0), aperture(0.), focal_len(0.), @@ -1068,7 +1080,8 @@ tm FramesData::getDateTime(unsigned int frame) const [](const FrameData& frame_data) { return frame_data.getDateTime(); - } + }, + modTime ); } @@ -1080,7 +1093,8 @@ time_t FramesData::getDateTimeAsTS(unsigned int frame) const [](const FrameData& frame_data) { return frame_data.getDateTimeAsTS(); - } + }, + modTimeStamp ); } @@ -1366,6 +1380,11 @@ failure: FramesData::FramesData(const Glib::ustring& fname, std::unique_ptr rml, bool firstFrameOnly) : iptc(nullptr), dcrawFrameCount(0) { + GStatBuf statbuf = {}; + g_stat(fname.c_str(), &statbuf); + modTimeStamp = statbuf.st_mtime; + modTime = timeFromTS(modTimeStamp); + if (rml && (rml->exifBase >= 0 || rml->ciffBase >= 0)) { FILE* f = g_fopen(fname.c_str(), "rb"); @@ -1384,7 +1403,7 @@ FramesData::FramesData(const Glib::ustring& fname, std::unique_ptr(new FrameData(currFrame, currFrame->getRoot(), roots.at(0)))); + frames.push_back(std::unique_ptr(new FrameData(currFrame, currFrame->getRoot(), roots.at(0), modTimeStamp))); } for (auto currRoot : roots) { @@ -1410,7 +1429,7 @@ FramesData::FramesData(const Glib::ustring& fname, std::unique_ptr(new FrameData(currFrame, currFrame->getRoot(), roots.at(0)))); + frames.push_back(std::unique_ptr(new FrameData(currFrame, currFrame->getRoot(), roots.at(0), modTimeStamp))); } rewind(exifManager.f); // Not sure this is necessary @@ -1430,7 +1449,7 @@ FramesData::FramesData(const Glib::ustring& fname, std::unique_ptr(new FrameData(currFrame, currFrame->getRoot(), roots.at(0)))); + frames.push_back(std::unique_ptr(new FrameData(currFrame, currFrame->getRoot(), roots.at(0), modTimeStamp))); } for (auto currRoot : roots) { diff --git a/rtengine/imagedata.h b/rtengine/imagedata.h index 4bf9bdf5b..752fafab3 100644 --- a/rtengine/imagedata.h +++ b/rtengine/imagedata.h @@ -72,7 +72,7 @@ protected: public: - FrameData (rtexif::TagDirectory* frameRootDir, rtexif::TagDirectory* rootDir, rtexif::TagDirectory* firstRootDir); + FrameData (rtexif::TagDirectory* frameRootDir, rtexif::TagDirectory* rootDir, rtexif::TagDirectory* firstRootDir, time_t ts = 0); virtual ~FrameData (); bool getPixelShift () const; @@ -109,6 +109,8 @@ private: std::vector roots; IptcData* iptc; unsigned int dcrawFrameCount; + struct tm modTime; + time_t modTimeStamp; public: explicit FramesData (const Glib::ustring& fname, std::unique_ptr rml = nullptr, bool firstFrameOnly = false); diff --git a/rtgui/batchqueueentry.cc b/rtgui/batchqueueentry.cc index 31a6f40c7..9fe4dd605 100644 --- a/rtgui/batchqueueentry.cc +++ b/rtgui/batchqueueentry.cc @@ -34,7 +34,7 @@ bool BatchQueueEntry::iconsLoaded(false); Glib::RefPtr BatchQueueEntry::savedAsIcon; BatchQueueEntry::BatchQueueEntry (rtengine::ProcessingJob* pjob, const rtengine::procparams::ProcParams& pparams, Glib::ustring fname, int prevw, int prevh, Thumbnail* thm, bool overwrite) : - ThumbBrowserEntryBase(fname), + ThumbBrowserEntryBase(fname, thm), opreview(nullptr), origpw(prevw), origph(prevh), diff --git a/rtgui/filebrowser.cc b/rtgui/filebrowser.cc index 66c84d86e..ac4a27dec 100644 --- a/rtgui/filebrowser.cc +++ b/rtgui/filebrowser.cc @@ -167,6 +167,41 @@ FileBrowser::FileBrowser () : pmenu->attach (*Gtk::manage(selall = new Gtk::MenuItem (M("FILEBROWSER_POPUPSELECTALL"))), 0, 1, p, p + 1); p++; + /*********************** + * sort + ***********************/ + const std::array cnameSortOrders = { + M("SORT_ASCENDING"), + M("SORT_DESCENDING"), + }; + + const std::array cnameSortMethods = { + M("SORT_BY_NAME"), + M("SORT_BY_DATE"), + M("SORT_BY_EXIF"), + M("SORT_BY_RANK"), + M("SORT_BY_LABEL"), + }; + + pmenu->attach (*Gtk::manage(menuSort = new Gtk::MenuItem (M("FILEBROWSER_POPUPSORTBY"))), 0, 1, p, p + 1); + p++; + Gtk::Menu* submenuSort = Gtk::manage (new Gtk::Menu ()); + Gtk::RadioButtonGroup sortOrderGroup, sortMethodGroup; + for (size_t i = 0; i < cnameSortOrders.size(); i++) { + submenuSort->attach (*Gtk::manage(sortOrder[i] = new Gtk::RadioMenuItem (sortOrderGroup, cnameSortOrders[i])), 0, 1, p, p + 1); + p++; + sortOrder[i]->set_active (i == options.sortDescending); + } + submenuSort->attach (*Gtk::manage(new Gtk::SeparatorMenuItem ()), 0, 1, p, p + 1); + p++; + for (size_t i = 0; i < cnameSortMethods.size(); i++) { + submenuSort->attach (*Gtk::manage(sortMethod[i] = new Gtk::RadioMenuItem (sortMethodGroup, cnameSortMethods[i])), 0, 1, p, p + 1); + p++; + sortMethod[i]->set_active (i == options.sortMethod); + } + submenuSort->show_all (); + menuSort->set_submenu (*submenuSort); + /*********************** * rank ***********************/ @@ -427,6 +462,14 @@ FileBrowser::FileBrowser () : inspect->signal_activate().connect (sigc::bind(sigc::mem_fun(*this, &FileBrowser::menuItemActivated), inspect)); } + for (int i = 0; i < 2; i++) { + sortOrder[i]->signal_activate().connect (sigc::bind(sigc::mem_fun(*this, &FileBrowser::menuItemActivated), sortOrder[i])); + } + + for (int i = 0; i < Options::SORT_METHOD_COUNT; i++) { + sortMethod[i]->signal_activate().connect (sigc::bind(sigc::mem_fun(*this, &FileBrowser::menuItemActivated), sortMethod[i])); + } + for (int i = 0; i < 6; i++) { rank[i]->signal_activate().connect (sigc::bind(sigc::mem_fun(*this, &FileBrowser::menuItemActivated), rank[i])); } @@ -610,27 +653,7 @@ void FileBrowser::addEntry_ (FileBrowserEntry* entry) entry->getThumbButtonSet()->setButtonListener(this); entry->resize(getThumbnailHeight()); entry->filtered = !checkFilter(entry); - - // find place in abc order - { - MYWRITERLOCK(l, entryRW); - - fd.insert( - std::lower_bound( - fd.begin(), - fd.end(), - entry, - [](const ThumbBrowserEntryBase* a, const ThumbBrowserEntryBase* b) - { - return *a < *b; - } - ), - entry - ); - - initEntry(entry); - } - redraw(entry); + insertEntry(entry); } FileBrowserEntry* FileBrowser::delEntry (const Glib::ustring& fname) @@ -724,6 +747,18 @@ void FileBrowser::menuItemActivated (Gtk::MenuItem* m) return; } + for (int i = 0; i < 2; i++) + if (m == sortOrder[i]) { + sortOrderRequested (i); + return; + } + + for (int i = 0; i < Options::SORT_METHOD_COUNT; i++) + if (m == sortMethod[i]) { + sortMethodRequested (i); + return; + } + for (int i = 0; i < 6; i++) if (m == rank[i]) { rankingRequested (mselected, i); @@ -1632,6 +1667,18 @@ void FileBrowser::fromTrashRequested (std::vector tbe) applyFilter (filter); } +void FileBrowser::sortMethodRequested (int method) +{ + options.sortMethod = Options::SortMethod(method); + resort (); +} + +void FileBrowser::sortOrderRequested (int order) +{ + options.sortDescending = !!order; + resort (); +} + void FileBrowser::rankingRequested (std::vector tbe, int rank) { diff --git a/rtgui/filebrowser.h b/rtgui/filebrowser.h index 4602ba9bb..0df1cf9eb 100644 --- a/rtgui/filebrowser.h +++ b/rtgui/filebrowser.h @@ -83,9 +83,12 @@ protected: Gtk::MenuItem* open; Gtk::MenuItem* inspect; Gtk::MenuItem* selall; + Gtk::RadioMenuItem* sortMethod[Options::SORT_METHOD_COUNT]; + Gtk::RadioMenuItem* sortOrder[2]; Gtk::MenuItem* copyTo; Gtk::MenuItem* moveTo; + Gtk::MenuItem* menuSort; Gtk::MenuItem* menuRank; Gtk::MenuItem* menuLabel; Gtk::MenuItem* menuFileOperations; @@ -131,6 +134,8 @@ protected: void toTrashRequested (std::vector tbe); void fromTrashRequested (std::vector tbe); + void sortMethodRequested (int method); + void sortOrderRequested (int order); void rankingRequested (std::vector tbe, int rank); void colorlabelRequested (std::vector tbe, int colorlabel); void requestRanking (int rank); diff --git a/rtgui/filebrowserentry.cc b/rtgui/filebrowserentry.cc index bf3f11a79..b89fe340d 100644 --- a/rtgui/filebrowserentry.cc +++ b/rtgui/filebrowserentry.cc @@ -45,10 +45,8 @@ Glib::RefPtr FileBrowserEntry::hdr; Glib::RefPtr FileBrowserEntry::ps; FileBrowserEntry::FileBrowserEntry (Thumbnail* thm, const Glib::ustring& fname) - : ThumbBrowserEntryBase (fname), wasInside(false), iatlistener(nullptr), press_x(0), press_y(0), action_x(0), action_y(0), rot_deg(0.0), landscape(true), cropParams(new rtengine::procparams::CropParams), cropgl(nullptr), state(SNormal), crop_custom_ratio(0.f) + : ThumbBrowserEntryBase (fname, thm), wasInside(false), iatlistener(nullptr), press_x(0), press_y(0), action_x(0), action_y(0), rot_deg(0.0), landscape(true), cropParams(new rtengine::procparams::CropParams), cropgl(nullptr), state(SNormal), crop_custom_ratio(0.f) { - thumbnail = thm; - feih = new FileBrowserEntryIdleHelper; feih->fbentry = this; feih->destroyed = false; diff --git a/rtgui/options.cc b/rtgui/options.cc index a1cc88c03..b2ef17d77 100644 --- a/rtgui/options.cc +++ b/rtgui/options.cc @@ -685,6 +685,8 @@ void Options::setDefaults() lastICCProfCreatorDir = ""; gimpPluginShowInfoDialog = true; maxRecentFolders = 15; + sortMethod = SORT_BY_NAME; + sortDescending = false; rtSettings.lensfunDbDirectory = ""; // set also in main.cc and main-cli.cc cropGuides = CROP_GUIDE_FULL; cropAutoFit = false; @@ -1150,6 +1152,19 @@ void Options::readFromFile(Glib::ustring fname) if (keyFile.has_key("File Browser", "RecentFolders")) { recentFolders = keyFile.get_string_list("File Browser", "RecentFolders"); } + + if (keyFile.has_key("File Browser", "SortMethod")) { + int v = keyFile.get_integer("File Browser", "SortMethod"); + if (v < int(0) || v >= int(SORT_METHOD_COUNT)) { + sortMethod = SORT_BY_NAME; + } else { + sortMethod = SortMethod(v); + } + } + + if (keyFile.has_key("File Browser", "SortDescending")) { + sortDescending = keyFile.get_boolean("File Browser", "SortDescending"); + } } if (keyFile.has_group("Clipping Indication")) { @@ -2217,6 +2232,8 @@ void Options::saveToFile(Glib::ustring fname) keyFile.set_string_list("File Browser", "RecentFolders", temp); } + keyFile.set_integer("File Browser", "SortMethod", sortMethod); + keyFile.set_boolean("File Browser", "SortDescending", sortDescending); keyFile.set_integer("Clipping Indication", "HighlightThreshold", highlightThreshold); keyFile.set_integer("Clipping Indication", "ShadowThreshold", shadowThreshold); keyFile.set_boolean("Clipping Indication", "BlinkClipped", blinkClipped); diff --git a/rtgui/options.h b/rtgui/options.h index bc5e41c91..d7523e699 100644 --- a/rtgui/options.h +++ b/rtgui/options.h @@ -452,6 +452,17 @@ public: size_t maxRecentFolders; // max. number of recent folders stored in options file std::vector recentFolders; // List containing all recent folders + enum SortMethod { + SORT_BY_NAME, + SORT_BY_DATE, + SORT_BY_EXIF, + SORT_BY_RANK, + SORT_BY_LABEL, + SORT_METHOD_COUNT, + }; + SortMethod sortMethod; // remembers current state of file browser + bool sortDescending; + Options (); diff --git a/rtgui/thumbbrowserbase.cc b/rtgui/thumbbrowserbase.cc index 06c662e51..8f3499c2a 100644 --- a/rtgui/thumbbrowserbase.cc +++ b/rtgui/thumbbrowserbase.cc @@ -1091,6 +1091,25 @@ bool ThumbBrowserBase::Internal::on_scroll_event (GdkEventScroll* event) } +void ThumbBrowserBase::resort () +{ + { + MYWRITERLOCK(l, entryRW); + + std::sort( + fd.begin(), + fd.end(), + [](const ThumbBrowserEntryBase* a, const ThumbBrowserEntryBase* b) + { + bool lt = a->compare(*b, options.sortMethod); + return options.sortDescending ? !lt : lt; + } + ); + } + + redraw (); +} + void ThumbBrowserBase::redraw (ThumbBrowserEntryBase* entry) { @@ -1218,9 +1237,30 @@ void ThumbBrowserBase::enableTabMode(bool enable) } } -void ThumbBrowserBase::initEntry (ThumbBrowserEntryBase* entry) +void ThumbBrowserBase::insertEntry (ThumbBrowserEntryBase* entry) { - entry->setOffset ((int)(hscroll.get_value()), (int)(vscroll.get_value())); + // find place in sort order + { + MYWRITERLOCK(l, entryRW); + + fd.insert( + std::lower_bound( + fd.begin(), + fd.end(), + entry, + [](const ThumbBrowserEntryBase* a, const ThumbBrowserEntryBase* b) + { + bool lt = a->compare(*b, options.sortMethod); + return options.sortDescending ? !lt : lt; + } + ), + entry + ); + + entry->setOffset ((int)(hscroll.get_value()), (int)(vscroll.get_value())); + } + + redraw (); } void ThumbBrowserBase::getScrollPosition (double& h, double& v) diff --git a/rtgui/thumbbrowserbase.h b/rtgui/thumbbrowserbase.h index 2d41cdfab..8c1ec49c8 100644 --- a/rtgui/thumbbrowserbase.h +++ b/rtgui/thumbbrowserbase.h @@ -208,12 +208,13 @@ public: return fd; } void on_style_updated () override; + void resort (); // re-apply sort method void redraw (ThumbBrowserEntryBase* entry = nullptr); // arrange files and draw area void refreshThumbImages (); // refresh thumbnail sizes, re-generate thumbnail images, arrange and draw void refreshQuickThumbImages (); // refresh thumbnail sizes, re-generate thumbnail images, arrange and draw void refreshEditedState (const std::set& efiles); - void initEntry (ThumbBrowserEntryBase* entry); + void insertEntry (ThumbBrowserEntryBase* entry); void getScrollPosition (double& h, double& v); void setScrollPosition (double h, double v); diff --git a/rtgui/thumbbrowserentrybase.cc b/rtgui/thumbbrowserentrybase.cc index 306b491be..3d1e6bdc4 100644 --- a/rtgui/thumbbrowserentrybase.cc +++ b/rtgui/thumbbrowserentrybase.cc @@ -119,7 +119,7 @@ Glib::ustring getPaddedName(const Glib::ustring& name) } -ThumbBrowserEntryBase::ThumbBrowserEntryBase (const Glib::ustring& fname) : +ThumbBrowserEntryBase::ThumbBrowserEntryBase (const Glib::ustring& fname, Thumbnail *thm) : fnlabw(0), fnlabh(0), dtlabw(0), @@ -153,7 +153,8 @@ ThumbBrowserEntryBase::ThumbBrowserEntryBase (const Glib::ustring& fname) : bbPreview(nullptr), cursor_type(CSUndefined), collate_name(getPaddedName(dispname).casefold_collate_key()), - thumbnail(nullptr), + collate_exif(getPaddedName(thm->getExifString()).casefold_collate_key()), + thumbnail(thm), filename(fname), selected(false), drawable(false), diff --git a/rtgui/thumbbrowserentrybase.h b/rtgui/thumbbrowserentrybase.h index 764f806fd..3db03a96e 100644 --- a/rtgui/thumbbrowserentrybase.h +++ b/rtgui/thumbbrowserentrybase.h @@ -26,6 +26,8 @@ #include "guiutils.h" #include "lwbuttonset.h" #include "threadutils.h" +#include "options.h" +#include "thumbnail.h" #include "../rtengine/coord2d.h" @@ -95,6 +97,7 @@ protected: private: const std::string collate_name; + const std::string collate_exif; public: @@ -117,7 +120,7 @@ public: bool updatepriority; eWithFilename withFilename; - explicit ThumbBrowserEntryBase (const Glib::ustring& fname); + explicit ThumbBrowserEntryBase (const Glib::ustring& fname, Thumbnail *thm); virtual ~ThumbBrowserEntryBase (); void setParent (ThumbBrowserBase* l) @@ -174,9 +177,32 @@ public: void setPosition (int x, int y, int w, int h); void setOffset (int x, int y); - bool operator <(const ThumbBrowserEntryBase& other) const + bool compare (const ThumbBrowserEntryBase& other, Options::SortMethod method) const { - return collate_name < other.collate_name; + int cmp = 0; + switch (method){ + case Options::SORT_BY_NAME: + return collate_name < other.collate_name; + case Options::SORT_BY_DATE: + cmp = thumbnail->getDateTime().compare(other.thumbnail->getDateTime()); + break; + case Options::SORT_BY_EXIF: + cmp = collate_exif.compare(other.collate_exif); + break; + case Options::SORT_BY_RANK: + cmp = thumbnail->getRank() - other.thumbnail->getRank(); + break; + case Options::SORT_BY_LABEL: + cmp = thumbnail->getColorLabel() - other.thumbnail->getColorLabel(); + break; + case Options::SORT_METHOD_COUNT: abort(); + } + + // Always fall back to sorting by name + if (!cmp) + cmp = collate_name.compare(other.collate_name); + + return cmp < 0; } virtual void refreshThumbnailImage () = 0; diff --git a/rtgui/thumbnail.cc b/rtgui/thumbnail.cc index 2baf03247..30766ebc9 100644 --- a/rtgui/thumbnail.cc +++ b/rtgui/thumbnail.cc @@ -31,6 +31,7 @@ #include "../rtengine/procparams.h" #include "../rtengine/rtthumbnail.h" #include +#include #include "../rtengine/dynamicprofile.h" #include "../rtengine/profilestore.h" @@ -718,11 +719,44 @@ rtengine::IImage8* Thumbnail::upgradeThumbImage (const rtengine::procparams::Pro void Thumbnail::generateExifDateTimeStrings () { + if (cfs.timeValid) { + std::string dateFormat = options.dateFormat; + std::ostringstream ostr; + bool spec = false; - exifString = ""; - dateTimeString = ""; + 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; } @@ -731,33 +765,6 @@ void Thumbnail::generateExifDateTimeStrings () 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 } - - 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 (); } const Glib::ustring& Thumbnail::getExifString () const @@ -772,6 +779,12 @@ 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) { @@ -802,6 +815,16 @@ int Thumbnail::infoFromImage (const Glib::ustring& fname, std::unique_ptrgetDateTimeAsTS() > 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 (); @@ -814,18 +837,11 @@ int Thumbnail::infoFromImage (const Glib::ustring& fname, std::unique_ptrgetPixelShift (); cfs.frameCount = idata->getFrameCount (); cfs.sampleFormat = idata->getSampleFormat (); - 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; - cfs.exifValid = true; 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; diff --git a/rtgui/thumbnail.h b/rtgui/thumbnail.h index cda69f030..491151028 100644 --- a/rtgui/thumbnail.h +++ b/rtgui/thumbnail.h @@ -22,6 +22,7 @@ #include #include +#include #include "cacheimagedata.h" #include "threadutils.h" @@ -73,6 +74,7 @@ class Thumbnail // exif & date/time strings Glib::ustring exifString; Glib::ustring dateTimeString; + Glib::DateTime dateTime; bool initial_; @@ -124,6 +126,7 @@ public: const Glib::ustring& getExifString () const; const Glib::ustring& getDateTimeString () const; + const Glib::DateTime& getDateTime () const; void getCamWB (double& temp, double& green) const; void getAutoWB (double& temp, double& green, double equal, double tempBias); void getSpotWB (int x, int y, int rect, double& temp, double& green); From 8d29d361a84b21d6c248e581f81d2ce4c1cf54e2 Mon Sep 17 00:00:00 2001 From: Ingo Weyrich Date: Mon, 2 Jan 2023 21:30:06 +0100 Subject: [PATCH 16/17] Support dnggainmap (embedded correction) for Bayer files (#6382) * dng gainmap support, #6379 * dng GainMap: control sensitivity of checkbox, #6379 * dng GainMap: partial paste * dng GainMap: moved isGainMapSupported() from dcraw.h to dcraw.cc * RawImageSource::applyDngGainMap: small speedup * Change GUI to separate gainmap from other flat-field; also reorder checkbox Co-authored-by: Thanatomanic <6567747+Thanatomanic@users.noreply.github.com> --- rtdata/languages/default | 3 + rtengine/array2D.h | 8 +++ rtengine/dcraw.cc | 109 +++++++++++++++++++++++++++++++++- rtengine/dcraw.h | 20 ++++++- rtengine/dnggainmap.h | 43 ++++++++++++++ rtengine/imagesource.h | 1 + rtengine/improccoordinator.cc | 2 +- rtengine/procparams.cc | 4 ++ rtengine/procparams.h | 1 + rtengine/rawimage.h | 5 -- rtengine/rawimagesource.cc | 35 ++++++++++- rtengine/rawimagesource.h | 4 ++ rtengine/rtengine.h | 2 +- rtengine/stdimagesource.h | 5 ++ rtgui/flatfield.cc | 36 ++++++++++- rtgui/flatfield.h | 8 ++- rtgui/paramsedited.cc | 8 ++- rtgui/paramsedited.h | 1 + rtgui/partialpastedlg.cc | 9 +++ rtgui/partialpastedlg.h | 3 +- rtgui/toolpanelcoord.cc | 11 ++-- rtgui/toolpanelcoord.h | 2 +- 22 files changed, 299 insertions(+), 21 deletions(-) create mode 100644 rtengine/dnggainmap.h diff --git a/rtdata/languages/default b/rtdata/languages/default index a33b2a9cd..82216f18f 100644 --- a/rtdata/languages/default +++ b/rtdata/languages/default @@ -1406,6 +1406,7 @@ HISTORY_MSG_DEHAZE_STRENGTH;Dehaze - Strength HISTORY_MSG_DUALDEMOSAIC_AUTO_CONTRAST;Dual demosaic - Auto threshold HISTORY_MSG_DUALDEMOSAIC_CONTRAST;Dual demosaic - Contrast threshold HISTORY_MSG_EDGEFFECT;Edge Attenuation response +HISTORY_MSG_FF_FROMMETADATA;Flat-Field - From Metadata HISTORY_MSG_FILMNEGATIVE_BALANCE;FN - Reference output HISTORY_MSG_FILMNEGATIVE_COLORSPACE;Film negative color space HISTORY_MSG_FILMNEGATIVE_ENABLED;Film Negative @@ -1736,6 +1737,7 @@ PARTIALPASTE_EXPOSURE;Exposure PARTIALPASTE_FILMNEGATIVE;Film negative PARTIALPASTE_FILMSIMULATION;Film simulation PARTIALPASTE_FLATFIELDAUTOSELECT;Flat-field auto-selection +PARTIALPASTE_FLATFIELDFROMMETADATA;Flat-field from Metadata PARTIALPASTE_FLATFIELDBLURRADIUS;Flat-field blur radius PARTIALPASTE_FLATFIELDBLURTYPE;Flat-field blur type PARTIALPASTE_FLATFIELDCLIPCONTROL;Flat-field clip control @@ -2481,6 +2483,7 @@ TP_FLATFIELD_BT_VERTHORIZ;Vertical + Horizontal TP_FLATFIELD_BT_VERTICAL;Vertical TP_FLATFIELD_CLIPCONTROL;Clip control TP_FLATFIELD_CLIPCONTROL_TOOLTIP;Clip control avoids clipped highlights caused by applying the flat field. If there are already clipped highlights before applying the flat field, value 0 is used. +TP_FLATFIELD_FROMMETADATA;From Metadata TP_FLATFIELD_LABEL;Flat-Field TP_GENERAL_11SCALE_TOOLTIP;The effects of this tool are only visible or only accurate at a preview scale of 1:1. TP_GRADIENT_CENTER;Center diff --git a/rtengine/array2D.h b/rtengine/array2D.h index 10d797999..eee6c3210 100644 --- a/rtengine/array2D.h +++ b/rtengine/array2D.h @@ -248,6 +248,14 @@ public: return *this; } + // import from flat data + void operator()(std::size_t w, std::size_t h, const T* const copy) + { + ar_realloc(w, h); + for (std::size_t y = 0; y < h; ++y) { + std::copy(copy + y * w, copy + y * w + w, rows.data()[y]); + } + } int getWidth() const { diff --git a/rtengine/dcraw.cc b/rtengine/dcraw.cc index bdd92c7da..8eca727b4 100644 --- a/rtengine/dcraw.cc +++ b/rtengine/dcraw.cc @@ -6938,7 +6938,6 @@ it under the terms of the one of two licenses as you choose: unsigned oldOrder = order; order = 0x4d4d; // always big endian per definition in https://www.adobe.com/content/dam/acom/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf chapter 7 unsigned ntags = get4(); // read the number of opcodes - if (ntags < ifp->size / 12) { // rough check for wrong value (happens for example with DNG files from DJI FC6310) while (ntags-- && !ifp->eof) { unsigned opcode = get4(); @@ -6957,8 +6956,48 @@ it under the terms of the one of two licenses as you choose: break; } case 51009: /* OpcodeList2 */ - meta_offset = ftell(ifp); - break; + { + meta_offset = ftell(ifp); + const unsigned oldOrder = order; + order = 0x4d4d; // always big endian per definition in https://www.adobe.com/content/dam/acom/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf chapter 7 + unsigned ntags = get4(); // read the number of opcodes + if (ntags < ifp->size / 12) { // rough check for wrong value (happens for example with DNG files from DJI FC6310) + while (ntags-- && !ifp->eof) { + unsigned opcode = get4(); + if (opcode == 9 && gainMaps.size() < 4) { + fseek(ifp, 4, SEEK_CUR); // skip 4 bytes as we know that the opcode 4 takes 4 byte + fseek(ifp, 8, SEEK_CUR); // skip 8 bytes as they don't interest us currently + GainMap gainMap; + gainMap.Top = get4(); + gainMap.Left = get4(); + gainMap.Bottom = get4(); + gainMap.Right = get4(); + gainMap.Plane = get4(); + gainMap.Planes = get4(); + gainMap.RowPitch = get4(); + gainMap.ColPitch = get4(); + gainMap.MapPointsV = get4(); + gainMap.MapPointsH = get4(); + gainMap.MapSpacingV = getreal(12); + gainMap.MapSpacingH = getreal(12); + gainMap.MapOriginV = getreal(12); + gainMap.MapOriginH = getreal(12); + gainMap.MapPlanes = get4(); + const std::size_t n = static_cast(gainMap.MapPointsV) * static_cast(gainMap.MapPointsH) * static_cast(gainMap.MapPlanes); + gainMap.MapGain.reserve(n); + for (std::size_t i = 0; i < n; ++i) { + gainMap.MapGain.push_back(getreal(11)); + } + gainMaps.push_back(std::move(gainMap)); + } else { + fseek(ifp, 8, SEEK_CUR); // skip 8 bytes as they don't interest us currently + fseek(ifp, get4(), SEEK_CUR); + } + } + } + order = oldOrder; + break; + } case 64772: /* Kodak P-series */ if (len < 13) break; fseek (ifp, 16, SEEK_CUR); @@ -11079,6 +11118,70 @@ void CLASS nikon_14bit_load_raw() free(buf); } +bool CLASS isGainMapSupported() const { + if (!(dng_version && isBayer())) { + return false; + } + const auto n = gainMaps.size(); + if (n != 4) { // we need 4 gainmaps for bayer files + if (rtengine::settings->verbose) { + std::cout << "GainMap has " << n << " maps, but 4 are needed" << std::endl; + } + return false; + } + unsigned int check = 0; + bool noOp = true; + for (const auto &m : gainMaps) { + if (m.MapGain.size() < 1) { + if (rtengine::settings->verbose) { + std::cout << "GainMap has invalid size of " << m.MapGain.size() << std::endl; + } + return false; + } + if (m.MapGain.size() != static_cast(m.MapPointsV) * static_cast(m.MapPointsH) * static_cast(m.MapPlanes)) { + if (rtengine::settings->verbose) { + std::cout << "GainMap has size of " << m.MapGain.size() << ", but needs " << m.MapPointsV * m.MapPointsH * m.MapPlanes << std::endl; + } + return false; + } + if (m.RowPitch != 2 || m.ColPitch != 2) { + if (rtengine::settings->verbose) { + std::cout << "GainMap needs Row/ColPitch of 2/2, but has " << m.RowPitch << "/" << m.ColPitch << std::endl; + } + return false; + } + if (m.Top == 0){ + if (m.Left == 0) { + check += 1; + } else if (m.Left == 1) { + check += 2; + } + } else if (m.Top == 1) { + if (m.Left == 0) { + check += 4; + } else if (m.Left == 1) { + check += 8; + } + } + for (size_t i = 0; noOp && i < m.MapGain.size(); ++i) { + if (m.MapGain[i] != 1.f) { // we have at least one value != 1.f => map is not a nop + noOp = false; + } + } + } + if (noOp || check != 15) { // all maps are nops or the structure of the combination of 4 maps is not correct + if (rtengine::settings->verbose) { + if (noOp) { + std::cout << "GainMap is a nop" << std::endl; + } else { + std::cout << "GainMap has unsupported type : " << check << std::endl; + } + } + return false; + } + return true; +} + /* RT: Delete from here */ /*RT*/#undef SQR /*RT*/#undef MAX diff --git a/rtengine/dcraw.h b/rtengine/dcraw.h index f78bd8aa6..e0a6cda92 100644 --- a/rtengine/dcraw.h +++ b/rtengine/dcraw.h @@ -19,9 +19,12 @@ #pragma once +#include + #include "myfile.h" #include - +#include "dnggainmap.h" +#include "settings.h" class DCraw { @@ -165,6 +168,8 @@ protected: PanasonicRW2Info(): bpp(0), encoding(0) {} }; PanasonicRW2Info RT_pana_info; + std::vector gainMaps; + public: struct CanonCR3Data { // contents of tag CMP1 for relevant track in CR3 file @@ -193,6 +198,18 @@ public: int crx_track_selected; short CR3_CTMDtag; }; + + bool isBayer() const + { + return (filters != 0 && filters != 9); + } + + const std::vector& getGainMaps() const { + return gainMaps; + } + + bool isGainMapSupported() const; + struct CanonLevelsData { unsigned cblack[4]; unsigned white; @@ -200,6 +217,7 @@ public: bool white_ok; CanonLevelsData(): cblack{0}, white{0}, black_ok(false), white_ok(false) {} }; + protected: CanonCR3Data RT_canon_CR3_data; diff --git a/rtengine/dnggainmap.h b/rtengine/dnggainmap.h new file mode 100644 index 000000000..25a01fd0f --- /dev/null +++ b/rtengine/dnggainmap.h @@ -0,0 +1,43 @@ +/* + * This file is part of RawTherapee. + * + * Copyright (c) 2021 Ingo Weyrich + * + * 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 . + */ + +#pragma once + +#include +#include + +struct GainMap +{ + std::uint32_t Top; + std::uint32_t Left; + std::uint32_t Bottom; + std::uint32_t Right; + std::uint32_t Plane; + std::uint32_t Planes; + std::uint32_t RowPitch; + std::uint32_t ColPitch; + std::uint32_t MapPointsV; + std::uint32_t MapPointsH; + double MapSpacingV; + double MapSpacingH; + double MapOriginV; + double MapOriginH; + std::uint32_t MapPlanes; + std::vector MapGain; +}; diff --git a/rtengine/imagesource.h b/rtengine/imagesource.h index 6cb279efc..a8ea8f851 100644 --- a/rtengine/imagesource.h +++ b/rtengine/imagesource.h @@ -137,6 +137,7 @@ public: virtual ImageMatrices* getImageMatrices () = 0; virtual bool isRAW () const = 0; + virtual bool isGainMapSupported () const = 0; virtual DCPProfile* getDCP (const procparams::ColorManagementParams &cmp, DCPProfileApplyState &as) { return nullptr; diff --git a/rtengine/improccoordinator.cc b/rtengine/improccoordinator.cc index 8d377d30c..df354bfd8 100644 --- a/rtengine/improccoordinator.cc +++ b/rtengine/improccoordinator.cc @@ -407,7 +407,7 @@ void ImProcCoordinator::updatePreviewImage(int todo, bool panningRelatedChange) // If high detail (=100%) is newly selected, do a demosaic update, since the last was just with FAST if (imageTypeListener) { - imageTypeListener->imageTypeChanged(imgsrc->isRAW(), imgsrc->getSensorType() == ST_BAYER, imgsrc->getSensorType() == ST_FUJI_XTRANS, imgsrc->isMono()); + imageTypeListener->imageTypeChanged(imgsrc->isRAW(), imgsrc->getSensorType() == ST_BAYER, imgsrc->getSensorType() == ST_FUJI_XTRANS, imgsrc->isMono(), imgsrc->isGainMapSupported()); } if ((todo & M_RAW) diff --git a/rtengine/procparams.cc b/rtengine/procparams.cc index 04ece8bc3..c9c420b44 100644 --- a/rtengine/procparams.cc +++ b/rtengine/procparams.cc @@ -5633,6 +5633,7 @@ bool RAWParams::PreprocessWB::operator !=(const PreprocessWB& other) const RAWParams::RAWParams() : df_autoselect(false), ff_AutoSelect(false), + ff_FromMetaData(false), ff_BlurRadius(32), ff_BlurType(getFlatFieldBlurTypeString(FlatFieldBlurType::AREA)), ff_AutoClipControl(false), @@ -5658,6 +5659,7 @@ bool RAWParams::operator ==(const RAWParams& other) const && df_autoselect == other.df_autoselect && ff_file == other.ff_file && ff_AutoSelect == other.ff_AutoSelect + && ff_FromMetaData == other.ff_FromMetaData && ff_BlurRadius == other.ff_BlurRadius && ff_BlurType == other.ff_BlurType && ff_AutoClipControl == other.ff_AutoClipControl @@ -7484,6 +7486,7 @@ int ProcParams::save(const Glib::ustring& fname, const Glib::ustring& fname2, bo saveToKeyfile(!pedited || pedited->raw.df_autoselect, "RAW", "DarkFrameAuto", raw.df_autoselect, keyFile); saveToKeyfile(!pedited || pedited->raw.ff_file, "RAW", "FlatFieldFile", relativePathIfInside(fname, fnameAbsolute, raw.ff_file), keyFile); saveToKeyfile(!pedited || pedited->raw.ff_AutoSelect, "RAW", "FlatFieldAutoSelect", raw.ff_AutoSelect, keyFile); + saveToKeyfile(!pedited || pedited->raw.ff_FromMetaData, "RAW", "FlatFieldFromMetaData", raw.ff_FromMetaData, keyFile); saveToKeyfile(!pedited || pedited->raw.ff_BlurRadius, "RAW", "FlatFieldBlurRadius", raw.ff_BlurRadius, keyFile); saveToKeyfile(!pedited || pedited->raw.ff_BlurType, "RAW", "FlatFieldBlurType", raw.ff_BlurType, keyFile); saveToKeyfile(!pedited || pedited->raw.ff_AutoClipControl, "RAW", "FlatFieldAutoClipControl", raw.ff_AutoClipControl, keyFile); @@ -10130,6 +10133,7 @@ int ProcParams::load(const Glib::ustring& fname, ParamsEdited* pedited) } assignFromKeyfile(keyFile, "RAW", "FlatFieldAutoSelect", pedited, raw.ff_AutoSelect, pedited->raw.ff_AutoSelect); + assignFromKeyfile(keyFile, "RAW", "FlatFieldFromMetaData", pedited, raw.ff_FromMetaData, pedited->raw.ff_FromMetaData); assignFromKeyfile(keyFile, "RAW", "FlatFieldBlurRadius", pedited, raw.ff_BlurRadius, pedited->raw.ff_BlurRadius); assignFromKeyfile(keyFile, "RAW", "FlatFieldBlurType", pedited, raw.ff_BlurType, pedited->raw.ff_BlurType); assignFromKeyfile(keyFile, "RAW", "FlatFieldAutoClipControl", pedited, raw.ff_AutoClipControl, pedited->raw.ff_AutoClipControl); diff --git a/rtengine/procparams.h b/rtengine/procparams.h index d730316e2..309bcb2ab 100644 --- a/rtengine/procparams.h +++ b/rtengine/procparams.h @@ -2440,6 +2440,7 @@ struct RAWParams { Glib::ustring ff_file; bool ff_AutoSelect; + bool ff_FromMetaData; int ff_BlurRadius; Glib::ustring ff_BlurType; bool ff_AutoClipControl; diff --git a/rtengine/rawimage.h b/rtengine/rawimage.h index 871267dac..2b1cd2156 100644 --- a/rtengine/rawimage.h +++ b/rtengine/rawimage.h @@ -245,11 +245,6 @@ public: return zero_is_bad == 1; } - bool isBayer() const - { - return (filters != 0 && filters != 9); - } - bool isXtrans() const { return filters == 9; diff --git a/rtengine/rawimagesource.cc b/rtengine/rawimagesource.cc index 48d7b0904..550c59e9c 100644 --- a/rtengine/rawimagesource.cc +++ b/rtengine/rawimagesource.cc @@ -39,6 +39,7 @@ #include "rawimage.h" #include "rawimagesource_i.h" #include "rawimagesource.h" +#include "rescale.h" #include "rt_math.h" #include "rtengine.h" #include "rtlensfun.h" @@ -1347,7 +1348,6 @@ void RawImageSource::preprocess (const RAWParams &raw, const LensProfParams &le rif = ffm.searchFlatField(idata->getMake(), idata->getModel(), idata->getLens(), idata->getFocalLen(), idata->getFNumber(), idata->getDateTimeAsTS()); } - bool hasFlatField = (rif != nullptr); if (hasFlatField && settings->verbose) { @@ -1387,6 +1387,9 @@ void RawImageSource::preprocess (const RAWParams &raw, const LensProfParams &le } //FLATFIELD end + if (raw.ff_FromMetaData && isGainMapSupported()) { + applyDngGainMap(c_black, ri->getGainMaps()); + } // Always correct camera badpixels from .badpixels file const std::vector *bp = DFManager::getInstance().getBadPixels(ri->get_maker(), ri->get_model(), idata->getSerialNumber()); @@ -6259,6 +6262,36 @@ void RawImageSource::getRawValues(int x, int y, int rotate, int &R, int &G, int } } +bool RawImageSource::isGainMapSupported() const { + return ri->isGainMapSupported(); +} + +void RawImageSource::applyDngGainMap(const float black[4], const std::vector &gainMaps) { + // now we can apply each gain map to raw_data + array2D mvals[2][2]; + for (auto &m : gainMaps) { + mvals[m.Top & 1][m.Left & 1](m.MapPointsH, m.MapPointsV, m.MapGain.data()); + } + + // now we assume, col_scale and row scale is the same for all maps + const float col_scale = float(gainMaps[0].MapPointsH-1) / float(W); + const float row_scale = float(gainMaps[0].MapPointsV-1) / float(H); + +#ifdef _OPENMP + #pragma omp parallel for schedule(dynamic, 16) +#endif + for (std::size_t y = 0; y < static_cast(H); ++y) { + const float rowBlack[2] = {black[FC(y,0)], black[FC(y,1)]}; + const float ys = y * row_scale; + float xs = 0.f; + for (std::size_t x = 0; x < static_cast(W); ++x, xs += col_scale) { + const float f = getBilinearValue(mvals[y & 1][x & 1], xs, ys); + const float b = rowBlack[x & 1]; + rawData[y][x] = rtengine::max((rawData[y][x] - b) * f + b, 0.f); + } + } +} + void RawImageSource::cleanup () { delete phaseOneIccCurve; diff --git a/rtengine/rawimagesource.h b/rtengine/rawimagesource.h index 41a400dd9..d7549fe71 100644 --- a/rtengine/rawimagesource.h +++ b/rtengine/rawimagesource.h @@ -24,6 +24,7 @@ #include "array2D.h" #include "colortemp.h" +#include "dnggainmap.h" #include "iimage.h" #include "imagesource.h" #include "procparams.h" @@ -177,6 +178,8 @@ public: return true; } + bool isGainMapSupported() const override; + void setProgressListener (ProgressListener* pl) override { plistener = pl; @@ -304,6 +307,7 @@ protected: void vflip (Imagefloat* im); void getRawValues(int x, int y, int rotate, int &R, int &G, int &B) override; void captureSharpening(const procparams::CaptureSharpeningParams &sharpeningParams, bool showMask, double &conrastThreshold, double &radius) override; + void applyDngGainMap(const float black[4], const std::vector &gainMaps); }; } diff --git a/rtengine/rtengine.h b/rtengine/rtengine.h index b9fc916f6..989ca3354 100644 --- a/rtengine/rtengine.h +++ b/rtengine/rtengine.h @@ -493,7 +493,7 @@ class ImageTypeListener { public: virtual ~ImageTypeListener() = default; - virtual void imageTypeChanged(bool isRaw, bool isBayer, bool isXtrans, bool is_Mono = false) = 0; + virtual void imageTypeChanged(bool isRaw, bool isBayer, bool isXtrans, bool is_Mono = false, bool isGainMapSupported = false) = 0; }; class AutoContrastListener diff --git a/rtengine/stdimagesource.h b/rtengine/stdimagesource.h index 9b95fe34e..f83c58a04 100644 --- a/rtengine/stdimagesource.h +++ b/rtengine/stdimagesource.h @@ -100,6 +100,11 @@ public: return false; } + bool isGainMapSupported() const override + { + return false; + } + void setProgressListener (ProgressListener* pl) override { plistener = pl; diff --git a/rtgui/flatfield.cc b/rtgui/flatfield.cc index 71fa0aab6..f493ba0b9 100644 --- a/rtgui/flatfield.cc +++ b/rtgui/flatfield.cc @@ -18,6 +18,7 @@ */ #include +#include "eventmapper.h" #include "flatfield.h" #include "guiutils.h" @@ -32,6 +33,9 @@ using namespace rtengine::procparams; FlatField::FlatField () : FoldableToolPanel(this, "flatfield", M("TP_FLATFIELD_LABEL")) { + auto m = ProcEventMapper::getInstance(); + EvFlatFieldFromMetaData = m->newEvent(DARKFRAME, "HISTORY_MSG_FF_FROMMETADATA"); + hbff = Gtk::manage(new Gtk::Box()); flatFieldFile = Gtk::manage(new MyFileChooserButton(M("TP_FLATFIELD_LABEL"), Gtk::FILE_CHOOSER_ACTION_OPEN)); bindCurrentFolder (*flatFieldFile, options.lastFlatfieldDir); @@ -42,6 +46,8 @@ FlatField::FlatField () : FoldableToolPanel(this, "flatfield", M("TP_FLATFIELD_L hbff->pack_start(*flatFieldFile); hbff->pack_start(*flatFieldFileReset, Gtk::PACK_SHRINK); flatFieldAutoSelect = Gtk::manage(new Gtk::CheckButton((M("TP_FLATFIELD_AUTOSELECT")))); + flatFieldFromMetaData = Gtk::manage(new CheckBox((M("TP_FLATFIELD_FROMMETADATA")), multiImage)); + flatFieldFromMetaData->setCheckBoxListener (this); ffInfo = Gtk::manage(new Gtk::Label("-")); setExpandAlignProperties(ffInfo, true, false, Gtk::ALIGN_CENTER, Gtk::ALIGN_CENTER); flatFieldBlurRadius = Gtk::manage(new Adjuster (M("TP_FLATFIELD_BLURRADIUS"), 0, 200, 2, 32)); @@ -70,8 +76,10 @@ FlatField::FlatField () : FoldableToolPanel(this, "flatfield", M("TP_FLATFIELD_L flatFieldClipControl->show(); flatFieldClipControl->set_tooltip_markup (M("TP_FLATFIELD_CLIPCONTROL_TOOLTIP")); - pack_start( *hbff, Gtk::PACK_SHRINK); + pack_start( *flatFieldFromMetaData, Gtk::PACK_SHRINK); + pack_start( *Gtk::manage( new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), Gtk::PACK_SHRINK, 0 ); pack_start( *flatFieldAutoSelect, Gtk::PACK_SHRINK); + pack_start( *hbff, Gtk::PACK_SHRINK); pack_start( *ffInfo, Gtk::PACK_SHRINK); pack_start( *hbffbt, Gtk::PACK_SHRINK); pack_start( *flatFieldBlurRadius, Gtk::PACK_SHRINK); @@ -128,12 +136,14 @@ void FlatField::read(const rtengine::procparams::ProcParams* pp, const ParamsEdi } flatFieldAutoSelect->set_active (pp->raw.ff_AutoSelect); + flatFieldFromMetaData->set_active (pp->raw.ff_FromMetaData); flatFieldBlurRadius->setValue (pp->raw.ff_BlurRadius); flatFieldClipControl->setValue (pp->raw.ff_clipControl); flatFieldClipControl->setAutoValue (pp->raw.ff_AutoClipControl); if(pedited ) { flatFieldAutoSelect->set_inconsistent (!pedited->raw.ff_AutoSelect); + flatFieldFromMetaData->set_inconsistent (!pedited->raw.ff_FromMetaData); flatFieldBlurRadius->setEditedState( pedited->raw.ff_BlurRadius ? Edited : UnEdited ); flatFieldClipControl->setEditedState( pedited->raw.ff_clipControl ? Edited : UnEdited ); flatFieldClipControl->setAutoInconsistent(multiImage && !pedited->raw.ff_AutoClipControl); @@ -214,6 +224,7 @@ void FlatField::write( rtengine::procparams::ProcParams* pp, ParamsEdited* pedit { pp->raw.ff_file = flatFieldFile->get_filename(); pp->raw.ff_AutoSelect = flatFieldAutoSelect->get_active(); + pp->raw.ff_FromMetaData = flatFieldFromMetaData->get_active(); pp->raw.ff_BlurRadius = flatFieldBlurRadius->getIntValue(); pp->raw.ff_clipControl = flatFieldClipControl->getIntValue(); pp->raw.ff_AutoClipControl = flatFieldClipControl->getAutoValue(); @@ -227,6 +238,7 @@ void FlatField::write( rtengine::procparams::ProcParams* pp, ParamsEdited* pedit if (pedited) { pedited->raw.ff_file = ffChanged; pedited->raw.ff_AutoSelect = !flatFieldAutoSelect->get_inconsistent(); + pedited->raw.ff_FromMetaData = !flatFieldFromMetaData->get_inconsistent(); pedited->raw.ff_BlurRadius = flatFieldBlurRadius->getEditedState (); pedited->raw.ff_clipControl = flatFieldClipControl->getEditedState (); pedited->raw.ff_AutoClipControl = !flatFieldClipControl->getAutoInconsistent(); @@ -352,6 +364,13 @@ void FlatField::flatFieldBlurTypeChanged () } } +void FlatField::checkBoxToggled (CheckBox* c, CheckValue newval) +{ + if (listener && c == flatFieldFromMetaData) { + listener->panelChanged (EvFlatFieldFromMetaData, flatFieldFromMetaData->getLastActive() ? M("GENERAL_ENABLED") : M("GENERAL_DISABLED")); + } +} + void FlatField::flatFieldAutoSelectChanged() { if (batchMode) { @@ -419,3 +438,18 @@ void FlatField::flatFieldAutoClipValueChanged(int n) } ); } + +void FlatField::setGainMap(bool enabled) { + flatFieldFromMetaData->set_sensitive(enabled); + if (!enabled) { + idle_register.add( + [this, enabled]() -> bool + { + disableListener(); + flatFieldFromMetaData->setValue(false); + enableListener(); + return false; + } + ); + } +} diff --git a/rtgui/flatfield.h b/rtgui/flatfield.h index 0d6f167e1..be46d5a1d 100644 --- a/rtgui/flatfield.h +++ b/rtgui/flatfield.h @@ -23,6 +23,7 @@ #include #include "adjuster.h" +#include "checkbox.h" #include "guiutils.h" #include "toolpanel.h" @@ -42,7 +43,7 @@ public: // add other info here }; -class FlatField final : public ToolParamBlock, public AdjusterListener, public FoldableToolPanel, public rtengine::FlatFieldAutoClipListener +class FlatField final : public ToolParamBlock, public AdjusterListener, public CheckBoxListener, public FoldableToolPanel, public rtengine::FlatFieldAutoClipListener { protected: @@ -52,6 +53,7 @@ protected: Gtk::Label *ffInfo; Gtk::Button *flatFieldFileReset; Gtk::CheckButton* flatFieldAutoSelect; + CheckBox* flatFieldFromMetaData; Adjuster* flatFieldClipControl; Adjuster* flatFieldBlurRadius; MyComboBoxText* flatFieldBlurType; @@ -64,8 +66,10 @@ protected: Glib::ustring lastShortcutPath; bool b_filter_asCurrent; bool israw; + rtengine::ProcEvent EvFlatFieldFromMetaData; IdleRegister idle_register; + public: FlatField (); @@ -90,4 +94,6 @@ public: ffp = p; }; void flatFieldAutoClipValueChanged(int n = 0) override; + void checkBoxToggled(CheckBox* c, CheckValue newval) override; + void setGainMap(bool enabled); }; diff --git a/rtgui/paramsedited.cc b/rtgui/paramsedited.cc index a7963b7dc..9c7a92f39 100644 --- a/rtgui/paramsedited.cc +++ b/rtgui/paramsedited.cc @@ -519,6 +519,7 @@ void ParamsEdited::set(bool v) raw.df_autoselect = v; raw.ff_file = v; raw.ff_AutoSelect = v; + raw.ff_FromMetaData = v; raw.ff_BlurRadius = v; raw.ff_BlurType = v; raw.ff_AutoClipControl = v; @@ -1930,6 +1931,7 @@ void ParamsEdited::initFrom(const std::vector& raw.df_autoselect = raw.df_autoselect && p.raw.df_autoselect == other.raw.df_autoselect; raw.ff_file = raw.ff_file && p.raw.ff_file == other.raw.ff_file; raw.ff_AutoSelect = raw.ff_AutoSelect && p.raw.ff_AutoSelect == other.raw.ff_AutoSelect; + raw.ff_FromMetaData = raw.ff_FromMetaData && p.raw.ff_FromMetaData == other.raw.ff_FromMetaData; raw.ff_BlurRadius = raw.ff_BlurRadius && p.raw.ff_BlurRadius == other.raw.ff_BlurRadius; raw.ff_BlurType = raw.ff_BlurType && p.raw.ff_BlurType == other.raw.ff_BlurType; raw.ff_AutoClipControl = raw.ff_AutoClipControl && p.raw.ff_AutoClipControl == other.raw.ff_AutoClipControl; @@ -6644,6 +6646,10 @@ void ParamsEdited::combine(rtengine::procparams::ProcParams& toEdit, const rteng toEdit.raw.ff_AutoSelect = mods.raw.ff_AutoSelect; } + if (raw.ff_FromMetaData) { + toEdit.raw.ff_FromMetaData = mods.raw.ff_FromMetaData; + } + if (raw.ff_BlurRadius) { toEdit.raw.ff_BlurRadius = mods.raw.ff_BlurRadius; } @@ -7375,7 +7381,7 @@ bool RAWParamsEdited::XTransSensor::isUnchanged() const bool RAWParamsEdited::isUnchanged() const { return bayersensor.isUnchanged() && xtranssensor.isUnchanged() && ca_autocorrect && ca_avoidcolourshift && caautoiterations && cared && cablue && hotPixelFilter && deadPixelFilter && hotdeadpix_thresh && darkFrame - && df_autoselect && ff_file && ff_AutoSelect && ff_BlurRadius && ff_BlurType && exPos && ff_AutoClipControl && ff_clipControl; + && df_autoselect && ff_file && ff_AutoSelect && ff_FromMetaData && ff_BlurRadius && ff_BlurType && exPos && ff_AutoClipControl && ff_clipControl; } bool LensProfParamsEdited::isUnchanged() const diff --git a/rtgui/paramsedited.h b/rtgui/paramsedited.h index 0c0c79f7c..3c108f33d 100644 --- a/rtgui/paramsedited.h +++ b/rtgui/paramsedited.h @@ -1489,6 +1489,7 @@ struct RAWParamsEdited { bool df_autoselect; bool ff_file; bool ff_AutoSelect; + bool ff_FromMetaData; bool ff_BlurRadius; bool ff_BlurType; bool ff_AutoClipControl; diff --git a/rtgui/partialpastedlg.cc b/rtgui/partialpastedlg.cc index a9f79d854..81847adc0 100644 --- a/rtgui/partialpastedlg.cc +++ b/rtgui/partialpastedlg.cc @@ -301,6 +301,7 @@ PartialPasteDlg::PartialPasteDlg (const Glib::ustring &title, Gtk::Window* paren //--- ff_file = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_FLATFIELDFILE"))); ff_AutoSelect = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_FLATFIELDAUTOSELECT"))); + ff_FromMetaData = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_FLATFIELDFROMMETADATA"))); ff_BlurType = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_FLATFIELDBLURTYPE"))); ff_BlurRadius = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_FLATFIELDBLURRADIUS"))); ff_ClipControl = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_FLATFIELDCLIPCONTROL"))); @@ -423,6 +424,7 @@ PartialPasteDlg::PartialPasteDlg (const Glib::ustring &title, Gtk::Window* paren vboxes[8]->pack_start (*Gtk::manage (new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), Gtk::PACK_SHRINK, 0); vboxes[8]->pack_start (*ff_file, Gtk::PACK_SHRINK, 2); vboxes[8]->pack_start (*ff_AutoSelect, Gtk::PACK_SHRINK, 2); + vboxes[8]->pack_start (*ff_FromMetaData, Gtk::PACK_SHRINK, 2); vboxes[8]->pack_start (*ff_BlurType, Gtk::PACK_SHRINK, 2); vboxes[8]->pack_start (*ff_BlurRadius, Gtk::PACK_SHRINK, 2); vboxes[8]->pack_start (*ff_ClipControl, Gtk::PACK_SHRINK, 2); @@ -574,6 +576,7 @@ PartialPasteDlg::PartialPasteDlg (const Glib::ustring &title, Gtk::Window* paren //--- ff_fileConn = ff_file->signal_toggled().connect (sigc::bind (sigc::mem_fun(*raw, &Gtk::CheckButton::set_inconsistent), true)); ff_AutoSelectConn = ff_AutoSelect->signal_toggled().connect (sigc::bind (sigc::mem_fun(*raw, &Gtk::CheckButton::set_inconsistent), true)); + ff_FromMetaDataConn = ff_FromMetaData->signal_toggled().connect (sigc::bind (sigc::mem_fun(*raw, &Gtk::CheckButton::set_inconsistent), true)); ff_BlurTypeConn = ff_BlurType->signal_toggled().connect (sigc::bind (sigc::mem_fun(*raw, &Gtk::CheckButton::set_inconsistent), true)); ff_BlurRadiusConn = ff_BlurRadius->signal_toggled().connect (sigc::bind (sigc::mem_fun(*raw, &Gtk::CheckButton::set_inconsistent), true)); ff_ClipControlConn = ff_ClipControl->signal_toggled().connect (sigc::bind (sigc::mem_fun(*raw, &Gtk::CheckButton::set_inconsistent), true)); @@ -655,6 +658,7 @@ void PartialPasteDlg::rawToggled () ConnectionBlocker df_AutoSelectBlocker(df_AutoSelectConn); ConnectionBlocker ff_fileBlocker(ff_fileConn); ConnectionBlocker ff_AutoSelectBlocker(ff_AutoSelectConn); + ConnectionBlocker ff_FromMetaDataBlocker(ff_FromMetaDataConn); ConnectionBlocker ff_BlurTypeBlocker(ff_BlurTypeConn); ConnectionBlocker ff_BlurRadiusBlocker(ff_BlurRadiusConn); ConnectionBlocker ff_ClipControlBlocker(ff_ClipControlConn); @@ -685,6 +689,7 @@ void PartialPasteDlg::rawToggled () df_AutoSelect->set_active (raw->get_active ()); ff_file->set_active (raw->get_active ()); ff_AutoSelect->set_active (raw->get_active ()); + ff_FromMetaData->set_active (raw->get_active ()); ff_BlurType->set_active (raw->get_active ()); ff_BlurRadius->set_active (raw->get_active ()); ff_ClipControl->set_active (raw->get_active ()); @@ -1173,6 +1178,10 @@ void PartialPasteDlg::applyPaste (rtengine::procparams::ProcParams* dstPP, Param filterPE.raw.ff_AutoSelect = falsePE.raw.ff_AutoSelect; } + if (!ff_FromMetaData->get_active ()) { + filterPE.raw.ff_FromMetaData = falsePE.raw.ff_FromMetaData; + } + if (!ff_BlurRadius->get_active ()) { filterPE.raw.ff_BlurRadius = falsePE.raw.ff_BlurRadius; } diff --git a/rtgui/partialpastedlg.h b/rtgui/partialpastedlg.h index 19e1eb462..dcf44bb72 100644 --- a/rtgui/partialpastedlg.h +++ b/rtgui/partialpastedlg.h @@ -214,6 +214,7 @@ public: Gtk::CheckButton* df_AutoSelect; Gtk::CheckButton* ff_file; Gtk::CheckButton* ff_AutoSelect; + Gtk::CheckButton* ff_FromMetaData; Gtk::CheckButton* ff_BlurRadius; Gtk::CheckButton* ff_BlurType; Gtk::CheckButton* ff_ClipControl; @@ -230,7 +231,7 @@ public: sigc::connection distortionConn, cacorrConn, vignettingConn, lcpConn; sigc::connection coarserotConn, finerotConn, cropConn, resizeConn, prsharpeningConn, perspectiveConn, commonTransConn; sigc::connection metadataConn, exifchConn, iptcConn, icmConn; - sigc::connection df_fileConn, df_AutoSelectConn, ff_fileConn, ff_AutoSelectConn, ff_BlurRadiusConn, ff_BlurTypeConn, ff_ClipControlConn; + sigc::connection df_fileConn, df_AutoSelectConn, ff_fileConn, ff_AutoSelectConn, ff_FromMetaDataConn, ff_BlurRadiusConn, ff_BlurTypeConn, ff_ClipControlConn; sigc::connection raw_caredblueConn, raw_ca_autocorrectConn, raw_ca_avoid_colourshiftconn, raw_hotpix_filtConn, raw_deadpix_filtConn, raw_pdaf_lines_filterConn, raw_linenoiseConn, raw_greenthreshConn, raw_ccStepsConn, raw_methodConn, raw_borderConn, raw_imagenumConn, raw_dcb_iterationsConn, raw_lmmse_iterationsConn, raw_pixelshiftConn, raw_dcb_enhanceConn, raw_exposConn, raw_blackConn; sigc::connection filmNegativeConn; sigc::connection captureSharpeningConn; diff --git a/rtgui/toolpanelcoord.cc b/rtgui/toolpanelcoord.cc index 9c14aeb6e..fe8b4f1bf 100644 --- a/rtgui/toolpanelcoord.cc +++ b/rtgui/toolpanelcoord.cc @@ -343,12 +343,12 @@ ToolPanelCoordinator::~ToolPanelCoordinator () delete toolBar; } -void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtrans, bool isMono) +void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtrans, bool isMono, bool isGainMapSupported) { if (isRaw) { if (isBayer) { idle_register.add( - [this]() -> bool + [this, isGainMapSupported]() -> bool { rawPanelSW->set_sensitive(true); sensorxtrans->FoldableToolPanel::hide(); @@ -362,6 +362,7 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr preprocessWB->FoldableToolPanel::show(); preprocess->FoldableToolPanel::show(); flatfield->FoldableToolPanel::show(); + flatfield->setGainMap(isGainMapSupported); pdSharpening->FoldableToolPanel::show(); retinex->FoldableToolPanel::setGrayedOut(false); return false; @@ -369,7 +370,7 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr ); } else if (isXtrans) { idle_register.add( - [this]() -> bool + [this, isGainMapSupported]() -> bool { rawPanelSW->set_sensitive(true); sensorxtrans->FoldableToolPanel::show(); @@ -383,6 +384,7 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr preprocessWB->FoldableToolPanel::show(); preprocess->FoldableToolPanel::show(); flatfield->FoldableToolPanel::show(); + flatfield->setGainMap(isGainMapSupported); pdSharpening->FoldableToolPanel::show(); retinex->FoldableToolPanel::setGrayedOut(false); return false; @@ -390,7 +392,7 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr ); } else if (isMono) { idle_register.add( - [this]() -> bool + [this, isGainMapSupported]() -> bool { rawPanelSW->set_sensitive(true); sensorbayer->FoldableToolPanel::hide(); @@ -403,6 +405,7 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr preprocessWB->FoldableToolPanel::hide(); preprocess->FoldableToolPanel::hide(); flatfield->FoldableToolPanel::show(); + flatfield->setGainMap(isGainMapSupported); pdSharpening->FoldableToolPanel::show(); retinex->FoldableToolPanel::setGrayedOut(false); return false; diff --git a/rtgui/toolpanelcoord.h b/rtgui/toolpanelcoord.h index 65e2f1e8f..cd4131ff4 100644 --- a/rtgui/toolpanelcoord.h +++ b/rtgui/toolpanelcoord.h @@ -260,7 +260,7 @@ public: void unsetTweakOperator (rtengine::TweakOperator *tOperator) override; // FilmNegProvider interface - void imageTypeChanged (bool isRaw, bool isBayer, bool isXtrans, bool isMono = false) override; + void imageTypeChanged (bool isRaw, bool isBayer, bool isXtrans, bool isMono = false, bool isGainMapSupported = false) override; // profilechangelistener interface void profileChange( From 57c1822b2c60c42b9b38caab6ade1c7f50201b16 Mon Sep 17 00:00:00 2001 From: Lawrence37 <45837045+Lawrence37@users.noreply.github.com> Date: Mon, 2 Jan 2023 12:32:15 -0800 Subject: [PATCH 17/17] Strict temporary image file permissions (#6358) * Write temp images to private tmp directory (Linux) The directory is in /tmp with 700 permissions. * Reduce temp image file permissions in Linux Set temporary image file permissions to read/write for the user only. * Use private tmp directory for temp images in MacOS * Use private tmp directory for temp images Windows * Use GLib to create temporary directories * Reuse temp directory if possible --- rtengine/winutils.h | 124 +++++++++++++++++++++++ rtgui/editorpanel.cc | 235 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 rtengine/winutils.h diff --git a/rtengine/winutils.h b/rtengine/winutils.h new file mode 100644 index 000000000..757849dd1 --- /dev/null +++ b/rtengine/winutils.h @@ -0,0 +1,124 @@ +/* + * This file is part of RawTherapee. + * + * Copyright (c) 2021 Lawrence Lee + * + * 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 . + */ +#pragma once + +#ifdef WIN32 + +#include +#include + +#include "noncopyable.h" + + +/** + * Wrapper for pointers to memory allocated by HeapAlloc. + * + * Memory is automatically freed when the object goes out of scope. + */ +template +class WinHeapPtr : public rtengine::NonCopyable +{ +private: + const T ptr; + +public: + WinHeapPtr() = delete; + + /** Allocates the specified number of bytes in the process heap. */ + explicit WinHeapPtr(SIZE_T bytes): ptr(static_cast(HeapAlloc(GetProcessHeap(), 0, bytes))) {}; + + ~WinHeapPtr() + { + // HeapFree does a null check. + HeapFree(GetProcessHeap(), 0, static_cast(ptr)); + } + + T operator ->() const + { + return ptr; + } + + operator T() const + { + return ptr; + } +}; + +/** + * Wrapper for HLOCAL pointers to memory allocated by LocalAlloc. + * + * Memory is automatically freed when the object goes out of scope. + */ +template +class WinLocalPtr : public rtengine::NonCopyable +{ +private: + const T ptr; + +public: + WinLocalPtr() = delete; + + /** Wraps a raw pointer. */ + WinLocalPtr(T pointer): ptr(pointer) {}; + + ~WinLocalPtr() + { + // LocalFree does a null check. + LocalFree(static_cast(ptr)); + } + + T operator ->() const + { + return ptr; + } + + operator T() const + { + return ptr; + } +}; + +/** + * Wrapper for HANDLEs. + * + * Handles are automatically closed when the object goes out of scope. + */ +class WinHandle : public rtengine::NonCopyable +{ +private: + const HANDLE handle; + +public: + WinHandle() = delete; + + /** Wraps a HANDLE. */ + WinHandle(HANDLE handle): handle(handle) {}; + + ~WinHandle() + { + CloseHandle(handle); + } + + operator HANDLE() const + { + return handle; + } +}; + +#endif diff --git a/rtgui/editorpanel.cc b/rtgui/editorpanel.cc index 78a07ddd6..190897c89 100644 --- a/rtgui/editorpanel.cc +++ b/rtgui/editorpanel.cc @@ -44,6 +44,8 @@ #ifdef WIN32 #include "windows.h" + +#include "../rtengine/winutils.h" #endif using namespace rtengine::procparams; @@ -134,6 +136,235 @@ bool find_default_monitor_profile (GdkWindow *rootwin, Glib::ustring &defprof, G } #endif +bool hasUserOnlyPermission(const Glib::ustring &dirname) +{ +#if defined(__linux__) || defined(__APPLE__) + const Glib::RefPtr file = Gio::File::create_for_path(dirname); + const Glib::RefPtr file_info = file->query_info("owner::user,unix::mode"); + + if (!file_info) { + return false; + } + + const Glib::ustring owner = file_info->get_attribute_string("owner::user"); + const guint32 mode = file_info->get_attribute_uint32("unix::mode"); + + return (mode & 0777) == 0700 && owner == Glib::get_user_name(); +#elif defined(WIN32) + const Glib::RefPtr file = Gio::File::create_for_path(dirname); + const Glib::RefPtr file_info = file->query_info("owner::user"); + if (!file_info) { + return false; + } + + // Current user must be the owner. + const Glib::ustring user_name = Glib::get_user_name(); + const Glib::ustring owner = file_info->get_attribute_string("owner::user"); + if (user_name != owner) { + return false; + } + + // Get security descriptor and discretionary access control list. + PACL dacl = nullptr; + PSECURITY_DESCRIPTOR sec_desc_raw_ptr = nullptr; + auto win_error = GetNamedSecurityInfo( + dirname.c_str(), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + nullptr, + nullptr, + &dacl, + nullptr, + &sec_desc_raw_ptr + ); + const WinLocalPtr sec_desc_ptr(sec_desc_raw_ptr); + if (win_error != ERROR_SUCCESS) { + return false; + } + + // Must not inherit permissions. + SECURITY_DESCRIPTOR_CONTROL sec_desc_control; + DWORD revision; + if (!( + GetSecurityDescriptorControl(sec_desc_ptr, &sec_desc_control, &revision) + && sec_desc_control & SE_DACL_PROTECTED + )) { + return false; + } + + // Check that there is one entry allowing full access. + ULONG acl_entry_count; + PEXPLICIT_ACCESS acl_entry_list_raw = nullptr; + win_error = GetExplicitEntriesFromAcl(dacl, &acl_entry_count, &acl_entry_list_raw); + const WinLocalPtr acl_entry_list(acl_entry_list_raw); + if (win_error != ERROR_SUCCESS || acl_entry_count != 1) { + return false; + } + const EXPLICIT_ACCESS &ace = acl_entry_list[0]; + if ( + ace.grfAccessMode != GRANT_ACCESS + || (ace.grfAccessPermissions & FILE_ALL_ACCESS) != FILE_ALL_ACCESS + || ace.Trustee.TrusteeForm != TRUSTEE_IS_SID // Should already be SID, but double check. + ) { + return false; + } + + // ACE must be for the current user. + HANDLE process_token_raw; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &process_token_raw)) { + return false; + } + const WinHandle process_token(process_token_raw); + DWORD actual_token_info_size = 0; + GetTokenInformation(process_token, TokenUser, nullptr, 0, &actual_token_info_size); + if (!actual_token_info_size) { + return false; + } + const WinHeapPtr user_token_ptr(actual_token_info_size); + if (!user_token_ptr || !GetTokenInformation( + process_token, + TokenUser, + user_token_ptr, + actual_token_info_size, + &actual_token_info_size + )) { + return false; + } + return EqualSid(ace.Trustee.ptstrName, user_token_ptr->User.Sid); +#endif + return false; +} + +/** + * Sets read and write permissions, and optionally the execute permission, for + * the user and no permissions for others. + */ +void setUserOnlyPermission(const Glib::RefPtr file, bool execute) +{ +#if defined(__linux__) || defined(__APPLE__) + const Glib::RefPtr file_info = file->query_info("unix::mode"); + if (!file_info) { + return; + } + + guint32 mode = file_info->get_attribute_uint32("unix::mode"); + mode = (mode & ~0777) | (execute ? 0700 : 0600); + try { + file->set_attribute_uint32("unix::mode", mode, Gio::FILE_QUERY_INFO_NONE); + } catch (Gio::Error &) { + } +#elif defined(WIN32) + // Get the current user's SID. + HANDLE process_token_raw; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &process_token_raw)) { + return; + } + const WinHandle process_token(process_token_raw); + DWORD actual_token_info_size = 0; + GetTokenInformation(process_token, TokenUser, nullptr, 0, &actual_token_info_size); + if (!actual_token_info_size) { + return; + } + const WinHeapPtr user_token_ptr(actual_token_info_size); + if (!user_token_ptr || !GetTokenInformation( + process_token, + TokenUser, + user_token_ptr, + actual_token_info_size, + &actual_token_info_size + )) { + return; + } + const PSID user_sid = user_token_ptr->User.Sid; + + // Get a handle to the file. + const Glib::ustring filename = file->get_path(); + const HANDLE file_handle_raw = CreateFile( + filename.c_str(), + READ_CONTROL | WRITE_DAC, + 0, + nullptr, + OPEN_EXISTING, + execute ? FILE_FLAG_BACKUP_SEMANTICS : FILE_ATTRIBUTE_NORMAL, + nullptr + ); + if (file_handle_raw == INVALID_HANDLE_VALUE) { + return; + } + const WinHandle file_handle(file_handle_raw); + + // Create the user-only permission and set it. + EXPLICIT_ACCESS ea = { + .grfAccessPermissions = FILE_ALL_ACCESS, + .grfAccessMode = GRANT_ACCESS, + .grfInheritance = NO_INHERITANCE, + }; + BuildTrusteeWithSid(&(ea.Trustee), user_sid); + PACL new_dacl_raw = nullptr; + auto win_error = SetEntriesInAcl(1, &ea, nullptr, &new_dacl_raw); + if (win_error != ERROR_SUCCESS) { + return; + } + const WinLocalPtr new_dacl(new_dacl_raw); + SetSecurityInfo( + file_handle, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, + nullptr, + nullptr, + new_dacl, + nullptr + ); +#endif +} + +/** + * Gets the path to the temp directory, creating it if necessary. + */ +Glib::ustring getTmpDirectory() +{ +#if defined(__linux__) || defined(__APPLE__) || defined(WIN32) + static Glib::ustring recent_dir = ""; + const Glib::ustring tmp_dir_root = Glib::get_tmp_dir(); + const Glib::ustring subdir_base = + Glib::ustring::compose("rawtherapee-%1", Glib::get_user_name()); + Glib::ustring dir = Glib::build_filename(tmp_dir_root, subdir_base); + + // Returns true if the directory doesn't exist or has the right permissions. + auto is_usable_dir = [](const Glib::ustring &dir_path) { + return !Glib::file_test(dir_path, Glib::FILE_TEST_EXISTS) || (Glib::file_test(dir_path, Glib::FILE_TEST_IS_DIR) && hasUserOnlyPermission(dir_path)); + }; + + if (!(is_usable_dir(dir) || recent_dir.empty())) { + // Try to reuse the random suffix directory. + dir = recent_dir; + } + + if (!is_usable_dir(dir)) { + // Create new directory with random suffix. + gchar *const rand_dir = g_dir_make_tmp((subdir_base + "-XXXXXX").c_str(), nullptr); + if (!rand_dir) { + return tmp_dir_root; + } + dir = recent_dir = rand_dir; + g_free(rand_dir); + Glib::RefPtr file = Gio::File::create_for_path(dir); + setUserOnlyPermission(file, true); + } else if (!Glib::file_test(dir, Glib::FILE_TEST_EXISTS)) { + // Create the directory. + Glib::RefPtr file = Gio::File::create_for_path(dir); + bool dir_created = file->make_directory(); + if (!dir_created) { + return tmp_dir_root; + } + setUserOnlyPermission(file, true); + } + + return dir; +#else + return Glib::get_tmp_dir(); +#endif +} } class EditorPanel::ColorManagementToolbar @@ -2058,7 +2289,7 @@ bool EditorPanel::idle_sendToGimp ( ProgressConnector *p dirname = options.editor_custom_out_dir; break; default: // Options::EDITOR_OUT_DIR_TEMP - dirname = Glib::get_tmp_dir(); + dirname = getTmpDirectory(); break; } Glib::ustring fullFileName = Glib::build_filename(dirname, shortname); @@ -2119,6 +2350,8 @@ bool EditorPanel::idle_sentToGimp (ProgressConnector *pc, rtengine::IImagef parent->setProgress (0.); bool success = false; + setUserOnlyPermission(Gio::File::create_for_path(filename), false); + if (options.editorToSendTo == 1) { success = ExtProgStore::openInGimp (filename); } else if (options.editorToSendTo == 2) {