From 3d2f5a0614b271ce38351f9a4ce3640323050e6e Mon Sep 17 00:00:00 2001 From: levy Date: Tue, 10 Dec 2024 12:01:00 +0100 Subject: [PATCH 01/51] Initial tiling_generator class stump --- src/img/griddy.cpp | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/img/griddy.cpp b/src/img/griddy.cpp index 0b44aa6..c516ba6 100644 --- a/src/img/griddy.cpp +++ b/src/img/griddy.cpp @@ -1,16 +1,27 @@ -/* -generateGrid( - array of images - preset struct - document width - gutter width - quantity of cells - whether correct quanity -) -{ +#include +#include -} +struct preset { + size_t document_width; + size_t gutter_width; + size_t tile_count; + bool fill; +}; +class tiling_generator { + std::vector imgs; + preset user_preset; + public: + tiling_generator(const std::vector& images, const preset& input_preset) { + imgs = images; + user_preset = input_preset; + } -*/ + /** + * @brief Generates the tiling using TODO: some algorithm. + * + * @return cv::Mat The image data of the tiling. + */ + cv::Mat static generate() {} +}; From 3cf6167e14f796de9127b2da774e2c065c24a57a Mon Sep 17 00:00:00 2001 From: levy Date: Tue, 10 Dec 2024 12:13:51 +0100 Subject: [PATCH 02/51] Renaming to tiling and breaking apart to a .cpp and .hpp file --- src/img/tiling.cpp | 4 ++++ src/img/tiling.hpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/img/tiling.cpp create mode 100644 src/img/tiling.hpp diff --git a/src/img/tiling.cpp b/src/img/tiling.cpp new file mode 100644 index 0000000..0eacd79 --- /dev/null +++ b/src/img/tiling.cpp @@ -0,0 +1,4 @@ +#include + +static cv::Mat generate() { +} diff --git a/src/img/tiling.hpp b/src/img/tiling.hpp new file mode 100644 index 0000000..50a26fa --- /dev/null +++ b/src/img/tiling.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +struct document_preset { + size_t document_width; + size_t gutter_width; + size_t tile_count; + bool fill; +}; + +class tiling_generator { + std::vector imgs; + document_preset user_preset; + + public: + tiling_generator(const std::vector& images, const document_preset& input_preset) { + imgs = images; + user_preset = input_preset; + } + + /** + * @brief Generates the tiling using TODO: some algorithm. + * + * @return cv::Mat The image data of the tiling. + */ + cv::Mat static generate(); +}; From a3fac20d5de9cbda3722a76a0f2dccc6fc0d5d9c Mon Sep 17 00:00:00 2001 From: levy Date: Tue, 10 Dec 2024 12:54:38 +0100 Subject: [PATCH 03/51] Tidied up source and tiling classes --- src/img/filters/filter.hpp | 0 src/img/filters/filter_store.hpp | 5 +++++ src/img/griddy.cpp | 4 ++-- src/img/source.cpp | 9 --------- src/img/source.hpp | 21 +++++++++++++++++++++ src/img/tiling.cpp | 2 +- src/img/tiling.hpp | 15 +++++++-------- 7 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 src/img/filters/filter.hpp create mode 100644 src/img/filters/filter_store.hpp delete mode 100644 src/img/source.cpp create mode 100644 src/img/source.hpp diff --git a/src/img/filters/filter.hpp b/src/img/filters/filter.hpp new file mode 100644 index 0000000..e69de29 diff --git a/src/img/filters/filter_store.hpp b/src/img/filters/filter_store.hpp new file mode 100644 index 0000000..99627c8 --- /dev/null +++ b/src/img/filters/filter_store.hpp @@ -0,0 +1,5 @@ +#pragma once + +class FilterStore { + +}; diff --git a/src/img/griddy.cpp b/src/img/griddy.cpp index c516ba6..31102c9 100644 --- a/src/img/griddy.cpp +++ b/src/img/griddy.cpp @@ -8,12 +8,12 @@ struct preset { bool fill; }; -class tiling_generator { +class TilingGenerator { std::vector imgs; preset user_preset; public: - tiling_generator(const std::vector& images, const preset& input_preset) { + TilingGenerator(const std::vector& images, const preset& input_preset) { imgs = images; user_preset = input_preset; } diff --git a/src/img/source.cpp b/src/img/source.cpp deleted file mode 100644 index 1f3296c..0000000 --- a/src/img/source.cpp +++ /dev/null @@ -1,9 +0,0 @@ -/* -class source - file - quantity - size_filter: - filters[] - reorder? - source(): implict size filter -*/ diff --git a/src/img/source.hpp b/src/img/source.hpp new file mode 100644 index 0000000..b00c269 --- /dev/null +++ b/src/img/source.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "filters/filter_store.hpp" + +class ImgSource { + /** + * NOTE: It may be optimal to store the file path instead + * of the file data. + */ + cv::Mat file_data; + size_t quantity; + FilterStore filters; + + public: + ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& fil) + : file_data(image), + quantity(amount), + filters(fil) {} +}; diff --git a/src/img/tiling.cpp b/src/img/tiling.cpp index 0eacd79..b5c7012 100644 --- a/src/img/tiling.cpp +++ b/src/img/tiling.cpp @@ -1,4 +1,4 @@ -#include +#include "tiling.hpp" static cv::Mat generate() { } diff --git a/src/img/tiling.hpp b/src/img/tiling.hpp index 50a26fa..55b00b8 100644 --- a/src/img/tiling.hpp +++ b/src/img/tiling.hpp @@ -3,22 +3,21 @@ #include #include -struct document_preset { +struct DocumentPreset { size_t document_width; size_t gutter_width; size_t tile_count; bool fill; }; -class tiling_generator { - std::vector imgs; - document_preset user_preset; +class TilingGenerator { + std::vector imgs; + DocumentPreset user_preset; public: - tiling_generator(const std::vector& images, const document_preset& input_preset) { - imgs = images; - user_preset = input_preset; - } + TilingGenerator(const std::vector& images, const DocumentPreset& input_preset) + : imgs(images), + user_preset(input_preset) {} /** * @brief Generates the tiling using TODO: some algorithm. From 4cdf2824babc85e974cc55e0e4e70dae95b3777d Mon Sep 17 00:00:00 2001 From: levy Date: Tue, 10 Dec 2024 13:02:52 +0100 Subject: [PATCH 04/51] Moved document preset to correct dir --- src/settings/document_presets.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/settings/document_presets.hpp diff --git a/src/settings/document_presets.hpp b/src/settings/document_presets.hpp new file mode 100644 index 0000000..fefc0a0 --- /dev/null +++ b/src/settings/document_presets.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +/* +gutter +margin +ppi +guide +min max height +*/ + +struct DocumentPreset { + size_t document_width; + size_t gutter_width; + size_t tile_count; + bool fill; +}; From da3b7e986f58c3531a06b1d13923dc922c3ee13c Mon Sep 17 00:00:00 2001 From: levy Date: Tue, 10 Dec 2024 16:32:05 +0100 Subject: [PATCH 05/51] Reorganized class structure --- .clang-format | 4 ++-- src/img/griddy.cpp | 27 --------------------------- src/img/source.hpp | 8 ++++---- src/img/tiling.cpp | 36 +++++++++++++++++++++++++++++++++--- src/img/tiling.hpp | 28 ---------------------------- 5 files changed, 39 insertions(+), 64 deletions(-) delete mode 100644 src/img/griddy.cpp delete mode 100644 src/img/tiling.hpp diff --git a/.clang-format b/.clang-format index 4e687af..cae31cb 100644 --- a/.clang-format +++ b/.clang-format @@ -5,11 +5,11 @@ AllowAllParametersOfDeclarationOnNextLine: 'false' AlwaysBreakTemplateDeclarations: 'No' BreakBeforeBraces: Attach - ColumnLimit: '100' + ColumnLimit: '80' ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' IncludeBlocks: Regroup IndentPPDirectives: AfterHash - IndentWidth: '2' + IndentWidth: '4' NamespaceIndentation: All BreakBeforeBinaryOperators: All BreakBeforeTernaryOperators: 'true' diff --git a/src/img/griddy.cpp b/src/img/griddy.cpp deleted file mode 100644 index 31102c9..0000000 --- a/src/img/griddy.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include - -struct preset { - size_t document_width; - size_t gutter_width; - size_t tile_count; - bool fill; -}; - -class TilingGenerator { - std::vector imgs; - preset user_preset; - - public: - TilingGenerator(const std::vector& images, const preset& input_preset) { - imgs = images; - user_preset = input_preset; - } - - /** - * @brief Generates the tiling using TODO: some algorithm. - * - * @return cv::Mat The image data of the tiling. - */ - cv::Mat static generate() {} -}; diff --git a/src/img/source.hpp b/src/img/source.hpp index b00c269..5fe83bf 100644 --- a/src/img/source.hpp +++ b/src/img/source.hpp @@ -13,9 +13,9 @@ class ImgSource { size_t quantity; FilterStore filters; - public: + public: ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& fil) - : file_data(image), - quantity(amount), - filters(fil) {} + : file_data(image), quantity(amount), filters(fil) {} + + cv::Mat getImg() const { return file_data; } }; diff --git a/src/img/tiling.cpp b/src/img/tiling.cpp index b5c7012..66ae58e 100644 --- a/src/img/tiling.cpp +++ b/src/img/tiling.cpp @@ -1,4 +1,34 @@ -#include "tiling.hpp" +#include +#include -static cv::Mat generate() { -} +#include "settings/document_presets.hpp" +#include "source.hpp" + +/** + * @brief Based on the following paper: 10.1016/j.ipl.2015.08.008 + */ +namespace tiling { + void recursive_helper(); + + std::vector> generate( + std::vector images, const DocumentPreset& preset) { + /** + * @brief The authors of the algorithm sort the rectangles internally by + * width. This is the most optimal for a lot of cases but not always. We + * solve the problem with both width-sorting and height-sorting, and compare + * the height of each, then return the smaller one. + */ + + /** + * (1) Let D = 1, for each item, swap its width and height + * if its width is larger than its height, sort all items by + * non-increasing width, H = 0, x = 0, y = 0. + */ + bool rotation_allowed = true; // D = 1 + + std::sort(images.begin(), images.end(), + [](const ImgSource& a, const ImgSource& b) { + return a.getImg().rows > b.getImg().rows; + }); + } +}; // namespace tiling diff --git a/src/img/tiling.hpp b/src/img/tiling.hpp deleted file mode 100644 index 55b00b8..0000000 --- a/src/img/tiling.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -struct DocumentPreset { - size_t document_width; - size_t gutter_width; - size_t tile_count; - bool fill; -}; - -class TilingGenerator { - std::vector imgs; - DocumentPreset user_preset; - - public: - TilingGenerator(const std::vector& images, const DocumentPreset& input_preset) - : imgs(images), - user_preset(input_preset) {} - - /** - * @brief Generates the tiling using TODO: some algorithm. - * - * @return cv::Mat The image data of the tiling. - */ - cv::Mat static generate(); -}; From 7873228060d0df24181ac8f9baa63fc7e11d98bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Tue, 10 Dec 2024 14:03:57 +0100 Subject: [PATCH 06/51] size filter --- CMakeLists.txt | 3 +++ src/img/filters/filter.cpp | 4 ---- src/img/filters/filter.hpp | 7 +++++++ src/img/filters/size.cpp | 21 +++++++++++++-------- src/img/filters/size.hpp | 16 ++++++++++++++++ src/settings/document_presets.cpp | 1 + src/settings/roll_presets.cpp | 5 ----- 7 files changed, 40 insertions(+), 17 deletions(-) delete mode 100644 src/img/filters/filter.cpp create mode 100644 src/img/filters/size.hpp delete mode 100644 src/settings/roll_presets.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 625ab30..9f7269b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,9 @@ qt_standard_project_setup() # Create executable qt_add_executable(printf src/main.cpp + src/img/filters/filter.hpp + src/img/filters/size.hpp + src/img/filters/size.cpp ) # link to your dependencies' targets here diff --git a/src/img/filters/filter.cpp b/src/img/filters/filter.cpp deleted file mode 100644 index 24483e3..0000000 --- a/src/img/filters/filter.cpp +++ /dev/null @@ -1,4 +0,0 @@ -/* -abstract class filter - public apply(img) -> img -*/ diff --git a/src/img/filters/filter.hpp b/src/img/filters/filter.hpp index e69de29..43de058 100644 --- a/src/img/filters/filter.hpp +++ b/src/img/filters/filter.hpp @@ -0,0 +1,7 @@ +#include + +class Filter +{ +public: + virtual cv::Mat &apply(const cv::Mat &) = 0; +}; diff --git a/src/img/filters/size.cpp b/src/img/filters/size.cpp index 8f2268f..e2653c9 100644 --- a/src/img/filters/size.cpp +++ b/src/img/filters/size.cpp @@ -1,8 +1,13 @@ -/* -implements filter - isUniform = true - width get set - height get set - - public apply() -*/ +#include "size.hpp" + +SizeFilter::SizeFilter(int width, int height) : width(width), height(height), isUniform(true) {} + +cv::Mat &SizeFilter::apply(const cv::Mat &image) +{ + cv::Mat resized; + + bool downScaling = width < image.cols || height < image.rows; + + cv::resize(image, resized, cv::Size(width, height), 0, 0, downScaling ? downScalingInterpolation : upScalingInterpolation); + return resized; +} diff --git a/src/img/filters/size.hpp b/src/img/filters/size.hpp new file mode 100644 index 0000000..ae405b4 --- /dev/null +++ b/src/img/filters/size.hpp @@ -0,0 +1,16 @@ +#include "filter.hpp" +#include + +class SizeFilter : public Filter +{ +private: + static const int downScalingInterpolation = cv::INTER_AREA; + static const int upScalingInterpolation = cv::INTER_CUBIC; + int width; + int height; + bool isUniform; +public: + SizeFilter(int width, int height); + + cv::Mat &apply(const cv::Mat &image) override; +}; diff --git a/src/settings/document_presets.cpp b/src/settings/document_presets.cpp index 3066326..aea9845 100644 --- a/src/settings/document_presets.cpp +++ b/src/settings/document_presets.cpp @@ -1,4 +1,5 @@ /* +width gutter margin ppi diff --git a/src/settings/roll_presets.cpp b/src/settings/roll_presets.cpp deleted file mode 100644 index 84d6a7a..0000000 --- a/src/settings/roll_presets.cpp +++ /dev/null @@ -1,5 +0,0 @@ -/* -name -width -pixel perfect? -*/ From 16f93594bda9dffe5687a0ed86560a666c18d8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Tue, 10 Dec 2024 16:21:22 +0100 Subject: [PATCH 07/51] mask filter --- .clang-format | 2 +- CMakeLists.txt | 2 ++ assets/mask.png | Bin 0 -> 56100 bytes src/img/filters/filter.hpp | 8 ++++---- src/img/filters/mask.cpp | 28 +++++++++++++++++++++++----- src/img/filters/mask.hpp | 17 +++++++++++++++++ src/img/filters/size.cpp | 11 ++++++----- src/img/filters/size.hpp | 23 ++++++++++++++--------- src/main.cpp | 28 +++++++++------------------- 9 files changed, 76 insertions(+), 43 deletions(-) create mode 100644 assets/mask.png create mode 100644 src/img/filters/mask.hpp diff --git a/.clang-format b/.clang-format index cae31cb..ca469b8 100644 --- a/.clang-format +++ b/.clang-format @@ -5,7 +5,7 @@ AllowAllParametersOfDeclarationOnNextLine: 'false' AlwaysBreakTemplateDeclarations: 'No' BreakBeforeBraces: Attach - ColumnLimit: '80' + ColumnLimit: '128' ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' IncludeBlocks: Regroup IndentPPDirectives: AfterHash diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f7269b..d19dd12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,8 @@ qt_add_executable(printf src/img/filters/filter.hpp src/img/filters/size.hpp src/img/filters/size.cpp + src/img/filters/mask.hpp + src/img/filters/mask.cpp ) # link to your dependencies' targets here diff --git a/assets/mask.png b/assets/mask.png new file mode 100644 index 0000000000000000000000000000000000000000..1e59a61364ff336c17a3e7a0cceece86fb4d05c6 GIT binary patch literal 56100 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

BRqY6pK)@9o4 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 literal 0 HcmV?d00001 diff --git a/src/img/filters/filter.hpp b/src/img/filters/filter.hpp index 43de058..f700a96 100644 --- a/src/img/filters/filter.hpp +++ b/src/img/filters/filter.hpp @@ -1,7 +1,7 @@ +#pragma once #include -class Filter -{ -public: - virtual cv::Mat &apply(const cv::Mat &) = 0; +class Filter { + public: + virtual cv::Mat apply(const cv::Mat &image) = 0; }; diff --git a/src/img/filters/mask.cpp b/src/img/filters/mask.cpp index 10fb5ce..1bbe6c2 100644 --- a/src/img/filters/mask.cpp +++ b/src/img/filters/mask.cpp @@ -1,6 +1,24 @@ -/* -implements filter - masksource +#include "mask.hpp" - public apply() -*/ +#include "size.hpp" + +MaskFilter::MaskFilter(const cv::Mat &mask) : mask(mask) {} + +cv::Mat MaskFilter::apply(const cv::Mat &image) { + auto fitMask = SizeFilter::resize(mask, image.cols, image.rows); + + if (invert) { + cv::bitwise_not(fitMask, fitMask); + } + cv::normalize(fitMask, fitMask, 0., 1., cv::NORM_MINMAX, CV_32F); + + cv::Mat result; + image.convertTo(result, CV_32F); + + cv::multiply(result, fitMask, result); + + result.convertTo(result, CV_8U); + return result; +} + +void MaskFilter::setInvert(bool invert) { this->invert = invert; } diff --git a/src/img/filters/mask.hpp b/src/img/filters/mask.hpp new file mode 100644 index 0000000..cf4dc14 --- /dev/null +++ b/src/img/filters/mask.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +#include "filter.hpp" + +class MaskFilter : public Filter { + private: + cv::Mat mask; + bool invert = false; + + public: + MaskFilter(const cv::Mat &mask); + + cv::Mat apply(const cv::Mat &image) override; + + void setInvert(bool invert); +}; diff --git a/src/img/filters/size.cpp b/src/img/filters/size.cpp index e2653c9..1745275 100644 --- a/src/img/filters/size.cpp +++ b/src/img/filters/size.cpp @@ -1,13 +1,14 @@ #include "size.hpp" -SizeFilter::SizeFilter(int width, int height) : width(width), height(height), isUniform(true) {} +SizeFilter::SizeFilter(int width, int height) : width(width), height(height) {} -cv::Mat &SizeFilter::apply(const cv::Mat &image) -{ - cv::Mat resized; +cv::Mat SizeFilter::apply(const cv::Mat &image) { return SizeFilter::resize(image, width, height); } +cv::Mat SizeFilter::resize(const cv::Mat &image, int width, int height, int interDown, int interUp) { bool downScaling = width < image.cols || height < image.rows; - cv::resize(image, resized, cv::Size(width, height), 0, 0, downScaling ? downScalingInterpolation : upScalingInterpolation); + cv::Mat resized; + cv::resize(image, resized, cv::Size(width, height), 0, 0, downScaling ? interDown : interUp); + return resized; } diff --git a/src/img/filters/size.hpp b/src/img/filters/size.hpp index ae405b4..50eeccf 100644 --- a/src/img/filters/size.hpp +++ b/src/img/filters/size.hpp @@ -1,16 +1,21 @@ -#include "filter.hpp" +#pragma once #include -class SizeFilter : public Filter -{ -private: - static const int downScalingInterpolation = cv::INTER_AREA; - static const int upScalingInterpolation = cv::INTER_CUBIC; +#include "filter.hpp" + +class SizeFilter : public Filter { + private: + static const int sizeInterDown = cv::INTER_AREA; + static const int sizeInterUp = cv::INTER_CUBIC; int width; int height; - bool isUniform; -public: + bool lockAspect = true; + + public: SizeFilter(int width, int height); - cv::Mat &apply(const cv::Mat &image) override; + cv::Mat apply(const cv::Mat &image) override; + + static cv::Mat resize(const cv::Mat &image, int width, int height, int interDown = sizeInterDown, + int interUp = sizeInterUp); }; diff --git a/src/main.cpp b/src/main.cpp index d0e5f5d..cc536d6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,34 +8,24 @@ #include #include -using json = nlohmann::json; - -cv::Mat generateGrid(const cv::Mat& image, int rows, int cols) { - if (image.empty()) { - throw std::runtime_error("Error: Image is empty!"); - } - - cv::Mat gridImage(image.rows * rows, image.cols * cols, image.type()); +#include "img/filters/mask.hpp" - for (int i = 0; i < rows; ++i) { - for (int j = 0; j < cols; ++j) { - cv::Rect cellRect(j * image.cols, i * image.rows, image.cols, image.rows); - image.copyTo(gridImage(cellRect)); - } - } +using json = nlohmann::json; - return gridImage; -} int main(void) { // read an image cv::Mat image = cv::imread("./assets/3.png", 1); - std::cout << "Image size: " << image.size() << std::endl; - std::cout << image.empty() << std::endl; + + cv::Mat mask = cv::imread("./assets/mask.png", 1); + + MaskFilter maskFilter(mask); + // create image window named "My Image" cv::namedWindow("My Image"); + // show the image on window - cv::imshow("My Image", generateGrid(image, 4, 4)); + cv::imshow("My Image", maskFilter.apply(image)); // aboszolút filmszínház cv::waitKey(0); From 13c88a0b0404ffa497cbd5c42035629caecdbfcf Mon Sep 17 00:00:00 2001 From: levy Date: Tue, 10 Dec 2024 18:12:58 +0100 Subject: [PATCH 08/51] Started work on tiling function --- .clang-format | 2 +- .vscode/settings.json | 3 + src/img/tiling.cpp | 129 ++++++++++++++++++++++++++++++++++-------- src/img/tiling.hpp | 33 +++++++++++ 4 files changed, 141 insertions(+), 26 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/img/tiling.hpp diff --git a/.clang-format b/.clang-format index cae31cb..e7424f1 100644 --- a/.clang-format +++ b/.clang-format @@ -5,7 +5,7 @@ AllowAllParametersOfDeclarationOnNextLine: 'false' AlwaysBreakTemplateDeclarations: 'No' BreakBeforeBraces: Attach - ColumnLimit: '80' + ColumnLimit: '100' ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' IncludeBlocks: Regroup IndentPPDirectives: AfterHash diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b4d8c35 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools" +} \ No newline at end of file diff --git a/src/img/tiling.cpp b/src/img/tiling.cpp index 66ae58e..897388e 100644 --- a/src/img/tiling.cpp +++ b/src/img/tiling.cpp @@ -1,34 +1,113 @@ +#include "tiling.hpp" + #include #include -#include "settings/document_presets.hpp" -#include "source.hpp" - /** * @brief Based on the following paper: 10.1016/j.ipl.2015.08.008 */ namespace tiling { - void recursive_helper(); - - std::vector> generate( - std::vector images, const DocumentPreset& preset) { - /** - * @brief The authors of the algorithm sort the rectangles internally by - * width. This is the most optimal for a lot of cases but not always. We - * solve the problem with both width-sorting and height-sorting, and compare - * the height of each, then return the smaller one. - */ - - /** - * (1) Let D = 1, for each item, swap its width and height - * if its width is larger than its height, sort all items by - * non-increasing width, H = 0, x = 0, y = 0. - */ - bool rotation_allowed = true; // D = 1 - - std::sort(images.begin(), images.end(), - [](const ImgSource& a, const ImgSource& b) { - return a.getImg().rows > b.getImg().rows; - }); + void Rect2D::rotate() { + size_t temp = width; + width = height; + height = temp; + rotated = true; } + + namespace { + void recursive_helper() {} + + std::pair> tile(std::vector rects, + const DocumentPreset& preset, + bool widthSort = true) { + /** + * (1) Let D = 1, for each item, swap its width and height + * if its width is larger than its height, sort all items by + * non-increasing width H = 0, x = 0, y = 0. + */ + bool rotation_allowed = true; // D = 1 + int height_of_tiling = 0; // H = 0 + + int x, y = 0; + + std::for_each(rects.begin(), rects.end(), [&](Rect2D rect) { + if (rect.width > rect.height) rect.rotate(); + }); + + if (widthSort) { + std::sort(rects.begin(), rects.end(), + [&](const Rect2D& a, const Rect2D& b) { return a.width > b.width; }); + } else { + std::sort(rects.begin(), rects.end(), + [&](const Rect2D& a, const Rect2D& b) { return a.height > b.height; }); + } + + /** + * (2) If h_{i} > W then x = w_{i} , y = H , w = W − w_{i} , h = h_{i} , + * H = H + h; otherwise, x = h_{i} , y = H , w = W − h_{i} , + * h = w_{i} , H = H + h. + */ + int w, h; + std::for_each(rects.begin(), rects.end(), [&](Rect2D rect) { + y = height_of_tiling; + if (rect.height > preset.document_width) { + x = rect.width; + w = preset.document_width - rect.width; + h = rect.height; + } else { + x = rect.height; + w = preset.document_width - rect.height; + h = rect.width; + } + height_of_tiling += h; + + /** + * (3) RecursivePacking(x, y, w, h). + */ + recursive_helper(x, y, w, h); + }); + + /** + * (4) If unpacked items remain, let B = B_{1} go to (2); otherwise stop. + * TODO: figure this out + */ + + return std::make_pair(height_of_tiling, rects); + } // namespace + + cv::Mat generate(std::vector images, const DocumentPreset& preset) { + /** + * @brief The authors of the algorithm sort the rectangles internally by + * width. This is the most optimal for a lot of cases but not always. We + * solve the problem with both width-sorting and height-sorting, and + * compare the height of each, then return the smaller one. + */ + + /** + * Create `Rect2D`-sform images + */ + std::vector rects = {}; + std::for_each(images.begin(), images.end(), + [&](const ImgSource& img) { rects.push_back(Rect2D(img)); }); + + /** + * Gutter stuff here + */ + + std::vector rects_clone = rects; + + std::pair> width_sorting_height = tile(rects, preset); + std::pair> height_sorting_height + = tile(rects_clone, preset, false); + + std::vector optimal_tiling + = width_sorting_height.first < height_sorting_height.first + ? width_sorting_height.second + : height_sorting_height.second; + + std::for_each(optimal_tiling.begin(), optimal_tiling.end(), [&](Rect2D rect) { + // TODO: gilg does his opencv magic... + }); + } + } // namespace }; // namespace tiling diff --git a/src/img/tiling.hpp b/src/img/tiling.hpp new file mode 100644 index 0000000..3be7f08 --- /dev/null +++ b/src/img/tiling.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "settings/document_presets.hpp" +#include "source.hpp" + +namespace tiling { + class Rect2D { + const ImgSource& image; + cv::Point bottom_left_corner; + bool rotated; + + public: + size_t width, height; + + Rect2D(const ImgSource& img) + : image(img), width(img.getImg().cols), height(img.getImg().rows), rotated(false) {} + + void rotate(); + + bool isRotated() const { return rotated; } + }; + + namespace { + void recursive_helper(); + + std::pair> tile(std::vector rects, + const DocumentPreset& preset, + bool widthSort = true); + } // namespace + + std::vector generate(std::vector images, + const DocumentPreset& preset); +}; // namespace tiling From 0169cea06967f3d19ccc0b302aa900c262f3019f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Tue, 10 Dec 2024 18:13:00 +0100 Subject: [PATCH 09/51] filterstore --- src/img/filters/filter_store.cpp | 17 +++++++++++++++++ src/img/filters/filter_store.hpp | 12 ++++++++++++ src/img/source.cpp | 8 ++++++++ src/img/source.hpp | 7 ++++--- 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/img/filters/filter_store.cpp create mode 100644 src/img/source.cpp diff --git a/src/img/filters/filter_store.cpp b/src/img/filters/filter_store.cpp new file mode 100644 index 0000000..268cfd2 --- /dev/null +++ b/src/img/filters/filter_store.cpp @@ -0,0 +1,17 @@ +#include "filter_store.hpp" + +void FilterStore::addFilter(Filter *filter) { filters.push_back(filter); } + +cv::Mat FilterStore::applyAll(const cv::Mat &image) const { + cv::Mat result = image; + for (auto filter : filters) { + result = filter->apply(result); + } + return result; +} + +FilterStore::~FilterStore() { + for (auto filter : filters) { + delete filter; + } +} diff --git a/src/img/filters/filter_store.hpp b/src/img/filters/filter_store.hpp index 99627c8..76c2163 100644 --- a/src/img/filters/filter_store.hpp +++ b/src/img/filters/filter_store.hpp @@ -1,5 +1,17 @@ #pragma once +#include + +#include "filter.hpp" + class FilterStore { + private: + std::vector filters; + + public: + void addFilter(Filter *filter); + + cv::Mat applyAll(const cv::Mat &image) const; + ~FilterStore(); }; diff --git a/src/img/source.cpp b/src/img/source.cpp new file mode 100644 index 0000000..6c022c2 --- /dev/null +++ b/src/img/source.cpp @@ -0,0 +1,8 @@ +#include "source.hpp" + +ImgSource::ImgSource(const cv::Mat &image, const size_t &amount, const FilterStore &fil) + : file_data(image), quantity(amount), filters(fil) {} + +cv::Mat ImgSource::getImg() const { return file_data; } + +cv::Mat ImgSource::applyFilters() const { return filters.applyAll(file_data); } diff --git a/src/img/source.hpp b/src/img/source.hpp index 5fe83bf..f818e74 100644 --- a/src/img/source.hpp +++ b/src/img/source.hpp @@ -14,8 +14,9 @@ class ImgSource { FilterStore filters; public: - ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& fil) - : file_data(image), quantity(amount), filters(fil) {} + ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& fil); - cv::Mat getImg() const { return file_data; } + cv::Mat getImg() const; + + cv::Mat applyFilters() const; }; From f8692012f0734ac6c6b1bdd80f8e3cc4de8443df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Tue, 10 Dec 2024 18:32:32 +0100 Subject: [PATCH 10/51] pasting --- src/img/tiling.cpp | 13 ++++++++++++- src/img/tiling.hpp | 6 +++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/img/tiling.cpp b/src/img/tiling.cpp index 897388e..d3c7b30 100644 --- a/src/img/tiling.cpp +++ b/src/img/tiling.cpp @@ -100,13 +100,24 @@ namespace tiling { std::pair> height_sorting_height = tile(rects_clone, preset, false); + int optimal_tiling_height + = std::min(width_sorting_height.first, height_sorting_height.first); std::vector optimal_tiling = width_sorting_height.first < height_sorting_height.first ? width_sorting_height.second : height_sorting_height.second; + cv::Mat canvas = cv::Mat::zeros(optimal_tiling_height, preset.document_width, CV_8UC4); std::for_each(optimal_tiling.begin(), optimal_tiling.end(), [&](Rect2D rect) { - // TODO: gilg does his opencv magic... + auto image = rect.getImg(); + + if (rect.isRotated()) { + cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); + } + auto dstCoord = rect.getPrimaryCorner(); + auto dstRect = cv::Rect(dstCoord.x, dstCoord.y, rect.width, rect.height); + + image.copyTo(canvas(dstRect)); }); } } // namespace diff --git a/src/img/tiling.hpp b/src/img/tiling.hpp index 3be7f08..6c02543 100644 --- a/src/img/tiling.hpp +++ b/src/img/tiling.hpp @@ -6,7 +6,7 @@ namespace tiling { class Rect2D { const ImgSource& image; - cv::Point bottom_left_corner; + cv::Point primaryCorner; bool rotated; public: @@ -18,6 +18,10 @@ namespace tiling { void rotate(); bool isRotated() const { return rotated; } + + cv::Mat getImg() const { return image.getImg(); } + + cv::Point getPrimaryCorner() const { return primaryCorner; } }; namespace { From b113b9e9e91c7f4778adc59c92409b9e976086dd Mon Sep 17 00:00:00 2001 From: levy Date: Wed, 11 Dec 2024 01:29:47 +0100 Subject: [PATCH 11/51] Research into recursive function, Rect2D refactor --- src/img/source.hpp | 8 ++- src/img/tiling.cpp | 156 ++++++++++++++++++++++++--------------------- src/img/tiling.hpp | 25 +++++--- 3 files changed, 104 insertions(+), 85 deletions(-) diff --git a/src/img/source.hpp b/src/img/source.hpp index 5fe83bf..1074b2e 100644 --- a/src/img/source.hpp +++ b/src/img/source.hpp @@ -10,12 +10,14 @@ class ImgSource { * of the file data. */ cv::Mat file_data; - size_t quantity; + size_t amount; FilterStore filters; public: - ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& fil) - : file_data(image), quantity(amount), filters(fil) {} + ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& filters) + : file_data(image), amount(amount), filters(filters) {} cv::Mat getImg() const { return file_data; } + + size_t getAmount() const { return amount; } }; diff --git a/src/img/tiling.cpp b/src/img/tiling.cpp index d3c7b30..267c3a0 100644 --- a/src/img/tiling.cpp +++ b/src/img/tiling.cpp @@ -1,90 +1,103 @@ #include "tiling.hpp" #include +#include #include /** - * @brief Based on the following paper: 10.1016/j.ipl.2015.08.008 + * @brief Based on the following paper (D. Zhang et al.): 10.1016/j.ipl.2015.08.008 */ namespace tiling { void Rect2D::rotate() { size_t temp = width; width = height; height = temp; - rotated = true; + rotated = !rotated; } namespace { - void recursive_helper() {} + /** + * @brief Recursive base case for `recursive_packing` function. Let `S` denote the area we + * are tiling. + * + * @param rects The rectangles we are using for tiling + * @param remainging_width Width of S + * @param current_height Height of S + * @return true If we have no more tiles to place, or S can't fit any that we can place. + * @return false If we have even a single tile that can be placed and fits into S. + */ + bool recursive_base_case(std::vector rects, size_t remainging_width, + size_t current_height) {} + + void recursive_packing(std::vector rects, cv::Point current_point, + size_t remainging_width, size_t current_height) { + if (recursive_base_case(rects, remainging_width, current_height)) return; + + std::vector unplaced; + std::ranges::copy_if(rects, std::back_inserter(unplaced), + [](Rect2D rect) { return rect.moreRemaining(); }); + + + } + /** + * @brief NOTE: As per D. Zhang et al., the sorting of the rectangles can have a very big + * effect on packing efficiency. On average, for our use case sorting by width performs + * best, followed by area, perimeter, and finally height. + * + * Since there are so many heuristics which this function could take into consideration, it + * is the duty of the caller to provide the sorting with which this function is to tile the + * canvas. + * + * @param rects The rectangles to tile + * @param preset Configurable document data + * @return std::pair> The total height of the packing, and a vector + * of rectangles which all have a position on the plane. + */ std::pair> tile(std::vector rects, - const DocumentPreset& preset, - bool widthSort = true) { - /** - * (1) Let D = 1, for each item, swap its width and height - * if its width is larger than its height, sort all items by - * non-increasing width H = 0, x = 0, y = 0. - */ - bool rotation_allowed = true; // D = 1 - int height_of_tiling = 0; // H = 0 + const DocumentPreset& preset) { + size_t height_of_tiling = 0; + cv::Point current_point = cv::Point(0, 0); - int x, y = 0; + size_t remaining_width, current_height; - std::for_each(rects.begin(), rects.end(), [&](Rect2D rect) { - if (rect.width > rect.height) rect.rotate(); - }); + for (auto&& rect : rects) { + if (!rect.moreRemaining()) continue; - if (widthSort) { - std::sort(rects.begin(), rects.end(), - [&](const Rect2D& a, const Rect2D& b) { return a.width > b.width; }); - } else { - std::sort(rects.begin(), rects.end(), - [&](const Rect2D& a, const Rect2D& b) { return a.height > b.height; }); - } + current_point.y = height_of_tiling; - /** - * (2) If h_{i} > W then x = w_{i} , y = H , w = W − w_{i} , h = h_{i} , - * H = H + h; otherwise, x = h_{i} , y = H , w = W − h_{i} , - * h = w_{i} , H = H + h. - */ - int w, h; - std::for_each(rects.begin(), rects.end(), [&](Rect2D rect) { - y = height_of_tiling; - if (rect.height > preset.document_width) { - x = rect.width; - w = preset.document_width - rect.width; - h = rect.height; - } else { - x = rect.height; - w = preset.document_width - rect.height; - h = rect.width; - } - height_of_tiling += h; + rect.place(current_point); + + if (rect.height <= preset.document_width) rect.rotate(); + + current_point.x = rect.width; + remaining_width = preset.document_width - rect.width; + current_height = rect.height; - /** - * (3) RecursivePacking(x, y, w, h). - */ - recursive_helper(x, y, w, h); - }); + height_of_tiling += current_height; + + recursive_packing(rects, current_point, remaining_width, current_height); + + current_point.x = 0; + } /** * (4) If unpacked items remain, let B = B_{1} go to (2); otherwise stop. - * TODO: figure this out */ + for (auto&& rect : rects) { + if (rect.moreRemaining()) { + /** + * TODO: Figure this out. + */ + } + } return std::make_pair(height_of_tiling, rects); } // namespace cv::Mat generate(std::vector images, const DocumentPreset& preset) { /** - * @brief The authors of the algorithm sort the rectangles internally by - * width. This is the most optimal for a lot of cases but not always. We - * solve the problem with both width-sorting and height-sorting, and - * compare the height of each, then return the smaller one. - */ - - /** - * Create `Rect2D`-sform images + * Create `Rect2D`'s from images */ std::vector rects = {}; std::for_each(images.begin(), images.end(), @@ -94,31 +107,26 @@ namespace tiling { * Gutter stuff here */ - std::vector rects_clone = rects; + /** + * TODO: Expand heuristics for total area and perimeter + * (and maybe even diagonal length). + */ + std::pair> width_sorting = tile(rects, preset); + std::vector optimal_tiling = width_sorting.second; - std::pair> width_sorting_height = tile(rects, preset); - std::pair> height_sorting_height - = tile(rects_clone, preset, false); + cv::Mat canvas = cv::Mat::zeros(width_sorting.first, preset.document_width, CV_8UC4); - int optimal_tiling_height - = std::min(width_sorting_height.first, height_sorting_height.first); - std::vector optimal_tiling - = width_sorting_height.first < height_sorting_height.first - ? width_sorting_height.second - : height_sorting_height.second; + for (auto&& rect : optimal_tiling) { + cv::Mat image = rect.getImg(); - cv::Mat canvas = cv::Mat::zeros(optimal_tiling_height, preset.document_width, CV_8UC4); - std::for_each(optimal_tiling.begin(), optimal_tiling.end(), [&](Rect2D rect) { - auto image = rect.getImg(); + if (rect.isRotated()) cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); - if (rect.isRotated()) { - cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); - } - auto dstCoord = rect.getPrimaryCorner(); - auto dstRect = cv::Rect(dstCoord.x, dstCoord.y, rect.width, rect.height); + cv::Point dstCoord = rect.getPrimaryCorner(); + cv::Rect dstRect = cv::Rect(dstCoord.x, dstCoord.y, rect.width, rect.height); image.copyTo(canvas(dstRect)); - }); + } + return canvas; } } // namespace }; // namespace tiling diff --git a/src/img/tiling.hpp b/src/img/tiling.hpp index 6c02543..3ec1795 100644 --- a/src/img/tiling.hpp +++ b/src/img/tiling.hpp @@ -6,30 +6,39 @@ namespace tiling { class Rect2D { const ImgSource& image; - cv::Point primaryCorner; - bool rotated; + /** + * TODO: Rewrite some parts of tiling.cpp because `primary_corner` changed + * from a cv::Point to a vector of points + */ + std::vector> primary_corner; + size_t amount_placed; public: size_t width, height; Rect2D(const ImgSource& img) - : image(img), width(img.getImg().cols), height(img.getImg().rows), rotated(false) {} + : image(img), width(img.getImg().cols), height(img.getImg().rows), amount_placed(0) {} void rotate(); - bool isRotated() const { return rotated; } + size_t getSize() const { return width * height; } cv::Mat getImg() const { return image.getImg(); } - cv::Point getPrimaryCorner() const { return primaryCorner; } + bool moreRemaining() const { return amount_placed < image.getAmount(); } + + void place(const cv::Point& point, bool rotated) { + primary_corner.push_back(std::make_pair(rotated, point)); + amount_placed++; + } }; namespace { - void recursive_helper(); + void recursive_packing(std::vector rects, cv::Point current_point, + size_t remainging_width, size_t current_height); std::pair> tile(std::vector rects, - const DocumentPreset& preset, - bool widthSort = true); + const DocumentPreset& preset); } // namespace std::vector generate(std::vector images, From 6f6c6405c0a7ae6e4ce67661ed4d3f14f4f135c4 Mon Sep 17 00:00:00 2001 From: levy Date: Wed, 11 Dec 2024 01:49:41 +0100 Subject: [PATCH 12/51] CRAZY HAMBURGEf --- src/img/filters/mask.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/img/filters/mask.cpp b/src/img/filters/mask.cpp index 1bbe6c2..e934939 100644 --- a/src/img/filters/mask.cpp +++ b/src/img/filters/mask.cpp @@ -3,7 +3,12 @@ #include "size.hpp" MaskFilter::MaskFilter(const cv::Mat &mask) : mask(mask) {} - +/** + * @brief CRAZY HAMBURGEf!!!! + * + * @param image + * @return cv::Mat + */ cv::Mat MaskFilter::apply(const cv::Mat &image) { auto fitMask = SizeFilter::resize(mask, image.cols, image.rows); From 62602cdf7b599bfceb5702743b242ff07e4c7ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orb=C3=A1n=20Levente=20L=C3=A1szl=C3=B3?= Date: Sat, 8 Feb 2025 13:56:02 +0100 Subject: [PATCH 13/51] change ImgSource to store path instead of cv::Mat --- .vscode/settings.json | 3 --- src/img/source.hpp | 12 ++++++------ 2 files changed, 6 insertions(+), 9 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index b4d8c35..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools" -} \ No newline at end of file diff --git a/src/img/source.hpp b/src/img/source.hpp index 1074b2e..a2791b2 100644 --- a/src/img/source.hpp +++ b/src/img/source.hpp @@ -1,23 +1,23 @@ #pragma once #include +#include #include "filters/filter_store.hpp" class ImgSource { /** - * NOTE: It may be optimal to store the file path instead - * of the file data. + * NOTE: maybe should store cv::Mat instead of img_path */ - cv::Mat file_data; + std::string img_path; size_t amount; FilterStore filters; public: - ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& filters) - : file_data(image), amount(amount), filters(filters) {} + ImgSource(const std::string& image_path, const size_t& amount, const FilterStore& filters) + : img_path(image_path), amount(amount), filters(filters) {} - cv::Mat getImg() const { return file_data; } + cv::Mat getImg() const { return cv::imread(img_path, 1); } size_t getAmount() const { return amount; } }; From 5703b790154cf4ce40bf0a445a41dbd70ab2aa94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Sat, 8 Feb 2025 13:59:30 +0100 Subject: [PATCH 14/51] eugh --- meson.build | 21 +++++++++++++++------ src/main.cpp | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/meson.build b/meson.build index 8b87164..cd297d0 100644 --- a/meson.build +++ b/meson.build @@ -10,12 +10,21 @@ qt6_dep = dependency('qt6', modules: ['Core']) # Source files srcs = files( - 'src/main.cpp', - 'src/img/filters/filter.hpp', - 'src/img/filters/size.hpp', - 'src/img/filters/size.cpp', - 'src/img/filters/mask.hpp', - 'src/img/filters/mask.cpp', + 'src/img/filters/filter.hpp', + 'src/img/filters/filter_store.hpp', + 'src/img/filters/filter_store.cpp', + 'src/img/filters/mask.hpp', + 'src/img/filters/mask.cpp', + 'src/img/filters/size.hpp', + 'src/img/filters/size.cpp', + 'src/img/source.hpp', + 'src/img/source.cpp', + # 'src/img/tiling.cpp', + # 'src/settings/document_presets.hpp', + # 'src/settings/document_presets.cpp', + # 'src/settings/source_presets.cpp', + 'src/util/convert.cpp', + 'src/main.cpp', ) # Executable diff --git a/src/main.cpp b/src/main.cpp index cc536d6..a4360dd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,7 +22,7 @@ int main(void) { MaskFilter maskFilter(mask); // create image window named "My Image" - cv::namedWindow("My Image"); + cv::namedWindow("My Image", cv::WINDOW_AUTOSIZE | cv::WINDOW_GUI_NORMAL); // show the image on window cv::imshow("My Image", maskFilter.apply(image)); From 56e6780a26219c6c2cf35c63f0ed5c647737e7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orb=C3=A1n=20Levente=20L=C3=A1szl=C3=B3?= Date: Sat, 8 Feb 2025 14:26:48 +0100 Subject: [PATCH 15/51] formatting --- src/img/tiling.hpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/img/tiling.hpp b/src/img/tiling.hpp index 3ec1795..54d06ee 100644 --- a/src/img/tiling.hpp +++ b/src/img/tiling.hpp @@ -16,8 +16,7 @@ namespace tiling { public: size_t width, height; - Rect2D(const ImgSource& img) - : image(img), width(img.getImg().cols), height(img.getImg().rows), amount_placed(0) {} + Rect2D(const ImgSource& img) : image(img), width(img.getImg().cols), height(img.getImg().rows), amount_placed(0) {} void rotate(); @@ -34,13 +33,11 @@ namespace tiling { }; namespace { - void recursive_packing(std::vector rects, cv::Point current_point, - size_t remainging_width, size_t current_height); + void recursive_packing(std::vector rects, cv::Point current_point, size_t remainging_width, + size_t current_height); - std::pair> tile(std::vector rects, - const DocumentPreset& preset); + std::pair> tile(std::vector rects, const DocumentPreset& preset); } // namespace - std::vector generate(std::vector images, - const DocumentPreset& preset); + std::vector generate(std::vector images, const DocumentPreset& preset); }; // namespace tiling From 7dbc070e80d691d62fd3993e5de85865e629e37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Sun, 16 Feb 2025 12:03:24 +0100 Subject: [PATCH 16/51] cleanup and ponder --- src/img/tiling.cpp | 160 ++++++++++++++++++--------------------------- src/img/tiling.hpp | 37 ++++------- 2 files changed, 75 insertions(+), 122 deletions(-) diff --git a/src/img/tiling.cpp b/src/img/tiling.cpp index 267c3a0..f481d0e 100644 --- a/src/img/tiling.cpp +++ b/src/img/tiling.cpp @@ -4,9 +4,7 @@ #include #include -/** - * @brief Based on the following paper (D. Zhang et al.): 10.1016/j.ipl.2015.08.008 - */ + namespace tiling { void Rect2D::rotate() { size_t temp = width; @@ -15,118 +13,84 @@ namespace tiling { rotated = !rotated; } - namespace { - /** - * @brief Recursive base case for `recursive_packing` function. Let `S` denote the area we - * are tiling. - * - * @param rects The rectangles we are using for tiling - * @param remainging_width Width of S - * @param current_height Height of S - * @return true If we have no more tiles to place, or S can't fit any that we can place. - * @return false If we have even a single tile that can be placed and fits into S. - */ - bool recursive_base_case(std::vector rects, size_t remainging_width, - size_t current_height) {} - - void recursive_packing(std::vector rects, cv::Point current_point, - size_t remainging_width, size_t current_height) { - if (recursive_base_case(rects, remainging_width, current_height)) return; - - std::vector unplaced; - std::ranges::copy_if(rects, std::back_inserter(unplaced), - [](Rect2D rect) { return rect.moreRemaining(); }); - - - } - - /** - * @brief NOTE: As per D. Zhang et al., the sorting of the rectangles can have a very big - * effect on packing efficiency. On average, for our use case sorting by width performs - * best, followed by area, perimeter, and finally height. - * - * Since there are so many heuristics which this function could take into consideration, it - * is the duty of the caller to provide the sorting with which this function is to tile the - * canvas. - * - * @param rects The rectangles to tile - * @param preset Configurable document data - * @return std::pair> The total height of the packing, and a vector - * of rectangles which all have a position on the plane. - */ - std::pair> tile(std::vector rects, - const DocumentPreset& preset) { - size_t height_of_tiling = 0; - cv::Point current_point = cv::Point(0, 0); - - size_t remaining_width, current_height; + /** + * @brief Recursive base case for `recursive_packing` function. Let `S` denote the area we + * are tiling. + * + * @param rects The rectangles we are using for tiling + * @param s_width Width of S + * @param s_height Height of S + * @return true If we have no more tiles to place, or S can't fit any that we can place. + * @return false If we have even a single tile that can be placed and fits into S. + */ + bool recursive_base_case(std::vector rects, size_t s_width, size_t s_height) {} + + void recursive_packing(std::vector rects, cv::Point current_point, size_t s_width, size_t s_height) { + recursive_base_case(rects, s_width, s_height); + } - for (auto&& rect : rects) { - if (!rect.moreRemaining()) continue; + /** + * @param rects The rectangles to tile + * @param preset Configurable document data + * @return std::pair> The total height of the packing, and a vector + * of rectangles which all have a position on the plane. + */ + std::pair> tile(std::vector rects, const DocumentPreset& preset) { + size_t height_of_tiling = 0; + cv::Point current_point = cv::Point(0, 0); - current_point.y = height_of_tiling; + size_t remaining_width, current_height; - rect.place(current_point); + for (auto&& rect : rects) { + current_point.y = height_of_tiling; - if (rect.height <= preset.document_width) rect.rotate(); + rect.primary_corner = current_point; - current_point.x = rect.width; - remaining_width = preset.document_width - rect.width; - current_height = rect.height; + if (rect.height <= preset.document_width) rect.rotate(); - height_of_tiling += current_height; + current_point.x = rect.width; + remaining_width = preset.document_width - rect.width; + current_height = rect.height; - recursive_packing(rects, current_point, remaining_width, current_height); + height_of_tiling += current_height; - current_point.x = 0; - } + recursive_packing(rects, current_point, remaining_width, current_height); - /** - * (4) If unpacked items remain, let B = B_{1} go to (2); otherwise stop. - */ - for (auto&& rect : rects) { - if (rect.moreRemaining()) { - /** - * TODO: Figure this out. - */ - } - } + current_point.x = 0; + } - return std::make_pair(height_of_tiling, rects); - } // namespace + return std::make_pair(height_of_tiling, rects); + } - cv::Mat generate(std::vector images, const DocumentPreset& preset) { - /** - * Create `Rect2D`'s from images - */ - std::vector rects = {}; - std::for_each(images.begin(), images.end(), - [&](const ImgSource& img) { rects.push_back(Rect2D(img)); }); + cv::Mat generate(std::vector images, const DocumentPreset& preset) { + /** + * Create `Rect2D`'s from images + */ + std::vector rects = {}; + std::for_each(images.begin(), images.end(), [&](const ImgSource& img) { rects.push_back(Rect2D(img)); }); - /** - * Gutter stuff here - */ + /** + * Gutter stuff here + */ - /** - * TODO: Expand heuristics for total area and perimeter - * (and maybe even diagonal length). - */ - std::pair> width_sorting = tile(rects, preset); - std::vector optimal_tiling = width_sorting.second; + /** + * TODO: Expand heuristics for total area + */ + std::pair> width_sorting = tile(rects, preset); + std::vector optimal_tiling = width_sorting.second; - cv::Mat canvas = cv::Mat::zeros(width_sorting.first, preset.document_width, CV_8UC4); + cv::Mat canvas = cv::Mat::zeros(width_sorting.first, preset.document_width, CV_8UC4); - for (auto&& rect : optimal_tiling) { - cv::Mat image = rect.getImg(); + for (auto&& rect : optimal_tiling) { + cv::Mat image = rect.image.getImg(); - if (rect.isRotated()) cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); + if (rect.rotated) cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); - cv::Point dstCoord = rect.getPrimaryCorner(); - cv::Rect dstRect = cv::Rect(dstCoord.x, dstCoord.y, rect.width, rect.height); + cv::Point dstCoord = rect.primary_corner; + cv::Rect dstRect = cv::Rect(dstCoord.x, dstCoord.y, rect.width, rect.height); - image.copyTo(canvas(dstRect)); - } - return canvas; + image.copyTo(canvas(dstRect)); } - } // namespace + return canvas; + } }; // namespace tiling diff --git a/src/img/tiling.hpp b/src/img/tiling.hpp index 54d06ee..b0b9ead 100644 --- a/src/img/tiling.hpp +++ b/src/img/tiling.hpp @@ -5,39 +5,28 @@ namespace tiling { class Rect2D { - const ImgSource& image; - /** - * TODO: Rewrite some parts of tiling.cpp because `primary_corner` changed - * from a cv::Point to a vector of points - */ - std::vector> primary_corner; - size_t amount_placed; - public: - size_t width, height; - - Rect2D(const ImgSource& img) : image(img), width(img.getImg().cols), height(img.getImg().rows), amount_placed(0) {} + const ImgSource& image; - void rotate(); + cv::Point primary_corner; - size_t getSize() const { return width * height; } + size_t width, height; - cv::Mat getImg() const { return image.getImg(); } + bool rotated = false; - bool moreRemaining() const { return amount_placed < image.getAmount(); } + Rect2D(const ImgSource& img) : image(img), width(img.getImg().cols), height(img.getImg().rows) {} - void place(const cv::Point& point, bool rotated) { - primary_corner.push_back(std::make_pair(rotated, point)); - amount_placed++; + void rotate() { + std::swap(width, height); + rotated = !rotated; } + + size_t getArea() const { return width * height; } }; - namespace { - void recursive_packing(std::vector rects, cv::Point current_point, size_t remainging_width, - size_t current_height); + void recursive_packing(std::vector rects, cv::Point current_point, size_t remainging_width, size_t current_height); - std::pair> tile(std::vector rects, const DocumentPreset& preset); - } // namespace + std::pair> tile(std::vector rects, const DocumentPreset& preset); - std::vector generate(std::vector images, const DocumentPreset& preset); + cv::Mat generate(std::vector images, const DocumentPreset& preset); }; // namespace tiling From c903214f50ee5ac7d3d6253b90a1d995eae9be0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Sun, 16 Feb 2025 15:30:53 +0100 Subject: [PATCH 17/51] it doesnt even build --- meson.build | 7 +-- src/img/source.cpp | 3 +- src/img/source.hpp | 5 +- src/img/tiling.hpp | 32 ----------- src/img/tiling/grid_tiling.cpp | 0 src/img/tiling/grid_tiling.hpp | 64 +++++++++++++++++++++ src/img/{tiling.cpp => tiling/strip_rg.cpp} | 30 +++++----- src/img/tiling/strip_rg.hpp | 13 +++++ src/img/tiling/tiling.hpp | 28 +++++++++ src/main.cpp | 13 +++-- src/settings/document_presets.cpp | 8 --- src/settings/document_presets.hpp | 15 ++--- 12 files changed, 139 insertions(+), 79 deletions(-) delete mode 100644 src/img/tiling.hpp create mode 100644 src/img/tiling/grid_tiling.cpp create mode 100644 src/img/tiling/grid_tiling.hpp rename src/img/{tiling.cpp => tiling/strip_rg.cpp} (69%) create mode 100644 src/img/tiling/strip_rg.hpp create mode 100644 src/img/tiling/tiling.hpp delete mode 100644 src/settings/document_presets.cpp diff --git a/meson.build b/meson.build index cd297d0..5257460 100644 --- a/meson.build +++ b/meson.build @@ -19,12 +19,11 @@ srcs = files( 'src/img/filters/size.cpp', 'src/img/source.hpp', 'src/img/source.cpp', - # 'src/img/tiling.cpp', - # 'src/settings/document_presets.hpp', - # 'src/settings/document_presets.cpp', - # 'src/settings/source_presets.cpp', + 'src/img/tiling/tiling.hpp', + 'src/img/tiling/grid_tiling.hpp', 'src/util/convert.cpp', 'src/main.cpp', + 'src/settings/document_presets.hpp', ) # Executable diff --git a/src/img/source.cpp b/src/img/source.cpp index 6c022c2..a14f1a7 100644 --- a/src/img/source.cpp +++ b/src/img/source.cpp @@ -1,7 +1,6 @@ #include "source.hpp" -ImgSource::ImgSource(const cv::Mat &image, const size_t &amount, const FilterStore &fil) - : file_data(image), quantity(amount), filters(fil) {} +ImgSource::ImgSource(const cv::Mat &image, const FilterStore &fil) : file_data(image), filters(fil) {} cv::Mat ImgSource::getImg() const { return file_data; } diff --git a/src/img/source.hpp b/src/img/source.hpp index c34564f..1193f17 100644 --- a/src/img/source.hpp +++ b/src/img/source.hpp @@ -9,12 +9,11 @@ class ImgSource { /** * NOTE: maybe should store cv::Mat instead of img_path */ - std::string img_path; - size_t amount; + cv::Mat file_data; FilterStore filters; public: - ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& fil); + ImgSource(const cv::Mat& image, const FilterStore& fil); cv::Mat getImg() const; diff --git a/src/img/tiling.hpp b/src/img/tiling.hpp deleted file mode 100644 index b0b9ead..0000000 --- a/src/img/tiling.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "settings/document_presets.hpp" -#include "source.hpp" - -namespace tiling { - class Rect2D { - public: - const ImgSource& image; - - cv::Point primary_corner; - - size_t width, height; - - bool rotated = false; - - Rect2D(const ImgSource& img) : image(img), width(img.getImg().cols), height(img.getImg().rows) {} - - void rotate() { - std::swap(width, height); - rotated = !rotated; - } - - size_t getArea() const { return width * height; } - }; - - void recursive_packing(std::vector rects, cv::Point current_point, size_t remainging_width, size_t current_height); - - std::pair> tile(std::vector rects, const DocumentPreset& preset); - - cv::Mat generate(std::vector images, const DocumentPreset& preset); -}; // namespace tiling diff --git a/src/img/tiling/grid_tiling.cpp b/src/img/tiling/grid_tiling.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/img/tiling/grid_tiling.hpp b/src/img/tiling/grid_tiling.hpp new file mode 100644 index 0000000..24157d9 --- /dev/null +++ b/src/img/tiling/grid_tiling.hpp @@ -0,0 +1,64 @@ +#pragma once +#include "tiling.hpp" + +class GridTiling : public Tiling { + // calculates the wasted area on the sides of the document + size_t calc_waste(size_t document_width, size_t tile_width, size_t tile_height, size_t amount) { + size_t columns = std::floor(document_width / tile_width); + + // the amount can be less than the number of columns + return (document_width - tile_width * std::min(amount, columns)) * tile_height; + } + + public: + cv::Mat generate(const DocumentPreset& preset, const std::vector& images) override { + std::vector tiles = {}; + std::for_each(images.begin(), images.end(), [&](const ImgSource& img) { tiles.push_back(Tile(img)); }); + // TODO + // set the size of every image to match the first one + // add gutter filter with guide parameter if the image doesnt already have one + size_t document_width = preset.document_width_px; // and take the gutter into account + + size_t tile_width = tiles[0].width; + size_t tile_height = tiles[0].height; + size_t quantity = tiles.size(); + + bool rotate = false; + // fits both ways + if (tile_width <= document_width && tile_height <= document_width) { + // check which way causes less waste + size_t waste_portrait = calc_waste(document_width, tile_width, tile_height, quantity); + size_t waste_landscape = calc_waste(document_width, tile_height, tile_width, quantity); + rotate = waste_landscape < waste_portrait; + } else if (tile_width <= document_width) { + rotate = false; + } else if (tile_height <= document_width) { + rotate = true; + } else { + // neither fits + // TODO: handle this + return cv::Mat(); + } + + if (rotate) { + std::for_each(tiles.begin(), tiles.end(), [](Tile& tile) { tile.rotate(); }); + std::swap(tile_width, tile_height); + } + + size_t columns = std::floor(document_width / tile_width); + size_t rows = std::ceil(quantity / columns); + size_t document_height = rows * tile_height; + + cv::Mat document = cv::Mat::zeros(document_height, document_width, CV_8UC3); + + // TODO: corrected quantity + + for (size_t i = 0; i < quantity; i++) { + Tile& tile = tiles[i]; + cv::Rect target_rect = cv::Rect((i % columns) * tile_width, (i / columns) * tile_height, tile_width, tile_height); + tile.image.getImg().copyTo(document(target_rect)); + } + + return document; + } +}; \ No newline at end of file diff --git a/src/img/tiling.cpp b/src/img/tiling/strip_rg.cpp similarity index 69% rename from src/img/tiling.cpp rename to src/img/tiling/strip_rg.cpp index f481d0e..9d3e1e5 100644 --- a/src/img/tiling.cpp +++ b/src/img/tiling/strip_rg.cpp @@ -6,7 +6,7 @@ namespace tiling { - void Rect2D::rotate() { + void Tile::rotate() { size_t temp = width; width = height; height = temp; @@ -23,19 +23,19 @@ namespace tiling { * @return true If we have no more tiles to place, or S can't fit any that we can place. * @return false If we have even a single tile that can be placed and fits into S. */ - bool recursive_base_case(std::vector rects, size_t s_width, size_t s_height) {} + bool recursive_base_case(std::vector rects, size_t s_width, size_t s_height) {} - void recursive_packing(std::vector rects, cv::Point current_point, size_t s_width, size_t s_height) { + void recursive_packing(std::vector rects, cv::Point current_point, size_t s_width, size_t s_height) { recursive_base_case(rects, s_width, s_height); } /** * @param rects The rectangles to tile * @param preset Configurable document data - * @return std::pair> The total height of the packing, and a vector + * @return std::pair> The total height of the packing, and a vector * of rectangles which all have a position on the plane. */ - std::pair> tile(std::vector rects, const DocumentPreset& preset) { + std::pair> tile(std::vector rects, const DocumentPreset& preset) { size_t height_of_tiling = 0; cv::Point current_point = cv::Point(0, 0); @@ -44,12 +44,12 @@ namespace tiling { for (auto&& rect : rects) { current_point.y = height_of_tiling; - rect.primary_corner = current_point; + rect.corner = current_point; - if (rect.height <= preset.document_width) rect.rotate(); + if (rect.height <= preset.document_width_px) rect.rotate(); current_point.x = rect.width; - remaining_width = preset.document_width - rect.width; + remaining_width = preset.document_width_px - rect.width; current_height = rect.height; height_of_tiling += current_height; @@ -64,10 +64,10 @@ namespace tiling { cv::Mat generate(std::vector images, const DocumentPreset& preset) { /** - * Create `Rect2D`'s from images + * Create `Tile`'s from images */ - std::vector rects = {}; - std::for_each(images.begin(), images.end(), [&](const ImgSource& img) { rects.push_back(Rect2D(img)); }); + std::vector rects = {}; + std::for_each(images.begin(), images.end(), [&](const ImgSource& img) { rects.push_back(Tile(img)); }); /** * Gutter stuff here @@ -76,17 +76,17 @@ namespace tiling { /** * TODO: Expand heuristics for total area */ - std::pair> width_sorting = tile(rects, preset); - std::vector optimal_tiling = width_sorting.second; + std::pair> width_sorting = tile(rects, preset); + std::vector optimal_tiling = width_sorting.second; - cv::Mat canvas = cv::Mat::zeros(width_sorting.first, preset.document_width, CV_8UC4); + cv::Mat canvas = cv::Mat::zeros(width_sorting.first, preset.document_width_px, CV_8UC4); for (auto&& rect : optimal_tiling) { cv::Mat image = rect.image.getImg(); if (rect.rotated) cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); - cv::Point dstCoord = rect.primary_corner; + cv::Point dstCoord = rect.corner; cv::Rect dstRect = cv::Rect(dstCoord.x, dstCoord.y, rect.width, rect.height); image.copyTo(canvas(dstRect)); diff --git a/src/img/tiling/strip_rg.hpp b/src/img/tiling/strip_rg.hpp new file mode 100644 index 0000000..59cd301 --- /dev/null +++ b/src/img/tiling/strip_rg.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "settings/document_presets.hpp" +#include "source.hpp" + +namespace tiling { + + void recursive_packing(std::vector rects, cv::Point current_point, size_t remainging_width, size_t current_height); + + std::pair> tile(std::vector rects, const DocumentPreset& preset); + + cv::Mat generate(std::vector images, const DocumentPreset& preset); +}; // namespace tiling diff --git a/src/img/tiling/tiling.hpp b/src/img/tiling/tiling.hpp new file mode 100644 index 0000000..22e63a5 --- /dev/null +++ b/src/img/tiling/tiling.hpp @@ -0,0 +1,28 @@ +#pragma once +#include "document_presets.hpp" +#include "source.hpp" + +class Tiling { + public: + virtual cv::Mat generate(const DocumentPreset &preset, const std::vector &images) = 0; +}; + +class Tile { + public: + const ImgSource& image; + + cv::Point corner; + + size_t width, height; + + bool rotated = false; + + Tile(const ImgSource& img) : image(img), width(img.getImg().cols), height(img.getImg().rows) {} + + void rotate() { + std::swap(width, height); + rotated = !rotated; + } + + size_t getArea() const { return width * height; } +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index a4360dd..34d8e7f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,23 +9,24 @@ #include #include "img/filters/mask.hpp" +#include "img/tiling/grid_tiling.hpp" -using json = nlohmann::json; +//using json = nlohmann::json; int main(void) { - // read an image cv::Mat image = cv::imread("./assets/3.png", 1); - cv::Mat mask = cv::imread("./assets/mask.png", 1); - - MaskFilter maskFilter(mask); + GridTiling tiling = GridTiling(); + DocumentPreset preset = {1000, 10, true, true}; + std::vector images = {ImgSource(image, FilterStore())}; + cv::Mat result = tiling.generate(preset, images); // create image window named "My Image" cv::namedWindow("My Image", cv::WINDOW_AUTOSIZE | cv::WINDOW_GUI_NORMAL); // show the image on window - cv::imshow("My Image", maskFilter.apply(image)); + cv::imshow("My Image", result); // aboszolút filmszínház cv::waitKey(0); diff --git a/src/settings/document_presets.cpp b/src/settings/document_presets.cpp deleted file mode 100644 index aea9845..0000000 --- a/src/settings/document_presets.cpp +++ /dev/null @@ -1,8 +0,0 @@ -/* -width -gutter -margin -ppi -guide -min max height -*/ diff --git a/src/settings/document_presets.hpp b/src/settings/document_presets.hpp index fefc0a0..af083f4 100644 --- a/src/settings/document_presets.hpp +++ b/src/settings/document_presets.hpp @@ -1,18 +1,15 @@ #pragma once -#include - /* -gutter -margin +TODO: ppi -guide +roll witdth - margin * 2 = document width min max height */ struct DocumentPreset { - size_t document_width; - size_t gutter_width; - size_t tile_count; - bool fill; + size_t document_width_px; + size_t gutter_width_px; + bool correct_quantity; + bool guide; }; From efa61b55ed841d8efe519edcc2f2b9752aa93f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Sun, 16 Feb 2025 18:41:00 +0100 Subject: [PATCH 18/51] bruh --- meson.build | 21 ++++---- src/img/filters/filter_store.cpp | 8 ++- src/img/filters/filter_store.hpp | 4 +- src/img/filters/rotate.hpp | 0 src/img/source.cpp | 6 +-- src/img/source.hpp | 16 +++--- src/img/tiling/grid_tiling.cpp | 84 +++++++++++++++++++++++++++++++ src/img/tiling/grid_tiling.hpp | 58 +-------------------- src/img/tiling/strip_rg.cpp | 2 +- src/img/tiling/tiling.hpp | 6 +-- src/main.cpp | 12 ++--- src/settings/document_presets.hpp | 1 + 12 files changed, 128 insertions(+), 90 deletions(-) create mode 100644 src/img/filters/rotate.hpp diff --git a/meson.build b/meson.build index 5257460..58da713 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('printf', 'cpp', - default_options: ['cpp_std=c++17'] + default_options: ['cpp_std=c++23'] ) # Dependencies @@ -8,25 +8,28 @@ opencv = dependency('opencv4', required: true) qt6_dep = dependency('qt6', modules: ['Core']) +incdir = include_directories( + 'src', + 'src/img', + 'src/util', + 'src/img/filters', + 'src/img/tiling', + 'src/settings' +) + # Source files srcs = files( - 'src/img/filters/filter.hpp', - 'src/img/filters/filter_store.hpp', 'src/img/filters/filter_store.cpp', - 'src/img/filters/mask.hpp', 'src/img/filters/mask.cpp', - 'src/img/filters/size.hpp', 'src/img/filters/size.cpp', - 'src/img/source.hpp', 'src/img/source.cpp', - 'src/img/tiling/tiling.hpp', - 'src/img/tiling/grid_tiling.hpp', 'src/util/convert.cpp', + 'src/img/tiling/grid_tiling.cpp', 'src/main.cpp', - 'src/settings/document_presets.hpp', ) # Executable executable('printf', srcs, + include_directories : incdir, dependencies: [nlohmann_json, opencv, qt6_dep] ) diff --git a/src/img/filters/filter_store.cpp b/src/img/filters/filter_store.cpp index 268cfd2..ce3085b 100644 --- a/src/img/filters/filter_store.cpp +++ b/src/img/filters/filter_store.cpp @@ -2,16 +2,22 @@ void FilterStore::addFilter(Filter *filter) { filters.push_back(filter); } -cv::Mat FilterStore::applyAll(const cv::Mat &image) const { +cv::Mat FilterStore::applyAll(const cv::Mat &image) { cv::Mat result = image; for (auto filter : filters) { result = filter->apply(result); } + clear_filters(); return result; } FilterStore::~FilterStore() { + clear_filters(); +} + +void FilterStore::clear_filters() { for (auto filter : filters) { delete filter; } + filters.clear(); } diff --git a/src/img/filters/filter_store.hpp b/src/img/filters/filter_store.hpp index 76c2163..e05b7f5 100644 --- a/src/img/filters/filter_store.hpp +++ b/src/img/filters/filter_store.hpp @@ -11,7 +11,9 @@ class FilterStore { public: void addFilter(Filter *filter); - cv::Mat applyAll(const cv::Mat &image) const; + cv::Mat applyAll(const cv::Mat &image); ~FilterStore(); + + void clear_filters(); }; diff --git a/src/img/filters/rotate.hpp b/src/img/filters/rotate.hpp new file mode 100644 index 0000000..e69de29 diff --git a/src/img/source.cpp b/src/img/source.cpp index a14f1a7..02b75ed 100644 --- a/src/img/source.cpp +++ b/src/img/source.cpp @@ -1,7 +1,5 @@ #include "source.hpp" -ImgSource::ImgSource(const cv::Mat &image, const FilterStore &fil) : file_data(image), filters(fil) {} +ImgSource::ImgSource(const cv::Mat &image, const size_t& amount, const FilterStore &fil) : data(image), amount(amount), filters(fil) {} -cv::Mat ImgSource::getImg() const { return file_data; } - -cv::Mat ImgSource::applyFilters() const { return filters.applyAll(file_data); } +cv::Mat ImgSource::apply_filters() { return filters.applyAll(data); } diff --git a/src/img/source.hpp b/src/img/source.hpp index 1193f17..0f28857 100644 --- a/src/img/source.hpp +++ b/src/img/source.hpp @@ -6,16 +6,16 @@ #include "filters/filter_store.hpp" class ImgSource { - /** - * NOTE: maybe should store cv::Mat instead of img_path - */ - cv::Mat file_data; + public: + cv::Mat data; FilterStore filters; + size_t amount; - public: - ImgSource(const cv::Mat& image, const FilterStore& fil); + ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& fil); + + size_t get_width() const { return data.cols; } - cv::Mat getImg() const; + size_t get_height() const { return data.rows; } - cv::Mat applyFilters() const; + cv::Mat apply_filters(); }; diff --git a/src/img/tiling/grid_tiling.cpp b/src/img/tiling/grid_tiling.cpp index e69de29..52eb1ae 100644 --- a/src/img/tiling/grid_tiling.cpp +++ b/src/img/tiling/grid_tiling.cpp @@ -0,0 +1,84 @@ +#include "grid_tiling.hpp" +#include + +size_t GridTiling::calc_waste(size_t document_width, size_t tile_width, size_t tile_height, size_t amount) { + size_t columns = std::floor(document_width / tile_width); + + // the amount can be less than the number of columns + return (document_width - tile_width * std::min(amount, columns)) * tile_height; +} + +cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector images) { + size_t document_width = preset.document_width_px; // and take the gutter into account + + size_t tile_width = images[0].get_width(); + size_t tile_height = images[0].get_height(); + + size_t quantity = std::accumulate(images.begin(), images.end(), 0, [](size_t sum, const ImgSource& img) { + return sum + img.amount; + }); + + + + bool rotate = false; + // fits both ways + if (tile_width <= document_width && tile_height <= document_width) { + // check which way causes less waste + size_t waste_portrait = calc_waste(document_width, tile_width, tile_height, quantity); + size_t waste_landscape = calc_waste(document_width, tile_height, tile_width, quantity); + rotate = waste_landscape < waste_portrait; + } else if (tile_width <= document_width) { + rotate = false; + } else if (tile_height <= document_width) { + rotate = true; + } else { + // neither fits + // TODO: handle this + return cv::Mat(); + } + + if (rotate) { + std::swap(tile_width, tile_height); + } + + size_t columns = std::floor(document_width / tile_width); + size_t rows = std::ceil((double)quantity / columns); + size_t document_height = rows * tile_height; + + cv::Mat document = cv::Mat::zeros(document_height, document_width, CV_8UC3); + + std::vector tiles = {}; + + + for (ImgSource& img : images) { + if (rotate) { + cv::Mat rotated; + cv::rotate(img.data, img.data, cv::ROTATE_90_CLOCKWISE); + } + // TODO + // set the size of every image to match the first one + // add gutter filter with guide parameter if the image doesnt already have one + + for (size_t i = 0; i < img.amount; i++) { + tiles.push_back(Tile(img)); + } + } + + + + // TODO: corrected quantity + + for (size_t i = 0; i < quantity; i++) { + Tile& tile = tiles[i]; + cv::Rect target_rect = cv::Rect((i % columns) * tile_width, (i / columns) * tile_height, tile_width, tile_height); + + if (tile.rotated) { + cv::Mat rotated; + cv::rotate(tile.image.data, rotated, cv::ROTATE_90_CLOCKWISE); + rotated.copyTo(document(target_rect)); + } else + tile.image.data.copyTo(document(target_rect)); + } + + return document; +} \ No newline at end of file diff --git a/src/img/tiling/grid_tiling.hpp b/src/img/tiling/grid_tiling.hpp index 24157d9..b397ff4 100644 --- a/src/img/tiling/grid_tiling.hpp +++ b/src/img/tiling/grid_tiling.hpp @@ -3,62 +3,8 @@ class GridTiling : public Tiling { // calculates the wasted area on the sides of the document - size_t calc_waste(size_t document_width, size_t tile_width, size_t tile_height, size_t amount) { - size_t columns = std::floor(document_width / tile_width); - - // the amount can be less than the number of columns - return (document_width - tile_width * std::min(amount, columns)) * tile_height; - } + size_t calc_waste(size_t document_width, size_t tile_width, size_t tile_height, size_t amount); public: - cv::Mat generate(const DocumentPreset& preset, const std::vector& images) override { - std::vector tiles = {}; - std::for_each(images.begin(), images.end(), [&](const ImgSource& img) { tiles.push_back(Tile(img)); }); - // TODO - // set the size of every image to match the first one - // add gutter filter with guide parameter if the image doesnt already have one - size_t document_width = preset.document_width_px; // and take the gutter into account - - size_t tile_width = tiles[0].width; - size_t tile_height = tiles[0].height; - size_t quantity = tiles.size(); - - bool rotate = false; - // fits both ways - if (tile_width <= document_width && tile_height <= document_width) { - // check which way causes less waste - size_t waste_portrait = calc_waste(document_width, tile_width, tile_height, quantity); - size_t waste_landscape = calc_waste(document_width, tile_height, tile_width, quantity); - rotate = waste_landscape < waste_portrait; - } else if (tile_width <= document_width) { - rotate = false; - } else if (tile_height <= document_width) { - rotate = true; - } else { - // neither fits - // TODO: handle this - return cv::Mat(); - } - - if (rotate) { - std::for_each(tiles.begin(), tiles.end(), [](Tile& tile) { tile.rotate(); }); - std::swap(tile_width, tile_height); - } - - size_t columns = std::floor(document_width / tile_width); - size_t rows = std::ceil(quantity / columns); - size_t document_height = rows * tile_height; - - cv::Mat document = cv::Mat::zeros(document_height, document_width, CV_8UC3); - - // TODO: corrected quantity - - for (size_t i = 0; i < quantity; i++) { - Tile& tile = tiles[i]; - cv::Rect target_rect = cv::Rect((i % columns) * tile_width, (i / columns) * tile_height, tile_width, tile_height); - tile.image.getImg().copyTo(document(target_rect)); - } - - return document; - } + cv::Mat generate(const DocumentPreset& preset, std::vector images) override; }; \ No newline at end of file diff --git a/src/img/tiling/strip_rg.cpp b/src/img/tiling/strip_rg.cpp index 9d3e1e5..9b80aee 100644 --- a/src/img/tiling/strip_rg.cpp +++ b/src/img/tiling/strip_rg.cpp @@ -82,7 +82,7 @@ namespace tiling { cv::Mat canvas = cv::Mat::zeros(width_sorting.first, preset.document_width_px, CV_8UC4); for (auto&& rect : optimal_tiling) { - cv::Mat image = rect.image.getImg(); + cv::Mat image = rect.image.get_img(); if (rect.rotated) cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); diff --git a/src/img/tiling/tiling.hpp b/src/img/tiling/tiling.hpp index 22e63a5..7bfe329 100644 --- a/src/img/tiling/tiling.hpp +++ b/src/img/tiling/tiling.hpp @@ -4,12 +4,12 @@ class Tiling { public: - virtual cv::Mat generate(const DocumentPreset &preset, const std::vector &images) = 0; + virtual cv::Mat generate(const DocumentPreset &preset, std::vector images) = 0; }; class Tile { public: - const ImgSource& image; + ImgSource& image; cv::Point corner; @@ -17,7 +17,7 @@ class Tile { bool rotated = false; - Tile(const ImgSource& img) : image(img), width(img.getImg().cols), height(img.getImg().rows) {} + Tile(ImgSource& img) : image(img), width(img.get_width()), height(img.get_height()) {} void rotate() { std::swap(width, height); diff --git a/src/main.cpp b/src/main.cpp index 34d8e7f..6fca959 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,22 +4,20 @@ #include #include #include - #include #include -#include "img/filters/mask.hpp" -#include "img/tiling/grid_tiling.hpp" - -//using json = nlohmann::json; +#include "mask.hpp" +#include "grid_tiling.hpp" +// using json = nlohmann::json; int main(void) { cv::Mat image = cv::imread("./assets/3.png", 1); GridTiling tiling = GridTiling(); - DocumentPreset preset = {1000, 10, true, true}; - std::vector images = {ImgSource(image, FilterStore())}; + DocumentPreset preset = {1500, 10, true, true}; + std::vector images = {ImgSource(image, 5, FilterStore())}; cv::Mat result = tiling.generate(preset, images); // create image window named "My Image" diff --git a/src/settings/document_presets.hpp b/src/settings/document_presets.hpp index af083f4..842e099 100644 --- a/src/settings/document_presets.hpp +++ b/src/settings/document_presets.hpp @@ -1,4 +1,5 @@ #pragma once +#include /* TODO: From 41bf7c499443e15cc2ce8d66ce8ccc27adccee9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Mon, 17 Feb 2025 10:58:39 +0100 Subject: [PATCH 19/51] uml --- .gitignore | 3 +++ .vscode/c_cpp_properties.json | 4 ++-- uml/img_source.puml | 36 +++++++++++++++++++++++++++++++++++ uml/presets.puml | 26 +++++++++++++++++++++++++ uml/tiling.puml | 24 +++++++++++++++++++++++ 5 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 uml/img_source.puml create mode 100644 uml/presets.puml create mode 100644 uml/tiling.puml diff --git a/.gitignore b/.gitignore index 03c31d4..ab9d886 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ output/ *.exe *.out *.app + +# vscode +.vscode/settings.json \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 0ac5d5c..688994a 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -8,8 +8,8 @@ ], "defines": [], "compilerPath": "/usr/bin/gcc", - "cStandard": "c17", - "cppStandard": "c++17", + "cStandard": "c23", + "cppStandard": "c++23", "intelliSenseMode": "gcc-x64" } ], diff --git a/uml/img_source.puml b/uml/img_source.puml new file mode 100644 index 0000000..db406c1 --- /dev/null +++ b/uml/img_source.puml @@ -0,0 +1,36 @@ +@startuml img_source + +title Kép forrás + +class ImgSource { + cv::Mat original + cv::Mat cached + size_t amount + std::vector filters + + bool rotated + size_t width, height + + void add_filter(Filter filter) + void clear_filters() + cv::Mat get_img() +} + +abstract Filter { + cv::Mat apply(cv::Mat &img) +} +ImgSource *-- Filter + + +class Mask { + cv::Mat mask + cv::Mat apply(cv::Mat &img) +} +Filter <|-- Mask + + + + + + +@enduml \ No newline at end of file diff --git a/uml/presets.puml b/uml/presets.puml new file mode 100644 index 0000000..b48f3d1 --- /dev/null +++ b/uml/presets.puml @@ -0,0 +1,26 @@ +@startuml presets + +title Beállítások + +class SourcePreset { + std::string name + size_t width, height + std::vector filters +} + +class DocumentPreset { + std::string name + double ppi + double roll_width_mm + double margin_mm + double gutter_mm + double min_height_mm + double max_height_mm + + size_t document_width_px + size_t gutter_px + bool guidelines +} + + +@enduml \ No newline at end of file diff --git a/uml/tiling.puml b/uml/tiling.puml new file mode 100644 index 0000000..70c1219 --- /dev/null +++ b/uml/tiling.puml @@ -0,0 +1,24 @@ +@startuml tiling + +title Csempézés + +abstract Tiling { + cv::Mat generate(DocumentPreset preset, std::vector images) +} + +class GridTiling { + cv::Mat generate(...) +} +Tiling <|-- GridTiling + +class StripTiling { + cv::Mat generate(...) +} +Tiling <|-- StripTiling + +class Tile { + ImgSource image + cv::Point corner +} + +@enduml \ No newline at end of file From 12c7f49b930099e7185c626551e256fa6137675e Mon Sep 17 00:00:00 2001 From: Gilgames Date: Mon, 24 Feb 2025 17:12:30 +0100 Subject: [PATCH 20/51] uml bump --- uml/img_source.puml | 8 +++++++- uml/tiling.puml | 5 ----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/uml/img_source.puml b/uml/img_source.puml index db406c1..2078be1 100644 --- a/uml/img_source.puml +++ b/uml/img_source.puml @@ -13,6 +13,7 @@ class ImgSource { void add_filter(Filter filter) void clear_filters() + void apply_filters() cv::Mat get_img() } @@ -29,7 +30,12 @@ class Mask { Filter <|-- Mask - +class Tile { + ImgSource image + cv::Point corner + bool rotated +} +ImgSource -* Tile diff --git a/uml/tiling.puml b/uml/tiling.puml index 70c1219..a8ace19 100644 --- a/uml/tiling.puml +++ b/uml/tiling.puml @@ -16,9 +16,4 @@ class StripTiling { } Tiling <|-- StripTiling -class Tile { - ImgSource image - cv::Point corner -} - @enduml \ No newline at end of file From 6110d8218689483b7f5e751a4b13429ba4c8ec85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Mon, 24 Feb 2025 18:30:01 +0100 Subject: [PATCH 21/51] cleanup --- meson.build | 11 ---- src/img/cached_image.hpp | 14 +++++ src/img/filters/filter_store.cpp | 23 -------- src/img/filters/filter_store.hpp | 19 ------- src/img/filters/mask.cpp | 7 +-- src/img/filters/rotate.hpp | 0 src/img/image_source.hpp | 34 +++++++++++ src/img/source.cpp | 5 -- src/img/source.hpp | 21 ------- src/img/tiling/grid_tiling.cpp | 24 ++++---- src/img/tiling/grid_tiling.hpp | 2 +- src/img/tiling/strip_rg.cpp | 96 -------------------------------- src/img/tiling/strip_rg.hpp | 13 ----- src/img/tiling/tile.hpp | 30 ++++++++++ src/img/tiling/tiling.hpp | 23 +------- src/main.cpp | 10 +--- src/settings/source_presets.cpp | 5 -- src/util/convert.cpp | 3 - 18 files changed, 94 insertions(+), 246 deletions(-) create mode 100644 src/img/cached_image.hpp delete mode 100644 src/img/filters/filter_store.cpp delete mode 100644 src/img/filters/filter_store.hpp delete mode 100644 src/img/filters/rotate.hpp create mode 100644 src/img/image_source.hpp delete mode 100644 src/img/source.cpp delete mode 100644 src/img/source.hpp delete mode 100644 src/img/tiling/strip_rg.cpp delete mode 100644 src/img/tiling/strip_rg.hpp create mode 100644 src/img/tiling/tile.hpp delete mode 100644 src/settings/source_presets.cpp delete mode 100644 src/util/convert.cpp diff --git a/meson.build b/meson.build index 58da713..d1c4759 100644 --- a/meson.build +++ b/meson.build @@ -10,21 +10,10 @@ qt6_dep = dependency('qt6', modules: ['Core']) incdir = include_directories( 'src', - 'src/img', - 'src/util', - 'src/img/filters', - 'src/img/tiling', - 'src/settings' ) # Source files srcs = files( - 'src/img/filters/filter_store.cpp', - 'src/img/filters/mask.cpp', - 'src/img/filters/size.cpp', - 'src/img/source.cpp', - 'src/util/convert.cpp', - 'src/img/tiling/grid_tiling.cpp', 'src/main.cpp', ) diff --git a/src/img/cached_image.hpp b/src/img/cached_image.hpp new file mode 100644 index 0000000..3d74b96 --- /dev/null +++ b/src/img/cached_image.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +// TODO +class CachedImage { + private: + cv::Mat image; + bool isDirty; + + public: + CachedImage(); + ~CachedImage(); +}; diff --git a/src/img/filters/filter_store.cpp b/src/img/filters/filter_store.cpp deleted file mode 100644 index ce3085b..0000000 --- a/src/img/filters/filter_store.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "filter_store.hpp" - -void FilterStore::addFilter(Filter *filter) { filters.push_back(filter); } - -cv::Mat FilterStore::applyAll(const cv::Mat &image) { - cv::Mat result = image; - for (auto filter : filters) { - result = filter->apply(result); - } - clear_filters(); - return result; -} - -FilterStore::~FilterStore() { - clear_filters(); -} - -void FilterStore::clear_filters() { - for (auto filter : filters) { - delete filter; - } - filters.clear(); -} diff --git a/src/img/filters/filter_store.hpp b/src/img/filters/filter_store.hpp deleted file mode 100644 index e05b7f5..0000000 --- a/src/img/filters/filter_store.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include - -#include "filter.hpp" - -class FilterStore { - private: - std::vector filters; - - public: - void addFilter(Filter *filter); - - cv::Mat applyAll(const cv::Mat &image); - - ~FilterStore(); - - void clear_filters(); -}; diff --git a/src/img/filters/mask.cpp b/src/img/filters/mask.cpp index e934939..1bbe6c2 100644 --- a/src/img/filters/mask.cpp +++ b/src/img/filters/mask.cpp @@ -3,12 +3,7 @@ #include "size.hpp" MaskFilter::MaskFilter(const cv::Mat &mask) : mask(mask) {} -/** - * @brief CRAZY HAMBURGEf!!!! - * - * @param image - * @return cv::Mat - */ + cv::Mat MaskFilter::apply(const cv::Mat &image) { auto fitMask = SizeFilter::resize(mask, image.cols, image.rows); diff --git a/src/img/filters/rotate.hpp b/src/img/filters/rotate.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/img/image_source.hpp b/src/img/image_source.hpp new file mode 100644 index 0000000..19f2cfa --- /dev/null +++ b/src/img/image_source.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "filter.hpp" +#include "cached_image.hpp" + +class ImageSource { + private: + cv::Mat original; + CachedImage cached; + size_t amount; + std::vector filters; + bool rotated; + size_t width, height; + + public: + ImageSource(cv::Mat source); + + void add_filter(const Filter& filter); + + void clear_filters(); + + void apply_filters(); + + cv::Mat get_img() const; + + size_t get_width() const; + + size_t get_height() const; + + size_t get_amount() const; +}; diff --git a/src/img/source.cpp b/src/img/source.cpp deleted file mode 100644 index 02b75ed..0000000 --- a/src/img/source.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "source.hpp" - -ImgSource::ImgSource(const cv::Mat &image, const size_t& amount, const FilterStore &fil) : data(image), amount(amount), filters(fil) {} - -cv::Mat ImgSource::apply_filters() { return filters.applyAll(data); } diff --git a/src/img/source.hpp b/src/img/source.hpp deleted file mode 100644 index 0f28857..0000000 --- a/src/img/source.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - -#include "filters/filter_store.hpp" - -class ImgSource { - public: - cv::Mat data; - FilterStore filters; - size_t amount; - - ImgSource(const cv::Mat& image, const size_t& amount, const FilterStore& fil); - - size_t get_width() const { return data.cols; } - - size_t get_height() const { return data.rows; } - - cv::Mat apply_filters(); -}; diff --git a/src/img/tiling/grid_tiling.cpp b/src/img/tiling/grid_tiling.cpp index 52eb1ae..12514ef 100644 --- a/src/img/tiling/grid_tiling.cpp +++ b/src/img/tiling/grid_tiling.cpp @@ -1,4 +1,5 @@ #include "grid_tiling.hpp" +#include "tile.hpp" #include size_t GridTiling::calc_waste(size_t document_width, size_t tile_width, size_t tile_height, size_t amount) { @@ -8,14 +9,14 @@ size_t GridTiling::calc_waste(size_t document_width, size_t tile_width, size_t t return (document_width - tile_width * std::min(amount, columns)) * tile_height; } -cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector images) { +cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector images) { size_t document_width = preset.document_width_px; // and take the gutter into account size_t tile_width = images[0].get_width(); size_t tile_height = images[0].get_height(); - size_t quantity = std::accumulate(images.begin(), images.end(), 0, [](size_t sum, const ImgSource& img) { - return sum + img.amount; + size_t quantity = std::accumulate(images.begin(), images.end(), 0, [](size_t sum, const ImageSource& img) { + return sum + img.get_amount(); }); @@ -45,21 +46,23 @@ cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector tiles = {}; - for (ImgSource& img : images) { + for (ImageSource& img : images) { if (rotate) { cv::Mat rotated; - cv::rotate(img.data, img.data, cv::ROTATE_90_CLOCKWISE); + + // TODO: rotate filter + // cv::rotate(img.data, img.data, cv::ROTATE_90_CLOCKWISE); } // TODO // set the size of every image to match the first one // add gutter filter with guide parameter if the image doesnt already have one - for (size_t i = 0; i < img.amount; i++) { + for (size_t i = 0; i < img.get_amount(); i++) { tiles.push_back(Tile(img)); } } @@ -72,12 +75,7 @@ cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector images) override; + cv::Mat generate(const DocumentPreset& preset, std::vector images) override; }; \ No newline at end of file diff --git a/src/img/tiling/strip_rg.cpp b/src/img/tiling/strip_rg.cpp deleted file mode 100644 index 9b80aee..0000000 --- a/src/img/tiling/strip_rg.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "tiling.hpp" - -#include -#include -#include - - -namespace tiling { - void Tile::rotate() { - size_t temp = width; - width = height; - height = temp; - rotated = !rotated; - } - - /** - * @brief Recursive base case for `recursive_packing` function. Let `S` denote the area we - * are tiling. - * - * @param rects The rectangles we are using for tiling - * @param s_width Width of S - * @param s_height Height of S - * @return true If we have no more tiles to place, or S can't fit any that we can place. - * @return false If we have even a single tile that can be placed and fits into S. - */ - bool recursive_base_case(std::vector rects, size_t s_width, size_t s_height) {} - - void recursive_packing(std::vector rects, cv::Point current_point, size_t s_width, size_t s_height) { - recursive_base_case(rects, s_width, s_height); - } - - /** - * @param rects The rectangles to tile - * @param preset Configurable document data - * @return std::pair> The total height of the packing, and a vector - * of rectangles which all have a position on the plane. - */ - std::pair> tile(std::vector rects, const DocumentPreset& preset) { - size_t height_of_tiling = 0; - cv::Point current_point = cv::Point(0, 0); - - size_t remaining_width, current_height; - - for (auto&& rect : rects) { - current_point.y = height_of_tiling; - - rect.corner = current_point; - - if (rect.height <= preset.document_width_px) rect.rotate(); - - current_point.x = rect.width; - remaining_width = preset.document_width_px - rect.width; - current_height = rect.height; - - height_of_tiling += current_height; - - recursive_packing(rects, current_point, remaining_width, current_height); - - current_point.x = 0; - } - - return std::make_pair(height_of_tiling, rects); - } - - cv::Mat generate(std::vector images, const DocumentPreset& preset) { - /** - * Create `Tile`'s from images - */ - std::vector rects = {}; - std::for_each(images.begin(), images.end(), [&](const ImgSource& img) { rects.push_back(Tile(img)); }); - - /** - * Gutter stuff here - */ - - /** - * TODO: Expand heuristics for total area - */ - std::pair> width_sorting = tile(rects, preset); - std::vector optimal_tiling = width_sorting.second; - - cv::Mat canvas = cv::Mat::zeros(width_sorting.first, preset.document_width_px, CV_8UC4); - - for (auto&& rect : optimal_tiling) { - cv::Mat image = rect.image.get_img(); - - if (rect.rotated) cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); - - cv::Point dstCoord = rect.corner; - cv::Rect dstRect = cv::Rect(dstCoord.x, dstCoord.y, rect.width, rect.height); - - image.copyTo(canvas(dstRect)); - } - return canvas; - } -}; // namespace tiling diff --git a/src/img/tiling/strip_rg.hpp b/src/img/tiling/strip_rg.hpp deleted file mode 100644 index 59cd301..0000000 --- a/src/img/tiling/strip_rg.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include "settings/document_presets.hpp" -#include "source.hpp" - -namespace tiling { - - void recursive_packing(std::vector rects, cv::Point current_point, size_t remainging_width, size_t current_height); - - std::pair> tile(std::vector rects, const DocumentPreset& preset); - - cv::Mat generate(std::vector images, const DocumentPreset& preset); -}; // namespace tiling diff --git a/src/img/tiling/tile.hpp b/src/img/tiling/tile.hpp new file mode 100644 index 0000000..7430cf5 --- /dev/null +++ b/src/img/tiling/tile.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "image_source.hpp" + +class Tile { + private: + ImageSource& image; + size_t width, height; + + public: + cv::Point corner; + + + bool rotated = false; + + Tile(ImageSource& img) : image(img), width(img.get_width()), height(img.get_height()) {} + + void rotate() { + std::swap(width, height); + rotated = !rotated; + } + + size_t get_area() const { return width * height; } + + size_t get_width() const { return width; } + + size_t get_height() const { return height; } + + cv::Mat get_image() { return image.get_img(); } +}; diff --git a/src/img/tiling/tiling.hpp b/src/img/tiling/tiling.hpp index 7bfe329..c5e23f1 100644 --- a/src/img/tiling/tiling.hpp +++ b/src/img/tiling/tiling.hpp @@ -1,28 +1,9 @@ #pragma once #include "document_presets.hpp" -#include "source.hpp" +#include "image_source.hpp" class Tiling { public: - virtual cv::Mat generate(const DocumentPreset &preset, std::vector images) = 0; + virtual cv::Mat generate(const DocumentPreset &preset, std::vector images) = 0; }; -class Tile { - public: - ImgSource& image; - - cv::Point corner; - - size_t width, height; - - bool rotated = false; - - Tile(ImgSource& img) : image(img), width(img.get_width()), height(img.get_height()) {} - - void rotate() { - std::swap(width, height); - rotated = !rotated; - } - - size_t getArea() const { return width * height; } -}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 6fca959..7d54f4c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,24 +7,16 @@ #include #include -#include "mask.hpp" -#include "grid_tiling.hpp" - // using json = nlohmann::json; int main(void) { cv::Mat image = cv::imread("./assets/3.png", 1); - GridTiling tiling = GridTiling(); - DocumentPreset preset = {1500, 10, true, true}; - std::vector images = {ImgSource(image, 5, FilterStore())}; - cv::Mat result = tiling.generate(preset, images); - // create image window named "My Image" cv::namedWindow("My Image", cv::WINDOW_AUTOSIZE | cv::WINDOW_GUI_NORMAL); // show the image on window - cv::imshow("My Image", result); + cv::imshow("My Image", image); // aboszolút filmszínház cv::waitKey(0); diff --git a/src/settings/source_presets.cpp b/src/settings/source_presets.cpp deleted file mode 100644 index 2237d3a..0000000 --- a/src/settings/source_presets.cpp +++ /dev/null @@ -1,5 +0,0 @@ -/* -width -height -mask -*/ diff --git a/src/util/convert.cpp b/src/util/convert.cpp deleted file mode 100644 index 0d070c0..0000000 --- a/src/util/convert.cpp +++ /dev/null @@ -1,3 +0,0 @@ -/* -mm to px given the dpi - */ From 4b964dfd60da7e7eb3924216345a9da3e03d1276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 26 Feb 2025 19:26:42 +0100 Subject: [PATCH 22/51] add cache --- meson.build | 15 +++++++++++++-- src/img/cached_image.cpp | 14 ++++++++++++++ src/img/cached_image.hpp | 14 +++++++++----- src/img/filters/filter.hpp | 2 +- src/img/filters/mask.cpp | 2 +- src/img/filters/mask.hpp | 2 +- src/img/filters/rotate.cpp | 9 +++++++++ src/img/filters/rotate.hpp | 13 +++++++++++++ src/img/filters/size.cpp | 6 +++++- src/img/filters/size.hpp | 2 +- src/img/image_source.cpp | 28 ++++++++++++++++++++++++++++ src/img/image_source.hpp | 24 ++++++++++++++---------- src/img/tiling/grid_tiling.cpp | 6 ++---- src/interfaces/icachable.hpp | 8 ++++++++ src/interfaces/icache.hpp | 8 ++++++++ src/main.cpp | 11 +++++++++-- 16 files changed, 136 insertions(+), 28 deletions(-) create mode 100644 src/img/cached_image.cpp create mode 100644 src/img/filters/rotate.cpp create mode 100644 src/img/filters/rotate.hpp create mode 100644 src/img/image_source.cpp create mode 100644 src/interfaces/icachable.hpp create mode 100644 src/interfaces/icache.hpp diff --git a/meson.build b/meson.build index d1c4759..c1f27a5 100644 --- a/meson.build +++ b/meson.build @@ -9,12 +9,23 @@ qt6_dep = dependency('qt6', modules: ['Core']) incdir = include_directories( - 'src', + 'src', + 'src/interfaces', + 'src/img', + 'src/img/filters', + 'src/img/tiling', + 'src/settings', ) # Source files srcs = files( - 'src/main.cpp', + 'src/img/filters/mask.cpp', + 'src/img/filters/rotate.cpp', + 'src/img/filters/size.cpp', + 'src/img/tiling/grid_tiling.cpp', + 'src/img/cached_image.cpp', + 'src/img/image_source.cpp', + 'src/main.cpp' ) # Executable diff --git a/src/img/cached_image.cpp b/src/img/cached_image.cpp new file mode 100644 index 0000000..8b48403 --- /dev/null +++ b/src/img/cached_image.cpp @@ -0,0 +1,14 @@ +#include "cached_image.hpp" + +CachedImage::CachedImage(const ICachableImage& source) : source(source), isDirty(true) {} + +cv::Mat CachedImage::get_img() { + if (isDirty) { + cache = source.get_cachable(); + isDirty = false; + } + return cache; +} + + +void CachedImage::set_dirty() { isDirty = true; } \ No newline at end of file diff --git a/src/img/cached_image.hpp b/src/img/cached_image.hpp index 3d74b96..3fc4566 100644 --- a/src/img/cached_image.hpp +++ b/src/img/cached_image.hpp @@ -1,14 +1,18 @@ #pragma once #include +#include "interfaces/icachable.hpp" +#include "interfaces/icache.hpp" -// TODO -class CachedImage { +class CachedImage : ICache { private: - cv::Mat image; + const ICachableImage& source; + cv::Mat cache; bool isDirty; public: - CachedImage(); - ~CachedImage(); + CachedImage(const ICachableImage& source); + cv::Mat get_img(); + + void set_dirty() override; }; diff --git a/src/img/filters/filter.hpp b/src/img/filters/filter.hpp index f700a96..4456695 100644 --- a/src/img/filters/filter.hpp +++ b/src/img/filters/filter.hpp @@ -3,5 +3,5 @@ class Filter { public: - virtual cv::Mat apply(const cv::Mat &image) = 0; + virtual cv::Mat apply(const cv::Mat &image) const = 0; }; diff --git a/src/img/filters/mask.cpp b/src/img/filters/mask.cpp index 1bbe6c2..9b11ab5 100644 --- a/src/img/filters/mask.cpp +++ b/src/img/filters/mask.cpp @@ -4,7 +4,7 @@ MaskFilter::MaskFilter(const cv::Mat &mask) : mask(mask) {} -cv::Mat MaskFilter::apply(const cv::Mat &image) { +cv::Mat MaskFilter::apply(const cv::Mat &image) const { auto fitMask = SizeFilter::resize(mask, image.cols, image.rows); if (invert) { diff --git a/src/img/filters/mask.hpp b/src/img/filters/mask.hpp index cf4dc14..99105d6 100644 --- a/src/img/filters/mask.hpp +++ b/src/img/filters/mask.hpp @@ -11,7 +11,7 @@ class MaskFilter : public Filter { public: MaskFilter(const cv::Mat &mask); - cv::Mat apply(const cv::Mat &image) override; + cv::Mat apply(const cv::Mat &image) const override; void setInvert(bool invert); }; diff --git a/src/img/filters/rotate.cpp b/src/img/filters/rotate.cpp new file mode 100644 index 0000000..f3b6c5c --- /dev/null +++ b/src/img/filters/rotate.cpp @@ -0,0 +1,9 @@ +#include "rotate.hpp" + +cv::Mat RotateFilter::apply(const cv::Mat &image) const { return RotateFilter::rotate(image, default_rotation_dir); } + +cv::Mat RotateFilter::rotate(const cv::Mat &image, cv::RotateFlags rotation_dir) { + cv::Mat rotated; + cv::rotate(image, rotated, rotation_dir); + return rotated; +} diff --git a/src/img/filters/rotate.hpp b/src/img/filters/rotate.hpp new file mode 100644 index 0000000..52ed333 --- /dev/null +++ b/src/img/filters/rotate.hpp @@ -0,0 +1,13 @@ +#pragma once +#include + +#include "filter.hpp" + +class RotateFilter : public Filter { + private: + static const cv::RotateFlags default_rotation_dir = cv::ROTATE_90_CLOCKWISE; + + public: + cv::Mat apply(const cv::Mat &image) const override; + static cv::Mat rotate(const cv::Mat &image, cv::RotateFlags rotation_dir); +}; diff --git a/src/img/filters/size.cpp b/src/img/filters/size.cpp index 1745275..800a2b8 100644 --- a/src/img/filters/size.cpp +++ b/src/img/filters/size.cpp @@ -2,9 +2,13 @@ SizeFilter::SizeFilter(int width, int height) : width(width), height(height) {} -cv::Mat SizeFilter::apply(const cv::Mat &image) { return SizeFilter::resize(image, width, height); } +cv::Mat SizeFilter::apply(const cv::Mat &image) const { return SizeFilter::resize(image, width, height); } cv::Mat SizeFilter::resize(const cv::Mat &image, int width, int height, int interDown, int interUp) { + if (width == image.cols && height == image.rows) { + return image; + } + bool downScaling = width < image.cols || height < image.rows; cv::Mat resized; diff --git a/src/img/filters/size.hpp b/src/img/filters/size.hpp index 50eeccf..dce275e 100644 --- a/src/img/filters/size.hpp +++ b/src/img/filters/size.hpp @@ -14,7 +14,7 @@ class SizeFilter : public Filter { public: SizeFilter(int width, int height); - cv::Mat apply(const cv::Mat &image) override; + cv::Mat apply(const cv::Mat &image) const override; static cv::Mat resize(const cv::Mat &image, int width, int height, int interDown = sizeInterDown, int interUp = sizeInterUp); diff --git a/src/img/image_source.cpp b/src/img/image_source.cpp new file mode 100644 index 0000000..275e693 --- /dev/null +++ b/src/img/image_source.cpp @@ -0,0 +1,28 @@ +#include "image_source.hpp" + +ImageSource::ImageSource(cv::Mat source, size_t amount) : original(source), amount(amount), cached(*this), filters(), rotated(false) { + width = source.cols; + height = source.rows; +} + +void ImageSource::add_filter(Filter * filter) { + filters.push_back(filter); + cached.set_dirty(); +} + +void ImageSource::clear_filters() { + for (auto filter : filters) + { + delete filter; + } + filters.clear(); + cached.set_dirty(); +} + +cv::Mat ImageSource::apply_filters() const { + cv::Mat image = original; + for (auto filter : filters) { + image = filter->apply(image); + } + return image; +} diff --git a/src/img/image_source.hpp b/src/img/image_source.hpp index 19f2cfa..1f3e60f 100644 --- a/src/img/image_source.hpp +++ b/src/img/image_source.hpp @@ -3,32 +3,36 @@ #include #include -#include "filter.hpp" #include "cached_image.hpp" +#include "filters/filter.hpp" -class ImageSource { +class ImageSource : ICachableImage { private: cv::Mat original; CachedImage cached; size_t amount; - std::vector filters; + std::vector filters; bool rotated; size_t width, height; public: - ImageSource(cv::Mat source); + ImageSource(cv::Mat source, size_t amount); - void add_filter(const Filter& filter); + ~ImageSource() { clear_filters(); } + + void add_filter(Filter* filter); void clear_filters(); - void apply_filters(); + cv::Mat apply_filters() const; + + cv::Mat get_img() { return cached.get_img(); } - cv::Mat get_img() const; + size_t get_width() const { return width; } - size_t get_width() const; + size_t get_height() const { return height; } - size_t get_height() const; + size_t get_amount() const { return amount; } - size_t get_amount() const; + cv::Mat get_cachable() const override { return apply_filters(); } }; diff --git a/src/img/tiling/grid_tiling.cpp b/src/img/tiling/grid_tiling.cpp index 12514ef..4dba9d7 100644 --- a/src/img/tiling/grid_tiling.cpp +++ b/src/img/tiling/grid_tiling.cpp @@ -1,5 +1,6 @@ #include "grid_tiling.hpp" #include "tile.hpp" +#include "rotate.hpp" #include size_t GridTiling::calc_waste(size_t document_width, size_t tile_width, size_t tile_height, size_t amount) { @@ -53,10 +54,7 @@ cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector + +class ICachableImage { + public: + virtual cv::Mat get_cachable() const = 0; +}; \ No newline at end of file diff --git a/src/interfaces/icache.hpp b/src/interfaces/icache.hpp new file mode 100644 index 0000000..e8ef8c7 --- /dev/null +++ b/src/interfaces/icache.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "icachable.hpp" + +class ICache { + public: + virtual void set_dirty() = 0; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 7d54f4c..aee8947 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,14 +9,21 @@ // using json = nlohmann::json; +#include "img/image_source.hpp" +#include "img/filters/rotate.hpp" + int main(void) { - cv::Mat image = cv::imread("./assets/3.png", 1); + cv::Mat source = cv::imread("./assets/3.png", 1); + + ImageSource img = ImageSource(source, 1); + + img.add_filter(new RotateFilter()); // create image window named "My Image" cv::namedWindow("My Image", cv::WINDOW_AUTOSIZE | cv::WINDOW_GUI_NORMAL); // show the image on window - cv::imshow("My Image", image); + cv::imshow("My Image", img.get_img()); // aboszolút filmszínház cv::waitKey(0); From 591de08b6f592fe1447a7c2bf8aad764c19bff01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 5 Mar 2025 10:01:52 +0100 Subject: [PATCH 23/51] include updates --- src/img/cached_image.hpp | 4 ++-- src/img/image_source.hpp | 3 ++- src/main.cpp | 9 ++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/img/cached_image.hpp b/src/img/cached_image.hpp index 3fc4566..46ca01c 100644 --- a/src/img/cached_image.hpp +++ b/src/img/cached_image.hpp @@ -1,8 +1,8 @@ #pragma once #include -#include "interfaces/icachable.hpp" -#include "interfaces/icache.hpp" +#include "icachable.hpp" +#include "icache.hpp" class CachedImage : ICache { private: diff --git a/src/img/image_source.hpp b/src/img/image_source.hpp index 1f3e60f..e8160e5 100644 --- a/src/img/image_source.hpp +++ b/src/img/image_source.hpp @@ -4,7 +4,8 @@ #include #include "cached_image.hpp" -#include "filters/filter.hpp" +#include "icachable.hpp" +#include "filter.hpp" class ImageSource : ICachableImage { private: diff --git a/src/main.cpp b/src/main.cpp index aee8947..7eb4d2a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,10 +7,9 @@ #include #include -// using json = nlohmann::json; - -#include "img/image_source.hpp" -#include "img/filters/rotate.hpp" +#include "image_source.hpp" +#include "rotate.hpp" +#include "size.hpp" int main(void) { cv::Mat source = cv::imread("./assets/3.png", 1); @@ -23,7 +22,7 @@ int main(void) { cv::namedWindow("My Image", cv::WINDOW_AUTOSIZE | cv::WINDOW_GUI_NORMAL); // show the image on window - cv::imshow("My Image", img.get_img()); + cv::imshow("My Image", SizeFilter::resize(img.get_img(), 800, 600)); // aboszolút filmszínház cv::waitKey(0); From 7e955a69436fe73163a4c5d7a7f23a16b7a992f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 5 Mar 2025 13:05:28 +0100 Subject: [PATCH 24/51] add document presets --- meson.build | 2 ++ presets/document/tekercs.json | 9 +++++++ presets/image/a4.json | 6 +++++ presets/printer/evil_plotter.json | 5 ++++ presets/sample.json | 3 --- src/img/filters/size.cpp | 4 ++++ src/img/filters/size.hpp | 2 ++ src/img/image_source.hpp | 10 ++++++++ src/img/tiling/grid_tiling.cpp | 39 +++++++++++++++++-------------- src/img/tiling/grid_tiling.hpp | 2 +- src/img/tiling/tile.hpp | 6 ++--- src/img/tiling/tiling.hpp | 4 ++-- src/main.cpp | 23 +++++++++++------- src/settings/document_preset.cpp | 31 ++++++++++++++++++++++++ src/settings/document_preset.hpp | 35 +++++++++++++++++++++++++++ src/settings/document_presets.hpp | 16 ------------- src/util/convert.hpp | 9 +++++++ 17 files changed, 155 insertions(+), 51 deletions(-) create mode 100644 presets/document/tekercs.json create mode 100644 presets/image/a4.json create mode 100644 presets/printer/evil_plotter.json delete mode 100644 presets/sample.json create mode 100644 src/settings/document_preset.cpp create mode 100644 src/settings/document_preset.hpp delete mode 100644 src/settings/document_presets.hpp create mode 100644 src/util/convert.hpp diff --git a/meson.build b/meson.build index c1f27a5..7f343a7 100644 --- a/meson.build +++ b/meson.build @@ -15,6 +15,7 @@ incdir = include_directories( 'src/img/filters', 'src/img/tiling', 'src/settings', + 'src/util' ) # Source files @@ -25,6 +26,7 @@ srcs = files( 'src/img/tiling/grid_tiling.cpp', 'src/img/cached_image.cpp', 'src/img/image_source.cpp', + 'src/settings/document_preset.cpp', 'src/main.cpp' ) diff --git a/presets/document/tekercs.json b/presets/document/tekercs.json new file mode 100644 index 0000000..96b3794 --- /dev/null +++ b/presets/document/tekercs.json @@ -0,0 +1,9 @@ +{ + "name": "609.6mm tekercs", + "roll_width_mm": 609.6, + "resolution_ppi": 300, + "margin_mm": 0, + "gutter_mm": 0, + "guide": true, + "correct_quantity": false +} \ No newline at end of file diff --git a/presets/image/a4.json b/presets/image/a4.json new file mode 100644 index 0000000..a5745ce --- /dev/null +++ b/presets/image/a4.json @@ -0,0 +1,6 @@ +{ + "name": "A4", + "width": 210, + "height": 297, + "suggested_resolution": 300 +} \ No newline at end of file diff --git a/presets/printer/evil_plotter.json b/presets/printer/evil_plotter.json new file mode 100644 index 0000000..6e06d24 --- /dev/null +++ b/presets/printer/evil_plotter.json @@ -0,0 +1,5 @@ +{ + "name": "Canon imagePROGRAF PRO-4000", + "max_print_height": 18000, + "min_print_height": 101.6 +} \ No newline at end of file diff --git a/presets/sample.json b/presets/sample.json deleted file mode 100644 index 199d62c..0000000 --- a/presets/sample.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "gyros": "karimi" -} diff --git a/src/img/filters/size.cpp b/src/img/filters/size.cpp index 800a2b8..5bee3e3 100644 --- a/src/img/filters/size.cpp +++ b/src/img/filters/size.cpp @@ -16,3 +16,7 @@ cv::Mat SizeFilter::resize(const cv::Mat &image, int width, int height, int inte return resized; } + +cv::Mat SizeFilter::resize_to_width(const cv::Mat &image, int width, int interDown, int interUp) { + return SizeFilter::resize(image, width, image.rows * width / image.cols, interDown, interUp); +} diff --git a/src/img/filters/size.hpp b/src/img/filters/size.hpp index dce275e..49ba538 100644 --- a/src/img/filters/size.hpp +++ b/src/img/filters/size.hpp @@ -18,4 +18,6 @@ class SizeFilter : public Filter { static cv::Mat resize(const cv::Mat &image, int width, int height, int interDown = sizeInterDown, int interUp = sizeInterUp); + + static cv::Mat resize_to_width(const cv::Mat &image, int width, int interDown = sizeInterDown, int interUp = sizeInterUp); }; diff --git a/src/img/image_source.hpp b/src/img/image_source.hpp index e8160e5..4cdb4cb 100644 --- a/src/img/image_source.hpp +++ b/src/img/image_source.hpp @@ -20,6 +20,16 @@ class ImageSource : ICachableImage { ImageSource(cv::Mat source, size_t amount); ~ImageSource() { clear_filters(); } + ImageSource(const ImageSource& other) + : original(other.original), + cached(*this), + amount(other.amount), + filters(other.filters), // FIXME + rotated(other.rotated), + width(other.width), + height(other.height) { + std::cout << "Copy constructor called" << std::endl; + } void add_filter(Filter* filter); diff --git a/src/img/tiling/grid_tiling.cpp b/src/img/tiling/grid_tiling.cpp index 4dba9d7..259fcbe 100644 --- a/src/img/tiling/grid_tiling.cpp +++ b/src/img/tiling/grid_tiling.cpp @@ -10,14 +10,14 @@ size_t GridTiling::calc_waste(size_t document_width, size_t tile_width, size_t t return (document_width - tile_width * std::min(amount, columns)) * tile_height; } -cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector images) { - size_t document_width = preset.document_width_px; // and take the gutter into account +cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector images) { + size_t document_width = preset.get_document_width_px(); // and take the gutter into account - size_t tile_width = images[0].get_width(); - size_t tile_height = images[0].get_height(); + size_t tile_width = images[0]->get_width(); + size_t tile_height = images[0]->get_height(); - size_t quantity = std::accumulate(images.begin(), images.end(), 0, [](size_t sum, const ImageSource& img) { - return sum + img.get_amount(); + size_t quantity = std::accumulate(images.begin(), images.end(), 0, [](size_t sum, const ImageSource* img) { + return sum + img->get_amount(); }); @@ -43,28 +43,31 @@ cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector tiles = {}; - - - for (ImageSource& img : images) { + for (ImageSource* img : images) { if (rotate) { - img.add_filter(new RotateFilter()); + img->add_filter(new RotateFilter()); } // TODO // set the size of every image to match the first one // add gutter filter with guide parameter if the image doesnt already have one - for (size_t i = 0; i < img.get_amount(); i++) { + for (size_t i = 0; i < img->get_amount(); i++) { tiles.push_back(Tile(img)); } } + size_t columns = std::floor(document_width / tile_width); + size_t rows = std::ceil((double)quantity / columns); + size_t document_height = rows * tile_height; + + cv::Mat document = cv::Mat::ones(document_height, document_width, CV_8UC3); + document.setTo(cv::Scalar(255, 255, 255)); + + + + + // TODO: corrected quantity @@ -72,7 +75,7 @@ cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector images) override; + cv::Mat generate(const DocumentPreset& preset, std::vector images) override; }; \ No newline at end of file diff --git a/src/img/tiling/tile.hpp b/src/img/tiling/tile.hpp index 7430cf5..67b4385 100644 --- a/src/img/tiling/tile.hpp +++ b/src/img/tiling/tile.hpp @@ -4,7 +4,7 @@ class Tile { private: - ImageSource& image; + ImageSource* image; size_t width, height; public: @@ -13,7 +13,7 @@ class Tile { bool rotated = false; - Tile(ImageSource& img) : image(img), width(img.get_width()), height(img.get_height()) {} + Tile(ImageSource* img) : image(img), width(img->get_width()), height(img->get_height()) {} void rotate() { std::swap(width, height); @@ -26,5 +26,5 @@ class Tile { size_t get_height() const { return height; } - cv::Mat get_image() { return image.get_img(); } + cv::Mat get_image() { return image->get_img(); } }; diff --git a/src/img/tiling/tiling.hpp b/src/img/tiling/tiling.hpp index c5e23f1..8d8964b 100644 --- a/src/img/tiling/tiling.hpp +++ b/src/img/tiling/tiling.hpp @@ -1,9 +1,9 @@ #pragma once -#include "document_presets.hpp" +#include "document_preset.hpp" #include "image_source.hpp" class Tiling { public: - virtual cv::Mat generate(const DocumentPreset &preset, std::vector images) = 0; + virtual cv::Mat generate(const DocumentPreset &preset, std::vector images) = 0; }; diff --git a/src/main.cpp b/src/main.cpp index 7eb4d2a..a0099a4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,22 +10,29 @@ #include "image_source.hpp" #include "rotate.hpp" #include "size.hpp" +#include "document_preset.hpp" +#include "grid_tiling.hpp" int main(void) { - cv::Mat source = cv::imread("./assets/3.png", 1); + DocumentPreset preset = DocumentPreset("./presets/document/tekercs.json"); + + std::cout << preset.get_document_width_px() << std::endl; - ImageSource img = ImageSource(source, 1); - img.add_filter(new RotateFilter()); + cv::Mat source = cv::imread("./assets/3.png"); - // create image window named "My Image" - cv::namedWindow("My Image", cv::WINDOW_AUTOSIZE | cv::WINDOW_GUI_NORMAL); + ImageSource img = ImageSource(source, 11); + + // img.add_filter(new RotateFilter()); + + GridTiling tiling = GridTiling(); + cv::Mat result = tiling.generate(preset, {&img}); - // show the image on window - cv::imshow("My Image", SizeFilter::resize(img.get_img(), 800, 600)); - // aboszolút filmszínház + cv::namedWindow("My Image", cv::WINDOW_AUTOSIZE | cv::WINDOW_GUI_NORMAL); + cv::imshow("My Image", SizeFilter::resize_to_width(result, 1080)); cv::waitKey(0); + return 0; } diff --git a/src/settings/document_preset.cpp b/src/settings/document_preset.cpp new file mode 100644 index 0000000..28d711f --- /dev/null +++ b/src/settings/document_preset.cpp @@ -0,0 +1,31 @@ +#include "document_preset.hpp" + +#include +#include + +#include "convert.hpp" + +using json = nlohmann::json; + + +DocumentPreset::DocumentPreset(std::string path) { + std::ifstream f(path); + json data = json::parse(f); + f.close(); + + name = data["name"]; + + ppi = data["resolution_ppi"]; + roll_width_mm = data["roll_width_mm"]; + margin_mm = data["margin_mm"]; + gutter_mm = data["gutter_mm"]; + + guide = data["guide"]; + correct_quantity = data["correct_quantity"]; +} + +size_t DocumentPreset::get_document_width_px() const { return convert::mm_to_pixels(roll_width_mm - margin_mm * 2, ppi); } + +size_t DocumentPreset::get_max_height_px() const { return convert::mm_to_pixels(max_height_mm, ppi); } + +size_t DocumentPreset::get_min_height_px() const { return convert::mm_to_pixels(min_height_mm, ppi); } diff --git a/src/settings/document_preset.hpp b/src/settings/document_preset.hpp new file mode 100644 index 0000000..2906ead --- /dev/null +++ b/src/settings/document_preset.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include + +/* +TODO: +ppi +roll witdth - margin * 2 = document width +min max height +*/ + +class DocumentPreset { + private: + std::string name; + + double ppi; + double roll_width_mm; + double margin_mm; + double gutter_mm; + + bool correct_quantity; + bool guide; + + double min_height_mm; + double max_height_mm; + + public: + DocumentPreset(std::string path); + + size_t get_document_width_px() const; + + size_t get_max_height_px() const; + + size_t get_min_height_px() const; +}; diff --git a/src/settings/document_presets.hpp b/src/settings/document_presets.hpp deleted file mode 100644 index 842e099..0000000 --- a/src/settings/document_presets.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once -#include - -/* -TODO: -ppi -roll witdth - margin * 2 = document width -min max height -*/ - -struct DocumentPreset { - size_t document_width_px; - size_t gutter_width_px; - bool correct_quantity; - bool guide; -}; diff --git a/src/util/convert.hpp b/src/util/convert.hpp new file mode 100644 index 0000000..258512f --- /dev/null +++ b/src/util/convert.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace convert { + + inline double mm_to_pixels(double mm, double ppi) { return (mm / 25.4) * ppi; } + + inline double inch_to_pixels(double inch, double ppi) { return inch * ppi; } + +} // namespace Convert \ No newline at end of file From 6597c5a1c69b1de8cb39a11a97de7ed94ea08338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 5 Mar 2025 13:46:18 +0100 Subject: [PATCH 25/51] add gutter and guides --- meson.build | 1 + src/img/cached_image.cpp | 22 +++++++++++++++++----- src/img/cached_image.hpp | 7 +++++++ src/img/filters/padding.cpp | 22 ++++++++++++++++++++++ src/img/filters/padding.hpp | 15 +++++++++++++++ src/img/image_source.hpp | 4 ++-- src/img/tiling/grid_tiling.cpp | 25 ++++++++++--------------- src/main.cpp | 4 ++++ src/settings/document_preset.cpp | 6 ++++++ src/settings/document_preset.hpp | 2 ++ 10 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 src/img/filters/padding.cpp create mode 100644 src/img/filters/padding.hpp diff --git a/meson.build b/meson.build index 7f343a7..f838519 100644 --- a/meson.build +++ b/meson.build @@ -23,6 +23,7 @@ srcs = files( 'src/img/filters/mask.cpp', 'src/img/filters/rotate.cpp', 'src/img/filters/size.cpp', + 'src/img/filters/padding.cpp', 'src/img/tiling/grid_tiling.cpp', 'src/img/cached_image.cpp', 'src/img/image_source.cpp', diff --git a/src/img/cached_image.cpp b/src/img/cached_image.cpp index 8b48403..bd8f590 100644 --- a/src/img/cached_image.cpp +++ b/src/img/cached_image.cpp @@ -2,13 +2,25 @@ CachedImage::CachedImage(const ICachableImage& source) : source(source), isDirty(true) {} -cv::Mat CachedImage::get_img() { - if (isDirty) { - cache = source.get_cachable(); - isDirty = false; - } +cv::Mat CachedImage::get_img() { + regenerate(); return cache; } +void CachedImage::regenerate() { + if (!isDirty) return; + cache = source.get_cachable(); + isDirty = false; +} + +size_t CachedImage::get_width() { + regenerate(); + return cache.cols; +} + +size_t CachedImage::get_height() { + regenerate(); + return cache.rows; +} void CachedImage::set_dirty() { isDirty = true; } \ No newline at end of file diff --git a/src/img/cached_image.hpp b/src/img/cached_image.hpp index 46ca01c..671b0f5 100644 --- a/src/img/cached_image.hpp +++ b/src/img/cached_image.hpp @@ -10,9 +10,16 @@ class CachedImage : ICache { cv::Mat cache; bool isDirty; + void regenerate(); + public: CachedImage(const ICachableImage& source); + cv::Mat get_img(); + size_t get_width(); + + size_t get_height(); + void set_dirty() override; }; diff --git a/src/img/filters/padding.cpp b/src/img/filters/padding.cpp new file mode 100644 index 0000000..02a3237 --- /dev/null +++ b/src/img/filters/padding.cpp @@ -0,0 +1,22 @@ +#include "padding.hpp" + +PaddingFilter::PaddingFilter(size_t padding, bool guide): padding(padding), guide(guide) {} + +cv::Mat PaddingFilter::apply(const cv::Mat &image) const { + cv::Mat padded; + cv::copyMakeBorder(image, padded, padding, padding, padding, padding, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255)); + + if (guide) { + // TODO: make this configurable + int thickness = 1; + cv::Scalar color = cv::Scalar(0, 0, 0); + + // the lines are outside of the image + cv::line(padded, cv::Point(0, padding - 1), cv::Point(padded.cols - 1, padding - 1), color, thickness, cv::LINE_8); // top + cv::line(padded, cv::Point(0, padded.rows - padding), cv::Point(padded.cols - 1, padded.rows - padding), color, thickness, cv::LINE_8); // bottom + cv::line(padded, cv::Point(padding - 1, 0), cv::Point(padding - 1, padded.rows - 1), color, thickness, cv::LINE_8); // left + cv::line(padded, cv::Point(padded.cols - padding, 0), cv::Point(padded.cols - padding, padded.rows - 1), color, thickness, cv::LINE_8); // right + } + + return padded; +} \ No newline at end of file diff --git a/src/img/filters/padding.hpp b/src/img/filters/padding.hpp new file mode 100644 index 0000000..736ca27 --- /dev/null +++ b/src/img/filters/padding.hpp @@ -0,0 +1,15 @@ +#pragma once +#include + +#include "filter.hpp" + +class PaddingFilter : public Filter { + private: + size_t padding; + bool guide; + + public: + PaddingFilter(size_t padding, bool guide = false); + + cv::Mat apply(const cv::Mat &image) const override; +}; \ No newline at end of file diff --git a/src/img/image_source.hpp b/src/img/image_source.hpp index 4cdb4cb..0fd6b64 100644 --- a/src/img/image_source.hpp +++ b/src/img/image_source.hpp @@ -39,9 +39,9 @@ class ImageSource : ICachableImage { cv::Mat get_img() { return cached.get_img(); } - size_t get_width() const { return width; } + size_t get_width() { return cached.get_width(); } - size_t get_height() const { return height; } + size_t get_height() { return cached.get_height(); } size_t get_amount() const { return amount; } diff --git a/src/img/tiling/grid_tiling.cpp b/src/img/tiling/grid_tiling.cpp index 259fcbe..86d308b 100644 --- a/src/img/tiling/grid_tiling.cpp +++ b/src/img/tiling/grid_tiling.cpp @@ -1,8 +1,11 @@ #include "grid_tiling.hpp" -#include "tile.hpp" -#include "rotate.hpp" + #include +#include "rotate.hpp" +#include "size.hpp" +#include "tile.hpp" + size_t GridTiling::calc_waste(size_t document_width, size_t tile_width, size_t tile_height, size_t amount) { size_t columns = std::floor(document_width / tile_width); @@ -10,17 +13,14 @@ size_t GridTiling::calc_waste(size_t document_width, size_t tile_width, size_t t return (document_width - tile_width * std::min(amount, columns)) * tile_height; } -cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector images) { - size_t document_width = preset.get_document_width_px(); // and take the gutter into account +cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector images) { + size_t document_width = preset.get_document_width_px() + 2 * preset.get_gutter_px(); // FIXME proper gutter size_t tile_width = images[0]->get_width(); size_t tile_height = images[0]->get_height(); - size_t quantity = std::accumulate(images.begin(), images.end(), 0, [](size_t sum, const ImageSource* img) { - return sum + img->get_amount(); - }); - - + size_t quantity = std::accumulate(images.begin(), images.end(), 0, + [](size_t sum, const ImageSource* img) { return sum + img->get_amount(); }); bool rotate = false; // fits both ways @@ -51,6 +51,7 @@ cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vectoradd_filter(new SizeFilter(tile_width, tile_height)); for (size_t i = 0; i < img->get_amount(); i++) { tiles.push_back(Tile(img)); @@ -64,12 +65,6 @@ cv::Mat GridTiling::generate(const DocumentPreset& preset, std::vector Date: Wed, 5 Mar 2025 14:22:12 +0100 Subject: [PATCH 26/51] better guides --- src/img/filters/padding.cpp | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/img/filters/padding.cpp b/src/img/filters/padding.cpp index 02a3237..b9f5423 100644 --- a/src/img/filters/padding.cpp +++ b/src/img/filters/padding.cpp @@ -10,12 +10,34 @@ cv::Mat PaddingFilter::apply(const cv::Mat &image) const { // TODO: make this configurable int thickness = 1; cv::Scalar color = cv::Scalar(0, 0, 0); + int width = padded.cols; + int height = padded.rows; - // the lines are outside of the image + // the lines are outside of the image, full width + /* cv::line(padded, cv::Point(0, padding - 1), cv::Point(padded.cols - 1, padding - 1), color, thickness, cv::LINE_8); // top cv::line(padded, cv::Point(0, padded.rows - padding), cv::Point(padded.cols - 1, padded.rows - padding), color, thickness, cv::LINE_8); // bottom cv::line(padded, cv::Point(padding - 1, 0), cv::Point(padding - 1, padded.rows - 1), color, thickness, cv::LINE_8); // left cv::line(padded, cv::Point(padded.cols - padding, 0), cv::Point(padded.cols - padding, padded.rows - 1), color, thickness, cv::LINE_8); // right + */ + + // the lines are only outside of the image, show only on the gutter + // top + cv::line(padded, cv::Point(0, padding), cv::Point(padding - 1, padding), color, thickness, cv::LINE_8); + cv::line(padded, cv::Point(width - padding, padding), cv::Point(width - 1, padding), color, thickness, cv::LINE_8); + + // bottom + cv::line(padded, cv::Point(0, height - padding - 1), cv::Point(padding - 1, height - padding - 1), color, thickness, cv::LINE_8); + cv::line(padded, cv::Point(width - padding, height - padding - 1), cv::Point(width - 1, height - padding - 1), color, thickness, cv::LINE_8); + + // left + cv::line(padded, cv::Point(padding, 0), cv::Point(padding, padding - 1), color, thickness, cv::LINE_8); + cv::line(padded, cv::Point(padding, height - padding), cv::Point(padding, height - 1), color, thickness, cv::LINE_8); + + // right + cv::line(padded, cv::Point(width - padding - 1, 0), cv::Point(width - padding - 1, padding - 1), color, thickness, cv::LINE_8); + cv::line(padded, cv::Point(width - padding - 1, height - padding), cv::Point(width - padding - 1, height - 1), color, thickness, cv::LINE_8); + } return padded; From 4e31d2f16aa660e38a6570c7498a533be667160f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Tue, 18 Mar 2025 17:47:47 +0100 Subject: [PATCH 27/51] ui building setup --- meson.build | 30 +++++++++++++++++++++------ src/main.cpp | 47 ++++++++----------------------------------- src/ui/mainwindow.cpp | 14 +++++++++++++ src/ui/mainwindow.hpp | 21 +++++++++++++++++++ src/ui/mainwindow.ui | 31 ++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 45 deletions(-) create mode 100644 src/ui/mainwindow.cpp create mode 100644 src/ui/mainwindow.hpp create mode 100644 src/ui/mainwindow.ui diff --git a/meson.build b/meson.build index f838519..f6e1969 100644 --- a/meson.build +++ b/meson.build @@ -5,17 +5,34 @@ project('printf', 'cpp', # Dependencies nlohmann_json = dependency('nlohmann_json', required: true) opencv = dependency('opencv4', required: true) -qt6_dep = dependency('qt6', modules: ['Core']) +qt6_dep = dependency('qt6', modules: ['Core', 'Widgets', 'Gui']) -incdir = include_directories( + + +# Qt6 preprocessing +qt6 = import('qt6') + +ui_files = files('src/ui/mainwindow.ui') + +moc_files = files('src/ui/mainwindow.hpp') + +prep = qt6.preprocess( + moc_headers : moc_files, + ui_files : ui_files +) + + +# Include directories +inc = include_directories( 'src', 'src/interfaces', 'src/img', 'src/img/filters', 'src/img/tiling', 'src/settings', - 'src/util' + 'src/util', + 'src/ui', ) # Source files @@ -28,11 +45,12 @@ srcs = files( 'src/img/cached_image.cpp', 'src/img/image_source.cpp', 'src/settings/document_preset.cpp', - 'src/main.cpp' + 'src/ui/mainwindow.cpp', + 'src/main.cpp', ) # Executable -executable('printf', srcs, - include_directories : incdir, +executable('printf', [srcs, prep], + include_directories: inc, dependencies: [nlohmann_json, opencv, qt6_dep] ) diff --git a/src/main.cpp b/src/main.cpp index aeb37e1..de2498a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,42 +1,11 @@ -#include -#include +#include "mainwindow.hpp" -#include -#include -#include -#include -#include +#include -#include "image_source.hpp" -#include "rotate.hpp" -#include "size.hpp" -#include "document_preset.hpp" -#include "grid_tiling.hpp" -#include "padding.hpp" - -int main(void) { - DocumentPreset preset = DocumentPreset("./presets/document/tekercs.json"); - - std::cout << preset.get_document_width_px() << std::endl; - - - cv::Mat source = cv::imread("./assets/3.png"); - - ImageSource img = ImageSource(source, 11); - - img.add_filter(new PaddingFilter(100, true)); - - // img.add_filter(new RotateFilter()); - - GridTiling tiling = GridTiling(); - cv::Mat result = tiling.generate(preset, {&img}); - - - cv::namedWindow("My Image", cv::WINDOW_AUTOSIZE | cv::WINDOW_GUI_NORMAL); - cv::imshow("My Image", SizeFilter::resize_to_width(result, 1080)); - // cv::imshow("My Image", result); - cv::waitKey(0); - - - return 0; +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + return a.exec(); } diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp new file mode 100644 index 0000000..0d9b619 --- /dev/null +++ b/src/ui/mainwindow.cpp @@ -0,0 +1,14 @@ +#include "mainwindow.hpp" +#include "./ui_mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); +} + +MainWindow::~MainWindow() +{ + delete ui; +} diff --git a/src/ui/mainwindow.hpp b/src/ui/mainwindow.hpp new file mode 100644 index 0000000..0a92801 --- /dev/null +++ b/src/ui/mainwindow.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +QT_BEGIN_NAMESPACE +namespace Ui { +class MainWindow; +} +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + Ui::MainWindow *ui; +}; diff --git a/src/ui/mainwindow.ui b/src/ui/mainwindow.ui new file mode 100644 index 0000000..03943e0 --- /dev/null +++ b/src/ui/mainwindow.ui @@ -0,0 +1,31 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + 0 + 0 + 800 + 27 + + + + + + + + From 0374504fdb82162a214577650d580b9c406bc7fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Tue, 18 Mar 2025 18:23:07 +0100 Subject: [PATCH 28/51] qml building --- meson.build | 12 ++++++++---- src/main.cpp | 22 +++++++++++++--------- src/qml/example.qml | 15 +++++++++++++++ src/qml/resources.qrc | 5 +++++ 4 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 src/qml/example.qml create mode 100644 src/qml/resources.qrc diff --git a/meson.build b/meson.build index f6e1969..43f1c52 100644 --- a/meson.build +++ b/meson.build @@ -5,7 +5,7 @@ project('printf', 'cpp', # Dependencies nlohmann_json = dependency('nlohmann_json', required: true) opencv = dependency('opencv4', required: true) -qt6_dep = dependency('qt6', modules: ['Core', 'Widgets', 'Gui']) +qt6_dep = dependency('qt6', modules: ['Core', 'Widgets', 'Gui', 'Qml']) @@ -15,11 +15,14 @@ qt6 = import('qt6') ui_files = files('src/ui/mainwindow.ui') -moc_files = files('src/ui/mainwindow.hpp') +moc_headers = files('src/ui/mainwindow.hpp') + +qresources = files('src/qml/resources.qrc') prep = qt6.preprocess( - moc_headers : moc_files, - ui_files : ui_files + moc_headers : moc_headers, + ui_files : ui_files, + qresources : qresources, ) @@ -33,6 +36,7 @@ inc = include_directories( 'src/settings', 'src/util', 'src/ui', + 'src/qml', ) # Source files diff --git a/src/main.cpp b/src/main.cpp index de2498a..7baf2d6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,11 +1,15 @@ -#include "mainwindow.hpp" - #include +#include + +int main(int argc, char *argv[]) { + QApplication app(argc, argv); + + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:/qml/example.qml"))); + + if (engine.rootObjects().isEmpty()) { + return -1; + } -int main(int argc, char *argv[]) -{ - QApplication a(argc, argv); - MainWindow w; - w.show(); - return a.exec(); -} + return app.exec(); +} \ No newline at end of file diff --git a/src/qml/example.qml b/src/qml/example.qml new file mode 100644 index 0000000..f0dcd56 --- /dev/null +++ b/src/qml/example.qml @@ -0,0 +1,15 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +ApplicationWindow { + visible: true + width: 640 + height: 480 + title: "Example QML Window" + + Button { + text: "Click Me" + anchors.centerIn: parent + onClicked: console.log("Button clicked!") + } +} \ No newline at end of file diff --git a/src/qml/resources.qrc b/src/qml/resources.qrc new file mode 100644 index 0000000..0dcc94f --- /dev/null +++ b/src/qml/resources.qrc @@ -0,0 +1,5 @@ + + + example.qml + + \ No newline at end of file From d3ad495ca42a6d678e1fd7fa7b8d35dbb7c7c9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Tue, 18 Mar 2025 18:35:14 +0100 Subject: [PATCH 29/51] toriel --- src/qml/example.qml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/qml/example.qml b/src/qml/example.qml index f0dcd56..1d5bec7 100644 --- a/src/qml/example.qml +++ b/src/qml/example.qml @@ -1,5 +1,7 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 +import QtQuick +import QtQuick.Controls + +// https://doc.qt.io/qt-6/qml-tutorial2.html ApplicationWindow { visible: true @@ -12,4 +14,20 @@ ApplicationWindow { anchors.centerIn: parent onClicked: console.log("Button clicked!") } -} \ No newline at end of file + + Rectangle { + id: page + // width: 320; height: 480 + color: "lightgray" + anchors.fill: parent + + Text { + id: helloText + text: "Hello world!" + y: 30 + anchors.centerIn: parent + anchors.horizontalCenter: page.horizontalCenter + font.pointSize: 24; font.bold: true + } + } +} From 0af3b865406b41b0c6d71db8f343057dca0910ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 19 Mar 2025 11:21:00 +0100 Subject: [PATCH 30/51] flick --- src/main.cpp | 2 +- src/qml/3.png | Bin 0 -> 219355 bytes src/qml/Application.qml | 16 ++++++++++++ src/qml/File.qml | 0 src/qml/FileList.qml | 32 ++++++++++++++++++++++++ src/qml/Preview.qml | 53 ++++++++++++++++++++++++++++++++++++++++ src/qml/example.qml | 33 ------------------------- src/qml/resources.qrc | 6 ++++- 8 files changed, 107 insertions(+), 35 deletions(-) create mode 100755 src/qml/3.png create mode 100644 src/qml/Application.qml create mode 100644 src/qml/File.qml create mode 100644 src/qml/FileList.qml create mode 100644 src/qml/Preview.qml delete mode 100644 src/qml/example.qml diff --git a/src/main.cpp b/src/main.cpp index 7baf2d6..07321be 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,7 @@ int main(int argc, char *argv[]) { QApplication app(argc, argv); QQmlApplicationEngine engine; - engine.load(QUrl(QStringLiteral("qrc:/qml/example.qml"))); + engine.load(QUrl(QStringLiteral("qrc:/qml/Application.qml"))); if (engine.rootObjects().isEmpty()) { return -1; diff --git a/src/qml/3.png b/src/qml/3.png new file mode 100755 index 0000000000000000000000000000000000000000..8330852d2c81178b2dce8aa4efc12c0d0a13681f GIT binary patch literal 219355 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 { + let zoomFactor = 1.1; + if (wheel.angleDelta.y > 0) + image.scale *= zoomFactor; + else + image.scale /= zoomFactor; + flickable.contentWidth = image.width * image.scale; + flickable.contentHeight = image.height * image.scale; + } + } + + Flickable { + id: flickable + + anchors.fill: parent + contentWidth: image.width * image.scale + contentHeight: image.height * image.scale + clip: true + + Image { + id: image + + source: "3.png" + anchors.centerIn: parent + transformOrigin: Item.Center + scale: 1 + } + + MouseArea { + anchors.fill: parent + onWheel: (wheel) => mouseArea.wheel(wheel) // emit the signal to the main mouse area + } + + } + +} diff --git a/src/qml/example.qml b/src/qml/example.qml deleted file mode 100644 index 1d5bec7..0000000 --- a/src/qml/example.qml +++ /dev/null @@ -1,33 +0,0 @@ -import QtQuick -import QtQuick.Controls - -// https://doc.qt.io/qt-6/qml-tutorial2.html - -ApplicationWindow { - visible: true - width: 640 - height: 480 - title: "Example QML Window" - - Button { - text: "Click Me" - anchors.centerIn: parent - onClicked: console.log("Button clicked!") - } - - Rectangle { - id: page - // width: 320; height: 480 - color: "lightgray" - anchors.fill: parent - - Text { - id: helloText - text: "Hello world!" - y: 30 - anchors.centerIn: parent - anchors.horizontalCenter: page.horizontalCenter - font.pointSize: 24; font.bold: true - } - } -} diff --git a/src/qml/resources.qrc b/src/qml/resources.qrc index 0dcc94f..7d32772 100644 --- a/src/qml/resources.qrc +++ b/src/qml/resources.qrc @@ -1,5 +1,9 @@ - example.qml + Application.qml + FileList.qml + Preview.qml + 3.png + File.qml \ No newline at end of file From 28c3ac16422938cbf06559dc77f4e7e229d1b335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 19 Mar 2025 13:16:20 +0100 Subject: [PATCH 31/51] filelist delegate experimenting --- src/qml/Application.qml | 44 ++++++++++++++++++++++++++++++++++++++--- src/qml/File.qml | 26 ++++++++++++++++++++++++ src/qml/FileList.qml | 24 ++-------------------- src/qml/Preview.qml | 6 +++--- src/qml/Properties.qml | 14 +++++++++++++ src/qml/resources.qrc | 1 + 6 files changed, 87 insertions(+), 28 deletions(-) create mode 100644 src/qml/Properties.qml diff --git a/src/qml/Application.qml b/src/qml/Application.qml index 7c8b6e3..0d98065 100644 --- a/src/qml/Application.qml +++ b/src/qml/Application.qml @@ -8,9 +8,47 @@ ApplicationWindow { height: 480 title: "Example QML Window" - Preview { - id: preview + RowLayout { anchors.fill: parent + spacing: 10 + + FileList { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: 1 + } + + ListModel { + id: fileModel + + ListElement { + filePath: "file1.txt" + } + + ListElement { + filePath: "file2.txt" + } + + ListElement { + filePath: "file3.txt" + } + + } + + Preview { + id: preview + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: 3 + } + + Properties { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: 1 + } + } -} \ No newline at end of file +} diff --git a/src/qml/File.qml b/src/qml/File.qml index e69de29..2f49b68 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -0,0 +1,26 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Item { + id: fileItem + height: 50 + + Row { + spacing: 10 + + Text { + id: textItem + text: model.filePath + font.pixelSize: 16 + } + + Button { + text: "Open" + onClicked: { + fileHandler.openFile(model.filePath); + } + } + + } + +} diff --git a/src/qml/FileList.qml b/src/qml/FileList.qml index a83a2a9..7145202 100644 --- a/src/qml/FileList.qml +++ b/src/qml/FileList.qml @@ -3,30 +3,10 @@ import QtQuick.Controls ListView { id: fileListView - anchors.fill: parent model: fileModel clip: true - delegate: Item { - - width: fileListView.width - height: 40 - - Row { - spacing: 10 - anchors.verticalCenter: parent.verticalCenter - - Text { - text: model.filePath - font.pixelSize: 16 - } - - Button { - text: "Open" - onClicked: { - fileHandler.openFile(model.filePath) - } - } - } + delegate: File { } + spacing: 10 } \ No newline at end of file diff --git a/src/qml/Preview.qml b/src/qml/Preview.qml index b326a41..8da5fe5 100644 --- a/src/qml/Preview.qml +++ b/src/qml/Preview.qml @@ -6,8 +6,6 @@ Item { width: 10 height: 10 color: "#323232" - border.color: "red" - border.width: 1 anchors.fill: parent } @@ -45,7 +43,9 @@ Item { MouseArea { anchors.fill: parent - onWheel: (wheel) => mouseArea.wheel(wheel) // emit the signal to the main mouse area + onWheel: (wheel) => { + return mouseArea.wheel(wheel); + } // emit the signal to the main mouse area } } diff --git a/src/qml/Properties.qml b/src/qml/Properties.qml new file mode 100644 index 0000000..10eb708 --- /dev/null +++ b/src/qml/Properties.qml @@ -0,0 +1,14 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Item { + Button { + id: button + + text: "dont click me" + onClicked: { + console.log("Button clicked"); + } + } + +} diff --git a/src/qml/resources.qrc b/src/qml/resources.qrc index 7d32772..1c56e77 100644 --- a/src/qml/resources.qrc +++ b/src/qml/resources.qrc @@ -3,6 +3,7 @@ Application.qml FileList.qml Preview.qml + Properties.qml 3.png File.qml From 3809cbd97cafeda122d658c1140c65b1ca186b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 19 Mar 2025 19:34:06 +0100 Subject: [PATCH 32/51] dynamic list prep --- meson.build | 6 +-- src/main.cpp | 19 ++++---- src/qml/Application.qml | 1 - src/qml/File.qml | 96 +++++++++++++++++++++++++++++++++------ src/qml/FileList.qml | 48 ++++++++++++++++---- src/ui/dataentrymodel.cpp | 44 ++++++++++++++++++ src/ui/dataentrymodel.h | 19 ++++++++ src/ui/mainwindow.cpp | 14 ------ src/ui/mainwindow.hpp | 21 --------- src/ui/mainwindow.ui | 31 ------------- 10 files changed, 198 insertions(+), 101 deletions(-) create mode 100644 src/ui/dataentrymodel.cpp create mode 100644 src/ui/dataentrymodel.h delete mode 100644 src/ui/mainwindow.cpp delete mode 100644 src/ui/mainwindow.hpp delete mode 100644 src/ui/mainwindow.ui diff --git a/meson.build b/meson.build index 43f1c52..206a251 100644 --- a/meson.build +++ b/meson.build @@ -13,9 +13,9 @@ qt6_dep = dependency('qt6', modules: ['Core', 'Widgets', 'Gui', 'Qml']) # Qt6 preprocessing qt6 = import('qt6') -ui_files = files('src/ui/mainwindow.ui') +ui_files = files() -moc_headers = files('src/ui/mainwindow.hpp') +moc_headers = files('src/ui/dataentrymodel.h') qresources = files('src/qml/resources.qrc') @@ -49,7 +49,7 @@ srcs = files( 'src/img/cached_image.cpp', 'src/img/image_source.cpp', 'src/settings/document_preset.cpp', - 'src/ui/mainwindow.cpp', + 'src/ui/dataentrymodel.cpp', 'src/main.cpp', ) diff --git a/src/main.cpp b/src/main.cpp index 07321be..34eef37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,15 +1,16 @@ -#include -#include +#include +#include -int main(int argc, char *argv[]) { - QApplication app(argc, argv); +#include "dataentrymodel.h" + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + qmlRegisterType("printf", 1, 0, "DataEntryModel"); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/qml/Application.qml"))); - if (engine.rootObjects().isEmpty()) { - return -1; - } - return app.exec(); -} \ No newline at end of file +} diff --git a/src/qml/Application.qml b/src/qml/Application.qml index 0d98065..6b58568 100644 --- a/src/qml/Application.qml +++ b/src/qml/Application.qml @@ -10,7 +10,6 @@ ApplicationWindow { RowLayout { anchors.fill: parent - spacing: 10 FileList { Layout.fillHeight: true diff --git a/src/qml/File.qml b/src/qml/File.qml index 2f49b68..56d663a 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -1,24 +1,94 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 -Item { +Rectangle { id: fileItem - height: 50 - Row { - spacing: 10 + border.color: "black" + border.width: 1 + radius: 5 + implicitHeight: paddingCol.implicitHeight + 20 // FIXME: hack - Text { - id: textItem - text: model.filePath - font.pixelSize: 16 - } + Item { + id: container + + anchors.fill: parent + anchors.margins: 10 + + Column { + id: paddingCol + + spacing: 10 + width: container.width + + RowLayout { + id: detailsLayout + + width: parent.width + spacing: 10 + + Rectangle { + color: model.display + Layout.preferredWidth: 100 + Layout.preferredHeight: 100 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 10 + + RowLayout { + spacing: 10 + width: parent.width + + Text { + text: "path" + font.pixelSize: 16 + Layout.fillWidth: true + } + + Button { + id: deleteButton + + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + text: "X" + } + + } + + RowLayout { + spacing: 10 + width: parent.width + + Text { + text: model.display + font.pixelSize: 16 + Layout.fillWidth: true + } + + SpinBox { + id: spinbox + + Layout.preferredWidth: 60 + value: 0 + from: 0 + stepSize: 1 + } + + } + + Text { + text: "extra info about the file" + font.pixelSize: 16 + } + + } - Button { - text: "Open" - onClicked: { - fileHandler.openFile(model.filePath); } + } } diff --git a/src/qml/FileList.qml b/src/qml/FileList.qml index 7145202..155a470 100644 --- a/src/qml/FileList.qml +++ b/src/qml/FileList.qml @@ -1,12 +1,42 @@ -import QtQuick -import QtQuick.Controls +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import printf 1.0 -ListView { - id: fileListView - model: fileModel - clip: true +Item { + ColumnLayout { + id: filePanel + + anchors.fill: parent + spacing: 10 + anchors.margins: 10 + + Button { + id: openButton + + text: "Open" + onClicked: { + fileHandler.openFile(model.display); + } + } + + ListView { + id: fileListView + + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: 10 + + model: DataEntryModel { + } + + delegate: File { + width: filePanel.width + } + + } - delegate: File { } - spacing: 10 -} \ No newline at end of file + +} diff --git a/src/ui/dataentrymodel.cpp b/src/ui/dataentrymodel.cpp new file mode 100644 index 0000000..ca87291 --- /dev/null +++ b/src/ui/dataentrymodel.cpp @@ -0,0 +1,44 @@ +#include "dataentrymodel.h" + +DataEntryModel::DataEntryModel(QObject *parent) + : QAbstractListModel(parent) +{ + // initialize our data (QList) with a list of color names + m_data = QColor::colorNames(); +} + +DataEntryModel::~DataEntryModel() +{ +} + +int DataEntryModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + // return our data count + return m_data.count(); +} + +QVariant DataEntryModel::data(const QModelIndex &index, int role) const +{ + // the index returns the requested row and column information. + // we ignore the column and only use the row information + int row = index.row(); + + // boundary check for the row + if(row < 0 || row >= m_data.count()) { + return QVariant(); + } + + // A model can return data for different roles. + // The default role is the display role. + // it can be accesses in QML with "model.display" + switch(role) { + case Qt::DisplayRole: + // Return the color name for the particular row + // Qt automatically converts it to the QVariant type + return m_data.value(row); + } + + // The view asked for other data, just return an empty QVariant + return QVariant(); +} diff --git a/src/ui/dataentrymodel.h b/src/ui/dataentrymodel.h new file mode 100644 index 0000000..7747515 --- /dev/null +++ b/src/ui/dataentrymodel.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +class DataEntryModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit DataEntryModel(QObject *parent = 0); + ~DataEntryModel(); + +public: + virtual int rowCount(const QModelIndex &parent) const; + virtual QVariant data(const QModelIndex &index, int role) const; +private: + QList m_data; +}; + diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp deleted file mode 100644 index 0d9b619..0000000 --- a/src/ui/mainwindow.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "mainwindow.hpp" -#include "./ui_mainwindow.h" - -MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent) - , ui(new Ui::MainWindow) -{ - ui->setupUi(this); -} - -MainWindow::~MainWindow() -{ - delete ui; -} diff --git a/src/ui/mainwindow.hpp b/src/ui/mainwindow.hpp deleted file mode 100644 index 0a92801..0000000 --- a/src/ui/mainwindow.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include - -QT_BEGIN_NAMESPACE -namespace Ui { -class MainWindow; -} -QT_END_NAMESPACE - -class MainWindow : public QMainWindow -{ - Q_OBJECT - -public: - MainWindow(QWidget *parent = nullptr); - ~MainWindow(); - -private: - Ui::MainWindow *ui; -}; diff --git a/src/ui/mainwindow.ui b/src/ui/mainwindow.ui deleted file mode 100644 index 03943e0..0000000 --- a/src/ui/mainwindow.ui +++ /dev/null @@ -1,31 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 800 - 600 - - - - MainWindow - - - - - - 0 - 0 - 800 - 27 - - - - - - - - From 5a26880709d0e5be08964244a0f888a7ab0d4d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 20 Mar 2025 10:58:28 +0100 Subject: [PATCH 33/51] add file picker dialog --- src/qml/Application.qml | 23 +++-------------------- src/qml/File.qml | 6 +++--- src/qml/FileList.qml | 13 +++++++++---- src/qml/ImagePicker.qml | 18 ++++++++++++++++++ src/qml/Preview.qml | 4 ++-- src/qml/Properties.qml | 4 ++-- src/qml/resources.qrc | 1 + 7 files changed, 38 insertions(+), 31 deletions(-) create mode 100644 src/qml/ImagePicker.qml diff --git a/src/qml/Application.qml b/src/qml/Application.qml index 6b58568..af5a1ad 100644 --- a/src/qml/Application.qml +++ b/src/qml/Application.qml @@ -1,6 +1,6 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 ApplicationWindow { visible: true @@ -17,23 +17,6 @@ ApplicationWindow { Layout.preferredWidth: 1 } - ListModel { - id: fileModel - - ListElement { - filePath: "file1.txt" - } - - ListElement { - filePath: "file2.txt" - } - - ListElement { - filePath: "file3.txt" - } - - } - Preview { id: preview diff --git a/src/qml/File.qml b/src/qml/File.qml index 56d663a..36d5dcc 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -1,6 +1,6 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 Rectangle { id: fileItem diff --git a/src/qml/FileList.qml b/src/qml/FileList.qml index 155a470..b062299 100644 --- a/src/qml/FileList.qml +++ b/src/qml/FileList.qml @@ -1,6 +1,6 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 import printf 1.0 Item { @@ -16,8 +16,13 @@ Item { text: "Open" onClicked: { - fileHandler.openFile(model.display); + imagePicker.open(); } + + ImagePicker { + id: imagePicker + } + } ListView { diff --git a/src/qml/ImagePicker.qml b/src/qml/ImagePicker.qml new file mode 100644 index 0000000..814b5c8 --- /dev/null +++ b/src/qml/ImagePicker.qml @@ -0,0 +1,18 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Dialogs 6.8 + +FileDialog { + id: fileDialog + + title: "Select an Image" + nameFilters: ["Image files (*.png *.jpg *.jpeg)"] + fileMode: FileDialog.OpenFiles + + onAccepted: { + console.log("Selected file:", fileDialog.selectedFiles); + } + onRejected: { + console.log("File selection canceled"); + } +} diff --git a/src/qml/Preview.qml b/src/qml/Preview.qml index 8da5fe5..82556ba 100644 --- a/src/qml/Preview.qml +++ b/src/qml/Preview.qml @@ -1,5 +1,5 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 +import QtQuick 6.8 +import QtQuick.Controls 6.8 Item { Rectangle { diff --git a/src/qml/Properties.qml b/src/qml/Properties.qml index 10eb708..13f17ad 100644 --- a/src/qml/Properties.qml +++ b/src/qml/Properties.qml @@ -1,5 +1,5 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 +import QtQuick 6.8 +import QtQuick.Controls 6.8 Item { Button { diff --git a/src/qml/resources.qrc b/src/qml/resources.qrc index 1c56e77..d66385f 100644 --- a/src/qml/resources.qrc +++ b/src/qml/resources.qrc @@ -6,5 +6,6 @@ Properties.qml 3.png File.qml + ImagePicker.qml \ No newline at end of file From c51f37fe2d88ca1ebeebafc5fe15f79e7a5fcdab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 20 Mar 2025 11:22:16 +0100 Subject: [PATCH 34/51] a little better flickable --- src/qml/Preview.qml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/qml/Preview.qml b/src/qml/Preview.qml index 82556ba..fc365d2 100644 --- a/src/qml/Preview.qml +++ b/src/qml/Preview.qml @@ -3,8 +3,7 @@ import QtQuick.Controls 6.8 Item { Rectangle { - width: 10 - height: 10 + id: flickArea color: "#323232" anchors.fill: parent } @@ -19,8 +18,6 @@ Item { image.scale *= zoomFactor; else image.scale /= zoomFactor; - flickable.contentWidth = image.width * image.scale; - flickable.contentHeight = image.height * image.scale; } } @@ -28,8 +25,8 @@ Item { id: flickable anchors.fill: parent - contentWidth: image.width * image.scale - contentHeight: image.height * image.scale + contentWidth: Math.max(image.width * image.scale, flickArea.width) + contentHeight: Math.max(image.height * image.scale, flickArea.height) clip: true Image { From 7d054a8ff68ea0bc15dfd5c2cbd92cdd01ded1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 20 Mar 2025 16:56:49 +0100 Subject: [PATCH 35/51] actually loading files (somewhat) --- meson.build | 1 + src/img/image_source.hpp | 2 +- src/qml/File.qml | 4 +-- src/qml/FileList.qml | 4 +++ src/qml/ImagePicker.qml | 6 ++-- src/ui/dataentrymodel.cpp | 72 ++++++++++++++++++++++++++------------- src/ui/dataentrymodel.h | 32 +++++++++++++---- src/ui/inputfile.cpp | 43 +++++++++++++++++++++++ src/ui/inputfile.hpp | 24 +++++++++++++ 9 files changed, 152 insertions(+), 36 deletions(-) create mode 100644 src/ui/inputfile.cpp create mode 100644 src/ui/inputfile.hpp diff --git a/meson.build b/meson.build index 206a251..0c2af20 100644 --- a/meson.build +++ b/meson.build @@ -50,6 +50,7 @@ srcs = files( 'src/img/image_source.cpp', 'src/settings/document_preset.cpp', 'src/ui/dataentrymodel.cpp', + 'src/ui/inputfile.cpp', 'src/main.cpp', ) diff --git a/src/img/image_source.hpp b/src/img/image_source.hpp index 0fd6b64..39b8c79 100644 --- a/src/img/image_source.hpp +++ b/src/img/image_source.hpp @@ -19,7 +19,7 @@ class ImageSource : ICachableImage { public: ImageSource(cv::Mat source, size_t amount); - ~ImageSource() { clear_filters(); } + virtual ~ImageSource() { clear_filters(); } ImageSource(const ImageSource& other) : original(other.original), cached(*this), diff --git a/src/qml/File.qml b/src/qml/File.qml index 36d5dcc..80d1884 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -29,7 +29,7 @@ Rectangle { spacing: 10 Rectangle { - color: model.display + color: "orange" Layout.preferredWidth: 100 Layout.preferredHeight: 100 } @@ -64,7 +64,7 @@ Rectangle { width: parent.width Text { - text: model.display + text: model.name font.pixelSize: 16 Layout.fillWidth: true } diff --git a/src/qml/FileList.qml b/src/qml/FileList.qml index b062299..0bd1104 100644 --- a/src/qml/FileList.qml +++ b/src/qml/FileList.qml @@ -21,6 +21,9 @@ Item { ImagePicker { id: imagePicker + onAcceptDelegate: (files) => { + dataEntryModel.addFiles(files); + } } } @@ -34,6 +37,7 @@ Item { spacing: 10 model: DataEntryModel { + id: dataEntryModel } delegate: File { diff --git a/src/qml/ImagePicker.qml b/src/qml/ImagePicker.qml index 814b5c8..5a4726b 100644 --- a/src/qml/ImagePicker.qml +++ b/src/qml/ImagePicker.qml @@ -4,15 +4,17 @@ import QtQuick.Dialogs 6.8 FileDialog { id: fileDialog + property var onAcceptDelegate: (files) => console.log("Selected files:", files); + property var onRejectDelegate: () => console.log("File selection canceled"); title: "Select an Image" nameFilters: ["Image files (*.png *.jpg *.jpeg)"] fileMode: FileDialog.OpenFiles onAccepted: { - console.log("Selected file:", fileDialog.selectedFiles); + onAcceptDelegate(fileDialog.selectedFiles); } onRejected: { - console.log("File selection canceled"); + onRejectDelegate(); } } diff --git a/src/ui/dataentrymodel.cpp b/src/ui/dataentrymodel.cpp index ca87291..99ffca1 100644 --- a/src/ui/dataentrymodel.cpp +++ b/src/ui/dataentrymodel.cpp @@ -1,44 +1,68 @@ #include "dataentrymodel.h" -DataEntryModel::DataEntryModel(QObject *parent) - : QAbstractListModel(parent) -{ - // initialize our data (QList) with a list of color names - m_data = QColor::colorNames(); +DataEntryModel::DataEntryModel(QObject *parent) : QAbstractListModel(parent) { + m_roleNames[NameRole] = "name"; + m_roleNames[HueRole] = "hue"; + m_roleNames[SaturationRole] = "saturation"; + m_roleNames[BrightnessRole] = "brightness"; + + m_data = QList(); } -DataEntryModel::~DataEntryModel() -{ +DataEntryModel::~DataEntryModel() { + qDeleteAll(m_data); + m_data.clear(); } -int DataEntryModel::rowCount(const QModelIndex &parent) const -{ +QHash DataEntryModel::roleNames() const { return m_roleNames; } + +int DataEntryModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); - // return our data count return m_data.count(); } -QVariant DataEntryModel::data(const QModelIndex &index, int role) const -{ - // the index returns the requested row and column information. - // we ignore the column and only use the row information +QVariant DataEntryModel::data(const QModelIndex &index, int role) const { int row = index.row(); - // boundary check for the row - if(row < 0 || row >= m_data.count()) { + // oob check + if (row < 0 || row >= m_data.count()) { return QVariant(); } - // A model can return data for different roles. - // The default role is the display role. - // it can be accesses in QML with "model.display" - switch(role) { + // property access + switch (role) { case Qt::DisplayRole: - // Return the color name for the particular row - // Qt automatically converts it to the QVariant type - return m_data.value(row); + return QVariant::fromValue(QString::fromStdString(m_data.value(row)->get_file_name())); + case NameRole: + return QVariant::fromValue(QString::fromStdString(m_data.value(row)->get_file_name())); } - // The view asked for other data, just return an empty QVariant + // default return QVariant(); } + +void DataEntryModel::remove(int index) { + // oob check + if (index < 0 || index >= m_data.count()) { + return; + } + + beginRemoveRows(QModelIndex(), index, index); + delete m_data.takeAt(index); + endRemoveRows(); +} + +void DataEntryModel::clear() { + qDeleteAll(m_data); + m_data.clear(); +} + +void DataEntryModel::addFiles(const QStringList &files) { + for (const QString &file : files) { + auto *input_file = new InputFile(file.toStdString()); + + beginInsertRows(QModelIndex(), m_data.count(), m_data.count()); + m_data.append(input_file); + endInsertRows(); + } +} diff --git a/src/ui/dataentrymodel.h b/src/ui/dataentrymodel.h index 7747515..a4e8374 100644 --- a/src/ui/dataentrymodel.h +++ b/src/ui/dataentrymodel.h @@ -3,17 +3,35 @@ #include #include -class DataEntryModel : public QAbstractListModel -{ +#include "inputfile.hpp" + +class DataEntryModel : public QAbstractListModel { Q_OBJECT -public: + public: + enum RoleNames { + NameRole = Qt::UserRole + 0, + HueRole = Qt::UserRole + 1, + SaturationRole = Qt::UserRole + 2, + BrightnessRole = Qt::UserRole + 3 + }; + explicit DataEntryModel(QObject *parent = 0); ~DataEntryModel(); -public: + protected: + virtual QHash roleNames() const override; + + private: + // TODO: shared pointer + // so that deleting while its generating wont result in bugs + QList m_data; + QHash m_roleNames; + + public: virtual int rowCount(const QModelIndex &parent) const; virtual QVariant data(const QModelIndex &index, int role) const; -private: - QList m_data; -}; + Q_INVOKABLE void remove(int index); + Q_INVOKABLE void clear(); + Q_INVOKABLE void addFiles(const QStringList &files); +}; diff --git a/src/ui/inputfile.cpp b/src/ui/inputfile.cpp new file mode 100644 index 0000000..c2b4ec9 --- /dev/null +++ b/src/ui/inputfile.cpp @@ -0,0 +1,43 @@ +#include "inputfile.hpp" +#include +#include +#include + +InputFile::InputFile(const std::string& path) : file_path(path) { + if (file_path.rfind("file://", 0) == 0) { + file_path = file_path.substr(7); + } + + if (!std::filesystem::exists(file_path)) { + throw std::invalid_argument("File does not exist: " + file_path); + } + + image = cv::imread(file_path); + if (image.empty()) { + throw std::runtime_error("Failed to load image: " + file_path); + } + + thumbnail = cv::Mat(); +} + +std::string InputFile::get_file_name() const { + return std::filesystem::path(file_path).filename().string(); +} + +std::string InputFile::get_file_path() const { + return file_path; +} + +std::pair InputFile::get_image_size() const { + return std::make_pair(image.cols, image.rows); +} + +cv::Mat InputFile::get_thumbnail(int width, int height) const { + // regenerate thumbnail if size is different + if (thumbnail.cols != width || thumbnail.rows != height) { + cv::resize(image, thumbnail, cv::Size(width, height)); + } + return thumbnail; +} + +cv::Mat InputFile::get_image() const { return image; } diff --git a/src/ui/inputfile.hpp b/src/ui/inputfile.hpp new file mode 100644 index 0000000..dcedccb --- /dev/null +++ b/src/ui/inputfile.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +class InputFile { +private: + std::string file_path; + cv::Mat thumbnail; + cv::Mat image; + +public: + explicit InputFile(const std::string& path); + + std::string get_file_name() const; + + std::string get_file_path() const; + + std::pair get_image_size() const; + + cv::Mat get_thumbnail(int width, int height) const; + + cv::Mat get_image() const; +}; \ No newline at end of file From e01dbbb83f0138693ecabe7240da6132f12cc53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 20 Mar 2025 21:31:16 +0100 Subject: [PATCH 36/51] model properties --- src/qml/File.qml | 35 ++++++++++++++++++++++++++++------- src/qml/FileList.qml | 1 + src/ui/dataentrymodel.cpp | 38 ++++++++++++++++++++++++++++++++------ src/ui/dataentrymodel.h | 7 ++++--- src/ui/inputfile.cpp | 17 ++++++----------- src/ui/inputfile.hpp | 8 +++++--- 6 files changed, 76 insertions(+), 30 deletions(-) diff --git a/src/qml/File.qml b/src/qml/File.qml index 80d1884..9c1d73e 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -5,6 +5,8 @@ import QtQuick.Layouts 6.8 Rectangle { id: fileItem + property var dataModel: null + border.color: "black" border.width: 1 radius: 5 @@ -28,15 +30,21 @@ Rectangle { width: parent.width spacing: 10 - Rectangle { - color: "orange" - Layout.preferredWidth: 100 + Image { + id: image + source: "file://" + model.path + + fillMode: Image.PreserveAspectFit + + Layout.fillWidth: true + Layout.preferredWidth: 1 Layout.preferredHeight: 100 } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true + Layout.preferredWidth: 4 spacing: 10 RowLayout { @@ -44,9 +52,10 @@ Rectangle { width: parent.width Text { - text: "path" + text: model.path font.pixelSize: 16 Layout.fillWidth: true + clip: true } Button { @@ -55,6 +64,9 @@ Rectangle { Layout.preferredWidth: 30 Layout.preferredHeight: 30 text: "X" + onClicked: { + dataModel.remove(model.index) + } } } @@ -67,21 +79,30 @@ Rectangle { text: model.name font.pixelSize: 16 Layout.fillWidth: true + clip: true } SpinBox { id: spinbox Layout.preferredWidth: 60 - value: 0 - from: 0 + value: model.amount + from: 1 + to: 1000 stepSize: 1 + editable: true + + onValueChanged: () => { + if (model.amount != spinbox.value){ + model.amount = spinbox.value; + } + } } } Text { - text: "extra info about the file" + text: model.imageSize.width + "x" + model.imageSize.height font.pixelSize: 16 } diff --git a/src/qml/FileList.qml b/src/qml/FileList.qml index 0bd1104..1254d50 100644 --- a/src/qml/FileList.qml +++ b/src/qml/FileList.qml @@ -41,6 +41,7 @@ Item { } delegate: File { + dataModel: dataEntryModel width: filePanel.width } diff --git a/src/ui/dataentrymodel.cpp b/src/ui/dataentrymodel.cpp index 99ffca1..33bd324 100644 --- a/src/ui/dataentrymodel.cpp +++ b/src/ui/dataentrymodel.cpp @@ -2,9 +2,9 @@ DataEntryModel::DataEntryModel(QObject *parent) : QAbstractListModel(parent) { m_roleNames[NameRole] = "name"; - m_roleNames[HueRole] = "hue"; - m_roleNames[SaturationRole] = "saturation"; - m_roleNames[BrightnessRole] = "brightness"; + m_roleNames[PathRole] = "path"; + m_roleNames[ImageSizeRole] = "imageSize"; + m_roleNames[AmountRole] = "amount"; m_data = QList(); } @@ -32,13 +32,39 @@ QVariant DataEntryModel::data(const QModelIndex &index, int role) const { // property access switch (role) { case Qt::DisplayRole: - return QVariant::fromValue(QString::fromStdString(m_data.value(row)->get_file_name())); case NameRole: return QVariant::fromValue(QString::fromStdString(m_data.value(row)->get_file_name())); + case PathRole: + return QVariant::fromValue(QString::fromStdString(m_data.value(row)->get_file_path())); + case ImageSizeRole: { + auto [width, height] = m_data.value(row)->get_image_size(); + return QVariant::fromValue(QSize(width, height)); + } + case AmountRole: + return QVariant::fromValue(m_data.value(row)->get_amount()); + default: + return QVariant(); + } +} + +bool DataEntryModel::setData(const QModelIndex &index, const QVariant &value, int role) { + int row = index.row(); + + // oob check + if (row < 0 || row >= m_data.count()) { + return false; + } + + switch (role) { + case AmountRole: + m_data.value(row)->set_amount(value.toInt()); + break; + default: + return false; } - // default - return QVariant(); + emit dataChanged(index, index, {role}); + return true; } void DataEntryModel::remove(int index) { diff --git a/src/ui/dataentrymodel.h b/src/ui/dataentrymodel.h index a4e8374..31f5408 100644 --- a/src/ui/dataentrymodel.h +++ b/src/ui/dataentrymodel.h @@ -10,9 +10,9 @@ class DataEntryModel : public QAbstractListModel { public: enum RoleNames { NameRole = Qt::UserRole + 0, - HueRole = Qt::UserRole + 1, - SaturationRole = Qt::UserRole + 2, - BrightnessRole = Qt::UserRole + 3 + PathRole = Qt::UserRole + 1, + ImageSizeRole = Qt::UserRole + 2, + AmountRole = Qt::UserRole + 3 }; explicit DataEntryModel(QObject *parent = 0); @@ -30,6 +30,7 @@ class DataEntryModel : public QAbstractListModel { public: virtual int rowCount(const QModelIndex &parent) const; virtual QVariant data(const QModelIndex &index, int role) const; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role); Q_INVOKABLE void remove(int index); Q_INVOKABLE void clear(); diff --git a/src/ui/inputfile.cpp b/src/ui/inputfile.cpp index c2b4ec9..9e96010 100644 --- a/src/ui/inputfile.cpp +++ b/src/ui/inputfile.cpp @@ -3,7 +3,8 @@ #include #include -InputFile::InputFile(const std::string& path) : file_path(path) { +// TODO: dont hardcode the initial amount +InputFile::InputFile(const std::string& path) : file_path(path), amount(20) { if (file_path.rfind("file://", 0) == 0) { file_path = file_path.substr(7); } @@ -16,8 +17,6 @@ InputFile::InputFile(const std::string& path) : file_path(path) { if (image.empty()) { throw std::runtime_error("Failed to load image: " + file_path); } - - thumbnail = cv::Mat(); } std::string InputFile::get_file_name() const { @@ -32,12 +31,8 @@ std::pair InputFile::get_image_size() const { return std::make_pair(image.cols, image.rows); } -cv::Mat InputFile::get_thumbnail(int width, int height) const { - // regenerate thumbnail if size is different - if (thumbnail.cols != width || thumbnail.rows != height) { - cv::resize(image, thumbnail, cv::Size(width, height)); - } - return thumbnail; -} - cv::Mat InputFile::get_image() const { return image; } + +int InputFile::get_amount() const { return amount; } + +void InputFile::set_amount(int amount) { this->amount = amount; } \ No newline at end of file diff --git a/src/ui/inputfile.hpp b/src/ui/inputfile.hpp index dcedccb..a47f997 100644 --- a/src/ui/inputfile.hpp +++ b/src/ui/inputfile.hpp @@ -6,8 +6,8 @@ class InputFile { private: std::string file_path; - cv::Mat thumbnail; cv::Mat image; + int amount; public: explicit InputFile(const std::string& path); @@ -18,7 +18,9 @@ class InputFile { std::pair get_image_size() const; - cv::Mat get_thumbnail(int width, int height) const; - cv::Mat get_image() const; + + int get_amount() const; + + void set_amount(int amount); }; \ No newline at end of file From 4198a53bbc361b31d7c150cf1d182336e78626d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 26 Mar 2025 10:56:21 +0100 Subject: [PATCH 37/51] use system palette --- src/qml/File.qml | 24 +++++++++++++++--------- src/qml/Preview.qml | 4 +++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/qml/File.qml b/src/qml/File.qml index 9c1d73e..2179b4e 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -7,11 +7,16 @@ Rectangle { property var dataModel: null - border.color: "black" - border.width: 1 + color: palette.base radius: 5 implicitHeight: paddingCol.implicitHeight + 20 // FIXME: hack + SystemPalette { + id: palette + + colorGroup: SystemPalette.Active + } + Item { id: container @@ -32,11 +37,10 @@ Rectangle { Image { id: image + source: "file://" + model.path - fillMode: Image.PreserveAspectFit - - Layout.fillWidth: true + Layout.fillWidth: true Layout.preferredWidth: 1 Layout.preferredHeight: 100 } @@ -52,6 +56,7 @@ Rectangle { width: parent.width Text { + color: palette.text text: model.path font.pixelSize: 16 Layout.fillWidth: true @@ -65,7 +70,7 @@ Rectangle { Layout.preferredHeight: 30 text: "X" onClicked: { - dataModel.remove(model.index) + dataModel.remove(model.index); } } @@ -76,6 +81,7 @@ Rectangle { width: parent.width Text { + color: palette.text text: model.name font.pixelSize: 16 Layout.fillWidth: true @@ -91,17 +97,17 @@ Rectangle { to: 1000 stepSize: 1 editable: true - onValueChanged: () => { - if (model.amount != spinbox.value){ + if (model.amount != spinbox.value) model.amount = spinbox.value; - } + } } } Text { + color: palette.text text: model.imageSize.width + "x" + model.imageSize.height font.pixelSize: 16 } diff --git a/src/qml/Preview.qml b/src/qml/Preview.qml index fc365d2..01fa76b 100644 --- a/src/qml/Preview.qml +++ b/src/qml/Preview.qml @@ -2,9 +2,11 @@ import QtQuick 6.8 import QtQuick.Controls 6.8 Item { + SystemPalette { id: palette; colorGroup: SystemPalette.Active } + Rectangle { id: flickArea - color: "#323232" + color: palette.dark anchors.fill: parent } From bb18c0ee259258ffa3eaa23ecded9961aee8d1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 26 Mar 2025 11:52:57 +0100 Subject: [PATCH 38/51] view uml proto and renamings --- meson.build | 6 +- src/main.cpp | 2 +- .../{inputfile.cpp => image_source_view.cpp} | 16 ++--- .../{inputfile.hpp => image_source_view.hpp} | 4 +- ...taentrymodel.cpp => source_entry_view.cpp} | 6 +- ...dataentrymodel.h => source_entry_view.hpp} | 4 +- uml/ui.puml | 66 +++++++++++++++++++ 7 files changed, 85 insertions(+), 19 deletions(-) rename src/ui/{inputfile.cpp => image_source_view.cpp} (56%) rename src/ui/{inputfile.hpp => image_source_view.hpp} (82%) rename src/ui/{dataentrymodel.cpp => source_entry_view.cpp} (94%) rename src/ui/{dataentrymodel.h => source_entry_view.hpp} (93%) create mode 100644 uml/ui.puml diff --git a/meson.build b/meson.build index 0c2af20..22a6ea8 100644 --- a/meson.build +++ b/meson.build @@ -15,7 +15,7 @@ qt6 = import('qt6') ui_files = files() -moc_headers = files('src/ui/dataentrymodel.h') +moc_headers = files('src/ui/source_entry_view.hpp') qresources = files('src/qml/resources.qrc') @@ -49,8 +49,8 @@ srcs = files( 'src/img/cached_image.cpp', 'src/img/image_source.cpp', 'src/settings/document_preset.cpp', - 'src/ui/dataentrymodel.cpp', - 'src/ui/inputfile.cpp', + 'src/ui/source_entry_view.cpp', + 'src/ui/image_source_view.cpp', 'src/main.cpp', ) diff --git a/src/main.cpp b/src/main.cpp index 34eef37..93fa4da 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,7 @@ #include #include -#include "dataentrymodel.h" +#include "source_entry_view.hpp" int main(int argc, char *argv[]) { diff --git a/src/ui/inputfile.cpp b/src/ui/image_source_view.cpp similarity index 56% rename from src/ui/inputfile.cpp rename to src/ui/image_source_view.cpp index 9e96010..c85c2be 100644 --- a/src/ui/inputfile.cpp +++ b/src/ui/image_source_view.cpp @@ -1,10 +1,10 @@ -#include "inputfile.hpp" +#include "image_source_view.hpp" #include #include #include // TODO: dont hardcode the initial amount -InputFile::InputFile(const std::string& path) : file_path(path), amount(20) { +ImageSourceView::ImageSourceView(const std::string& path) : file_path(path), amount(20) { if (file_path.rfind("file://", 0) == 0) { file_path = file_path.substr(7); } @@ -19,20 +19,20 @@ InputFile::InputFile(const std::string& path) : file_path(path), amount(20) { } } -std::string InputFile::get_file_name() const { +std::string ImageSourceView::get_file_name() const { return std::filesystem::path(file_path).filename().string(); } -std::string InputFile::get_file_path() const { +std::string ImageSourceView::get_file_path() const { return file_path; } -std::pair InputFile::get_image_size() const { +std::pair ImageSourceView::get_image_size() const { return std::make_pair(image.cols, image.rows); } -cv::Mat InputFile::get_image() const { return image; } +cv::Mat ImageSourceView::get_image() const { return image; } -int InputFile::get_amount() const { return amount; } +int ImageSourceView::get_amount() const { return amount; } -void InputFile::set_amount(int amount) { this->amount = amount; } \ No newline at end of file +void ImageSourceView::set_amount(int amount) { this->amount = amount; } \ No newline at end of file diff --git a/src/ui/inputfile.hpp b/src/ui/image_source_view.hpp similarity index 82% rename from src/ui/inputfile.hpp rename to src/ui/image_source_view.hpp index a47f997..34c13f7 100644 --- a/src/ui/inputfile.hpp +++ b/src/ui/image_source_view.hpp @@ -3,14 +3,14 @@ #include #include -class InputFile { +class ImageSourceView { private: std::string file_path; cv::Mat image; int amount; public: - explicit InputFile(const std::string& path); + explicit ImageSourceView(const std::string& path); std::string get_file_name() const; diff --git a/src/ui/dataentrymodel.cpp b/src/ui/source_entry_view.cpp similarity index 94% rename from src/ui/dataentrymodel.cpp rename to src/ui/source_entry_view.cpp index 33bd324..a6ebbca 100644 --- a/src/ui/dataentrymodel.cpp +++ b/src/ui/source_entry_view.cpp @@ -1,4 +1,4 @@ -#include "dataentrymodel.h" +#include "source_entry_view.hpp" DataEntryModel::DataEntryModel(QObject *parent) : QAbstractListModel(parent) { m_roleNames[NameRole] = "name"; @@ -6,7 +6,7 @@ DataEntryModel::DataEntryModel(QObject *parent) : QAbstractListModel(parent) { m_roleNames[ImageSizeRole] = "imageSize"; m_roleNames[AmountRole] = "amount"; - m_data = QList(); + m_data = QList(); } DataEntryModel::~DataEntryModel() { @@ -85,7 +85,7 @@ void DataEntryModel::clear() { void DataEntryModel::addFiles(const QStringList &files) { for (const QString &file : files) { - auto *input_file = new InputFile(file.toStdString()); + auto *input_file = new ImageSourceView(file.toStdString()); beginInsertRows(QModelIndex(), m_data.count(), m_data.count()); m_data.append(input_file); diff --git a/src/ui/dataentrymodel.h b/src/ui/source_entry_view.hpp similarity index 93% rename from src/ui/dataentrymodel.h rename to src/ui/source_entry_view.hpp index 31f5408..f1cd553 100644 --- a/src/ui/dataentrymodel.h +++ b/src/ui/source_entry_view.hpp @@ -3,7 +3,7 @@ #include #include -#include "inputfile.hpp" +#include "image_source_view.hpp" class DataEntryModel : public QAbstractListModel { Q_OBJECT @@ -24,7 +24,7 @@ class DataEntryModel : public QAbstractListModel { private: // TODO: shared pointer // so that deleting while its generating wont result in bugs - QList m_data; + QList m_data; QHash m_roleNames; public: diff --git a/uml/ui.puml b/uml/ui.puml new file mode 100644 index 0000000..22c8d18 --- /dev/null +++ b/uml/ui.puml @@ -0,0 +1,66 @@ +@startuml ui + +title UI + +entity Application { +} + + +class SourceEntryView { + void remove(index) + void addFiles(files) + void clear() +} +Application *- SourceEntryView + +class ImageSourceView { + ImageSource* get_imagesource() + void load_from_json(json) +} +SourceEntryView *-- "*" ImageSourceView + +abstract FilterView { + Filter* get_filter() + void load_from_json(json) +} +ImageSourceView *-- "*" FilterView + +class MaskView { +} +FilterView <|-- MaskView + +class RotateView { +} +FilterView <|-- RotateView + +class SizeView { +} +FilterView <|-- SizeView + + +class DocumentPresetView { + DocumentPreset* get_preset() + void load_from_json(json) +} +DocumentPresetView --* Application + + +class ControlView { + void generate() + void save() + void print() +} +ControlView -* Application + +class PreView{ + void set_image(cv::Mat) +} +PreView --* Application + + +abstract Tiling { + cv::Mat generate(...) +} +ControlView --> Tiling + +@enduml \ No newline at end of file From 2b5417c461cc71c0cc7fceeb6572ebdf2b7d5a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 26 Mar 2025 12:00:59 +0100 Subject: [PATCH 39/51] renaming in ui --- src/main.cpp | 2 +- src/qml/Application.qml | 2 +- src/qml/FileList.qml | 8 ++++---- src/ui/source_entry_view.cpp | 18 +++++++++--------- src/ui/source_entry_view.hpp | 6 +++--- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 93fa4da..e2ed120 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,7 +7,7 @@ int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); - qmlRegisterType("printf", 1, 0, "DataEntryModel"); + qmlRegisterType("printf", 1, 0, "SourceEntryView"); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/qml/Application.qml"))); diff --git a/src/qml/Application.qml b/src/qml/Application.qml index af5a1ad..6ff822c 100644 --- a/src/qml/Application.qml +++ b/src/qml/Application.qml @@ -6,7 +6,7 @@ ApplicationWindow { visible: true width: 640 height: 480 - title: "Example QML Window" + title: "printf" RowLayout { anchors.fill: parent diff --git a/src/qml/FileList.qml b/src/qml/FileList.qml index 1254d50..6ebe51a 100644 --- a/src/qml/FileList.qml +++ b/src/qml/FileList.qml @@ -22,7 +22,7 @@ Item { ImagePicker { id: imagePicker onAcceptDelegate: (files) => { - dataEntryModel.addFiles(files); + sourceEntryView.addFiles(files); } } @@ -36,12 +36,12 @@ Item { clip: true spacing: 10 - model: DataEntryModel { - id: dataEntryModel + model: SourceEntryView { + id: sourceEntryView } delegate: File { - dataModel: dataEntryModel + dataModel: sourceEntryView width: filePanel.width } diff --git a/src/ui/source_entry_view.cpp b/src/ui/source_entry_view.cpp index a6ebbca..36cf1a5 100644 --- a/src/ui/source_entry_view.cpp +++ b/src/ui/source_entry_view.cpp @@ -1,6 +1,6 @@ #include "source_entry_view.hpp" -DataEntryModel::DataEntryModel(QObject *parent) : QAbstractListModel(parent) { +SourceEntryView::SourceEntryView(QObject *parent) : QAbstractListModel(parent) { m_roleNames[NameRole] = "name"; m_roleNames[PathRole] = "path"; m_roleNames[ImageSizeRole] = "imageSize"; @@ -9,19 +9,19 @@ DataEntryModel::DataEntryModel(QObject *parent) : QAbstractListModel(parent) { m_data = QList(); } -DataEntryModel::~DataEntryModel() { +SourceEntryView::~SourceEntryView() { qDeleteAll(m_data); m_data.clear(); } -QHash DataEntryModel::roleNames() const { return m_roleNames; } +QHash SourceEntryView::roleNames() const { return m_roleNames; } -int DataEntryModel::rowCount(const QModelIndex &parent) const { +int SourceEntryView::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_data.count(); } -QVariant DataEntryModel::data(const QModelIndex &index, int role) const { +QVariant SourceEntryView::data(const QModelIndex &index, int role) const { int row = index.row(); // oob check @@ -47,7 +47,7 @@ QVariant DataEntryModel::data(const QModelIndex &index, int role) const { } } -bool DataEntryModel::setData(const QModelIndex &index, const QVariant &value, int role) { +bool SourceEntryView::setData(const QModelIndex &index, const QVariant &value, int role) { int row = index.row(); // oob check @@ -67,7 +67,7 @@ bool DataEntryModel::setData(const QModelIndex &index, const QVariant &value, in return true; } -void DataEntryModel::remove(int index) { +void SourceEntryView::remove(int index) { // oob check if (index < 0 || index >= m_data.count()) { return; @@ -78,12 +78,12 @@ void DataEntryModel::remove(int index) { endRemoveRows(); } -void DataEntryModel::clear() { +void SourceEntryView::clear() { qDeleteAll(m_data); m_data.clear(); } -void DataEntryModel::addFiles(const QStringList &files) { +void SourceEntryView::addFiles(const QStringList &files) { for (const QString &file : files) { auto *input_file = new ImageSourceView(file.toStdString()); diff --git a/src/ui/source_entry_view.hpp b/src/ui/source_entry_view.hpp index f1cd553..e2bd7d9 100644 --- a/src/ui/source_entry_view.hpp +++ b/src/ui/source_entry_view.hpp @@ -5,7 +5,7 @@ #include "image_source_view.hpp" -class DataEntryModel : public QAbstractListModel { +class SourceEntryView : public QAbstractListModel { Q_OBJECT public: enum RoleNames { @@ -15,8 +15,8 @@ class DataEntryModel : public QAbstractListModel { AmountRole = Qt::UserRole + 3 }; - explicit DataEntryModel(QObject *parent = 0); - ~DataEntryModel(); + explicit SourceEntryView(QObject *parent = 0); + ~SourceEntryView(); protected: virtual QHash roleNames() const override; From 2d22cf4042ea5e4f7e9298d3251c21b06a0858b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 26 Mar 2025 13:31:18 +0100 Subject: [PATCH 40/51] file input ui stuff --- src/qml/File.qml | 75 ++++++++++++++++++++++++++++++++++++------- src/qml/UnitInput.qml | 42 ++++++++++++++++++++++++ src/qml/resources.qrc | 1 + uml/ui.puml | 2 ++ 4 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 src/qml/UnitInput.qml diff --git a/src/qml/File.qml b/src/qml/File.qml index 2179b4e..6530206 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -20,6 +20,7 @@ Rectangle { Item { id: container + clip: true anchors.fill: parent anchors.margins: 10 @@ -47,18 +48,17 @@ Rectangle { ColumnLayout { Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredWidth: 4 - spacing: 10 + Layout.preferredWidth: 3 RowLayout { spacing: 10 - width: parent.width + Layout.fillWidth: true Text { color: palette.text - text: model.path + text: model.name font.pixelSize: 16 + font.bold: true Layout.fillWidth: true clip: true } @@ -76,14 +76,22 @@ Rectangle { } + Text { + Layout.fillWidth: true + color: palette.text + text: model.path + font.pixelSize: 12 + clip: true + } + RowLayout { spacing: 10 - width: parent.width + Layout.fillWidth: true Text { color: palette.text - text: model.name - font.pixelSize: 16 + text: model.imageSize.width + "x" + model.imageSize.height + font.pixelSize: 12 Layout.fillWidth: true clip: true } @@ -106,10 +114,53 @@ Rectangle { } - Text { - color: palette.text - text: model.imageSize.width + "x" + model.imageSize.height - font.pixelSize: 16 + } + + } + + ComboBox { + id: comboBox + + width: parent.width + textRole: "text" + onCurrentIndexChanged: { + } + + model: ListModel { + ListElement { + text: "none" + } + + ListElement { + text: "idk" + } + + } + + } + + GroupBox { + title: "Size" + width: parent.width + + ColumnLayout { + spacing: 10 + width: parent.width + + UnitInput { + id: widthSpinbox + + label.text: "Width" + unit.text: "mm" + Layout.fillWidth: true + } + + UnitInput { + id: heightSpinbox + + label.text: "Height" + unit.text: "mm" + Layout.fillWidth: true } } diff --git a/src/qml/UnitInput.qml b/src/qml/UnitInput.qml new file mode 100644 index 0000000..e4b97b4 --- /dev/null +++ b/src/qml/UnitInput.qml @@ -0,0 +1,42 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 + +RowLayout { + property alias unit: unit + property alias label: label + property alias spinbox: spinbox + + spacing: 10 + + Text { + id: label + + color: palette.text + font.pixelSize: 12 + Layout.fillWidth: true + clip: true + } + + SpinBox { + id: spinbox + + Layout.preferredWidth: 60 + value: 1 + from: 1 + to: 1000 + stepSize: 1 + editable: true + } + + Text { + id: unit + + color: palette.text + text: unit + font.pixelSize: 12 + //Layout.fillWidth: true + clip: true + } + +} diff --git a/src/qml/resources.qrc b/src/qml/resources.qrc index d66385f..f4d8d1d 100644 --- a/src/qml/resources.qrc +++ b/src/qml/resources.qrc @@ -7,5 +7,6 @@ 3.png File.qml ImagePicker.qml + UnitInput.qml \ No newline at end of file diff --git a/uml/ui.puml b/uml/ui.puml index 22c8d18..6fdcbfa 100644 --- a/uml/ui.puml +++ b/uml/ui.puml @@ -16,6 +16,7 @@ Application *- SourceEntryView class ImageSourceView { ImageSource* get_imagesource() void load_from_json(json) + std::map propbe_presets() } SourceEntryView *-- "*" ImageSourceView @@ -41,6 +42,7 @@ FilterView <|-- SizeView class DocumentPresetView { DocumentPreset* get_preset() void load_from_json(json) + std::map propbe_presets() } DocumentPresetView --* Application From fa8a1a8cea4134c6540fa5be255a8b72ddee5a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Fri, 28 Mar 2025 20:38:28 +0100 Subject: [PATCH 41/51] fancy size input, photoshop style --- src/qml/File.qml | 22 +++--------------- src/qml/SizeInput.qml | 54 +++++++++++++++++++++++++++++++++++++++++++ src/qml/UnitInput.qml | 1 - src/qml/resources.qrc | 1 + 4 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 src/qml/SizeInput.qml diff --git a/src/qml/File.qml b/src/qml/File.qml index 6530206..f7b4a6a 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -143,26 +143,10 @@ Rectangle { title: "Size" width: parent.width - ColumnLayout { - spacing: 10 - width: parent.width - - UnitInput { - id: widthSpinbox - - label.text: "Width" - unit.text: "mm" - Layout.fillWidth: true - } - - UnitInput { - id: heightSpinbox - - label.text: "Height" - unit.text: "mm" - Layout.fillWidth: true - } + SizeInput { + id: sizeInput + width: parent.width } } diff --git a/src/qml/SizeInput.qml b/src/qml/SizeInput.qml new file mode 100644 index 0000000..b393b7a --- /dev/null +++ b/src/qml/SizeInput.qml @@ -0,0 +1,54 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 + +RowLayout { + CheckBox { + + Layout.fillHeight: true + + id: checkBox + + checked: true + clip: true + + Rectangle { + color: "transparent" + border.color: palette.midlight + border.width: 2 + + width: parent.width + height: parent.height * 0.6 + + x: parent.width / 2 + + anchors.verticalCenter: parent.verticalCenter + + radius: 5 + } + } + + ColumnLayout { + spacing: 10 + + UnitInput { + id: widthSpinbox + + Layout.alignment: Qt.AlignRight + label.text: "Width" + unit.text: "mm" + Layout.fillWidth: true + } + + UnitInput { + id: heightSpinbox + + Layout.alignment: Qt.AlignRight + label.text: "Height" + unit.text: "mm" + Layout.fillWidth: true + } + + } + +} diff --git a/src/qml/UnitInput.qml b/src/qml/UnitInput.qml index e4b97b4..37da5e8 100644 --- a/src/qml/UnitInput.qml +++ b/src/qml/UnitInput.qml @@ -14,7 +14,6 @@ RowLayout { color: palette.text font.pixelSize: 12 - Layout.fillWidth: true clip: true } diff --git a/src/qml/resources.qrc b/src/qml/resources.qrc index f4d8d1d..1814448 100644 --- a/src/qml/resources.qrc +++ b/src/qml/resources.qrc @@ -8,5 +8,6 @@ File.qml ImagePicker.qml UnitInput.qml + SizeInput.qml \ No newline at end of file From ba088d232df5b662aa6c528056c426ce7e5bd083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Fri, 28 Mar 2025 21:56:20 +0100 Subject: [PATCH 42/51] mask ui preview --- src/qml/File.qml | 34 +++++++++++++++------- src/qml/MaskInput.qml | 66 +++++++++++++++++++++++++++++++++++++++++++ src/qml/resources.qrc | 1 + 3 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 src/qml/MaskInput.qml diff --git a/src/qml/File.qml b/src/qml/File.qml index f7b4a6a..6a64c23 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -122,17 +122,13 @@ Rectangle { id: comboBox width: parent.width - textRole: "text" + textRole: "text" // ? onCurrentIndexChanged: { } model: ListModel { ListElement { - text: "none" - } - - ListElement { - text: "idk" + text: "todo" } } @@ -140,13 +136,31 @@ Rectangle { } GroupBox { - title: "Size" width: parent.width - SizeInput { - id: sizeInput - + Column { width: parent.width + spacing: 10 + + SizeInput { + id: sizeInput + + width: parent.width + } + + CheckBox { + id: guidesCheckBox + + checked: true + text: "Guides" + } + + MaskInput { + id: maskInput + + width: parent.width + } + } } diff --git a/src/qml/MaskInput.qml b/src/qml/MaskInput.qml new file mode 100644 index 0000000..0b5cd11 --- /dev/null +++ b/src/qml/MaskInput.qml @@ -0,0 +1,66 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 + +Column { + CheckBox { + id: maskCheckBox + + checked: false + text: "Mask" + } + + GroupBox { + visible: maskCheckBox.checked + width: parent.width + + RowLayout { + id: maskLayout + + width: parent.width + spacing: 10 + + Image { + id: maskImage + + source: "file://" + model.path + fillMode: Image.PreserveAspectFit + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.preferredHeight: 50 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 3 + + ComboBox { + width: parent.width + textRole: "text" // ? + onCurrentIndexChanged: { + } + + model: ListModel { + ListElement { + text: "todo" + } + + } + + } + + Text { + color: palette.text + text: model.name + font.pixelSize: 12 + Layout.fillWidth: true + clip: true + } + + } + + } + + } + +} diff --git a/src/qml/resources.qrc b/src/qml/resources.qrc index 1814448..8f3896e 100644 --- a/src/qml/resources.qrc +++ b/src/qml/resources.qrc @@ -9,5 +9,6 @@ ImagePicker.qml UnitInput.qml SizeInput.qml + MaskInput.qml \ No newline at end of file From a1ba608c08065ac09bb6b8b1f0b9ca07dbbfdcbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 3 Apr 2025 16:08:20 +0200 Subject: [PATCH 43/51] json probe --- presets/image/a3.json | 6 ++++++ src/util/jsonprobe.cpp | 30 ++++++++++++++++++++++++++++++ src/util/jsonprobe.hpp | 11 +++++++++++ 3 files changed, 47 insertions(+) create mode 100644 presets/image/a3.json create mode 100644 src/util/jsonprobe.cpp create mode 100644 src/util/jsonprobe.hpp diff --git a/presets/image/a3.json b/presets/image/a3.json new file mode 100644 index 0000000..d9a774f --- /dev/null +++ b/presets/image/a3.json @@ -0,0 +1,6 @@ +{ + "name": "A3", + "width": 297, + "height": 420, + "suggested_resolution": 300 +} \ No newline at end of file diff --git a/src/util/jsonprobe.cpp b/src/util/jsonprobe.cpp new file mode 100644 index 0000000..daad285 --- /dev/null +++ b/src/util/jsonprobe.cpp @@ -0,0 +1,30 @@ +#include "jsonprobe.hpp" +#include +#include "nlohmann/json.hpp" + +using json = nlohmann::json; + +ProbeList jsonprobe::probe_presets(const std::string& preset_dir_path, const std::string& display, const std::string& extension) { + std::filesystem::path preset_dir(preset_dir_path); // TODO: make this configurable + + ProbeList presets; // TODO: shared pointer + + if (std::filesystem::exists(preset_dir) && std::filesystem::is_directory(preset_dir)) { + for (const auto& entry : std::filesystem::directory_iterator(preset_dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + json json_data; + std::ifstream file(entry.path()); + json_data = json::parse(file); + file.close(); + if (json_data.contains("name")) { + presets.push_back({json_data["name"], entry.path().string()}); + } + } + } + } + else { + // TODO: handle error + } + + return presets; +} \ No newline at end of file diff --git a/src/util/jsonprobe.hpp b/src/util/jsonprobe.hpp new file mode 100644 index 0000000..9ff56b9 --- /dev/null +++ b/src/util/jsonprobe.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +typedef std::vector> ProbeList; + +namespace jsonprobe +{ + ProbeList probe_presets(const std::string& preset_dir_path, const std::string& display, const std::string& extension = ".json"); +} From d5a53ff8623d87966efb0484662b1a1cc93e0eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 3 Apr 2025 20:39:22 +0200 Subject: [PATCH 44/51] preset dropdown stuff --- meson.build | 7 ++++- src/main.cpp | 3 ++ src/qml/File.qml | 16 +++++------ src/qml/FileList.qml | 5 ++++ src/ui/image_preset_view.cpp | 56 ++++++++++++++++++++++++++++++++++++ src/ui/image_preset_view.hpp | 31 ++++++++++++++++++++ src/util/jsonprobe.cpp | 3 ++ 7 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 src/ui/image_preset_view.cpp create mode 100644 src/ui/image_preset_view.hpp diff --git a/meson.build b/meson.build index 22a6ea8..2402948 100644 --- a/meson.build +++ b/meson.build @@ -15,7 +15,10 @@ qt6 = import('qt6') ui_files = files() -moc_headers = files('src/ui/source_entry_view.hpp') +moc_headers = files( + 'src/ui/source_entry_view.hpp', + 'src/ui/image_preset_view.hpp', +) qresources = files('src/qml/resources.qrc') @@ -51,6 +54,8 @@ srcs = files( 'src/settings/document_preset.cpp', 'src/ui/source_entry_view.cpp', 'src/ui/image_source_view.cpp', + 'src/ui/image_preset_view.cpp', + 'src/util/jsonprobe.cpp', 'src/main.cpp', ) diff --git a/src/main.cpp b/src/main.cpp index e2ed120..ddfd918 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,12 +2,15 @@ #include #include "source_entry_view.hpp" +#include "image_preset_view.hpp" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); qmlRegisterType("printf", 1, 0, "SourceEntryView"); + qmlRegisterType("printf", 1, 0, "ImagePresetView"); + QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/qml/Application.qml"))); diff --git a/src/qml/File.qml b/src/qml/File.qml index 6a64c23..9829cda 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -6,6 +6,7 @@ Rectangle { id: fileItem property var dataModel: null + property var imagePresetModel: null color: palette.base radius: 5 @@ -122,19 +123,16 @@ Rectangle { id: comboBox width: parent.width - textRole: "text" // ? - onCurrentIndexChanged: { - } - - model: ListModel { - ListElement { - text: "todo" - } - + textRole: "name" + onActivated: (index) => { + console.log(imagePresetModel.getPath(index)); } + model: imagePresetModel } + + GroupBox { width: parent.width diff --git a/src/qml/FileList.qml b/src/qml/FileList.qml index 6ebe51a..a6e38a6 100644 --- a/src/qml/FileList.qml +++ b/src/qml/FileList.qml @@ -40,8 +40,13 @@ Item { id: sourceEntryView } + ImagePresetView { + id: imagePresetView + } + delegate: File { dataModel: sourceEntryView + imagePresetModel: imagePresetView width: filePanel.width } diff --git a/src/ui/image_preset_view.cpp b/src/ui/image_preset_view.cpp new file mode 100644 index 0000000..fea8a16 --- /dev/null +++ b/src/ui/image_preset_view.cpp @@ -0,0 +1,56 @@ +#include "image_preset_view.hpp" + + +ImagePresetView::ImagePresetView(QObject *parent) : QAbstractListModel(parent) { + m_roleNames[NameRole] = "name"; + m_roleNames[PathRole] = "path"; + + m_data = QList>(); + m_data.append(std::pair("None", "")); // TODO is this default path good? + + auto presets = jsonprobe::probe_presets("presets/image", "name"); + for (const auto& preset : presets) { + m_data.append(preset); + } +} + +ImagePresetView::~ImagePresetView() { + m_data.clear(); +} + +QHash ImagePresetView::roleNames() const { return m_roleNames; } + +int ImagePresetView::rowCount(const QModelIndex &parent) const { + Q_UNUSED(parent); + return m_data.count(); +} + +QVariant ImagePresetView::data(const QModelIndex &index, int role) const { + int row = index.row(); + + // oob check + if (row < 0 || row >= m_data.count()) { + return QVariant(); + } + + // property access + switch (role) { + case Qt::DisplayRole: + case NameRole: + return QVariant::fromValue(QString::fromStdString(m_data.value(row).first)); + case PathRole: + return QVariant::fromValue(QString::fromStdString(m_data.value(row).second)); + default: + return QVariant(); + } +} + +QString ImagePresetView::getPath(int index) { + // oob check + if (index < 0 || index >= m_data.count()) { + return QString(); + } + + return QString::fromStdString(m_data.value(index).second); +} + diff --git a/src/ui/image_preset_view.hpp b/src/ui/image_preset_view.hpp new file mode 100644 index 0000000..80cd4fa --- /dev/null +++ b/src/ui/image_preset_view.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "jsonprobe.hpp" + +class ImagePresetView : public QAbstractListModel { + Q_OBJECT + public: + enum RoleNames { + NameRole = Qt::UserRole + 0, + PathRole = Qt::UserRole + 1, + }; + + explicit ImagePresetView(QObject *parent = 0); + ~ImagePresetView(); + + protected: + virtual QHash roleNames() const override; + + private: + QList> m_data; + QHash m_roleNames; + + public: + virtual int rowCount(const QModelIndex &parent) const; + virtual QVariant data(const QModelIndex &index, int role) const; + + Q_INVOKABLE QString getPath(int index); +}; diff --git a/src/util/jsonprobe.cpp b/src/util/jsonprobe.cpp index daad285..6250cdd 100644 --- a/src/util/jsonprobe.cpp +++ b/src/util/jsonprobe.cpp @@ -1,5 +1,6 @@ #include "jsonprobe.hpp" #include +#include #include "nlohmann/json.hpp" using json = nlohmann::json; @@ -26,5 +27,7 @@ ProbeList jsonprobe::probe_presets(const std::string& preset_dir_path, const std // TODO: handle error } + std::cout << "Found " << presets.size() << " presets in " << preset_dir_path << std::endl; + return presets; } \ No newline at end of file From 2a45a754d93b8e9b56717e874be46f16a97571aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 3 Apr 2025 22:25:57 +0200 Subject: [PATCH 45/51] model bindings --- src/qml/Application.qml | 2 +- src/qml/File.qml | 22 +++++++++++++++++++++- src/qml/SizeInput.qml | 12 +++++++++++- src/qml/UnitInput.qml | 6 ++++++ src/ui/image_preset_view.cpp | 2 +- src/ui/image_source_view.cpp | 35 +++++++++++++++++++++++++++++++++-- src/ui/image_source_view.hpp | 10 ++++++++++ src/ui/source_entry_view.cpp | 25 ++++++++++++++++++++++--- src/ui/source_entry_view.hpp | 7 +++++-- 9 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/qml/Application.qml b/src/qml/Application.qml index 6ff822c..7b6f68c 100644 --- a/src/qml/Application.qml +++ b/src/qml/Application.qml @@ -22,7 +22,7 @@ ApplicationWindow { Layout.fillHeight: true Layout.fillWidth: true - Layout.preferredWidth: 3 + Layout.preferredWidth: 2 } Properties { diff --git a/src/qml/File.qml b/src/qml/File.qml index 9829cda..b1b7da7 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -7,6 +7,7 @@ Rectangle { property var dataModel: null property var imagePresetModel: null + property var absoluteModel: model color: palette.base radius: 5 @@ -91,7 +92,7 @@ Rectangle { Text { color: palette.text - text: model.imageSize.width + "x" + model.imageSize.height + text: model.resolution.width + "x" + model.resolution.height font.pixelSize: 12 Layout.fillWidth: true clip: true @@ -126,6 +127,8 @@ Rectangle { textRole: "name" onActivated: (index) => { console.log(imagePresetModel.getPath(index)); + console.log(absoluteModel.index); + dataModel.setPreset(absoluteModel.index, imagePresetModel.getPath(index)); } model: imagePresetModel @@ -142,6 +145,23 @@ Rectangle { SizeInput { id: sizeInput + imageWidth: model.size.width + imageHeight: model.size.height + onWidthChangedDelegate: (value) => { + if (model.size.width == value) return; + model.size.width = value; + if (sizeInput.locked) { + model.size.height = value / model.aspect; + } + } + onHeightChangedDelegate: (value) => { + if (model.size.height == value) return; + model.size.height = value; + console.log(sizeInput.locked); + if (sizeInput.locked) { + model.size.width = value * model.aspect; + } + } width: parent.width } diff --git a/src/qml/SizeInput.qml b/src/qml/SizeInput.qml index b393b7a..f663c3c 100644 --- a/src/qml/SizeInput.qml +++ b/src/qml/SizeInput.qml @@ -3,13 +3,19 @@ import QtQuick.Controls 6.8 import QtQuick.Layouts 6.8 RowLayout { + property var imageWidth: 1 + property var imageHeight: 1 + property var onWidthChangedDelegate: (value) => console.log("Width changed to: " + value) + property var onHeightChangedDelegate: (value) => console.log("Height changed to: " + value) + property alias locked: checkBox.checked + CheckBox { Layout.fillHeight: true id: checkBox - checked: true + checked: true // TODO: on check force aspect ratio clip: true Rectangle { @@ -33,6 +39,8 @@ RowLayout { UnitInput { id: widthSpinbox + value: imageWidth + onValueChangedDelegate: (value) => onWidthChangedDelegate(value) Layout.alignment: Qt.AlignRight label.text: "Width" @@ -42,6 +50,8 @@ RowLayout { UnitInput { id: heightSpinbox + value: imageHeight + onValueChangedDelegate: (value) => onHeightChangedDelegate(value) Layout.alignment: Qt.AlignRight label.text: "Height" diff --git a/src/qml/UnitInput.qml b/src/qml/UnitInput.qml index 37da5e8..6a331df 100644 --- a/src/qml/UnitInput.qml +++ b/src/qml/UnitInput.qml @@ -6,6 +6,10 @@ RowLayout { property alias unit: unit property alias label: label property alias spinbox: spinbox + property alias value: spinbox.value + + property var onValueChangedDelegate: (value) => console.log("Value changed to: " + value) + spacing: 10 @@ -26,6 +30,8 @@ RowLayout { to: 1000 stepSize: 1 editable: true + + onValueChanged: onValueChangedDelegate(spinbox.value); } Text { diff --git a/src/ui/image_preset_view.cpp b/src/ui/image_preset_view.cpp index fea8a16..a71d9d0 100644 --- a/src/ui/image_preset_view.cpp +++ b/src/ui/image_preset_view.cpp @@ -6,7 +6,7 @@ ImagePresetView::ImagePresetView(QObject *parent) : QAbstractListModel(parent) { m_roleNames[PathRole] = "path"; m_data = QList>(); - m_data.append(std::pair("None", "")); // TODO is this default path good? + m_data.append(std::make_pair("None", "")); // TODO is this default path good? auto presets = jsonprobe::probe_presets("presets/image", "name"); for (const auto& preset : presets) { diff --git a/src/ui/image_source_view.cpp b/src/ui/image_source_view.cpp index c85c2be..a70eac6 100644 --- a/src/ui/image_source_view.cpp +++ b/src/ui/image_source_view.cpp @@ -2,6 +2,10 @@ #include #include #include +#include + +#include "nlohmann/json.hpp" +using json = nlohmann::json; // TODO: dont hardcode the initial amount ImageSourceView::ImageSourceView(const std::string& path) : file_path(path), amount(20) { @@ -17,6 +21,9 @@ ImageSourceView::ImageSourceView(const std::string& path) : file_path(path), amo if (image.empty()) { throw std::runtime_error("Failed to load image: " + file_path); } + + width = image.cols; + height = image.rows; } std::string ImageSourceView::get_file_name() const { @@ -27,12 +34,36 @@ std::string ImageSourceView::get_file_path() const { return file_path; } -std::pair ImageSourceView::get_image_size() const { +std::pair ImageSourceView::get_image_resolution() const { return std::make_pair(image.cols, image.rows); } +float ImageSourceView::get_image_aspect_ratio() const { + return float(image.cols) / float(image.rows); +} + +std::pair ImageSourceView::get_image_size() const { return std::make_pair(width, height); } + +void ImageSourceView::set_image_size(int width, int height) { + this->width = width; + this->height = height; + std::cout << "Image size set to: " << width << "x" << height << std::endl; +} + cv::Mat ImageSourceView::get_image() const { return image; } int ImageSourceView::get_amount() const { return amount; } -void ImageSourceView::set_amount(int amount) { this->amount = amount; } \ No newline at end of file +void ImageSourceView::set_amount(int amount) { this->amount = amount; } + +void ImageSourceView::load_from_preset(const std::string& preset_path) { + std::cout << "Loading preset from: " << preset_path << std::endl; + + json json_data; + std::ifstream file(preset_path); + json_data = json::parse(file); + file.close(); + + width = json_data["width"]; + height = json_data["height"]; +} diff --git a/src/ui/image_source_view.hpp b/src/ui/image_source_view.hpp index 34c13f7..b5525ce 100644 --- a/src/ui/image_source_view.hpp +++ b/src/ui/image_source_view.hpp @@ -8,6 +8,8 @@ class ImageSourceView { std::string file_path; cv::Mat image; int amount; + int width = 100; + int height = 100; public: explicit ImageSourceView(const std::string& path); @@ -16,11 +18,19 @@ class ImageSourceView { std::string get_file_path() const; + std::pair get_image_resolution() const; + + float get_image_aspect_ratio() const; + std::pair get_image_size() const; + void set_image_size(int width, int height); + cv::Mat get_image() const; int get_amount() const; void set_amount(int amount); + + void load_from_preset(const std::string& preset_path); }; \ No newline at end of file diff --git a/src/ui/source_entry_view.cpp b/src/ui/source_entry_view.cpp index 36cf1a5..c9dfa7e 100644 --- a/src/ui/source_entry_view.cpp +++ b/src/ui/source_entry_view.cpp @@ -1,10 +1,12 @@ #include "source_entry_view.hpp" -SourceEntryView::SourceEntryView(QObject *parent) : QAbstractListModel(parent) { +SourceEntryView::SourceEntryView(QObject *parent) : QAbstractListModel(parent) { m_roleNames[NameRole] = "name"; m_roleNames[PathRole] = "path"; - m_roleNames[ImageSizeRole] = "imageSize"; + m_roleNames[ImageResolutionRole] = "resolution"; m_roleNames[AmountRole] = "amount"; + m_roleNames[ImageSizeRole] = "size"; + m_roleNames[ImageAspectRole] = "aspect"; m_data = QList(); } @@ -36,10 +38,16 @@ QVariant SourceEntryView::data(const QModelIndex &index, int role) const { return QVariant::fromValue(QString::fromStdString(m_data.value(row)->get_file_name())); case PathRole: return QVariant::fromValue(QString::fromStdString(m_data.value(row)->get_file_path())); + case ImageResolutionRole: { + auto [width, height] = m_data.value(row)->get_image_resolution(); + return QVariant::fromValue(QSize(width, height)); + } case ImageSizeRole: { auto [width, height] = m_data.value(row)->get_image_size(); return QVariant::fromValue(QSize(width, height)); } + case ImageAspectRole: + return QVariant::fromValue(m_data.value(row)->get_image_aspect_ratio()); case AmountRole: return QVariant::fromValue(m_data.value(row)->get_amount()); default: @@ -59,6 +67,11 @@ bool SourceEntryView::setData(const QModelIndex &index, const QVariant &value, i case AmountRole: m_data.value(row)->set_amount(value.toInt()); break; + case ImageSizeRole: { + auto [width, height] = value.value(); + m_data.value(row)->set_image_size(width, height); + break; + } default: return false; } @@ -88,7 +101,13 @@ void SourceEntryView::addFiles(const QStringList &files) { auto *input_file = new ImageSourceView(file.toStdString()); beginInsertRows(QModelIndex(), m_data.count(), m_data.count()); - m_data.append(input_file); + m_data.append(input_file); endInsertRows(); } } + +void SourceEntryView::setPreset(int index, const QString &presetPath) { + m_data[index]->load_from_preset(presetPath.toStdString()); + QModelIndex modelIndex = this->index(index, 0); + emit dataChanged(modelIndex, modelIndex, {ImageSizeRole}); // TODO: update +} diff --git a/src/ui/source_entry_view.hpp b/src/ui/source_entry_view.hpp index e2bd7d9..9bedb0f 100644 --- a/src/ui/source_entry_view.hpp +++ b/src/ui/source_entry_view.hpp @@ -11,8 +11,10 @@ class SourceEntryView : public QAbstractListModel { enum RoleNames { NameRole = Qt::UserRole + 0, PathRole = Qt::UserRole + 1, - ImageSizeRole = Qt::UserRole + 2, - AmountRole = Qt::UserRole + 3 + ImageResolutionRole = Qt::UserRole + 2, + AmountRole = Qt::UserRole + 3, + ImageSizeRole = Qt::UserRole + 4, + ImageAspectRole = Qt::UserRole + 5, }; explicit SourceEntryView(QObject *parent = 0); @@ -35,4 +37,5 @@ class SourceEntryView : public QAbstractListModel { Q_INVOKABLE void remove(int index); Q_INVOKABLE void clear(); Q_INVOKABLE void addFiles(const QStringList &files); + Q_INVOKABLE void setPreset(int index, const QString &presetPath); }; From 65ec1f3da4ac64003ea8880aa1debd113b5b91b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 3 Apr 2025 22:36:23 +0200 Subject: [PATCH 46/51] missing file handling --- src/qml/File.qml | 28 +++++++++++++++------------- src/ui/image_source_view.cpp | 4 ++++ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/qml/File.qml b/src/qml/File.qml index b1b7da7..bc61a04 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -126,16 +126,14 @@ Rectangle { width: parent.width textRole: "name" onActivated: (index) => { - console.log(imagePresetModel.getPath(index)); - console.log(absoluteModel.index); + let path = imagePresetModel.getPath(index); + if (path == "") + return ; dataModel.setPreset(absoluteModel.index, imagePresetModel.getPath(index)); } - - model: imagePresetModel + model: imagePresetModel } - - GroupBox { width: parent.width @@ -145,24 +143,28 @@ Rectangle { SizeInput { id: sizeInput + imageWidth: model.size.width imageHeight: model.size.height onWidthChangedDelegate: (value) => { - if (model.size.width == value) return; + if (model.size.width == value) + return ; + model.size.width = value; - if (sizeInput.locked) { + if (sizeInput.locked) model.size.height = value / model.aspect; - } + } onHeightChangedDelegate: (value) => { - if (model.size.height == value) return; + if (model.size.height == value) + return ; + model.size.height = value; console.log(sizeInput.locked); - if (sizeInput.locked) { + if (sizeInput.locked) model.size.width = value * model.aspect; - } - } + } width: parent.width } diff --git a/src/ui/image_source_view.cpp b/src/ui/image_source_view.cpp index a70eac6..256e316 100644 --- a/src/ui/image_source_view.cpp +++ b/src/ui/image_source_view.cpp @@ -59,6 +59,10 @@ void ImageSourceView::set_amount(int amount) { this->amount = amount; } void ImageSourceView::load_from_preset(const std::string& preset_path) { std::cout << "Loading preset from: " << preset_path << std::endl; + if (!std::filesystem::exists(preset_path)) { + throw std::invalid_argument("Preset file does not exist: " + preset_path); + } + json json_data; std::ifstream file(preset_path); json_data = json::parse(file); From 5713358cd7c1c0860ef9bea82aee708974e37856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Wed, 9 Apr 2025 18:01:10 +0200 Subject: [PATCH 47/51] refarctor and better serparate the list and its entries --- meson.build | 1 + src/main.cpp | 1 + src/qml/File.qml | 35 +++++++++--------- src/qml/MaskInput.qml | 4 +- src/qml/SizeInput.qml | 4 +- src/ui/image_source_view.cpp | 70 +++++++++++++++-------------------- src/ui/image_source_view.hpp | 46 +++++++++++++++-------- src/ui/source_entry_view.cpp | 71 +++--------------------------------- src/ui/source_entry_view.hpp | 13 +------ 9 files changed, 90 insertions(+), 155 deletions(-) diff --git a/meson.build b/meson.build index 2402948..cb5caa5 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,7 @@ ui_files = files() moc_headers = files( 'src/ui/source_entry_view.hpp', + 'src/ui/image_source_view.hpp', 'src/ui/image_preset_view.hpp', ) diff --git a/src/main.cpp b/src/main.cpp index ddfd918..b524413 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ int main(int argc, char *argv[]) qmlRegisterType("printf", 1, 0, "SourceEntryView"); qmlRegisterType("printf", 1, 0, "ImagePresetView"); + qRegisterMetaType("ImageSourceView*"); QQmlApplicationEngine engine; diff --git a/src/qml/File.qml b/src/qml/File.qml index bc61a04..7f480d6 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -41,7 +41,7 @@ Rectangle { Image { id: image - source: "file://" + model.path + source: "file://" + model.entry.filePath fillMode: Image.PreserveAspectFit Layout.fillWidth: true Layout.preferredWidth: 1 @@ -58,7 +58,7 @@ Rectangle { Text { color: palette.text - text: model.name + text: model.entry.name font.pixelSize: 16 font.bold: true Layout.fillWidth: true @@ -72,7 +72,7 @@ Rectangle { Layout.preferredHeight: 30 text: "X" onClicked: { - dataModel.remove(model.index); + absoluteModel.remove(model.index); } } @@ -81,7 +81,7 @@ Rectangle { Text { Layout.fillWidth: true color: palette.text - text: model.path + text: model.entry.filePath font.pixelSize: 12 clip: true } @@ -92,7 +92,7 @@ Rectangle { Text { color: palette.text - text: model.resolution.width + "x" + model.resolution.height + text: model.entry.resolution.width + "x" + model.entry.resolution.height font.pixelSize: 12 Layout.fillWidth: true clip: true @@ -102,14 +102,14 @@ Rectangle { id: spinbox Layout.preferredWidth: 60 - value: model.amount + value: model.entry.amount from: 1 to: 1000 stepSize: 1 editable: true onValueChanged: () => { - if (model.amount != spinbox.value) - model.amount = spinbox.value; + if (model.entry.amount != spinbox.value) + model.entry.amount = spinbox.value; } } @@ -129,7 +129,7 @@ Rectangle { let path = imagePresetModel.getPath(index); if (path == "") return ; - dataModel.setPreset(absoluteModel.index, imagePresetModel.getPath(index)); + absoluteModel.entry.setPreset(imagePresetModel.getPath(index)); } model: imagePresetModel } @@ -144,25 +144,24 @@ Rectangle { SizeInput { id: sizeInput - imageWidth: model.size.width - imageHeight: model.size.height + imageWidth: model.entry.width + imageHeight: model.entry.height onWidthChangedDelegate: (value) => { - if (model.size.width == value) + if (model.entry.width == value) return ; - model.size.width = value; + model.entry.width = value; if (sizeInput.locked) - model.size.height = value / model.aspect; + model.entry.height = value / model.entry.aspect; } onHeightChangedDelegate: (value) => { - if (model.size.height == value) + if (model.entry.height == value) return ; - model.size.height = value; - console.log(sizeInput.locked); + model.entry.height = value; if (sizeInput.locked) - model.size.width = value * model.aspect; + model.entry.width = value * model.entry.aspect; } width: parent.width diff --git a/src/qml/MaskInput.qml b/src/qml/MaskInput.qml index 0b5cd11..f9b1674 100644 --- a/src/qml/MaskInput.qml +++ b/src/qml/MaskInput.qml @@ -23,7 +23,7 @@ Column { Image { id: maskImage - source: "file://" + model.path + source: "file://" + model.entry.filePath fillMode: Image.PreserveAspectFit Layout.fillWidth: true Layout.preferredWidth: 1 @@ -51,7 +51,7 @@ Column { Text { color: palette.text - text: model.name + text: model.entry.name font.pixelSize: 12 Layout.fillWidth: true clip: true diff --git a/src/qml/SizeInput.qml b/src/qml/SizeInput.qml index f663c3c..aefe9b5 100644 --- a/src/qml/SizeInput.qml +++ b/src/qml/SizeInput.qml @@ -3,8 +3,8 @@ import QtQuick.Controls 6.8 import QtQuick.Layouts 6.8 RowLayout { - property var imageWidth: 1 - property var imageHeight: 1 + property alias imageWidth: widthSpinbox.value + property alias imageHeight: heightSpinbox.value property var onWidthChangedDelegate: (value) => console.log("Width changed to: " + value) property var onHeightChangedDelegate: (value) => console.log("Height changed to: " + value) property alias locked: checkBox.checked diff --git a/src/ui/image_source_view.cpp b/src/ui/image_source_view.cpp index 256e316..cb8e53a 100644 --- a/src/ui/image_source_view.cpp +++ b/src/ui/image_source_view.cpp @@ -1,73 +1,63 @@ #include "image_source_view.hpp" -#include + #include -#include #include +#include +#include #include "nlohmann/json.hpp" using json = nlohmann::json; // TODO: dont hardcode the initial amount -ImageSourceView::ImageSourceView(const std::string& path) : file_path(path), amount(20) { - if (file_path.rfind("file://", 0) == 0) { - file_path = file_path.substr(7); +ImageSourceView::ImageSourceView(const std::string& path) : m_file_path(path), m_amount(20) { + if (m_file_path.rfind("file://", 0) == 0) { + m_file_path = m_file_path.substr(7); } - if (!std::filesystem::exists(file_path)) { - throw std::invalid_argument("File does not exist: " + file_path); + if (!std::filesystem::exists(m_file_path)) { + throw std::invalid_argument("File does not exist: " + m_file_path); } - image = cv::imread(file_path); - if (image.empty()) { - throw std::runtime_error("Failed to load image: " + file_path); + m_image = cv::imread(m_file_path); + if (m_image.empty()) { + throw std::runtime_error("Failed to load image: " + m_file_path); } - width = image.cols; - height = image.rows; -} - -std::string ImageSourceView::get_file_name() const { - return std::filesystem::path(file_path).filename().string(); -} - -std::string ImageSourceView::get_file_path() const { - return file_path; -} - -std::pair ImageSourceView::get_image_resolution() const { - return std::make_pair(image.cols, image.rows); + m_width = m_image.cols; + m_height = m_image.rows; } -float ImageSourceView::get_image_aspect_ratio() const { - return float(image.cols) / float(image.rows); +QString ImageSourceView::get_file_name() const { + return QString::fromStdString(std::filesystem::path(m_file_path).filename().string()); } -std::pair ImageSourceView::get_image_size() const { return std::make_pair(width, height); } - -void ImageSourceView::set_image_size(int width, int height) { - this->width = width; - this->height = height; - std::cout << "Image size set to: " << width << "x" << height << std::endl; -} +QString ImageSourceView::get_file_path() const { return QString::fromStdString(m_file_path); } -cv::Mat ImageSourceView::get_image() const { return image; } +QSize ImageSourceView::get_image_resolution() const { return QSize(m_image.cols, m_image.rows); } -int ImageSourceView::get_amount() const { return amount; } +float ImageSourceView::get_image_aspect_ratio() const { return float(m_image.cols) / float(m_image.rows); } -void ImageSourceView::set_amount(int amount) { this->amount = amount; } +cv::Mat ImageSourceView::get_image() const { return m_image; } void ImageSourceView::load_from_preset(const std::string& preset_path) { std::cout << "Loading preset from: " << preset_path << std::endl; - + if (!std::filesystem::exists(preset_path)) { throw std::invalid_argument("Preset file does not exist: " + preset_path); } - + json json_data; std::ifstream file(preset_path); json_data = json::parse(file); file.close(); - width = json_data["width"]; - height = json_data["height"]; + m_width = json_data["width"]; + m_height = json_data["height"]; } + +Q_INVOKABLE void ImageSourceView::setPreset(const QString& presetPath) { + load_from_preset(presetPath.toStdString()); + // TODO: update signals + widthChanged(); + heightChanged(); +} \ No newline at end of file diff --git a/src/ui/image_source_view.hpp b/src/ui/image_source_view.hpp index b5525ce..d26fe21 100644 --- a/src/ui/image_source_view.hpp +++ b/src/ui/image_source_view.hpp @@ -2,35 +2,49 @@ #include #include +#include +#include + +class ImageSourceView : public QObject { + Q_OBJECT + Q_PROPERTY(QString name READ get_file_name NOTIFY nameChanged) + Q_PROPERTY(QString filePath READ get_file_path NOTIFY filePathChanged) + Q_PROPERTY(QSize resolution READ get_image_resolution NOTIFY resolutionChanged) + Q_PROPERTY(float aspectRatio READ get_image_aspect_ratio NOTIFY aspectRatioChanged) + Q_PROPERTY(int amount MEMBER m_amount NOTIFY amountChanged) + Q_PROPERTY(int width MEMBER m_width NOTIFY widthChanged) + Q_PROPERTY(int height MEMBER m_height NOTIFY heightChanged) -class ImageSourceView { private: - std::string file_path; - cv::Mat image; - int amount; - int width = 100; - int height = 100; + std::string m_file_path; + cv::Mat m_image; + int m_amount; + int m_width = 100; + int m_height = 100; public: explicit ImageSourceView(const std::string& path); - std::string get_file_name() const; + QString get_file_name() const; - std::string get_file_path() const; + QString get_file_path() const; - std::pair get_image_resolution() const; + QSize get_image_resolution() const; float get_image_aspect_ratio() const; - std::pair get_image_size() const; - - void set_image_size(int width, int height); - cv::Mat get_image() const; - int get_amount() const; + void load_from_preset(const std::string& preset_path); - void set_amount(int amount); + Q_INVOKABLE void setPreset(const QString &presetPath); - void load_from_preset(const std::string& preset_path); +signals: + void nameChanged(); + void filePathChanged(); + void resolutionChanged(); + void aspectRatioChanged(); + void amountChanged(); + void widthChanged(); + void heightChanged(); }; \ No newline at end of file diff --git a/src/ui/source_entry_view.cpp b/src/ui/source_entry_view.cpp index c9dfa7e..1c53403 100644 --- a/src/ui/source_entry_view.cpp +++ b/src/ui/source_entry_view.cpp @@ -1,13 +1,6 @@ #include "source_entry_view.hpp" SourceEntryView::SourceEntryView(QObject *parent) : QAbstractListModel(parent) { - m_roleNames[NameRole] = "name"; - m_roleNames[PathRole] = "path"; - m_roleNames[ImageResolutionRole] = "resolution"; - m_roleNames[AmountRole] = "amount"; - m_roleNames[ImageSizeRole] = "size"; - m_roleNames[ImageAspectRole] = "aspect"; - m_data = QList(); } @@ -16,7 +9,7 @@ SourceEntryView::~SourceEntryView() { m_data.clear(); } -QHash SourceEntryView::roleNames() const { return m_roleNames; } +QHash SourceEntryView::roleNames() const { return { { Qt::UserRole, "entry" } }; } int SourceEntryView::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); @@ -24,65 +17,19 @@ int SourceEntryView::rowCount(const QModelIndex &parent) const { } QVariant SourceEntryView::data(const QModelIndex &index, int role) const { - int row = index.row(); - // oob check - if (row < 0 || row >= m_data.count()) { + if (!index.isValid() || index.row() >= m_data.size()) return QVariant(); - } - // property access - switch (role) { - case Qt::DisplayRole: - case NameRole: - return QVariant::fromValue(QString::fromStdString(m_data.value(row)->get_file_name())); - case PathRole: - return QVariant::fromValue(QString::fromStdString(m_data.value(row)->get_file_path())); - case ImageResolutionRole: { - auto [width, height] = m_data.value(row)->get_image_resolution(); - return QVariant::fromValue(QSize(width, height)); - } - case ImageSizeRole: { - auto [width, height] = m_data.value(row)->get_image_size(); - return QVariant::fromValue(QSize(width, height)); - } - case ImageAspectRole: - return QVariant::fromValue(m_data.value(row)->get_image_aspect_ratio()); - case AmountRole: - return QVariant::fromValue(m_data.value(row)->get_amount()); - default: - return QVariant(); - } -} - -bool SourceEntryView::setData(const QModelIndex &index, const QVariant &value, int role) { - int row = index.row(); + if (role == Qt::UserRole) + return QVariant::fromValue(m_data.at(index.row())); // Return the pointer directly - // oob check - if (row < 0 || row >= m_data.count()) { - return false; - } - - switch (role) { - case AmountRole: - m_data.value(row)->set_amount(value.toInt()); - break; - case ImageSizeRole: { - auto [width, height] = value.value(); - m_data.value(row)->set_image_size(width, height); - break; - } - default: - return false; - } - - emit dataChanged(index, index, {role}); - return true; + return QVariant(); } void SourceEntryView::remove(int index) { // oob check - if (index < 0 || index >= m_data.count()) { + if (index < 0 || index >= m_data.size()) { return; } @@ -105,9 +52,3 @@ void SourceEntryView::addFiles(const QStringList &files) { endInsertRows(); } } - -void SourceEntryView::setPreset(int index, const QString &presetPath) { - m_data[index]->load_from_preset(presetPath.toStdString()); - QModelIndex modelIndex = this->index(index, 0); - emit dataChanged(modelIndex, modelIndex, {ImageSizeRole}); // TODO: update -} diff --git a/src/ui/source_entry_view.hpp b/src/ui/source_entry_view.hpp index 9bedb0f..f7b0c68 100644 --- a/src/ui/source_entry_view.hpp +++ b/src/ui/source_entry_view.hpp @@ -7,16 +7,8 @@ class SourceEntryView : public QAbstractListModel { Q_OBJECT + public: - enum RoleNames { - NameRole = Qt::UserRole + 0, - PathRole = Qt::UserRole + 1, - ImageResolutionRole = Qt::UserRole + 2, - AmountRole = Qt::UserRole + 3, - ImageSizeRole = Qt::UserRole + 4, - ImageAspectRole = Qt::UserRole + 5, - }; - explicit SourceEntryView(QObject *parent = 0); ~SourceEntryView(); @@ -27,15 +19,12 @@ class SourceEntryView : public QAbstractListModel { // TODO: shared pointer // so that deleting while its generating wont result in bugs QList m_data; - QHash m_roleNames; public: virtual int rowCount(const QModelIndex &parent) const; virtual QVariant data(const QModelIndex &index, int role) const; - virtual bool setData(const QModelIndex &index, const QVariant &value, int role); Q_INVOKABLE void remove(int index); Q_INVOKABLE void clear(); Q_INVOKABLE void addFiles(const QStringList &files); - Q_INVOKABLE void setPreset(int index, const QString &presetPath); }; From 9386f9ae97f924efe246e84b5b2200454aaeb40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 10 Apr 2025 09:50:02 +0200 Subject: [PATCH 48/51] fix binding loop --- src/qml/File.qml | 20 ++++++-------------- src/qml/SizeInput.qml | 32 +++++++++++++++++--------------- src/ui/image_source_view.cpp | 29 +++++++++++++++++++++++++++-- src/ui/image_source_view.hpp | 24 ++++++++++++++---------- 4 files changed, 64 insertions(+), 41 deletions(-) diff --git a/src/qml/File.qml b/src/qml/File.qml index 7f480d6..1dccb24 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -129,6 +129,7 @@ Rectangle { let path = imagePresetModel.getPath(index); if (path == "") return ; + absoluteModel.entry.setPreset(imagePresetModel.getPath(index)); } model: imagePresetModel @@ -144,24 +145,15 @@ Rectangle { SizeInput { id: sizeInput - imageWidth: model.entry.width - imageHeight: model.entry.height + imageSize: model.entry.size onWidthChangedDelegate: (value) => { - if (model.entry.width == value) - return ; - - model.entry.width = value; - if (sizeInput.locked) - model.entry.height = value / model.entry.aspect; + if (model.entry.size.width != value) + model.entry.setSizeToWidth(value, sizeInput.locked); } onHeightChangedDelegate: (value) => { - if (model.entry.height == value) - return ; - - model.entry.height = value; - if (sizeInput.locked) - model.entry.width = value * model.entry.aspect; + if (model.entry.size.height != value) + model.entry.setSizeToHeight(value, sizeInput.locked); } width: parent.width diff --git a/src/qml/SizeInput.qml b/src/qml/SizeInput.qml index aefe9b5..1c5d768 100644 --- a/src/qml/SizeInput.qml +++ b/src/qml/SizeInput.qml @@ -3,18 +3,19 @@ import QtQuick.Controls 6.8 import QtQuick.Layouts 6.8 RowLayout { - property alias imageWidth: widthSpinbox.value - property alias imageHeight: heightSpinbox.value - property var onWidthChangedDelegate: (value) => console.log("Width changed to: " + value) - property var onHeightChangedDelegate: (value) => console.log("Height changed to: " + value) + property var imageSize: null + property var onWidthChangedDelegate: (value) => { + return console.log("Width changed to: " + value); + } + property var onHeightChangedDelegate: (value) => { + return console.log("Height changed to: " + value); + } property alias locked: checkBox.checked CheckBox { - - Layout.fillHeight: true - id: checkBox + Layout.fillHeight: true checked: true // TODO: on check force aspect ratio clip: true @@ -22,16 +23,13 @@ RowLayout { color: "transparent" border.color: palette.midlight border.width: 2 - width: parent.width height: parent.height * 0.6 - x: parent.width / 2 - anchors.verticalCenter: parent.verticalCenter - radius: 5 } + } ColumnLayout { @@ -39,9 +37,11 @@ RowLayout { UnitInput { id: widthSpinbox - value: imageWidth - onValueChangedDelegate: (value) => onWidthChangedDelegate(value) + value: imageSize.width + onValueChangedDelegate: (value) => { + return onWidthChangedDelegate(value); + } Layout.alignment: Qt.AlignRight label.text: "Width" unit.text: "mm" @@ -50,9 +50,11 @@ RowLayout { UnitInput { id: heightSpinbox - value: imageHeight - onValueChangedDelegate: (value) => onHeightChangedDelegate(value) + value: imageSize.height + onValueChangedDelegate: (value) => { + return onHeightChangedDelegate(value); + } Layout.alignment: Qt.AlignRight label.text: "Height" unit.text: "mm" diff --git a/src/ui/image_source_view.cpp b/src/ui/image_source_view.cpp index cb8e53a..ce73011 100644 --- a/src/ui/image_source_view.cpp +++ b/src/ui/image_source_view.cpp @@ -37,6 +37,16 @@ QSize ImageSourceView::get_image_resolution() const { return QSize(m_image.cols, float ImageSourceView::get_image_aspect_ratio() const { return float(m_image.cols) / float(m_image.rows); } +QSize ImageSourceView::get_size() const { return QSize(m_width, m_height); } + +void ImageSourceView::set_size(const QSize& size) { + if (size.width() != m_width || size.height() != m_height) { + m_width = size.width(); + m_height = size.height(); + emit sizeChanged(); + } +} + cv::Mat ImageSourceView::get_image() const { return m_image; } void ImageSourceView::load_from_preset(const std::string& preset_path) { @@ -58,6 +68,21 @@ void ImageSourceView::load_from_preset(const std::string& preset_path) { Q_INVOKABLE void ImageSourceView::setPreset(const QString& presetPath) { load_from_preset(presetPath.toStdString()); // TODO: update signals - widthChanged(); - heightChanged(); + emit sizeChanged(); +} + +Q_INVOKABLE void ImageSourceView::setSizeToWidth(int width, bool keepAspectRatio) { + m_width = width; + + if (keepAspectRatio) m_height = std::round(width / get_image_aspect_ratio()); + + emit sizeChanged(); +} + +Q_INVOKABLE void ImageSourceView::setSizeToHeight(int height, bool keepAspectRatio) { + m_height = height; + + if (keepAspectRatio) m_width = std::round(height * get_image_aspect_ratio()); + + emit sizeChanged(); } \ No newline at end of file diff --git a/src/ui/image_source_view.hpp b/src/ui/image_source_view.hpp index d26fe21..229a3cc 100644 --- a/src/ui/image_source_view.hpp +++ b/src/ui/image_source_view.hpp @@ -1,9 +1,9 @@ #pragma once -#include -#include #include #include +#include +#include class ImageSourceView : public QObject { Q_OBJECT @@ -12,17 +12,16 @@ class ImageSourceView : public QObject { Q_PROPERTY(QSize resolution READ get_image_resolution NOTIFY resolutionChanged) Q_PROPERTY(float aspectRatio READ get_image_aspect_ratio NOTIFY aspectRatioChanged) Q_PROPERTY(int amount MEMBER m_amount NOTIFY amountChanged) - Q_PROPERTY(int width MEMBER m_width NOTIFY widthChanged) - Q_PROPERTY(int height MEMBER m_height NOTIFY heightChanged) + Q_PROPERTY(QSize size READ get_size WRITE set_size NOTIFY sizeChanged) -private: + private: std::string m_file_path; cv::Mat m_image; int m_amount; int m_width = 100; int m_height = 100; -public: + public: explicit ImageSourceView(const std::string& path); QString get_file_name() const; @@ -33,18 +32,23 @@ class ImageSourceView : public QObject { float get_image_aspect_ratio() const; + QSize get_size() const; + + void set_size(const QSize& size); + cv::Mat get_image() const; void load_from_preset(const std::string& preset_path); - Q_INVOKABLE void setPreset(const QString &presetPath); + Q_INVOKABLE void setPreset(const QString& presetPath); + Q_INVOKABLE void setSizeToWidth(int width, bool keepAspectRatio = true); + Q_INVOKABLE void setSizeToHeight(int height, bool keepAspectRatio = true); -signals: + signals: void nameChanged(); void filePathChanged(); void resolutionChanged(); void aspectRatioChanged(); void amountChanged(); - void widthChanged(); - void heightChanged(); + void sizeChanged(); }; \ No newline at end of file From c741fc7a2c1ca1eaed43c30d0eba982ce0ab9765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 10 Apr 2025 10:34:22 +0200 Subject: [PATCH 49/51] refactor presets view --- meson.build | 4 +- presets/mask/circlemask.json | 4 ++ src/main.cpp | 4 +- src/qml/File.qml | 6 ++- src/qml/FileList.qml | 11 ++-- src/qml/MaskInput.qml | 20 ++++---- ...{image_preset_view.cpp => preset_view.cpp} | 50 ++++++++++++++----- ...{image_preset_view.hpp => preset_view.hpp} | 17 +++++-- src/ui/source_entry_view.cpp | 2 +- 9 files changed, 82 insertions(+), 36 deletions(-) create mode 100644 presets/mask/circlemask.json rename src/ui/{image_preset_view.cpp => preset_view.cpp} (55%) rename src/ui/{image_preset_view.hpp => preset_view.hpp} (62%) diff --git a/meson.build b/meson.build index cb5caa5..2c437f0 100644 --- a/meson.build +++ b/meson.build @@ -18,7 +18,7 @@ ui_files = files() moc_headers = files( 'src/ui/source_entry_view.hpp', 'src/ui/image_source_view.hpp', - 'src/ui/image_preset_view.hpp', + 'src/ui/preset_view.hpp', ) qresources = files('src/qml/resources.qrc') @@ -55,7 +55,7 @@ srcs = files( 'src/settings/document_preset.cpp', 'src/ui/source_entry_view.cpp', 'src/ui/image_source_view.cpp', - 'src/ui/image_preset_view.cpp', + 'src/ui/preset_view.cpp', 'src/util/jsonprobe.cpp', 'src/main.cpp', ) diff --git a/presets/mask/circlemask.json b/presets/mask/circlemask.json new file mode 100644 index 0000000..a3c7805 --- /dev/null +++ b/presets/mask/circlemask.json @@ -0,0 +1,4 @@ +{ + "name": "Circle Mask", + "path": "assets/mask.png" +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index b524413..b7b6969 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,14 +2,14 @@ #include #include "source_entry_view.hpp" -#include "image_preset_view.hpp" +#include "preset_view.hpp" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); qmlRegisterType("printf", 1, 0, "SourceEntryView"); - qmlRegisterType("printf", 1, 0, "ImagePresetView"); + qmlRegisterType("printf", 1, 0, "PresetView"); qRegisterMetaType("ImageSourceView*"); diff --git a/src/qml/File.qml b/src/qml/File.qml index 1dccb24..3209535 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -7,6 +7,7 @@ Rectangle { property var dataModel: null property var imagePresetModel: null + property var maskPresetModel: null property var absoluteModel: model color: palette.base @@ -72,7 +73,7 @@ Rectangle { Layout.preferredHeight: 30 text: "X" onClicked: { - absoluteModel.remove(model.index); + dataModel.remove(model.index); } } @@ -128,7 +129,7 @@ Rectangle { onActivated: (index) => { let path = imagePresetModel.getPath(index); if (path == "") - return ; + return; absoluteModel.entry.setPreset(imagePresetModel.getPath(index)); } @@ -169,6 +170,7 @@ Rectangle { MaskInput { id: maskInput + presetModel: maskPresetModel width: parent.width } diff --git a/src/qml/FileList.qml b/src/qml/FileList.qml index a6e38a6..17bcd91 100644 --- a/src/qml/FileList.qml +++ b/src/qml/FileList.qml @@ -40,13 +40,14 @@ Item { id: sourceEntryView } - ImagePresetView { - id: imagePresetView - } - delegate: File { dataModel: sourceEntryView - imagePresetModel: imagePresetView + imagePresetModel: PresetView { + path: "presets/image" + } + maskPresetModel: PresetView { + path: "presets/mask" + } width: filePanel.width } diff --git a/src/qml/MaskInput.qml b/src/qml/MaskInput.qml index f9b1674..1aa7296 100644 --- a/src/qml/MaskInput.qml +++ b/src/qml/MaskInput.qml @@ -3,6 +3,8 @@ import QtQuick.Controls 6.8 import QtQuick.Layouts 6.8 Column { + property var presetModel: null + CheckBox { id: maskCheckBox @@ -36,17 +38,17 @@ Column { ComboBox { width: parent.width - textRole: "text" // ? - onCurrentIndexChanged: { - } - - model: ListModel { - ListElement { - text: "todo" - } - + textRole: "name" + + onActivated: (index) => { + let path = imagePresetModel.getPath(index); + if (path == "") + return; + + // absoluteModel.entry.setPreset(imagePresetModel.getPath(index)); } + model: presetModel } Text { diff --git a/src/ui/image_preset_view.cpp b/src/ui/preset_view.cpp similarity index 55% rename from src/ui/image_preset_view.cpp rename to src/ui/preset_view.cpp index a71d9d0..f673a25 100644 --- a/src/ui/image_preset_view.cpp +++ b/src/ui/preset_view.cpp @@ -1,31 +1,41 @@ -#include "image_preset_view.hpp" +#include "preset_view.hpp" -ImagePresetView::ImagePresetView(QObject *parent) : QAbstractListModel(parent) { +PresetView::PresetView(QObject *parent) : QAbstractListModel(parent) { + m_path = QString(); + m_roleNames[NameRole] = "name"; m_roleNames[PathRole] = "path"; m_data = QList>(); - m_data.append(std::make_pair("None", "")); // TODO is this default path good? +} + +PresetView::~PresetView() { + m_data.clear(); +} - auto presets = jsonprobe::probe_presets("presets/image", "name"); +QHash PresetView::roleNames() const { return m_roleNames; } + +void PresetView::fetch_entries() { + beginResetModel(); + + m_data.clear(); + m_data.append(std::make_pair("None", "")); // TODO is this default path good? + + auto presets = jsonprobe::probe_presets(m_path.toStdString(), "name"); for (const auto& preset : presets) { m_data.append(preset); } -} -ImagePresetView::~ImagePresetView() { - m_data.clear(); + endResetModel(); } -QHash ImagePresetView::roleNames() const { return m_roleNames; } - -int ImagePresetView::rowCount(const QModelIndex &parent) const { +int PresetView::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_data.count(); } -QVariant ImagePresetView::data(const QModelIndex &index, int role) const { +QVariant PresetView::data(const QModelIndex &index, int role) const { int row = index.row(); // oob check @@ -45,7 +55,23 @@ QVariant ImagePresetView::data(const QModelIndex &index, int role) const { } } -QString ImagePresetView::getPath(int index) { + + +QString PresetView::get_path() const { + return m_path; +} + +void PresetView::set_path(const QString &path) { + if (m_path != path) { + m_path = path; + + fetch_entries(); + + emit pathChanged(); + } +} + +QString PresetView::getPath(int index) { // oob check if (index < 0 || index >= m_data.count()) { return QString(); diff --git a/src/ui/image_preset_view.hpp b/src/ui/preset_view.hpp similarity index 62% rename from src/ui/image_preset_view.hpp rename to src/ui/preset_view.hpp index 80cd4fa..5725b01 100644 --- a/src/ui/image_preset_view.hpp +++ b/src/ui/preset_view.hpp @@ -5,27 +5,38 @@ #include "jsonprobe.hpp" -class ImagePresetView : public QAbstractListModel { +class PresetView : public QAbstractListModel { Q_OBJECT public: + Q_PROPERTY(QString path READ get_path WRITE set_path NOTIFY pathChanged) + enum RoleNames { NameRole = Qt::UserRole + 0, PathRole = Qt::UserRole + 1, }; - explicit ImagePresetView(QObject *parent = 0); - ~ImagePresetView(); + explicit PresetView(QObject *parent = 0); + ~PresetView(); protected: virtual QHash roleNames() const override; private: + QString m_path; QList> m_data; QHash m_roleNames; + void fetch_entries(); + public: virtual int rowCount(const QModelIndex &parent) const; virtual QVariant data(const QModelIndex &index, int role) const; + QString get_path() const; + void set_path(const QString &path); + Q_INVOKABLE QString getPath(int index); + + signals: + void pathChanged(); }; diff --git a/src/ui/source_entry_view.cpp b/src/ui/source_entry_view.cpp index 1c53403..cd6dad1 100644 --- a/src/ui/source_entry_view.cpp +++ b/src/ui/source_entry_view.cpp @@ -22,7 +22,7 @@ QVariant SourceEntryView::data(const QModelIndex &index, int role) const { return QVariant(); if (role == Qt::UserRole) - return QVariant::fromValue(m_data.at(index.row())); // Return the pointer directly + return QVariant::fromValue(m_data.at(index.row())); return QVariant(); } From eab6aef4e1998138107a02cde37ea81a49fbe0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 10 Apr 2025 11:21:26 +0200 Subject: [PATCH 50/51] integrate mask filter view --- meson.build | 2 ++ presets/mask/circlemask.json | 2 +- src/main.cpp | 4 ++++ src/qml/File.qml | 29 ++++++++++++----------- src/qml/MaskInput.qml | 19 ++++++++------- src/ui/image_source_view.cpp | 6 ++--- src/ui/mask_filter_view.cpp | 45 ++++++++++++++++++++++++++++++++++++ src/ui/mask_filter_view.hpp | 38 ++++++++++++++++++++++++++++++ src/ui/preset_view.hpp | 2 +- 9 files changed, 120 insertions(+), 27 deletions(-) create mode 100644 src/ui/mask_filter_view.cpp create mode 100644 src/ui/mask_filter_view.hpp diff --git a/meson.build b/meson.build index 2c437f0..a699a67 100644 --- a/meson.build +++ b/meson.build @@ -19,6 +19,7 @@ moc_headers = files( 'src/ui/source_entry_view.hpp', 'src/ui/image_source_view.hpp', 'src/ui/preset_view.hpp', + 'src/ui/mask_filter_view.hpp', ) qresources = files('src/qml/resources.qrc') @@ -55,6 +56,7 @@ srcs = files( 'src/settings/document_preset.cpp', 'src/ui/source_entry_view.cpp', 'src/ui/image_source_view.cpp', + 'src/ui/mask_filter_view.cpp', 'src/ui/preset_view.cpp', 'src/util/jsonprobe.cpp', 'src/main.cpp', diff --git a/presets/mask/circlemask.json b/presets/mask/circlemask.json index a3c7805..f4b3ce0 100644 --- a/presets/mask/circlemask.json +++ b/presets/mask/circlemask.json @@ -1,4 +1,4 @@ { "name": "Circle Mask", - "path": "assets/mask.png" + "path": "/home/gilgames/Documents/printf/assets/mask.png" } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index b7b6969..ca211ae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include "source_entry_view.hpp" #include "preset_view.hpp" +#include "mask_filter_view.hpp" int main(int argc, char *argv[]) { @@ -10,7 +11,10 @@ int main(int argc, char *argv[]) qmlRegisterType("printf", 1, 0, "SourceEntryView"); qmlRegisterType("printf", 1, 0, "PresetView"); + qmlRegisterType("printf", 1, 0, "MaskFilterView"); + qRegisterMetaType("ImageSourceView*"); + //qRegisterMetaType("MaskFilterView"); QQmlApplicationEngine engine; diff --git a/src/qml/File.qml b/src/qml/File.qml index 3209535..0ef820e 100644 --- a/src/qml/File.qml +++ b/src/qml/File.qml @@ -8,7 +8,8 @@ Rectangle { property var dataModel: null property var imagePresetModel: null property var maskPresetModel: null - property var absoluteModel: model + + property var entry: model.entry color: palette.base radius: 5 @@ -42,7 +43,7 @@ Rectangle { Image { id: image - source: "file://" + model.entry.filePath + source: "file://" + entry.filePath fillMode: Image.PreserveAspectFit Layout.fillWidth: true Layout.preferredWidth: 1 @@ -59,7 +60,7 @@ Rectangle { Text { color: palette.text - text: model.entry.name + text: entry.name font.pixelSize: 16 font.bold: true Layout.fillWidth: true @@ -82,7 +83,7 @@ Rectangle { Text { Layout.fillWidth: true color: palette.text - text: model.entry.filePath + text: entry.filePath font.pixelSize: 12 clip: true } @@ -93,7 +94,7 @@ Rectangle { Text { color: palette.text - text: model.entry.resolution.width + "x" + model.entry.resolution.height + text: entry.resolution.width + "x" + entry.resolution.height font.pixelSize: 12 Layout.fillWidth: true clip: true @@ -103,14 +104,14 @@ Rectangle { id: spinbox Layout.preferredWidth: 60 - value: model.entry.amount + value: entry.amount from: 1 to: 1000 stepSize: 1 editable: true onValueChanged: () => { - if (model.entry.amount != spinbox.value) - model.entry.amount = spinbox.value; + if (entry.amount != spinbox.value) + entry.amount = spinbox.value; } } @@ -131,7 +132,7 @@ Rectangle { if (path == "") return; - absoluteModel.entry.setPreset(imagePresetModel.getPath(index)); + entry.setPreset(imagePresetModel.getPath(index)); } model: imagePresetModel } @@ -146,15 +147,15 @@ Rectangle { SizeInput { id: sizeInput - imageSize: model.entry.size + imageSize: entry.size onWidthChangedDelegate: (value) => { - if (model.entry.size.width != value) - model.entry.setSizeToWidth(value, sizeInput.locked); + if (entry.size.width != value) + entry.setSizeToWidth(value, sizeInput.locked); } onHeightChangedDelegate: (value) => { - if (model.entry.size.height != value) - model.entry.setSizeToHeight(value, sizeInput.locked); + if (entry.size.height != value) + entry.setSizeToHeight(value, sizeInput.locked); } width: parent.width diff --git a/src/qml/MaskInput.qml b/src/qml/MaskInput.qml index 1aa7296..f5ea82b 100644 --- a/src/qml/MaskInput.qml +++ b/src/qml/MaskInput.qml @@ -1,14 +1,19 @@ import QtQuick 6.8 import QtQuick.Controls 6.8 import QtQuick.Layouts 6.8 +import printf 1.0 Column { property var presetModel: null - + + MaskFilterView { + id: maskObject + } + CheckBox { id: maskCheckBox - checked: false + checked: maskObject.enabled text: "Mask" } @@ -25,7 +30,7 @@ Column { Image { id: maskImage - source: "file://" + model.entry.filePath + source: "file://" + maskObject.filePath fillMode: Image.PreserveAspectFit Layout.fillWidth: true Layout.preferredWidth: 1 @@ -39,21 +44,19 @@ Column { ComboBox { width: parent.width textRole: "name" - onActivated: (index) => { - let path = imagePresetModel.getPath(index); + let path = presetModel.getPath(index); if (path == "") return; - // absoluteModel.entry.setPreset(imagePresetModel.getPath(index)); + maskObject.setPreset(presetModel.getPath(index)); } - model: presetModel } Text { color: palette.text - text: model.entry.name + text: maskObject.filePath font.pixelSize: 12 Layout.fillWidth: true clip: true diff --git a/src/ui/image_source_view.cpp b/src/ui/image_source_view.cpp index ce73011..87655e1 100644 --- a/src/ui/image_source_view.cpp +++ b/src/ui/image_source_view.cpp @@ -65,13 +65,13 @@ void ImageSourceView::load_from_preset(const std::string& preset_path) { m_height = json_data["height"]; } -Q_INVOKABLE void ImageSourceView::setPreset(const QString& presetPath) { +void ImageSourceView::setPreset(const QString& presetPath) { load_from_preset(presetPath.toStdString()); // TODO: update signals emit sizeChanged(); } -Q_INVOKABLE void ImageSourceView::setSizeToWidth(int width, bool keepAspectRatio) { +void ImageSourceView::setSizeToWidth(int width, bool keepAspectRatio) { m_width = width; if (keepAspectRatio) m_height = std::round(width / get_image_aspect_ratio()); @@ -79,7 +79,7 @@ Q_INVOKABLE void ImageSourceView::setSizeToWidth(int width, bool keepAspectRatio emit sizeChanged(); } -Q_INVOKABLE void ImageSourceView::setSizeToHeight(int height, bool keepAspectRatio) { +void ImageSourceView::setSizeToHeight(int height, bool keepAspectRatio) { m_height = height; if (keepAspectRatio) m_width = std::round(height * get_image_aspect_ratio()); diff --git a/src/ui/mask_filter_view.cpp b/src/ui/mask_filter_view.cpp new file mode 100644 index 0000000..2c41073 --- /dev/null +++ b/src/ui/mask_filter_view.cpp @@ -0,0 +1,45 @@ +#include "mask_filter_view.hpp" + +#include +#include +#include +#include + +#include "nlohmann/json.hpp" +using json = nlohmann::json; + +MaskFilterView::MaskFilterView(): m_is_enabled(false) {} + +QString MaskFilterView::get_file_name() const { + return QString::fromStdString(std::filesystem::path(m_file_path).filename().string()); +} + +QString MaskFilterView::get_file_path() const { return QString::fromStdString(m_file_path); } + +float MaskFilterView::get_image_aspect_ratio() const { return float(m_image.cols) / float(m_image.rows); } + +cv::Mat MaskFilterView::get_image() const { return m_image; } + +void MaskFilterView::load_from_preset(const std::string& preset_path) { + std::cout << "Loading preset from: " << preset_path << std::endl; + + if (!std::filesystem::exists(preset_path)) { + throw std::invalid_argument("Preset file does not exist: " + preset_path); + } + + json json_data; + std::ifstream file(preset_path); + json_data = json::parse(file); + file.close(); + + // TODO: handle errors + m_file_path = json_data["path"].get(); + // TODO: load m_image +} + +void MaskFilterView::setPreset(const QString& presetPath) { + load_from_preset(presetPath.toStdString()); + // TODO: update signals + emit filePathChanged(); + emit nameChanged(); +} \ No newline at end of file diff --git a/src/ui/mask_filter_view.hpp b/src/ui/mask_filter_view.hpp new file mode 100644 index 0000000..45e8377 --- /dev/null +++ b/src/ui/mask_filter_view.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +class MaskFilterView : public QObject { + Q_OBJECT + Q_PROPERTY(bool enabled MEMBER m_is_enabled NOTIFY isEnabledChanged) + Q_PROPERTY(QString name READ get_file_name NOTIFY nameChanged) + Q_PROPERTY(QString filePath READ get_file_path NOTIFY filePathChanged) + + private: + bool m_is_enabled; + std::string m_file_path; + cv::Mat m_image; + + public: + explicit MaskFilterView(); + + QString get_file_name() const; + + QString get_file_path() const; + + cv::Mat get_image() const; + + float get_image_aspect_ratio() const; + + void load_from_preset(const std::string& preset_path); + + Q_INVOKABLE void setPreset(const QString& presetPath); + + signals: + void isEnabledChanged(); + void nameChanged(); + void filePathChanged(); +}; \ No newline at end of file diff --git a/src/ui/preset_view.hpp b/src/ui/preset_view.hpp index 5725b01..919ed12 100644 --- a/src/ui/preset_view.hpp +++ b/src/ui/preset_view.hpp @@ -7,9 +7,9 @@ class PresetView : public QAbstractListModel { Q_OBJECT - public: Q_PROPERTY(QString path READ get_path WRITE set_path NOTIFY pathChanged) + public: enum RoleNames { NameRole = Qt::UserRole + 0, PathRole = Qt::UserRole + 1, From ed6dcab60e8bd085f7a0c19238e710a8901b43b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs?= Date: Thu, 10 Apr 2025 11:35:44 +0200 Subject: [PATCH 51/51] relative path fixes --- presets/mask/circlemask.json | 2 +- src/qml/MaskInput.qml | 2 +- src/ui/mask_filter_view.cpp | 8 ++++++++ src/ui/mask_filter_view.hpp | 3 +++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/presets/mask/circlemask.json b/presets/mask/circlemask.json index f4b3ce0..a3c7805 100644 --- a/presets/mask/circlemask.json +++ b/presets/mask/circlemask.json @@ -1,4 +1,4 @@ { "name": "Circle Mask", - "path": "/home/gilgames/Documents/printf/assets/mask.png" + "path": "assets/mask.png" } \ No newline at end of file diff --git a/src/qml/MaskInput.qml b/src/qml/MaskInput.qml index f5ea82b..1bca436 100644 --- a/src/qml/MaskInput.qml +++ b/src/qml/MaskInput.qml @@ -30,7 +30,7 @@ Column { Image { id: maskImage - source: "file://" + maskObject.filePath + source: "file://" + maskObject.absoluteFilePath fillMode: Image.PreserveAspectFit Layout.fillWidth: true Layout.preferredWidth: 1 diff --git a/src/ui/mask_filter_view.cpp b/src/ui/mask_filter_view.cpp index 2c41073..4b234e2 100644 --- a/src/ui/mask_filter_view.cpp +++ b/src/ui/mask_filter_view.cpp @@ -16,6 +16,14 @@ QString MaskFilterView::get_file_name() const { QString MaskFilterView::get_file_path() const { return QString::fromStdString(m_file_path); } +QString MaskFilterView::get_absolute_file_path() const { + std::filesystem::path path(m_file_path); + if (std::filesystem::exists(path) && path.is_relative()) { + return QString::fromStdString(std::filesystem::absolute(path).string()); + } + return QString::fromStdString(m_file_path); +} + float MaskFilterView::get_image_aspect_ratio() const { return float(m_image.cols) / float(m_image.rows); } cv::Mat MaskFilterView::get_image() const { return m_image; } diff --git a/src/ui/mask_filter_view.hpp b/src/ui/mask_filter_view.hpp index 45e8377..1071168 100644 --- a/src/ui/mask_filter_view.hpp +++ b/src/ui/mask_filter_view.hpp @@ -10,6 +10,7 @@ class MaskFilterView : public QObject { Q_PROPERTY(bool enabled MEMBER m_is_enabled NOTIFY isEnabledChanged) Q_PROPERTY(QString name READ get_file_name NOTIFY nameChanged) Q_PROPERTY(QString filePath READ get_file_path NOTIFY filePathChanged) + Q_PROPERTY(QString absoluteFilePath READ get_absolute_file_path NOTIFY filePathChanged) private: bool m_is_enabled; @@ -23,6 +24,8 @@ class MaskFilterView : public QObject { QString get_file_path() const; + QString get_absolute_file_path() const; + cv::Mat get_image() const; float get_image_aspect_ratio() const;