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/meson.build b/meson.build index 8b87164..a699a67 100644 --- a/meson.build +++ b/meson.build @@ -1,24 +1,69 @@ project('printf', 'cpp', - default_options: ['cpp_std=c++17'] + default_options: ['cpp_std=c++23'] ) # 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', 'Qml']) + + +# Qt6 preprocessing +qt6 = import('qt6') + +ui_files = files() + +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') + +prep = qt6.preprocess( + moc_headers : moc_headers, + ui_files : ui_files, + qresources : qresources, +) + + +# Include directories +inc = include_directories( + 'src', + 'src/interfaces', + 'src/img', + 'src/img/filters', + 'src/img/tiling', + 'src/settings', + 'src/util', + 'src/ui', + 'src/qml', +) + # 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/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', + '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', ) # Executable -executable('printf', srcs, +executable('printf', [srcs, prep], + include_directories: inc, dependencies: [nlohmann_json, opencv, qt6_dep] ) 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/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/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/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/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/cached_image.cpp b/src/img/cached_image.cpp new file mode 100644 index 0000000..bd8f590 --- /dev/null +++ b/src/img/cached_image.cpp @@ -0,0 +1,26 @@ +#include "cached_image.hpp" + +CachedImage::CachedImage(const ICachableImage& source) : source(source), isDirty(true) {} + +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 new file mode 100644 index 0000000..671b0f5 --- /dev/null +++ b/src/img/cached_image.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "icachable.hpp" +#include "icache.hpp" + +class CachedImage : ICache { + private: + const ICachableImage& source; + 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/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/padding.cpp b/src/img/filters/padding.cpp new file mode 100644 index 0000000..b9f5423 --- /dev/null +++ b/src/img/filters/padding.cpp @@ -0,0 +1,44 @@ +#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); + int width = padded.cols; + int height = padded.rows; + + // 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; +} \ 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/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..5bee3e3 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; @@ -12,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 50eeccf..49ba538 100644 --- a/src/img/filters/size.hpp +++ b/src/img/filters/size.hpp @@ -14,8 +14,10 @@ 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); + + static cv::Mat resize_to_width(const cv::Mat &image, int width, int interDown = sizeInterDown, int interUp = sizeInterUp); }; diff --git a/src/img/griddy.cpp b/src/img/griddy.cpp deleted file mode 100644 index 0b44aa6..0000000 --- a/src/img/griddy.cpp +++ /dev/null @@ -1,16 +0,0 @@ -/* -generateGrid( - array of images - preset struct - document width - gutter width - quantity of cells - whether correct quanity -) -{ - -} - - - -*/ 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 new file mode 100644 index 0000000..39b8c79 --- /dev/null +++ b/src/img/image_source.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include "cached_image.hpp" +#include "icachable.hpp" +#include "filter.hpp" + +class ImageSource : ICachableImage { + private: + cv::Mat original; + CachedImage cached; + size_t amount; + std::vector filters; + bool rotated; + size_t width, height; + + public: + ImageSource(cv::Mat source, size_t amount); + + virtual ~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); + + void clear_filters(); + + cv::Mat apply_filters() const; + + cv::Mat get_img() { return cached.get_img(); } + + size_t get_width() { return cached.get_width(); } + + size_t get_height() { return cached.get_height(); } + + size_t get_amount() const { return amount; } + + cv::Mat get_cachable() const override { return apply_filters(); } +}; 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/tiling/grid_tiling.cpp b/src/img/tiling/grid_tiling.cpp new file mode 100644 index 0000000..86d308b --- /dev/null +++ b/src/img/tiling/grid_tiling.cpp @@ -0,0 +1,78 @@ +#include "grid_tiling.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); + + // 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.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(); }); + + 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); + } + + std::vector tiles = {}; + for (ImageSource* img : images) { + if (rotate) { + 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 + img->add_filter(new SizeFilter(tile_width, tile_height)); + + 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 + + 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.get_image().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 new file mode 100644 index 0000000..bd88630 --- /dev/null +++ b/src/img/tiling/grid_tiling.hpp @@ -0,0 +1,10 @@ +#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); + + public: + 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 new file mode 100644 index 0000000..67b4385 --- /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 new file mode 100644 index 0000000..8d8964b --- /dev/null +++ b/src/img/tiling/tiling.hpp @@ -0,0 +1,9 @@ +#pragma once +#include "document_preset.hpp" +#include "image_source.hpp" + +class Tiling { + public: + virtual cv::Mat generate(const DocumentPreset &preset, std::vector images) = 0; +}; + diff --git a/src/interfaces/icachable.hpp b/src/interfaces/icachable.hpp new file mode 100644 index 0000000..09512f3 --- /dev/null +++ b/src/interfaces/icachable.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include + +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 cc536d6..ca211ae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,34 +1,24 @@ -#include -#include +#include +#include -#include -#include -#include +#include "source_entry_view.hpp" +#include "preset_view.hpp" +#include "mask_filter_view.hpp" -#include -#include +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); -#include "img/filters/mask.hpp" + qmlRegisterType("printf", 1, 0, "SourceEntryView"); + qmlRegisterType("printf", 1, 0, "PresetView"); + qmlRegisterType("printf", 1, 0, "MaskFilterView"); -using json = nlohmann::json; + qRegisterMetaType("ImageSourceView*"); + //qRegisterMetaType("MaskFilterView"); -int main(void) { - // read an image - cv::Mat image = cv::imread("./assets/3.png", 1); + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:/qml/Application.qml"))); - 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", maskFilter.apply(image)); - - // aboszolút filmszínház - cv::waitKey(0); - - return 0; + return app.exec(); } diff --git a/src/qml/3.png b/src/qml/3.png new file mode 100755 index 0000000..8330852 Binary files /dev/null and b/src/qml/3.png differ diff --git a/src/qml/Application.qml b/src/qml/Application.qml new file mode 100644 index 0000000..7b6f68c --- /dev/null +++ b/src/qml/Application.qml @@ -0,0 +1,36 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 + +ApplicationWindow { + visible: true + width: 640 + height: 480 + title: "printf" + + RowLayout { + anchors.fill: parent + + FileList { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: 1 + } + + Preview { + id: preview + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: 2 + } + + Properties { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: 1 + } + + } + +} diff --git a/src/qml/File.qml b/src/qml/File.qml new file mode 100644 index 0000000..0ef820e --- /dev/null +++ b/src/qml/File.qml @@ -0,0 +1,186 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 + +Rectangle { + id: fileItem + + property var dataModel: null + property var imagePresetModel: null + property var maskPresetModel: null + + property var entry: model.entry + + color: palette.base + radius: 5 + implicitHeight: paddingCol.implicitHeight + 20 // FIXME: hack + + SystemPalette { + id: palette + + colorGroup: SystemPalette.Active + } + + Item { + id: container + + clip: true + anchors.fill: parent + anchors.margins: 10 + + Column { + id: paddingCol + + spacing: 10 + width: container.width + + RowLayout { + id: detailsLayout + + width: parent.width + spacing: 10 + + Image { + id: image + + source: "file://" + entry.filePath + fillMode: Image.PreserveAspectFit + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.preferredHeight: 100 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 3 + + RowLayout { + spacing: 10 + Layout.fillWidth: true + + Text { + color: palette.text + text: entry.name + font.pixelSize: 16 + font.bold: true + Layout.fillWidth: true + clip: true + } + + Button { + id: deleteButton + + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + text: "X" + onClicked: { + dataModel.remove(model.index); + } + } + + } + + Text { + Layout.fillWidth: true + color: palette.text + text: entry.filePath + font.pixelSize: 12 + clip: true + } + + RowLayout { + spacing: 10 + Layout.fillWidth: true + + Text { + color: palette.text + text: entry.resolution.width + "x" + entry.resolution.height + font.pixelSize: 12 + Layout.fillWidth: true + clip: true + } + + SpinBox { + id: spinbox + + Layout.preferredWidth: 60 + value: entry.amount + from: 1 + to: 1000 + stepSize: 1 + editable: true + onValueChanged: () => { + if (entry.amount != spinbox.value) + entry.amount = spinbox.value; + + } + } + + } + + } + + } + + ComboBox { + id: comboBox + + width: parent.width + textRole: "name" + onActivated: (index) => { + let path = imagePresetModel.getPath(index); + if (path == "") + return; + + entry.setPreset(imagePresetModel.getPath(index)); + } + model: imagePresetModel + } + + GroupBox { + width: parent.width + + Column { + width: parent.width + spacing: 10 + + SizeInput { + id: sizeInput + + imageSize: entry.size + onWidthChangedDelegate: (value) => { + if (entry.size.width != value) + entry.setSizeToWidth(value, sizeInput.locked); + + } + onHeightChangedDelegate: (value) => { + if (entry.size.height != value) + entry.setSizeToHeight(value, sizeInput.locked); + + } + width: parent.width + } + + CheckBox { + id: guidesCheckBox + + checked: true + text: "Guides" + } + + MaskInput { + id: maskInput + + presetModel: maskPresetModel + width: parent.width + } + + } + + } + + } + + } + +} diff --git a/src/qml/FileList.qml b/src/qml/FileList.qml new file mode 100644 index 0000000..17bcd91 --- /dev/null +++ b/src/qml/FileList.qml @@ -0,0 +1,58 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 +import printf 1.0 + +Item { + ColumnLayout { + id: filePanel + + anchors.fill: parent + spacing: 10 + anchors.margins: 10 + + Button { + id: openButton + + text: "Open" + onClicked: { + imagePicker.open(); + } + + ImagePicker { + id: imagePicker + onAcceptDelegate: (files) => { + sourceEntryView.addFiles(files); + } + } + + } + + ListView { + id: fileListView + + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: 10 + + model: SourceEntryView { + id: sourceEntryView + } + + delegate: File { + dataModel: sourceEntryView + imagePresetModel: PresetView { + path: "presets/image" + } + maskPresetModel: PresetView { + path: "presets/mask" + } + width: filePanel.width + } + + } + + } + +} diff --git a/src/qml/ImagePicker.qml b/src/qml/ImagePicker.qml new file mode 100644 index 0000000..5a4726b --- /dev/null +++ b/src/qml/ImagePicker.qml @@ -0,0 +1,20 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 +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: { + onAcceptDelegate(fileDialog.selectedFiles); + } + onRejected: { + onRejectDelegate(); + } +} diff --git a/src/qml/MaskInput.qml b/src/qml/MaskInput.qml new file mode 100644 index 0000000..1bca436 --- /dev/null +++ b/src/qml/MaskInput.qml @@ -0,0 +1,71 @@ +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: maskObject.enabled + text: "Mask" + } + + GroupBox { + visible: maskCheckBox.checked + width: parent.width + + RowLayout { + id: maskLayout + + width: parent.width + spacing: 10 + + Image { + id: maskImage + + source: "file://" + maskObject.absoluteFilePath + fillMode: Image.PreserveAspectFit + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.preferredHeight: 50 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 3 + + ComboBox { + width: parent.width + textRole: "name" + onActivated: (index) => { + let path = presetModel.getPath(index); + if (path == "") + return; + + maskObject.setPreset(presetModel.getPath(index)); + } + model: presetModel + } + + Text { + color: palette.text + text: maskObject.filePath + font.pixelSize: 12 + Layout.fillWidth: true + clip: true + } + + } + + } + + } + +} diff --git a/src/qml/Preview.qml b/src/qml/Preview.qml new file mode 100644 index 0000000..01fa76b --- /dev/null +++ b/src/qml/Preview.qml @@ -0,0 +1,52 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 + +Item { + SystemPalette { id: palette; colorGroup: SystemPalette.Active } + + Rectangle { + id: flickArea + color: palette.dark + anchors.fill: parent + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + onWheel: (wheel) => { + let zoomFactor = 1.1; + if (wheel.angleDelta.y > 0) + image.scale *= zoomFactor; + else + image.scale /= zoomFactor; + } + } + + Flickable { + id: flickable + + anchors.fill: parent + contentWidth: Math.max(image.width * image.scale, flickArea.width) + contentHeight: Math.max(image.height * image.scale, flickArea.height) + clip: true + + Image { + id: image + + source: "3.png" + anchors.centerIn: parent + transformOrigin: Item.Center + scale: 1 + } + + MouseArea { + anchors.fill: parent + 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..13f17ad --- /dev/null +++ b/src/qml/Properties.qml @@ -0,0 +1,14 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 + +Item { + Button { + id: button + + text: "dont click me" + onClicked: { + console.log("Button clicked"); + } + } + +} diff --git a/src/qml/SizeInput.qml b/src/qml/SizeInput.qml new file mode 100644 index 0000000..1c5d768 --- /dev/null +++ b/src/qml/SizeInput.qml @@ -0,0 +1,66 @@ +import QtQuick 6.8 +import QtQuick.Controls 6.8 +import QtQuick.Layouts 6.8 + +RowLayout { + 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 { + id: checkBox + + Layout.fillHeight: true + checked: true // TODO: on check force aspect ratio + 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 + + value: imageSize.width + onValueChangedDelegate: (value) => { + return onWidthChangedDelegate(value); + } + Layout.alignment: Qt.AlignRight + label.text: "Width" + unit.text: "mm" + Layout.fillWidth: true + } + + UnitInput { + id: heightSpinbox + + value: imageSize.height + onValueChangedDelegate: (value) => { + return onHeightChangedDelegate(value); + } + 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 new file mode 100644 index 0000000..6a331df --- /dev/null +++ b/src/qml/UnitInput.qml @@ -0,0 +1,47 @@ +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 + property alias value: spinbox.value + + property var onValueChangedDelegate: (value) => console.log("Value changed to: " + value) + + + spacing: 10 + + Text { + id: label + + color: palette.text + font.pixelSize: 12 + clip: true + } + + SpinBox { + id: spinbox + + Layout.preferredWidth: 60 + value: 1 + from: 1 + to: 1000 + stepSize: 1 + editable: true + + onValueChanged: onValueChangedDelegate(spinbox.value); + } + + 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 new file mode 100644 index 0000000..8f3896e --- /dev/null +++ b/src/qml/resources.qrc @@ -0,0 +1,14 @@ + + + Application.qml + FileList.qml + Preview.qml + Properties.qml + 3.png + File.qml + ImagePicker.qml + UnitInput.qml + SizeInput.qml + MaskInput.qml + + \ No newline at end of file diff --git a/src/settings/document_preset.cpp b/src/settings/document_preset.cpp new file mode 100644 index 0000000..acbb533 --- /dev/null +++ b/src/settings/document_preset.cpp @@ -0,0 +1,37 @@ +#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"]; + + // FIXME + max_height_mm = 18000; + min_height_mm = 1000; +} + +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); } + +size_t DocumentPreset::get_gutter_px() const { return convert::mm_to_pixels(gutter_mm, ppi); } diff --git a/src/settings/document_preset.hpp b/src/settings/document_preset.hpp new file mode 100644 index 0000000..b51f3d2 --- /dev/null +++ b/src/settings/document_preset.hpp @@ -0,0 +1,37 @@ +#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; + + size_t get_gutter_px() const; +}; 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/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/ui/image_source_view.cpp b/src/ui/image_source_view.cpp new file mode 100644 index 0000000..87655e1 --- /dev/null +++ b/src/ui/image_source_view.cpp @@ -0,0 +1,88 @@ +#include "image_source_view.hpp" + +#include +#include +#include +#include + +#include "nlohmann/json.hpp" +using json = nlohmann::json; + +// TODO: dont hardcode the initial amount +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(m_file_path)) { + throw std::invalid_argument("File does not exist: " + m_file_path); + } + + m_image = cv::imread(m_file_path); + if (m_image.empty()) { + throw std::runtime_error("Failed to load image: " + m_file_path); + } + + m_width = m_image.cols; + m_height = m_image.rows; +} + +QString ImageSourceView::get_file_name() const { + return QString::fromStdString(std::filesystem::path(m_file_path).filename().string()); +} + +QString ImageSourceView::get_file_path() const { return QString::fromStdString(m_file_path); } + +QSize ImageSourceView::get_image_resolution() const { return QSize(m_image.cols, m_image.rows); } + +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) { + 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(); + + m_width = json_data["width"]; + m_height = json_data["height"]; +} + +void ImageSourceView::setPreset(const QString& presetPath) { + load_from_preset(presetPath.toStdString()); + // TODO: update signals + emit sizeChanged(); +} + +void ImageSourceView::setSizeToWidth(int width, bool keepAspectRatio) { + m_width = width; + + if (keepAspectRatio) m_height = std::round(width / get_image_aspect_ratio()); + + emit sizeChanged(); +} + +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 new file mode 100644 index 0000000..229a3cc --- /dev/null +++ b/src/ui/image_source_view.hpp @@ -0,0 +1,54 @@ +#pragma once + +#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(QSize size READ get_size WRITE set_size NOTIFY sizeChanged) + + private: + 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); + + QString get_file_name() const; + + QString get_file_path() const; + + QSize get_image_resolution() const; + + 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 setSizeToWidth(int width, bool keepAspectRatio = true); + Q_INVOKABLE void setSizeToHeight(int height, bool keepAspectRatio = true); + + signals: + void nameChanged(); + void filePathChanged(); + void resolutionChanged(); + void aspectRatioChanged(); + void amountChanged(); + void sizeChanged(); +}; \ No newline at end of file diff --git a/src/ui/mask_filter_view.cpp b/src/ui/mask_filter_view.cpp new file mode 100644 index 0000000..4b234e2 --- /dev/null +++ b/src/ui/mask_filter_view.cpp @@ -0,0 +1,53 @@ +#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); } + +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; } + +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..1071168 --- /dev/null +++ b/src/ui/mask_filter_view.hpp @@ -0,0 +1,41 @@ +#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) + Q_PROPERTY(QString absoluteFilePath READ get_absolute_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; + + QString get_absolute_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.cpp b/src/ui/preset_view.cpp new file mode 100644 index 0000000..f673a25 --- /dev/null +++ b/src/ui/preset_view.cpp @@ -0,0 +1,82 @@ +#include "preset_view.hpp" + + +PresetView::PresetView(QObject *parent) : QAbstractListModel(parent) { + m_path = QString(); + + m_roleNames[NameRole] = "name"; + m_roleNames[PathRole] = "path"; + + m_data = QList>(); +} + +PresetView::~PresetView() { + m_data.clear(); +} + +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); + } + + endResetModel(); +} + +int PresetView::rowCount(const QModelIndex &parent) const { + Q_UNUSED(parent); + return m_data.count(); +} + +QVariant PresetView::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 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(); + } + + return QString::fromStdString(m_data.value(index).second); +} + diff --git a/src/ui/preset_view.hpp b/src/ui/preset_view.hpp new file mode 100644 index 0000000..919ed12 --- /dev/null +++ b/src/ui/preset_view.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include "jsonprobe.hpp" + +class PresetView : public QAbstractListModel { + Q_OBJECT + Q_PROPERTY(QString path READ get_path WRITE set_path NOTIFY pathChanged) + + public: + enum RoleNames { + NameRole = Qt::UserRole + 0, + PathRole = Qt::UserRole + 1, + }; + + 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 new file mode 100644 index 0000000..cd6dad1 --- /dev/null +++ b/src/ui/source_entry_view.cpp @@ -0,0 +1,54 @@ +#include "source_entry_view.hpp" + +SourceEntryView::SourceEntryView(QObject *parent) : QAbstractListModel(parent) { + m_data = QList(); +} + +SourceEntryView::~SourceEntryView() { + qDeleteAll(m_data); + m_data.clear(); +} + +QHash SourceEntryView::roleNames() const { return { { Qt::UserRole, "entry" } }; } + +int SourceEntryView::rowCount(const QModelIndex &parent) const { + Q_UNUSED(parent); + return m_data.count(); +} + +QVariant SourceEntryView::data(const QModelIndex &index, int role) const { + // oob check + if (!index.isValid() || index.row() >= m_data.size()) + return QVariant(); + + if (role == Qt::UserRole) + return QVariant::fromValue(m_data.at(index.row())); + + return QVariant(); +} + +void SourceEntryView::remove(int index) { + // oob check + if (index < 0 || index >= m_data.size()) { + return; + } + + beginRemoveRows(QModelIndex(), index, index); + delete m_data.takeAt(index); + endRemoveRows(); +} + +void SourceEntryView::clear() { + qDeleteAll(m_data); + m_data.clear(); +} + +void SourceEntryView::addFiles(const QStringList &files) { + for (const QString &file : files) { + auto *input_file = new ImageSourceView(file.toStdString()); + + beginInsertRows(QModelIndex(), m_data.count(), m_data.count()); + m_data.append(input_file); + endInsertRows(); + } +} diff --git a/src/ui/source_entry_view.hpp b/src/ui/source_entry_view.hpp new file mode 100644 index 0000000..f7b0c68 --- /dev/null +++ b/src/ui/source_entry_view.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "image_source_view.hpp" + +class SourceEntryView : public QAbstractListModel { + Q_OBJECT + + public: + explicit SourceEntryView(QObject *parent = 0); + ~SourceEntryView(); + + protected: + virtual QHash roleNames() const override; + + private: + // TODO: shared pointer + // so that deleting while its generating wont result in bugs + QList m_data; + + public: + virtual int rowCount(const QModelIndex &parent) const; + virtual QVariant data(const QModelIndex &index, int role) const; + + Q_INVOKABLE void remove(int index); + Q_INVOKABLE void clear(); + Q_INVOKABLE void addFiles(const QStringList &files); +}; 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 - */ 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 diff --git a/src/util/jsonprobe.cpp b/src/util/jsonprobe.cpp new file mode 100644 index 0000000..6250cdd --- /dev/null +++ b/src/util/jsonprobe.cpp @@ -0,0 +1,33 @@ +#include "jsonprobe.hpp" +#include +#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 + } + + std::cout << "Found " << presets.size() << " presets in " << preset_dir_path << std::endl; + + 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"); +} diff --git a/uml/img_source.puml b/uml/img_source.puml new file mode 100644 index 0000000..2078be1 --- /dev/null +++ b/uml/img_source.puml @@ -0,0 +1,42 @@ +@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() + void apply_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 + + +class Tile { + ImgSource image + cv::Point corner + bool rotated +} +ImgSource -* Tile + + + +@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..a8ace19 --- /dev/null +++ b/uml/tiling.puml @@ -0,0 +1,19 @@ +@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 + +@enduml \ No newline at end of file diff --git a/uml/ui.puml b/uml/ui.puml new file mode 100644 index 0000000..6fdcbfa --- /dev/null +++ b/uml/ui.puml @@ -0,0 +1,68 @@ +@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) + std::map propbe_presets() +} +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) + std::map propbe_presets() +} +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