diff --git a/.clang-format b/.clang-format index 5fc7c04..fc18271 100644 --- a/.clang-format +++ b/.clang-format @@ -1,22 +1,24 @@ --- -Language: Cpp -BasedOnStyle: Google +Language: Cpp +BasedOnStyle: Google IndentWidth: 4 ColumnLimit: 100 -#IncludeBlocks: Preserve # 保持头文件分组结构 -SortIncludes: CaseInsensitive # 开启排序(但通过分组控制例外) +SortIncludes: CaseInsensitive # 开启排序(但通过分组控制例外) +AlignConsecutiveDeclarations: true # 开启对连续声明的对齐。 +AlignConsecutiveAssignments: true +AlignTrailingComments: true IncludeCategories: # 1. 优先排序标准库头文件(尖括号) - - Regex: '^<[a-z_]+>' - Priority: 1 + - Regex: "^<[a-z_]+>" + Priority: 1 # 2. 禁止排序第三方库(通过低优先级 + 不排序同一分组) - - Regex: '^<*.*>' # 匹配第三方路径 - Priority: 2 + - Regex: "^<*.*>" # 匹配第三方路径 + Priority: 2 # 3. 其他头文件(如项目自有头文件) - - Regex: '^".*"' - Priority: 3 + - Regex: '^".*"' + Priority: 3 ... diff --git a/.clang-tidy b/.clang-tidy index f7b7ce1..f9a4465 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,4 +1,6 @@ -Checks: +HeaderFilterRegex: 'src/.*' + +Checks: > -*, bugprone-undelegated-constructor, bugprone-argument-comment, @@ -47,16 +49,24 @@ Checks: bugprone-exception-escape, bugprone-missing-return, + performance-avoid-endl, performance-faster-string-find, performance-for-range-copy, performance-implicit-conversion-in-loop, performance-inefficient-algorithm, + performance-inefficient-string-concatenation, performance-inefficient-vector-operation, + performance-move-const-arg, performance-move-constructor-init, performance-no-automatic-move, + performance-no-int-to-ptr, + performance-noexcept-destructor, + performance-noexcept-move-constructor, + performance-noexcept-swap, performance-trivially-destructible, + performance-type-promotion-in-math-fn, performance-unnecessary-copy-initialization, - performance-move-const-arg, + performance-unnecessary-value-param, modernize-avoid-bind, modernize-loop-convert, @@ -68,23 +78,39 @@ Checks: modernize-replace-random-shuffle, modernize-use-auto, modernize-use-bool-literals, - modernize-use-nullptr, - modernize-use-using, - modernize-use-override, + modernize-use-constraints, + modernize-use-default-member-init, + modernize-use-designated-initializers, + modernize-use-emplace, modernize-use-equals-default, modernize-use-equals-delete, + modernize-use-integer-sign-comparison, + modernize-use-nodiscard, + modernize-use-noexcept, + modernize-use-nullptr, + modernize-use-override, + modernize-use-ranges, + modernize-use-scoped-lock, + modernize-use-starts-ends-with, + modernize-use-std-format, + modernize-use-std-numbers, + modernize-use-std-print, + modernize-use-transparent-functors, + modernize-use-uncaught-exceptions, + modernize-use-using, - misc-throw-by-value-catch-by-reference, + misc-definitions-in-headers, + misc-header-include-cycle, misc-misplaced-const, - misc-unconventional-assign-operator, misc-redundant-expression, misc-static-assert, + misc-throw-by-value-catch-by-reference, misc-unconventional-assign-operator, misc-uniqueptr-reset-release, misc-unused-alias-decls, misc-unused-parameters, misc-unused-using-decls, - misc-definitions-in-headers + misc-use-internal-linkage, readability-avoid-const-params-in-decls readability-const-return-type, @@ -92,6 +118,7 @@ Checks: readability-delete-null-pointer, readability-deleted-default, readability-misplaced-array-index, + readability-make-member-function-const, readability-non-const-parameter, readability-redundant-control-flow, readability-redundant-function-ptr-dereference, @@ -111,11 +138,20 @@ Checks: cert-mem57-cpp, cert-oop58-cpp, cert-err34-c, + google-build-explicit-make-pair, google-runtime-operator, + hicpp-exception-baseclass, hicpp-uppercase-literal-suffix, + + cppcoreguidelines-misleading-capture-default-by-value, + cppcoreguidelines-missing-std-forward, + cppcoreguidelines-no-suspend-with-lock, + cppcoreguidelines-pro-type-member-init, + cppcoreguidelines-rvalue-reference-param-not-moved, cppcoreguidelines-virtual-class-destructor, + security-api-misuse, -FormatStyle: 'file' +FormatStyle: "file" diff --git a/.clangd b/.clangd index f37df64..bd3ee8a 100644 --- a/.clangd +++ b/.clangd @@ -1,5 +1,17 @@ -Index: - Background: Build - -CompileFlags: - Add: [-xc++, -Wall, -std=c++20] \ No newline at end of file +Index: + Background: Build + +CompileFlags: + Add: [-xc++, -Wall, -std=c++20] + +Diagnostics: + UnusedIncludes: Strict + MissingIncludes: Strict + ClangTidy: + Add: [] + Remove: [] + + Includes: + IgnoreHeader: + - "wx/.*" + - "opencv2/.*" diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 6468a2f..ba87232 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -2,43 +2,75 @@ name: cpp-linter on: pull_request: branches: [main] - paths: ['**.c', '**.cpp', '**.h', '**.hpp', '**.cxx', '**.hxx', '**.cc', '**.hh', '**CMakeLists.txt', 'meson.build', '**.cmake'] + paths: + [ + "**.c", + "**.cpp", + "**.h", + "**.hpp", + "**.cxx", + "**.hxx", + "**.cc", + "**.hh", + "**CMakeLists.txt", + "meson.build", + "**.cmake", + ] push: branches: [main] - paths: ['**.c', '**.cpp', '**.h', '**.hpp', '**.cxx', '**.hxx', '**.cc', '**.hh', '**CMakeLists.txt', 'meson.build', '**.cmake'] + paths: + [ + "**.c", + "**.cpp", + "**.h", + "**.hpp", + "**.cxx", + "**.hxx", + "**.cc", + "**.hh", + "**CMakeLists.txt", + "meson.build", + "**.cmake", + ] jobs: cpp-linter: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - uses: actions/checkout@v5 - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - libwxgtk3.2-dev \ - libopencv-dev \ - libeigen3-dev \ - libomp-dev \ - build-essential - + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: CLANG64 + update: true + install: >- + mingw-w64-clang-x86_64-cmake + mingw-w64-clang-x86_64-toolchain + mingw-w64-clang-x86_64-cereal + mingw-w64-clang-x86_64-wxwidgets3.3-common + mingw-w64-clang-x86_64-wxwidgets3.3-common-libs + mingw-w64-clang-x86_64-wxwidgets3.3-msw + mingw-w64-clang-x86_64-wxwidgets3.3-msw-libs + mingw-w64-clang-x86_64-opencv + mingw-w64-clang-x86_64-eigen3 + - name: Generate Compilation Database + shell: msys2 {0} run: | cmake -S . -B build \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - -DCMAKE_BUILD_TYPE=Debug - + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + - uses: cpp-linter/cpp-linter-action@v2 id: linter env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - style: 'file' # Use .clang-format config file - tidy-checks: '' # Use .clang-tidy config file + style: "file" # Use .clang-format config file + tidy-checks: "" # Use .clang-tidy config file # only 'update' a single comment in a pull request thread. thread-comments: ${{ github.event_name == 'pull_request' && 'update' }} - database: 'build' + database: "build" - name: Fail fast?! if: steps.linter.outputs.checks-failed > 0 diff --git a/.vscode/launch.json b/.vscode/launch.json index 7ef1025..a7c2d7d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,15 @@ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Launch", + "program": "${workspaceFolder}/build/Croplines.exe", + "args": [], + "cwd": "${workspaceFolder}/build", + "preLaunchTask": "CMake: 生成", + }, { "name": "(gdb) 启动", "type": "cppdbg", diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 0000000..8b375ec --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,10 @@ +[ + { + "label": "Debug", + "cwd": "./build", + "program": "Croplines.exe", + "request": "launch", + "adapter": "GDB", + "build": "CMake build debug" + } +] diff --git a/.zed/settings.json b/.zed/settings.json index 3e6ff72..1fd2cea 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -7,24 +7,14 @@ "clangd": { "binary": { "arguments": [ - "--query-driver=D:\\MSYS2\\ucrt64\\bin\\g*", - "--all-scopes-completion", // 全局补全(补全建议会给出在当前作用域不可见的索引,插入后自动补充作用域标识符),例如在main()中直接写cout,即使没有`#include `,也会给出`std::cout`的建议,配合"--header-insertion=iwyu",还可自动插入缺失的头文件 - "--background-index", // 后台分析并保存索引文件 - "--clang-tidy", // 启用 Clang-Tidy 以提供「静态检查」,下面设置 clang tidy 规则 - "--clang-tidy-checks=performance-*, bugprone-*, misc-*, google-*, modernize-*, readability-*, portability-*", - "--compile-commands-dir=./build/", // 编译数据库(例如 compile_commands.json 文件)的目录位置 - "--completion-parse=auto", // 当 clangd 准备就绪时,用它来分析建议 - "--completion-style=detailed", // 建议风格:打包(重载函数只会给出一个建议);还可以设置为 detailed - "--enable-config", // 启用配置文件(YAML格式)项目配置文件是在项目文件夹里的“.clangd”,用户配置文件是“clangd/config.yaml”,该文件来自:Windows: %USERPROFILE%\AppData\Local || MacOS: ~/Library/Preferences/ || Others: $XDG_CONFIG_HOME, usually ~/.config - "--function-arg-placeholders=true", // 补全函数时,将会给参数提供占位符,键入后按 Tab 可以切换到下一占位符,乃至函数末 - "--header-insertion-decorators", // 输入建议中,已包含头文件的项与还未包含头文件的项会以圆点加以区分 - "--header-insertion=iwyu", // 插入建议时自动引入头文件 iwyu - "--include-cleaner-stdlib", // 为标准库头文件启用清理功能(不成熟!!!) - "--log=verbose", // 让 Clangd 生成更详细的日志 - "--pch-storage=memory", // pch 优化的位置(Memory 或 Disk,前者会增加内存开销,但会提升性能) - "--pretty", // 输出的 JSON 文件更美观 - "--ranking-model=decision_forest", // 建议的排序方案:hueristics (启发式), decision_forest (决策树) - "-j=12" // 同时开启的任务数量 + "--query-driver=D:/MSYS2/ucrt64/bin/g++.exe", + "--background-index", + "--all-scopes-completion", + "--completion-style=detailed", + "--clang-tidy", + "--pch-storage=memory", + "--compile-commands-dir=./build/", + "-j=12" ] } } diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 0000000..1f7322e --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,20 @@ +[ + { + "label": "CMake build debug", + "command": "cmake --build build --config Debug --target all -j 20 --", + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "hide": "never", + "shell": "system", + }, + { + "label": "CMake build release", + "command": "cmake --build build -j 20 --config Release", + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "hide": "never", + "shell": "system", + }, +] diff --git a/Cmakelists.txt b/CMakeLists.txt similarity index 69% rename from Cmakelists.txt rename to CMakeLists.txt index 9e2d19d..81d360b 100644 --- a/Cmakelists.txt +++ b/CMakeLists.txt @@ -1,45 +1,48 @@ -cmake_minimum_required(VERSION 3.21) -project(Croplines) - -add_compile_options("$<$:/source-charset:utf-8>") - -set(CMAKE_CXX_STANDARD 20) -set (CMAKE_EXPORT_COMPILE_COMMANDS ON) -if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) - set(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++ -static-libgcc") - set(CMAKE_CXX_FLAGS_RELEASE "-s") -endif() - -if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) - set(wxWidgets_USE_DEBUG ON) -else() - set(wxWidgets_USE_STATIC ON) -endif() -find_package(wxWidgets REQUIRED aui core base gl) -find_package(Eigen3 REQUIRED) # required by opencv -find_package(OpenCV REQUIRED) -find_package(OpenMP REQUIRED) -include(${wxWidgets_USE_FILE}) -include_directories(${OpenCV_INCLUDE_DIRS}) - -file(GLOB_RECURSE SRCS src/*.cpp) -if (${CMAKE_SYSTEM_NAME} MATCHES "Windows") -set (WIN32_RESOURCES src/resource.rc) -endif() - -if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) - add_executable(Croplines ${SRCS} ${WIN32_RESOURCES}) -else() - add_executable(Croplines WIN32 ${SRCS} ${WIN32_RESOURCES}) -endif() -target_link_libraries(Croplines ${wxWidgets_LIBRARIES}) -target_link_libraries(Croplines ${OpenCV_LIBS}) -target_link_libraries(Croplines opengl32) - - -if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) - target_link_libraries(Croplines OpenMP::OpenMP_CXX -static-libgcc - -static-libstdc++ - -Wl,-Bstatic -lgomp -Wl,-Bdynamic) - target_link_libraries(Croplines -Wl,-Bstatic stdc++ pthread -Wl,-Bdynamic) # 解决libwinpthread-1.dll依赖问题 -endif() +cmake_minimum_required(VERSION 3.21) +project(Croplines) + +add_compile_options("$<$:/source-charset:utf-8>") + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) + set(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++ -static-libgcc") + set(CMAKE_CXX_FLAGS_RELEASE "-s") +endif() + +if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) + set(wxWidgets_USE_DEBUG ON) +else() + set(wxWidgets_USE_STATIC ON) +endif() +find_package(wxWidgets REQUIRED aui core base gl) +find_package(Eigen3 REQUIRED) # required by opencv +find_package(OpenCV REQUIRED) +find_package(OpenMP REQUIRED) +find_package(cereal REQUIRED) +include(${wxWidgets_USE_FILE}) + +file(GLOB_RECURSE SRCS src/*.cpp) +if (${CMAKE_SYSTEM_NAME} MATCHES "Windows") +set (WIN32_RESOURCES src/resource.rc) +endif() + +if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) +add_executable(Croplines ${SRCS} ${WIN32_RESOURCES}) +else() +add_executable(Croplines WIN32 ${SRCS} ${WIN32_RESOURCES}) +endif() +target_include_directories(Croplines PRIVATE src) +target_include_directories(Croplines PRIVATE ${OpenCV_INCLUDE_DIRS}) +target_link_libraries(Croplines PRIVATE ${wxWidgets_LIBRARIES}) +target_link_libraries(Croplines PRIVATE ${OpenCV_LIBS}) +target_link_libraries(Croplines PRIVATE opengl32) +target_link_libraries(Croplines PRIVATE cereal::cereal) + + +if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) + target_link_libraries(Croplines OpenMP::OpenMP_CXX -static-libgcc + -static-libstdc++ + -Wl,-Bstatic -lgomp -Wl,-Bdynamic) + target_link_libraries(Croplines -Wl,-Bstatic stdc++ pthread -Wl,-Bdynamic) # 解决libwinpthread-1.dll依赖问题 +endif() diff --git a/src/License.hpp b/src/License.hpp new file mode 100644 index 0000000..adc7400 --- /dev/null +++ b/src/License.hpp @@ -0,0 +1,22 @@ +[[maybe_unused]] static constexpr char LICENSE[]{ + "MIT License\n" + "\n" + "Copyright (c) 2024 Likend\n" + "\n" + "Permission is hereby granted, free of charge, to any person obtaining a copy\n" + "of this software and associated documentation files (the \"Software\"), to deal\n" + "in the Software without restriction, including without limitation the rights\n" + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n" + "copies of the Software, and to permit persons to whom the Software is\n" + "furnished to do so, subject to the following conditions:\n" + "\n" + "The above copyright notice and this permission notice shall be included in all\n" + "copies or substantial portions of the Software.\n" + "\n" + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n" + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n" + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n" + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n" + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n" + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n" + "SOFTWARE.\n"}; diff --git a/src/app.cpp b/src/app.cpp deleted file mode 100644 index 262f099..0000000 --- a/src/app.cpp +++ /dev/null @@ -1,296 +0,0 @@ -#include "app.h" - -#include -#include - -#include -#include -#include -#include - -#include "ctrl.h" - -using namespace Croplines; - -// // 设置加速器表 -// const static wxAcceleratorEntry accel_entries[] = { -// {wxACCEL_CTRL, 'S', wxID_SAVE}, -// {wxACCEL_CTRL, 'O', wxID_OPEN}, -// {wxACCEL_CTRL, 'W', wxID_EXIT}, -// {wxACCEL_NORMAL, WXK_UP, wxID_UP}, -// {wxACCEL_NORMAL, WXK_DOWN, wxID_DOWN}}; -// const static wxAcceleratorTable accel_table(std::size(accel_entries), -// accel_entries); - -MainWindow::MainWindow(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, - const wxSize& size, long style) - : MainUI(parent, id, title, pos, size, style) { - SetIcon(wxICON(MAIN_ICON)); - - menubar = new MenuBar(); - SetMenuBar(menubar); - - EnableTools(false); - EnableMenu(false); - EnableConfigs(false); - - // SetAcceleratorTable(accel_table); -} - -void MainWindow::EnableTools(bool state) { - toolbar->EnableTool(wxID_UP, state); - toolbar->EnableTool(wxID_DOWN, state); - toolbar->EnableTool(wxID_SAVE, state); - // toolbar->EnableTool(wxID_OPEN, state); - toolbar->EnableTool(wxID_ZOOM_FIT, state); - toolbar->EnableTool(btnid_CROP_CURR_PAGE, state); - toolbar->EnableTool(btnid_CROP_ALL_PAGE, state); - toolbar->Refresh(); -} - -void MainWindow::EnableConfigs(bool enable) { - ntbk_cfg_output->Enable(enable); - ntbk_cfg_process->Enable(enable); - sld_cfg_border->Enable(enable); - sld_cfg_pix_filter->Enable(enable); -} - -void MainWindow::EnableMenu(bool enable) { - menubar->Enable(wxID_SAVE, enable); - menubar->Enable(wxID_CLOSE, enable); - menubar->Enable(wxID_UP, enable); - menubar->Enable(wxID_DOWN, enable); - menubar->Enable(wxID_ZOOM_FIT, enable); - menubar->Enable(wxID_ZOOM_OUT, enable); - menubar->Enable(wxID_ZOOM_IN, enable); - menubar->Enable(wxID_ZOOM_100, enable); - menubar->Enable(btnid_CROP_CURR_PAGE, enable); - menubar->Enable(btnid_CROP_ALL_PAGE, enable); - if (!enable) { - menubar->Enable(wxID_UNDO, false); - menubar->Enable(wxID_REDO, false); - } else { - menubar->Enable(wxID_UNDO, prj->CanUndo()); - menubar->Enable(wxID_REDO, prj->CanRedo()); - } -} - -void MainWindow::Load(std::filesystem::path path) { - auto prj_res = Prj::Load(path); - if (prj_res) { - prj = std::move(prj_res); - prj->menubar = menubar; - - // panel page list - std::vector file_names(prj->GetPages().size()); - std::ranges::transform(prj->GetPages(), file_names.begin(), [](const Prj::Page& page) { - return wxString(page.image_path.filename()); - }); - pn_page_list->Set(file_names); - if (!prj->GetPages().empty()) { - CurrentPage(0); - ShowPage(); - } - sld_cfg_border->SetValue(static_cast(prj->config.border)); - sld_cfg_pix_filter->SetValue(static_cast(prj->config.filter_noise_size)); - - EnableTools(true); - EnableConfigs(true); - EnableMenu(true); - canvas->SetPrj(*prj); - SetTitle(wxString(path.filename())); - } -} - -static bool ShowCloseDialog(wxWindow* parent, Prj& prj) { - wxMessageDialog dialog(parent, wxT("项目已更改,是否保存?"), - wxASCII_STR(wxMessageBoxCaptionStr), - wxICON_QUESTION | wxYES_NO | wxCANCEL); - switch (dialog.ShowModal()) { - case wxID_YES: - prj.Save(); - case wxID_NO: - return true; - - case wxID_CANCEL: - default: - return false; - } -} - -bool MainWindow::ClosePrj() { - if (prj) { - if (prj->IsChange()) { - if (!ShowCloseDialog(this, *prj)) return false; - } - EnableTools(false); - EnableMenu(false); - EnableConfigs(false); - prj->GetPages()[CurrentPage()].get().Close(); - prj.reset(); - canvas->Clear(); - pn_page_list->Clear(); - } - return true; -} - -bool MainWindow::Exit() { - if (ClosePrj()) { - Destroy(); - return true; - } else - return false; -} - -void MainWindow::CurrentPage(std::size_t page) { - if (prj) { - current_page = std::clamp(page, static_cast(0), prj->GetPages().size() - 1); - toolbar->EnableTool(wxID_UP, current_page != 0); - toolbar->EnableTool(wxID_DOWN, current_page != prj->GetPages().size() - 1); - pn_page_list->SetSelection(static_cast(current_page)); - } -} - -void MainWindow::PrevPage() { - if (prj && CurrentPage() != 0) { - prj->GetPages()[CurrentPage()].get().Close(); - CurrentPage(CurrentPage() - 1); - ShowPage(); - } -} - -void MainWindow::NextPage() { - if (prj && CurrentPage() < prj->GetPages().size()) { - prj->GetPages()[CurrentPage()].get().Close(); - CurrentPage(CurrentPage() + 1); - ShowPage(); - } -} - -void MainWindow::ShowPage() { - canvas->SetPage(prj->GetPages()[CurrentPage()]); - canvas->Refresh(); -} - -void MainWindow::OnLoad([[maybe_unused]] wxCommandEvent& event) { - if (prj && prj->IsChange()) { - if (!ShowCloseDialog(this, *prj)) return; - } - wxDirDialog dir_dialog(this, wxT("选择目录"), wxEmptyString, - wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); - auto result = dir_dialog.ShowModal(); - if (result == wxID_OK) { - wxString dir = dir_dialog.GetPath(); - Load(std::filesystem::path(dir.utf8_string())); - } -} - -void MainWindow::OnUndo([[maybe_unused]] wxCommandEvent& event) { - if (prj) { - prj->Undo(); - canvas->Refresh(); - menubar->Enable(wxID_UNDO, prj->CanUndo()); - menubar->Enable(wxID_REDO, true); - } -} - -void MainWindow::OnRedo([[maybe_unused]] wxCommandEvent& event) { - if (prj) { - prj->Redo(); - canvas->Refresh(); - menubar->Enable(wxID_UNDO, true); - menubar->Enable(wxID_REDO, prj->CanRedo()); - } -} - -void MainWindow::OnCropCurrPage([[maybe_unused]] wxCommandEvent& event) { - if (canvas->IsLoaded()) { - Prj::Page& page = *canvas->page; - SetStatusText(std::format("Page {} is croping...", CurrentPage() + 1)); - if (prj->SaveCrops(page)) { - SetStatusText(std::format("Page {} finished!", CurrentPage() + 1)); - } else { - SetStatusText(std::format("Page {} failed!", CurrentPage() + 1)); - } - } -} - -void MainWindow::OnCropAllPage([[maybe_unused]] wxCommandEvent& event) { - if (canvas->IsLoaded()) { - std::thread t([this]() { - std::size_t count = 1; - for (Prj::Page& page : prj->GetPages()) { - SetStatusText(std::format("Page {} is croping...", count)); - prj->SaveCrops(page); - count++; - } - SetStatusText(wxT("Croping finised!")); - }); - t.detach(); - } -} - -void MainWindow::OnAbout([[maybe_unused]] wxCommandEvent& event) { - static const char LICENSE[] = { -#embed "../LICENSE" - }; - - wxAboutDialogInfo aboutInfo; - aboutInfo.SetName(wxT("Croplines")); - aboutInfo.SetIcon(wxICON(MAIN_ICON)); - // aboutInfo.AddArtist(wxT("Likend")); - aboutInfo.AddDeveloper(wxT("Likend")); - aboutInfo.SetWebSite(wxT("https://github.com/Likend/Croplines")); - aboutInfo.SetCopyright(wxT("(C) 2024-2025")); - aboutInfo.SetLicence(LICENSE); - wxAboutBox(aboutInfo); -} - -void MainWindow::OnClickListBox(wxCommandEvent& event) { - if (CurrentPage() != event.GetSelection()) { - CurrentPage(event.GetSelection()); - ShowPage(); - } -} - -// clang-format off -wxBEGIN_EVENT_TABLE(MainWindow, wxFrame) - EVT_MENU(wxID_UP, MainWindow::OnPrevPage) - EVT_MENU(wxID_DOWN, MainWindow::OnNextPage) - EVT_MENU(wxID_SAVE, MainWindow::OnSave) - EVT_MENU(wxID_OPEN, MainWindow::OnLoad) - EVT_MENU(wxID_UNDO, MainWindow::OnUndo) - EVT_MENU(wxID_REDO, MainWindow::OnRedo) - EVT_MENU(wxID_ZOOM_IN, MainWindow::OnZoomIn) - EVT_MENU(wxID_ZOOM_OUT, MainWindow::OnZoomOut) - EVT_MENU(wxID_ZOOM_FIT, MainWindow::OnZoomFit) - EVT_MENU(wxID_ZOOM_100, MainWindow::OnZoom100) - EVT_MENU(btnid_CROP_CURR_PAGE, MainWindow::OnCropCurrPage) - EVT_MENU(btnid_CROP_ALL_PAGE, MainWindow::OnCropAllPage) - EVT_MENU(wxID_CLOSE, MainWindow::OnClose) - EVT_MENU(wxID_EXIT, MainWindow::OnExit) - EVT_MENU(wxID_ABOUT, MainWindow::OnAbout) - EVT_CLOSE(MainWindow::OnExit) - EVT_LISTBOX(pnid_PAGE_LIST, MainWindow::OnClickListBox) - EVT_SLIDER(sldid_cfg_PIX_FILTER, MainWindow::OnChnageCfgFilerPixSize) - EVT_SLIDER(sldid_cfg_BORDER, MainWindow::OnChangeCfgBorder) -wxEND_EVENT_TABLE(); -// clang-format on - -// 告诉wxWidgets主应用程序是哪个类 -IMPLEMENT_APP(MyApp) - -// 初始化程序 -bool MyApp::OnInit() { - SetAppearance(Appearance::System); - // wxImage::AddHandler(new wxBMPHandler); - wxImage::AddHandler(new wxPNGHandler); - wxImage::AddHandler(new wxTIFFHandler); - wxImage::AddHandler(new wxJPEGHandler); - wxImage::AddHandler(new wxWEBPHandler); - - frame = new MainWindow(nullptr); // 创建主窗口 - frame->Show(true); // 显示主窗口 - - return true; // 开始事件处理循环 -} diff --git a/src/app.h b/src/app.h deleted file mode 100644 index 2c07103..0000000 --- a/src/app.h +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -#include "wxUI.h" - -namespace Croplines { - -class MainWindow final : public MainUI { - public: - MainWindow(wxWindow* parent, wxWindowID id = wxID_ANY, - const wxString& title = wxT("Crop Lines"), const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxSize(972, 651), - long style = wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL); - ~MainWindow() {}; - - void EnableTools(bool state); - void EnableConfigs(bool enable); - void EnableMenu(bool enable); - - void Load(std::filesystem::path path); - void Save() { - if (prj) prj->Save(); - } - bool ClosePrj(); - bool Exit(); // true if close succesfully; false if cancelled - - std::size_t CurrentPage() { return current_page; } - void CurrentPage(std::size_t); - void PrevPage(); - void NextPage(); - - private: - std::size_t current_page = 0; - wxMenuBar* menubar; - - private: - void ShowPage(); - - void OnPrevPage([[maybe_unused]] wxCommandEvent& event) { PrevPage(); } - void OnNextPage([[maybe_unused]] wxCommandEvent& event) { NextPage(); } - void OnSave([[maybe_unused]] wxCommandEvent& event) { Save(); } - void OnLoad(wxCommandEvent& event); - void OnUndo(wxCommandEvent& event); - void OnRedo(wxCommandEvent& event); - void OnZoomIn([[maybe_unused]] wxCommandEvent& event) { canvas->ZoomIn(); } - void OnZoomOut([[maybe_unused]] wxCommandEvent& event) { canvas->ZoomOut(); } - void OnZoomFit([[maybe_unused]] wxCommandEvent& event) { canvas->ZoomFit(); } - void OnZoom100([[maybe_unused]] wxCommandEvent& event) { canvas->Zoom(1.0); } - void OnCropCurrPage(wxCommandEvent& event); - void OnCropAllPage(wxCommandEvent& event); - void OnClose([[maybe_unused]] wxCommandEvent& event) { ClosePrj(); } - void OnExit(wxCloseEvent& event) { - if (!Exit()) event.Veto(); - } - void OnExit([[maybe_unused]] wxCommandEvent& event) { Close(); } - void OnAbout(wxCommandEvent& event); - void OnClickListBox(wxCommandEvent& event); - void OnChnageCfgFilerPixSize(wxCommandEvent& event) { - prj->config.filter_noise_size = event.GetInt(); - prj->Change(); - } - void OnChangeCfgBorder(wxCommandEvent& event) { - prj->config.border = event.GetInt(); - prj->Change(); - } - - wxDECLARE_EVENT_TABLE(); - - private: - std::optional prj; -}; - -class MyApp : public wxApp { - private: - MainWindow* frame; - - public: - // 这个函数将会在程序启动的时候被调用 - virtual bool OnInit(); -}; - -// 有了这一行就可以使用 MyApp& wxGetApp()了 -DECLARE_APP(MyApp) -} // namespace Croplines diff --git a/src/canvas.cpp b/src/canvas.cpp deleted file mode 100644 index bc2cee5..0000000 --- a/src/canvas.cpp +++ /dev/null @@ -1,508 +0,0 @@ -#include "canvas.h" - -#include -#include -#include - -#include -#include - -#include "config.h" - -using namespace Croplines; - -ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale) - : imageSize(imageSize), windowSize(windowSize), scale(scale) { - scaledSize = imageSize * scale; - MoveToCenter(); -} - -ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize) - : imageSize(imageSize), windowSize(windowSize) { - scale = GetScaleSuitesPage(); - scaledSize = imageSize * scale; - MoveToCenter(); -} - -void ImageScaleModel::Clamp() { - wxSize border = windowSize - scaledSize; - if (border.x < 0) { - offset.x = std::clamp(offset.x, border.x, 0); - } else { - offset.x = std::clamp(offset.x, 0, border.x); - } - if (border.y < 0) { - offset.y = std::clamp(offset.y, border.y, 0); - } else { - offset.y = std::clamp(offset.y, 0, border.y); - } -} - -void ImageScaleModel::Scale(double factor, wxPoint center) { - double scaleNew = scale * factor; - if (scaleNew > Config::zoom_max) { - factor = Config::zoom_max / scale; - } else if (scaleNew < Config::zoom_min) { - factor = Config::zoom_min / scale; - } - scale *= factor; - offset = center + factor * (offset - center); - scaledSize = imageSize * scale; - Clamp(); - modified = true; -} - -void ImageScaleModel::ScaleTo(double scale, wxPoint center) { - scale = std::clamp(scale, Config::zoom_min, Config::zoom_max); - double factor = scale / this->scale; - this->scale = scale; - offset = center + factor * (offset - center); - scaledSize = imageSize * scale; - Clamp(); - modified = true; -} - -void ImageScaleModel::Move(wxPoint dr) { - offset += dr; - Clamp(); - modified = true; -} - -void ImageScaleModel::MoveToCenter() { - offset = wxPoint{} + (windowSize - scaledSize) / 2; - modified = true; -} - -void ImageScaleModel::WindowResize(wxSize windowSizeNew) { - offset += (windowSizeNew - windowSize) / 2; - windowSize = windowSizeNew; - Clamp(); - modified = true; -} - -void ImageScaleModel::ImageResize(wxSize imageSizeNew) { - offset += (imageSize - imageSizeNew) / 2; - imageSize = imageSizeNew; - scaledSize = scale * imageSize; - Clamp(); - modified = true; -} - -double ImageScaleModel::GetScaleSuitesWidth() const { - return static_cast(windowSize.GetWidth()) / imageSize.GetWidth(); -} - -double ImageScaleModel::GetScaleSuitesHeight() const { - return static_cast(windowSize.GetHeight()) / imageSize.GetHeight(); -} - -double ImageScaleModel::GetScaleSuitesPage() const { - return std::min(GetScaleSuitesWidth(), GetScaleSuitesHeight()); -} - -cv::Mat ImageScaleModel::GetTransformMatrix() const { - return (cv::Mat_(2, 3) << scale, 0, offset.x, 0, scale, offset.y); -} - -bool ImageScaleModel::IsInsideImage(wxRealPoint worldPoint) const { - wxRealPoint imagePoint = ReverseTransform(worldPoint); - return 0 <= imagePoint.x && imagePoint.x <= imageSize.GetWidth() && 0 <= imagePoint.y && - imagePoint.y <= imageSize.GetHeight(); -} - -Canvas::Canvas(wxWindow* parent, wxWindowID id) - : wxGLCanvas(parent, id, nullptr, wxDefaultPosition, wxDefaultSize, - wxFULL_REPAINT_ON_RESIZE | wxVSCROLL | wxHSCROLL) { - AlwaysShowScrollbars(); - - // // disable on default - SetScrollbar(wxHORIZONTAL, -1, -1, -1); - SetScrollbar(wxVERTICAL, -1, -1, -1); - - context = new wxGLContext(this); -} - -Canvas::~Canvas() { - if (texture) glDeleteTextures(1, &texture); - delete context; -} - -static void SetTextrue(GLuint texture, void* pixels, int width, int height) { - glBindTexture(GL_TEXTURE_2D, texture); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // 解决glTexImage2D崩溃问题 - glTexImage2D(GL_TEXTURE_2D, 0, 3, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels); -} - -void Canvas::SetPage(Prj::Page& page) { - wxImage img = prj->LoadPage(page); - if (!img.IsOk()) { - return; - } - if (IsLoaded()) { - scaleModel.ImageResize(img.GetSize()); - } else { - scaleModel = ImageScaleModel{img.GetSize(), this->GetClientSize()}; - } - this->page = &page; - - SetCurrent(*context); - if (texture) glDeleteTextures(1, &texture); - glGenTextures(1, &texture); - SetTextrue(texture, img.GetData(), img.GetWidth(), img.GetHeight()); - Refresh(); -} - -void Canvas::Clear() { - if (texture) glDeleteTextures(1, &texture); - prj = nullptr; - page = nullptr; - Refresh(); -} - -void Canvas::ZoomIn() { - if (!IsLoaded()) return; - scaleModel.Scale(Config::zoom_in_rate); - Refresh(); -} - -void Canvas::ZoomOut() { - if (!IsLoaded()) return; - scaleModel.Scale(Config::zoom_out_rate); - Refresh(); -} - -void Canvas::ZoomFit() { - if (!IsLoaded()) return; - scaleModel.ScaleTo(scaleModel.GetScaleSuitesPage()); - scaleModel.MoveToCenter(); - Refresh(); -} - -void Canvas::Zoom(double scale) { - if (IsLoaded()) { - scaleModel.ScaleTo(scale); - Refresh(); - } -} - -void Canvas::UpdateScrollbars() { - if (!IsLoaded()) { - SetScrollbar(wxHORIZONTAL, -1, -1, -1); - SetScrollbar(wxVERTICAL, -1, -1, -1); - return; - } - if (scaleModel.scaledSize.GetWidth() > scaleModel.windowSize.GetWidth()) { - SetScrollbar(wxHORIZONTAL, -scaleModel.offset.x, scaleModel.windowSize.GetWidth(), - scaleModel.scaledSize.GetWidth()); - } else { - SetScrollbar(wxHORIZONTAL, -1, -1, -1); - } - if (scaleModel.scaledSize.GetHeight() > scaleModel.windowSize.GetHeight()) { - SetScrollbar(wxVERTICAL, -scaleModel.offset.y, scaleModel.windowSize.GetHeight(), - scaleModel.scaledSize.GetHeight()); - } else { - SetScrollbar(wxVERTICAL, -1, -1, -1); - } -} - -static void InitGL() { - // 设置OpenGL状态 - glClearColor(0.0F, 0.0F, 0.0F, 1.0F); - glEnable(GL_DEPTH_TEST); - glEnable(GL_TEXTURE_2D); - // 设置深度测试函数 - glDepthFunc(GL_LEQUAL); - // GL_SMOOTH(光滑着色)/GL_FLAT(恒定着色) - glShadeModel(GL_SMOOTH); - // 开启混合 - glEnable(GL_BLEND); - // 设置混合函数 - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_ALPHA_TEST); // 启用Alpha测试 - // 设置Alpha测试条件为大于0.05则通过 - glAlphaFunc(GL_GREATER, 0.05); - // 设置逆时针索引为正面(GL_CCW/GL_CW) - glFrontFace(GL_CW); - // 开启线段反走样 - glEnable(GL_LINE_SMOOTH); - glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); -} - -void UpdateProjection(wxSize size) { - if (size.GetWidth() <= 0 || size.GetHeight() <= 0) return; - - glViewport(0, 0, size.GetWidth(), size.GetHeight()); - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glOrtho(0, size.GetWidth(), size.GetHeight(), 0, -1, 1); -} - -void Canvas::OnPaint([[maybe_unused]] wxPaintEvent& event) { - wxPaintDC dc(this); - - SetCurrent(*context); - - // 初始化OpenGL(首次绘制时执行) - if (!initialized) { - InitGL(); - initialized = true; - } - - // 清除缓冲区 - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - // 设置正交投影(模拟视口坐标系) - UpdateProjection(GetClientSize()); - - // 设置投影(透视投影) - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - glTranslated(scaleModel.offset.x, scaleModel.offset.y, 0); - glScaled(scaleModel.scale, scaleModel.scale, 1.0); - - if (IsLoaded()) { - // 绑定纹理 - wxSize size = scaleModel.imageSize; - glBegin(GL_QUADS); - glColor3d(1.0, 1.0, 1.0); - glBindTexture(GL_TEXTURE_2D, texture); - glTexCoord2d(0, 0); - glVertex2d(0, 0); - glTexCoord2d(1, 0); - glVertex2d(size.GetWidth(), 0); - glTexCoord2d(1, 1); - glVertex2d(size.GetWidth(), size.GetHeight()); - glTexCoord2d(0, 1); - glVertex2d(0, size.GetHeight()); - glEnd(); - - UpdateScrollbars(); - - glColor4d(0.8, 0.84, 0.8, 0.25); - for (const wxRect& area : prj->GetSelectArea(*page)) { - glBegin(GL_QUADS); - glVertex2d(area.GetLeft(), area.GetTop()); - glVertex2d(area.GetLeft(), area.GetBottom()); - glVertex2d(area.GetRight(), area.GetBottom()); - glVertex2d(area.GetRight(), area.GetTop()); - glEnd(); - } - - auto DrawLine = [&size, this](int width, double line_y) { - double w = FromDIP(width) / scaleModel.scale; - glBegin(GL_QUADS); - glVertex2d(0, line_y - w); - glVertex2d(0, line_y + w); - glVertex2d(size.GetWidth(), line_y + w); - glVertex2d(size.GetWidth(), line_y - w); - glEnd(); - }; - - // draw crop lines - std::optional deleting_line; - if (is_deleting && mouse_position) { - int y = mouse_position->y; - y = std::lround(scaleModel.ReverseTransformY(y)); - auto search_line = - page->SearchNearestLine(y, FromDIP(static_cast(5.0 / scaleModel.scale) + 1)); - if (search_line) { - deleting_line = **search_line; - glColor4d(0.15, 0.58, 0.36, 0.5); - DrawLine(4, *deleting_line); - } - } - - glColor4d(0.30, 0.74, 0.52, 0.5); - for (int line : page->GetCropLines()) { - if (deleting_line == line) continue; - DrawLine(2, line); - } - - // draw mouse line - if (mouse_position) { - if (is_deleting) - glColor4d(0.90, 0.08, 0, 0.5); - else - glColor4d(0.34, 0.61, 0.84, 0.5); - double y = scaleModel.ReverseTransformY(mouse_position->y); - DrawLine(2, y); - } - } - - SwapBuffers(); -} - -void Canvas::OnSize(wxSizeEvent& event) { - if (context) { - SetCurrent(*context); - UpdateProjection(GetClientSize()); - } - if (IsLoaded()) scaleModel.WindowResize(event.GetSize()); - Refresh(); -} - -void Canvas::OnMouseWheel(wxMouseEvent& event) { - if (!IsLoaded()) return; - - double factor; - if (event.GetWheelRotation() > 0) { // zoom in - factor = Config::zoom_in_rate; - } else { // zoom out - factor = Config::zoom_out_rate; - } - scaleModel.Scale(factor, event.GetPosition()); - Refresh(); -} - -void Canvas::OnMouseLeftDown(wxMouseEvent& event) { - if (!is_mouse_capture) { - CaptureMouse(); - is_mouse_capture = true; - } - // m_parent->GetParent()->SetFocus(); - SetFocus(); - mouse_drag_start = event.GetPosition(); -} - -void Canvas::OnMouseLeftUp([[maybe_unused]] wxMouseEvent& event) { - if (is_mouse_capture) { - ReleaseMouse(); - is_mouse_capture = false; - } - mouse_drag_start.reset(); -} - -void Canvas::OnMouseLeftUp([[maybe_unused]] wxMouseCaptureLostEvent& event) { - if (is_mouse_capture) { - ReleaseMouse(); - is_mouse_capture = false; - } - mouse_drag_start.reset(); -} - -void Canvas::OnMouseRightDown(wxMouseEvent& event) { - SetFocus(); - event.Skip(); -} - -void Canvas::OnMouseRightUp(wxMouseEvent& event) { - if (!IsLoaded()) return; - - wxPoint mouse_position = event.GetPosition(); - if (!scaleModel.IsInsideImage(mouse_position)) return; - - int y = mouse_position.y; - y = std::lround(scaleModel.ReverseTransformY(y)); - if (is_deleting) { - std::optional::iterator> it = - page->SearchNearestLine(y, FromDIP(static_cast(5.0 / scaleModel.scale) + 1)); - if (it) prj->Execute(Prj::EraseLineRecord(*it, *page)); - } else { - prj->Execute(Prj::InsertLineRecord(y, *page)); - } - Refresh(); -} - -void Canvas::OnMouseMotion(wxMouseEvent& event) { - if (!IsLoaded()) return; - wxPoint mouse_drag = event.GetPosition(); - if (mouse_drag_start && event.Dragging()) { - auto dr = mouse_drag - *mouse_drag_start; - scaleModel.Move(dr); - *mouse_drag_start = mouse_drag; - Refresh(); - } - - // if mouse inside image - wxPoint mouse_position = event.GetPosition(); - if (scaleModel.IsInsideImage(mouse_position)) { - this->mouse_position = mouse_position; - Refresh(); - } else if (this->mouse_position) { - this->mouse_position = std::nullopt; - Refresh(); - } -} - -void Canvas::OnScroll(wxScrollWinEvent& event) { - if (!IsLoaded()) return; - - const int orientation = event.GetOrientation() & wxORIENTATION_MASK; - switch (orientation & wxORIENTATION_MASK) { - case wxHORIZONTAL: { - const int pos0 = GetScrollPos(wxHORIZONTAL); - const int pos1 = event.GetPosition(); - scaleModel.Move(wxPoint{pos0 - pos1, 0}); - Refresh(); - return; - } - case wxVERTICAL: { - const int pos0 = GetScrollPos(wxVERTICAL); - const int pos1 = event.GetPosition(); - scaleModel.Move(wxPoint{0, pos0 - pos1}); - Refresh(); - return; - } - // default: - // return; // ignore other orientations - } -} - -void Canvas::OnKeyUp(wxKeyEvent& event) { - switch (event.GetKeyCode()) { - case 'D': - if (is_deleting) { - is_deleting = false; - Refresh(); - } - break; - } -} - -void Canvas::OnKeyDown(wxKeyEvent& event) { - switch (event.GetKeyCode()) { - case 'D': - if (!is_deleting) { - is_deleting = true; - Refresh(); - } - break; - } -} - -void Canvas::OnKillFocus(wxFocusEvent& event) { - if (is_deleting) { - is_deleting = false; - Refresh(); - } - if (is_mouse_capture) { - ReleaseMouse(); - is_mouse_capture = false; - } - event.Skip(); -} - -// clang-format off -wxBEGIN_EVENT_TABLE(Canvas, wxPanel) - EVT_PAINT(Canvas::OnPaint) - EVT_SIZE(Canvas::OnSize) - - EVT_MOUSEWHEEL(Canvas::OnMouseWheel) - EVT_LEFT_DOWN(Canvas::OnMouseLeftDown) - EVT_LEFT_UP(Canvas::OnMouseLeftUp) - EVT_RIGHT_DOWN(Canvas::OnMouseRightDown) - EVT_RIGHT_UP(Canvas::OnMouseRightUp) - EVT_MOUSE_CAPTURE_LOST(Canvas::OnMouseLeftUp) - EVT_MOTION(Canvas::OnMouseMotion) - - EVT_SCROLLWIN(Canvas::OnScroll) - - EVT_KEY_DOWN(Canvas::OnKeyDown) - EVT_KEY_UP(Canvas::OnKeyUp) - - EVT_KILL_FOCUS(Canvas::OnKillFocus) -wxEND_EVENT_TABLE(); diff --git a/src/canvas.h b/src/canvas.h deleted file mode 100644 index 690f9cf..0000000 --- a/src/canvas.h +++ /dev/null @@ -1,123 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include - -#include "prj.h" - -namespace Croplines { - -class ImageScaleModel { - public: - wxSize imageSize; - wxSize windowSize; - wxSize scaledSize; - // matrix trans - // [[scale, 0, offset_x, - // 0, scale, offset_y ]] - double scale; - wxPoint offset; - - ImageScaleModel() = default; - ImageScaleModel(wxSize imageSize, wxSize windowSize); - ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale); - - private: - void Clamp(); - - public: - void Scale(double factor, wxPoint center); - void Scale(double factor) { Scale(factor, wxPoint{} + windowSize / 2); } - void ScaleTo(double scale, wxPoint center); - void ScaleTo(double scale) { ScaleTo(scale, wxPoint{} + windowSize / 2); } - void Move(wxPoint dr); - void MoveToCenter(); - void WindowResize(wxSize windowSizeNew); - void ImageResize(wxSize imageSizeNew); - - double GetScaleSuitesPage() const; - double GetScaleSuitesWidth() const; - double GetScaleSuitesHeight() const; - - cv::Mat GetTransformMatrix() const; - - wxRealPoint Transform(wxRealPoint point) const { return scale * point + wxRealPoint(offset); } - double TransformX(double x) const { return scale * x + offset.x; } - double TransformY(double y) const { return scale * y + offset.y; } - wxRealPoint ReverseTransform(wxRealPoint point) const { - return (point - wxRealPoint(offset)) / scale; - } - double ReverseTransformX(double x) const { return (x - offset.x) / scale; } - double ReverseTransformY(double y) const { return (y - offset.y) / scale; } - - bool IsInsideImage(wxRealPoint worldPoint) const; - - private: - friend class Canvas; - bool modified = true; -}; - -class Canvas : public wxGLCanvas { - public: - Prj* prj = nullptr; - Prj::Page* page = nullptr; - ImageScaleModel scaleModel; - - private: - wxGLContext* context = nullptr; - GLuint texture; - cv::UMat uimageSrc; - wxImage imageDst; - bool imageModified = false; - - public: - Canvas(wxWindow* parent, wxWindowID id); - ~Canvas(); - void SetPrj(Prj& prj) { this->prj = &prj; } - void SetPage(Prj::Page& pageData); - void Clear(); - bool IsLoaded() const { return page; } - ImageScaleModel& GetScaleModel() { return scaleModel; } - - void ZoomIn(); - void ZoomOut(); - void ZoomFit(); - void Zoom(double scale); - - private: - bool is_deleting = false; - bool is_mouse_capture = false; - std::optional mouse_drag_start; - std::optional mouse_position; - bool initialized = false; - wxBitmap drawBmp; - - private: - void UpdateScrollbars(); - - void OnPaint(wxPaintEvent& event); - void OnSize(wxSizeEvent& event); - - void OnMouseWheel(wxMouseEvent& event); - void OnMouseLeftDown(wxMouseEvent& event); - void OnMouseLeftUp(wxMouseEvent& event); - void OnMouseLeftUp(wxMouseCaptureLostEvent& event); - void OnMouseRightDown(wxMouseEvent& event); - void OnMouseRightUp(wxMouseEvent& event); - void OnMouseMotion(wxMouseEvent& event); - - void OnScroll(wxScrollWinEvent& event); - - void OnKeyDown(wxKeyEvent& event); - void OnKeyUp(wxKeyEvent& event); - - void OnKillFocus(wxFocusEvent& event); - - wxDECLARE_EVENT_TABLE(); -}; -} // namespace Croplines diff --git a/src/config.cpp b/src/config.cpp deleted file mode 100644 index c1eaac5..0000000 --- a/src/config.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "config.h" - -using namespace Croplines; - -double Config::zoom_in_rate = ZOOM_IN_RATE_DEFAULT; -double Config::zoom_out_rate = ZOOM_OUT_RATE_DEFAULT; -double Config::zoom_min = ZOOM_MIN_DEFAULT; -double Config::zoom_max = ZOOM_MAX_DEFAULT; \ No newline at end of file diff --git a/src/config.h b/src/config.h deleted file mode 100644 index ba7f68d..0000000 --- a/src/config.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -namespace Croplines { -namespace Config { -constexpr double ZOOM_IN_RATE_DEFAULT = 1.2; -constexpr double ZOOM_OUT_RATE_DEFAULT = 1 / ZOOM_IN_RATE_DEFAULT; -constexpr double ZOOM_MIN_DEFAULT = 0.1; -constexpr double ZOOM_MAX_DEFAULT = 100; - -extern double zoom_in_rate; -extern double zoom_out_rate; -extern double zoom_min; -extern double zoom_max; -} // namespace Config -} // namespace Croplines diff --git a/src/core/Document.cpp b/src/core/Document.cpp new file mode 100644 index 0000000..7c91065 --- /dev/null +++ b/src/core/Document.cpp @@ -0,0 +1,105 @@ +#include "core/Document.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "core/DocumentData.hpp" +#include "core/Page.hpp" +#include "utils/Asserts.hpp" +#include "utils/Compare.hpp" + +using namespace croplines; +namespace fs = std::filesystem; + +bool Document::Load(const fs::path& path) { + cwd = path; + + if (!fs::exists(path)) return false; + fs::current_path(path); + + m_data.emplace(); + + const fs::path prj_path = path / PROJECT_FILE_NAME; + if (fs::exists(prj_path) && fs::is_regular_file(prj_path)) { + std::ifstream file(prj_path); + if (file) { + cereal::JSONInputArchive archive(file); + archive(cereal::make_nvp("prj", GetData())); + m_modified = false; + return true; + } + } + InitializeEmptyProject(); + return true; +} + +void Document::InitializeEmptyProject() { + auto extensionFilter = [](const fs::directory_entry& entry) { + return entry.is_regular_file() && + std::ranges::find(VALID_EXTENSION, entry.path().extension().string()) != + std::end(VALID_EXTENSION); + }; + for (auto const& entry : fs::directory_iterator(cwd) | std::views::filter(extensionFilter)) { + fs::path relativePath = fs::relative(entry.path(), cwd); + m_data->pages.push_back(std::make_unique(std::move(relativePath))); + } + + // sort pages + auto pred = [](const std::unique_ptr& pageData) { + return pageData->path.filename().string(); + }; + std::ranges::sort( + m_data->pages, + [](std::string_view s1, std::string_view s2) { return NaturalCompare(s1, s2) < 0; }, pred); + + SetModified(); +} + +bool Document::Save() { + ASSERT_WITH(IsLoad(), "Project not loaded!"); + const fs::path prj_path = cwd / PROJECT_FILE_NAME; + + std::ofstream file(prj_path); + cereal::JSONOutputArchive archive(file); + archive(cereal::make_nvp("prj", GetData())); + m_modified = false; + return true; +} + +bool Document::Close() { + ASSERT_WITH(IsLoad(), "Project not loaded!"); + m_modified = false; + m_data.reset(); + m_processor->ClearCommands(); + return true; +} + +void Document::SetModified(bool modified) { m_modified = modified; } + +Page Document::LoadPage(size_t index) { + ASSERT_WITH(index < PagesSize(), "indx out of range"); + auto& pageData = *m_data->pages[index]; + return Page{*this, pageData}; +} + +bool Document::SaveAllCrops() { + ASSERT_WITH(IsLoad(), "Project not loaded!"); + bool success = true; + for (size_t i = 0; i < PagesSize(); i++) { + Page page = LoadPage(i); + success &= page.SaveCrops(); + } + return success; +} diff --git a/src/core/Document.hpp b/src/core/Document.hpp new file mode 100644 index 0000000..27f1d49 --- /dev/null +++ b/src/core/Document.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +#include + +#include "core/DocumentData.hpp" +#include "core/Page.hpp" + +namespace croplines { + +class Document { + public: + constexpr static const char* VALID_EXTENSION[] = {".png", ".jpg", ".jpeg", ".bmp", + ".tiff", ".tif", ".webp"}; + constexpr static const char* PROJECT_FILE_NAME = "croplines.json"; + + bool Load(const std::filesystem::path& path); + bool Save(); + bool Close(); + [[nodiscard]] bool IsLoad() const { return m_data.has_value(); }; + + [[nodiscard]] const std::filesystem::path& GetPath() const { return cwd; } + + [[nodiscard]] wxCommandProcessor* GetProcessor() const { return m_processor; } + + [[nodiscard]] bool IsModified() const { return m_modified; } + void SetModified(bool modified = true); + + [[nodiscard]] size_t PagesSize() const { return m_data->pages.size(); }; + Page LoadPage(size_t index); + + bool SaveAllCrops(); + + DocumentData& GetData() { return m_data.value(); } + DocumentConfig& GetConfig() { return GetData().config; } + + private: + bool m_modified = false; + + std::optional m_data; + + std::filesystem::path cwd; + + wxCommandProcessor* m_processor = new wxCommandProcessor(); + + void InitializeEmptyProject(); +}; + +} // namespace croplines diff --git a/src/core/DocumentData.hpp b/src/core/DocumentData.hpp new file mode 100644 index 0000000..c32d69a --- /dev/null +++ b/src/core/DocumentData.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace croplines { +struct DocumentConfig { + constexpr static const char* DEFAULT_OUTPUT_DIR = "out"; + + std::filesystem::path output_dir = DEFAULT_OUTPUT_DIR; + int border = 10; + int filter_noise_size = 8; + + template + void serialize(Archive& archive) { + archive(cereal::make_nvp("border", border)); + archive(cereal::make_nvp("filter_noise_size", filter_noise_size)); + archive(cereal::make_nvp("output_dir", output_dir)); + } +}; + +struct PageData { + std::filesystem::path path; + std::set crop_lines; + + PageData() = default; + PageData(std::filesystem::path path) : path(std::move(path)) {} + + template + void serialize(Archive& archive) { + archive(cereal::make_nvp("image_path", path)); + archive(cereal::make_nvp("crop_lines", crop_lines)); + } +}; + +struct DocumentData { + DocumentConfig config; + + std::vector> pages; + + struct PagesProxy { + std::vector>& pages; + + template + void save(Archive& ar) const { + ar(cereal::make_size_tag(static_cast(pages.size()))); + for (auto const& p : pages) { + if (p) ar(*p); + } + } + + template + void load(Archive& ar) { + cereal::size_type size; + ar(cereal::make_size_tag(size)); + pages.clear(); + pages.reserve(size); + for (size_t i = 0; i < size; ++i) { + auto p = std::make_unique(); + ar(*p); + pages.push_back(std::move(p)); + } + } + }; + + template + void serialize(Archive& ar) { + ar(cereal::make_nvp("config", config)); + ar(cereal::make_nvp("pages", PagesProxy{pages})); + } +}; +} // namespace croplines + +// Add support for cereal serializing std::filesystem::path +namespace std { +namespace filesystem { +template +inline void load_minimal(const Archive&, std::filesystem::path& path, const std::string& name) { + path = name; +} + +template +inline std::string save_minimal(const Archive&, const std::filesystem::path& path) { + return path.string(); +} +} // namespace filesystem +} // namespace std diff --git a/src/core/ImageScaleModel.cpp b/src/core/ImageScaleModel.cpp new file mode 100644 index 0000000..d6ef3d2 --- /dev/null +++ b/src/core/ImageScaleModel.cpp @@ -0,0 +1,94 @@ +#include "core/ImageScaleModel.hpp" + +#include + +#include + +using namespace croplines; + +ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale) + : imageSize(imageSize), windowSize(windowSize), scale(scale) { + scaledSize = imageSize * scale; + MoveToCenter(); +} + +void ImageScaleModel::Clamp() { + wxSize border = windowSize - scaledSize; + if (border.x < 0) { + offset.x = std::clamp(offset.x, border.x, 0); + } else { + offset.x = std::clamp(offset.x, 0, border.x); + } + if (border.y < 0) { + offset.y = std::clamp(offset.y, border.y, 0); + } else { + offset.y = std::clamp(offset.y, 0, border.y); + } +} + +void ImageScaleModel::Scale(double factor, wxPoint center) { + double scaleNew = scale * factor; + if (scaleNew > ZOOM_MAX) { + factor = ZOOM_MAX / scale; + } else if (scaleNew < ZOOM_MIN) { + factor = ZOOM_MIN / scale; + } + scale *= factor; + offset = center + factor * (offset - center); + scaledSize = imageSize * scale; + Clamp(); +} + +void ImageScaleModel::ScaleTo(double scale, wxPoint center) { + scale = std::clamp(scale, ZOOM_MIN, ZOOM_MAX); + double factor = scale / this->scale; + this->scale = scale; + offset = center + factor * (offset - center); + scaledSize = imageSize * scale; + Clamp(); +} + +void ImageScaleModel::Move(wxPoint dr) { + offset += dr; + Clamp(); +} + +void ImageScaleModel::MoveToCenter() { offset = wxPoint{} + (windowSize - scaledSize) / 2; } + +void ImageScaleModel::OnWindowResize(wxSize windowSizeNew) { + offset += (windowSizeNew - windowSize) / 2; + windowSize = windowSizeNew; + Clamp(); +} + +void ImageScaleModel::OnImageResize(wxSize imageSizeNew) { + offset += (imageSize - imageSizeNew) / 2; + imageSize = imageSizeNew; + scaledSize = scale * imageSize; + Clamp(); +} + +double ImageScaleModel::GetScaleSuitesWidth() const { + return static_cast(windowSize.GetWidth()) / imageSize.GetWidth(); +} + +double ImageScaleModel::GetScaleSuitesHeight() const { + return static_cast(windowSize.GetHeight()) / imageSize.GetHeight(); +} + +double ImageScaleModel::GetScaleSuitesPage() const { + return std::min(GetScaleSuitesWidth(), GetScaleSuitesHeight()); +} + +cv::Mat ImageScaleModel::GetTransformMatrix() const { + return (cv::Mat_(2, 3) << scale, 0, offset.x, 0, scale, offset.y); +} + +bool ImageScaleModel::IsInsideImage(wxRealPoint worldPoint) const { + wxRealPoint imagePoint = ReverseTransform(worldPoint); + return 0 <= imagePoint.x && imagePoint.x <= imageSize.GetWidth() && 0 <= imagePoint.y && + imagePoint.y <= imageSize.GetHeight(); +} +wxRealPoint croplines::ImageScaleModel::Transform(wxRealPoint point) const { + return scale * point + wxRealPoint(offset); +} diff --git a/src/core/ImageScaleModel.hpp b/src/core/ImageScaleModel.hpp new file mode 100644 index 0000000..18e4e69 --- /dev/null +++ b/src/core/ImageScaleModel.hpp @@ -0,0 +1,57 @@ +#pragma once +#include +#include +#include + +namespace croplines { + +class ImageScaleModel { + public: + constexpr static double ZOOM_IN_RATE = 1.2; + constexpr static double ZOOM_OUT_RATE = 1 / ZOOM_IN_RATE; + constexpr static double ZOOM_MIN = 0.1; + constexpr static double ZOOM_MAX = 100; + + wxSize imageSize; + wxSize windowSize; + wxSize scaledSize; + // matrix trans + // [[scale, 0, offset_x, + // 0, scale, offset_y ]] + double scale; + wxPoint offset; + + ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale); + ImageScaleModel(wxSize imageSize, wxSize windowSize) + : ImageScaleModel(imageSize, windowSize, GetScaleSuitesPage()) {} + + void Scale(double factor, wxPoint center); + void Scale(double factor) { Scale(factor, wxPoint{} + windowSize / 2); } + void ScaleTo(double scale, wxPoint center); + void ScaleTo(double scale) { ScaleTo(scale, wxPoint{} + windowSize / 2); } + void Move(wxPoint dr); + void MoveToCenter(); + void OnWindowResize(wxSize windowSizeNew); + void OnImageResize(wxSize imageSizeNew); + + [[nodiscard]] double GetScaleSuitesPage() const; + [[nodiscard]] double GetScaleSuitesWidth() const; + [[nodiscard]] double GetScaleSuitesHeight() const; + + [[nodiscard]] cv::Mat GetTransformMatrix() const; + + [[nodiscard]] wxRealPoint Transform(wxRealPoint point) const; + [[nodiscard]] double TransformX(double x) const { return scale * x + offset.x; } + [[nodiscard]] double TransformY(double y) const { return scale * y + offset.y; } + [[nodiscard]] wxRealPoint ReverseTransform(wxRealPoint point) const { + return (point - wxRealPoint(offset)) / scale; + } + [[nodiscard]] double ReverseTransformX(double x) const { return (x - offset.x) / scale; } + [[nodiscard]] double ReverseTransformY(double y) const { return (y - offset.y) / scale; } + + [[nodiscard]] bool IsInsideImage(wxRealPoint worldPoint) const; + + private: + void Clamp(); +}; +} // namespace croplines diff --git a/src/core/Page.cpp b/src/core/Page.cpp new file mode 100644 index 0000000..9c66081 --- /dev/null +++ b/src/core/Page.cpp @@ -0,0 +1,165 @@ +#include "core/Page.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "core/Document.hpp" +#include "core/DocumentData.hpp" +#include "utils/Asserts.hpp" + +using namespace croplines; +namespace fs = std::filesystem; + +DocumentConfig& Page::GetConfig() const { return m_doc.GetConfig(); } + +bool Page::InsertLine(int line) { + auto [it, modified] = m_pageData.crop_lines.insert(line); + m_modified |= modified; + GetDocument().SetModified(); + return modified; +} + +bool Page::EraseLine(int line) { + bool modified = m_pageData.crop_lines.erase(line) != 0; + m_modified |= modified; + GetDocument().SetModified(); + return modified; +} + +bool Page::SaveCrops() { + if (!m_image.IsOk()) return false; + + std::size_t count = 1; + fs::create_directories(GetConfig().output_dir); + for (wxRect area : getSelectAreas()) { + wxImage sub_image = m_image.GetSubImage(area); + // TODO + // wxSize border_size = wxSize{static_cast(config.border), + // static_cast(config.border)}; + // wxBitmap bitmap(area.GetSize() + 2 * border_size); + // wxMemoryDC memDC; + // memDC.SelectObject(bitmap); + // memDC.SetBrush(*wxWHITE_BRUSH); + // memDC.DrawRectangle(wxPoint{}, bitmap.GetSize()); + // memDC.DrawBitmap(wxBitmap(sub_image), wxPoint{} + border_size); + + // optimize compress for tiff + int sample_per_pixel = m_image.GetOptionInt(wxIMAGE_OPTION_TIFF_SAMPLESPERPIXEL); + int bits_per_sample = m_image.GetOptionInt(wxIMAGE_OPTION_TIFF_BITSPERSAMPLE); + sub_image.SetOption(wxIMAGE_OPTION_TIFF_SAMPLESPERPIXEL, sample_per_pixel); + sub_image.SetOption(wxIMAGE_OPTION_TIFF_BITSPERSAMPLE, bits_per_sample); + if (sample_per_pixel == 1 && bits_per_sample == 1) + sub_image.SetOption(wxIMAGE_OPTION_TIFF_COMPRESSION, 4); + else + sub_image.SetOption(wxIMAGE_OPTION_TIFF_COMPRESSION, + m_image.GetOptionInt(wxIMAGE_OPTION_TIFF_COMPRESSION)); + + fs::path file_path = + GetConfig().output_dir / std::format("{}-{}{}", GetImagePath().stem().string(), count, + GetImagePath().extension().string()); + // bitmap.SaveFile(wxString(file_path), image.GetType()); + sub_image.SaveFile(wxString(file_path), m_image.GetType()); + count++; + } + return true; +} + +std::optional Page::SearchNearestLine(int searchPosition, int threshold) const { + const std::set& cropLines = GetCropLines(); + if (cropLines.empty()) return std::nullopt; + + auto it1 = cropLines.lower_bound(searchPosition); + if (it1 == cropLines.begin()) { + int d = *it1 - searchPosition; + if (d < threshold) return *it1; + } else if (it1 == cropLines.end()) { + --it1; + int d = searchPosition - *it1; + if (d < threshold) return *it1; + } else { + int d1 = *it1 - searchPosition; + auto it2 = it1; + --it2; + int d2 = searchPosition - *it2; + if (d1 < d2 && d1 < threshold) return *it1; + if (d2 < d1 && d2 < threshold) return *it2; + } + return std::nullopt; +} + +/*自动选择黑色像素区域 + filter_noise_size: 忽略黑像素的大小 + expand_size: 留边空白大小 +*/ +static std::optional CalculateSelectArea(const cv::Mat& image, int filter_noise_size, + int expand_size, int base_line) { + cv::Mat img_dst; + cv::cvtColor(image, img_dst, cv::COLOR_RGB2GRAY); + cv::threshold(img_dst, img_dst, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); + std::vector> contours; + cv::findContours(img_dst, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); + int x_min = std::numeric_limits::max(), x_max = std::numeric_limits::min(), + y_min = std::numeric_limits::max(), y_max = std::numeric_limits::min(); + bool has_point = false; + for (const auto& contour : contours) { + if (cv::contourArea(contour) >= filter_noise_size) { + cv::Rect r = cv::boundingRect(contour); + + // 边缘触碰逻辑(如果靠边则忽略) + if (r.x == 0 || r.y == 0 || (r.x + r.width) >= image.cols || + (r.y + r.height) >= image.rows) { + continue; + } + // 更新全局最小/最大边界 + x_min = std::min(x_min, r.x); + y_min = std::min(y_min, r.y); + x_max = std::max(x_max, r.x + r.width); + y_max = std::max(y_max, r.y + r.height); + has_point = true; + } + } + if (!has_point) return std::nullopt; + + int l = x_min - expand_size; + int t = y_min - expand_size + base_line; + int r = x_max + expand_size; + int b = y_max + expand_size + base_line; + if (l < 0) l = 0; + if (t < 0) t = 0; + if (r > image.cols) r = image.cols; + if (b > image.rows + base_line) b = image.rows + base_line; + return wxRect{wxPoint{l, t}, wxPoint{r, b}}; +} + +void Page::CalculateSelectAreas() { + m_selectAreas.clear(); + ASSERT_WITH(m_image.IsOk(), "Image not load!"); + cv::Mat image(m_image.GetHeight(), m_image.GetWidth(), CV_8UC3, + static_cast(m_image.GetData())); + + int prev_line = 0; + + auto invokeCalculation = [this, &image](int line, int prev_line) { + cv::Mat sub_image = image.rowRange(prev_line, line); + auto area = CalculateSelectArea(sub_image, GetConfig().filter_noise_size, 0, prev_line); + if (area) m_selectAreas.push_back(*area); + }; + + for (int line : GetCropLines()) { + invokeCalculation(line, prev_line); + prev_line = line; + } + std::int32_t line = image.rows; + invokeCalculation(line, prev_line); + + m_modified = false; +} diff --git a/src/core/Page.hpp b/src/core/Page.hpp new file mode 100644 index 0000000..ed1e0ab --- /dev/null +++ b/src/core/Page.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "core/DocumentData.hpp" + +namespace croplines { + +class Document; + +class Page { + public: + ~Page() { m_image.Destroy(); } + + [[nodiscard]] Document& GetDocument() const { return m_doc; } + [[nodiscard]] DocumentConfig& GetConfig() const; + [[nodiscard]] const std::filesystem::path& GetImagePath() const { return m_pageData.path; } + [[nodiscard]] const std::set& GetCropLines() const { return m_pageData.crop_lines; } + wxImage& GetImage() { return m_image; } + + const std::vector& getSelectAreas() { + if (m_modified) CalculateSelectAreas(); + return m_selectAreas; + } + + bool InsertLine(int line); + bool EraseLine(int line); + + bool SaveCrops(); + + [[nodiscard]] std::optional SearchNearestLine(int searchPosition, int threshold) const; + + private: + friend class Document; + + Page(Document& doc, PageData& data) : m_doc(doc), m_pageData(data) { LoadImageFromFile(); } + + bool m_modified = true; // 用来提示 CalculateSelectAreas 的惰性求值 + + Document& m_doc; + PageData& m_pageData; + + std::vector m_selectAreas; + wxImage m_image; + + void LoadImageFromFile() { m_image.LoadFile(wxString(GetImagePath())); } + void CalculateSelectAreas(); +}; +} // namespace croplines diff --git a/src/ctrl.cpp b/src/ctrl.cpp deleted file mode 100644 index 21dd25d..0000000 --- a/src/ctrl.cpp +++ /dev/null @@ -1,126 +0,0 @@ -#include "ctrl.h" - -#include "wxUI.h" - -using namespace Croplines; - -constexpr int SLIDER_ID = 1100; -constexpr int SPIN_ID = 1101; - -SliderWithSpin::SliderWithSpin(wxWindow* parent, wxWindowID id, const wxString& label, int value, - int minValue, int maxValue, const wxPoint& pos, const wxSize& size) - : wxPanel(parent, id, pos, size) { - m_label = new wxStaticText(this, wxID_ANY, label); - m_slider = new wxSlider(this, SLIDER_ID, value, minValue, maxValue); - m_spin = new wxSpinCtrl(this, SPIN_ID, wxEmptyString, wxDefaultPosition, wxDefaultSize, - wxSP_ARROW_KEYS, minValue, maxValue, value); - - wxBoxSizer* bSizer = new wxBoxSizer(wxHORIZONTAL); - bSizer->Add(m_label, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - bSizer->Add(m_slider, 1, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5); - bSizer->Add(m_spin, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM | wxRIGHT, 5); - - SetSizer(bSizer); -} - -bool SliderWithSpin::Enable(bool enable) { - wxWindow* const items[] = {m_label, m_slider, m_spin}; - const bool prevEnable = IsEnabled(); - bool ret = wxPanel::Enable(enable); - if (!ret) { - return false; - } - for (const auto& item : items) { - ret = item->Enable(enable); - if (!ret) { - wxPanel::Enable(prevEnable); - for (const auto* it = items; it != &item; it++) { - (*it)->Enable(prevEnable); - } - return false; - } - } - return true; -} - -void SliderWithSpin::SetValue(int value) { - this->value = value; - if (m_spin->GetValue() != value) { - m_spin->SetValue(value); - } - if (m_slider->GetValue() != value) { - m_slider->SetValue(GetValue()); - } -} - -void SliderWithSpin::CallEvent(int value) { - wxCommandEvent evt(wxEVT_COMMAND_SLIDER_UPDATED, GetId()); - evt.SetEventObject(this); - evt.SetInt(value); - ProcessWindowEvent(evt); -} - -void SliderWithSpin::OnSliderChanged([[maybe_unused]] wxCommandEvent& event) { - const int value = m_slider->GetValue(); - if (m_spin->GetValue() != value) { - m_spin->SetValue(value); - } - this->value = value; - CallEvent(value); -} - -void SliderWithSpin::OnSpinChanged([[maybe_unused]] wxSpinEvent& event) { - const int value = m_spin->GetValue(); - if (m_slider->GetValue() != value) { - m_slider->SetValue(value); - } - this->value = value; - CallEvent(value); -} - -// clang-format off -wxBEGIN_EVENT_TABLE(SliderWithSpin, wxPanel) - EVT_SLIDER(SLIDER_ID, SliderWithSpin::OnSliderChanged) - EVT_SPINCTRL(SPIN_ID, SliderWithSpin::OnSpinChanged) -wxEND_EVENT_TABLE(); -// clang-format on - -MenuBar::MenuBar() : wxMenuBar() { - menu_file = new wxMenu(); - menu_file->Append(wxID_OPEN, wxT("&Load\tCtrl+O")); - menu_file->Append(wxID_SAVE); - menu_file->AppendSeparator(); - menu_file->Append(btnid_CROP_CURR_PAGE, wxT("Crop ¤t page"), - wxT("Crop current page to subimages and save each one to " - "output directory")); - menu_file->Append(btnid_CROP_ALL_PAGE, wxT("Crop &all pages"), - wxT("Crop all pages to subimages and save each one to " - "output directory")); - menu_file->AppendSeparator(); - menu_file->Append(wxID_CLOSE); - menu_file->Append(wxID_EXIT); - - Append(menu_file, wxT("&File")); - - menu_edit = new wxMenu(); - menu_edit->Append(wxID_UNDO); - menu_edit->Append(wxID_REDO); - menu_edit->AppendSeparator(); - menu_edit->Append(wxID_UP, wxT("Last page\tUp"), wxT("Move to last page")); - menu_edit->Append(wxID_DOWN, wxT("Next page\tDown"), wxT("Move to next page")); - - Append(menu_edit, wxT("&Edit")); - - menu_view = new wxMenu(); - menu_view->Append(wxID_ZOOM_IN); - menu_view->Append(wxID_ZOOM_OUT); - menu_view->Append(wxID_ZOOM_FIT); - menu_view->Append(wxID_ZOOM_100); - - Append(menu_view, wxT("&View")); - - menu_help = new wxMenu(); - menu_help->Append(wxID_ABOUT); - - Append(menu_help, wxT("&Help")); -} diff --git a/src/ctrl.h b/src/ctrl.h deleted file mode 100644 index 2330bc5..0000000 --- a/src/ctrl.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once -#include -#include - -namespace Croplines { -class SliderWithSpin : public wxPanel { - private: - wxStaticText* m_label; - wxSlider* m_slider; - wxSpinCtrl* m_spin; - - public: - SliderWithSpin(wxWindow* parent, wxWindowID id, const wxString& label, int value, int minValue, - int maxValue, const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize); - - bool Enable(bool enable = false) override; - - private: - int value; - - public: - inline int GetValue() { return value; } - void SetValue(int value); - - private: - void CallEvent(int value); - - void OnSliderChanged(wxCommandEvent& event); - - void OnSpinChanged(wxSpinEvent& event); - - wxDECLARE_EVENT_TABLE(); -}; - -class MenuBar : public wxMenuBar { - public: - wxMenu* menu_file; - wxMenu* menu_edit; - wxMenu* menu_view; - wxMenu* menu_help; - - MenuBar(); -}; -} // namespace Croplines diff --git a/src/prj.cpp b/src/prj.cpp deleted file mode 100644 index 416879c..0000000 --- a/src/prj.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include "prj.h" - -#include -#include -#include -#include -#include -#include - -#include -#include - -using namespace Croplines; - -namespace fs = std::filesystem; - -using Page = Prj::Page; - -wxImage Prj::LoadPage(Page& page) { - if (page.IsLoaded()) return page.image; - page.image.LoadFile(wxString(page.image_path)); - return page.image; -} - -bool Prj::SaveCrops(Page& page) { - // if (!page.IsLoaded()) LoadPage(page); - wxImage image = LoadPage(page); - std::size_t count = 1; - fs::create_directories(config.output_dir); - for (wxRect area : GetSelectArea(page)) { - wxImage sub_image = image.GetSubImage(area); - // TODO - // wxSize border_size = wxSize{static_cast(config.border), - // static_cast(config.border)}; - // wxBitmap bitmap(area.GetSize() + 2 * border_size); - // wxMemoryDC memDC; - // memDC.SelectObject(bitmap); - // memDC.SetBrush(*wxWHITE_BRUSH); - // memDC.DrawRectangle(wxPoint{}, bitmap.GetSize()); - // memDC.DrawBitmap(wxBitmap(sub_image), wxPoint{} + border_size); - - // optimize compress for tiff - int sample_per_pixel = image.GetOptionInt(wxIMAGE_OPTION_TIFF_SAMPLESPERPIXEL); - int bits_per_sample = image.GetOptionInt(wxIMAGE_OPTION_TIFF_BITSPERSAMPLE); - sub_image.SetOption(wxIMAGE_OPTION_TIFF_SAMPLESPERPIXEL, sample_per_pixel); - sub_image.SetOption(wxIMAGE_OPTION_TIFF_BITSPERSAMPLE, bits_per_sample); - if (sample_per_pixel == 1 && bits_per_sample == 1) - sub_image.SetOption(wxIMAGE_OPTION_TIFF_COMPRESSION, 4); - else - sub_image.SetOption(wxIMAGE_OPTION_TIFF_COMPRESSION, - image.GetOptionInt(wxIMAGE_OPTION_TIFF_COMPRESSION)); - - fs::path file_path = - config.output_dir / std::format("{}-{}{}", page.image_path.stem().string(), count, - page.image_path.extension().string()); - // bitmap.SaveFile(wxString(file_path), image.GetType()); - sub_image.SaveFile(wxString(file_path), image.GetType()); - count++; - } - return true; -} - -/*自动选择黑色像素区域 - filter_noise_size: 忽略黑像素的大小 - expand_size: 留边空白大小 -*/ -static std::optional CalcuateSelectArea(cv::Mat image, int filter_noise_size, - int expand_size, int base_line) { - cv::Mat img_dst; - cv::cvtColor(image, img_dst, cv::COLOR_RGB2GRAY); - cv::threshold(img_dst, img_dst, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); - std::vector> contours; - cv::findContours(img_dst, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); - int x_min = INT_MAX, x_max = INT_MIN, y_min = INT_MAX, y_max = INT_MIN; - bool has_point = false; - for (const auto& contour : contours) { - if (cv::contourArea(contour) >= filter_noise_size) { - int x_min_inner = x_min, x_max_inner = x_max, y_min_inner = y_min, y_max_inner = y_max; - for (const auto& point : contour) { - if (point.x == 0 || point.x == image.cols - 1 || point.y == 0 || - point.y == image.rows - 1) { - goto skip_contour; - } - if (point.x < x_min_inner) x_min_inner = point.x; - if (point.x > x_max_inner) x_max_inner = point.x; - if (point.y < y_min_inner) y_min_inner = point.y; - if (point.y > y_max_inner) y_max_inner = point.y; - } - x_min = x_min_inner; - x_max = x_max_inner; - y_min = y_min_inner; - y_max = y_max_inner; - has_point = true; - skip_contour: - (void)0; - } - } - if (!has_point) return std::nullopt; - - int l = x_min - expand_size; - int t = y_min - expand_size + base_line; - int r = x_max + expand_size; - int b = y_max + expand_size + base_line; - if (l < 0) l = 0; - if (t < 0) t = 0; - if (r > image.cols) r = image.cols; - if (b > image.rows + base_line) b = image.rows + base_line; - return wxRect{wxPoint{l, t}, wxPoint{r, b}}; -} - -const std::vector& Prj::GetSelectArea(Page& page) { - if (page.modified) { - page.modified = false; - page.select_area.clear(); - wxImage ii = LoadPage(page); - cv::Mat image(ii.GetHeight(), ii.GetWidth(), CV_8UC3, static_cast(ii.GetData())); - int prev_line = 0; - for (int line : page.crop_lines) { - cv::Mat sub_image = image.rowRange(prev_line, line); - auto area = CalcuateSelectArea(sub_image, config.filter_noise_size, 0, prev_line); - if (area) page.select_area.push_back(*area); - prev_line = line; - } - std::int32_t line = image.rows; - cv::Mat sub_image = image.rowRange(prev_line, line); - auto area = CalcuateSelectArea(sub_image, config.filter_noise_size, 0, prev_line); - if (area) page.select_area.push_back(*area); - } - return page.select_area; -} - -std::optional::iterator> Page::SearchNearestLine(int key, int limit) const { - if (crop_lines.empty()) return std::nullopt; - - auto it1 = std::lower_bound(crop_lines.begin(), crop_lines.end(), key); - if (it1 == crop_lines.begin()) { - int d = *it1 - key; - if (d < limit) return it1; - } - if (it1 == crop_lines.end()) { - --it1; - int d = key - *it1; - if (d < limit) return it1; - } - int d1 = *it1 - key; - auto it2 = it1; - --it2; - int d2 = key - *it2; - if (d1 < d2 && d1 < limit) return it1; - if (d2 < d1 && d2 < limit) return it2; - - return std::nullopt; -} - -bool Prj::InsertLineRecord::Do([[maybe_unused]] Prj& prj) { - auto [_, actual_modified] = page.get().crop_lines.insert(line); - page.get().modified = true; - return actual_modified; -} - -void Prj::InsertLineRecord::Undo([[maybe_unused]] Prj& prj) { - page.get().crop_lines.erase(line); - page.get().modified = true; -} - -bool Prj::EraseLineRecord::Do([[maybe_unused]] Prj& prj) { - page.get().crop_lines.erase(it); - page.get().modified = true; - return true; -} - -void Prj::EraseLineRecord::Undo([[maybe_unused]] Prj& prj) { - page.get().crop_lines.insert(line); - page.get().modified = true; -} - -struct UndoVisitor { - std::reference_wrapper prj; - - template - void operator()(A& action) { - action.Undo(prj); - } -}; - -struct RedoVisitor { - std::reference_wrapper prj; - - template - void operator()(A& action) { - action.Do(prj); - } -}; - -void Prj::Undo() { - if (!undo_stack.empty()) { - auto action = undo_stack.top(); - undo_stack.pop(); - std::visit(UndoVisitor(*this), action); - redo_stack.push(action); - } -} - -void Prj::Redo() { - if (!redo_stack.empty()) { - auto action = redo_stack.top(); - redo_stack.pop(); - std::visit(RedoVisitor(*this), action); - undo_stack.push(action); - } -} - -static std::strong_ordering NaturalCompare(std::string_view a, std::string_view b) { - for (const char *i1 = a.begin(), *i2 = b.begin(); i1 != a.end() && i2 != b.end(); ++i1, ++i2) { - if (std::isdigit(*i1) && std::isdigit(*i2)) { - const char *ii1 = i1, *ii2 = i2; - while (ii1 != a.end() && *ii1 == '0') ++ii1; - while (ii2 != b.end() && *ii2 == '0') ++ii2; - auto zero_count1 = std::distance(i1, ii1); - auto zero_count2 = std::distance(i2, ii2); - - i1 = ii1; - i2 = ii2; - while (ii1 != a.end() && std::isdigit(*ii1)) ++ii1; - while (ii2 != b.end() && std::isdigit(*ii2)) ++ii2; - auto num1 = std::string_view(i1, ii1 - i1); - auto num2 = std::string_view(i2, ii2 - i2); - - if (auto cmp = num1.length() <=> num2.length(); cmp != 0) return cmp; - if (auto cmp = num1 <=> num2; cmp != 0) return cmp; - if (auto cmp = zero_count1 <=> zero_count2; cmp != 0) return cmp; - } else { - return std::toupper(*i1) <=> std::toupper(*i2); - } - } - return std::strong_ordering::equal; -} - -static std::vector> SortPages(std::vector& pages) { - std::vector> pages_sorted; - pages_sorted.reserve(pages.size()); - for (Page& page : pages) { - pages_sorted.push_back(std::ref(page)); - } - // natural sort - std::ranges::sort( - pages_sorted, - [](std::string_view s1, std::string_view s2) { return NaturalCompare(s1, s2) < 0; }, - [](const Page& page) { return page.image_path.filename().string(); }); - return pages_sorted; -} - -void Prj::Initialize() { - // Initialize project data with default values - config.output_dir = DEFAULT_OUTPUT_DIR; - config.border = 10; - config.filter_noise_size = 8; - - auto extension_filter = [](const fs::directory_entry& entry) { - return entry.is_regular_file() && - std::find(std::begin(VALID_EXTENSION), std::end(VALID_EXTENSION), - entry.path().extension().string()) != std::end(VALID_EXTENSION); - }; - for (auto const& dir_entry : - fs::directory_iterator(cwd) | std::views::filter(extension_filter)) { - Page page(fs::relative(dir_entry.path(), cwd)); - pages.push_back(page); - } - is_change = true; -} - -std::optional Prj::Load(const fs::path& path) { - if (!fs::exists(path)) return std::nullopt; - fs::current_path(path); - Prj prj; - prj.cwd = path; - fs::path prj_path = path / PROJECT_FILE_NAME; - if (fs::exists(prj_path) && fs::is_regular_file(prj_path)) { - std::ifstream file(prj_path); - if (file) { - cereal::JSONInputArchive archive(file); - archive(cereal::make_nvp("prj", prj)); - prj.is_change = false; - } else { - prj.Initialize(); - } - } else { - prj.Initialize(); - } - prj.pages_sorted = SortPages(prj.pages); - return prj; -} - -void Prj::Save() { - fs::path prj_path = cwd / PROJECT_FILE_NAME; - std::ofstream file(prj_path); - cereal::JSONOutputArchive archive(file); - archive(cereal::make_nvp("prj", *this)); - is_change = false; -} diff --git a/src/prj.h b/src/prj.h deleted file mode 100644 index 788ef58..0000000 --- a/src/prj.h +++ /dev/null @@ -1,162 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace Croplines { -constexpr const char* VALID_EXTENSION[] = {".png", ".jpg", ".jpeg", ".bmp", - ".tiff", ".tif", ".webp"}; -constexpr const char* PROJECT_FILE_NAME = "croplines.json"; -constexpr const char* DEFAULT_OUTPUT_DIR = "out"; - -class Prj; - -template -concept ActionRecord = requires(T record, Prj& prj) { - { record.Do(prj) } -> std::same_as; - { record.Undo(prj) } -> std::same_as; -}; - -class Prj { - public: - class Page { - friend class Prj; - std::set crop_lines; // 从小到大排序 - std::vector select_area; - bool modified = true; - wxImage image; // can be empty - public: - std::filesystem::path image_path; - - Page(std::filesystem::path image_path) : image_path(std::move(image_path)) {} - Page() = default; - - bool IsLoaded() const { return image.IsOk(); }; - void Close() { image.Destroy(); } - - const std::set GetCropLines() const { return crop_lines; } - std::optional::iterator> SearchNearestLine(int key, int limit) const; - - template - void serialize(Archive& archive) { - archive(cereal::make_nvp("image_path", image_path)); - archive(CEREAL_NVP(crop_lines)); - } - }; - - struct Config { - std::filesystem::path output_dir; - int border; - int filter_noise_size; - - template - void serialize(Archive& archive) { - archive(CEREAL_NVP(border)); - archive(CEREAL_NVP(filter_noise_size)); - archive(cereal::make_nvp("output_dir", output_dir)); - } - } config; - - class InsertLineRecord { - int line; - std::reference_wrapper page; - - public: - InsertLineRecord(int line, Page& page) : line(line), page(page) {} - bool Do(Prj& prj); - void Undo(Prj& prj); - }; - - class EraseLineRecord { - using SetIteratorType = decltype(Page::crop_lines)::iterator; - SetIteratorType it; - std::reference_wrapper page; - int line; - - public: - EraseLineRecord(SetIteratorType it, Page& page) : it(it), page(page), line(*it) {} - bool Do(Prj& prj); - void Undo(Prj& prj); - }; - - using Action = std::variant; - - private: - std::vector pages; - std::vector> pages_sorted; - std::filesystem::path cwd; // current working directory - bool is_change; - - std::stack undo_stack; - std::stack redo_stack; - - public: - wxMenuBar* menubar = nullptr; - - public: - static std::optional Load(const std::filesystem::path& path); - void Save(); - std::vector> GetPages() const { return pages_sorted; } - inline bool IsChange() { return is_change; } - inline void Change() { is_change = true; } - - wxImage LoadPage(Page& page); - bool SaveCrops(Page& page); - const std::vector& GetSelectArea(Page& page); - - template - void Execute(A action) { - bool success = action.Do(*this); - if (success) { - is_change = true; - undo_stack.push(action); - redo_stack = std::stack(); - if (menubar) { - menubar->Enable(wxID_UNDO, true); - menubar->Enable(wxID_REDO, false); - } - } - } - bool CanUndo() const { return !undo_stack.empty(); } - bool CanRedo() const { return !redo_stack.empty(); } - void Undo(); - void Redo(); - - template - void serialize(Archive& archive) { - archive(CEREAL_NVP(config)); - archive(CEREAL_NVP(pages)); - } - - private: - void Initialize(); -}; - -} // namespace Croplines - -// Add support for cereal serializing std::filesystem::path -namespace std { -namespace filesystem { -template -inline void load_minimal(const Archive&, std::filesystem::path& path, const std::string& name) { - path = name; -} - -template -inline std::string save_minimal(const Archive&, const std::filesystem::path& path) { - return path.string(); -} -} // namespace filesystem -} // namespace std diff --git a/src/ui/App.cpp b/src/ui/App.cpp new file mode 100644 index 0000000..d4e3f55 --- /dev/null +++ b/src/ui/App.cpp @@ -0,0 +1,22 @@ +#include "ui/App.hpp" + +#include + +#include "ui/MainFrame.hpp" + +using namespace croplines; + +IMPLEMENT_APP(CroplinesApp) + +bool CroplinesApp::OnInit() { + SetAppearance(Appearance::System); + wxImage::AddHandler(new wxPNGHandler); + wxImage::AddHandler(new wxTIFFHandler); + wxImage::AddHandler(new wxJPEGHandler); + wxImage::AddHandler(new wxWEBPHandler); + + m_frame = new MainFrame(nullptr, wxID_ANY, wxT("Croplines"), wxDefaultPosition, {900, 600}); + m_frame->Show(true); + + return true; +} diff --git a/src/ui/App.hpp b/src/ui/App.hpp new file mode 100644 index 0000000..f2dcda5 --- /dev/null +++ b/src/ui/App.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "ui/MainFrame.hpp" + +namespace croplines { +class CroplinesApp : public wxApp { + private: + MainFrame* m_frame; + + public: + bool OnInit() override; +}; +} // namespace croplines diff --git a/src/ui/Canvas.cpp b/src/ui/Canvas.cpp new file mode 100644 index 0000000..df3ff6a --- /dev/null +++ b/src/ui/Canvas.cpp @@ -0,0 +1,444 @@ +#include "ui/Canvas.hpp" + +#include +#include +#include + +#include +#include +// Include wx before gl +#include + +#include "core/ImageScaleModel.hpp" +#include "core/Page.hpp" + +using namespace croplines; + +Canvas::Canvas(wxWindow* parent, wxWindowID id) + : wxGLCanvas(parent, id, nullptr, wxDefaultPosition, wxDefaultSize, + wxFULL_REPAINT_ON_RESIZE | wxVSCROLL | wxHSCROLL), + m_glContext(std::make_unique(this)) { + AlwaysShowScrollbars(); + + // disable on default + SetScrollbar(wxHORIZONTAL, -1, -1, -1); + SetScrollbar(wxVERTICAL, -1, -1, -1); +} + +Canvas::~Canvas() { + if (m_glTexture) glDeleteTextures(1, &m_glTexture); +} + +static void SetTextrue(GLuint texture, void* pixels, int width, int height) { + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // 解决glTexImage2D崩溃问题 + glTexImage2D(GL_TEXTURE_2D, 0, 3, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels); +} + +void Canvas::SetPage(Page& page) { + wxImage img = page.GetImage(); + if (!img.IsOk()) { + return; + } + if (IsLoaded()) { + GetScaleModel().OnImageResize(img.GetSize()); + } else { + m_scaleModel = ImageScaleModel{img.GetSize(), GetClientSize()}; + } + m_page = &page; + + SetCurrent(*m_glContext); + if (m_glTexture) glDeleteTextures(1, &m_glTexture); + glGenTextures(1, &m_glTexture); + SetTextrue(m_glTexture, img.GetData(), img.GetWidth(), img.GetHeight()); + Refresh(); +} + +void Canvas::Clear() { + if (m_glTexture) glDeleteTextures(1, &m_glTexture); + m_page = nullptr; + m_scaleModel.reset(); + Refresh(); +} + +void Canvas::ZoomIn() { + if (!IsLoaded()) return; + GetScaleModel().Scale(ImageScaleModel::ZOOM_IN_RATE); + Refresh(); +} + +void Canvas::ZoomOut() { + if (!IsLoaded()) return; + GetScaleModel().Scale(ImageScaleModel::ZOOM_OUT_RATE); + Refresh(); +} + +void Canvas::ZoomFit() { + if (!IsLoaded()) return; + GetScaleModel().ScaleTo(GetScaleModel().GetScaleSuitesPage()); + GetScaleModel().MoveToCenter(); + Refresh(); +} + +void Canvas::Zoom(double scale) { + if (IsLoaded()) { + GetScaleModel().ScaleTo(scale); + Refresh(); + } +} + +void Canvas::UpdateScrollbars() { + if (!IsLoaded()) { + SetScrollbar(wxHORIZONTAL, -1, -1, -1); + SetScrollbar(wxVERTICAL, -1, -1, -1); + return; + } + if (GetScaleModel().scaledSize.GetWidth() > GetScaleModel().windowSize.GetWidth()) { + SetScrollbar(wxHORIZONTAL, -GetScaleModel().offset.x, GetScaleModel().windowSize.GetWidth(), + GetScaleModel().scaledSize.GetWidth()); + } else { + SetScrollbar(wxHORIZONTAL, -1, -1, -1); + } + if (GetScaleModel().scaledSize.GetHeight() > GetScaleModel().windowSize.GetHeight()) { + SetScrollbar(wxVERTICAL, -GetScaleModel().offset.y, GetScaleModel().windowSize.GetHeight(), + GetScaleModel().scaledSize.GetHeight()); + } else { + SetScrollbar(wxVERTICAL, -1, -1, -1); + } +} + +static void InitGL() { + // 设置OpenGL状态 + glClearColor(0.0F, 0.0F, 0.0F, 1.0F); + glEnable(GL_DEPTH_TEST); + glEnable(GL_TEXTURE_2D); + // 设置深度测试函数 + glDepthFunc(GL_LEQUAL); + // GL_SMOOTH(光滑着色)/GL_FLAT(恒定着色) + glShadeModel(GL_SMOOTH); + // 开启混合 + glEnable(GL_BLEND); + // 设置混合函数 + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_ALPHA_TEST); // 启用Alpha测试 + // 设置Alpha测试条件为大于0.05则通过 + glAlphaFunc(GL_GREATER, 0.05); + // 设置逆时针索引为正面(GL_CCW/GL_CW) + glFrontFace(GL_CW); + // 开启线段反走样 + glEnable(GL_LINE_SMOOTH); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); +} + +static void UpdateProjection(wxSize size) { + if (size.GetWidth() <= 0 || size.GetHeight() <= 0) return; + + glViewport(0, 0, size.GetWidth(), size.GetHeight()); + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0, size.GetWidth(), size.GetHeight(), 0, -1, 1); +} + +void Canvas::OnPaint(wxPaintEvent&) { + wxPaintDC dc(this); + + SetCurrent(*m_glContext); + + // 初始化OpenGL(首次绘制时执行) + if (!m_isInitialized) { + InitGL(); + m_isInitialized = true; + } + + // 清除缓冲区 + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // 设置正交投影(模拟视口坐标系) + UpdateProjection(GetClientSize()); + + // 设置投影(透视投影) + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glTranslated(GetScaleModel().offset.x, GetScaleModel().offset.y, 0); + glScaled(GetScaleModel().scale, GetScaleModel().scale, 1.0); + + if (IsLoaded()) { + // 绑定纹理 + wxSize size = GetScaleModel().imageSize; + glBegin(GL_QUADS); + glColor3d(1.0, 1.0, 1.0); + glBindTexture(GL_TEXTURE_2D, m_glTexture); + glTexCoord2d(0, 0); + glVertex2d(0, 0); + glTexCoord2d(1, 0); + glVertex2d(size.GetWidth(), 0); + glTexCoord2d(1, 1); + glVertex2d(size.GetWidth(), size.GetHeight()); + glTexCoord2d(0, 1); + glVertex2d(0, size.GetHeight()); + glEnd(); + + UpdateScrollbars(); + + glColor4d(0.8, 0.84, 0.8, 0.25); + for (const wxRect& area : m_page->getSelectAreas()) { + glBegin(GL_QUADS); + glVertex2d(area.GetLeft(), area.GetTop()); + glVertex2d(area.GetLeft(), area.GetBottom()); + glVertex2d(area.GetRight(), area.GetBottom()); + glVertex2d(area.GetRight(), area.GetTop()); + glEnd(); + } + + auto DrawLine = [&size, this](int width, double line_y) { + double w = FromDIP(width) / GetScaleModel().scale; + glBegin(GL_QUADS); + glVertex2d(0, line_y - w); + glVertex2d(0, line_y + w); + glVertex2d(size.GetWidth(), line_y + w); + glVertex2d(size.GetWidth(), line_y - w); + glEnd(); + }; + + // draw crop lines + std::optional deleting_line; + if (m_isDeleting && m_mouseCurrentPosition) { + int y = m_mouseCurrentPosition->y; + y = std::lround(GetScaleModel().ReverseTransformY(y)); + + auto search_line = m_page->SearchNearestLine( + y, FromDIP(static_cast(5.0 / GetScaleModel().scale) + 1)); + if (search_line.has_value()) { + deleting_line = *search_line; + glColor4d(0.15, 0.58, 0.36, 0.5); + DrawLine(4, *deleting_line); + } + } + + glColor4d(0.30, 0.74, 0.52, 0.5); + for (int line : m_page->GetCropLines()) { + if (deleting_line == line) continue; + DrawLine(2, line); + } + + // draw mouse line + if (m_mouseCurrentPosition) { + if (m_isDeleting) + glColor4d(0.90, 0.08, 0, 0.5); + else + glColor4d(0.34, 0.61, 0.84, 0.5); + double y = GetScaleModel().ReverseTransformY(m_mouseCurrentPosition->y); + DrawLine(2, y); + } + } + + SwapBuffers(); +} + +void Canvas::OnSize(wxSizeEvent& event) { + if (m_glContext) { + SetCurrent(*m_glContext); + UpdateProjection(GetClientSize()); + } + if (IsLoaded()) GetScaleModel().OnWindowResize(event.GetSize()); + Refresh(); +} + +void Canvas::OnMouseWheel(wxMouseEvent& event) { + if (!IsLoaded()) return; + + double factor; + if (event.GetWheelRotation() > 0) { // zoom in + factor = ImageScaleModel::ZOOM_IN_RATE; + } else { // zoom out + factor = ImageScaleModel::ZOOM_OUT_RATE; + } + GetScaleModel().Scale(factor, event.GetPosition()); + Refresh(); +} + +void Canvas::OnMouseLeftDown(wxMouseEvent& event) { + if (!IsLoaded()) return; + + if (!m_isMouseCaptured) { + CaptureMouse(); + m_isMouseCaptured = true; + } + // m_parent->GetParent()->SetFocus(); + SetFocus(); + m_mouseDragStartPosition = event.GetPosition(); +} + +void Canvas::OnMouseLeftUp(wxMouseEvent&) { + if (!IsLoaded()) return; + + if (m_isMouseCaptured) { + ReleaseMouse(); + m_isMouseCaptured = false; + } + m_mouseDragStartPosition.reset(); +} + +void Canvas::OnMouseLeftUp(wxMouseCaptureLostEvent&) { + if (!IsLoaded()) return; + + if (m_isMouseCaptured) { + ReleaseMouse(); + m_isMouseCaptured = false; + } + m_mouseDragStartPosition.reset(); +} + +void Canvas::OnMouseRightDown(wxMouseEvent& event) { + if (!IsLoaded()) return; + + SetFocus(); + event.Skip(); +} + +struct EraseLineCommand final : public wxCommand { + Page& page; + int line; + EraseLineCommand(Page& page, int line) + : wxCommand(true, wxT("删除直线")), page(page), line(line) {} + bool Do() override { return page.EraseLine(line); } + bool Undo() override { return page.InsertLine(line); } +}; + +struct InsertLineCommand final : public wxCommand { + Page& page; + int line; + InsertLineCommand(Page& page, int line) + : wxCommand(true, wxT("添加直线")), page(page), line(line) {} + bool Do() override { return page.InsertLine(line); } + bool Undo() override { return page.EraseLine(line); } +}; + +void Canvas::OnMouseRightUp(wxMouseEvent& event) { + if (!IsLoaded()) return; + + wxPoint mousePosition = event.GetPosition(); + if (!GetScaleModel().IsInsideImage(mousePosition)) return; + + int y = mousePosition.y; + y = std::lround(GetScaleModel().ReverseTransformY(y)); + if (m_isDeleting) { + int threshold = static_cast(5.0 / GetScaleModel().scale) + 1; + auto line = m_page->SearchNearestLine(y, FromDIP(threshold)); + if (line.has_value()) GetProcessor()->Submit(new EraseLineCommand{GetPage(), *line}); + } else { + GetProcessor()->Submit(new InsertLineCommand{GetPage(), y}); + } + Refresh(); +} + +void Canvas::OnMouseMotion(wxMouseEvent& event) { + if (!IsLoaded()) return; + + wxPoint mouse_drag = event.GetPosition(); + if (m_mouseDragStartPosition && event.Dragging()) { + auto dr = mouse_drag - *m_mouseDragStartPosition; + GetScaleModel().Move(dr); + *m_mouseDragStartPosition = mouse_drag; + Refresh(); + } + + // if mouse inside image + wxPoint mouse_position = event.GetPosition(); + if (GetScaleModel().IsInsideImage(mouse_position)) { + m_mouseCurrentPosition = mouse_position; + Refresh(); + } else if (m_mouseCurrentPosition) { + m_mouseCurrentPosition = std::nullopt; + Refresh(); + } +} + +void Canvas::OnScroll(wxScrollWinEvent& event) { + if (!IsLoaded()) return; + + const unsigned orientation = static_cast(event.GetOrientation()) & wxORIENTATION_MASK; + switch (orientation & wxORIENTATION_MASK) { + case wxHORIZONTAL: { + const int pos0 = GetScrollPos(wxHORIZONTAL); + const int pos1 = event.GetPosition(); + GetScaleModel().Move(wxPoint{pos0 - pos1, 0}); + Refresh(); + return; + } + case wxVERTICAL: { + const int pos0 = GetScrollPos(wxVERTICAL); + const int pos1 = event.GetPosition(); + GetScaleModel().Move(wxPoint{0, pos0 - pos1}); + Refresh(); + return; + } + default: + return; // ignore other orientations + } +} + +void Canvas::OnKeyUp(wxKeyEvent& event) { + if (!IsLoaded()) return; + + switch (event.GetKeyCode()) { + case 'D': + if (m_isDeleting) { + m_isDeleting = false; + Refresh(); + } + break; + } +} + +void Canvas::OnKeyDown(wxKeyEvent& event) { + if (!IsLoaded()) return; + + switch (event.GetKeyCode()) { + case 'D': + if (!m_isDeleting) { + m_isDeleting = true; + Refresh(); + } + break; + } +} + +void Canvas::OnKillFocus(wxFocusEvent& event) { + if (!IsLoaded()) return; + + if (m_isDeleting) { + m_isDeleting = false; + Refresh(); + } + if (m_isMouseCaptured) { + ReleaseMouse(); + m_isMouseCaptured = false; + } + event.Skip(); +} + +// clang-format off +wxBEGIN_EVENT_TABLE(Canvas, wxPanel) + EVT_PAINT(Canvas::OnPaint) + EVT_SIZE(Canvas::OnSize) + + EVT_MOUSEWHEEL(Canvas::OnMouseWheel) + EVT_LEFT_DOWN(Canvas::OnMouseLeftDown) + EVT_LEFT_UP(Canvas::OnMouseLeftUp) + EVT_RIGHT_DOWN(Canvas::OnMouseRightDown) + EVT_RIGHT_UP(Canvas::OnMouseRightUp) + EVT_MOUSE_CAPTURE_LOST(Canvas::OnMouseLeftUp) + EVT_MOTION(Canvas::OnMouseMotion) + + EVT_SCROLLWIN(Canvas::OnScroll) + + EVT_KEY_DOWN(Canvas::OnKeyDown) + EVT_KEY_UP(Canvas::OnKeyUp) + + EVT_KILL_FOCUS(Canvas::OnKillFocus) +wxEND_EVENT_TABLE(); diff --git a/src/ui/Canvas.hpp b/src/ui/Canvas.hpp new file mode 100644 index 0000000..291a62b --- /dev/null +++ b/src/ui/Canvas.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +#include +#include +#include +// include wx before gl +#include + + +#include "core/Document.hpp" +#include "core/ImageScaleModel.hpp" +#include "core/Page.hpp" + +namespace croplines { +class Canvas : public wxGLCanvas { + public: + Canvas(wxWindow* parent, wxWindowID id); + ~Canvas() override; + void SetPage(Page& page); + Page& GetPage() { return *m_page; } + + Document& GetDocument() { return GetPage().GetDocument(); } + wxCommandProcessor* GetProcessor() { return GetDocument().GetProcessor(); } + + void Clear(); + [[nodiscard]] bool IsLoaded() const { return m_page != nullptr; } + ImageScaleModel& GetScaleModel() { return m_scaleModel.value(); } + + void ZoomIn(); + void ZoomOut(); + void ZoomFit(); + void Zoom(double scale); + + private: + bool m_isDeleting = false; + bool m_isInitialized = false; + + bool m_isMouseCaptured = false; + std::optional m_mouseDragStartPosition; + std::optional m_mouseCurrentPosition; + + Page* m_page = nullptr; + + std::optional m_scaleModel; + + std::unique_ptr m_glContext; + GLuint m_glTexture = 0; + wxImage m_imageDst; + bool m_isImageModified = false; + + private: + void UpdateScrollbars(); + + void OnPaint(wxPaintEvent& event); + void OnSize(wxSizeEvent& event); + + void OnMouseWheel(wxMouseEvent& event); + void OnMouseLeftDown(wxMouseEvent& event); + void OnMouseLeftUp(wxMouseEvent& event); + void OnMouseLeftUp(wxMouseCaptureLostEvent& event); + void OnMouseRightDown(wxMouseEvent& event); + void OnMouseRightUp(wxMouseEvent& event); + void OnMouseMotion(wxMouseEvent& event); + + void OnScroll(wxScrollWinEvent& event); + + void OnKeyDown(wxKeyEvent& event); + void OnKeyUp(wxKeyEvent& event); + + void OnKillFocus(wxFocusEvent& event); + + wxDECLARE_EVENT_TABLE(); +}; +} // namespace croplines diff --git a/src/ui/ConfigPanel.cpp b/src/ui/ConfigPanel.cpp new file mode 100644 index 0000000..4224d8a --- /dev/null +++ b/src/ui/ConfigPanel.cpp @@ -0,0 +1,54 @@ +#include "ui/ConfigPanel.hpp" + +#include +#include + +#include "core/DocumentData.hpp" +#include "ui/components/SliderWithSpin.hpp" +#include "ui/Defs.hpp" + +using namespace croplines; + +ConfigPanel::ConfigPanel(wxWindow* parent, wxWindowID id) + : wxNotebook(parent, id), + m_processPage(new ProcessConfigPage{this, wxID_ANY}), + m_outputPage(new OutputConfigPage{this, wxID_ANY}) { + AddPage(m_processPage, wxT("处理")); + AddPage(m_outputPage, wxT("输出")); +} + +ProcessConfigPage::ProcessConfigPage(wxWindow* parent, wxWindowID id) : wxPanel(parent, id) { + m_sliderPixFilter = + new SliderWithSpin{this, sliderID_cfg_PIX_FILTER, wxT("忽略斑点直径"), 8, 0, 50}; + + auto* bSizer = new wxBoxSizer{wxVERTICAL}; + bSizer->Add(m_sliderPixFilter, 0, wxEXPAND, 5); + + SetSizer(bSizer); + Layout(); + bSizer->Fit(this); +} + +OutputConfigPage::OutputConfigPage(wxWindow* parent, wxWindowID id) : wxPanel(parent, id) { + m_sliderBorder = new SliderWithSpin{this, sliderID_cfg_BORDER, wxT("空白边距"), 10, 0, 100}; + + auto* bSizer = new wxBoxSizer{wxVERTICAL}; + bSizer->Add(m_sliderBorder, 0, wxEXPAND, 5); + + SetSizer(bSizer); + Layout(); + bSizer->Fit(this); +} + +void ConfigPanel::SyncUI(const DocumentConfig& config) { + m_processPage->SyncUI(config); + m_outputPage->SyncUI(config); +} + +void ProcessConfigPage::SyncUI(const DocumentConfig& config) { + m_sliderPixFilter->SetValue(config.filter_noise_size); +} + +void OutputConfigPage::SyncUI(const DocumentConfig& config) { + m_sliderBorder->SetValue(config.border); +} diff --git a/src/ui/ConfigPanel.hpp b/src/ui/ConfigPanel.hpp new file mode 100644 index 0000000..ac7384c --- /dev/null +++ b/src/ui/ConfigPanel.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include "core/DocumentData.hpp" +#include "ui/components/SliderWithSpin.hpp" + +namespace croplines { + +class ProcessConfigPage; +class OutputConfigPage; + +class ConfigPanel : public wxNotebook { + public: + ConfigPanel(wxWindow* parent, wxWindowID id); + + void SyncUI(const DocumentConfig& config); + + private: + ProcessConfigPage* m_processPage; + OutputConfigPage* m_outputPage; +}; + +class ProcessConfigPage : public wxPanel { + public: + ProcessConfigPage(wxWindow* parent, wxWindowID id); + + void SyncUI(const DocumentConfig& config); + + private: + SliderWithSpin* m_sliderPixFilter; +}; + +class OutputConfigPage : public wxPanel { + public: + OutputConfigPage(wxWindow* parent, wxWindowID id); + + void SyncUI(const DocumentConfig& config); + + private: + SliderWithSpin* m_sliderBorder; +}; + +} // namespace croplines diff --git a/src/ui/Defs.hpp b/src/ui/Defs.hpp new file mode 100644 index 0000000..fedc4b5 --- /dev/null +++ b/src/ui/Defs.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace croplines { +enum : int { + buttonID_CROP_CURR_PAGE = 1000, + buttonID_CROP_ALL_PAGE, + + panelID_PAGE_LIST, + + sliderID_cfg_PIX_FILTER, + sliderID_cfg_BORDER +}; +} diff --git a/src/ui/MainFrame.cpp b/src/ui/MainFrame.cpp new file mode 100644 index 0000000..e69ecd9 --- /dev/null +++ b/src/ui/MainFrame.cpp @@ -0,0 +1,367 @@ +#include "ui/MainFrame.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +// #include + +#include +#include +#include + +#include "core/Document.hpp" +#include "core/DocumentData.hpp" +#include "core/Page.hpp" +#include "ui/Canvas.hpp" +#include "ui/components/SliderWithSpin.hpp" +#include "ui/ConfigPanel.hpp" +#include "ui/Defs.hpp" +#include "ui/ToolBar.hpp" +#include "utils/Asserts.hpp" + +using namespace croplines; + +MainFrame::MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, + const wxSize& size) + : wxFrame(parent, id, title, pos) { + SetClientSize(FromDIP(size)); + SetIcon(wxICON(MAIN_ICON)); + SetMenuBar(m_menuBar); + + m_mgr.SetManagedWindow(this); + + m_toolBar = new ToolBar{this, wxID_ANY}; + m_mgr.AddPane(m_toolBar, wxAuiPaneInfo() + .Top() + .CaptionVisible(false) + .CloseButton(false) + .PaneBorder(false) + .Movable(false) + .Dock() + .Resizable() + .FloatingSize(wxSize(-1, -1)) + .DockFixed(true) + .LeftDockable(false) + .RightDockable(false) + .Floatable(false) + .Layer(10) + .ToolbarPane()); + + m_statusBar = CreateStatusBar(1, wxSTB_SIZEGRIP, wxID_ANY); + + auto* m_canvasPanel = new wxPanel{this, wxID_ANY}; + m_mgr.AddPane(m_canvasPanel, wxAuiPaneInfo() + .Left() + .CaptionVisible(false) + .PinButton(true) + .Dock() + .Resizable() + .FloatingSize(wxDefaultSize) + .CentrePane()); + auto* bSizer = new wxBoxSizer{wxVERTICAL}; + m_canvas = new Canvas{m_canvasPanel, wxID_ANY}; + bSizer->Add(m_canvas, 1, wxEXPAND, 5); + m_canvasPanel->SetSizer(bSizer); + m_canvasPanel->Layout(); + bSizer->Fit(m_canvasPanel); + + m_configPanel = new ConfigPanel{this, wxID_ANY}; + m_mgr.AddPane(m_configPanel, wxAuiPaneInfo() + .Right() + .Caption(wxT("设置")) + .PinButton(true) + .Dock() + .Resizable() + .FloatingSize(wxDefaultSize) + .BottomDockable(false) + .TopDockable(false) + .BestSize(FromDIP(wxSize(350, 500))) + .MinSize(FromDIP(wxSize(350, -1)))); + + m_pageListPanel = new wxListBox{this, panelID_PAGE_LIST, wxDefaultPosition, wxDefaultSize, + 0, nullptr, wxLB_NEEDED_SB}; + m_mgr.AddPane(m_pageListPanel, wxAuiPaneInfo() + .Left() + .Caption(wxT("页面列表")) + .PinButton(true) + .Dock() + .Resizable() + .FloatingSize(wxDefaultSize) + .BottomDockable(false) + .TopDockable(false) + .BestSize(FromDIP(wxSize(200, 500))) + .MinSize(FromDIP(wxSize(130, -1)))); + + m_mgr.Update(); + Center(wxBOTH); + + m_toolBar->Disable(); + m_configPanel->Disable(); + m_menuBar->Disable(); +} + +bool MainFrame::Load(const std::filesystem::path& path) { + if (m_doc.IsLoad()) { + if (!Close()) return false; + } + + m_doc.Load(path); + m_toolBar->Enable(); + m_configPanel->Enable(); + m_menuBar->Enable(); + SyncUIPageListPanel(); + SyncUIConfigPanel(); + if (m_doc.PagesSize() != 0) CurrentPage(0); + SetTitle(wxString(path.filename())); + return true; +} + +static bool ShowCloseDialog(wxWindow* parent, Document& doc) { + wxMessageDialog dialog(parent, wxT("项目已更改,是否保存?"), + wxASCII_STR(wxMessageBoxCaptionStr), + wxICON_QUESTION | wxYES_NO | wxCANCEL); + switch (dialog.ShowModal()) { + case wxID_YES: + return doc.Save(); + case wxID_NO: + return true; + case wxID_CANCEL: + return false; + default: + UNREACHABLE(); + } +} + +bool MainFrame::Save() { + if (m_doc.IsLoad()) { + m_doc.Save(); + return true; + } + return false; +} + +bool MainFrame::Close() { + if (m_doc.IsLoad()) { + if (m_doc.IsModified()) { + if (!ShowCloseDialog(this, m_doc)) return false; + } + SetFocus(); // 防止焦点在需要禁用的组件中 + m_toolBar->Disable(); + m_menuBar->Disable(); + m_configPanel->Disable(); + m_doc.Close(); + m_canvas->Clear(); + m_pageListPanel->Clear(); + } + return true; +} + +void MainFrame::CurrentPage(std::size_t page) { + if (m_doc.IsLoad()) { + m_currentPageIdx = std::clamp(page, static_cast(0), m_doc.PagesSize() - 1); + m_toolBar->EnableTool(wxID_UP, m_currentPageIdx != 0); + m_toolBar->EnableTool(wxID_DOWN, m_currentPageIdx != m_doc.PagesSize() - 1); + m_currentPage.emplace(m_doc.LoadPage(m_currentPageIdx)); + m_pageListPanel->SetSelection(static_cast(m_currentPageIdx)); + m_canvas->SetPage(*m_currentPage); + m_canvas->Refresh(); + } +} + +void MainFrame::SyncUIPageListPanel() { + m_pageListPanel->Clear(); + if (m_doc.IsLoad() && m_doc.PagesSize() != 0) { + std::vector file_names; + file_names.reserve(m_doc.PagesSize()); + std::ranges::transform( + m_doc.GetData().pages, std::back_inserter(file_names), + [](const std::unique_ptr& page) { return wxString(page->path.filename()); }); + + m_pageListPanel->Set(file_names); + } +} + +void MainFrame::SyncUIConfigPanel() { m_configPanel->SyncUI(m_doc.GetConfig()); } + +void MainFrame::OnLoad(wxCommandEvent&) { + if (m_doc.IsLoad() && m_doc.IsModified()) { + if (!ShowCloseDialog(this, m_doc)) return; + } + wxDirDialog dir_dialog(this, wxT("选择目录"), wxEmptyString, + wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); + auto result = dir_dialog.ShowModal(); + if (result == wxID_OK) { + wxString dir = dir_dialog.GetPath(); + Load(std::filesystem::path(dir.utf8_string())); + } +} + +void MainFrame::OnUndo(wxCommandEvent&) { + if (m_doc.IsLoad()) { + m_doc.GetProcessor()->Undo(); + m_canvas->Refresh(); + } +} + +void MainFrame::OnRedo(wxCommandEvent&) { + if (m_doc.IsLoad()) { + m_doc.GetProcessor()->Redo(); + m_canvas->Refresh(); + } +} + +void MainFrame::OnCropCurrPage(wxCommandEvent&) { + if (m_currentPage) { + SetStatusText(std::format("Page {} is croping...", CurrentPage() + 1)); + if (m_currentPage->SaveCrops()) { + SetStatusText(std::format("Page {} finished!", CurrentPage() + 1)); + } else { + SetStatusText(std::format("Page {} failed!", CurrentPage() + 1)); + } + } +} + +void MainFrame::OnCropAllPage(wxCommandEvent&) { + if (!m_doc.IsLoad()) return; + + if (m_canvas->IsLoaded()) { + // std::thread t([this]() { + for (size_t i = 0; i < m_doc.PagesSize(); i++) { + Page page = m_doc.LoadPage(i); + SetStatusText(std::format("Page {} is croping...", i + 1)); + page.SaveCrops(); + } + SetStatusText(wxT("Croping finised!")); + // }); + // t.detach(); + // TODO! + } +} + +void MainFrame::OnExit(wxCloseEvent& event) { + if (Close()) + Destroy(); + else + event.Veto(); +} + +void MainFrame::OnAbout(wxCommandEvent&) { +#include "License.hpp" + wxAboutDialogInfo aboutInfo; + aboutInfo.SetName(wxT("Croplines")); + aboutInfo.SetIcon(wxICON(MAIN_ICON)); + aboutInfo.AddDeveloper(wxT("Likend")); + aboutInfo.SetWebSite(wxT("https://github.com/Likend/Croplines")); + aboutInfo.SetCopyright(wxT("(C) 2024-2026")); + aboutInfo.SetLicence(LICENSE); + wxAboutBox(aboutInfo); +} + +void MainFrame::OnClickListBox(wxCommandEvent& event) { + if (CurrentPage() != event.GetSelection()) { + CurrentPage(event.GetSelection()); + } +} + +template +class ChangeConfigCommand : public wxCommand { + public: + ChangeConfigCommand(SliderWithSpin* slider, Document& doc, T& placeToMidify, T newValue) + : wxCommand(true, wxT("更改设置")), + m_doc(doc), + m_slider(slider), + m_placeToModify(placeToMidify), + m_newValue(newValue), + m_oldValue(placeToMidify) { + std::cout << "ChangeConfigCommand " << m_oldValue << " to " << m_newValue << std::endl; + } + + bool Do() override { + std::cout << "Change current " << m_placeToModify << " from " << m_oldValue << " to " + << m_newValue << std::endl; + if (m_slider && m_slider->GetValue() != m_newValue) m_slider->SetValue(m_newValue); + m_placeToModify = m_newValue; + m_doc.SetModified(); + return true; + } + + bool Undo() override { + std::cout << "Change current " << m_placeToModify << " from " << m_newValue << " to " + << m_oldValue << std::endl; + if (m_slider && m_slider->GetValue() != m_oldValue) m_slider->SetValue(m_oldValue); + m_placeToModify = m_oldValue; + m_doc.SetModified(); + return true; + } + + private: + Document& m_doc; + SliderWithSpin* m_slider; + + T& m_placeToModify; + T m_newValue; + T m_oldValue; +}; + +void MainFrame::OnChnageCfgFilerPixSize(wxCommandEvent& event) { + wxWindow* win = FindWindowById(sliderID_cfg_PIX_FILTER); + SliderWithSpin* slider = wxDynamicCast(win, SliderWithSpin); + auto* command = new ChangeConfigCommand{slider, m_doc, m_doc.GetConfig().filter_noise_size, + event.GetInt()}; + m_doc.GetProcessor()->Submit(command); +} + +void MainFrame::OnChangeCfgBorder(wxCommandEvent& event) { + wxWindow* win = FindWindowById(sliderID_cfg_BORDER); + SliderWithSpin* slider = wxDynamicCast(win, SliderWithSpin); + auto* command = + new ChangeConfigCommand{slider, m_doc, m_doc.GetConfig().border, event.GetInt()}; + m_doc.GetProcessor()->Submit(command); +} + +void MainFrame::OnUpdateUndo(wxUpdateUIEvent&) { + m_menuBar->Enable(wxID_UNDO, m_doc.GetProcessor()->CanUndo()); +} +void MainFrame::OnUpdateRedo(wxUpdateUIEvent&) { + m_menuBar->Enable(wxID_REDO, m_doc.GetProcessor()->CanRedo()); +} +void MainFrame::OnUpdateSave(wxUpdateUIEvent&) { + m_menuBar->Enable(wxID_SAVE, m_doc.IsModified()); + m_toolBar->EnableTool(wxID_SAVE, m_doc.IsModified()); + m_toolBar->Refresh(); +} + +// clang-format off +wxBEGIN_EVENT_TABLE(MainFrame, wxFrame) + EVT_MENU(wxID_UP, MainFrame::OnPrevPage) + EVT_MENU(wxID_DOWN, MainFrame::OnNextPage) + EVT_MENU(wxID_SAVE, MainFrame::OnSave) + EVT_MENU(wxID_OPEN, MainFrame::OnLoad) + EVT_MENU(wxID_UNDO, MainFrame::OnUndo) + EVT_MENU(wxID_REDO, MainFrame::OnRedo) + EVT_MENU(wxID_ZOOM_IN, MainFrame::OnZoomIn) + EVT_MENU(wxID_ZOOM_OUT, MainFrame::OnZoomOut) + EVT_MENU(wxID_ZOOM_FIT, MainFrame::OnZoomFit) + EVT_MENU(wxID_ZOOM_100, MainFrame::OnZoom100) + EVT_MENU(buttonID_CROP_CURR_PAGE, MainFrame::OnCropCurrPage) + EVT_MENU(buttonID_CROP_ALL_PAGE, MainFrame::OnCropAllPage) + EVT_MENU(wxID_CLOSE, MainFrame::OnClose) + EVT_MENU(wxID_EXIT, MainFrame::OnExit) + EVT_MENU(wxID_ABOUT, MainFrame::OnAbout) + EVT_CLOSE(MainFrame::OnExit) + EVT_LISTBOX(panelID_PAGE_LIST, MainFrame::OnClickListBox) + EVT_SLIDER(sliderID_cfg_PIX_FILTER, MainFrame::OnChnageCfgFilerPixSize) + EVT_SLIDER(sliderID_cfg_BORDER, MainFrame::OnChangeCfgBorder) + + // Update ui + EVT_UPDATE_UI(wxID_UNDO, MainFrame::OnUpdateUndo) + EVT_UPDATE_UI(wxID_REDO, MainFrame::OnUpdateRedo) + EVT_UPDATE_UI(wxID_SAVE, MainFrame::OnUpdateSave) + +wxEND_EVENT_TABLE(); diff --git a/src/ui/MainFrame.hpp b/src/ui/MainFrame.hpp new file mode 100644 index 0000000..f4c0e40 --- /dev/null +++ b/src/ui/MainFrame.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "core/Document.hpp" +#include "core/Page.hpp" +#include "ui/Canvas.hpp" +#include "ui/ConfigPanel.hpp" +#include "ui/MenuBar.hpp" +#include "ui/ToolBar.hpp" + +namespace croplines { +class MainFrame final : public wxFrame { + public: + MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, + const wxSize& size); + ~MainFrame() override { m_mgr.UnInit(); } + + private: + wxAuiManager m_mgr; + + MenuBar* m_menuBar = new MenuBar{}; + ToolBar* m_toolBar; + wxStatusBar* m_statusBar; + Canvas* m_canvas; + wxListBox* m_pageListPanel; + ConfigPanel* m_configPanel; + + std::size_t m_currentPageIdx = 0; + Document m_doc; + std::optional m_currentPage; + + bool Load(const std::filesystem::path& path); + bool Save(); + bool Close(); + + [[nodiscard]] std::size_t CurrentPage() const { return m_currentPageIdx; } + void CurrentPage(std::size_t); + void PrevPage() { CurrentPage(CurrentPage() - 1); } + void NextPage() { CurrentPage(CurrentPage() + 1); } + + void SyncUIPageListPanel(); + void SyncUIConfigPanel(); + + void OnPrevPage(wxCommandEvent&) { PrevPage(); } + void OnNextPage(wxCommandEvent&) { NextPage(); } + void OnSave(wxCommandEvent&) { Save(); } + void OnLoad(wxCommandEvent&); + void OnUndo(wxCommandEvent&); + void OnRedo(wxCommandEvent&); + void OnZoomIn(wxCommandEvent&) { m_canvas->ZoomIn(); } + void OnZoomOut(wxCommandEvent&) { m_canvas->ZoomOut(); } + void OnZoomFit(wxCommandEvent&) { m_canvas->ZoomFit(); } + void OnZoom100(wxCommandEvent&) { m_canvas->Zoom(1.0); } + void OnCropCurrPage(wxCommandEvent&); + void OnCropAllPage(wxCommandEvent&); + void OnClose(wxCommandEvent&) { Close(); } + void OnExit(wxCloseEvent&); + void OnExit(wxCommandEvent&) { Close(); } + void OnAbout(wxCommandEvent&); + void OnClickListBox(wxCommandEvent&); + void OnChnageCfgFilerPixSize(wxCommandEvent& event); + void OnChangeCfgBorder(wxCommandEvent& event); + + // Update ui + void OnUpdateUndo(wxUpdateUIEvent&); + void OnUpdateRedo(wxUpdateUIEvent&); + void OnUpdateSave(wxUpdateUIEvent&); + + wxDECLARE_EVENT_TABLE(); +}; +} // namespace croplines diff --git a/src/ui/MenuBar.cpp b/src/ui/MenuBar.cpp new file mode 100644 index 0000000..dd48d45 --- /dev/null +++ b/src/ui/MenuBar.cpp @@ -0,0 +1,63 @@ +#include "ui/MenuBar.hpp" + +#include + +#include "ui/Defs.hpp" + +using namespace croplines; + +MenuBar::MenuBar() : wxMenuBar() { + m_menuFile->Append(wxID_OPEN, wxT("&Load\tCtrl+O")); + m_menuFile->Append(wxID_SAVE); + m_menuFile->AppendSeparator(); + m_menuFile->Append(buttonID_CROP_CURR_PAGE, wxT("Crop ¤t page"), + wxT("Crop current page to subimages and save each one to " + "output directory")); + m_menuFile->Append(buttonID_CROP_ALL_PAGE, wxT("Crop &all pages"), + wxT("Crop all pages to subimages and save each one to " + "output directory")); + m_menuFile->AppendSeparator(); + m_menuFile->Append(wxID_CLOSE); + m_menuFile->Append(wxID_EXIT); + + Append(m_menuFile, wxT("&File")); + + m_menuEdit->Append(wxID_UNDO); + m_menuEdit->Append(wxID_REDO); + m_menuEdit->AppendSeparator(); + m_menuEdit->Append(wxID_UP, wxT("Last page\tUp"), wxT("Move to last page")); + m_menuEdit->Append(wxID_DOWN, wxT("Next page\tDown"), wxT("Move to next page")); + + Append(m_menuEdit, wxT("&Edit")); + + m_menuView->Append(wxID_ZOOM_IN); + m_menuView->Append(wxID_ZOOM_OUT); + m_menuView->Append(wxID_ZOOM_FIT); + m_menuView->Append(wxID_ZOOM_100); + + Append(m_menuView, wxT("&View")); + + m_menuHelp->Append(wxID_ABOUT); + + Append(m_menuHelp, wxT("&Help")); +} + +bool MenuBar::Enable(bool enable) { + // Do not enable wxID_OPEN + static int needToEnable[] = { + wxID_SAVE, + wxID_CLOSE, + wxID_UP, + wxID_DOWN, + wxID_ZOOM_FIT, + wxID_ZOOM_OUT, + wxID_ZOOM_IN, + wxID_ZOOM_100, + buttonID_CROP_ALL_PAGE, + buttonID_CROP_CURR_PAGE, + }; + + for (int id : needToEnable) Enable(id, enable); + + return true; +} diff --git a/src/ui/MenuBar.hpp b/src/ui/MenuBar.hpp new file mode 100644 index 0000000..de8ec93 --- /dev/null +++ b/src/ui/MenuBar.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include // IWYU pragma: keep + +namespace croplines { +class MenuBar : public wxMenuBar { + public: + wxMenu* m_menuFile = new wxMenu{}; + wxMenu* m_menuEdit = new wxMenu{}; + wxMenu* m_menuView = new wxMenu{}; + wxMenu* m_menuHelp = new wxMenu{}; + + MenuBar(); + + using wxMenuBar::Enable; + bool Enable(bool enable = true) override; +}; +} // namespace croplines diff --git a/src/ui/ToolBar.cpp b/src/ui/ToolBar.cpp new file mode 100644 index 0000000..c8f402e --- /dev/null +++ b/src/ui/ToolBar.cpp @@ -0,0 +1,54 @@ +#include "ui/ToolBar.hpp" + +#include +#include + +#include "ui/Defs.hpp" + +using namespace croplines; + +ToolBar::ToolBar(wxWindow* parent, wxWindowID id) + : wxAuiToolBar(parent, id, wxDefaultPosition, wxDefaultSize, + wxAUI_TB_HORZ_LAYOUT | wxAUI_TB_OVERFLOW | wxAUI_TB_TEXT) { + SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + AddTool(wxID_UP, wxT("上一页"), wxBITMAP_PNG(IMG_PREV_PAGE), wxNullBitmap, wxITEM_NORMAL, + wxT("上一页"), wxT("上一页"), nullptr); + AddTool(wxID_DOWN, wxT("下一页"), wxBITMAP_PNG(IMG_NEXT_PAGE), wxNullBitmap, wxITEM_NORMAL, + wxT("下一页"), wxT("下一页"), nullptr); + + AddSeparator(); + + AddTool(wxID_SAVE, wxT("保存"), wxBITMAP_PNG(IMG_SAVE), wxNullBitmap, wxITEM_NORMAL, + wxT("保存"), wxT("保存"), nullptr); + AddTool(wxID_OPEN, wxT("载入"), wxBITMAP_PNG(IMG_LOAD), wxNullBitmap, wxITEM_NORMAL, + wxT("载入"), wxT("载入"), nullptr); + + AddSeparator(); + + AddTool(wxID_ZOOM_FIT, wxT("缩放合适大小"), wxBITMAP_PNG(IMG_ZOOM_PAGE), wxNullBitmap, + wxITEM_NORMAL, wxT("缩放合适大小"), wxT("缩放合适大小"), nullptr); + AddTool(buttonID_CROP_CURR_PAGE, wxT("裁剪当前页"), wxBITMAP_PNG(IMG_CROP_CURR_PAGE), + wxNullBitmap, wxITEM_NORMAL, wxT("裁剪当前页"), wxT("裁剪当前页"), nullptr); + AddTool(buttonID_CROP_ALL_PAGE, wxT("裁剪全部"), wxBITMAP_PNG(IMG_CROP_ALL_PAGE), wxNullBitmap, + wxITEM_NORMAL, wxT("裁剪全部"), wxT("裁剪全部"), nullptr); + + Realize(); +} + +bool ToolBar::Enable(bool state) { + // Do not enable wxID_OPEN + static int needToEnable[] = { + wxID_UP, + wxID_DOWN, + wxID_SAVE, + wxID_ZOOM_FIT, + buttonID_CROP_ALL_PAGE, + buttonID_CROP_CURR_PAGE, + }; + + for (int id : needToEnable) EnableTool(id, state); + + Refresh(); + return true; +} diff --git a/src/ui/ToolBar.hpp b/src/ui/ToolBar.hpp new file mode 100644 index 0000000..67d558c --- /dev/null +++ b/src/ui/ToolBar.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +namespace croplines { +class ToolBar final : public wxAuiToolBar { + public: + ToolBar(wxWindow* parent, wxWindowID id); + + bool Enable(bool state = true) override; +}; +} // namespace croplines diff --git a/src/ui/components/SliderWithSpin.cpp b/src/ui/components/SliderWithSpin.cpp new file mode 100644 index 0000000..c63e822 --- /dev/null +++ b/src/ui/components/SliderWithSpin.cpp @@ -0,0 +1,89 @@ +#include "ui/components/SliderWithSpin.hpp" + +#include + +using namespace croplines; + +constexpr int SLIDER_ID = 1100; +constexpr int SPIN_ID = 1101; + +SliderWithSpin::SliderWithSpin(wxWindow* parent, wxWindowID id, const wxString& label, int value, + int minValue, int maxValue, const wxPoint& pos, const wxSize& size) + : wxPanel(parent, id, pos, size) { + m_label = new wxStaticText(this, wxID_ANY, label); + m_slider = new wxSlider(this, SLIDER_ID, value, minValue, maxValue); + m_spin = new wxSpinCtrl(this, SPIN_ID, wxEmptyString, wxDefaultPosition, wxDefaultSize, + wxSP_ARROW_KEYS, minValue, maxValue, value); + + auto* bSizer = new wxBoxSizer(wxHORIZONTAL); + bSizer->Add(m_label, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + bSizer->Add(m_slider, 1, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5); + bSizer->Add(m_spin, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM | wxRIGHT, 5); + + SetSizer(bSizer); +} + +bool SliderWithSpin::Enable(bool enable) { + wxWindow* const items[] = {m_label, m_slider, m_spin}; + const bool prevEnable = IsEnabled(); + bool ret = wxPanel::Enable(enable); + if (!ret) { + return false; + } + for (const auto& item : items) { + ret = item->Enable(enable); + if (!ret) { + wxPanel::Enable(prevEnable); + for (const auto* it = items; it != &item; it++) { + (*it)->Enable(prevEnable); + } + return false; + } + } + return true; +} + +void SliderWithSpin::SetValue(int value) { + this->m_value = value; + m_spin->SetValue(value); + m_slider->SetValue(value); +} + +void SliderWithSpin::CallEvent(int value) { + wxCommandEvent evt(wxEVT_COMMAND_SLIDER_UPDATED, GetId()); + evt.SetEventObject(this); + evt.SetInt(value); + ProcessWindowEvent(evt); +} + +void SliderWithSpin::OnSliderChanging(wxCommandEvent&) { + const int value = m_slider->GetValue(); + if (m_spin->GetValue() != value) { + m_spin->SetValue(value); + } + this->m_value = value; +} + +void SliderWithSpin::OnSpinChanged(wxSpinEvent&) { + const int value = m_spin->GetValue(); + if (m_slider->GetValue() != value) { + m_slider->SetValue(value); + } + this->m_value = value; + CallEvent(value); +} + +void SliderWithSpin::OnSliderChanged(wxScrollEvent& event) { + const int value = m_slider->GetValue(); + CallEvent(value); + event.Skip(); +} + +IMPLEMENT_DYNAMIC_CLASS(SliderWithSpin, wxFrame); + +// clang-format off +wxBEGIN_EVENT_TABLE(SliderWithSpin, wxPanel) + EVT_SLIDER(SLIDER_ID, SliderWithSpin::OnSliderChanging) + EVT_SPINCTRL(SPIN_ID, SliderWithSpin::OnSpinChanged) + EVT_SCROLL_CHANGED(SliderWithSpin::OnSliderChanged) +wxEND_EVENT_TABLE(); diff --git a/src/ui/components/SliderWithSpin.hpp b/src/ui/components/SliderWithSpin.hpp new file mode 100644 index 0000000..65a5c4a --- /dev/null +++ b/src/ui/components/SliderWithSpin.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace croplines { +class SliderWithSpin : public wxPanel { + public: + SliderWithSpin() : wxPanel() {} + SliderWithSpin(wxWindow* parent, wxWindowID id, const wxString& label, int value, int minValue, + int maxValue, const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize); + + bool Enable(bool enable = false) override; + + [[nodiscard]] int GetValue() const { return m_value; } + void SetValue(int value); + + private: + wxStaticText* m_label = nullptr; + wxSlider* m_slider = nullptr; + wxSpinCtrl* m_spin = nullptr; + int m_value = 0; + + void CallEvent(int value); + void OnSliderChanging(wxCommandEvent&); + void OnSpinChanged(wxSpinEvent&); + void OnSliderScrollEnd(wxScrollEvent&); + void OnSliderChanged(wxScrollEvent&); + + wxDECLARE_EVENT_TABLE(); + wxDECLARE_DYNAMIC_CLASS_NO_COPY(SliderWithSpin); +}; +} // namespace croplines diff --git a/src/utils/Asserts.hpp b/src/utils/Asserts.hpp new file mode 100644 index 0000000..6d29f26 --- /dev/null +++ b/src/utils/Asserts.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#define ANNOTATE_LOCATION __FUNCTION__, __FILE__, __LINE__ + +#ifndef NDEBUG + +namespace assert_detail { +template +[[noreturn]] static inline void assert_throw(std::string_view func_name, std::string_view file_name, + size_t file_line, const Arg&... args) { + std::stringstream ss; + ((ss << args << ' '), ...); + ss << '(' << func_name << " @ " << file_name << ':' << file_line << ')'; + throw std::runtime_error(ss.str()); +} +} // namespace assert_detail + +#define ASSERT_WITH(x, ...) \ + do { \ + if (x) { \ + } else { \ + ::assert_detail::assert_throw(ANNOTATE_LOCATION, __VA_ARGS__); \ + } \ + } while (0); + +#define ASSERT(x) ASSERT_WITH(x, "Assert failed!") +#define UNREACHABLE() ASSERT_WITH(false, "Unreachable!"); + +#else + +#define UNREACHABLE() __builtin_unreachable() +#define ASSERT_WITH(x, ...) \ + do { \ + if (x) { \ + } else { \ + UNREACHABLE(); \ + } \ + } +#define ASSERT(x) ASSERT_WITH(x) + +#endif diff --git a/src/utils/Compare.cpp b/src/utils/Compare.cpp new file mode 100644 index 0000000..4b2c80b --- /dev/null +++ b/src/utils/Compare.cpp @@ -0,0 +1,32 @@ +#include "utils/Compare.hpp" + +#include +#include +#include +#include + +std::strong_ordering croplines::NaturalCompare(std::string_view a, std::string_view b) { + for (const char *i1 = a.begin(), *i2 = b.begin(); i1 != a.end() && i2 != b.end(); ++i1, ++i2) { + if (std::isdigit(*i1) && std::isdigit(*i2)) { + const char *ii1 = i1, *ii2 = i2; + while (ii1 != a.end() && *ii1 == '0') ++ii1; + while (ii2 != b.end() && *ii2 == '0') ++ii2; + auto zero_count1 = std::distance(i1, ii1); + auto zero_count2 = std::distance(i2, ii2); + + i1 = ii1; + i2 = ii2; + while (ii1 != a.end() && std::isdigit(*ii1)) ++ii1; + while (ii2 != b.end() && std::isdigit(*ii2)) ++ii2; + auto num1 = std::string_view(i1, ii1 - i1); + auto num2 = std::string_view(i2, ii2 - i2); + + if (auto cmp = num1.length() <=> num2.length(); cmp != 0) return cmp; + if (auto cmp = num1 <=> num2; cmp != 0) return cmp; + if (auto cmp = zero_count1 <=> zero_count2; cmp != 0) return cmp; + } else { + return std::toupper(*i1) <=> std::toupper(*i2); + } + } + return std::strong_ordering::equal; +} diff --git a/src/utils/Compare.hpp b/src/utils/Compare.hpp new file mode 100644 index 0000000..eab39eb --- /dev/null +++ b/src/utils/Compare.hpp @@ -0,0 +1,6 @@ +#include +#include + +namespace croplines { +std::strong_ordering NaturalCompare(std::string_view a, std::string_view b); +} diff --git a/src/wxUI.cpp b/src/wxUI.cpp deleted file mode 100644 index 9adfba8..0000000 --- a/src/wxUI.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "wxUI.h" - -#include - -using namespace Croplines; - -MainUI::MainUI(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, - const wxSize& size, long style) - : wxFrame(parent, id, title, pos, parent->FromDIP(size), style) { - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - m_mgr.SetManagedWindow(this); - m_mgr.SetFlags(wxAUI_MGR_DEFAULT); - - toolbar = new wxAuiToolBar(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, - wxAUI_TB_HORZ_LAYOUT | wxAUI_TB_OVERFLOW | wxAUI_TB_TEXT); - toolbar->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - - btn_prev_page = - toolbar->AddTool(wxID_UP, wxT("上一页"), wxBITMAP_PNG(IMG_PREV_PAGE), wxNullBitmap, - wxITEM_NORMAL, wxT("上一页"), wxT("上一页"), NULL); - - btn_next_page = - toolbar->AddTool(wxID_DOWN, wxT("下一页"), wxBITMAP_PNG(IMG_NEXT_PAGE), wxNullBitmap, - wxITEM_NORMAL, wxT("下一页"), wxT("下一页"), NULL); - - toolbar->AddSeparator(); - - btn_save = toolbar->AddTool(wxID_SAVE, wxT("保存"), wxBITMAP_PNG(IMG_SAVE), wxNullBitmap, - wxITEM_NORMAL, wxT("保存"), wxT("保存"), NULL); - - btn_load = toolbar->AddTool(wxID_OPEN, wxT("载入"), wxBITMAP_PNG(IMG_LOAD), wxNullBitmap, - wxITEM_NORMAL, wxT("载入"), wxT("载入"), NULL); - - toolbar->AddSeparator(); - - btn_zoom_page = toolbar->AddTool(wxID_ZOOM_FIT, wxT("缩放合适大小"), - wxBITMAP_PNG(IMG_ZOOM_PAGE), wxNullBitmap, wxITEM_NORMAL, - wxT("缩放合适大小"), wxT("缩放合适大小"), NULL); - - btn_crop_curr_page = - toolbar->AddTool(btnid_CROP_CURR_PAGE, wxT("裁剪当前页"), wxBITMAP_PNG(IMG_CROP_CURR_PAGE), - wxNullBitmap, wxITEM_NORMAL, wxT("裁剪当前页"), wxT("裁剪当前页"), NULL); - - btn_crop_all_page = - toolbar->AddTool(btnid_CROP_ALL_PAGE, wxT("裁剪全部"), wxBITMAP_PNG(IMG_CROP_ALL_PAGE), - wxNullBitmap, wxITEM_NORMAL, wxT("裁剪全部"), wxT("裁剪全部"), NULL); - - toolbar->Realize(); - m_mgr.AddPane(toolbar, wxAuiPaneInfo() - .Top() - .CaptionVisible(false) - .CloseButton(false) - .PaneBorder(false) - .Movable(false) - .Dock() - .Resizable() - .FloatingSize(wxSize(-1, -1)) - .DockFixed(true) - .LeftDockable(false) - .RightDockable(false) - .Floatable(false) - .Layer(10) - .ToolbarPane()); - - status_bar = this->CreateStatusBar(1, wxSTB_SIZEGRIP, wxID_ANY); - pn_page_list = new wxListBox(this, pnid_PAGE_LIST, wxDefaultPosition, wxDefaultSize, 0, NULL, - wxLB_NEEDED_SB); - m_mgr.AddPane(pn_page_list, wxAuiPaneInfo() - .Left() - .Caption(wxT("页面列表")) - .PinButton(true) - .Dock() - .Resizable() - .FloatingSize(wxDefaultSize) - .BottomDockable(false) - .TopDockable(false) - .BestSize(FromDIP(wxSize(200, 500))) - .MinSize(FromDIP(wxSize(130, -1)))); - - pn_canvas = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - m_mgr.AddPane(pn_canvas, wxAuiPaneInfo() - .Left() - .CaptionVisible(false) - .PinButton(true) - .Dock() - .Resizable() - .FloatingSize(wxDefaultSize) - .CentrePane()); - auto* bSizer31 = new wxBoxSizer(wxVERTICAL); - canvas = new Canvas(pn_canvas, wxID_ANY); - bSizer31->Add(canvas, 1, wxEXPAND, 5); - pn_canvas->SetSizer(bSizer31); - // pn_canvas->Layout(); - // bSizer31->Fit(pn_canvas); - - pn_config = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, wxT("设置")); - m_mgr.AddPane(pn_config, wxAuiPaneInfo() - .Right() - .Caption(wxT("设置")) - .PinButton(true) - .Dock() - .Resizable() - .FloatingSize(wxDefaultSize) - .BottomDockable(false) - .TopDockable(false) - .BestSize(FromDIP(wxSize(350, 500))) - .MinSize(FromDIP(wxSize(350, -1)))); - - ntbk_cfg_process = - new wxPanel(pn_config, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - wxBoxSizer* bSizer4; - bSizer4 = new wxBoxSizer(wxVERTICAL); - - sld_cfg_pix_filter = - new SliderWithSpin(ntbk_cfg_process, sldid_cfg_PIX_FILTER, wxT("忽略斑点直径"), 8, 0, 50); - bSizer4->Add(sld_cfg_pix_filter, 0, wxEXPAND, 5); - - ntbk_cfg_process->SetSizer(bSizer4); - ntbk_cfg_process->Layout(); - bSizer4->Fit(ntbk_cfg_process); - pn_config->AddPage(ntbk_cfg_process, wxT("处理"), false); - ntbk_cfg_output = - new wxPanel(pn_config, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - wxBoxSizer* bSizer41; - bSizer41 = new wxBoxSizer(wxVERTICAL); - - sld_cfg_border = - new SliderWithSpin(ntbk_cfg_output, sldid_cfg_BORDER, wxT("空白边距"), 10, 0, 100); - bSizer41->Add(sld_cfg_border, 0, wxEXPAND, 5); - - ntbk_cfg_output->SetSizer(bSizer41); - ntbk_cfg_output->Layout(); - bSizer41->Fit(ntbk_cfg_output); - pn_config->AddPage(ntbk_cfg_output, wxT(" 输出"), true); - - m_mgr.Update(); - this->Centre(wxBOTH); -} - -MainUI::~MainUI() { m_mgr.UnInit(); } diff --git a/src/wxUI.h b/src/wxUI.h deleted file mode 100644 index 7d6c84a..0000000 --- a/src/wxUI.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "canvas.h" -#include "ctrl.h" - -namespace Croplines { - -enum { - btnid_CROP_CURR_PAGE = 1000, - btnid_CROP_ALL_PAGE, - - pnid_PAGE_LIST, - - sldid_cfg_PIX_FILTER, - sldid_cfg_BORDER -}; -class MainUI : public wxFrame { - public: - wxAuiToolBar* toolbar; - wxAuiToolBarItem* btn_prev_page; - wxAuiToolBarItem* btn_next_page; - wxAuiToolBarItem* btn_save; - wxAuiToolBarItem* btn_load; - wxAuiToolBarItem* btn_zoom_page; - wxAuiToolBarItem* btn_crop_curr_page; - wxAuiToolBarItem* btn_crop_all_page; - wxStatusBar* status_bar; - wxListBox* pn_page_list; - wxPanel* pn_canvas; - Canvas* canvas; - wxNotebook* pn_config; - wxPanel* ntbk_cfg_process; - SliderWithSpin* sld_cfg_pix_filter; - wxPanel* ntbk_cfg_output; - SliderWithSpin* sld_cfg_border; - - MainUI(wxWindow* parent, wxWindowID id, const wxString& title, - const wxPoint& pos, const wxSize& size, long style); - wxAuiManager m_mgr; - - virtual ~MainUI(); -}; -} // namespace Croplines \ No newline at end of file